From fde01e094e34cb76e3b8aa9e1b613204c8d26d53 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Wed, 27 Jan 2016 17:47:36 +0000 Subject: [PATCH 0001/1237] integrate touch screen camera manipulation controls Set Avatar _driveKeys via touch manipulation --- interface/src/Application.cpp | 52 +++++++++++++++++++++++++++++++++-- interface/src/Application.h | 2 ++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c202331041..496c1c4278 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2343,6 +2343,7 @@ void Application::touchBeginEvent(QTouchEvent* event) { TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts + _currentTouchEvent = thisEvent; // and we reset our current event to this event before we call our update _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update touchUpdateEvent(event); @@ -2361,7 +2362,9 @@ void Application::touchEndEvent(QTouchEvent* event) { _altPressed = false; TouchEvent thisEvent(*event, _lastTouchEvent); _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts + _currentTouchEvent = TouchEvent(); _lastTouchEvent = thisEvent; + _lastTouchTimeout = 30; // timeout used as gestures can be misinterpreted for some frames // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isTouchCaptured()) { @@ -3154,12 +3157,55 @@ void Application::update(float deltaTime) { myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + if (_currentTouchEvent.isPressed == false) { + if (_lastTouchTimeout > 0) { + --_lastTouchTimeout; // disable non-touch input for some frames to disallow interpretation as movement + } else { + myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + } + } else { + const bool allowTouchPan = _lastTouchEvent.isPinching == false && _lastTouchEvent.isPinchOpening == false + && _lastTouchEvent.x - _currentTouchEvent.x != 0; + if (_lastTouchEvent.x > _currentTouchEvent.x && allowTouchPan) { + myAvatar->setDriveKeys(YAW, -1.0f); + } else if (_lastTouchEvent.x < _currentTouchEvent.x && allowTouchPan) { + myAvatar->setDriveKeys(YAW, 1.0f); + } + } myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } - myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + + if (_currentTouchEvent.isPressed == true) { + static const float TOUCH_ZOOM_THRESHOLD = 5.0f; + const float boomLength = myAvatar->getBoomLength(); + QScreen* windowScreen = getWindow()->windowHandle()->screen(); + const float dpiScale = glm::clamp((float)(windowScreen->physicalDotsPerInchY() / 100.0f), 1.0f, 10.0f) * 15.0f; // at DPI 100 divide radius by 15 + + float scaledRadius = _lastTouchEvent.radius / dpiScale; + if (scaledRadius < TOUCH_ZOOM_THRESHOLD) { + const float extraRadiusScale = TOUCH_ZOOM_THRESHOLD / scaledRadius; + scaledRadius = _lastTouchEvent.radius / (dpiScale * extraRadiusScale); + } + + if (_lastTouchEvent.isPinching == true) { + const bool boomChangeValid = boomLength - scaledRadius < 2.0f; // restrict changes to small increments to negate large jumps + if (boomChangeValid && scaledRadius < boomLength && scaledRadius > MyAvatar::ZOOM_MIN) { + myAvatar->setBoomLength(scaledRadius); + } else if (scaledRadius <= MyAvatar::ZOOM_MIN) { + _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + myAvatar->setBoomLength(MyAvatar::ZOOM_MIN); + } + } else if (_lastTouchEvent.isPinchOpening == true) { + const bool boomChangeValid = scaledRadius - boomLength < 2.0f; // restrict changes to small increments to negate large jumps + if (boomChangeValid && scaledRadius > boomLength && scaledRadius < MyAvatar::ZOOM_MAX) { + myAvatar->setBoomLength(scaledRadius); + } + } + } else { + myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + } } controller::Pose leftHand = userInputMapper->getPoseState(controller::Action::LEFT_HAND); diff --git a/interface/src/Application.h b/interface/src/Application.h index d5b677302a..aed4308491 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -453,7 +453,9 @@ private: FileLogger* _logger; + TouchEvent _currentTouchEvent; TouchEvent _lastTouchEvent; + int _lastTouchTimeout; quint64 _lastNackTime; quint64 _lastSendDownstreamAudioStats; From 9d03f0eb66b71a186401340c3aad8842c9d0edc5 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Tue, 2 Feb 2016 16:24:01 +0000 Subject: [PATCH 0002/1237] Revert "integrate touch screen camera manipulation controls" This reverts commit 4855511512d6df09691d7d54d48a341512d92392. --- interface/src/Application.cpp | 52 ++--------------------------------- interface/src/Application.h | 2 -- 2 files changed, 3 insertions(+), 51 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 496c1c4278..c202331041 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2343,7 +2343,6 @@ void Application::touchBeginEvent(QTouchEvent* event) { TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts - _currentTouchEvent = thisEvent; // and we reset our current event to this event before we call our update _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update touchUpdateEvent(event); @@ -2362,9 +2361,7 @@ void Application::touchEndEvent(QTouchEvent* event) { _altPressed = false; TouchEvent thisEvent(*event, _lastTouchEvent); _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts - _currentTouchEvent = TouchEvent(); _lastTouchEvent = thisEvent; - _lastTouchTimeout = 30; // timeout used as gestures can be misinterpreted for some frames // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isTouchCaptured()) { @@ -3157,55 +3154,12 @@ void Application::update(float deltaTime) { myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - if (_currentTouchEvent.isPressed == false) { - if (_lastTouchTimeout > 0) { - --_lastTouchTimeout; // disable non-touch input for some frames to disallow interpretation as movement - } else { - myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - } - } else { - const bool allowTouchPan = _lastTouchEvent.isPinching == false && _lastTouchEvent.isPinchOpening == false - && _lastTouchEvent.x - _currentTouchEvent.x != 0; - if (_lastTouchEvent.x > _currentTouchEvent.x && allowTouchPan) { - myAvatar->setDriveKeys(YAW, -1.0f); - } else if (_lastTouchEvent.x < _currentTouchEvent.x && allowTouchPan) { - myAvatar->setDriveKeys(YAW, 1.0f); - } - } + myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } - - if (_currentTouchEvent.isPressed == true) { - static const float TOUCH_ZOOM_THRESHOLD = 5.0f; - const float boomLength = myAvatar->getBoomLength(); - QScreen* windowScreen = getWindow()->windowHandle()->screen(); - const float dpiScale = glm::clamp((float)(windowScreen->physicalDotsPerInchY() / 100.0f), 1.0f, 10.0f) * 15.0f; // at DPI 100 divide radius by 15 - - float scaledRadius = _lastTouchEvent.radius / dpiScale; - if (scaledRadius < TOUCH_ZOOM_THRESHOLD) { - const float extraRadiusScale = TOUCH_ZOOM_THRESHOLD / scaledRadius; - scaledRadius = _lastTouchEvent.radius / (dpiScale * extraRadiusScale); - } - - if (_lastTouchEvent.isPinching == true) { - const bool boomChangeValid = boomLength - scaledRadius < 2.0f; // restrict changes to small increments to negate large jumps - if (boomChangeValid && scaledRadius < boomLength && scaledRadius > MyAvatar::ZOOM_MIN) { - myAvatar->setBoomLength(scaledRadius); - } else if (scaledRadius <= MyAvatar::ZOOM_MIN) { - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - myAvatar->setBoomLength(MyAvatar::ZOOM_MIN); - } - } else if (_lastTouchEvent.isPinchOpening == true) { - const bool boomChangeValid = scaledRadius - boomLength < 2.0f; // restrict changes to small increments to negate large jumps - if (boomChangeValid && scaledRadius > boomLength && scaledRadius < MyAvatar::ZOOM_MAX) { - myAvatar->setBoomLength(scaledRadius); - } - } - } else { - myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); - } + myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); } controller::Pose leftHand = userInputMapper->getPoseState(controller::Action::LEFT_HAND); diff --git a/interface/src/Application.h b/interface/src/Application.h index aed4308491..d5b677302a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -453,9 +453,7 @@ private: FileLogger* _logger; - TouchEvent _currentTouchEvent; TouchEvent _lastTouchEvent; - int _lastTouchTimeout; quint64 _lastNackTime; quint64 _lastSendDownstreamAudioStats; From 087e2e7f66d57e61a1c7d394fe3dc9435096b0a4 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Tue, 2 Feb 2016 18:05:17 +0000 Subject: [PATCH 0003/1237] revise touchscreen camera control manipulation Touchscreen camera control is now via a touchscreen device. Input must be enabled with the menu option. Currently supports dragging and gesturing to control avatar camera. Gesturing is handled by integration of the Qt implementation. --- .../resources/controllers/touchscreen.json | 24 ++++ interface/src/Application.cpp | 24 +++- interface/src/Application.h | 3 + libraries/gl/src/gl/GLWidget.cpp | 2 + .../src/input-plugins/InputPlugin.cpp | 2 + .../src/input-plugins/KeyboardMouseDevice.cpp | 67 +++++----- .../src/input-plugins/KeyboardMouseDevice.h | 4 + .../src/input-plugins/TouchscreenDevice.cpp | 119 ++++++++++++++++++ .../src/input-plugins/TouchscreenDevice.h | 81 ++++++++++++ tests/controllers/src/main.cpp | 4 + 10 files changed, 299 insertions(+), 31 deletions(-) create mode 100644 interface/resources/controllers/touchscreen.json create mode 100644 libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp create mode 100644 libraries/input-plugins/src/input-plugins/TouchscreenDevice.h diff --git a/interface/resources/controllers/touchscreen.json b/interface/resources/controllers/touchscreen.json new file mode 100644 index 0000000000..041259bd36 --- /dev/null +++ b/interface/resources/controllers/touchscreen.json @@ -0,0 +1,24 @@ +{ + "name": "Touchscreen to Actions", + "channels": [ + + { "from": "Touchscreen.GesturePinchOut", "to": "Actions.BoomOut", "filters": [ { "type": "scale", "scale": 0.02 } ]}, + { "from": "Touchscreen.GesturePinchIn", "to": "Actions.BoomIn", "filters": [ { "type": "scale", "scale": 0.02 } ]}, + + { "from": { "makeAxis" : [ + [ "Touchscreen.DragLeft" ], + [ "Touchscreen.DragRight" ] + ] + }, + "to": "Actions.Yaw" + }, + + { "from": { "makeAxis" : [ + [ "Touchscreen.DragUp" ], + [ "Touchscreen.DragDown" ] + ] + }, + "to": "Actions.Pitch" + } + ] +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c202331041..40bef91ba5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -862,8 +862,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : userInputMapper->registerDevice(_applicationStateDevice); - // Setup the keyboardMouseDevice and the user input mapper with the default bindings + // Setup the _keyboardMouseDevice, _touchscreenDevice and the user input mapper with the default bindings userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); + userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); userInputMapper->loadDefaultMapping(userInputMapper->getStandardDeviceID()); // force the model the look at the correct directory (weird order of operations issue) @@ -1308,6 +1309,9 @@ void Application::initializeUi() { if (name == KeyboardMouseDevice::NAME) { _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); } + if (name == TouchscreenDevice::NAME) { + _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); + } } updateInputModes(); } @@ -1790,6 +1794,9 @@ bool Application::event(QEvent* event) { case QEvent::TouchUpdate: touchUpdateEvent(static_cast(event)); return true; + case QEvent::Gesture: + touchGestureEvent((QGestureEvent*)event); + return true; case QEvent::Wheel: wheelEvent(static_cast(event)); return true; @@ -2336,6 +2343,9 @@ void Application::touchUpdateEvent(QTouchEvent* event) { if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->touchUpdateEvent(event); } + if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + _touchscreenDevice->touchUpdateEvent(event); + } } void Application::touchBeginEvent(QTouchEvent* event) { @@ -2354,6 +2364,9 @@ void Application::touchBeginEvent(QTouchEvent* event) { if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->touchBeginEvent(event); } + if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + _touchscreenDevice->touchBeginEvent(event); + } } @@ -2371,10 +2384,19 @@ void Application::touchEndEvent(QTouchEvent* event) { if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->touchEndEvent(event); } + if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + _touchscreenDevice->touchEndEvent(event); + } // put any application specific touch behavior below here.. } +void Application::touchGestureEvent(QGestureEvent* event) { + if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + _touchscreenDevice->touchGestureEvent(event); + } +} + void Application::wheelEvent(QWheelEvent* event) { _altPressed = false; _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts diff --git a/interface/src/Application.h b/interface/src/Application.h index d5b677302a..26df5c8b0c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -376,6 +377,7 @@ private: void touchBeginEvent(QTouchEvent* event); void touchEndEvent(QTouchEvent* event); void touchUpdateEvent(QTouchEvent* event); + void touchGestureEvent(QGestureEvent* event); void wheelEvent(QWheelEvent* event); void dropEvent(QDropEvent* event); @@ -421,6 +423,7 @@ private: std::shared_ptr _applicationStateDevice; // Default ApplicationDevice reflecting the state of different properties of the session std::shared_ptr _keyboardMouseDevice; // Default input device, the good old keyboard mouse and maybe touchpad + std::shared_ptr _touchscreenDevice; // the good old touchscreen AvatarUpdate* _avatarUpdate {nullptr}; SimpleMovingAverage _avatarSimsPerSecond {10}; int _avatarSimsPerSecondReport {0}; diff --git a/libraries/gl/src/gl/GLWidget.cpp b/libraries/gl/src/gl/GLWidget.cpp index c67dec1e51..8618fad58c 100644 --- a/libraries/gl/src/gl/GLWidget.cpp +++ b/libraries/gl/src/gl/GLWidget.cpp @@ -43,6 +43,7 @@ int GLWidget::getDeviceHeight() const { void GLWidget::initializeGL() { setAttribute(Qt::WA_AcceptTouchEvents); + grabGesture(Qt::PinchGesture); setAcceptDrops(true); // Note, we *DO NOT* want Qt to automatically swap buffers for us. This results in the "ringing" bug mentioned in WL#19514 when we're throttling the framerate. setAutoBufferSwap(false); @@ -76,6 +77,7 @@ bool GLWidget::event(QEvent* event) { case QEvent::TouchBegin: case QEvent::TouchEnd: case QEvent::TouchUpdate: + case QEvent::Gesture: case QEvent::Wheel: case QEvent::DragEnter: case QEvent::Drop: diff --git a/libraries/input-plugins/src/input-plugins/InputPlugin.cpp b/libraries/input-plugins/src/input-plugins/InputPlugin.cpp index 4d59adb602..28c1d3e0e0 100644 --- a/libraries/input-plugins/src/input-plugins/InputPlugin.cpp +++ b/libraries/input-plugins/src/input-plugins/InputPlugin.cpp @@ -13,11 +13,13 @@ #include #include "KeyboardMouseDevice.h" +#include "TouchscreenDevice.h" // TODO migrate to a DLL model where plugins are discovered and loaded at runtime by the PluginManager class InputPluginList getInputPlugins() { InputPlugin* PLUGIN_POOL[] = { new KeyboardMouseDevice(), + new TouchscreenDevice(), nullptr }; diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index c3d2f7c51c..6457f923a7 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -19,6 +19,7 @@ #include const QString KeyboardMouseDevice::NAME = "Keyboard/Mouse"; +bool KeyboardMouseDevice::_enableMouse = true; void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); @@ -58,28 +59,32 @@ void KeyboardMouseDevice::keyReleaseEvent(QKeyEvent* event) { } void KeyboardMouseDevice::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { - auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); - auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); - if (!result.second) { - // key pressed again ? without catching the release event ? - } - _lastCursor = event->pos(); - _mousePressTime = usecTimestampNow(); - _mouseMoved = false; + if (_enableMouse) { + auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); + auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); + if (!result.second) { + // key pressed again ? without catching the release event ? + } + _lastCursor = event->pos(); + _mousePressTime = usecTimestampNow(); + _mouseMoved = false; - eraseMouseClicked(); + eraseMouseClicked(); + } } void KeyboardMouseDevice::mouseReleaseEvent(QMouseEvent* event, unsigned int deviceID) { - auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); - _inputDevice->_buttonPressedMap.erase(input.getChannel()); + if (_enableMouse) { + auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); + _inputDevice->_buttonPressedMap.erase(input.getChannel()); - // if we pressed and released at the same location within a small time window, then create a "_CLICKED" - // input for this button we might want to add some small tolerance to this so if you do a small drag it - // till counts as a clicked. - static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click - if (!_mouseMoved && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) { - _inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel()); + // if we pressed and released at the same location within a small time window, then create a "_CLICKED" + // input for this button we might want to add some small tolerance to this so if you do a small drag it + // still counts as a click. + static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click + if (!_mouseMoved && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) { + _inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel()); + } } } @@ -90,22 +95,24 @@ void KeyboardMouseDevice::eraseMouseClicked() { } void KeyboardMouseDevice::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { - QPoint currentPos = event->pos(); - QPoint currentMove = currentPos - _lastCursor; + if (_enableMouse) { + QPoint currentPos = event->pos(); + QPoint currentMove = currentPos - _lastCursor; - _inputDevice->_axisStateMap[MOUSE_AXIS_X_POS] = (currentMove.x() > 0 ? currentMove.x() : 0.0f); - _inputDevice->_axisStateMap[MOUSE_AXIS_X_NEG] = (currentMove.x() < 0 ? -currentMove.x() : 0.0f); - // Y mouse is inverted positive is pointing up the screen - _inputDevice->_axisStateMap[MOUSE_AXIS_Y_POS] = (currentMove.y() < 0 ? -currentMove.y() : 0.0f); - _inputDevice->_axisStateMap[MOUSE_AXIS_Y_NEG] = (currentMove.y() > 0 ? currentMove.y() : 0.0f); + _inputDevice->_axisStateMap[MOUSE_AXIS_X_POS] = (currentMove.x() > 0 ? currentMove.x() : 0.0f); + _inputDevice->_axisStateMap[MOUSE_AXIS_X_NEG] = (currentMove.x() < 0 ? -currentMove.x() : 0.0f); + // Y mouse is inverted positive is pointing up the screen + _inputDevice->_axisStateMap[MOUSE_AXIS_Y_POS] = (currentMove.y() < 0 ? -currentMove.y() : 0.0f); + _inputDevice->_axisStateMap[MOUSE_AXIS_Y_NEG] = (currentMove.y() > 0 ? currentMove.y() : 0.0f); - // FIXME - this has the characteristic that it will show large jumps when you move the cursor - // outside of the application window, because we don't get MouseEvents when the cursor is outside - // of the application window. - _lastCursor = currentPos; - _mouseMoved = true; + // FIXME - this has the characteristic that it will show large jumps when you move the cursor + // outside of the application window, because we don't get MouseEvents when the cursor is outside + // of the application window. + _lastCursor = currentPos; + _mouseMoved = true; - eraseMouseClicked(); + eraseMouseClicked(); + } } void KeyboardMouseDevice::wheelEvent(QWheelEvent* event) { diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h index 55ca9a1704..3cbb2a9244 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h @@ -85,6 +85,8 @@ public: void touchUpdateEvent(const QTouchEvent* event); void wheelEvent(QWheelEvent* event); + + static void enableMouse(bool enableMouse) { _enableMouse = enableMouse; } static const QString NAME; @@ -123,6 +125,8 @@ protected: bool _isTouching = false; std::chrono::high_resolution_clock _clock; std::chrono::high_resolution_clock::time_point _lastTouchTime; + + static bool _enableMouse; }; #endif // hifi_KeyboardMouseDevice_h diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp new file mode 100644 index 0000000000..987ff2cac4 --- /dev/null +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -0,0 +1,119 @@ +// +// TouchscreenDevice.cpp +// input-plugins/src/input-plugins +// +// Created by Triplelexx on 01/31/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "TouchscreenDevice.h" +#include "KeyboardMouseDevice.h" + +#include +#include +#include +#include + +#include +#include +#include + +const QString TouchscreenDevice::NAME = "Touchscreen"; + +void TouchscreenDevice::pluginUpdate(float deltaTime, bool jointsCaptured) { + _inputDevice->update(deltaTime, jointsCaptured); + + // at DPI 100 use these arbitrary values to divide dragging distance + static const float DPI_SCALE_X = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0f), 1.0f, 10.0f) + * 600.0f; + static const float DPI_SCALE_Y = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchY() / 100.0f), 1.0f, 10.0f) + * 200.0f; + + float distanceScaleX, distanceScaleY; + if (_touchPointCount == 1) { + if (_firstTouchVec.x < _currentTouchVec.x) { + distanceScaleX = (_currentTouchVec.x - _firstTouchVec.x) / DPI_SCALE_X; + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_POS).getChannel()] = distanceScaleX; + } else if (_firstTouchVec.x > _currentTouchVec.x) { + distanceScaleX = (_firstTouchVec.x - _currentTouchVec.x) / DPI_SCALE_X; + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_NEG).getChannel()] = distanceScaleX; + } + // Y axis is inverted, positive is pointing up the screen + if (_firstTouchVec.y > _currentTouchVec.y) { + distanceScaleY = (_firstTouchVec.y - _currentTouchVec.y) / DPI_SCALE_Y; + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_POS).getChannel()] = distanceScaleY; + } else if (_firstTouchVec.y < _currentTouchVec.y) { + distanceScaleY = (_currentTouchVec.y - _firstTouchVec.y) / DPI_SCALE_Y; + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_NEG).getChannel()] = distanceScaleY; + } + } +} + +void TouchscreenDevice::InputDevice::update(float deltaTime, bool jointsCaptured) { + _axisStateMap.clear(); +} + +void TouchscreenDevice::InputDevice::focusOutEvent() { +} + +void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { + const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); + _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); + KeyboardMouseDevice::enableMouse(false); +} + +void TouchscreenDevice::touchEndEvent(const QTouchEvent* event) { + _touchPointCount = 0; + KeyboardMouseDevice::enableMouse(true); +} + +void TouchscreenDevice::touchUpdateEvent(const QTouchEvent* event) { + const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); + _currentTouchVec = glm::vec2(point.pos().x(), point.pos().y()); + _touchPointCount = event->touchPoints().count(); +} + +void TouchscreenDevice::touchGestureEvent(const QGestureEvent* event) { + // pinch gesture + if (QGesture* gesture = event->gesture(Qt::PinchGesture)) { + QPinchGesture* pinch = static_cast(gesture); + qreal scaleFactor = pinch->totalScaleFactor(); + if (scaleFactor > _lastPinchScale && scaleFactor != 0) { + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_POS).getChannel()] = 1.0f; + } else if (scaleFactor != 0) { + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_NEG).getChannel()] = 1.0f; + } + _lastPinchScale = scaleFactor; + } +} + +controller::Input TouchscreenDevice::InputDevice::makeInput(TouchscreenDevice::TouchAxisChannel axis) const { + return controller::Input(_deviceID, axis, controller::ChannelType::AXIS); +} + +controller::Input TouchscreenDevice::InputDevice::makeInput(TouchscreenDevice::TouchGestureAxisChannel gesture) const { + return controller::Input(_deviceID, gesture, controller::ChannelType::AXIS); +} + +controller::Input::NamedVector TouchscreenDevice::InputDevice::getAvailableInputs() const { + using namespace controller; + static QVector availableInputs; + static std::once_flag once; + std::call_once(once, [&] { + availableInputs.append(Input::NamedPair(makeInput(TOUCH_AXIS_X_POS), "DragRight")); + availableInputs.append(Input::NamedPair(makeInput(TOUCH_AXIS_X_NEG), "DragLeft")); + availableInputs.append(Input::NamedPair(makeInput(TOUCH_AXIS_Y_POS), "DragUp")); + availableInputs.append(Input::NamedPair(makeInput(TOUCH_AXIS_Y_NEG), "DragDown")); + + availableInputs.append(Input::NamedPair(makeInput(TOUCH_GESTURE_PINCH_POS), "GesturePinchOut")); + availableInputs.append(Input::NamedPair(makeInput(TOUCH_GESTURE_PINCH_NEG), "GesturePinchIn")); + }); + return availableInputs; +} + +QString TouchscreenDevice::InputDevice::getDefaultMappingConfig() const { + static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/touchscreen.json"; + return MAPPING_JSON; +} \ No newline at end of file diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h new file mode 100644 index 0000000000..c0d1ec0112 --- /dev/null +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -0,0 +1,81 @@ +// +// TouchscreenDevice.h +// input-plugins/src/input-plugins +// +// Created by Triplelexx on 1/31/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TouchscreenDevice_h +#define hifi_TouchscreenDevice_h + +#include +#include "InputPlugin.h" + +class QTouchEvent; +class QGestureEvent; + +class TouchscreenDevice : public InputPlugin { + Q_OBJECT +public: + + enum TouchAxisChannel { + TOUCH_AXIS_X_POS = 0, + TOUCH_AXIS_X_NEG, + TOUCH_AXIS_Y_POS, + TOUCH_AXIS_Y_NEG, + }; + + enum TouchGestureAxisChannel { + TOUCH_GESTURE_PINCH_POS = TOUCH_AXIS_Y_NEG + 1, + TOUCH_GESTURE_PINCH_NEG, + }; + + // Plugin functions + virtual bool isSupported() const override { return true; } + virtual bool isJointController() const override { return false; } + virtual const QString& getName() const override { return NAME; } + + virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } + virtual void pluginUpdate(float deltaTime, bool jointsCaptured) override; + + void touchBeginEvent(const QTouchEvent* event); + void touchEndEvent(const QTouchEvent* event); + void touchUpdateEvent(const QTouchEvent* event); + void touchGestureEvent(const QGestureEvent* event); + + static const QString NAME; + +protected: + + class InputDevice : public controller::InputDevice { + public: + InputDevice() : controller::InputDevice("Touchscreen") {} + private: + // Device functions + virtual controller::Input::NamedVector getAvailableInputs() const override; + virtual QString getDefaultMappingConfig() const override; + virtual void update(float deltaTime, bool jointsCaptured) override; + virtual void focusOutEvent() override; + + controller::Input makeInput(TouchAxisChannel axis) const; + controller::Input makeInput(TouchGestureAxisChannel gesture) const; + + friend class TouchscreenDevice; + }; + +public: + const std::shared_ptr& getInputDevice() const { return _inputDevice; } + +protected: + qreal _lastPinchScale; + glm::vec2 _firstTouchVec; + glm::vec2 _currentTouchVec; + int _touchPointCount; + std::shared_ptr _inputDevice { std::make_shared() }; +}; + +#endif // hifi_TouchscreenDevice_h diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index 13b6b51d82..41746be9d8 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -153,6 +154,9 @@ int main(int argc, char** argv) { if (name == KeyboardMouseDevice::NAME) { userInputMapper->registerDevice(std::dynamic_pointer_cast(inputPlugin)->getInputDevice()); } + if (name == TouchscreenDevice::NAME) { + userInputMapper->registerDevice(std::dynamic_pointer_cast(inputPlugin)->getInputDevice()); + } inputPlugin->pluginUpdate(0, calibrationData, false); } rootContext->setContextProperty("Controllers", new MyControllerScriptingInterface()); From 9db45c01cc9560bf480f3d6e5b6f49c13ac4e271 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Wed, 3 Feb 2016 00:13:45 +0000 Subject: [PATCH 0004/1237] indentation fixes caught some inconsistencies --- .../src/input-plugins/TouchscreenDevice.cpp | 27 +++++++++---------- .../src/input-plugins/TouchscreenDevice.h | 6 ++--- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 987ff2cac4..788a75dc76 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -76,17 +76,16 @@ void TouchscreenDevice::touchUpdateEvent(const QTouchEvent* event) { } void TouchscreenDevice::touchGestureEvent(const QGestureEvent* event) { - // pinch gesture - if (QGesture* gesture = event->gesture(Qt::PinchGesture)) { - QPinchGesture* pinch = static_cast(gesture); - qreal scaleFactor = pinch->totalScaleFactor(); - if (scaleFactor > _lastPinchScale && scaleFactor != 0) { - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_POS).getChannel()] = 1.0f; - } else if (scaleFactor != 0) { - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_NEG).getChannel()] = 1.0f; - } - _lastPinchScale = scaleFactor; - } + if (QGesture* gesture = event->gesture(Qt::PinchGesture)) { + QPinchGesture* pinch = static_cast(gesture); + qreal scaleFactor = pinch->totalScaleFactor(); + if (scaleFactor > _lastPinchScale && scaleFactor != 0) { + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_POS).getChannel()] = 1.0f; + } else if (scaleFactor != 0) { + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_NEG).getChannel()] = 1.0f; + } + _lastPinchScale = scaleFactor; + } } controller::Input TouchscreenDevice::InputDevice::makeInput(TouchscreenDevice::TouchAxisChannel axis) const { @@ -94,7 +93,7 @@ controller::Input TouchscreenDevice::InputDevice::makeInput(TouchscreenDevice::T } controller::Input TouchscreenDevice::InputDevice::makeInput(TouchscreenDevice::TouchGestureAxisChannel gesture) const { - return controller::Input(_deviceID, gesture, controller::ChannelType::AXIS); + return controller::Input(_deviceID, gesture, controller::ChannelType::AXIS); } controller::Input::NamedVector TouchscreenDevice::InputDevice::getAvailableInputs() const { @@ -107,8 +106,8 @@ controller::Input::NamedVector TouchscreenDevice::InputDevice::getAvailableInput availableInputs.append(Input::NamedPair(makeInput(TOUCH_AXIS_Y_POS), "DragUp")); availableInputs.append(Input::NamedPair(makeInput(TOUCH_AXIS_Y_NEG), "DragDown")); - availableInputs.append(Input::NamedPair(makeInput(TOUCH_GESTURE_PINCH_POS), "GesturePinchOut")); - availableInputs.append(Input::NamedPair(makeInput(TOUCH_GESTURE_PINCH_NEG), "GesturePinchIn")); + availableInputs.append(Input::NamedPair(makeInput(TOUCH_GESTURE_PINCH_POS), "GesturePinchOut")); + availableInputs.append(Input::NamedPair(makeInput(TOUCH_GESTURE_PINCH_NEG), "GesturePinchIn")); }); return availableInputs; } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index c0d1ec0112..8cc59c2532 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -31,7 +31,7 @@ public: enum TouchGestureAxisChannel { TOUCH_GESTURE_PINCH_POS = TOUCH_AXIS_Y_NEG + 1, - TOUCH_GESTURE_PINCH_NEG, + TOUCH_GESTURE_PINCH_NEG, }; // Plugin functions @@ -45,7 +45,7 @@ public: void touchBeginEvent(const QTouchEvent* event); void touchEndEvent(const QTouchEvent* event); void touchUpdateEvent(const QTouchEvent* event); - void touchGestureEvent(const QGestureEvent* event); + void touchGestureEvent(const QGestureEvent* event); static const QString NAME; @@ -62,7 +62,7 @@ protected: virtual void focusOutEvent() override; controller::Input makeInput(TouchAxisChannel axis) const; - controller::Input makeInput(TouchGestureAxisChannel gesture) const; + controller::Input makeInput(TouchGestureAxisChannel gesture) const; friend class TouchscreenDevice; }; From b291e32b7cd35ca73f2befeccbd1fd66f8840cac Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Fri, 19 Feb 2016 13:54:13 +0000 Subject: [PATCH 0005/1237] update TouchscreenDevice pass inputCalibrationData to update --- libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp | 2 +- libraries/input-plugins/src/input-plugins/TouchscreenDevice.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 788a75dc76..6abf2a9fae 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -51,7 +51,7 @@ void TouchscreenDevice::pluginUpdate(float deltaTime, bool jointsCaptured) { } } -void TouchscreenDevice::InputDevice::update(float deltaTime, bool jointsCaptured) { +void TouchscreenDevice::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { _axisStateMap.clear(); } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index 8cc59c2532..568dedf7b4 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -40,7 +40,7 @@ public: virtual const QString& getName() const override { return NAME; } virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } - virtual void pluginUpdate(float deltaTime, bool jointsCaptured) override; + virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; void touchBeginEvent(const QTouchEvent* event); void touchEndEvent(const QTouchEvent* event); From 742f741095712595b3e318f33a8d80a60781eaea Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Sun, 21 Feb 2016 21:27:21 +0000 Subject: [PATCH 0006/1237] update TouchscreenDevice again added passing of inputCalibrationData missed from last commit --- .../input-plugins/src/input-plugins/TouchscreenDevice.cpp | 4 ++-- libraries/input-plugins/src/input-plugins/TouchscreenDevice.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 6abf2a9fae..8fb1f6bd0a 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -22,8 +22,8 @@ const QString TouchscreenDevice::NAME = "Touchscreen"; -void TouchscreenDevice::pluginUpdate(float deltaTime, bool jointsCaptured) { - _inputDevice->update(deltaTime, jointsCaptured); +void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { + _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); // at DPI 100 use these arbitrary values to divide dragging distance static const float DPI_SCALE_X = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0f), 1.0f, 10.0f) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index 568dedf7b4..acd7d89333 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -58,7 +58,7 @@ protected: // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, bool jointsCaptured) override; + virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; virtual void focusOutEvent() override; controller::Input makeInput(TouchAxisChannel axis) const; From fb242be50fc986c61fabee75b3b91f13db1992f3 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Sun, 3 Apr 2016 22:59:48 +0100 Subject: [PATCH 0007/1237] resolved merge conflict got to stick to command line, this should have been added with add command at merge.... --- interface/src/Application.h | 73 ++++++++++--------- .../src/procedural/ProceduralSkybox.slv | 39 ---------- 2 files changed, 39 insertions(+), 73 deletions(-) delete mode 100644 libraries/procedural/src/procedural/ProceduralSkybox.slv diff --git a/interface/src/Application.h b/interface/src/Application.h index 96887e300d..6eaab7039c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -41,7 +41,6 @@ #include #include -#include "avatar/AvatarUpdate.h" #include "avatar/MyAvatar.h" #include "Bookmarks.h" #include "Camera.h" @@ -52,7 +51,6 @@ #include "render/Engine.h" #include "scripting/ControllerScriptingInterface.h" #include "scripting/DialogsManagerScriptingInterface.h" -#include "ui/ApplicationCompositor.h" #include "ui/ApplicationOverlay.h" #include "ui/AudioStatsDialog.h" #include "ui/BandwidthDialog.h" @@ -68,6 +66,7 @@ class GLCanvas; class FaceTracker; class MainWindow; class AssetUpload; +class CompositorHelper; namespace controller { class StateController; @@ -121,9 +120,12 @@ public: QSize getDeviceSize() const; bool hasFocus() const; + void showCursor(const QCursor& cursor); + bool isThrottleRendering() const; Camera* getCamera() { return &_myCamera; } + const Camera* getCamera() const { return &_myCamera; } // Represents the current view frustum of the avatar. ViewFrustum* getViewFrustum(); const ViewFrustum* getViewFrustum() const; @@ -148,8 +150,8 @@ public: ApplicationOverlay& getApplicationOverlay() { return _applicationOverlay; } const ApplicationOverlay& getApplicationOverlay() const { return _applicationOverlay; } - ApplicationCompositor& getApplicationCompositor() { return _compositor; } - const ApplicationCompositor& getApplicationCompositor() const { return _compositor; } + CompositorHelper& getApplicationCompositor() const; + Overlays& getOverlays() { return _overlays; } bool isForeground() const { return _isForeground; } @@ -210,31 +212,31 @@ public: render::EnginePointer getRenderEngine() override { return _renderEngine; } gpu::ContextPointer getGPUContext() const { return _gpuContext; } + virtual void pushPreRenderLambda(void* key, std::function func) override; + const QRect& getMirrorViewRect() const { return _mirrorViewRect; } void updateMyAvatarLookAtPosition(); - AvatarUpdate* getAvatarUpdater() { return _avatarUpdate; } float getAvatarSimrate(); void setAvatarSimrateSample(float sample); float getAverageSimsPerSecond(); - void fakeMouseEvent(QMouseEvent* event); - signals: void svoImportRequested(const QString& url); void checkBackgroundDownloads(); - void domainConnectionRefused(const QString& reason); void fullAvatarURLChanged(const QString& newValue, const QString& modelName); void beforeAboutToQuit(); void activeDisplayPluginChanged(); + void uploadRequest(QString path); + public slots: QVector pasteEntities(float x, float y, float z); - bool exportEntities(const QString& filename, const QVector& entityIDs); + bool exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset = nullptr); bool exportEntities(const QString& filename, float x, float y, float z, float scale); bool importEntities(const QString& url); @@ -243,6 +245,7 @@ public slots: Q_INVOKABLE void loadScriptURLDialog(); void toggleLogDialog(); void toggleRunningScriptsWidget(); + void toggleAssetServerWidget(QString filePath = ""); void handleLocalServerConnection(); void readArgumentsFromLocalSocket(); @@ -251,11 +254,6 @@ public slots: void openUrl(const QUrl& url); - void setAvatarUpdateThreading(); - void setAvatarUpdateThreading(bool isThreaded); - void setRawAvatarUpdateThreading(); - void setRawAvatarUpdateThreading(bool isThreaded); - void resetSensors(bool andReload = false); void setActiveFaceTracker(); @@ -271,40 +269,41 @@ public slots: void cycleCamera(); void cameraMenuChanged(); + void toggleOverlays(); + void setOverlaysVisible(bool visible); void reloadResourceCaches(); + void updateHeartbeat(); + void crashApplication(); + void deadlockApplication(); void rotationModeChanged(); void runTests(); private slots: + void showDesktop(); void clearDomainOctreeDetails(); void idle(uint64_t now); void aboutToQuit(); - void connectedToDomain(const QString& hostname); + void resettingDomain(); void audioMuteToggled(); void faceTrackerMuteToggled(); void activeChanged(Qt::ApplicationState state); - void domainSettingsReceived(const QJsonObject& domainSettingsObject); - void handleDomainConnectionDeniedPacket(QSharedPointer message); - void notifyPacketVersionMismatch(); void loadSettings(); void saveSettings(); - + bool acceptSnapshot(const QString& urlString); bool askToSetAvatarUrl(const QString& url); bool askToLoadScript(const QString& scriptFilenameOrURL); - bool askToUploadAsset(const QString& asset); - void modelUploadFinished(AssetUpload* upload, const QString& hash); bool askToWearAvatarAttachmentUrl(const QString& url); void displayAvatarAttachmentWarning(const QString& message) const; @@ -314,6 +313,7 @@ private slots: void domainChanged(const QString& domainHostname); void updateWindowTitle(); void nodeAdded(SharedNodePointer node); + void nodeActivated(SharedNodePointer node); void nodeKilled(SharedNodePointer node); void packetSent(quint64 length); void updateDisplayMode(); @@ -325,12 +325,8 @@ private: void cleanupBeforeQuit(); - void emptyLocalCache(); - void update(float deltaTime); - void setPalmData(Hand* hand, const controller::Pose& pose, float deltaTime, HandData::Hand whichHand, float triggerValue); - // Various helper functions called during update() void updateLOD(); void updateThreads(float deltaTime); @@ -352,7 +348,6 @@ private: void checkSkeleton(); void initializeAcceptedFiles(); - int getRenderAmbientLight() const; void displaySide(RenderArgs* renderArgs, Camera& whichCamera, bool selfAvatarOnly = false); @@ -386,16 +381,18 @@ private: void maybeToggleMenuVisible(QMouseEvent* event); - bool _dependencyManagerIsSetup; + MainWindow* _window; + QElapsedTimer& _sessionRunTimer; + + bool _previousSessionCrashed; OffscreenGLCanvas* _offscreenContext { nullptr }; DisplayPluginPointer _displayPlugin; + std::mutex _displayPluginLock; InputPluginList _activeInputPlugins; bool _activatingDisplayPlugin { false }; - QMap _lockedFramebufferMap; - - MainWindow* _window; + QMap _lockedFramebufferMap; QUndoStack _undoStack; UndoStackScriptingInterface _undoStackScriptingInterface; @@ -425,7 +422,6 @@ private: std::shared_ptr _applicationStateDevice; // Default ApplicationDevice reflecting the state of different properties of the session std::shared_ptr _keyboardMouseDevice; // Default input device, the good old keyboard mouse and maybe touchpad std::shared_ptr _touchscreenDevice; // the good old touchscreen - AvatarUpdate* _avatarUpdate {nullptr}; SimpleMovingAverage _avatarSimsPerSecond {10}; int _avatarSimsPerSecondReport {0}; quint64 _lastAvatarSimsPerSecondUpdate {0}; @@ -476,7 +472,6 @@ private: typedef bool (Application::* AcceptURLMethod)(const QString &); static const QHash _acceptedExtensions; - QList _domainConnectionRefusals; glm::uvec2 _renderResolution; int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS; @@ -489,7 +484,6 @@ private: Overlays _overlays; ApplicationOverlay _applicationOverlay; - ApplicationCompositor _compositor; OverlayConductor _overlayConductor; DialogsManagerScriptingInterface* _dialogsManagerScriptingInterface = new DialogsManagerScriptingInterface(); @@ -512,10 +506,21 @@ private: int _avatarAttachmentRequest = 0; bool _settingsLoaded { false }; - bool _pendingPaint { false }; QTimer* _idleTimer { nullptr }; bool _fakedMouseEvent { false }; + + void checkChangeCursor(); + mutable QMutex _changeCursorLock { QMutex::Recursive }; + QCursor _desiredCursor{ Qt::BlankCursor }; + bool _cursorNeedsChanging { false }; + + QThread* _deadlockWatchdogThread; + + std::map> _preRenderLambdas; + std::mutex _preRenderLambdasLock; + + std::atomic _processOctreeStatsCounter { 0 }; }; #endif // hifi_Application_h diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.slv b/libraries/procedural/src/procedural/ProceduralSkybox.slv deleted file mode 100644 index 810afb1033..0000000000 --- a/libraries/procedural/src/procedural/ProceduralSkybox.slv +++ /dev/null @@ -1,39 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// skybox.vert -// vertex shader -// -// Created by Sam Gateau on 5/5/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/Transform.slh@> - -<$declareStandardTransform()$> - -out vec3 _normal; - -void main(void) { - const float depth = 0.0; - const vec4 UNIT_QUAD[4] = vec4[4]( - vec4(-1.0, -1.0, depth, 1.0), - vec4(1.0, -1.0, depth, 1.0), - vec4(-1.0, 1.0, depth, 1.0), - vec4(1.0, 1.0, depth, 1.0) - ); - vec4 inPosition = UNIT_QUAD[gl_VertexID]; - - // standard transform - TransformCamera cam = getTransformCamera(); - vec3 clipDir = vec3(inPosition.xy, 0.0); - vec3 eyeDir; - <$transformClipToEyeDir(cam, clipDir, eyeDir)$> - <$transformEyeToWorldDir(cam, eyeDir, _normal)$> - - // Position is supposed to come in clip space - gl_Position = vec4(inPosition.xy, 0.0, 1.0); -} \ No newline at end of file From bc0ea315343c564d8978671f8c0db660df42e3bb Mon Sep 17 00:00:00 2001 From: Lexx Date: Mon, 4 Apr 2016 00:32:05 +0100 Subject: [PATCH 0008/1237] Rename Skybox.slf to skybox.slf --- libraries/model/src/model/{Skybox.slf => skybox.slf} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename libraries/model/src/model/{Skybox.slf => skybox.slf} (100%) diff --git a/libraries/model/src/model/Skybox.slf b/libraries/model/src/model/skybox.slf similarity index 100% rename from libraries/model/src/model/Skybox.slf rename to libraries/model/src/model/skybox.slf From 0b313afd74757d195eceeb60352ed17e2ab7fcd8 Mon Sep 17 00:00:00 2001 From: Lexx Date: Mon, 4 Apr 2016 00:32:20 +0100 Subject: [PATCH 0009/1237] Rename Skybox.slv to skybox.slv --- libraries/model/src/model/{Skybox.slv => skybox.slv} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename libraries/model/src/model/{Skybox.slv => skybox.slv} (99%) diff --git a/libraries/model/src/model/Skybox.slv b/libraries/model/src/model/skybox.slv similarity index 99% rename from libraries/model/src/model/Skybox.slv rename to libraries/model/src/model/skybox.slv index 810afb1033..5df1aa0a4a 100755 --- a/libraries/model/src/model/Skybox.slv +++ b/libraries/model/src/model/skybox.slv @@ -36,4 +36,4 @@ void main(void) { // Position is supposed to come in clip space gl_Position = vec4(inPosition.xy, 0.0, 1.0); -} \ No newline at end of file +} From b5d50e8b3f21824a3c11b85e98d2093f72ce74b3 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Tue, 5 Apr 2016 00:35:12 +0100 Subject: [PATCH 0010/1237] attempt at resolving build warnings make both values operated on double before casting --- libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 8fb1f6bd0a..be42957eb8 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -26,9 +26,7 @@ void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCal _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); // at DPI 100 use these arbitrary values to divide dragging distance - static const float DPI_SCALE_X = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0f), 1.0f, 10.0f) * 600.0f; - static const float DPI_SCALE_Y = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchY() / 100.0f), 1.0f, 10.0f) * 200.0f; float distanceScaleX, distanceScaleY; From 3501749896d161c3efc3a490e77dca7ac2baf119 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Tue, 5 Apr 2016 00:37:14 +0100 Subject: [PATCH 0011/1237] attempt at resolving build warning make both values operated on double before casting --- libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index be42957eb8..f8da94e5e6 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -26,7 +26,9 @@ void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCal _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); // at DPI 100 use these arbitrary values to divide dragging distance + static const float DPI_SCALE_X = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0), 1.0f, 10.0f) * 600.0f; + static const float DPI_SCALE_Y = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchY() / 100.0), 1.0f, 10.0f) * 200.0f; float distanceScaleX, distanceScaleY; From 0fe4e42511cbb67464131279ba6c44df3e209601 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 1 May 2016 13:53:44 -0700 Subject: [PATCH 0012/1237] added zone properties to allow flying/ghosting or not --- .../entities/src/EntityItemProperties.cpp | 27 +++++++++++++++++++ libraries/entities/src/EntityItemProperties.h | 6 +++++ libraries/entities/src/EntityPropertyFlags.h | 3 +++ libraries/entities/src/ZoneEntityItem.cpp | 24 ++++++++++++++--- libraries/entities/src/ZoneEntityItem.h | 16 ++++++++--- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + scripts/system/html/entityProperties.html | 20 +++++++++++++- 8 files changed, 91 insertions(+), 8 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 92849d6e2f..feacb4ab98 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -309,6 +309,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube); CHECK_PROPERTY_CHANGE(PROP_LOCAL_POSITION, localPosition); CHECK_PROPERTY_CHANGE(PROP_LOCAL_ROTATION, localRotation); + CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed); + CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed); changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); @@ -467,6 +469,9 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool _stage.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); _skybox.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FLYING_ALLOWED, flyingAllowed); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GHOSTING_ALLOWED, ghostingAllowed); } // Web only @@ -679,6 +684,9 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); + COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed); + COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed); + _lastEdited = usecTimestampNow(); } @@ -849,6 +857,9 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_HOUR, Stage, stage, Hour, hour); ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_AUTOMATIC_HOURDAY, Stage, stage, AutomaticHourDay, automaticHourDay); + ADD_PROPERTY_TO_MAP(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool); + ADD_PROPERTY_TO_MAP(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool); + // FIXME - these are not yet handled //ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64); @@ -1089,6 +1100,9 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem _staticSkybox.setProperties(properties); _staticSkybox.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); + + APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, properties.getFlyingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, properties.getGhostingAllowed()); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1373,6 +1387,9 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode); properties.getSkybox().decodeFromEditPacket(propertyFlags, dataAt , processedBytes); + + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1564,6 +1581,9 @@ void EntityItemProperties::markAllChanged() { _jointTranslationsChanged = true; _queryAACubeChanged = true; + + _flyingAllowedChanged = true; + _ghostingAllowedChanged = true; } // The minimum bounding box for the entity. @@ -1885,6 +1905,13 @@ QList EntityItemProperties::listChangedProperties() { out += "queryAACube"; } + if (flyingAllowedChanged()) { + out += "flyingAllowed"; + } + if (ghostingAllowedChanged()) { + out += "ghostingAllowed"; + } + getAnimation().listChangedProperties(out); getKeyLight().listChangedProperties(out); getSkybox().listChangedProperties(out); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 2cf31e5632..f32bc977e0 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -205,6 +205,9 @@ public: DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector, QVector()); DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector, QVector()); + DEFINE_PROPERTY(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool, ZoneEntityItem::DEFAULT_FLYING_ALLOWED); + DEFINE_PROPERTY(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool, ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED); + static QString getBackgroundModeString(BackgroundMode mode); @@ -421,6 +424,9 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslationsSet, jointTranslationsSet, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslations, jointTranslations, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, FlyingAllowed, flyingAllowed, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, ""); + properties.getAnimation().debugDump(); properties.getSkybox().debugDump(); properties.getStage().debugDump(); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 90a7c1e2f7..19b47da8e7 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -169,6 +169,9 @@ enum EntityPropertyList { PROP_FALLOFF_RADIUS, // for Light entity + PROP_FLYING_ALLOWED, // can avatars in a zone fly? + PROP_GHOSTING_ALLOWED, // can avatars in a zone turn off physics? + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 6aabef5cc0..a28b8210c2 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -23,8 +23,12 @@ bool ZoneEntityItem::_zonesArePickable = false; bool ZoneEntityItem::_drawZoneBoundaries = false; + const ShapeType ZoneEntityItem::DEFAULT_SHAPE_TYPE = SHAPE_TYPE_BOX; const QString ZoneEntityItem::DEFAULT_COMPOUND_SHAPE_URL = ""; +const bool ZoneEntityItem::DEFAULT_FLYING_ALLOWED = true; +const bool ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED = true; + EntityItemPointer ZoneEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity { new ZoneEntityItem(entityID) }; @@ -55,6 +59,9 @@ EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredPr _skyboxProperties.getProperties(properties); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed); + return properties; } @@ -70,6 +77,9 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(backgroundMode, setBackgroundMode); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(flyingAllowed, setFlyingAllowed); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed); + bool somethingChangedInSkybox = _skyboxProperties.setProperties(properties); somethingChanged = somethingChanged || somethingChangedInKeyLight || somethingChangedInStage || somethingChangedInSkybox; @@ -116,6 +126,9 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, bytesRead += bytesFromSkybox; dataAt += bytesFromSkybox; + READ_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); + READ_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); + return bytesRead; } @@ -125,13 +138,16 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += _keyLightProperties.getEntityProperties(params); - + requestedProperties += PROP_SHAPE_TYPE; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_BACKGROUND_MODE; requestedProperties += _stageProperties.getEntityProperties(params); requestedProperties += _skyboxProperties.getEntityProperties(params); - + + requestedProperties += PROP_FLYING_ALLOWED; + requestedProperties += PROP_GHOSTING_ALLOWED; + return requestedProperties; } @@ -155,10 +171,12 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); APPEND_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, (uint32_t)getBackgroundMode()); // could this be a uint16?? - + _skyboxProperties.appendSubclassData(packetData, params, modelTreeElementExtraEncodeData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); + APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, getFlyingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, getGhostingAllowed()); } void ZoneEntityItem::debugDump() const { diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 0326a0f441..56968aa9c9 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -70,6 +70,11 @@ public: const SkyboxPropertyGroup& getSkyboxProperties() const { return _skyboxProperties; } const StagePropertyGroup& getStageProperties() const { return _stageProperties; } + bool getFlyingAllowed() const { return _flyingAllowed; } + void setFlyingAllowed(bool value) { _flyingAllowed = value; } + bool getGhostingAllowed() const { return _ghostingAllowed; } + void setGhostingAllowed(bool value) { _ghostingAllowed = value; } + virtual bool supportsDetailedRayIntersection() const { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, @@ -80,18 +85,23 @@ public: static const ShapeType DEFAULT_SHAPE_TYPE; static const QString DEFAULT_COMPOUND_SHAPE_URL; - + static const bool DEFAULT_FLYING_ALLOWED; + static const bool DEFAULT_GHOSTING_ALLOWED; + protected: KeyLightPropertyGroup _keyLightProperties; - + ShapeType _shapeType = DEFAULT_SHAPE_TYPE; QString _compoundShapeURL; - + BackgroundMode _backgroundMode = BACKGROUND_MODE_INHERIT; StagePropertyGroup _stageProperties; SkyboxPropertyGroup _skyboxProperties; + bool _flyingAllowed { DEFAULT_FLYING_ALLOWED }; + bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED }; + static bool _drawZoneBoundaries; static bool _zonesArePickable; }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e4aab94090..6b917df87a 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -47,7 +47,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_LIGHT_HAS_FALLOFF_RADIUS; + return VERSION_ENTITIES_NO_FLY_ZONES; case PacketType::AvatarData: case PacketType::BulkAvatarData: return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index b98a87e439..42e5fff420 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -171,6 +171,7 @@ const PacketVersion VERSION_ENTITITES_HAVE_QUERY_BOX = 54; const PacketVersion VERSION_ENTITITES_HAVE_COLLISION_MASK = 55; const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; +const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index ae5684d6c8..35a78682f3 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -484,6 +484,9 @@ var elZoneSkyboxColorGreen = document.getElementById("property-zone-skybox-color-green"); var elZoneSkyboxColorBlue = document.getElementById("property-zone-skybox-color-blue"); var elZoneSkyboxURL = document.getElementById("property-zone-skybox-url"); + + var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); + var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); allSections.push(elPolyVoxSections); @@ -770,6 +773,9 @@ elZoneSkyboxColorGreen.value = properties.skybox.color.green; elZoneSkyboxColorBlue.value = properties.skybox.color.blue; elZoneSkyboxURL.value = properties.skybox.url; + + elZoneFlyingAllowed.checked = properties.flyingAllowed; + elZoneGhostingAllowed.checked = properties.ghostingAllowed; showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); } else if (properties.type == "PolyVox") { @@ -1076,7 +1082,10 @@ })); elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox','url')); - + + elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); + elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); @@ -1791,6 +1800,15 @@ +
+ + +
+
+ + +
+
M From ef85cc7803b0360f04ee0196b044a409096c9e23 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 1 May 2016 14:47:12 -0700 Subject: [PATCH 0013/1237] hook up zone flyingAllowed flag to character controller --- interface/src/avatar/MyAvatar.cpp | 6 ++++++ .../entities-renderer/src/EntityTreeRenderer.h | 2 ++ libraries/physics/src/CharacterController.cpp | 14 ++++++++++++++ libraries/physics/src/CharacterController.h | 5 +++++ 4 files changed, 27 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 614f7bd9fe..e039633961 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -426,7 +426,12 @@ void MyAvatar::simulate(float deltaTime) { EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { + bool flyingAllowed = true; entityTree->withWriteLock([&] { + std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); + if (zone) { + flyingAllowed = zone->getFlyingAllowed(); + } auto now = usecTimestampNow(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); MovingEntitiesOperator moveOperator(entityTree); @@ -454,6 +459,7 @@ void MyAvatar::simulate(float deltaTime) { entityTree->recurseTreeWithOperator(&moveOperator); } }); + _characterController.setFlyingAllowed(flyingAllowed); } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a6fc58e5f1..3eb3796c94 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -88,6 +88,8 @@ public: // For Scene.shouldRenderEntities QList& getEntitiesLastInScene() { return _entityIDsLastInScene; } + std::shared_ptr myAvatarZone() { return _bestZone; } + signals: void mousePressOnEntity(const RayToEntityIntersectionResult& intersection, const QMouseEvent* event); void mousePressOffEntity(const RayToEntityIntersectionResult& intersection, const QMouseEvent* event); diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index f685aee748..b9306b67b0 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -294,6 +294,10 @@ void CharacterController::setState(State desiredState, const char* reason) { #else void CharacterController::setState(State desiredState) { #endif + if (!_flyingAllowed && desiredState == State::Hover) { + desiredState = State::InAir; + } + if (desiredState != _state) { #ifdef DEBUG_STATE_CHANGE qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason; @@ -544,3 +548,13 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio avatarRigidBodyRotation = bulletToGLM(worldTrans.getRotation()); return true; } + +void CharacterController::setFlyingAllowed(bool value) { + if (_flyingAllowed != value) { + _flyingAllowed = value; + + if (!_flyingAllowed && _state == State::Hover) { + SET_STATE(State::InAir, "flying not allowed"); + } + } +} diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index d810e904a7..eac8379ad0 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -98,6 +98,9 @@ public: bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); + void setFlyingAllowed(bool value); + + protected: #ifdef DEBUG_STATE_CHANGE void setState(State state, const char* reason); @@ -147,6 +150,8 @@ protected: btRigidBody* _rigidBody { nullptr }; uint32_t _pendingFlags { 0 }; uint32_t _previousFlags { 0 }; + + bool _flyingAllowed { true }; }; #endif // hifi_CharacterControllerInterface_h From 4feb2944edaab41830588df38da2a12e788f401f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 1 May 2016 15:27:49 -0700 Subject: [PATCH 0014/1237] hook up zone's ghostingAllowed flag --- interface/src/avatar/MyAvatar.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e039633961..2513bb7849 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -427,10 +427,12 @@ void MyAvatar::simulate(float deltaTime) { EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { bool flyingAllowed = true; + bool ghostingAllowed = true; entityTree->withWriteLock([&] { std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); if (zone) { flyingAllowed = zone->getFlyingAllowed(); + ghostingAllowed = zone->getGhostingAllowed(); } auto now = usecTimestampNow(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); @@ -460,6 +462,9 @@ void MyAvatar::simulate(float deltaTime) { } }); _characterController.setFlyingAllowed(flyingAllowed); + if (!_characterController.isEnabled() && !ghostingAllowed) { + _characterController.setEnabled(true); + } } } @@ -1839,7 +1844,21 @@ void MyAvatar::updateMotionBehaviorFromMenu() { } else { _motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; } - _characterController.setEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController)); + + bool ghostingAllowed = true; + EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); + if (entityTreeRenderer) { + std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); + if (zone) { + ghostingAllowed = zone->getGhostingAllowed(); + } + } + bool checked = menu->isOptionChecked(MenuOption::EnableCharacterController); + if (!ghostingAllowed) { + checked = true; + } + + _characterController.setEnabled(checked); } void MyAvatar::clearDriveKeys() { From 0e6d9a1eecbbc82da30a5409f0c30912750ecbac Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 14:48:31 -0700 Subject: [PATCH 0015/1237] avatar mixer can relay "client-only" entities between interfaces -- the entity server wont know about them. --- interface/src/Application.cpp | 6 ++ interface/src/avatar/Avatar.cpp | 85 +++++++++++++++++ interface/src/avatar/Avatar.h | 1 + interface/src/avatar/MyAvatar.cpp | 28 +++++- libraries/avatars/src/AvatarData.cpp | 93 ++++++++++++++++++- libraries/avatars/src/AvatarData.h | 20 ++++ libraries/avatars/src/AvatarHashMap.cpp | 9 +- .../src/RenderableWebEntityItem.cpp | 5 + .../entities/src/EntityEditPacketSender.cpp | 45 ++++++++- .../entities/src/EntityEditPacketSender.h | 11 ++- libraries/entities/src/EntityItem.h | 14 ++- libraries/entities/src/EntityItemProperties.h | 9 ++ .../entities/src/EntityScriptingInterface.cpp | 17 ++-- .../entities/src/EntityScriptingInterface.h | 2 +- libraries/entities/src/EntityTree.cpp | 5 +- libraries/physics/src/EntityMotionState.cpp | 12 ++- 16 files changed, 336 insertions(+), 26 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4a829b3191..0b1154d983 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -796,6 +796,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); + _entityEditSender.setMyAvatar(getMyAvatar()); // For now we're going to set the PPS for outbound packets to be super high, this is // probably not the right long term solution. But for now, we're going to do this to @@ -1087,6 +1088,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Make sure we don't time out during slow operations at startup updateHeartbeat(); + OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); + EntityEditPacketSender* entityPacketSender = static_cast(packetSender); + entityPacketSender->setMyAvatar(getMyAvatar()); + connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); @@ -4186,6 +4191,7 @@ void Application::clearDomainOctreeDetails() { qCDebug(interfaceapp) << "Clearing domain octree details..."; resetPhysicsReadyInformation(); + getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities // reset our node to stats and node to jurisdiction maps... since these must be changing... _entityServerJurisdictions.withWriteLock([&] { diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 2cacb81ce4..0090dcedf6 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "Application.h" #include "Avatar.h" @@ -160,6 +161,89 @@ void Avatar::animateScaleChanges(float deltaTime) { } } +void Avatar::updateAvatarEntities() { + // - if queueEditEntityMessage sees clientOnly flag it does _myAvatar->updateAvatarEntity() + // - updateAvatarEntity saves the bytes and sets _avatarEntityDataLocallyEdited + // - MyAvatar::update notices _avatarEntityDataLocallyEdited and calls sendIdentityPacket + // - sendIdentityPacket sends the entity bytes to the server which relays them to other interfaces + // - AvatarHashMap::processAvatarIdentityPacket's on other interfaces call avatar->setAvatarEntityData() + // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true + // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... + + quint64 now = usecTimestampNow(); + + const static quint64 refreshTime = 3 * USECS_PER_SECOND; + if (!_avatarEntityDataChanged && now - _avatarEntityChangedTime < refreshTime) { + return; + } + + EntityTreeRenderer* treeRenderer = qApp->getEntities(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (!entityTree) { + return; + } + + bool success = true; + QScriptEngine scriptEngine; + entityTree->withWriteLock([&] { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + // see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties + // and either add or update the entity. + QByteArray jsonByteArray = avatarEntities.value(entityID); + QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(jsonByteArray); + if (!jsonProperties.isObject()) { + qDebug() << "got bad avatarEntity json"; + continue; + } + QVariant variantProperties = jsonProperties.toVariant(); + QVariantMap asMap = variantProperties.toMap(); + QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); + EntityItemProperties properties; + EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, properties); + properties.setClientOnly(true); + properties.setOwningAvatarID(getID()); + + if (properties.getParentID() == AVATAR_SELF_ID) { + properties.setParentID(getID()); + } + + EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + + if (entity) { + if (!entityTree->updateEntity(entityID, properties)) { + qDebug() << "AVATAR-ENTITES -- updateEntity failed: " << properties.getType(); + success = false; + } + } else { + entity = entityTree->addEntity(entityID, properties); + if (!entity) { + qDebug() << "AVATAR-ENTITES -- addEntity failed: " << properties.getType(); + success = false; + } + } + } + }); + + AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); + foreach (auto entityID, recentlyDettachedAvatarEntities) { + if (!_avatarEntityData.contains(entityID)) { + EntityItemPointer dettachedEntity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (dettachedEntity) { + // this will cause this interface to listen to data from the entity-server about this entity. + dettachedEntity->setClientOnly(false); + } + } + } + + if (success) { + setAvatarEntityDataChanged(false); + _avatarEntityChangedTime = now; + } +} + + + void Avatar::simulate(float deltaTime) { PerformanceTimer perfTimer("simulate"); @@ -228,6 +312,7 @@ void Avatar::simulate(float deltaTime) { simulateAttachments(deltaTime); updatePalms(); + updateAvatarEntities(); } bool Avatar::isLookingAtMe(AvatarSharedPointer avatar) const { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index cb35fbb5eb..ded5ee6433 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -64,6 +64,7 @@ public: typedef std::shared_ptr PayloadPointer; void init(); + void updateAvatarEntities(); void simulate(float deltaTime); virtual void simulateAttachments(float deltaTime); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index bad60643ec..f9daad923b 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -311,6 +311,10 @@ void MyAvatar::update(float deltaTime) { head->setAudioLoudness(audio->getLastInputLoudness()); head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); + if (_avatarEntityDataLocallyEdited) { + sendIdentityPacket(); + } + simulate(deltaTime); currentEnergy += energyChargeRate; @@ -443,7 +447,7 @@ void MyAvatar::simulate(float deltaTime) { EntityItemProperties properties = entity->getProperties(); properties.setQueryAACubeDirty(); properties.setLastEdited(now); - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entity->getID(), properties); + packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entity->getID(), properties); entity->setLastBroadcast(usecTimestampNow()); } } @@ -455,6 +459,8 @@ void MyAvatar::simulate(float deltaTime) { } }); } + + updateAvatarEntities(); } // thread-safe @@ -696,6 +702,16 @@ void MyAvatar::saveData() { } settings.endArray(); + settings.beginWriteArray("avatarEntityData"); + int avatarEntityIndex = 0; + for (auto entityID : _avatarEntityData.keys()) { + settings.setArrayIndex(avatarEntityIndex); + settings.setValue("id", entityID); + settings.setValue("properties", _avatarEntityData.value(entityID)); + avatarEntityIndex++; + } + settings.endArray(); + settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); settings.setValue("useSnapTurn", _useSnapTurn); @@ -807,6 +823,16 @@ void MyAvatar::loadData() { settings.endArray(); setAttachmentData(attachmentData); + int avatarEntityCount = settings.beginReadArray("avatarEntityData"); + for (int i = 0; i < avatarEntityCount; i++) { + settings.setArrayIndex(i); + QUuid entityID = settings.value("id").toUuid(); + QByteArray properties = settings.value("properties").toByteArray(); + updateAvatarEntity(entityID, properties); + } + settings.endArray(); + setAvatarEntityDataChanged(true); + setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index fd13f8c370..9a0fc1a835 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -962,8 +962,9 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { QUrl unusedModelURL; // legacy faceModel support QUrl skeletonModelURL; QVector attachmentData; + AvatarEntityMap avatarEntityData; QString displayName; - packetStream >> avatarUUID >> unusedModelURL >> skeletonModelURL >> attachmentData >> displayName; + packetStream >> avatarUUID >> unusedModelURL >> skeletonModelURL >> attachmentData >> displayName >> avatarEntityData; bool hasIdentityChanged = false; @@ -983,6 +984,11 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { hasIdentityChanged = true; } + if (avatarEntityData != _avatarEntityData) { + setAvatarEntityData(avatarEntityData); + hasIdentityChanged = true; + } + return hasIdentityChanged; } @@ -994,7 +1000,7 @@ QByteArray AvatarData::identityByteArray() { QUrl unusedModelURL; // legacy faceModel support - identityStream << QUuid() << unusedModelURL << urlToSend << _attachmentData << _displayName; + identityStream << QUuid() << unusedModelURL << urlToSend << _attachmentData << _displayName << _avatarEntityData; return identityData; } @@ -1202,6 +1208,8 @@ void AvatarData::sendIdentityPacket() { [&](const SharedNodePointer& node) { nodeList->sendPacketList(std::move(packetList), *node); }); + + _avatarEntityDataLocallyEdited = false; } void AvatarData::sendBillboardPacket() { @@ -1389,6 +1397,7 @@ static const QString JSON_AVATAR_HEAD_MODEL = QStringLiteral("headModel"); static const QString JSON_AVATAR_BODY_MODEL = QStringLiteral("bodyModel"); static const QString JSON_AVATAR_DISPLAY_NAME = QStringLiteral("displayName"); static const QString JSON_AVATAR_ATTACHEMENTS = QStringLiteral("attachments"); +static const QString JSON_AVATAR_ENTITIES = QStringLiteral("attachedEntities"); static const QString JSON_AVATAR_SCALE = QStringLiteral("scale"); QJsonValue toJsonValue(const JointData& joint) { @@ -1427,6 +1436,17 @@ QJsonObject AvatarData::toJson() const { root[JSON_AVATAR_ATTACHEMENTS] = attachmentsJson; } + if (!_avatarEntityData.empty()) { + QJsonArray avatarEntityJson; + for (auto entityID : _avatarEntityData.keys()) { + QVariantMap entityData; + entityData.insert("id", entityID); + entityData.insert("properties", _avatarEntityData.value(entityID)); + avatarEntityJson.push_back(QVariant(entityData).toJsonObject()); + } + root[JSON_AVATAR_ENTITIES] = avatarEntityJson; + } + auto recordingBasis = getRecordingBasis(); bool success; Transform avatarTransform = getTransform(success); @@ -1526,6 +1546,13 @@ void AvatarData::fromJson(const QJsonObject& json) { setAttachmentData(attachments); } + // if (json.contains(JSON_AVATAR_ENTITIES) && json[JSON_AVATAR_ENTITIES].isArray()) { + // QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHEMENTS].toArray(); + // for (auto attachmentJson : attachmentsJson) { + // // TODO -- something + // } + // } + // Joint rotations are relative to the avatar, so they require no basis correction if (json.contains(JSON_AVATAR_JOINT_ARRAY)) { QVector jointArray; @@ -1678,9 +1705,69 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) { QVector newAttachments; newAttachments.reserve(variant.size()); for (const auto& attachmentVar : variant) { - AttachmentData attachment; + AttachmentData attachment; attachment.fromVariant(attachmentVar); newAttachments.append(attachment); } setAttachmentData(newAttachments); } + +void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "updateAvatarEntity", Q_ARG(const QUuid&, entityID), Q_ARG(QByteArray, entityData)); + return; + } + _avatarEntityData.insert(entityID, entityData); + _avatarEntityDataLocallyEdited = true; +} + +void AvatarData::clearAvatarEntity(const QUuid& entityID) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "clearAvatarEntity", Q_ARG(const QUuid&, entityID)); + return; + } + _avatarEntityData.remove(entityID); + _avatarEntityDataLocallyEdited = true; +} + +AvatarEntityMap AvatarData::getAvatarEntityData() const { + if (QThread::currentThread() != thread()) { + AvatarEntityMap result; + QMetaObject::invokeMethod(const_cast(this), "getAvatarEntityData", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(AvatarEntityMap, result)); + return result; + } + return _avatarEntityData; +} + +void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setAvatarEntityData", Q_ARG(const AvatarEntityMap&, avatarEntityData)); + return; + } + if (_avatarEntityData != avatarEntityData) { + // keep track of entities that were attached to this avatar but no longer are + AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); + + _avatarEntityData = avatarEntityData; + setAvatarEntityDataChanged(true); + + foreach (auto entityID, previousAvatarEntityIDs) { + if (!_avatarEntityData.contains(entityID)) { + _avatarEntityDetached.insert(entityID); + } + } + } +} + +AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { + if (QThread::currentThread() != thread()) { + AvatarEntityIDs result; + QMetaObject::invokeMethod(const_cast(this), "getRecentlyDetachedIDs", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(AvatarEntityIDs, result)); + return result; + } + AvatarEntityIDs result = _avatarEntityDetached; + _avatarEntityDetached.clear(); + return result; +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 900da38ffa..72d34af9d9 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -61,6 +61,8 @@ typedef unsigned long long quint64; using AvatarSharedPointer = std::shared_ptr; using AvatarWeakPointer = std::weak_ptr; using AvatarHash = QHash; +using AvatarEntityMap = QMap; +using AvatarEntityIDs = QSet; using AvatarDataSequenceNumber = uint16_t; @@ -135,6 +137,10 @@ class AttachmentData; class Transform; using TransformPointer = std::shared_ptr; +// When writing out avatarEntities to a QByteArray, if the parentID is the ID of MyAvatar, use this ID instead. This allows +// the value to be reset when the sessionID changes. +const QUuid AVATAR_SELF_ID = QUuid("{00000000-0000-0000-0000-000000000001}"); + class AvatarData : public QObject, public SpatiallyNestable { Q_OBJECT @@ -274,6 +280,9 @@ public: Q_INVOKABLE QVariantList getAttachmentsVariant() const; Q_INVOKABLE void setAttachmentsVariant(const QVariantList& variant); + Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); + Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID); + void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } // key state @@ -333,6 +342,11 @@ public: glm::vec3 getClientGlobalPosition() { return _globalPosition; } + Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; + Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); + void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } + AvatarEntityIDs getAndClearRecentlyDetachedIDs(); + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); @@ -405,6 +419,12 @@ protected: // updates about one avatar to another. glm::vec3 _globalPosition; + AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar + AvatarEntityMap _avatarEntityData; + bool _avatarEntityDataLocallyEdited { false }; + bool _avatarEntityDataChanged { false }; + quint64 _avatarEntityChangedTime { 0 }; + private: friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar); static QUrl _defaultFullAvatarModelUrl; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 75fb5e6028..62e87ce285 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -111,17 +111,18 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer QDataStream identityStream(message->getMessage()); QUuid sessionUUID; - + while (!identityStream.atEnd()) { QUrl faceMeshURL, skeletonURL; QVector attachmentData; + AvatarEntityMap avatarEntityData; QString displayName; - identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName; + identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName >> avatarEntityData; // mesh URL for a UUID, find avatar in our list auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); - + if (avatar->getSkeletonModelURL().isEmpty() || (avatar->getSkeletonModelURL() != skeletonURL)) { avatar->setSkeletonModelURL(skeletonURL); // Will expand "" to default and so will not continuously fire } @@ -130,6 +131,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer avatar->setAttachmentData(attachmentData); } + avatar->setAvatarEntityData(avatarEntityData); + if (avatar->getDisplayName() != displayName) { avatar->setDisplayName(displayName); } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 26aecf6050..891e1dca3b 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -174,9 +174,14 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { + #if defined(Q_OS_LINUX) + // these don't seem to work on Linux + return; + #else if (!buildWebSurface(static_cast(args->_renderer))) { return; } + #endif } _lastRenderTime = usecTimestampNow(); diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 1e38c32964..ea86d3d542 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -10,6 +10,7 @@ // #include +#include #include #include #include @@ -35,18 +36,54 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte } } -void EntityEditPacketSender::queueEditEntityMessage(PacketType type, EntityItemID modelID, - const EntityItemProperties& properties) { +void EntityEditPacketSender::queueEditEntityMessage(PacketType type, + EntityTreePointer entityTree, + EntityItemID entityItemID, + const EntityItemProperties& properties) { if (!_shouldSend) { return; // bail early } + if (properties.getClientOnly()) { + // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server + assert(_myAvatar); + + if (!entityTree) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage null entityTree."; + return; + } + EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); + if (!entity) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; + return; + } + + // the properties that get serialized into the avatar identity packet should be the entire set + // rather than just the ones being edited. + entity->setProperties(properties); + EntityItemProperties entityProperties = entity->getProperties(); + + QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); + QVariant variantProperties = scriptProperties.toVariant(); + QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties); + + // the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar + QJsonObject jsonObject = jsonProperties.object(); + if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { + jsonObject["parentID"] = AVATAR_SELF_ID.toString(); + } + jsonProperties = QJsonDocument(jsonObject); + + QByteArray binaryProperties = jsonProperties.toBinaryData(); + _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + return; + } QByteArray bufferOut(NLPacket::maxPayloadSize(type), 0); - if (EntityItemProperties::encodeEntityEditPacket(type, modelID, properties, bufferOut)) { + if (EntityItemProperties::encodeEntityEditPacket(type, entityItemID, properties, bufferOut)) { #ifdef WANT_DEBUG qCDebug(entities) << "calling queueOctreeEditMessage()..."; - qCDebug(entities) << " id:" << modelID; + qCDebug(entities) << " id:" << entityItemID; qCDebug(entities) << " properties:" << properties; #endif queueOctreeEditMessage(type, bufferOut); diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 26e4dd83ff..90c6cb988d 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -15,6 +15,7 @@ #include #include "EntityItem.h" +#include "AvatarData.h" /// Utility for processing, packing, queueing and sending of outbound edit voxel messages. class EntityEditPacketSender : public OctreeEditPacketSender { @@ -22,11 +23,17 @@ class EntityEditPacketSender : public OctreeEditPacketSender { public: EntityEditPacketSender(); + void setMyAvatar(AvatarData* myAvatar) { _myAvatar = myAvatar; } + AvatarData* getMyAvatar() { return _myAvatar; } + void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); } + /// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines /// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in /// which case up to MaxPendingMessages will be buffered and processed when voxel servers are known. /// NOTE: EntityItemProperties assumes that all distances are in meter units - void queueEditEntityMessage(PacketType type, EntityItemID modelID, const EntityItemProperties& properties); + void queueEditEntityMessage(PacketType type, EntityTreePointer entityTree, + EntityItemID entityItemID, const EntityItemProperties& properties); + void queueEraseEntityMessage(const EntityItemID& entityItemID); @@ -40,5 +47,7 @@ public slots: private: bool _shouldProcessNack = true; + AvatarData* _myAvatar { nullptr }; + QScriptEngine _scriptEngine; }; #endif // hifi_EntityEditPacketSender_h diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index ecb9800e70..61f7fb0082 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -420,12 +420,19 @@ public: /// entity to definitively state if the preload signal should be sent. /// /// We only want to preload if: - /// there is some script, and either the script value or the scriptTimestamp + /// there is some script, and either the script value or the scriptTimestamp /// value have changed since our last preload - bool shouldPreloadScript() const { return !_script.isEmpty() && + bool shouldPreloadScript() const { return !_script.isEmpty() && ((_loadedScript != _script) || (_loadedScriptTimestamp != _scriptTimestamp)); } void scriptHasPreloaded() { _loadedScript = _script; _loadedScriptTimestamp = _scriptTimestamp; } + bool getClientOnly() const { return _clientOnly; } + void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } + // if this entity is client-only, which avatar is it associated with? + QUuid getOwningAvatarID() const { return _owningAvatarID; } + void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } + + protected: void setSimulated(bool simulated) { _simulated = simulated; } @@ -537,6 +544,9 @@ protected: mutable QHash _previouslyDeletedActions; QUuid _sourceUUID; /// the server node UUID we came from + + bool _clientOnly { false }; + QUuid _owningAvatarID; }; #endif // hifi_EntityItem_h diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 2cf31e5632..fb24e711f4 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -276,6 +276,12 @@ public: void setJointRotationsDirty() { _jointRotationsSetChanged = true; _jointRotationsChanged = true; } void setJointTranslationsDirty() { _jointTranslationsSetChanged = true; _jointTranslationsChanged = true; } + bool getClientOnly() const { return _clientOnly; } + void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } + // if this entity is client-only, which avatar is it associated with? + QUuid getOwningAvatarID() const { return _owningAvatarID; } + void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } + protected: QString getCollisionMaskAsString() const; void setCollisionMaskFromString(const QString& maskString); @@ -302,6 +308,9 @@ private: glm::vec3 _naturalPosition; EntityPropertyFlags _desiredProperties; // if set will narrow scopes of copy/to/from to just these properties + + bool _clientOnly { false }; + QUuid _owningAvatarID; }; Q_DECLARE_METATYPE(EntityItemProperties); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 093fa73ace..0869ac40da 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -36,7 +36,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership void EntityScriptingInterface::queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties) { - getEntityPacketSender()->queueEditEntityMessage(packetType, entityID, properties); + getEntityPacketSender()->queueEditEntityMessage(packetType, _entityTree, entityID, properties); } bool EntityScriptingInterface::canAdjustLocks() { @@ -123,9 +123,10 @@ EntityItemProperties convertLocationFromScriptSemantics(const EntityItemProperti } -QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties) { +QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) { EntityItemProperties propertiesWithSimID = convertLocationFromScriptSemantics(properties); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); + propertiesWithSimID = clientOnly; auto dimensions = propertiesWithSimID.getDimensions(); float volume = dimensions.x * dimensions.y * dimensions.z; @@ -272,13 +273,15 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& bool updatedEntity = false; _entityTree->withWriteLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); + if (!entity) { + return; + } + if (scriptSideProperties.parentRelatedPropertyChanged()) { // All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them. // If any of these changed, pull any missing properties from the entity. - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); - if (!entity) { - return; - } + //existing entity, retrieve old velocity for check down below oldVelocity = entity->getVelocity().length(); @@ -296,6 +299,8 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& } } properties = convertLocationFromScriptSemantics(properties); + properties.setClientOnly(entity->getClientOnly()); + properties.setOwningAvatarID(entity->getOwningAvatarID()); float cost = calculateCost(density * volume, oldVelocity, newVelocity); cost *= costMultiplier; diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e5f913dbf8..2bd08f8e3f 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -82,7 +82,7 @@ public slots: Q_INVOKABLE bool canRez(); /// adds a model with the specific properties - Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties); + Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false); /// temporary method until addEntity can be used from QJSEngine Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index b4f0c484d5..86bbf0b74d 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1373,8 +1373,11 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity + EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); + EntityTreePointer tree = entityTreeElement->getTree(); + // queue the packet to send to the server - args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, newID, properties); + args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, tree, newID, properties); // also update the local tree instantly (note: this is not our tree, but an alternate tree) if (args->otherTree) { diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index f0539110d3..070bf81e3a 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -547,8 +547,11 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ qCDebug(physics) << "EntityMotionState::sendUpdate()... calling queueEditEntityMessage()..."; #endif - entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, id, properties); - _entity->setLastBroadcast(usecTimestampNow()); + EntityTreeElementPointer element = _entity->getElement(); + EntityTreePointer tree = element ? element->getTree() : nullptr; + + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, id, properties); + _entity->setLastBroadcast(now); // if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server // if they've changed. @@ -559,8 +562,9 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); - entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, descendant->getID(), newQueryCubeProperties); - entityDescendant->setLastBroadcast(usecTimestampNow()); + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, + descendant->getID(), newQueryCubeProperties); + entityDescendant->setLastBroadcast(now); } } }); From 473010f634ea9fc68821d02c06c0ba028f0bb832 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 16:45:09 -0700 Subject: [PATCH 0016/1237] addEntity has a clientOnly flag now --- libraries/entities/src/EntityScriptingInterface.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 0869ac40da..499b146d30 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -126,7 +126,13 @@ EntityItemProperties convertLocationFromScriptSemantics(const EntityItemProperti QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) { EntityItemProperties propertiesWithSimID = convertLocationFromScriptSemantics(properties); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); - propertiesWithSimID = clientOnly; + + if (clientOnly) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + propertiesWithSimID.setClientOnly(clientOnly); + propertiesWithSimID.setOwningAvatarID(myNodeID); + } auto dimensions = propertiesWithSimID.getDimensions(); float volume = dimensions.x * dimensions.y * dimensions.z; From 91ff851bf8851189dc28c59d37862e7cfd580301 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 16:59:54 -0700 Subject: [PATCH 0017/1237] fix call to queueEditEntityMessage --- libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 6c4e3994c6..858b34c97f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -981,7 +981,7 @@ void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() { PhysicalEntitySimulation* peSimulation = static_cast(simulation); EntityEditPacketSender* packetSender = peSimulation ? peSimulation->getPacketSender() : nullptr; if (packetSender) { - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entity->getID(), properties); + packetSender->queueEditEntityMessage(PacketType::EntityEdit, tree, entity->getID(), properties); } }); }); From 46c1049a353f5d6317c6c4adf91890c6ee4eb823 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 17:48:06 -0700 Subject: [PATCH 0018/1237] bump protocol version --- libraries/avatars/src/AvatarData.cpp | 1 + libraries/avatars/src/AvatarHashMap.cpp | 1 + libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9a0fc1a835..c68240bdca 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -956,6 +956,7 @@ void AvatarData::clearJointsData() { } bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { + // this is used by the avatar-mixer QDataStream packetStream(data); QUuid avatarUUID; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 62e87ce285..612f4c6f96 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -107,6 +107,7 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer mess } void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode) { + // this is used by clients // setup a data stream to parse the packet QDataStream identityStream(message->getMessage()); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e4aab94090..fbe31d1e8d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -50,7 +50,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return VERSION_LIGHT_HAS_FALLOFF_RADIUS; case PacketType::AvatarData: case PacketType::BulkAvatarData: - return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); + return static_cast(AvatarMixerPacketVersion::AvatarEntities); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetGetInfo: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index b98a87e439..b29fccb45e 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -174,7 +174,8 @@ const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, - SoftAttachmentSupport + SoftAttachmentSupport, + AvatarEntities }; #endif // hifi_PacketHeaders_h From 0ab0409979b20c39ca61dbf6d1a46c2d87e66df0 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 17:48:37 -0700 Subject: [PATCH 0019/1237] revert accidental change --- libraries/entities-renderer/src/RenderableWebEntityItem.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 891e1dca3b..26aecf6050 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -174,14 +174,9 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { - #if defined(Q_OS_LINUX) - // these don't seem to work on Linux - return; - #else if (!buildWebSurface(static_cast(args->_renderer))) { return; } - #endif } _lastRenderTime = usecTimestampNow(); From 6fa62ce26ffc843bd8edd0d1e52338ff1b310246 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Sat, 7 May 2016 18:42:43 -0700 Subject: [PATCH 0020/1237] Working on OSX Rift functionality --- .../display-plugins/OpenGLDisplayPlugin.cpp | 18 +-- .../src/display-plugins/OpenGLDisplayPlugin.h | 1 + .../display-plugins/hmd/HmdDisplayPlugin.cpp | 21 ++-- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 13 ++- libraries/gl/src/gl/QOpenGLContextWrapper.h | 3 + plugins/oculusLegacy/CMakeLists.txt | 8 +- .../src/OculusLegacyDisplayPlugin.cpp | 104 ++++++++++++++---- .../src/OculusLegacyDisplayPlugin.h | 31 +++--- 8 files changed, 144 insertions(+), 55 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 7d4eebb7a2..a0b6ae3939 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -237,12 +237,6 @@ bool OpenGLDisplayPlugin::activate() { _vsyncSupported = _container->getPrimaryWidget()->isVsyncSupported(); - // Child classes may override this in order to do things like initialize - // libraries, etc - if (!internalActivate()) { - return false; - } - #if THREADED_PRESENT // Start the present thread if necessary @@ -258,8 +252,18 @@ bool OpenGLDisplayPlugin::activate() { // Start execution presentThread->start(); } + _presentThread = presentThread.data(); +#endif + + // Child classes may override this in order to do things like initialize + // libraries, etc + if (!internalActivate()) { + return false; + } - // This should not return until the new context has been customized +#if THREADED_PRESENT + + // This should not return until the new context has been customized // and the old context (if any) has been uncustomized presentThread->setNewDisplayPlugin(this); #else diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index defa57fc58..c87ff1bc93 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -103,6 +103,7 @@ protected: virtual void updateFrameData(); + QThread* _presentThread{ nullptr }; ProgramPtr _program; int32_t _mvpUniform { -1 }; int32_t _alphaUniform { -1 }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 79b50a7f88..372337ae4d 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -63,7 +63,7 @@ bool HmdDisplayPlugin::internalActivate() { } -static const char * REPROJECTION_VS = R"VS(#version 450 core +static const char * REPROJECTION_VS = R"VS(#version 410 core in vec3 Position; in vec2 TexCoord; @@ -78,15 +78,15 @@ void main() { )VS"; -static const GLint REPROJECTION_MATRIX_LOCATION = 0; -static const GLint INVERSE_PROJECTION_MATRIX_LOCATION = 4; -static const GLint PROJECTION_MATRIX_LOCATION = 12; -static const char * REPROJECTION_FS = R"FS(#version 450 core +static GLint REPROJECTION_MATRIX_LOCATION = -1; +static GLint INVERSE_PROJECTION_MATRIX_LOCATION = -1; +static GLint PROJECTION_MATRIX_LOCATION = -1; +static const char * REPROJECTION_FS = R"FS(#version 410 core uniform sampler2D sampler; -layout (location = 0) uniform mat3 reprojection = mat3(1); -layout (location = 4) uniform mat4 inverseProjections[2]; -layout (location = 12) uniform mat4 projections[2]; +uniform mat3 reprojection = mat3(1); +uniform mat4 inverseProjections[2]; +uniform mat4 projections[2]; in vec2 vTexCoord; in vec3 vPosition; @@ -205,6 +205,11 @@ void HmdDisplayPlugin::customizeContext() { _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); + + using namespace oglplus; + REPROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "reprojection").Location(); + INVERSE_PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "inverseProjections").Location(); + PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "projections").Location(); } void HmdDisplayPlugin::uncustomizeContext() { diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 185fdaf7f4..8c9a1ba113 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -17,8 +17,15 @@ QOpenGLContext* QOpenGLContextWrapper::currentContext() { return QOpenGLContext::currentContext(); } + QOpenGLContextWrapper::QOpenGLContextWrapper() : - _context(new QOpenGLContext) +_context(new QOpenGLContext) +{ +} + + +QOpenGLContextWrapper::QOpenGLContextWrapper(QOpenGLContext* context) : + _context(context) { } @@ -50,3 +57,7 @@ bool isCurrentContext(QOpenGLContext* context) { return QOpenGLContext::currentContext() == context; } +void QOpenGLContextWrapper::moveToThread(QThread* thread) { + _context->moveToThread(thread); +} + diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index 09f1d67280..33cc0b25e9 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -15,16 +15,19 @@ class QOpenGLContext; class QSurface; class QSurfaceFormat; +class QThread; class QOpenGLContextWrapper { public: QOpenGLContextWrapper(); + QOpenGLContextWrapper(QOpenGLContext* context); void setFormat(const QSurfaceFormat& format); bool create(); void swapBuffers(QSurface* surface); bool makeCurrent(QSurface* surface); void doneCurrent(); void setShareContext(QOpenGLContext* otherContext); + void moveToThread(QThread* thread); static QOpenGLContext* currentContext(); diff --git a/plugins/oculusLegacy/CMakeLists.txt b/plugins/oculusLegacy/CMakeLists.txt index 9e97b3791c..a4e00013f1 100644 --- a/plugins/oculusLegacy/CMakeLists.txt +++ b/plugins/oculusLegacy/CMakeLists.txt @@ -12,13 +12,17 @@ if (APPLE) set(TARGET_NAME oculusLegacy) setup_hifi_plugin() - link_hifi_libraries(shared gl gpu plugins display-plugins input-plugins) + link_hifi_libraries(shared gl gpu plugins ui display-plugins input-plugins) include_hifi_library_headers(octree) add_dependency_external_projects(LibOVR) find_package(LibOVR REQUIRED) target_include_directories(${TARGET_NAME} PRIVATE ${LIBOVR_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${LIBOVR_LIBRARIES}) + find_library(COCOA_LIBRARY Cocoa) + find_library(IOKIT_LIBRARY IOKit) + target_link_libraries(${TARGET_NAME} ${LIBOVR_LIBRARIES} ${COCOA_LIBRARY} ${IOKIT_LIBRARY}) + + endif() diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 34e36d6a6b..6ceddec11b 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -16,7 +17,12 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -39,7 +45,7 @@ void OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo = FrameInfo(); _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _trackingState = ovrHmd_GetTrackingState(_hmd, _currentRenderFrameInfo.predictedDisplayTime); - _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); + _currentRenderFrameInfo.rawRenderPose = _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); Lock lock(_mutex); _frameInfos[frameIndex] = _currentRenderFrameInfo; } @@ -72,14 +78,34 @@ bool OculusLegacyDisplayPlugin::isSupported() const { } bool OculusLegacyDisplayPlugin::internalActivate() { - Parent::internalActivate(); - + if (!Parent::internalActivate()) { + return false; + } + if (!(ovr_Initialize(nullptr))) { Q_ASSERT(false); qFatal("Failed to Initialize SDK"); return false; } + + _hmdWindow = new GLWindow(); + _hmdWindow->create(); + _hmdWindow->createContext(_container->getPrimaryContext()); + auto hmdScreen = qApp->screens()[_hmdScreen]; + auto hmdGeometry = hmdScreen->geometry(); + _hmdWindow->setGeometry(hmdGeometry); + _hmdWindow->showFullScreen(); + + _hmdWindow->makeCurrent(); + glClearColor(1, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + _hmdWindow->swapBuffers(); + + _container->makeRenderingContextCurrent(); + + QOpenGLContextWrapper(_hmdWindow->context()).moveToThread(_presentThread); + _hswDismissed = false; _hmd = ovrHmd_Create(0); if (!_hmd) { @@ -96,7 +122,8 @@ bool OculusLegacyDisplayPlugin::internalActivate() { ovrMatrix4f ovrPerspectiveProjection = ovrMatrix4f_Projection(erd.Fov, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, ovrProjection_RightHanded); _eyeProjections[eye] = toGlm(ovrPerspectiveProjection); - _eyeOffsets[eye] = glm::translate(mat4(), toGlm(erd.HmdToEyeViewOffset)); + _ovrEyeOffsets[eye] = erd.HmdToEyeViewOffset; + _eyeOffsets[eye] = glm::translate(mat4(), -1.0f * toGlm(_ovrEyeOffsets[eye])); eyeSizes[eye] = toGlm(ovrHmd_GetFovTextureSize(_hmd, eye, erd.Fov, 1.0f)); }); @@ -121,6 +148,11 @@ void OculusLegacyDisplayPlugin::internalDeactivate() { ovrHmd_Destroy(_hmd); _hmd = nullptr; ovr_Shutdown(); + _hmdWindow->showNormal(); + _hmdWindow->destroy(); + _hmdWindow->deleteLater(); + _hmdWindow = nullptr; + _container->makeRenderingContextCurrent(); } // DLL based display plugins MUST initialize GLEW inside the DLL code. @@ -131,20 +163,21 @@ void OculusLegacyDisplayPlugin::customizeContext() { glewInit(); glGetError(); }); + _hmdWindow->requestActivate(); + QThread::msleep(1000); Parent::customizeContext(); -#if 0 ovrGLConfig config; memset(&config, 0, sizeof(ovrRenderAPIConfig)); auto& header = config.Config.Header; header.API = ovrRenderAPI_OpenGL; header.BackBufferSize = _hmd->Resolution; header.Multisample = 1; - int distortionCaps = ovrDistortionCap_TimeWarp; + int distortionCaps = ovrDistortionCap_TimeWarp | ovrDistortionCap_Vignette; memset(_eyeTextures, 0, sizeof(ovrTexture) * 2); ovr_for_each_eye([&](ovrEyeType eye) { auto& header = _eyeTextures[eye].Header; header.API = ovrRenderAPI_OpenGL; - header.TextureSize = { (int)_desiredFramebufferSize.x, (int)_desiredFramebufferSize.y }; + header.TextureSize = { (int)_renderTargetSize.x, (int)_renderTargetSize.y }; header.RenderViewport.Size = header.TextureSize; header.RenderViewport.Size.w /= 2; if (eye == ovrEye_Right) { @@ -152,29 +185,57 @@ void OculusLegacyDisplayPlugin::customizeContext() { } }); + if (_hmdWindow->makeCurrent()) { #ifndef NDEBUG - ovrBool result = -#endif - ovrHmd_ConfigureRendering(_hmd, &config.Config, distortionCaps, _eyeFovs, _eyeRenderDescs); - assert(result); + ovrBool result = #endif + ovrHmd_ConfigureRendering(_hmd, &config.Config, distortionCaps, _eyeFovs, _eyeRenderDescs); + assert(result); + _hmdWindow->doneCurrent(); + } + } -#if 0 void OculusLegacyDisplayPlugin::uncustomizeContext() { - HmdDisplayPlugin::uncustomizeContext(); + _hmdWindow->doneCurrent(); + QOpenGLContextWrapper(_hmdWindow->context()).moveToThread(qApp->thread()); + Parent::uncustomizeContext(); } -void OculusLegacyDisplayPlugin::internalPresent() { - ovrHmd_BeginFrame(_hmd, 0); - ovr_for_each_eye([&](ovrEyeType eye) { - reinterpret_cast(_eyeTextures[eye]).OGL.TexId = _currentSceneTexture; - }); - ovrHmd_EndFrame(_hmd, _eyePoses, _eyeTextures); -} +void OculusLegacyDisplayPlugin::hmdPresent() { + if (!_hswDismissed) { + ovrHSWDisplayState hswState; + ovrHmd_GetHSWDisplayState(_hmd, &hswState); + if (hswState.Displayed) { + ovrHmd_DismissHSWDisplay(_hmd); + } -#endif + } + auto r = glm::quat_cast(_currentPresentFrameInfo.presentPose); + ovrQuatf ovrRotation = { r.x, r.y, r.z, r.w }; + ovrPosef eyePoses[2]; + memset(eyePoses, 0, sizeof(ovrPosef) * 2); + eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; + + GLint texture = oglplus::GetName(_compositeFramebuffer->color); + auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + if (_hmdWindow->makeCurrent()) { + glClearColor(0, 0.4, 0.8, 1); + glClear(GL_COLOR_BUFFER_BIT); + ovrHmd_BeginFrame(_hmd, _currentPresentFrameIndex); + glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(sync); + ovr_for_each_eye([&](ovrEyeType eye) { + reinterpret_cast(_eyeTextures[eye]).OGL.TexId = texture; + }); + ovrHmd_EndFrame(_hmd, eyePoses, _eyeTextures); + _hmdWindow->doneCurrent(); + } + static auto widget = _container->getPrimaryWidget(); + widget->makeCurrent(); +} int OculusLegacyDisplayPlugin::getHmdScreen() const { return _hmdScreen; @@ -184,4 +245,3 @@ float OculusLegacyDisplayPlugin::getTargetFrameRate() const { return TARGET_RATE_OculusLegacy; } - diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index bebf3fa2c7..5900ad4c58 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -14,42 +14,43 @@ #include const float TARGET_RATE_OculusLegacy = 75.0f; +class GLWindow; class OculusLegacyDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; public: OculusLegacyDisplayPlugin(); - virtual bool isSupported() const override; - virtual const QString& getName() const override { return NAME; } + bool isSupported() const override; + const QString& getName() const override { return NAME; } - virtual int getHmdScreen() const override; + int getHmdScreen() const override; // Stereo specific methods - virtual void resetSensors() override; - virtual void beginFrameRender(uint32_t frameIndex) override; + void resetSensors() override; + void beginFrameRender(uint32_t frameIndex) override; - virtual float getTargetFrameRate() const override; + float getTargetFrameRate() const override; protected: - virtual bool internalActivate() override; - virtual void internalDeactivate() override; + bool internalActivate() override; + void internalDeactivate() override; - virtual void customizeContext() override; - void hmdPresent() override {} + void customizeContext() override; + void uncustomizeContext() override; + void hmdPresent() override; bool isHmdMounted() const override { return true; } -#if 0 - virtual void uncustomizeContext() override; - virtual void internalPresent() override; -#endif private: static const QString NAME; + GLWindow* _hmdWindow{ nullptr }; ovrHmd _hmd; mutable ovrTrackingState _trackingState; ovrEyeRenderDesc _eyeRenderDescs[2]; + ovrVector3f _ovrEyeOffsets[2]; + ovrFovPort _eyeFovs[2]; - //ovrTexture _eyeTextures[2]; // FIXME - not currently in use + ovrTexture _eyeTextures[2]; // FIXME - not currently in use mutable int _hmdScreen { -1 }; bool _hswDismissed { false }; }; From 38bf1edf1cd4fd2fd3ce52bd6efca2d63df6d1fe Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 8 May 2016 23:35:18 -0700 Subject: [PATCH 0021/1237] Fix plugin loading on OSX --- libraries/plugins/src/plugins/PluginManager.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 4429f49346..936cb8a7ee 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -32,7 +32,11 @@ const LoaderList& getLoadedPlugins() { static std::once_flag once; static LoaderList loadedPlugins; std::call_once(once, [&] { +#ifdef Q_OS_MAC + QString pluginPath = QCoreApplication::applicationDirPath() + "/../PlugIns/"; +#else QString pluginPath = QCoreApplication::applicationDirPath() + "/plugins/"; +#endif QDir pluginDir(pluginPath); pluginDir.setFilter(QDir::Files); if (pluginDir.exists()) { From 6e59abc8f6ec64d8810ae4c80a600d200ba5316e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 12:13:46 +1200 Subject: [PATCH 0022/1237] Switch file selection dialog over to using UI Toolkit files --- .../resources/qml/dialogs/FileDialog.qml | 54 ++++++++++++------- tests/ui/qml/main.qml | 37 +++++++------ 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 916ef434b6..8bb92b60d6 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -1,4 +1,14 @@ -import QtQuick 2.0 +// +// Desktop.qml +// +// Created by Bradley Austin Davis on 14 Jan 2016 +// 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 +// + +import QtQuick 2.5 import QtQuick.Controls 1.4 import Qt.labs.folderlistmodel 2.1 import Qt.labs.settings 1.0 @@ -6,17 +16,19 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import ".." -import "../windows" -import "../styles" -import "../controls" as VrControls +import "../controls-uit" +import "../styles-uit" +import "../windows-uit" + import "fileDialog" //FIXME implement shortcuts for favorite location ModalWindow { id: root - resizable: true - width: 640 - height: 480 + //resizable: true + implicitWidth: 640 + implicitHeight: 480 + HifiConstants { id: hifi } Settings { category: "FileDialog" @@ -46,6 +58,8 @@ ModalWindow { property alias model: fileTableView.model property var drives: helper.drives() + property int titleWidth: 0 + signal selectedFile(var file); signal canceled(); @@ -56,14 +70,17 @@ ModalWindow { }) } - Rectangle { - anchors.fill: parent - color: "white" + Item { + clip: true + width: pane.width + height: pane.height + anchors.margins: 0 Row { id: navControls anchors { left: parent.left; top: parent.top; margins: 8 } spacing: 8 + // FIXME implement back button //VrControls.ButtonAwesome { // id: backButton @@ -72,30 +89,29 @@ ModalWindow { // enabled: d.backStack.length != 0 // MouseArea { anchors.fill: parent; onClicked: d.navigateBack() } //} - VrControls.ButtonAwesome { + + Button { id: upButton enabled: model.parentFolder && model.parentFolder !== "" - text: "\uf0aa" - size: 32 + text: "up" onClicked: d.navigateUp(); } - VrControls.ButtonAwesome { + + Button { id: homeButton property var destination: helper.home(); enabled: d.homeDestination ? true : false - text: "\uf015" - size: 32 + text: "home" onClicked: d.navigateHome(); } - VrControls.ComboBox { + ComboBox { id: drivesSelector width: 48 height: homeButton.height model: drives visible: drives.length > 1 currentIndex: 0 - } } @@ -385,5 +401,3 @@ ModalWindow { } } } - - diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index e45749e1de..54ce16fbc2 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -211,6 +211,26 @@ ApplicationWindow { } } + Button { + text: "Open File" + property var builder: Component { + FileDialog { + title: "Open File" + filter: "*.js" + } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + Button { text: "Add Tab" onClicked: { @@ -246,24 +266,7 @@ ApplicationWindow { } } - Button { - text: "Open File" - property var builder: Component { - FileDialog { } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } } - /* Window { id: blue From 43ee64c251b83f8ef2ebfb1d1a8f9d77c0cd162e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 12:50:32 +1200 Subject: [PATCH 0023/1237] Upate general layout of file selection dialog --- .../resources/qml/dialogs/FileDialog.qml | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 8bb92b60d6..d74913b301 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -78,8 +78,12 @@ ModalWindow { Row { id: navControls - anchors { left: parent.left; top: parent.top; margins: 8 } - spacing: 8 + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: parent.left + } + spacing: hifi.dimensions.contentSpacing.x // FIXME implement back button //VrControls.ButtonAwesome { @@ -119,7 +123,13 @@ ModalWindow { id: currentDirectory height: homeButton.height style: TextFieldStyle { renderType: Text.QtRendering } - anchors { left: navControls.right; right: parent.right; top: parent.top; margins: 8 } + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } property var lastValidFolder: helper.urlToPath(model.folder) onLastValidFolderChanged: text = lastValidFolder; verticalAlignment: Text.AlignVCenter @@ -179,7 +189,14 @@ ModalWindow { FileTableView { id: fileTableView - anchors { left: parent.left; right: parent.right; top: currentDirectory.bottom; bottom: currentSelection.top; margins: 8 } + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + } onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); @@ -277,7 +294,13 @@ ModalWindow { TextField { id: currentSelection style: TextFieldStyle { renderType: Text.QtRendering } - anchors { right: root.selectDirectory ? parent.right : selectionType.left; rightMargin: 8; left: parent.left; leftMargin: 8; top: selectionType.top } + anchors { + left: parent.left + right: root.selectDirectory ? parent.right : selectionType.left + rightMargin: hifi.dimensions.contentSpacing.x + bottom: buttonRow.top + bottomMargin: hifi.dimensions.contentSpacing.y + } readOnly: !root.saveDialog activeFocusOnTab: !readOnly onActiveFocusChanged: if (activeFocus) { selectAll(); } @@ -286,7 +309,11 @@ ModalWindow { FileTypeSelection { id: selectionType - anchors { bottom: buttonRow.top; bottomMargin: 8; right: parent.right; rightMargin: 8; left: buttonRow.left } + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } visible: !selectDirectory KeyNavigation.left: fileTableView KeyNavigation.right: openButton @@ -294,19 +321,22 @@ ModalWindow { Row { id: buttonRow - anchors.right: parent.right - anchors.rightMargin: 8 - anchors.bottom: parent.bottom - anchors.bottomMargin: 8 - spacing: 8 + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + Button { id: openButton + color: hifi.buttons.blue action: okAction Keys.onReturnPressed: okAction.trigger() KeyNavigation.up: selectionType KeyNavigation.left: selectionType KeyNavigation.right: cancelButton } + Button { id: cancelButton action: cancelAction From 663a2ddc6446ce3eebb6737f0273ab8042e6fb17 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 14:43:54 +1200 Subject: [PATCH 0024/1237] Style path and filter controls --- interface/resources/qml/dialogs/FileDialog.qml | 8 ++++---- .../qml/dialogs/fileDialog/FileTypeSelection.qml | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index d74913b301..42fe7120ea 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -1,5 +1,5 @@ // -// Desktop.qml +// FileDialog.qml // // Created by Bradley Austin Davis on 14 Jan 2016 // Copyright 2015 High Fidelity, Inc. @@ -195,7 +195,7 @@ ModalWindow { left: parent.left right: parent.right bottom: currentSelection.top - bottomMargin: hifi.dimensions.contentSpacing.y + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } onDoubleClicked: navigateToRow(row); focus: true @@ -293,11 +293,11 @@ ModalWindow { TextField { id: currentSelection - style: TextFieldStyle { renderType: Text.QtRendering } + label: "Path:" anchors { left: parent.left right: root.selectDirectory ? parent.right : selectionType.left - rightMargin: hifi.dimensions.contentSpacing.x + rightMargin: root.selectDirectory ? 0 : hifi.dimensions.contentSpacing.x bottom: buttonRow.top bottomMargin: hifi.dimensions.contentSpacing.y } diff --git a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml index 57ad2028ad..3d66b37b67 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml @@ -1,8 +1,18 @@ +// +// FileTypeSelection.qml +// +// Created by Bradley Austin Davis on 29 Jan 2016 +// 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 +// + import QtQuick 2.5 -import "../../controls" as VrControls +import "../../controls-uit" -VrControls.ComboBox { +ComboBox { id: root property string filtersString: "All Files (*.*)"; property var currentFilter: [ "*.*" ]; From b8c0ec86af7d663636c1bde1d2b969b9bb763f55 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 15:09:13 +1200 Subject: [PATCH 0025/1237] Hide file filter if only 1 file type --- interface/resources/qml/dialogs/FileDialog.qml | 6 +++--- .../resources/qml/dialogs/fileDialog/FileTypeSelection.qml | 1 + tests/ui/qml/main.qml | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 42fe7120ea..3be7fbc667 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -296,8 +296,8 @@ ModalWindow { label: "Path:" anchors { left: parent.left - right: root.selectDirectory ? parent.right : selectionType.left - rightMargin: root.selectDirectory ? 0 : hifi.dimensions.contentSpacing.x + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 bottom: buttonRow.top bottomMargin: hifi.dimensions.contentSpacing.y } @@ -314,7 +314,7 @@ ModalWindow { left: buttonRow.left right: parent.right } - visible: !selectDirectory + visible: !selectDirectory && filtersCount > 1 KeyNavigation.left: fileTableView KeyNavigation.right: openButton } diff --git a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml index 3d66b37b67..50a10974b5 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml @@ -16,6 +16,7 @@ ComboBox { id: root property string filtersString: "All Files (*.*)"; property var currentFilter: [ "*.*" ]; + property int filtersCount: filtersString.split(';;').length // Per http://doc.qt.io/qt-5/qfiledialog.html#getOpenFileName the string can contain // multiple filters separated by semicolons diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 54ce16fbc2..1745658193 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -216,7 +216,8 @@ ApplicationWindow { property var builder: Component { FileDialog { title: "Open File" - filter: "*.js" + filter: "All Files (*.*)" + //filter: "HTML files (*.html);;Other(*.png)" } } From 88207b727b8cfa6628883a88dfc2256abb1117b7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 15:45:10 +1200 Subject: [PATCH 0026/1237] Style directory and navigation controls "as is" --- .../qml/controls-uit/GlyphButton.qml | 2 +- .../resources/qml/dialogs/FileDialog.qml | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml index c4ee53c04f..2625dda723 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -17,7 +17,7 @@ import "../styles-uit" Original.Button { property int color: 0 - property int colorScheme: hifi.colorShemes.light + property int colorScheme: hifi.colorSchemes.light property string glyph: "" width: 120 diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 3be7fbc667..1f710b4ef5 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -94,25 +94,26 @@ ModalWindow { // MouseArea { anchors.fill: parent; onClicked: d.navigateBack() } //} - Button { + GlyphButton { id: upButton + glyph: hifi.glyphs.levelUp + width: height enabled: model.parentFolder && model.parentFolder !== "" - text: "up" onClicked: d.navigateUp(); } - Button { + GlyphButton { id: homeButton property var destination: helper.home(); + glyph: hifi.glyphs.home + width: height enabled: d.homeDestination ? true : false - text: "home" onClicked: d.navigateHome(); } ComboBox { id: drivesSelector - width: 48 - height: homeButton.height + width: 62 model: drives visible: drives.length > 1 currentIndex: 0 @@ -121,8 +122,8 @@ ModalWindow { TextField { id: currentDirectory + property var lastValidFolder: helper.urlToPath(model.folder) height: homeButton.height - style: TextFieldStyle { renderType: Text.QtRendering } anchors { top: parent.top topMargin: hifi.dimensions.contentMargin.y @@ -130,11 +131,7 @@ ModalWindow { leftMargin: hifi.dimensions.contentSpacing.x right: parent.right } - property var lastValidFolder: helper.urlToPath(model.folder) onLastValidFolderChanged: text = lastValidFolder; - verticalAlignment: Text.AlignVCenter - font.pointSize: 14 - font.bold: true // FIXME add support auto-completion onAccepted: { From c572e1dc3a837e0c1f0d42678ba0ba67591697b6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 10:30:49 -0700 Subject: [PATCH 0027/1237] delete avatar-associated entities when the avatar goes away --- interface/src/avatar/Avatar.cpp | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 1819611af6..67f8d9c967 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -107,6 +107,18 @@ Avatar::Avatar(RigPointer rig) : Avatar::~Avatar() { assert(isDead()); // mark dead before calling the dtor + + EntityTreeRenderer* treeRenderer = qApp->getEntities(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + entityTree->withWriteLock([&] { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + entityTree->deleteEntity(entityID, true, true); + } + }); + } + if (_motionState) { delete _motionState; _motionState = nullptr; @@ -167,7 +179,7 @@ void Avatar::updateAvatarEntities() { // - updateAvatarEntity saves the bytes and sets _avatarEntityDataLocallyEdited // - MyAvatar::update notices _avatarEntityDataLocallyEdited and calls sendIdentityPacket // - sendIdentityPacket sends the entity bytes to the server which relays them to other interfaces - // - AvatarHashMap::processAvatarIdentityPacket's on other interfaces call avatar->setAvatarEntityData() + // - AvatarHashMap::processAvatarIdentityPacket on other interfaces call avatar->setAvatarEntityData() // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... @@ -224,18 +236,14 @@ void Avatar::updateAvatarEntities() { } } } - }); - AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); - foreach (auto entityID, recentlyDettachedAvatarEntities) { - if (!_avatarEntityData.contains(entityID)) { - EntityItemPointer dettachedEntity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (dettachedEntity) { - // this will cause this interface to listen to data from the entity-server about this entity. - dettachedEntity->setClientOnly(false); + AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); + foreach (auto entityID, recentlyDettachedAvatarEntities) { + if (!_avatarEntityData.contains(entityID)) { + entityTree->deleteEntity(entityID, true, true); } } - } + }); if (success) { setAvatarEntityDataChanged(false); From 51d6d99c73cf4ee520784e03b7b113f22b5c4e70 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 11 May 2016 09:17:11 +1200 Subject: [PATCH 0028/1237] Fix up files list data and basic layout --- .../qml/dialogs/fileDialog/FileTableView.qml | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml b/interface/resources/qml/dialogs/fileDialog/FileTableView.qml index 93cdeebab0..d51f9e1ed9 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTableView.qml @@ -1,4 +1,14 @@ -import QtQuick 2.0 +// +// FileDialog.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// 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 +// + +import QtQuick 2.5 import QtQuick.Controls 1.4 TableView { @@ -8,6 +18,7 @@ TableView { root.selection.select(0) } } + //horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff itemDelegate: Component { Item { @@ -23,6 +34,7 @@ TableView { function getText() { switch (styleData.column) { //case 1: return Date.fromLocaleString(locale, styleData.value, "yyyy-MM-dd hh:mm:ss"); + case 1: return root.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; case 2: return root.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); default: return styleData.value; } @@ -45,20 +57,23 @@ TableView { } TableViewColumn { + id: fileNameColumn role: "fileName" title: "Name" - width: 400 + width: Math.floor(0.55 * parent.width) + resizable: true } TableViewColumn { + id: fileMofifiedColumn role: "fileModified" - title: "Date Modified" - width: 200 + title: "Date" + width: Math.floor(0.3 * parent.width) + resizable: true } TableViewColumn { role: "fileSize" title: "Size" - width: 200 + width: Math.floor(0.15 * parent.width) - 16 - 2 // Allow space for vertical scrollbar and borders + resizable: true } } - - From de4c9530c9d81953f517f9c4cb3fc5185cd37684 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 14:20:58 -0700 Subject: [PATCH 0029/1237] carry clientOnly flag over from properties when addEntity is called --- libraries/entities/src/EntityTree.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 1cd2b4a47b..9d9c0fdba9 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -311,7 +311,9 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer result = NULL; - if (getIsClient()) { + bool clientOnly = properties.getClientOnly(); + + if (!clientOnly && getIsClient()) { // if our Node isn't allowed to create entities in this domain, don't try. auto nodeList = DependencyManager::get(); if (nodeList && !nodeList->getThisNodeCanRez()) { @@ -337,6 +339,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti // construct the instance of the entity EntityTypes::EntityType type = properties.getType(); result = EntityTypes::constructEntityItem(type, entityID, properties); + result->setClientOnly(clientOnly); if (result) { if (recordCreationTime) { From 1e849956c9ed74dd9156e44f94173b2a4bda9263 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 14:47:41 -0700 Subject: [PATCH 0030/1237] get rid of _avatarEntityChangedTime --- interface/src/avatar/Avatar.cpp | 5 +---- libraries/avatars/src/AvatarData.h | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 67f8d9c967..d3664b9f20 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -183,10 +183,8 @@ void Avatar::updateAvatarEntities() { // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... - quint64 now = usecTimestampNow(); - const static quint64 refreshTime = 3 * USECS_PER_SECOND; - if (!_avatarEntityDataChanged && now - _avatarEntityChangedTime < refreshTime) { + if (!_avatarEntityDataChanged) { return; } @@ -247,7 +245,6 @@ void Avatar::updateAvatarEntities() { if (success) { setAvatarEntityDataChanged(false); - _avatarEntityChangedTime = now; } } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 72d34af9d9..2242860e22 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -423,7 +423,6 @@ protected: AvatarEntityMap _avatarEntityData; bool _avatarEntityDataLocallyEdited { false }; bool _avatarEntityDataChanged { false }; - quint64 _avatarEntityChangedTime { 0 }; private: friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar); From 4b13fd969e0609e9abc5ac8e08daf92835f19e7e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 15:37:54 -0700 Subject: [PATCH 0031/1237] split code that sends edits via avatar-mixer out of queueEditEntityMessage --- interface/src/avatar/Avatar.cpp | 1 - .../entities/src/EntityEditPacketSender.cpp | 78 +++++++++++-------- .../entities/src/EntityEditPacketSender.h | 4 + 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index d3664b9f20..80e7aaa8a7 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -183,7 +183,6 @@ void Avatar::updateAvatarEntities() { // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... - const static quint64 refreshTime = 3 * USECS_PER_SECOND; if (!_avatarEntityDataChanged) { return; } diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index ea86d3d542..416d3c971e 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -36,6 +36,51 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte } } +void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, + EntityTreePointer entityTree, + EntityItemID entityItemID, + const EntityItemProperties& properties) { + if (!_shouldSend) { + return; // bail early + } + + assert(properties.getClientOnly()); + + // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server + assert(_myAvatar); + + if (!entityTree) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage null entityTree."; + return; + } + EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); + if (!entity) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; + return; + } + + // the properties that get serialized into the avatar identity packet should be the entire set + // rather than just the ones being edited. + entity->setProperties(properties); + EntityItemProperties entityProperties = entity->getProperties(); + + QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); + QVariant variantProperties = scriptProperties.toVariant(); + QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties); + + // the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar + QJsonObject jsonObject = jsonProperties.object(); + if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { + jsonObject["parentID"] = AVATAR_SELF_ID.toString(); + } + jsonProperties = QJsonDocument(jsonObject); + + QByteArray binaryProperties = jsonProperties.toBinaryData(); + _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + return; +} + + void EntityEditPacketSender::queueEditEntityMessage(PacketType type, EntityTreePointer entityTree, EntityItemID entityItemID, @@ -43,38 +88,9 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, if (!_shouldSend) { return; // bail early } + if (properties.getClientOnly()) { - // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server - assert(_myAvatar); - - if (!entityTree) { - qDebug() << "EntityEditPacketSender::queueEditEntityMessage null entityTree."; - return; - } - EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); - if (!entity) { - qDebug() << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; - return; - } - - // the properties that get serialized into the avatar identity packet should be the entire set - // rather than just the ones being edited. - entity->setProperties(properties); - EntityItemProperties entityProperties = entity->getProperties(); - - QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); - QVariant variantProperties = scriptProperties.toVariant(); - QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties); - - // the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar - QJsonObject jsonObject = jsonProperties.object(); - if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { - jsonObject["parentID"] = AVATAR_SELF_ID.toString(); - } - jsonProperties = QJsonDocument(jsonObject); - - QByteArray binaryProperties = jsonProperties.toBinaryData(); - _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + queueEditAvatarEntityMessage(type, entityTree, entityItemID, properties); return; } diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 90c6cb988d..9366fc9329 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -27,6 +27,10 @@ public: AvatarData* getMyAvatar() { return _myAvatar; } void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); } + void queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree, + EntityItemID entityItemID, const EntityItemProperties& properties); + + /// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines /// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in /// which case up to MaxPendingMessages will be buffered and processed when voxel servers are known. From b05ab1b17e219186e2c8c64d122b64e41b9ca342 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 15:59:25 -0700 Subject: [PATCH 0032/1237] set simulationOwner to be the same as the owningAvatar --- interface/src/avatar/Avatar.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 80e7aaa8a7..cbe69185af 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -214,6 +214,10 @@ void Avatar::updateAvatarEntities() { properties.setClientOnly(true); properties.setOwningAvatarID(getID()); + // there's not entity-server to tell us we're the simulation owner, so always set the + // simulationOwner to the owningAvatarID and a high priority. + properties.setSimulationOwner(getID(), 129); + if (properties.getParentID() == AVATAR_SELF_ID) { properties.setParentID(getID()); } From 144715f00cf7b8bb33ed551d9f57e4fed5859051 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:05:40 -0700 Subject: [PATCH 0033/1237] don't make changes to other avatar's avatarEntities --- libraries/entities/src/EntityScriptingInterface.cpp | 6 ++++++ libraries/physics/src/EntityMotionState.cpp | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index e7b386e544..b160a23ced 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -284,6 +284,12 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& return; } + auto nodeList = DependencyManager::get(); + if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) { + // don't edit other avatar's avatarEntities + return; + } + if (scriptSideProperties.parentRelatedPropertyChanged()) { // All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them. // If any of these changed, pull any missing properties from the entity. diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 070bf81e3a..f1897275ed 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -404,6 +404,11 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { assert(_body); assert(entityTreeIsLocked()); + if (_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID()) { + // don't send updates for someone else's avatarEntities + return false; + } + if (_entity->actionDataNeedsTransmit()) { return true; } From 356ccdba26cb668e999c4af45dc691f134b17f8f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:16:06 -0700 Subject: [PATCH 0034/1237] debug icon for clientOnly --- .../src/RenderableEntityItem.cpp | 21 ++++++++++++++++--- .../src/RenderableEntityItem.h | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index d148145dde..011675fc82 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -47,6 +47,9 @@ namespace render { } void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status::Getters& statusGetters) { + auto nodeList = DependencyManager::get(); + const QUuid& myNodeID = nodeList->getSessionUUID(); + statusGetters.push_back([entity] () -> render::Item::Status::Value { quint64 delta = usecTimestampNow() - entity->getLastEditedFromRemote(); const float WAIT_THRESHOLD_INV = 1.0f / (0.2f * USECS_PER_SECOND); @@ -81,9 +84,7 @@ void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status: (unsigned char)RenderItemStatusIcon::ACTIVE_IN_BULLET); }); - statusGetters.push_back([entity] () -> render::Item::Status::Value { - auto nodeList = DependencyManager::get(); - const QUuid& myNodeID = nodeList->getSessionUUID(); + statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value { bool weOwnSimulation = entity->getSimulationOwner().matchesValidID(myNodeID); bool otherOwnSimulation = !weOwnSimulation && !entity->getSimulationOwner().isNull(); @@ -106,4 +107,18 @@ void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status: return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN, (unsigned char)RenderItemStatusIcon::HAS_ACTIONS); }); + + statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value { + if (entity->getClientOnly()) { + if (entity->getOwningAvatarID() == myNodeID) { + return render::Item::Status::Value(1.0f, render::Item::Status::Value::GREEN, + (unsigned char)RenderItemStatusIcon::CLIENT_ONLY); + } else { + return render::Item::Status::Value(1.0f, render::Item::Status::Value::RED, + (unsigned char)RenderItemStatusIcon::CLIENT_ONLY); + } + } + return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN, + (unsigned char)RenderItemStatusIcon::CLIENT_ONLY); + }); } diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 09451e87d4..9840bf3150 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -26,6 +26,7 @@ enum class RenderItemStatusIcon { SIMULATION_OWNER = 3, HAS_ACTIONS = 4, OTHER_SIMULATION_OWNER = 5, + CLIENT_ONLY = 6, NONE = 255 }; From de12680ff1dab61f4eb97b431c2d8e2228194499 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:43:27 -0700 Subject: [PATCH 0035/1237] don't put actions on other people's avatarEntities --- interface/src/avatar/Avatar.cpp | 4 ++++ libraries/entities/src/EntityEditPacketSender.cpp | 4 ++++ libraries/entities/src/EntityScriptingInterface.cpp | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index cbe69185af..83cec224b6 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -187,6 +187,10 @@ void Avatar::updateAvatarEntities() { return; } + if (getID() == QUuid()) { + return; // wait until MyAvatar gets an ID before doing this. + } + EntityTreeRenderer* treeRenderer = qApp->getEntities(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (!entityTree) { diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 416d3c971e..6cb0b6ef60 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -44,6 +44,10 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, return; // bail early } + if (properties.getOwningAvatarID() != _myAvatar->getID()) { + return; // don't send updates for someone else's avatarEntity + } + assert(properties.getClientOnly()); // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index b160a23ced..3b06c5a998 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -788,6 +788,9 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, return false; } + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + EntityItemPointer entity; bool doTransmit = false; _entityTree->withWriteLock([&] { @@ -803,6 +806,10 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, return; } + if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) { + return; + } + doTransmit = actor(simulation, entity); if (doTransmit) { _entityTree->entityChanged(entity); From e4e0be8fa3ba0baf38eaa5b5a0588c7a9f630dc7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:51:38 -0700 Subject: [PATCH 0036/1237] relay owningAvatar from properties to entity on entity creation --- libraries/entities/src/EntityTree.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 9d9c0fdba9..2da3604c2a 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -312,6 +312,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti EntityItemPointer result = NULL; bool clientOnly = properties.getClientOnly(); + QUuid owningAvatarID = properties.getOwningAvatarID(); if (!clientOnly && getIsClient()) { // if our Node isn't allowed to create entities in this domain, don't try. @@ -340,6 +341,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti EntityTypes::EntityType type = properties.getType(); result = EntityTypes::constructEntityItem(type, entityID, properties); result->setClientOnly(clientOnly); + result->setOwningAvatarID(owningAvatarID); if (result) { if (recordCreationTime) { From 872f1b0c64d9dc7479dbc1a9de3615263dc21818 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 17:48:55 -0700 Subject: [PATCH 0037/1237] clear avatarEntity data for entities that are deleted --- interface/src/avatar/Avatar.cpp | 5 ++++- interface/src/avatar/MyAvatar.cpp | 1 + libraries/entities/src/EntityEditPacketSender.cpp | 6 ++++++ libraries/entities/src/EntityItem.h | 1 + libraries/entities/src/EntityScriptingInterface.cpp | 10 +++++++++- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 83cec224b6..d959b298ef 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -229,7 +229,10 @@ void Avatar::updateAvatarEntities() { EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { - if (!entityTree->updateEntity(entityID, properties)) { + if (entityTree->updateEntity(entityID, properties)) { + entity->markAsChangedOnServer(); + entity->updateLastEditedFromRemote(); + } else { qDebug() << "AVATAR-ENTITES -- updateEntity failed: " << properties.getType(); success = false; } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index dc0d6d7e97..304aabdcc6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -828,6 +828,7 @@ void MyAvatar::loadData() { for (int i = 0; i < avatarEntityCount; i++) { settings.setArrayIndex(i); QUuid entityID = settings.value("id").toUuid(); + // QUuid entityID = QUuid::createUuid(); // generate a new ID QByteArray properties = settings.value("properties").toByteArray(); updateAvatarEntity(entityID, properties); } diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 6cb0b6ef60..28f1871346 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -81,6 +81,8 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, QByteArray binaryProperties = jsonProperties.toBinaryData(); _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + + entity->setLastBroadcast(usecTimestampNow()); return; } @@ -115,6 +117,10 @@ void EntityEditPacketSender::queueEraseEntityMessage(const EntityItemID& entityI return; // bail early } + // in case this was a clientOnly entity: + assert(_myAvatar); + _myAvatar->clearAvatarEntity(entityItemID); + QByteArray bufferOut(NLPacket::maxPayloadSize(PacketType::EntityErase), 0); if (EntityItemProperties::encodeEraseEntityMessage(entityItemID, bufferOut)) { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4286a9d6ae..c2e497e602 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -375,6 +375,7 @@ public: glm::vec3 entityToWorld(const glm::vec3& point) const; quint64 getLastEditedFromRemote() const { return _lastEditedFromRemote; } + void updateLastEditedFromRemote() { _lastEditedFromRemote = usecTimestampNow(); } void getAllTerseUpdateProperties(EntityItemProperties& properties) const; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 3b06c5a998..ade8ade1c4 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -401,6 +401,14 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (entity) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { + // don't delete other avatar's avatarEntities + shouldDelete = false; + return; + } + auto dimensions = entity->getDimensions(); float volume = dimensions.x * dimensions.y * dimensions.z; auto density = entity->getDensity(); @@ -806,7 +814,7 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, return; } - if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) { + if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { return; } From ac506dc4b77de0bd8831796de3ffe6fad855a6c9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 12:46:16 -0700 Subject: [PATCH 0038/1237] where needed, copy clientOnly into properties before calling queueEditEntityMessage --- interface/src/avatar/MyAvatar.cpp | 1 + libraries/entities/src/EntityItem.cpp | 11 ++++++++++ .../entities/src/EntityItemProperties.cpp | 20 +++++++++++++++++++ libraries/entities/src/EntityItemProperties.h | 15 ++++++-------- libraries/entities/src/EntityPropertyFlags.h | 3 +++ .../entities/src/EntityScriptingInterface.cpp | 5 ++++- libraries/entities/src/EntityTree.cpp | 3 --- libraries/physics/src/EntityMotionState.cpp | 7 +++++++ 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 304aabdcc6..33905d5e81 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -446,6 +446,7 @@ void MyAvatar::simulate(float deltaTime) { EntityItemProperties properties = entity->getProperties(); properties.setQueryAACubeDirty(); properties.setLastEdited(now); + packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entity->getID(), properties); entity->setLastBroadcast(usecTimestampNow()); } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 187c4f51be..04307892f1 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -138,6 +138,9 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_PARENT_JOINT_INDEX; requestedProperties += PROP_QUERY_AA_CUBE; + requestedProperties += PROP_CLIENT_ONLY; + requestedProperties += PROP_OWNING_AVATAR_ID; + return requestedProperties; } @@ -1094,6 +1097,8 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper properties._id = getID(); properties._idSet = true; properties._created = _created; + properties.setClientOnly(_clientOnly); + properties.setOwningAvatarID(_owningAvatarID); properties._type = getType(); @@ -1135,6 +1140,9 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(localPosition, getLocalPosition); COPY_ENTITY_PROPERTY_TO_PROPERTIES(localRotation, getLocalOrientation); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(clientOnly, getClientOnly); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(owningAvatarID, getOwningAvatarID); + properties._defaultSettings = false; return properties; @@ -1225,6 +1233,9 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentJointIndex, setParentJointIndex); SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(clientOnly, setClientOnly); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(owningAvatarID, setOwningAvatarID); + AACube saveQueryAACube = _queryAACube; checkAndAdjustQueryAACube(); if (saveQueryAACube != _queryAACube) { diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 738e8910fe..354cbd03ef 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -309,6 +309,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube); CHECK_PROPERTY_CHANGE(PROP_LOCAL_POSITION, localPosition); CHECK_PROPERTY_CHANGE(PROP_LOCAL_ROTATION, localRotation); + CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly); + CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); @@ -541,6 +543,12 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_POSITION, localPosition); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_ROTATION, localRotation); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID); + + properties.setProperty("clientOnly", convertScriptValue(engine, getClientOnly())); + properties.setProperty("owningAvatarID", convertScriptValue(engine, getOwningAvatarID())); + // FIXME - I don't think these properties are supported any more //COPY_PROPERTY_TO_QSCRIPTVALUE(glowLevel); //COPY_PROPERTY_TO_QSCRIPTVALUE(localRenderAlpha); @@ -679,6 +687,9 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); + COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); + COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); + _lastEdited = usecTimestampNow(); } @@ -1564,6 +1575,9 @@ void EntityItemProperties::markAllChanged() { _jointTranslationsChanged = true; _queryAACubeChanged = true; + + _clientOnlyChanged = true; + _owningAvatarIDChanged = true; } // The minimum bounding box for the entity. @@ -1884,6 +1898,12 @@ QList EntityItemProperties::listChangedProperties() { if (queryAACubeChanged()) { out += "queryAACube"; } + if (clientOnlyChanged()) { + out += "clientOnly"; + } + if (owningAvatarIDChanged()) { + out += "owningAvatarID"; + } getAnimation().listChangedProperties(out); getKeyLight().listChangedProperties(out); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index fb24e711f4..fb36834238 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -205,6 +205,9 @@ public: DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector, QVector()); DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector, QVector()); + DEFINE_PROPERTY(PROP_CLIENT_ONLY, ClientOnly, clientOnly, bool, false); + DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID); + static QString getBackgroundModeString(BackgroundMode mode); @@ -276,12 +279,6 @@ public: void setJointRotationsDirty() { _jointRotationsSetChanged = true; _jointRotationsChanged = true; } void setJointTranslationsDirty() { _jointTranslationsSetChanged = true; _jointTranslationsChanged = true; } - bool getClientOnly() const { return _clientOnly; } - void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } - // if this entity is client-only, which avatar is it associated with? - QUuid getOwningAvatarID() const { return _owningAvatarID; } - void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } - protected: QString getCollisionMaskAsString() const; void setCollisionMaskFromString(const QString& maskString); @@ -308,9 +305,6 @@ private: glm::vec3 _naturalPosition; EntityPropertyFlags _desiredProperties; // if set will narrow scopes of copy/to/from to just these properties - - bool _clientOnly { false }; - QUuid _owningAvatarID; }; Q_DECLARE_METATYPE(EntityItemProperties); @@ -430,6 +424,9 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslationsSet, jointTranslationsSet, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslations, jointTranslations, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ClientOnly, clientOnly, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, ""); + properties.getAnimation().debugDump(); properties.getSkybox().debugDump(); properties.getStage().debugDump(); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 90a7c1e2f7..394f61b5e8 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -169,6 +169,9 @@ enum EntityPropertyList { PROP_FALLOFF_RADIUS, // for Light entity + PROP_CLIENT_ONLY, // doesn't go over wire + PROP_OWNING_AVATAR_ID, // doesn't go over wire + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index ade8ade1c4..15c2bffd80 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -799,6 +799,8 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); + EntityItemProperties properties; + EntityItemPointer entity; bool doTransmit = false; _entityTree->withWriteLock([&] { @@ -820,13 +822,14 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, doTransmit = actor(simulation, entity); if (doTransmit) { + properties.setClientOnly(entity->getClientOnly()); + properties.setOwningAvatarID(entity->getOwningAvatarID()); _entityTree->entityChanged(entity); } }); // transmit the change if (doTransmit) { - EntityItemProperties properties; _entityTree->withReadLock([&] { properties = entity->getProperties(); }); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 2da3604c2a..087225c865 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -312,7 +312,6 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti EntityItemPointer result = NULL; bool clientOnly = properties.getClientOnly(); - QUuid owningAvatarID = properties.getOwningAvatarID(); if (!clientOnly && getIsClient()) { // if our Node isn't allowed to create entities in this domain, don't try. @@ -340,8 +339,6 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti // construct the instance of the entity EntityTypes::EntityType type = properties.getType(); result = EntityTypes::constructEntityItem(type, entityID, properties); - result->setClientOnly(clientOnly); - result->setOwningAvatarID(owningAvatarID); if (result) { if (recordCreationTime) { diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index f1897275ed..053bfcbd85 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -555,6 +555,9 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ EntityTreeElementPointer element = _entity->getElement(); EntityTreePointer tree = element ? element->getTree() : nullptr; + properties.setClientOnly(_entity->getClientOnly()); + properties.setOwningAvatarID(_entity->getOwningAvatarID()); + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, id, properties); _entity->setLastBroadcast(now); @@ -567,6 +570,10 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); + + newQueryCubeProperties.setClientOnly(entityDescendant->getClientOnly()); + newQueryCubeProperties.setOwningAvatarID(entityDescendant->getOwningAvatarID()); + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, descendant->getID(), newQueryCubeProperties); entityDescendant->setLastBroadcast(now); From 73c06b3bf42524d5d1898f60ef2be4d059a936ee Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 14:13:08 -0700 Subject: [PATCH 0039/1237] updating attachedEntitiesManager to work with new system --- scripts/system/assets/images/lock.svg | 70 +++++++++++++++++++ scripts/system/assets/images/unlock.svg | 70 +++++++++++++++++++ scripts/system/attachedEntitiesManager.js | 84 ++++++++++------------- scripts/system/libraries/toolBars.js | 14 +++- 4 files changed, 188 insertions(+), 50 deletions(-) create mode 100644 scripts/system/assets/images/lock.svg create mode 100644 scripts/system/assets/images/unlock.svg diff --git a/scripts/system/assets/images/lock.svg b/scripts/system/assets/images/lock.svg new file mode 100644 index 0000000000..1480359f43 --- /dev/null +++ b/scripts/system/assets/images/lock.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/scripts/system/assets/images/unlock.svg b/scripts/system/assets/images/unlock.svg new file mode 100644 index 0000000000..a7664c1f59 --- /dev/null +++ b/scripts/system/assets/images/unlock.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index 9ddb040297..124b2f76ec 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -22,7 +22,7 @@ var MINIMUM_DROP_DISTANCE_FROM_JOINT = 0.8; var ATTACHED_ENTITY_SEARCH_DISTANCE = 10.0; var ATTACHED_ENTITIES_SETTINGS_KEY = "ATTACHED_ENTITIES"; var DRESSING_ROOM_DISTANCE = 2.0; -var SHOW_TOOL_BAR = false; +var SHOW_TOOL_BAR = true; // tool bar @@ -30,34 +30,20 @@ if (SHOW_TOOL_BAR) { var BUTTON_SIZE = 32; var PADDING = 3; Script.include(["libraries/toolBars.js"]); - var toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.attachedEntities.toolbar", function(screenSize) { - return { - x: (BUTTON_SIZE + PADDING), - y: (screenSize.y / 2 - BUTTON_SIZE * 2 + PADDING) - }; - }); - var saveButton = toolBar.addOverlay("image", { + + var toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.attachedEntities.toolbar"); + var lockButton = toolBar.addTool({ width: BUTTON_SIZE, height: BUTTON_SIZE, - imageURL: ".../save.png", + imageURL: Script.resolvePath("assets/images/lock.svg"), color: { red: 255, green: 255, blue: 255 }, - alpha: 1 - }); - var loadButton = toolBar.addOverlay("image", { - width: BUTTON_SIZE, - height: BUTTON_SIZE, - imageURL: ".../load.png", - color: { - red: 255, - green: 255, - blue: 255 - }, - alpha: 1 - }); + alpha: 1, + visible: true + }, false); } @@ -67,10 +53,8 @@ function mousePressEvent(event) { y: event.y }); - if (clickedOverlay == saveButton) { - manager.saveAttachedEntities(); - } else if (clickedOverlay == loadButton) { - manager.loadAttachedEntities(); + if (lockButton === toolBar.clicked(clickedOverlay)) { + manager.toggleLocked(); } } @@ -92,6 +76,8 @@ Script.scriptEnding.connect(scriptEnding); function AttachedEntitiesManager() { + var clothingLocked = true; + this.subscribeToMessages = function() { Messages.subscribe('Hifi-Object-Manipulation'); Messages.messageReceived.connect(this.handleWearableMessages); @@ -128,19 +114,6 @@ function AttachedEntitiesManager() { } } - this.avatarIsInDressingRoom = function() { - // return true if MyAvatar is near the dressing room - var possibleDressingRoom = Entities.findEntities(MyAvatar.position, DRESSING_ROOM_DISTANCE); - for (i = 0; i < possibleDressingRoom.length; i++) { - var entityID = possibleDressingRoom[i]; - var props = Entities.getEntityProperties(entityID); - if (props.name == 'Hifi-Dressing-Room-Base') { - return true; - } - } - return false; - } - this.handleEntityRelease = function(grabbedEntity, releasedFromJoint) { // if this is still equipped, just rewrite the position information. var grabData = getEntityCustomData('grabKey', grabbedEntity, {}); @@ -179,21 +152,23 @@ function AttachedEntitiesManager() { } if (bestJointIndex != -1) { - var wearProps = { - parentID: MyAvatar.sessionUUID, - parentJointIndex: bestJointIndex - }; + var wearProps = Entities.getEntityProperties(grabbedEntity); + wearProps.parentID = MyAvatar.sessionUUID; + wearProps.parentJointIndex = bestJointIndex; if (bestJointOffset && bestJointOffset.constructor === Array) { - if (this.avatarIsInDressingRoom() || bestJointOffset.length < 2) { + if (!clothingLocked || bestJointOffset.length < 2) { this.updateRelativeOffsets(grabbedEntity); } else { - // don't snap the entity to the preferred position if the avatar is in the dressing room. + // don't snap the entity to the preferred position if unlocked wearProps.localPosition = bestJointOffset[0]; wearProps.localRotation = bestJointOffset[1]; } } - Entities.editEntity(grabbedEntity, wearProps); + + // Entities.editEntity(grabbedEntity, wearProps); + Entities.deleteEntity(grabbedEntity); + Entities.addEntity(wearProps, true); } else if (props.parentID != NULL_UUID) { // drop the entity and set it to have no parent (not on the avatar), unless it's being equipped in a hand. if (props.parentID === MyAvatar.sessionUUID && @@ -201,7 +176,11 @@ function AttachedEntitiesManager() { props.parentJointIndex == MyAvatar.getJointIndex("LeftHand"))) { // this is equipped on a hand -- don't clear the parent. } else { - Entities.editEntity(grabbedEntity, { parentID: NULL_UUID }); + var wearProps = Entities.getEntityProperties(grabbedEntity); + wearProps.parentID = NULL_UUID; + wearProps.parentJointIndex = -1; + Entities.deleteEntity(grabbedEntity); + Entities.addEntity(wearProps, false); } } } @@ -221,6 +200,17 @@ function AttachedEntitiesManager() { return false; } + this.toggleLocked = function() { + print("toggleLocked"); + if (clothingLocked) { + clothingLocked = false; + toolBar.setImageURL(Script.resolvePath("assets/images/unlock.svg"), lockButton); + } else { + clothingLocked = true; + toolBar.setImageURL(Script.resolvePath("assets/images/lock.svg"), lockButton); + } + } + this.saveAttachedEntities = function() { print("--- saving attached entities ---"); saveData = []; diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index d97575d349..9efe533457 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -56,6 +56,10 @@ Overlay2D = function(properties, overlay) { // overlay is an optional variable properties.alpha = alpha; Overlays.editOverlay(overlay, { alpha: alpha }); } + this.setImageURL = function(imageURL) { + properties.imageURL = imageURL; + Overlays.editOverlay(overlay, { imageURL: imageURL }); + } this.show = function(doShow) { properties.visible = doShow; Overlays.editOverlay(overlay, { visible: doShow }); @@ -254,7 +258,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit } this.save(); } - + this.setAlpha = function(alpha, tool) { if(typeof(tool) === 'undefined') { for(var tool in this.tools) { @@ -268,7 +272,11 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit this.tools[tool].setAlpha(alpha); } } - + + this.setImageURL = function(imageURL, tool) { + this.tools[tool].setImageURL(imageURL); + } + this.setBack = function(color, alpha) { if (color == null) { Overlays.editOverlay(this.back, { visible: false }); @@ -478,4 +486,4 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit } ToolBar.SPACING = 6; ToolBar.VERTICAL = 0; -ToolBar.HORIZONTAL = 1; \ No newline at end of file +ToolBar.HORIZONTAL = 1; From 57fae3fc5121a979bb836cafccd1fd643a18a234 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 09:28:25 +1200 Subject: [PATCH 0040/1237] Add FiraSans-Regular font --- .../resources/fonts/FiraSans-Regular.ttf | Bin 0 -> 403924 bytes .../qml/styles-uit/FiraSansRegular.qml | 23 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 interface/resources/fonts/FiraSans-Regular.ttf create mode 100644 interface/resources/qml/styles-uit/FiraSansRegular.qml diff --git a/interface/resources/fonts/FiraSans-Regular.ttf b/interface/resources/fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d9fdc0e922030c6836a9ad5f808fd7e1c5970873 GIT binary patch literal 403924 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 r-(pXe}H&MjC7NAiF2}5 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 War7s!{wz%rN_!OQ*CO@X^#2EvSW`a$ literal 0 HcmV?d00001 diff --git a/interface/resources/qml/styles-uit/FiraSansRegular.qml b/interface/resources/qml/styles-uit/FiraSansRegular.qml new file mode 100644 index 0000000000..4ae0826772 --- /dev/null +++ b/interface/resources/qml/styles-uit/FiraSansRegular.qml @@ -0,0 +1,23 @@ +// +// FiraSansRegular.qml +// +// Created by David Rowe on 12 May 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +Text { + id: root + FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + property real size: 32 + font.pixelSize: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.family: firaSansRegular.name +} From 032fbd9a55b6ff220c7ac0e17f989ded353bae6d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 16:41:00 -0700 Subject: [PATCH 0041/1237] taking something off now makes it an entity-server-aware entity --- scripts/system/attachedEntitiesManager.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index 124b2f76ec..e0261b3c7a 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -166,7 +166,6 @@ function AttachedEntitiesManager() { } } - // Entities.editEntity(grabbedEntity, wearProps); Entities.deleteEntity(grabbedEntity); Entities.addEntity(wearProps, true); } else if (props.parentID != NULL_UUID) { @@ -179,8 +178,23 @@ function AttachedEntitiesManager() { var wearProps = Entities.getEntityProperties(grabbedEntity); wearProps.parentID = NULL_UUID; wearProps.parentJointIndex = -1; + + delete wearProps.id; + delete wearProps.created; + delete wearProps.age; + delete wearProps.ageAsText; + delete wearProps.naturalDimensions; + delete wearProps.naturalPosition; + delete wearProps.actionData; + delete wearProps.sittingPoints; + delete wearProps.boundingBox; + delete wearProps.clientOnly; + delete wearProps.owningAvatarID; + delete wearProps.localPosition; + delete wearProps.localRotation; + Entities.deleteEntity(grabbedEntity); - Entities.addEntity(wearProps, false); + Entities.addEntity(wearProps); } } } @@ -271,7 +285,7 @@ function AttachedEntitiesManager() { this.scrubProperties(savedProps); delete savedProps["id"]; savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions - var loadedEntityID = Entities.addEntity(savedProps); + var loadedEntityID = Entities.addEntity(savedProps, true); Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'loaded', From c784ca9771b9e733d45e25083fdfb43c0376ee8a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 18:45:56 -0700 Subject: [PATCH 0042/1237] debugging --- interface/src/avatar/Avatar.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 1e8ff7775e..95a3ef6132 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -197,6 +197,7 @@ void Avatar::updateAvatarEntities() { QScriptEngine scriptEngine; entityTree->withWriteLock([&] { AvatarEntityMap avatarEntities = getAvatarEntityData(); + qDebug() << "---------------"; for (auto entityID : avatarEntities.keys()) { // see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties // and either add or update the entity. @@ -206,6 +207,9 @@ void Avatar::updateAvatarEntities() { qDebug() << "got bad avatarEntity json"; continue; } + + qDebug() << jsonProperties.toJson(); + QVariant variantProperties = jsonProperties.toVariant(); QVariantMap asMap = variantProperties.toMap(); QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); @@ -214,7 +218,7 @@ void Avatar::updateAvatarEntities() { properties.setClientOnly(true); properties.setOwningAvatarID(getID()); - // there's not entity-server to tell us we're the simulation owner, so always set the + // there's no entity-server to tell us we're the simulation owner, so always set the // simulationOwner to the owningAvatarID and a high priority. properties.setSimulationOwner(getID(), 129); @@ -225,17 +229,19 @@ void Avatar::updateAvatarEntities() { EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { + qDebug() << "avatar-entities existing entity, element =" << entity->getElement().get(); if (entityTree->updateEntity(entityID, properties)) { - entity->markAsChangedOnServer(); entity->updateLastEditedFromRemote(); + qDebug() << "avatar-entities after entityTree->updateEntity(), element =" << entity->getElement().get(); } else { - qDebug() << "AVATAR-ENTITES -- updateEntity failed: " << properties.getType(); + qDebug() << "AVATAR-ENTITIES -- updateEntity failed: " << properties.getType(); success = false; } } else { + qDebug() << "avatar-entities new entity"; entity = entityTree->addEntity(entityID, properties); if (!entity) { - qDebug() << "AVATAR-ENTITES -- addEntity failed: " << properties.getType(); + qDebug() << "AVATAR-ENTITIES -- addEntity failed: " << properties.getType(); success = false; } } From c29b82e24de6a0183c3cd838f01e03f9720ba877 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:08:28 +1200 Subject: [PATCH 0043/1237] Restyle file selection dialog First pass --- .../qml/controls-uit/AttachmentsTable.qml | 7 +- .../resources/qml/controls-uit/Table.qml | 163 +++++++----------- .../resources/qml/dialogs/FileDialog.qml | 76 +++++++- .../qml/dialogs/fileDialog/FileTableView.qml | 79 --------- .../qml/styles-uit/HifiConstants.qml | 19 +- 5 files changed, 155 insertions(+), 189 deletions(-) delete mode 100644 interface/resources/qml/dialogs/fileDialog/FileTableView.qml diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index 6022de7f93..ce93b8f4df 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -21,7 +21,6 @@ import "../hifi/models" TableView { id: tableView - // property var tableModel: ListModel { } property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light @@ -46,7 +45,7 @@ TableView { RalewayRegular { id: textHeader - size: hifi.fontSizes.tableText + size: hifi.fontSizes.tableHeading color: hifi.colors.lightGrayText text: styleData.value anchors { @@ -87,7 +86,7 @@ TableView { bottomMargin: 3 // "" } radius: 3 - color: hifi.colors.tableScrollHandle + color: hifi.colors.tableScrollHandleDark } } @@ -107,7 +106,7 @@ TableView { margins: 1 // Shrink } radius: 4 - color: hifi.colors.tableScrollBackground + color: hifi.colors.tableScrollBackgroundDark } } diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index de6950c07e..2a0fe545ef 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -17,20 +17,54 @@ import "../styles-uit" TableView { id: tableView - property var tableModel: ListModel { } property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light + property bool expandSelectedRow: false - model: tableModel - - TableViewColumn { - role: "name" - } - - anchors { left: parent.left; right: parent.right } + model: ListModel { } headerVisible: false - headerDelegate: Item { } // Fix OSX QML bug that displays scrollbar starting too low. + headerDelegate: Rectangle { + height: hifi.dimensions.tableHeaderHeight + color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + + RalewayRegular { + text: styleData.value + size: hifi.fontSizes.tableHeading + font.capitalization: Font.AllUppercase + color: hifi.colors.baseGrayHighlight + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: 1 + anchors { + left: parent.left + top: parent.top + topMargin: 1 + bottom: parent.bottom + bottomMargin: 2 + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + visible: styleData.column > 0 + } + + Rectangle { + height: 1 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + } + } // Use rectangle to draw border with rounded corners. frameVisible: false @@ -50,8 +84,10 @@ TableView { style: TableViewStyle { // Needed in order for rows to keep displaying rows after end of table entries. - backgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightEven : hifi.colors.tableRowDarkEven - alternateBackgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + backgroundColor: tableView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + alternateBackgroundColor: tableView.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + + padding.top: headerVisible ? hifi.dimensions.tableHeaderHeight: 0 handle: Item { id: scrollbarHandle @@ -59,33 +95,38 @@ TableView { Rectangle { anchors { fill: parent + topMargin: 3 + bottomMargin: 3 // "" leftMargin: 2 // Move it right rightMargin: -2 // "" - topMargin: 3 // Shrink vertically - bottomMargin: 3 // "" } radius: 3 - color: hifi.colors.tableScrollHandle + color: isLightColorScheme ? hifi.colors.tableScrollHandleLight : hifi.colors.tableScrollHandleDark } } scrollBarBackground: Item { - implicitWidth: 10 + implicitWidth: 9 Rectangle { anchors { fill: parent margins: -1 // Expand + topMargin: headerVisible ? -hifi.dimensions.tableHeaderHeight : -1 } - color: hifi.colors.baseGrayHighlight - } + color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark - Rectangle { - anchors { - fill: parent - margins: 1 // Shrink + Rectangle { + // Extend header bottom border + anchors { + top: parent.top + topMargin: hifi.dimensions.tableHeaderHeight - 1 + left: parent.left + right: parent.right + } + height: 1 + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + visible: headerVisible } - radius: 4 - color: hifi.colors.tableScrollBackground } } @@ -99,85 +140,11 @@ TableView { } rowDelegate: Rectangle { - height: (styleData.selected ? 1.8 : 1) * hifi.dimensions.tableRowHeight + height: (styleData.selected && expandSelectedRow ? 1.8 : 1) * hifi.dimensions.tableRowHeight color: styleData.selected ? hifi.colors.primaryHighlight : tableView.isLightColorScheme ? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) : (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd) } - - itemDelegate: Item { - anchors { - left: parent ? parent.left : undefined - leftMargin: hifi.dimensions.tablePadding - right: parent ? parent.right : undefined - rightMargin: hifi.dimensions.tablePadding - } - - FiraSansSemiBold { - id: textItem - text: styleData.value - size: hifi.fontSizes.tableText - color: colorScheme == hifi.colorSchemes.light - ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) - : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) - anchors { - left: parent.left - right: parent.right - top: parent.top - topMargin: 3 - } - - // FIXME: Put reload item in tableModel passed in from RunningScripts. - HiFiGlyphs { - id: reloadButton - text: hifi.glyphs.reloadSmall - color: reloadButtonArea.pressed ? hifi.colors.white : parent.color - anchors { - top: parent.top - right: stopButton.left - verticalCenter: parent.verticalCenter - } - MouseArea { - id: reloadButtonArea - anchors { fill: parent; margins: -2 } - onClicked: reloadScript(model.url) - } - } - - // FIXME: Put stop item in tableModel passed in from RunningScripts. - HiFiGlyphs { - id: stopButton - text: hifi.glyphs.closeSmall - color: stopButtonArea.pressed ? hifi.colors.white : parent.color - anchors { - top: parent.top - right: parent.right - verticalCenter: parent.verticalCenter - } - MouseArea { - id: stopButtonArea - anchors { fill: parent; margins: -2 } - onClicked: stopScript(model.url) - } - } - } - - // FIXME: Automatically use aux. information from tableModel - FiraSansSemiBold { - text: tableModel.get(styleData.row) ? tableModel.get(styleData.row).url : "" - elide: Text.ElideMiddle - size: hifi.fontSizes.tableText - color: colorScheme == hifi.colorSchemes.light - ? (styleData.selected ? hifi.colors.black : hifi.colors.lightGray) - : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) - anchors { - top: textItem.bottom - left: parent.left - right: parent.right - } - visible: styleData.selected - } - } } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 1f710b4ef5..ac0858c134 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -28,6 +28,7 @@ ModalWindow { //resizable: true implicitWidth: 640 implicitHeight: 480 + HifiConstants { id: hifi } Settings { @@ -184,8 +185,9 @@ ModalWindow { } } - FileTableView { + Table { id: fileTableView + colorScheme: hifi.colorSchemes.light anchors { top: navControls.bottom topMargin: hifi.dimensions.contentSpacing.y @@ -194,10 +196,12 @@ ModalWindow { bottom: currentSelection.top bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } + headerVisible: true onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); + model: FolderListModel { id: model nameFilters: selectionType.currentFilter @@ -218,6 +222,76 @@ ModalWindow { } } + onActiveFocusChanged: { + if (activeFocus && currentRow == -1) { + fileTableView.selection.select(0) + } + } + + itemDelegate: Item { + clip: true + + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + + FiraSansSemiBold { + text: getText(); + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + font.family: fileTableView.model.get(styleData.row, "fileIsDir") ? firaSansSemiBold.name : firaSansRegular.name + + function getText() { + switch (styleData.column) { + case 1: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); + default: return styleData.value; + } + } + function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; + } + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: 0.5 * fileTableView.width + resizable: true + } + TableViewColumn { + id: fileMofifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + resizable: true + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + resizable: true + } + function navigateToRow(row) { currentRow = row; navigateToCurrentRow(); diff --git a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml b/interface/resources/qml/dialogs/fileDialog/FileTableView.qml deleted file mode 100644 index d51f9e1ed9..0000000000 --- a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml +++ /dev/null @@ -1,79 +0,0 @@ -// -// FileDialog.qml -// -// Created by Bradley Austin Davis on 22 Jan 2016 -// 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 -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -TableView { - id: root - onActiveFocusChanged: { - if (activeFocus && currentRow == -1) { - root.selection.select(0) - } - } - //horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - itemDelegate: Component { - Item { - clip: true - Text { - x: 3 - anchors.verticalCenter: parent.verticalCenter - color: styleData.textColor - elide: styleData.elideMode - text: getText(); - font.italic: root.model.get(styleData.row, "fileIsDir") ? true : false - - function getText() { - switch (styleData.column) { - //case 1: return Date.fromLocaleString(locale, styleData.value, "yyyy-MM-dd hh:mm:ss"); - case 1: return root.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; - case 2: return root.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); - default: return styleData.value; - } - } - function formatSize(size) { - var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; - var suffixIndex = 0 - while ((size / 1024.0) > 1.1) { - size /= 1024.0; - ++suffixIndex; - } - - size = Math.round(size*1000)/1000; - size = size.toLocaleString() - - return size + " " + suffixes[suffixIndex]; - } - } - } - } - - TableViewColumn { - id: fileNameColumn - role: "fileName" - title: "Name" - width: Math.floor(0.55 * parent.width) - resizable: true - } - TableViewColumn { - id: fileMofifiedColumn - role: "fileModified" - title: "Date" - width: Math.floor(0.3 * parent.width) - resizable: true - } - TableViewColumn { - role: "fileSize" - title: "Size" - width: Math.floor(0.15 * parent.width) - 16 - 2 // Allow space for vertical scrollbar and borders - resizable: true - } -} diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index cb4d2157ca..4d5052f086 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -89,12 +89,16 @@ Item { readonly property color transparent: "#00ffffff" // Control specific colors - readonly property color tableRowLightOdd: white50 - readonly property color tableRowLightEven: "#1a575757" - readonly property color tableRowDarkOdd: "#80393939" - readonly property color tableRowDarkEven: "#a6181818" - readonly property color tableScrollHandle: "#707070" - readonly property color tableScrollBackground: "#323232" + readonly property color tableRowLightOdd: "#eaeaea" // Equivalent to white50 over #e3e3e3 background + readonly property color tableRowLightEven: "#c6c6c6" // Equivavlent to "#1a575757" over #e3e3e3 background + readonly property color tableRowDarkOdd: "#2e2e2e" // Equivalent to "#80393939" over #404040 background + readonly property color tableRowDarkEven: "#1c1c1c" // Equivalent to "#a6181818" over #404040 background + readonly property color tableBackgroundLight: tableRowLightEven + readonly property color tableBackgroundDark: tableRowDarkEven + readonly property color tableScrollHandleLight: tableRowLightOdd + readonly property color tableScrollHandleDark: "#707070" + readonly property color tableScrollBackgroundLight: tableRowLightEven + readonly property color tableScrollBackgroundDark: "#323232" readonly property color checkboxLightStart: "#ffffff" readonly property color checkboxLightFinish: "#afafaf" readonly property color checkboxDarkStart: "#7d7d7d" @@ -140,7 +144,7 @@ Item { readonly property real spinnerSize: 42 readonly property real tablePadding: 12 readonly property real tableRowHeight: largeScreen ? 26 : 23 - readonly property real tableHeaderHeight: 40 + readonly property real tableHeaderHeight: 29 readonly property vector2d modalDialogMargin: Qt.vector2d(50, 30) readonly property real modalDialogTitleHeight: 40 readonly property real controlLineHeight: 28 // Height of spinbox control on 1920 x 1080 monitor @@ -157,6 +161,7 @@ Item { readonly property real textFieldInput: dimensions.largeScreen ? 15 : 12 readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9 readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24 + readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 readonly property real tableText: dimensions.largeScreen ? 15 : 12 readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 From b6fcb77d6f76306ad2a4022c2430b7656e9583fc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:16:56 +1200 Subject: [PATCH 0044/1237] Update Running Scripts list to use updated QML table control --- .../qml/hifi/dialogs/RunningScripts.qml | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 071789fe16..31bb553809 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -118,11 +118,89 @@ Window { } HifiControls.Table { - tableModel: runningScriptsModel + model: runningScriptsModel + id: table height: 185 colorScheme: hifi.colorSchemes.dark anchors.left: parent.left anchors.right: parent.right + expandSelectedRow: true + + itemDelegate: Item { + anchors { + left: parent ? parent.left : undefined + leftMargin: hifi.dimensions.tablePadding + right: parent ? parent.right : undefined + rightMargin: hifi.dimensions.tablePadding + } + + FiraSansSemiBold { + id: textItem + text: styleData.value + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 3 + } + + HiFiGlyphs { + id: reloadButton + text: hifi.glyphs.reloadSmall + color: reloadButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: stopButton.left + verticalCenter: parent.verticalCenter + } + MouseArea { + id: reloadButtonArea + anchors { fill: parent; margins: -2 } + onClicked: reloadScript(model.url) + } + } + + HiFiGlyphs { + id: stopButton + text: hifi.glyphs.closeSmall + color: stopButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: parent.right + verticalCenter: parent.verticalCenter + } + MouseArea { + id: stopButtonArea + anchors { fill: parent; margins: -2 } + onClicked: stopScript(model.url) + } + } + + } + + FiraSansSemiBold { + text: runningScriptsModel.get(styleData.row) ? runningScriptsModel.get(styleData.row).url : "" + elide: Text.ElideMiddle + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.lightGray) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + top: textItem.bottom + left: parent.left + right: parent.right + } + visible: styleData.selected + } + } + + TableViewColumn { + role: "name" + } } HifiControls.VerticalSpacer { From d798e562fc7b101c836e0ef74f711b09c507f5a9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:18:30 +1200 Subject: [PATCH 0045/1237] Update scrollbar in tree view to match table scrollbar Used in Running Scripts and Asset Browser dialogs. --- interface/resources/qml/controls-uit/Tree.qml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controls-uit/Tree.qml index 6d4d202e2c..aa1d10f030 100644 --- a/interface/resources/qml/controls-uit/Tree.qml +++ b/interface/resources/qml/controls-uit/Tree.qml @@ -1,5 +1,5 @@ // -// Table.qml +// Tree.qml // // Created by David Rowe on 17 Feb 2016 // Copyright 2016 High Fidelity, Inc. @@ -85,27 +85,18 @@ TreeView { bottomMargin: 3 // "" } radius: 3 - color: hifi.colors.tableScrollHandle + color: hifi.colors.tableScrollHandleDark } } scrollBarBackground: Item { - implicitWidth: 10 + implicitWidth: 9 Rectangle { anchors { fill: parent margins: -1 // Expand } - color: hifi.colors.baseGrayHighlight - } - - Rectangle { - anchors { - fill: parent - margins: 1 // Shrink - } - radius: 4 - color: hifi.colors.tableScrollBackground + color: hifi.colors.tableBackgroundDark } } From 7be33a85848fe9434201ff2ece0602604ae788a0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:25:44 +1200 Subject: [PATCH 0046/1237] Don't show header or dates and sizes when choosing a directory --- interface/resources/qml/dialogs/FileDialog.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ac0858c134..93c89e7393 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -196,7 +196,7 @@ ModalWindow { bottom: currentSelection.top bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } - headerVisible: true + headerVisible: !selectDirectory onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); @@ -275,7 +275,7 @@ ModalWindow { id: fileNameColumn role: "fileName" title: "Name" - width: 0.5 * fileTableView.width + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width resizable: true } TableViewColumn { @@ -284,12 +284,14 @@ ModalWindow { title: "Date" width: 0.3 * fileTableView.width resizable: true + visible: !selectDirectory } TableViewColumn { role: "fileSize" title: "Size" width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width resizable: true + visible: !selectDirectory } function navigateToRow(row) { From 30fb9349c46727a5700b4ca9e54a83eaa7767198 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 12 May 2016 17:52:40 -0700 Subject: [PATCH 0047/1237] dry up some code --- .../src/RenderableModelEntityItem.cpp | 50 +++++++++---------- .../src/RenderableModelEntityItem.h | 2 + 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c4ac9b09e5..a9aee1e527 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -162,6 +162,19 @@ void RenderableModelEntityItem::remapTextures() { } } +void RenderableModelEntityItem::doInitialModelSimulation() { + _model->setScaleToFit(true, getDimensions()); + _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); + _model->setRotation(getRotation()); + _model->setTranslation(getPosition()); + { + PerformanceTimer perfTimer("_model->simulate"); + _model->simulate(0.0f); + } + _needsInitialSimulation = false; +} + + // TODO: we need a solution for changes to the postion/rotation/etc of a model... // this current code path only addresses that in this setup case... not the changing/moving case bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) { @@ -172,22 +185,12 @@ bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) { getModel(renderer); } if (renderArgs && _model && _needsInitialSimulation && _model->isActive() && _model->isLoaded()) { - _model->setScaleToFit(true, getDimensions()); - _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); - _model->setRotation(getRotation()); - _model->setTranslation(getPosition()); - // make sure to simulate so everything gets set up correctly for rendering - { - PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - } - _needsInitialSimulation = false; - + doInitialModelSimulation(); _model->renderSetup(renderArgs); } bool ready = !_needsInitialSimulation && _model && _model->readyToAddToScene(renderArgs); - return ready; + return ready; } class RenderableModelEntityItemMeta { @@ -348,18 +351,7 @@ void RenderableModelEntityItem::updateModelBounds() { _model->getRotation() != getRotation() || _model->getRegistrationPoint() != getRegistrationPoint()) && _model->isActive() && _dimensionsInitialized) { - _model->setScaleToFit(true, dimensions); - _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); - _model->setRotation(getRotation()); - _model->setTranslation(getPosition()); - - // make sure to simulate so everything gets set up correctly for rendering - { - PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - } - - _needsInitialSimulation = false; + doInitialModelSimulation(); _needsJointSimulation = false; } } @@ -592,8 +584,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { if (_needsInitialSimulation) { // the _model's offset will be wrong until _needsInitialSimulation is false PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - _needsInitialSimulation = false; + doInitialModelSimulation(); } return true; @@ -807,6 +798,13 @@ void RenderableModelEntityItem::locationChanged(bool tellPhysics) { if (_model && _model->isActive()) { _model->setRotation(getRotation()); _model->setTranslation(getPosition()); + + // { + // render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + // render::PendingChanges pendingChanges; + // pendingChanges.updateItem(_myMetaItem, [](RenderableModelEntityItemMeta& data){}); + // scene->enqueuePendingChanges(pendingChanges); + // } } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 59208d209d..5fd767c6ee 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -38,6 +38,8 @@ public: EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) override; + void doInitialModelSimulation(); + virtual bool readyToAddToScene(RenderArgs* renderArgs = nullptr); virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; From 3b34b908d7f066635b055baa6c32dc9f78eb5c78 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 13 May 2016 17:39:15 +1200 Subject: [PATCH 0048/1237] Tweak level-up and home buttons --- interface/resources/qml/controls-uit/GlyphButton.qml | 7 +++++++ interface/resources/qml/dialogs/FileDialog.qml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml index 2625dda723..ac353b5a52 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -19,6 +19,7 @@ Original.Button { property int color: 0 property int colorScheme: hifi.colorSchemes.light property string glyph: "" + property int size: 32 width: 120 height: 28 @@ -65,7 +66,13 @@ Original.Button { : hifi.buttons.disabledTextColor[control.colorScheme] verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + anchors { + // Tweak horizontal alignment so that it looks right. + left: parent.left + leftMargin: -0.5 + } text: control.glyph + size: control.size } } } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 93c89e7393..1175c8a14f 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -99,6 +99,7 @@ ModalWindow { id: upButton glyph: hifi.glyphs.levelUp width: height + size: 30 enabled: model.parentFolder && model.parentFolder !== "" onClicked: d.navigateUp(); } @@ -107,6 +108,7 @@ ModalWindow { id: homeButton property var destination: helper.home(); glyph: hifi.glyphs.home + size: 28 width: height enabled: d.homeDestination ? true : false onClicked: d.navigateHome(); From efe02c0fa120666b5296d0e559e272b67b2fb412 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 13 May 2016 10:57:41 -0700 Subject: [PATCH 0049/1237] make lock/unlock button easier to see --- scripts/system/assets/images/lock.svg | 32 ++++++++++++++++++----- scripts/system/assets/images/unlock.svg | 30 +++++++++++++++++---- scripts/system/attachedEntitiesManager.js | 4 +-- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/scripts/system/assets/images/lock.svg b/scripts/system/assets/images/lock.svg index 1480359f43..bb9658de00 100644 --- a/scripts/system/assets/images/lock.svg +++ b/scripts/system/assets/images/lock.svg @@ -2,11 +2,13 @@ + id="defs4"> + + + + + image/svg+xml - + @@ -54,15 +74,15 @@ id="layer1" transform="translate(0,-825.59055)"> diff --git a/scripts/system/assets/images/unlock.svg b/scripts/system/assets/images/unlock.svg index a7664c1f59..789a8b0ed5 100644 --- a/scripts/system/assets/images/unlock.svg +++ b/scripts/system/assets/images/unlock.svg @@ -2,11 +2,13 @@ + id="defs4"> + + + + + image/svg+xml - + @@ -54,14 +74,14 @@ id="layer1" transform="translate(0,-825.59055)"> Date: Fri, 13 May 2016 11:01:15 -0700 Subject: [PATCH 0050/1237] Trying to support ambient occlusion map from blender --- libraries/fbx/src/FBXReader.cpp | 7 ++++++- libraries/fbx/src/FBXReader.h | 2 ++ libraries/fbx/src/FBXReader_Material.cpp | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 2df388e1d4..5a57a3c6d8 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -924,6 +924,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // material.emissiveColor = getVec3(property.properties, index); // material.emissiveFactor = 1.0; + } else if (property.properties.at(0) == "AmbientFactor") { + material.ambientFactor = property.properties.at(index).value(); + // Detected just for BLender AO vs lightmap } else if (property.properties.at(0) == "Shininess") { material.shininess = property.properties.at(index).value(); @@ -1126,8 +1129,10 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS emissiveTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_emissive_map")) { emissiveTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); - } else if (type.contains("ambient")) { + } else if (type.contains("ambientcolor")) { ambientTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + } else if (type.contains("ambientfactor")) { + ambientFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_ao_map")) { occlusionTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index c1952fc550..9e960126b5 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -151,6 +151,7 @@ public: float metallic{ 0.0f }; float roughness{ 1.0f }; float emissiveIntensity{ 1.0f }; + float ambientFactor{ 1.0f }; QString materialID; QString name; @@ -436,6 +437,7 @@ public: QHash shininessTextures; QHash emissiveTextures; QHash ambientTextures; + QHash ambientFactorTextures; QHash occlusionTextures; QHash _fbxMaterials; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index 11c6dad2f2..3503fe1054 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -175,6 +175,14 @@ void FBXReader::consolidateFBXMaterials() { FBXTexture occlusionTexture; QString occlusionTextureID = occlusionTextures.value(material.materialID); + if (occlusionTextureID.isNull()) { + // 2nd chance + // For blender we use the ambient factor texture ONLY if the ambientFactor value is set to 0 + if (material.ambientFactor == 0.0) { + occlusionTextureID = ambientFactorTextures.value(material.materialID); + } + } + if (!occlusionTextureID.isNull()) { occlusionTexture = getTexture(occlusionTextureID); detectDifferentUVs |= (occlusionTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); @@ -187,6 +195,14 @@ void FBXReader::consolidateFBXMaterials() { FBXTexture ambientTexture; QString ambientTextureID = ambientTextures.value(material.materialID); + if (ambientTextureID.isNull()) { + // 2nd chance + // For blender we use the ambient factor texture ONLY if the ambientFactor value is set to 1 + if (material.ambientFactor == 1.0) { + ambientTextureID = ambientFactorTextures.value(material.materialID); + } + } + if (_loadLightmaps && !ambientTextureID.isNull()) { ambientTexture = getTexture(ambientTextureID); detectDifferentUVs |= (ambientTexture.texcoordSet != 0) || (!ambientTexture.transform.isIdentity()); From 1c06c1a6ab9915b255812d994c1c88886d454a4f Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 13 May 2016 11:09:53 -0700 Subject: [PATCH 0051/1237] Cleaning up the pr from tthe transform.h file --- libraries/gpu/src/gpu/Transform.h | 36 ------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 libraries/gpu/src/gpu/Transform.h diff --git a/libraries/gpu/src/gpu/Transform.h b/libraries/gpu/src/gpu/Transform.h deleted file mode 100644 index 82974964a8..0000000000 --- a/libraries/gpu/src/gpu/Transform.h +++ /dev/null @@ -1,36 +0,0 @@ -// -// Transform.h -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 06/12/2016. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_gpu_Transform_h -#define hifi_gpu_Transform_h - -#include - -#include - -#include "Resource.h" - -namespace gpu { - -class TransformBuffer { -public: - - TransformBuffer() {} - ~TransformBuffer() {} - -protected: - BufferPointer _buffer; -}; -typedef std::shared_ptr TransformBufferPointer; - -}; - - -#endif From 1f664671f91070e868f4a29b12835b588670d200 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 13 May 2016 14:32:54 -0700 Subject: [PATCH 0052/1237] Remove overlay when velocity first indicates travel, and restore it (with reset) when returning to reset. --- interface/src/ui/OverlayConductor.cpp | 48 ++++++++++++++++++++++----- interface/src/ui/OverlayConductor.h | 4 ++- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 95054869e5..543a500188 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -67,6 +67,32 @@ void OverlayConductor::update(float dt) { } void OverlayConductor::updateMode() { + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + float speed = glm::length(myAvatar->getVelocity()); + bool nowDriving = _driving; + const float MIN_DRIVING = 0.2; + const float MAX_NOT_DRIVING = 0.01; + if (speed > MIN_DRIVING) { + nowDriving = true; + } + else if (speed < MAX_NOT_DRIVING) { + nowDriving = false; + } + if (nowDriving != _driving) { + if (nowDriving) { + _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); + } + else { // reset when coming out of driving + _mode = FLAT; // Seems appropriate to let things reset, below, after the following. + // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. + qApp->getActiveDisplayPlugin()->resetSensors(); + } + if (_wantsOverlays) { + qDebug() << "flipping" << !nowDriving; + setEnabled(!nowDriving, false); + } + _driving = nowDriving; + } Mode newMode; if (qApp->isHMDMode()) { @@ -84,10 +110,9 @@ void OverlayConductor::updateMode() { qApp->getApplicationCompositor().setModelTransform(identity); break; } - case STANDING: { + case STANDING: { // STANDING mode is not currently used. // enter the STANDING state // place the overlay at the current hmd position in world space - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); Transform t; t.setTranslation(extractTranslation(camMat)); @@ -103,15 +128,18 @@ void OverlayConductor::updateMode() { } _mode = newMode; + } -void OverlayConductor::setEnabled(bool enabled) { +void OverlayConductor::setEnabled(bool enabled, bool toggleQmlEvents) { if (enabled == _enabled) { return; } - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, enabled); + if (toggleQmlEvents) { // Could recurse on us with the wrong toggleQmlEvents flag, and not need in the !toggleQmlEvent case anyway. + Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, enabled); + } _enabled = enabled; // set the new value @@ -124,8 +152,10 @@ void OverlayConductor::setEnabled(bool enabled) { qApp->getOverlays().enable(); // enable QML events - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(true); + if (toggleQmlEvents) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->getRootItem()->setEnabled(true); + } if (_mode == STANDING) { // place the overlay at the current hmd position in world space @@ -144,8 +174,10 @@ void OverlayConductor::setEnabled(bool enabled) { qApp->getOverlays().disable(); // disable QML events - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(false); + if (toggleQmlEvents) { // I'd really rather always do this, but it looses drive state. bugzid:501 + auto offscreenUi = DependencyManager::get(); + offscreenUi->getRootItem()->setEnabled(false); + } } } diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index b94c5be7dd..02b2035b07 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -17,7 +17,7 @@ public: ~OverlayConductor(); void update(float dt); - void setEnabled(bool enable); + void setEnabled(bool enable, bool toggleQmlEvents = true); bool getEnabled() const; private: @@ -31,6 +31,8 @@ private: Mode _mode { FLAT }; bool _enabled { false }; + bool _driving { false }; + bool _wantsOverlays { true }; }; #endif From 654ed33dd4e870a6f1973ef6efe0b3b31b4b93dd Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 13 May 2016 15:32:59 -0700 Subject: [PATCH 0053/1237] Fix warnings. --- interface/src/ui/OverlayConductor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 543a500188..b44ea91a60 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -70,8 +70,8 @@ void OverlayConductor::updateMode() { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); float speed = glm::length(myAvatar->getVelocity()); bool nowDriving = _driving; - const float MIN_DRIVING = 0.2; - const float MAX_NOT_DRIVING = 0.01; + const float MIN_DRIVING = 0.2f; + const float MAX_NOT_DRIVING = 0.01f; if (speed > MIN_DRIVING) { nowDriving = true; } From c21087ad8726f754e215c8fabd7c0c899b9faeee Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 14 May 2016 13:04:18 +1200 Subject: [PATCH 0054/1237] Add icon to title --- interface/resources/qml/dialogs/FileDialog.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 1175c8a14f..a3c5c02f9b 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -49,6 +49,8 @@ ModalWindow { // Set from OffscreenUi::getOpenFile() property int options; // <-- FIXME unused + property string iconText: text !== "" ? hifi.glyphs.scriptUpload : "" + property int iconSize: 40 property bool selectDirectory: false; property bool showHidden: false; @@ -69,6 +71,8 @@ ModalWindow { drivesSelector.onCurrentTextChanged.connect(function(){ root.dir = helper.pathToUrl(drivesSelector.currentText); }) + + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } Item { From 14c733cd6e382fc4db961bc6c72eec5d937aa87d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 14 May 2016 17:19:18 +1200 Subject: [PATCH 0055/1237] Make columns sortable --- .../resources/qml/controls-uit/Table.qml | 18 +++++++++++++++-- .../resources/qml/dialogs/FileDialog.qml | 20 ++++++++++++++++++- .../qml/styles-uit/HifiConstants.qml | 1 + 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index 2a0fe545ef..35029ad8bf 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -29,6 +29,7 @@ TableView { color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark RalewayRegular { + id: titleText text: styleData.value size: hifi.fontSizes.tableHeading font.capitalization: Font.AllUppercase @@ -36,12 +37,25 @@ TableView { anchors { left: parent.left leftMargin: hifi.dimensions.tablePadding - right: parent.right - rightMargin: hifi.dimensions.tablePadding verticalCenter: parent.verticalCenter } } + HiFiGlyphs { + id: titleSort + text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn + color: hifi.colors.baseGrayHighlight + size: hifi.fontSizes.tableHeadingIcon + anchors { + left: titleText.right + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: titleText.verticalCenter + } + visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column + } + Rectangle { width: 1 anchors { diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index a3c5c02f9b..f185508457 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -208,6 +208,10 @@ ModalWindow { Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); + sortIndicatorColumn: 0 + sortIndicatorOrder: Qt.AscendingOrder + sortIndicatorVisible: true + model: FolderListModel { id: model nameFilters: selectionType.currentFilter @@ -228,7 +232,18 @@ ModalWindow { } } - onActiveFocusChanged: { + function updateSort() { + model.sortReversed = sortIndicatorColumn == 0 + ? (sortIndicatorOrder == Qt.DescendingOrder) + : (sortIndicatorOrder == Qt.AscendingOrder); // Date and size fields have opposite sense + model.sortField = sortIndicatorColumn + 1; + } + + onSortIndicatorColumnChanged: { updateSort(); } + + onSortIndicatorOrderChanged: { updateSort(); } + + onActiveFocusChanged: { if (activeFocus && currentRow == -1) { fileTableView.selection.select(0) } @@ -282,6 +297,7 @@ ModalWindow { role: "fileName" title: "Name" width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false resizable: true } TableViewColumn { @@ -289,6 +305,7 @@ ModalWindow { role: "fileModified" title: "Date" width: 0.3 * fileTableView.width + movable: false resizable: true visible: !selectDirectory } @@ -296,6 +313,7 @@ ModalWindow { role: "fileSize" title: "Size" width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + movable: false resizable: true visible: !selectDirectory } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 4d5052f086..640fe8625b 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -162,6 +162,7 @@ Item { readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9 readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24 readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 + readonly property real tableHeadingIcon: dimensions.largeScreen ? 40 : 33 readonly property real tableText: dimensions.largeScreen ? 15 : 12 readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 From 880b52aad288ec91d73fdfc478acbe57a110eb3e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 16 May 2016 09:52:48 -0700 Subject: [PATCH 0056/1237] update attachedEntitiesManager to work better with avatarEntities --- scripts/system/attachedEntitiesManager.js | 104 ++++++++++++---------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index 780cfe06fb..79c5973a6e 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -117,10 +117,11 @@ function AttachedEntitiesManager() { this.handleEntityRelease = function(grabbedEntity, releasedFromJoint) { // if this is still equipped, just rewrite the position information. var grabData = getEntityCustomData('grabKey', grabbedEntity, {}); - if ("refCount" in grabData && grabData.refCount > 0) { - manager.updateRelativeOffsets(grabbedEntity); - return; - } + // if ("refCount" in grabData && grabData.refCount > 0) { + // // for adjusting things in your other hand + // manager.updateRelativeOffsets(grabbedEntity); + // return; + // } var allowedJoints = getEntityCustomData('wearable', grabbedEntity, DEFAULT_WEARABLE_DATA).joints; @@ -156,9 +157,11 @@ function AttachedEntitiesManager() { wearProps.parentID = MyAvatar.sessionUUID; wearProps.parentJointIndex = bestJointIndex; + var updatePresets = false; if (bestJointOffset && bestJointOffset.constructor === Array) { if (!clothingLocked || bestJointOffset.length < 2) { - this.updateRelativeOffsets(grabbedEntity); + // we're unlocked or this thing didn't have a preset position, so update it + updatePresets = true; } else { // don't snap the entity to the preferred position if unlocked wearProps.localPosition = bestJointOffset[0]; @@ -167,7 +170,10 @@ function AttachedEntitiesManager() { } Entities.deleteEntity(grabbedEntity); - Entities.addEntity(wearProps, true); + grabbedEntity = Entities.addEntity(wearProps, true); + if (updatePresets) { + this.updateRelativeOffsets(grabbedEntity); + } } else if (props.parentID != NULL_UUID) { // drop the entity and set it to have no parent (not on the avatar), unless it's being equipped in a hand. if (props.parentID === MyAvatar.sessionUUID && @@ -225,20 +231,20 @@ function AttachedEntitiesManager() { } } - this.saveAttachedEntities = function() { - print("--- saving attached entities ---"); - saveData = []; - var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE); - for (i = 0; i < nearbyEntities.length; i++) { - var entityID = nearbyEntities[i]; - if (this.updateRelativeOffsets(entityID)) { - var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them - this.scrubProperties(props); - saveData.push(props); - } - } - Settings.setValue(ATTACHED_ENTITIES_SETTINGS_KEY, JSON.stringify(saveData)); - } + // this.saveAttachedEntities = function() { + // print("--- saving attached entities ---"); + // saveData = []; + // var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE); + // for (i = 0; i < nearbyEntities.length; i++) { + // var entityID = nearbyEntities[i]; + // if (this.updateRelativeOffsets(entityID)) { + // var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them + // this.scrubProperties(props); + // saveData.push(props); + // } + // } + // Settings.setValue(ATTACHED_ENTITIES_SETTINGS_KEY, JSON.stringify(saveData)); + // } this.scrubProperties = function(props) { var toScrub = ["queryAACube", "position", "rotation", @@ -262,37 +268,37 @@ function AttachedEntitiesManager() { } } - this.loadAttachedEntities = function(grabbedEntity) { - print("--- loading attached entities ---"); - jsonAttachmentData = Settings.getValue(ATTACHED_ENTITIES_SETTINGS_KEY); - var loadData = []; - try { - loadData = JSON.parse(jsonAttachmentData); - } catch (e) { - print('error parsing saved attachment data'); - return; - } + // this.loadAttachedEntities = function(grabbedEntity) { + // print("--- loading attached entities ---"); + // jsonAttachmentData = Settings.getValue(ATTACHED_ENTITIES_SETTINGS_KEY); + // var loadData = []; + // try { + // loadData = JSON.parse(jsonAttachmentData); + // } catch (e) { + // print('error parsing saved attachment data'); + // return; + // } - for (i = 0; i < loadData.length; i ++) { - var savedProps = loadData[ i ]; - var currentProps = Entities.getEntityProperties(savedProps.id); - if (currentProps.id == savedProps.id && - // TODO -- also check that parentJointIndex matches? - currentProps.parentID == MyAvatar.sessionUUID) { - // entity is already in-world. TODO -- patch it up? - continue; - } - this.scrubProperties(savedProps); - delete savedProps["id"]; - savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions - var loadedEntityID = Entities.addEntity(savedProps, true); + // for (i = 0; i < loadData.length; i ++) { + // var savedProps = loadData[ i ]; + // var currentProps = Entities.getEntityProperties(savedProps.id); + // if (currentProps.id == savedProps.id && + // // TODO -- also check that parentJointIndex matches? + // currentProps.parentID == MyAvatar.sessionUUID) { + // // entity is already in-world. TODO -- patch it up? + // continue; + // } + // this.scrubProperties(savedProps); + // delete savedProps["id"]; + // savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions + // var loadedEntityID = Entities.addEntity(savedProps, true); - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'loaded', - grabbedEntity: loadedEntityID - })); - } - } + // Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + // action: 'loaded', + // grabbedEntity: loadedEntityID + // })); + // } + // } } var manager = new AttachedEntitiesManager(); From c1fa096e78847f9aef8ef95bd730105f5188bb66 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 16 May 2016 15:42:48 -0700 Subject: [PATCH 0057/1237] adding texcoord 1 on all the material/model and go fetch occlusion map with this uv instead of texcoor0. this allow for separate transforms just for occlusoin map --- libraries/fbx/src/FBXReader_Material.cpp | 8 ++++---- .../src/model-networking/ModelCache.cpp | 4 ++++ libraries/model/src/model/Material.cpp | 4 ++++ libraries/render-utils/src/MaterialTextures.slh | 16 +++++++++------- libraries/render-utils/src/model.slf | 4 +++- libraries/render-utils/src/model.slv | 2 ++ libraries/render-utils/src/model_lightmap.slf | 4 ++-- .../src/model_lightmap_normal_map.slf | 4 ++-- .../src/model_lightmap_normal_specular_map.slf | 4 ++-- .../src/model_lightmap_specular_map.slf | 4 ++-- libraries/render-utils/src/model_normal_map.slf | 4 +++- .../src/model_normal_specular_map.slf | 4 +++- .../render-utils/src/model_specular_map.slf | 4 +++- libraries/render-utils/src/model_translucent.slf | 6 ++++-- .../render-utils/src/model_translucent_unlit.slf | 2 +- libraries/render-utils/src/model_unlit.slf | 4 ++-- libraries/render-utils/src/skin_model.slv | 2 ++ .../render-utils/src/skin_model_normal_map.slv | 2 ++ 18 files changed, 54 insertions(+), 28 deletions(-) diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index e539f849c7..01878d6ccf 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -188,8 +188,8 @@ void FBXReader::consolidateFBXMaterials() { QString occlusionTextureID = occlusionTextures.value(material.materialID); if (occlusionTextureID.isNull()) { // 2nd chance - // For blender we use the ambient factor texture ONLY if the ambientFactor value is set to 0 - if (material.ambientFactor == 0.0) { + // For blender we use the ambient factor texture as AOMap ONLY if the ambientFactor value is > 0.0 + if (material.ambientFactor > 0.0f) { occlusionTextureID = ambientFactorTextures.value(material.materialID); } } @@ -208,8 +208,8 @@ void FBXReader::consolidateFBXMaterials() { QString ambientTextureID = ambientTextures.value(material.materialID); if (ambientTextureID.isNull()) { // 2nd chance - // For blender we use the ambient factor texture ONLY if the ambientFactor value is set to 1 - if (material.ambientFactor == 1.0) { + // For blender we use the ambient factor texture as Lightmap ONLY if the ambientFactor value is set to 0 + if (material.ambientFactor == 0.0f) { ambientTextureID = ambientFactorTextures.value(material.materialID); } } diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 8cd6d9b65e..40388e6123 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -418,6 +418,8 @@ model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, c auto map = std::make_shared(); map->setTextureSource(texture->_textureSource); + map->setTextureTransform(fbxTexture.transform); + return map; } @@ -427,6 +429,7 @@ model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, Textu auto map = std::make_shared(); map->setTextureSource(texture->_textureSource); + return map; } @@ -475,6 +478,7 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur if (!material.occlusionTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, NetworkTexture::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + map->setTextureTransform(material.occlusionTexture.transform); setTextureMap(MapChannel::OCCLUSION_MAP, map); } diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index 53478be536..dbe3cabdeb 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -122,6 +122,10 @@ void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textur _texMapArrayBuffer.edit()._texcoordTransforms[0] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); } + if (channel == MaterialKey::OCCLUSION_MAP) { + _texMapArrayBuffer.edit()._texcoordTransforms[1] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); + } + if (channel == MaterialKey::LIGHTMAP_MAP) { // update the texcoord1 with lightmap _texMapArrayBuffer.edit()._texcoordTransforms[1] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index f9b1c76104..5cede16e13 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -90,7 +90,7 @@ float fetchOcclusionMap(vec2 uv) { <@endfunc@> -<@func fetchMaterialTextures(matKey, texcoord0, albedo, roughness, normal, metallic, emissive, occlusion)@> +<@func fetchMaterialTexturesCoord0(matKey, texcoord0, albedo, roughness, normal, metallic, emissive)@> <@if albedo@> vec4 <$albedo$> = (((<$matKey$> & (ALBEDO_MAP_BIT | OPACITY_MASK_MAP_BIT | OPACITY_TRANSLUCENT_MAP_BIT)) != 0) ? fetchAlbedoMap(<$texcoord0$>) : vec4(1.0)); <@endif@> @@ -106,12 +106,19 @@ float fetchOcclusionMap(vec2 uv) { <@if emissive@> vec3 <$emissive$> = (((<$matKey$> & EMISSIVE_MAP_BIT) != 0) ? fetchEmissiveMap(<$texcoord0$>) : vec3(0.0)); <@endif@> +<@endfunc@> + +<@func fetchMaterialTexturesCoord1(matKey, texcoord1, occlusion, lightmapVal)@> <@if occlusion@> - float <$occlusion$> = (((<$matKey$> & OCCLUSION_MAP_BIT) != 0) ? fetchOcclusionMap(<$texcoord0$>) : 1.0); + float <$occlusion$> = (((<$matKey$> & OCCLUSION_MAP_BIT) != 0) ? fetchOcclusionMap(<$texcoord1$>) : 1.0); +<@endif@> +<@if lightmapVal@> + vec3 <$lightmapVal$> = fetchLightmapMap(<$texcoord1$>); <@endif@> <@endfunc@> + <@func declareMaterialLightmap()@> <$declareMaterialTexMapArrayBuffer()$> @@ -123,11 +130,6 @@ vec3 fetchLightmapMap(vec2 uv) { } <@endfunc@> -<@func fetchMaterialLightmap(texcoord1, lightmapVal)@> - vec3 <$lightmapVal$> = fetchLightmapMap(<$texcoord1$>); -<@endfunc@> - - <@func tangentToViewSpace(fetchedNormal, interpolatedNormal, interpolatedTangent, normal)@> { vec3 normalizedNormal = normalize(<$interpolatedNormal$>.xyz); diff --git a/libraries/render-utils/src/model.slf b/libraries/render-utils/src/model.slf index f1dcc942c9..c1f5cb1f88 100755 --- a/libraries/render-utils/src/model.slf +++ b/libraries/render-utils/src/model.slf @@ -22,12 +22,14 @@ in vec4 _position; in vec3 _normal; in vec3 _color; in vec2 _texCoord0; +in vec2 _texCoord1; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex, occlusionTex)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> float opacity = 1.0; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; diff --git a/libraries/render-utils/src/model.slv b/libraries/render-utils/src/model.slv index f1989dcf76..4d36673123 100755 --- a/libraries/render-utils/src/model.slv +++ b/libraries/render-utils/src/model.slv @@ -22,6 +22,7 @@ out vec3 _color; out float _alpha; out vec2 _texCoord0; +out vec2 _texCoord1; out vec4 _position; out vec3 _normal; @@ -31,6 +32,7 @@ void main(void) { TexMapArray texMapArray = getTexMapArray(); <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> // standard transform TransformCamera cam = getTransformCamera(); diff --git a/libraries/render-utils/src/model_lightmap.slf b/libraries/render-utils/src/model_lightmap.slf index 3afbbfd405..3a8cfde290 100755 --- a/libraries/render-utils/src/model_lightmap.slf +++ b/libraries/render-utils/src/model_lightmap.slf @@ -29,8 +29,8 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedo, roughness)$> - <$fetchMaterialLightmap(_texCoord1, lightmapVal)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedo, roughness)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> packDeferredFragmentLightmap( diff --git a/libraries/render-utils/src/model_lightmap_normal_map.slf b/libraries/render-utils/src/model_lightmap_normal_map.slf index 9ccc6e5352..64c61e255d 100755 --- a/libraries/render-utils/src/model_lightmap_normal_map.slf +++ b/libraries/render-utils/src/model_lightmap_normal_map.slf @@ -30,8 +30,8 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedo, roughness, normalTexel)$> - <$fetchMaterialLightmap(_texCoord1, lightmapVal)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedo, roughness, normalTexel)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> vec3 viewNormal; <$tangentToViewSpace(normalTexel, _normal, _tangent, viewNormal)$> diff --git a/libraries/render-utils/src/model_lightmap_normal_specular_map.slf b/libraries/render-utils/src/model_lightmap_normal_specular_map.slf index 71909a789f..34a116eac1 100755 --- a/libraries/render-utils/src/model_lightmap_normal_specular_map.slf +++ b/libraries/render-utils/src/model_lightmap_normal_specular_map.slf @@ -30,8 +30,8 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedo, roughness, normalTexel, metallicTex)$> - <$fetchMaterialLightmap(_texCoord1, lightmapVal)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedo, roughness, normalTexel, metallicTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> vec3 viewNormal; <$tangentToViewSpace(normalTexel, _normal, _tangent, viewNormal)$> diff --git a/libraries/render-utils/src/model_lightmap_specular_map.slf b/libraries/render-utils/src/model_lightmap_specular_map.slf index 5eefefdc29..4dbc10a834 100755 --- a/libraries/render-utils/src/model_lightmap_specular_map.slf +++ b/libraries/render-utils/src/model_lightmap_specular_map.slf @@ -29,8 +29,8 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedo, roughness, _SCRIBE_NULL, metallicTex)$> - <$fetchMaterialLightmap(_texCoord1, lightmapVal)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedo, roughness, _SCRIBE_NULL, metallicTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> packDeferredFragmentLightmap( normalize(_normal), diff --git a/libraries/render-utils/src/model_normal_map.slf b/libraries/render-utils/src/model_normal_map.slf index 519b41e17f..daaa1ed977 100755 --- a/libraries/render-utils/src/model_normal_map.slf +++ b/libraries/render-utils/src/model_normal_map.slf @@ -21,6 +21,7 @@ in vec4 _position; in vec2 _texCoord0; +in vec2 _texCoord1; in vec3 _normal; in vec3 _tangent; in vec3 _color; @@ -28,7 +29,8 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedoTex, roughnessTex, normalTex, _SCRIBE_NULL, emissiveTex, occlusionTex)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, normalTex, _SCRIBE_NULL, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> float opacity = 1.0; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; diff --git a/libraries/render-utils/src/model_normal_specular_map.slf b/libraries/render-utils/src/model_normal_specular_map.slf index 2529596818..dd2d3cc951 100755 --- a/libraries/render-utils/src/model_normal_specular_map.slf +++ b/libraries/render-utils/src/model_normal_specular_map.slf @@ -21,6 +21,7 @@ in vec4 _position; in vec2 _texCoord0; +in vec2 _texCoord1; in vec3 _normal; in vec3 _tangent; in vec3 _color; @@ -28,7 +29,8 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedoTex, roughnessTex, normalTex, metallicTex, emissiveTex, occlusionTex)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, normalTex, metallicTex, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> float opacity = 1.0; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)&>; diff --git a/libraries/render-utils/src/model_specular_map.slf b/libraries/render-utils/src/model_specular_map.slf index 3cbb060ab5..f0fe20293c 100755 --- a/libraries/render-utils/src/model_specular_map.slf +++ b/libraries/render-utils/src/model_specular_map.slf @@ -21,6 +21,7 @@ in vec4 _position; in vec2 _texCoord0; +in vec2 _texCoord1; in vec3 _normal; in vec3 _color; @@ -28,7 +29,8 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, metallicTex, emissiveTex, occlusionTex)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, metallicTex, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> float opacity = 1.0; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; diff --git a/libraries/render-utils/src/model_translucent.slf b/libraries/render-utils/src/model_translucent.slf index 27a22a9763..8f62a3a3e0 100755 --- a/libraries/render-utils/src/model_translucent.slf +++ b/libraries/render-utils/src/model_translucent.slf @@ -25,6 +25,7 @@ <$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$> in vec2 _texCoord0; +in vec2 _texCoord1; in vec4 _position; in vec3 _normal; in vec3 _color; @@ -35,7 +36,8 @@ out vec4 _fragColor; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex, occlusionTex)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; @@ -61,7 +63,7 @@ void main(void) { _fragColor = vec4(evalGlobalLightingAlphaBlended( cam._viewInverse, 1.0, - 1.0, + occlusionTex, fragPosition, fragNormal, albedo, diff --git a/libraries/render-utils/src/model_translucent_unlit.slf b/libraries/render-utils/src/model_translucent_unlit.slf index b9d6c64d6f..e2676636bf 100644 --- a/libraries/render-utils/src/model_translucent_unlit.slf +++ b/libraries/render-utils/src/model_translucent_unlit.slf @@ -26,7 +26,7 @@ out vec4 _fragColor; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedoTex, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex)$> float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; diff --git a/libraries/render-utils/src/model_unlit.slf b/libraries/render-utils/src/model_unlit.slf index 50778153fb..3c2e4ae2e0 100644 --- a/libraries/render-utils/src/model_unlit.slf +++ b/libraries/render-utils/src/model_unlit.slf @@ -16,7 +16,7 @@ <@include model/Material.slh@> <@include MaterialTextures.slh@> -<$declareMaterialTextures(ALBEDO, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL)$> +<$declareMaterialTextures(ALBEDO)$> in vec2 _texCoord0; in vec3 _normal; @@ -27,7 +27,7 @@ void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTextures(matKey, _texCoord0, albedoTex, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex)$> float opacity = 1.0; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; diff --git a/libraries/render-utils/src/skin_model.slv b/libraries/render-utils/src/skin_model.slv index c8501b8ddf..268b203e35 100755 --- a/libraries/render-utils/src/skin_model.slv +++ b/libraries/render-utils/src/skin_model.slv @@ -24,6 +24,7 @@ out vec4 _position; out vec2 _texCoord0; +out vec2 _texCoord1; out vec3 _normal; out vec3 _color; out float _alpha; @@ -40,6 +41,7 @@ void main(void) { TexMapArray texMapArray = getTexMapArray(); <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> // standard transform TransformCamera cam = getTransformCamera(); diff --git a/libraries/render-utils/src/skin_model_normal_map.slv b/libraries/render-utils/src/skin_model_normal_map.slv index db4b206405..05524385ef 100755 --- a/libraries/render-utils/src/skin_model_normal_map.slv +++ b/libraries/render-utils/src/skin_model_normal_map.slv @@ -24,6 +24,7 @@ out vec4 _position; out vec2 _texCoord0; +out vec2 _texCoord1; out vec3 _normal; out vec3 _tangent; out vec3 _color; @@ -42,6 +43,7 @@ void main(void) { TexMapArray texMapArray = getTexMapArray(); <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> interpolatedNormal = vec4(normalize(interpolatedNormal.xyz), 0.0); interpolatedTangent = vec4(normalize(interpolatedTangent.xyz), 0.0); From 1fc3d9229c9735e445533f678a1b7777df65de81 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 17 May 2016 10:51:42 +1200 Subject: [PATCH 0058/1237] Fix initial drive change and go-up not working properly on first screen --- interface/resources/qml/dialogs/FileDialog.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index f185508457..9c68767e2b 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -72,6 +72,12 @@ ModalWindow { root.dir = helper.pathToUrl(drivesSelector.currentText); }) + // HACK: The following two lines force the model to initialize properly such that: + // - Selecting a different drive at the initial screen updates the path displayed. + // - The go-up button works properly from the initial screen. + root.dir = helper.pathToUrl(drivesSelector.currentText); + root.dir = helper.pathToUrl(currentDirectory.lastValidFolder); + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } From a12e56f8802b0e52d9683a683499aaefb6be2b43 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 16 May 2016 17:43:54 -0700 Subject: [PATCH 0059/1237] Fix forgotten vertex shader case --- libraries/render-utils/src/model_normal_map.slv | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/render-utils/src/model_normal_map.slv b/libraries/render-utils/src/model_normal_map.slv index ded37923c2..e70d93f909 100755 --- a/libraries/render-utils/src/model_normal_map.slv +++ b/libraries/render-utils/src/model_normal_map.slv @@ -34,6 +34,7 @@ void main(void) { TexMapArray texMapArray = getTexMapArray(); <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> // standard transform TransformCamera cam = getTransformCamera(); From 55b0060df937c3d6d9a2b50073688cbb83c48662 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 12 May 2016 14:40:27 -0700 Subject: [PATCH 0060/1237] Convert JointData from relative frame to absolute. --- libraries/animation/src/Rig.cpp | 82 +++++++++++++------- libraries/animation/src/Rig.h | 12 +-- libraries/avatars/src/AvatarData.cpp | 6 +- libraries/networking/src/udt/PacketHeaders.h | 3 +- libraries/render-utils/src/Model.cpp | 4 - libraries/render-utils/src/Model.h | 4 - 6 files changed, 63 insertions(+), 48 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 67dfbec24a..378bd55667 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -165,6 +165,7 @@ void Rig::destroyAnimGraph() { void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset) { _geometryOffset = AnimPose(geometry.offset); + _invGeometryOffset = _geometryOffset.inverse(); setModelOffset(modelOffset); _animSkeleton = std::make_shared(geometry); @@ -193,6 +194,7 @@ void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOff void Rig::reset(const FBXGeometry& geometry) { _geometryOffset = AnimPose(geometry.offset); + _invGeometryOffset = _geometryOffset.inverse(); _animSkeleton = std::make_shared(geometry); _internalPoseSet._relativePoses.clear(); @@ -272,24 +274,6 @@ void Rig::setModelOffset(const glm::mat4& modelOffsetMat) { } } -bool Rig::getJointStateRotation(int index, glm::quat& rotation) const { - if (isIndexValid(index)) { - rotation = _internalPoseSet._relativePoses[index].rot; - return !isEqual(rotation, _animSkeleton->getRelativeDefaultPose(index).rot); - } else { - return false; - } -} - -bool Rig::getJointStateTranslation(int index, glm::vec3& translation) const { - if (isIndexValid(index)) { - translation = _internalPoseSet._relativePoses[index].trans; - return !isEqual(translation, _animSkeleton->getRelativeDefaultPose(index).trans); - } else { - return false; - } -} - void Rig::clearJointState(int index) { if (isIndexValid(index)) { _internalPoseSet._overrideFlags[index] = false; @@ -446,6 +430,23 @@ void Rig::calcAnimAlpha(float speed, const std::vector& referenceSpeeds, *alphaOut = alpha; } +bool Rig::getJointData(int index, JointData& jointDataOut) const { + if (isIndexValid(index)) { + jointDataOut.rotation = _internalPoseSet._absolutePoses[index].rot; + jointDataOut.rotationSet = !isEqual(jointDataOut.rotation, _animSkeleton->getAbsoluteDefaultPose(index).rot); + + // geometry offset is used here so that translations are in meters, this is what the avatar mixer expects + jointDataOut.translation = _geometryOffset * _internalPoseSet._absolutePoses[index].trans; + jointDataOut.translationSet = !isEqual(jointDataOut.translation, _animSkeleton->getAbsoluteDefaultPose(index).trans); + return true; + } else { + jointDataOut.translationSet = false; + jointDataOut.rotationSet = false; + return false; + } +} + + void Rig::setEnableInverseKinematics(bool enable) { _enableInverseKinematics = enable; } @@ -1232,21 +1233,44 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { jointDataVec.resize((int)getJointStateCount()); for (auto i = 0; i < jointDataVec.size(); i++) { JointData& data = jointDataVec[i]; - data.rotationSet |= getJointStateRotation(i, data.rotation); - // geometry offset is used here so that translations are in meters. - // this is what the avatar mixer expects - data.translationSet |= getJointStateTranslation(i, data.translation); - data.translation = _geometryOffset * data.translation; + getJointData(i, data); } } void Rig::copyJointsFromJointData(const QVector& jointDataVec) { - AnimPose invGeometryOffset = _geometryOffset.inverse(); - for (int i = 0; i < jointDataVec.size(); i++) { - const JointData& data = jointDataVec.at(i); - setJointRotation(i, data.rotationSet, data.rotation, 1.0f); - // geometry offset is used here to undo the fact that avatar mixer translations are in meters. - setJointTranslation(i, data.translationSet, invGeometryOffset * data.translation, 1.0f); + + if (_animSkeleton) { + + std::vector overrideFlags(_internalPoseSet._overridePoses.size(), false); + AnimPoseVec overridePoses = _animSkeleton->getAbsoluteDefaultPoses(); // start with the default poses. + + ASSERT(overrideFlags.size() == absoluteOverridePoses.size()); + + for (int i = 0; i < jointDataVec.size(); i++) { + if (isIndexValid(i)) { + const JointData& data = jointDataVec.at(i); + if (data.rotationSet) { + overrideFlags[i] = true; + overridePoses[i].rot = data.rotation; + } + if (data.translationSet) { + overrideFlags[i] = true; + // convert from meters back into geometry units. + overridePoses[i].trans = _invGeometryOffset * data.translation; + } + } + } + + ASSERT(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); + + _animSkeleton->convertAbsolutePosesToRelative(overridePoses); + + for (int i = 0; i < jointDataVec.size(); i++) { + if (overrideFlags[i]) { + _internalPoseSet._overrideFlags[i] = true; + _internalPoseSet._overridePoses[i] = overridePoses[i]; + } + } } } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 363006d48c..7446fddc47 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -104,12 +104,6 @@ public: void setModelOffset(const glm::mat4& modelOffsetMat); - // geometry space - bool getJointStateRotation(int index, glm::quat& rotation) const; - - // geometry space - bool getJointStateTranslation(int index, glm::vec3& translation) const; - void clearJointState(int index); void clearJointStates(); void clearJointAnimationPriority(int index); @@ -119,8 +113,6 @@ public: // geometry space void setJointTranslation(int index, bool valid, const glm::vec3& translation, float priority); - - // geometry space void setJointRotation(int index, bool valid, const glm::quat& rotation, float priority); // legacy @@ -237,8 +229,12 @@ protected: void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade); void calcAnimAlpha(float speed, const std::vector& referenceSpeeds, float* alphaOut) const; + // geometry space + bool getJointData(int index, JointData& jointDataOut) const; + AnimPose _modelOffset; // model to rig space AnimPose _geometryOffset; // geometry to model space (includes unit offset & fst offsets) + AnimPose _invGeometryOffset; struct PoseSet { AnimPoseVec _relativePoses; // geometry space relative to parent. diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9c556dc42b..784044da2e 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -121,6 +121,8 @@ void AvatarData::setHandPosition(const glm::vec3& handPosition) { _handPosition = glm::inverse(getOrientation()) * (handPosition - getPosition()); } +#define WANT_DEBUG + QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { // TODO: DRY this up to a shared method // that can pack any type given the number of bytes @@ -329,7 +331,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { } #ifdef WANT_DEBUG - if (sendAll) { + //if (sendAll) { qDebug() << "AvatarData::toByteArray" << cullSmallChanges << sendAll << "rotations:" << rotationSentCount << "translations:" << translationSentCount << "largest:" << maxTranslationDimension @@ -339,7 +341,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { << (beforeTranslations - beforeRotations) << "+" << (destinationBuffer - beforeTranslations) << "=" << (destinationBuffer - startPosition); - } + //} #endif return avatarDataByteArray.left(destinationBuffer - startPosition); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index b98a87e439..e0d854ab71 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -174,7 +174,8 @@ const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, - SoftAttachmentSupport + SoftAttachmentSupport, + AbsoluteFortyEightBitRotations }; #endif // hifi_PacketHeaders_h diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 2fe4427333..e4c414d30b 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -726,10 +726,6 @@ glm::vec3 Model::calculateScaledOffsetPoint(const glm::vec3& point) const { return translatedPoint; } -bool Model::getJointState(int index, glm::quat& rotation) const { - return _rig->getJointStateRotation(index, rotation); -} - void Model::clearJointState(int index) { _rig->clearJointState(index); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 581184918d..6a7c9ec560 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -252,10 +252,6 @@ protected: /// Returns the scaled equivalent of a point in model space. glm::vec3 calculateScaledOffsetPoint(const glm::vec3& point) const; - /// Fetches the joint state at the specified index. - /// \return whether or not the joint state is "valid" (that is, non-default) - bool getJointState(int index, glm::quat& rotation) const; - /// Clear the joint states void clearJointState(int index); From 36c175d4ccf22b5c64b22b4a10714e2f31bd147c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 12 May 2016 15:55:48 -0700 Subject: [PATCH 0061/1237] Ensure that JointData is in the absolute rig coordinate frame. --- libraries/animation/src/Rig.cpp | 42 ++++++++++++++++----------------- libraries/animation/src/Rig.h | 3 --- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 378bd55667..34c917da74 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -430,23 +430,6 @@ void Rig::calcAnimAlpha(float speed, const std::vector& referenceSpeeds, *alphaOut = alpha; } -bool Rig::getJointData(int index, JointData& jointDataOut) const { - if (isIndexValid(index)) { - jointDataOut.rotation = _internalPoseSet._absolutePoses[index].rot; - jointDataOut.rotationSet = !isEqual(jointDataOut.rotation, _animSkeleton->getAbsoluteDefaultPose(index).rot); - - // geometry offset is used here so that translations are in meters, this is what the avatar mixer expects - jointDataOut.translation = _geometryOffset * _internalPoseSet._absolutePoses[index].trans; - jointDataOut.translationSet = !isEqual(jointDataOut.translation, _animSkeleton->getAbsoluteDefaultPose(index).trans); - return true; - } else { - jointDataOut.translationSet = false; - jointDataOut.rotationSet = false; - return false; - } -} - - void Rig::setEnableInverseKinematics(bool enable) { _enableInverseKinematics = enable; } @@ -1230,10 +1213,22 @@ glm::mat4 Rig::getJointTransform(int jointIndex) const { } void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { + + const AnimPose geometryToRigPose(_geometryToRigTransform); + jointDataVec.resize((int)getJointStateCount()); for (auto i = 0; i < jointDataVec.size(); i++) { JointData& data = jointDataVec[i]; - getJointData(i, data); + if (isIndexValid(i)) { + AnimPose defaultPose = geometryToRigPose * _animSkeleton->getAbsoluteDefaultPose(i); + data.rotation = _internalPoseSet._absolutePoses[i].rot; + data.rotationSet = !isEqual(data.rotation, defaultPose.rot); + data.translation = _internalPoseSet._absolutePoses[i].trans; + data.translationSet = !isEqual(data.translation, defaultPose.trans); + } else { + data.translationSet = false; + data.rotationSet = false; + } } } @@ -1243,8 +1238,11 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { std::vector overrideFlags(_internalPoseSet._overridePoses.size(), false); AnimPoseVec overridePoses = _animSkeleton->getAbsoluteDefaultPoses(); // start with the default poses. + for (int i = 0; i < overridePoses.size(); i++) { + overridePoses[i] = AnimPose(_geometryToRigTransform) * overridePoses[i]; + } - ASSERT(overrideFlags.size() == absoluteOverridePoses.size()); + ASSERT(overrideFlags.size() == overridePoses.size()); for (int i = 0; i < jointDataVec.size(); i++) { if (isIndexValid(i)) { @@ -1255,8 +1253,7 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { } if (data.translationSet) { overrideFlags[i] = true; - // convert from meters back into geometry units. - overridePoses[i].trans = _invGeometryOffset * data.translation; + overridePoses[i].trans = data.translation; } } } @@ -1265,10 +1262,11 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { _animSkeleton->convertAbsolutePosesToRelative(overridePoses); + AnimPose rigToGeometryPose(_rigToGeometryTransform); for (int i = 0; i < jointDataVec.size(); i++) { if (overrideFlags[i]) { _internalPoseSet._overrideFlags[i] = true; - _internalPoseSet._overridePoses[i] = overridePoses[i]; + _internalPoseSet._overridePoses[i] = rigToGeometryPose * overridePoses[i]; } } } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 7446fddc47..891d9fdb92 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -229,9 +229,6 @@ protected: void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade); void calcAnimAlpha(float speed, const std::vector& referenceSpeeds, float* alphaOut) const; - // geometry space - bool getJointData(int index, JointData& jointDataOut) const; - AnimPose _modelOffset; // model to rig space AnimPose _geometryOffset; // geometry to model space (includes unit offset & fst offsets) AnimPose _invGeometryOffset; From d86a608825bcf189057a794e68f8a9e2d990200c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 12 May 2016 17:10:48 -0700 Subject: [PATCH 0062/1237] Properly convert from absolute rig frame to relative geom frame --- libraries/animation/src/Rig.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 34c917da74..289612d712 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1236,14 +1236,17 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { if (_animSkeleton) { + // transform all the default poses into rig space. + const AnimPose geometryToRigPose(_geometryToRigTransform); std::vector overrideFlags(_internalPoseSet._overridePoses.size(), false); AnimPoseVec overridePoses = _animSkeleton->getAbsoluteDefaultPoses(); // start with the default poses. - for (int i = 0; i < overridePoses.size(); i++) { - overridePoses[i] = AnimPose(_geometryToRigTransform) * overridePoses[i]; + for (auto& pose : overridePoses) { + pose = geometryToRigPose * pose; } ASSERT(overrideFlags.size() == overridePoses.size()); + // copy over data from the jointDataVec, which is also in rig space. for (int i = 0; i < jointDataVec.size(); i++) { if (isIndexValid(i)) { const JointData& data = jointDataVec.at(i); @@ -1260,13 +1263,20 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { ASSERT(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); + // convert resulting poses into geometry space. + const AnimPose rigToGeometryPose(_rigToGeometryTransform); + for (auto& pose : overridePoses) { + pose = rigToGeometryPose * pose; + } + + // convert all poses from absolute to parent relative. _animSkeleton->convertAbsolutePosesToRelative(overridePoses); - AnimPose rigToGeometryPose(_rigToGeometryTransform); + // copy the geometry space parent relative poses into _overridePoses for (int i = 0; i < jointDataVec.size(); i++) { if (overrideFlags[i]) { _internalPoseSet._overrideFlags[i] = true; - _internalPoseSet._overridePoses[i] = rigToGeometryPose * overridePoses[i]; + _internalPoseSet._overridePoses[i] = overridePoses[i]; } } } From a251b9e3dfc1be0d6e47408a4fa5b549634c7e1e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 13 May 2016 11:16:31 -0700 Subject: [PATCH 0063/1237] Moved translations back into parent relative frame. --- libraries/animation/src/AnimSkeleton.cpp | 12 ++++++ libraries/animation/src/AnimSkeleton.h | 2 + libraries/animation/src/Rig.cpp | 54 ++++++++++++++++-------- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 2d37be9b87..351c09beee 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -107,6 +107,18 @@ void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const { } } +void AnimSkeleton::convertAbsoluteRotationsToRelative(std::vector& rotations) const { + // poses start off absolute and leave in relative frame + int lastIndex = std::min((int)rotations.size(), (int)_joints.size()); + for (int i = lastIndex - 1; i >= 0; --i) { + int parentIndex = _joints[i].parentIndex; + if (parentIndex != -1) { + rotations[i] = glm::inverse(rotations[parentIndex]) * rotations[i]; + } + } +} + + void AnimSkeleton::mirrorRelativePoses(AnimPoseVec& poses) const { convertRelativePosesToAbsolute(poses); mirrorAbsolutePoses(poses); diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index e2cd20d63e..68cce11326 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -55,6 +55,8 @@ public: void convertRelativePosesToAbsolute(AnimPoseVec& poses) const; void convertAbsolutePosesToRelative(AnimPoseVec& poses) const; + void convertAbsoluteRotationsToRelative(std::vector& rotations) const; + void mirrorRelativePoses(AnimPoseVec& poses) const; void mirrorAbsolutePoses(AnimPoseVec& poses) const; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 289612d712..9bba9ffc33 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1220,11 +1220,16 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { for (auto i = 0; i < jointDataVec.size(); i++) { JointData& data = jointDataVec[i]; if (isIndexValid(i)) { - AnimPose defaultPose = geometryToRigPose * _animSkeleton->getAbsoluteDefaultPose(i); + // rotations are in absolute rig frame. + glm::quat defaultAbsRot = geometryToRigPose.rot * _animSkeleton->getAbsoluteDefaultPose(i).rot; data.rotation = _internalPoseSet._absolutePoses[i].rot; - data.rotationSet = !isEqual(data.rotation, defaultPose.rot); - data.translation = _internalPoseSet._absolutePoses[i].trans; - data.translationSet = !isEqual(data.translation, defaultPose.trans); + data.rotationSet = !isEqual(data.rotation, defaultAbsRot); + + // translations are in relative frame but scaled so that they are in meters, + // instead of geometry units. + glm::vec3 defaultRelTrans = _geometryOffset.scale * _animSkeleton->getRelativeDefaultPose(i).trans; + data.translation = _geometryOffset.scale * _internalPoseSet._relativePoses[i].trans; + data.translationSet = !isEqual(data.translation, defaultRelTrans); } else { data.translationSet = false; data.rotationSet = false; @@ -1239,44 +1244,57 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { // transform all the default poses into rig space. const AnimPose geometryToRigPose(_geometryToRigTransform); std::vector overrideFlags(_internalPoseSet._overridePoses.size(), false); - AnimPoseVec overridePoses = _animSkeleton->getAbsoluteDefaultPoses(); // start with the default poses. - for (auto& pose : overridePoses) { - pose = geometryToRigPose * pose; + + // start with the default rotations in absolute rig frame + std::vector rotations; + rotations.reserve(_animSkeleton->getAbsoluteDefaultPoses().size()); + for (auto& pose : _animSkeleton->getAbsoluteDefaultPoses()) { + rotations.push_back(geometryToRigPose.rot * pose.rot); } - ASSERT(overrideFlags.size() == overridePoses.size()); + // start translations in relative frame but scaled to meters. + std::vector translations; + translations.reserve(_animSkeleton->getRelativeDefaultPoses().size()); + for (auto& pose : _animSkeleton->getRelativeDefaultPoses()) { + translations.push_back(_geometryOffset.scale * pose.trans); + } - // copy over data from the jointDataVec, which is also in rig space. + ASSERT(overrideFlags.size() == rotations.size()); + + // copy over rotations from the jointDataVec, which is also in absolute rig frame for (int i = 0; i < jointDataVec.size(); i++) { if (isIndexValid(i)) { const JointData& data = jointDataVec.at(i); if (data.rotationSet) { overrideFlags[i] = true; - overridePoses[i].rot = data.rotation; + rotations[i] = data.rotation; } if (data.translationSet) { overrideFlags[i] = true; - overridePoses[i].trans = data.translation; + translations[i] = data.translation; } } } ASSERT(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); - // convert resulting poses into geometry space. - const AnimPose rigToGeometryPose(_rigToGeometryTransform); - for (auto& pose : overridePoses) { - pose = rigToGeometryPose * pose; + // convert resulting rotations into geometry space. + const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); + for (auto& rot : rotations) { + rot = rigToGeometryRot * rot; } - // convert all poses from absolute to parent relative. - _animSkeleton->convertAbsolutePosesToRelative(overridePoses); + // convert all rotations from absolute to parent relative. + _animSkeleton->convertAbsoluteRotationsToRelative(rotations); // copy the geometry space parent relative poses into _overridePoses for (int i = 0; i < jointDataVec.size(); i++) { if (overrideFlags[i]) { _internalPoseSet._overrideFlags[i] = true; - _internalPoseSet._overridePoses[i] = overridePoses[i]; + _internalPoseSet._overridePoses[i].scale = Vectors::ONE; + _internalPoseSet._overridePoses[i].rot = rotations[i]; + // scale translations from meters back into geometry units. + _internalPoseSet._overridePoses[i].trans = _invGeometryOffset.scale * translations[i]; } } } From 818d1f4601507256321dbacdef89c72d367495b0 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 13 May 2016 16:19:32 -0700 Subject: [PATCH 0064/1237] Added six byte quaternion compression routines & tests --- libraries/shared/src/GLMHelpers.cpp | 81 ++++++++++++++++++++++++++++ libraries/shared/src/GLMHelpers.h | 8 +++ tests/shared/src/GLMHelpersTests.cpp | 47 ++++++++++++++++ tests/shared/src/GLMHelpersTests.h | 1 + 4 files changed, 137 insertions(+) diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index bd8bffefd2..e81958f407 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -135,6 +135,87 @@ int unpackOrientationQuatFromBytes(const unsigned char* buffer, glm::quat& quatO return sizeof(quatParts); } +#define HI_BYTE(x) (uint8_t)(x >> 8) +#define LO_BYTE(x) (uint8_t)(0xff & x) + +int packOrientationQuatToSixBytes(unsigned char* buffer, const glm::quat& quatInput) { + + // find largest component + uint8_t largestComponent = 0; + for (int i = 1; i < 4; i++) { + if (fabs(quatInput[i]) > fabs(quatInput[largestComponent])) { + largestComponent = i; + } + } + + // ensure that the sign of the dropped component is always negative. + glm::quat q = quatInput[largestComponent] > 0 ? -quatInput : quatInput; + + const float MAGNITUDE = 1.0f / sqrt(2.0f); + const uint32_t NUM_BITS_PER_COMPONENT = 15; + const uint32_t RANGE = (1 << NUM_BITS_PER_COMPONENT) - 1; + + // quantize the smallest three components into integers + uint16_t components[3]; + for (int i = 0, j = 0; i < 4; i++) { + if (i != largestComponent) { + // transform component into 0..1 range. + float value = (q[i] + MAGNITUDE) / (2.0f * MAGNITUDE); + + // quantize 0..1 into 0..range + components[j] = (uint16_t)(value * RANGE); + j++; + } + } + + // encode the largestComponent into the high bits of the first two components + components[0] = (0x7fff & components[0]) | ((0x01 & largestComponent) << 15); + components[1] = (0x7fff & components[1]) | ((0x02 & largestComponent) << 14); + + buffer[0] = HI_BYTE(components[0]); + buffer[1] = LO_BYTE(components[0]); + buffer[2] = HI_BYTE(components[1]); + buffer[3] = LO_BYTE(components[1]); + buffer[4] = HI_BYTE(components[2]); + buffer[5] = LO_BYTE(components[2]); + + return 6; +} + +int unpackOrientationQuatFromSixBytes(const unsigned char* buffer, glm::quat& quatOutput) { + + uint16_t components[3]; + components[0] = ((uint16_t)(0x7f & buffer[0]) << 8) | buffer[1]; + components[1] = ((uint16_t)(0x7f & buffer[2]) << 8) | buffer[3]; + components[2] = ((uint16_t)(0x7f & buffer[4]) << 8) | buffer[5]; + + // largestComponent is encoded into the highest bits of the first 2 components + uint8_t largestComponent = ((0x80 & buffer[2]) >> 6) | ((0x80 & buffer[0]) >> 7); + + const uint32_t NUM_BITS_PER_COMPONENT = 15; + const float RANGE = (float)((1 << NUM_BITS_PER_COMPONENT) - 1); + const float MAGNITUDE = 1.0f / sqrtf(2.0f); + float floatComponents[3]; + for (int i = 0; i < 3; i++) { + floatComponents[i] = ((float)components[i] / RANGE) * (2.0f * MAGNITUDE) - MAGNITUDE; + } + + // missingComponent is always negative. + float missingComponent = -sqrtf(1.0f - floatComponents[0] * floatComponents[0] - floatComponents[1] * floatComponents[1] - floatComponents[2] * floatComponents[2]); + + for (int i = 0, j = 0; i < 4; i++) { + if (i != largestComponent) { + quatOutput[i] = floatComponents[j]; + j++; + } else { + quatOutput[i] = missingComponent; + } + } + + return 6; +} + + // Safe version of glm::eulerAngles; uses the factorization method described in David Eberly's // http://www.geometrictools.com/Documentation/EulerAngles.pdf (via Clyde, // https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Quaternion.java) diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 8b1446d4e5..ae9ec25195 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -97,6 +97,14 @@ int unpackFloatAngleFromTwoByte(const uint16_t* byteAnglePointer, float* destina int packOrientationQuatToBytes(unsigned char* buffer, const glm::quat& quatInput); int unpackOrientationQuatFromBytes(const unsigned char* buffer, glm::quat& quatOutput); +// alternate compression method that picks the smallest three quaternion components. +// and packs them into 15 bits each. An additional 2 bits are used to encode which component +// was omitted. Also because the components are encoded from the -1/sqrt(2) to 1/sqrt(2) which +// gives us some extra precision over the -1 to 1 range. The final result will have a maximum +// error of +- 4.3e-5 error per compoenent. +int packOrientationQuatToSixBytes(unsigned char* buffer, const glm::quat& quatInput); +int unpackOrientationQuatFromSixBytes(const unsigned char* buffer, glm::quat& quatOutput); + // Ratios need the be highly accurate when less than 10, but not very accurate above 10, and they // are never greater than 1000 to 1, this allows us to encode each component in 16bits int packFloatRatioToTwoByte(unsigned char* buffer, float ratio); diff --git a/tests/shared/src/GLMHelpersTests.cpp b/tests/shared/src/GLMHelpersTests.cpp index afb634ecbd..a796d62ba5 100644 --- a/tests/shared/src/GLMHelpersTests.cpp +++ b/tests/shared/src/GLMHelpersTests.cpp @@ -54,4 +54,51 @@ void GLMHelpersTests::testEulerDecomposition() { } } +static void testQuatCompression(glm::quat testQuat) { + float MAX_COMPONENT_ERROR = 4.3e-5f; + + glm::quat q; + uint8_t bytes[6]; + packOrientationQuatToSixBytes(bytes, testQuat); + unpackOrientationQuatFromSixBytes(bytes, q); + if (glm::dot(q, testQuat) < 0.0f) { + q = -q; + } + QCOMPARE_WITH_ABS_ERROR(q.x, testQuat.x, MAX_COMPONENT_ERROR); + QCOMPARE_WITH_ABS_ERROR(q.y, testQuat.y, MAX_COMPONENT_ERROR); + QCOMPARE_WITH_ABS_ERROR(q.z, testQuat.z, MAX_COMPONENT_ERROR); + QCOMPARE_WITH_ABS_ERROR(q.w, testQuat.w, MAX_COMPONENT_ERROR); +} + +void GLMHelpersTests::testSixByteOrientationCompression() { + const glm::quat ROT_X_90 = glm::angleAxis(PI / 2.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); + const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + + testQuatCompression(ROT_X_90); + testQuatCompression(ROT_Y_180); + testQuatCompression(ROT_Z_30); + testQuatCompression(ROT_X_90 * ROT_Y_180); + testQuatCompression(ROT_Y_180 * ROT_X_90); + testQuatCompression(ROT_Z_30 * ROT_X_90); + testQuatCompression(ROT_X_90 * ROT_Z_30); + testQuatCompression(ROT_Z_30 * ROT_Y_180); + testQuatCompression(ROT_Y_180 * ROT_Z_30); + testQuatCompression(ROT_X_90 * ROT_Y_180 * ROT_Z_30); + testQuatCompression(ROT_Y_180 * ROT_Z_30 * ROT_X_90); + testQuatCompression(ROT_Z_30 * ROT_X_90 * ROT_Y_180); + + testQuatCompression(-ROT_X_90); + testQuatCompression(-ROT_Y_180); + testQuatCompression(-ROT_Z_30); + testQuatCompression(-(ROT_X_90 * ROT_Y_180)); + testQuatCompression(-(ROT_Y_180 * ROT_X_90)); + testQuatCompression(-(ROT_Z_30 * ROT_X_90)); + testQuatCompression(-(ROT_X_90 * ROT_Z_30)); + testQuatCompression(-(ROT_Z_30 * ROT_Y_180)); + testQuatCompression(-(ROT_Y_180 * ROT_Z_30)); + testQuatCompression(-(ROT_X_90 * ROT_Y_180 * ROT_Z_30)); + testQuatCompression(-(ROT_Y_180 * ROT_Z_30 * ROT_X_90)); + testQuatCompression(-(ROT_Z_30 * ROT_X_90 * ROT_Y_180)); +} diff --git a/tests/shared/src/GLMHelpersTests.h b/tests/shared/src/GLMHelpersTests.h index 5e880899e8..40d552a07b 100644 --- a/tests/shared/src/GLMHelpersTests.h +++ b/tests/shared/src/GLMHelpersTests.h @@ -19,6 +19,7 @@ class GLMHelpersTests : public QObject { Q_OBJECT private slots: void testEulerDecomposition(); + void testSixByteOrientationCompression(); }; float getErrorDifference(const float& a, const float& b); From 3d91c5b54def5317b7f4e6f6ee6097f85654ca5f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 13 May 2016 16:24:20 -0700 Subject: [PATCH 0065/1237] AvatarData.cpp: hooked up 6 byte quat compression --- libraries/avatars/src/AvatarData.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 784044da2e..3db3cea4cf 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -263,7 +263,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { for (int i = 0; i < _jointData.size(); i ++) { const JointData& data = _jointData[ i ]; if (validity & (1 << validityBit)) { - destinationBuffer += packOrientationQuatToBytes(destinationBuffer, data.rotation); + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); } if (++validityBit == BITS_IN_BYTE) { validityBit = 0; @@ -650,10 +650,10 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { if (validRotations[i]) { _hasNewJointRotations = true; data.rotationSet = true; - sourceBuffer += unpackOrientationQuatFromBytes(sourceBuffer, data.rotation); + sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); } } - } // numJoints * 8 bytes + } // numJoints * 6 bytes // joint translations // get translation validity bits -- these indicate which translations were packed From d06c6e2a5967ef72a8333749623457d6820377f6 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 16 May 2016 17:54:42 -0700 Subject: [PATCH 0066/1237] Fix forgotten vertex shader case --- libraries/render-utils/src/model_normal_map.slv | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/render-utils/src/model_normal_map.slv b/libraries/render-utils/src/model_normal_map.slv index e70d93f909..494e40ccff 100755 --- a/libraries/render-utils/src/model_normal_map.slv +++ b/libraries/render-utils/src/model_normal_map.slv @@ -22,6 +22,7 @@ out vec4 _position; out vec2 _texCoord0; +out vec2 _texCoord1; out vec3 _normal; out vec3 _tangent; out vec3 _color; From 424517e3de2a9647cc6c99503c1f6b45066cd629 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 16 May 2016 19:11:50 -0700 Subject: [PATCH 0067/1237] Fix for Malformed packet errors --- libraries/avatars/src/AvatarData.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 3db3cea4cf..251d83c19b 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -37,6 +37,8 @@ #include "AvatarLogging.h" +#define WANT_DEBUG + quint64 DEFAULT_FILTERED_LOG_EXPIRY = 2 * USECS_PER_SECOND; using namespace std; @@ -121,8 +123,6 @@ void AvatarData::setHandPosition(const glm::vec3& handPosition) { _handPosition = glm::inverse(getOrientation()) * (handPosition - getPosition()); } -#define WANT_DEBUG - QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { // TODO: DRY this up to a shared method // that can pack any type given the number of bytes @@ -331,7 +331,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { } #ifdef WANT_DEBUG - //if (sendAll) { + if (sendAll) { qDebug() << "AvatarData::toByteArray" << cullSmallChanges << sendAll << "rotations:" << rotationSentCount << "translations:" << translationSentCount << "largest:" << maxTranslationDimension @@ -341,7 +341,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { << (beforeTranslations - beforeRotations) << "+" << (destinationBuffer - beforeTranslations) << "=" << (destinationBuffer - startPosition); - //} + } #endif return avatarDataByteArray.left(destinationBuffer - startPosition); @@ -372,6 +372,12 @@ void AvatarData::doneEncoding(bool cullSmallChanges) { } bool AvatarData::shouldLogError(const quint64& now) { +#ifdef WANT_DEBUG + if (now > 0) { + return true; + } +#endif + if (now > _errorLogExpiry) { _errorLogExpiry = now + DEFAULT_FILTERED_LOG_EXPIRY; return true; @@ -631,9 +637,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } } // 1 + bytesOfValidity bytes - // each joint rotation component is stored in two bytes (sizeof(uint16_t)) - int COMPONENTS_PER_QUATERNION = 4; - minPossibleSize += numValidJointRotations * COMPONENTS_PER_QUATERNION * sizeof(uint16_t); + // each joint rotation is stored in 6 bytes. + const size_t COMPRESSED_QUATERNION_SIZE = 6; + minPossibleSize += numValidJointRotations * COMPRESSED_QUATERNION_SIZE; if (minPossibleSize > maxAvailableSize) { if (shouldLogError(now)) { qCDebug(avatars) << "Malformed AvatarData packet after JointData rotation validity;" From e0483585b85822a4d6220fefc8e08683a89d236f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 17 May 2016 14:43:49 +1200 Subject: [PATCH 0068/1237] Consistently capitalize drive letter for Windows --- interface/resources/qml/dialogs/FileDialog.qml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 9c68767e2b..e8b6b9b2b7 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -144,7 +144,16 @@ ModalWindow { leftMargin: hifi.dimensions.contentSpacing.x right: parent.right } - onLastValidFolderChanged: text = lastValidFolder; + + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + + onLastValidFolderChanged: text = capitalizeDrive(lastValidFolder); // FIXME add support auto-completion onAccepted: { From ef6d758e7f19084b3d25a507a3aaed0827c86226 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 10 May 2016 10:30:38 -0700 Subject: [PATCH 0069/1237] Fix JurisdictionMap multithreading issues Make octal code pointers use shared_ptr, add locks around access. --- interface/src/Application.cpp | 14 +- interface/src/ui/OctreeStatsDialog.cpp | 6 +- libraries/octree/src/JurisdictionMap.cpp | 187 ++++++++---------- libraries/octree/src/JurisdictionMap.h | 18 +- libraries/octree/src/OctreeHeadlessViewer.cpp | 8 +- libraries/octree/src/OctreeSceneStats.cpp | 96 +++------ libraries/octree/src/OctreeSceneStats.h | 9 +- libraries/shared/src/OctalCode.cpp | 9 +- libraries/shared/src/OctalCode.h | 7 +- 9 files changed, 138 insertions(+), 216 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7a40c0b554..4d772d4b19 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3656,11 +3656,11 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node } else { const JurisdictionMap& map = (jurisdictions)[nodeUUID]; - unsigned char* rootCode = map.getRootOctalCode(); + auto rootCode = map.getRootOctalCode(); if (rootCode) { VoxelPositionSize rootDetails; - voxelDetailsForCode(rootCode, rootDetails); + voxelDetailsForCode(rootCode.get(), rootDetails); AACube serverBounds(glm::vec3(rootDetails.x * TREE_SCALE, rootDetails.y * TREE_SCALE, rootDetails.z * TREE_SCALE) - glm::vec3(HALF_TREE_SCALE), @@ -3720,11 +3720,11 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node } else { const JurisdictionMap& map = (jurisdictions)[nodeUUID]; - unsigned char* rootCode = map.getRootOctalCode(); + auto rootCode = map.getRootOctalCode(); if (rootCode) { VoxelPositionSize rootDetails; - voxelDetailsForCode(rootCode, rootDetails); + voxelDetailsForCode(rootCode.get(), rootDetails); AACube serverBounds(glm::vec3(rootDetails.x * TREE_SCALE, rootDetails.y * TREE_SCALE, rootDetails.z * TREE_SCALE) - glm::vec3(HALF_TREE_SCALE), @@ -4251,9 +4251,9 @@ void Application::nodeKilled(SharedNodePointer node) { return; } - unsigned char* rootCode = _entityServerJurisdictions[nodeUUID].getRootOctalCode(); + auto rootCode = _entityServerJurisdictions[nodeUUID].getRootOctalCode(); VoxelPositionSize rootDetails; - voxelDetailsForCode(rootCode, rootDetails); + voxelDetailsForCode(rootCode.get(), rootDetails); qCDebug(interfaceapp, "model server going away...... v[%f, %f, %f, %f]", (double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s); @@ -4378,7 +4378,7 @@ int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer } VoxelPositionSize rootDetails; - voxelDetailsForCode(octreeStats.getJurisdictionRoot(), rootDetails); + voxelDetailsForCode(octreeStats.getJurisdictionRoot().get(), rootDetails); qCDebug(interfaceapp, "stats from new %s server... [%f, %f, %f, %f]", qPrintable(serverType), diff --git a/interface/src/ui/OctreeStatsDialog.cpp b/interface/src/ui/OctreeStatsDialog.cpp index 6b6d600e20..fc0e6781d7 100644 --- a/interface/src/ui/OctreeStatsDialog.cpp +++ b/interface/src/ui/OctreeStatsDialog.cpp @@ -433,13 +433,13 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser } const JurisdictionMap& map = serverJurisdictions[nodeUUID]; - unsigned char* rootCode = map.getRootOctalCode(); + auto rootCode = map.getRootOctalCode(); if (rootCode) { - QString rootCodeHex = octalCodeToHexString(rootCode); + QString rootCodeHex = octalCodeToHexString(rootCode.get()); VoxelPositionSize rootDetails; - voxelDetailsForCode(rootCode, rootDetails); + voxelDetailsForCode(rootCode.get(), rootDetails); AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); serverDetails << " jurisdiction: " << qPrintable(rootCodeHex) diff --git a/libraries/octree/src/JurisdictionMap.cpp b/libraries/octree/src/JurisdictionMap.cpp index cac2211d29..86de7467d3 100644 --- a/libraries/octree/src/JurisdictionMap.cpp +++ b/libraries/octree/src/JurisdictionMap.cpp @@ -17,90 +17,11 @@ #include #include #include -#include #include "OctreeLogging.h" #include "JurisdictionMap.h" - -// standard assignment -// copy assignment -JurisdictionMap& JurisdictionMap::operator=(const JurisdictionMap& other) { - copyContents(other); - return *this; -} - -// Copy constructor -JurisdictionMap::JurisdictionMap(const JurisdictionMap& other) : _rootOctalCode(NULL) { - copyContents(other); -} - -void JurisdictionMap::copyContents(unsigned char* rootCodeIn, const std::vector& endNodesIn) { - unsigned char* rootCode; - std::vector endNodes; - if (rootCodeIn) { - size_t bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(rootCodeIn)); - rootCode = new unsigned char[bytes]; - memcpy(rootCode, rootCodeIn, bytes); - } else { - rootCode = new unsigned char[1]; - *rootCode = 0; - } - - for (size_t i = 0; i < endNodesIn.size(); i++) { - if (endNodesIn[i]) { - size_t bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodesIn[i])); - unsigned char* endNodeCode = new unsigned char[bytes]; - memcpy(endNodeCode, endNodesIn[i], bytes); - endNodes.push_back(endNodeCode); - } - } - init(rootCode, endNodes); -} - -void JurisdictionMap::copyContents(const JurisdictionMap& other) { - _nodeType = other._nodeType; - copyContents(other._rootOctalCode, other._endNodes); -} - -JurisdictionMap::~JurisdictionMap() { - clear(); -} - -void JurisdictionMap::clear() { - if (_rootOctalCode) { - delete[] _rootOctalCode; - _rootOctalCode = NULL; - } - - for (size_t i = 0; i < _endNodes.size(); i++) { - if (_endNodes[i]) { - delete[] _endNodes[i]; - } - } - _endNodes.clear(); -} - -JurisdictionMap::JurisdictionMap(NodeType_t type) : _rootOctalCode(NULL) { - _nodeType = type; - unsigned char* rootCode = new unsigned char[1]; - *rootCode = 0; - - std::vector emptyEndNodes; - init(rootCode, emptyEndNodes); -} - -JurisdictionMap::JurisdictionMap(const char* filename) : _rootOctalCode(NULL) { - clear(); // clean up our own memory - readFromFile(filename); -} - -JurisdictionMap::JurisdictionMap(unsigned char* rootOctalCode, const std::vector& endNodes) - : _rootOctalCode(NULL) { - init(rootOctalCode, endNodes); -} - -void myDebugoutputBits(unsigned char byte, bool withNewLine) { +void myDebugOutputBits(unsigned char byte, bool withNewLine) { if (isalnum(byte)) { printf("[ %d (%c): ", byte, byte); } else { @@ -117,13 +38,12 @@ void myDebugoutputBits(unsigned char byte, bool withNewLine) { } } - void myDebugPrintOctalCode(const unsigned char* octalCode, bool withNewLine) { if (!octalCode) { - printf("NULL"); + printf("nullptr"); } else { for (size_t i = 0; i < bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(octalCode)); i++) { - myDebugoutputBits(octalCode[i],false); + myDebugOutputBits(octalCode[i], false); } } if (withNewLine) { @@ -131,16 +51,53 @@ void myDebugPrintOctalCode(const unsigned char* octalCode, bool withNewLine) { } } +// standard assignment +// copy assignment +JurisdictionMap& JurisdictionMap::operator=(const JurisdictionMap& other) { + copyContents(other); + return *this; +} + +// Copy constructor +JurisdictionMap::JurisdictionMap(const JurisdictionMap& other) : _rootOctalCode(nullptr) { + copyContents(other); +} + +void JurisdictionMap::copyContents(const OctalCodePtr& rootCodeIn, const std::vector& endNodesIn) { + init(rootCodeIn, endNodesIn); +} + +void JurisdictionMap::copyContents(const JurisdictionMap& other) { + _nodeType = other._nodeType; + + init(other.getRootOctalCode(), other.getEndNodeOctalCodes()); +} + +JurisdictionMap::~JurisdictionMap() { +} + +JurisdictionMap::JurisdictionMap(NodeType_t type) : _rootOctalCode(nullptr) { + _nodeType = type; + auto rootCode = std::shared_ptr(new unsigned char[1], std::default_delete()); + *rootCode = 0; + + std::vector emptyEndNodes; + init(rootCode, emptyEndNodes); +} + +JurisdictionMap::JurisdictionMap(const char* filename) : _rootOctalCode(nullptr) { + readFromFile(filename); +} JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHexCodes) { qCDebug(octree, "JurisdictionMap::JurisdictionMap(const char* rootHexCode=[%p] %s, const char* endNodesHexCodes=[%p] %s)", rootHexCode, rootHexCode, endNodesHexCodes, endNodesHexCodes); - _rootOctalCode = hexStringToOctalCode(QString(rootHexCode)); + _rootOctalCode = std::shared_ptr(hexStringToOctalCode(QString(rootHexCode))); - qCDebug(octree, "JurisdictionMap::JurisdictionMap() _rootOctalCode=%p octalCode=", _rootOctalCode); - myDebugPrintOctalCode(_rootOctalCode, true); + qCDebug(octree, "JurisdictionMap::JurisdictionMap() _rootOctalCode=%p octalCode=", _rootOctalCode.get()); + myDebugPrintOctalCode(_rootOctalCode.get(), true); QString endNodesHexStrings(endNodesHexCodes); QString delimiterPattern(","); @@ -149,7 +106,7 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe for (int i = 0; i < endNodeList.size(); i++) { QString endNodeHexString = endNodeList.at(i); - unsigned char* endNodeOctcode = hexStringToOctalCode(endNodeHexString); + auto endNodeOctcode = hexStringToOctalCode(endNodeHexString); qCDebug(octree, "JurisdictionMap::JurisdictionMap() endNodeList(%d)=%s", i, endNodeHexString.toLocal8Bit().constData()); @@ -158,14 +115,14 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe _endNodes.push_back(endNodeOctcode); qCDebug(octree, "JurisdictionMap::JurisdictionMap() endNodeOctcode=%p octalCode=", endNodeOctcode); - myDebugPrintOctalCode(endNodeOctcode, true); + myDebugPrintOctalCode(endNodeOctcode.get(), true); } } -void JurisdictionMap::init(unsigned char* rootOctalCode, const std::vector& endNodes) { - clear(); // clean up our own memory +void JurisdictionMap::init(OctalCodePtr rootOctalCode, const std::vector& endNodes) { + std::lock_guard lock(_octalCodeMutex); _rootOctalCode = rootOctalCode; _endNodes = endNodes; } @@ -173,17 +130,19 @@ void JurisdictionMap::init(unsigned char* rootOctalCode, const std::vector lock(_octalCodeMutex); + // if the node is an ancestor of my root, then we return ABOVE - if (isAncestorOf(nodeOctalCode, _rootOctalCode)) { + if (isAncestorOf(nodeOctalCode, _rootOctalCode.get())) { return ABOVE; } // otherwise... - bool isInJurisdiction = isAncestorOf(_rootOctalCode, nodeOctalCode, childIndex); + bool isInJurisdiction = isAncestorOf(_rootOctalCode.get(), nodeOctalCode, childIndex); // if we're under the root, then we can't be under any of the endpoints if (isInJurisdiction) { for (size_t i = 0; i < _endNodes.size(); i++) { - bool isUnderEndNode = isAncestorOf(_endNodes[i], nodeOctalCode); + bool isUnderEndNode = isAncestorOf(_endNodes[i].get(), nodeOctalCode); if (isUnderEndNode) { isInJurisdiction = false; break; @@ -200,8 +159,9 @@ bool JurisdictionMap::readFromFile(const char* filename) { QString rootCode = settings.value("root","00").toString(); qCDebug(octree) << "rootCode=" << rootCode; - _rootOctalCode = hexStringToOctalCode(rootCode); - printOctalCode(_rootOctalCode); + std::lock_guard lock(_octalCodeMutex); + _rootOctalCode = std::shared_ptr(hexStringToOctalCode(rootCode)); + printOctalCode(_rootOctalCode.get()); settings.beginGroup("endNodes"); const QStringList childKeys = settings.childKeys(); @@ -211,8 +171,8 @@ bool JurisdictionMap::readFromFile(const char* filename) { values.insert(childKey, childValue); qCDebug(octree) << childKey << "=" << childValue; - unsigned char* octcode = hexStringToOctalCode(childValue); - printOctalCode(octcode); + auto octcode = hexStringToOctalCode(childValue); + printOctalCode(octcode.get()); _endNodes.push_back(octcode); } @@ -221,30 +181,34 @@ bool JurisdictionMap::readFromFile(const char* filename) { } void JurisdictionMap::displayDebugDetails() const { - QString rootNodeValue = octalCodeToHexString(_rootOctalCode); + std::lock_guard lock(_octalCodeMutex); + + QString rootNodeValue = octalCodeToHexString(_rootOctalCode.get()); qCDebug(octree) << "root:" << rootNodeValue; for (size_t i = 0; i < _endNodes.size(); i++) { - QString value = octalCodeToHexString(_endNodes[i]); + QString value = octalCodeToHexString(_endNodes[i].get()); qCDebug(octree) << "End node[" << i << "]: " << rootNodeValue; } } bool JurisdictionMap::writeToFile(const char* filename) { + std::lock_guard lock(_octalCodeMutex); + QString settingsFile(filename); QSettings settings(settingsFile, QSettings::IniFormat); - QString rootNodeValue = octalCodeToHexString(_rootOctalCode); + QString rootNodeValue = octalCodeToHexString(_rootOctalCode.get()); settings.setValue("root", rootNodeValue); settings.beginGroup("endNodes"); for (size_t i = 0; i < _endNodes.size(); i++) { QString key = QString("endnode%1").arg(i); - QString value = octalCodeToHexString(_endNodes[i]); + QString value = octalCodeToHexString(_endNodes[i].get()); settings.setValue(key, value); } settings.endGroup(); @@ -271,18 +235,19 @@ std::unique_ptr JurisdictionMap::packIntoPacket() { packet->writePrimitive(type); // add the root jurisdiction + std::lock_guard lock(_octalCodeMutex); if (_rootOctalCode) { - size_t bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(_rootOctalCode)); + size_t bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(_rootOctalCode.get())); // No root or end node details to pack! packet->writePrimitive(bytes); - packet->write(reinterpret_cast(_rootOctalCode), bytes); + packet->write(reinterpret_cast(_rootOctalCode.get()), bytes); // if and only if there's a root jurisdiction, also include the end nodes int endNodeCount = (int)_endNodes.size(); packet->writePrimitive(endNodeCount); for (int i=0; i < endNodeCount; i++) { - unsigned char* endNodeCode = _endNodes[i]; + auto endNodeCode = _endNodes[i].get(); size_t bytes = 0; if (endNodeCode) { bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode)); @@ -299,15 +264,17 @@ std::unique_ptr JurisdictionMap::packIntoPacket() { } int JurisdictionMap::unpackFromPacket(ReceivedMessage& message) { - clear(); - // read the root jurisdiction int bytes = 0; message.readPrimitive(&bytes); + std::lock_guard lock(_octalCodeMutex); + _rootOctalCode = nullptr; + _endNodes.clear(); + if (bytes > 0 && bytes <= message.getBytesLeftToRead()) { - _rootOctalCode = new unsigned char[bytes]; - message.read(reinterpret_cast(_rootOctalCode), bytes); + _rootOctalCode = std::shared_ptr(new unsigned char[bytes], std::default_delete()); + message.read(reinterpret_cast(_rootOctalCode.get()), bytes); // if and only if there's a root jurisdiction, also include the end nodes int endNodeCount = 0; @@ -318,8 +285,8 @@ int JurisdictionMap::unpackFromPacket(ReceivedMessage& message) { message.readPrimitive(&bytes); if (bytes <= message.getBytesLeftToRead()) { - unsigned char* endNodeCode = new unsigned char[bytes]; - message.read(reinterpret_cast(endNodeCode), bytes); + auto endNodeCode = std::shared_ptr(new unsigned char[bytes], std::default_delete()); + message.read(reinterpret_cast(endNodeCode.get()), bytes); // if the endNodeCode was 0 length then don't add it if (bytes > 0) { diff --git a/libraries/octree/src/JurisdictionMap.h b/libraries/octree/src/JurisdictionMap.h index fb217e59db..0deedb41e1 100644 --- a/libraries/octree/src/JurisdictionMap.h +++ b/libraries/octree/src/JurisdictionMap.h @@ -23,6 +23,7 @@ #include #include +#include class JurisdictionMap { public: @@ -41,7 +42,7 @@ public: // application constructors JurisdictionMap(const char* filename); - JurisdictionMap(unsigned char* rootOctalCode, const std::vector& endNodes); +// JurisdictionMap(unsigned char* rootOctalCode, const std::vector& endNodes); JurisdictionMap(const char* rootHextString, const char* endNodesHextString); ~JurisdictionMap(); @@ -50,11 +51,10 @@ public: bool writeToFile(const char* filename); bool readFromFile(const char* filename); - unsigned char* getRootOctalCode() const { return _rootOctalCode; } - unsigned char* getEndNodeOctalCode(int index) const { return _endNodes[index]; } - int getEndNodeCount() const { return (int)_endNodes.size(); } + OctalCodePtr getRootOctalCode() const { return _rootOctalCode; } + std::vector getEndNodeOctalCodes() const { return _endNodes; } - void copyContents(unsigned char* rootCodeIn, const std::vector& endNodesIn); + void copyContents(const OctalCodePtr& rootCodeIn, const std::vector& endNodesIn); int unpackFromPacket(ReceivedMessage& message); std::unique_ptr packIntoPacket(); @@ -69,11 +69,11 @@ public: private: void copyContents(const JurisdictionMap& other); // use assignment instead - void clear(); - void init(unsigned char* rootOctalCode, const std::vector& endNodes); + void init(OctalCodePtr rootOctalCode, const std::vector& endNodes); - unsigned char* _rootOctalCode; - std::vector _endNodes; + mutable std::mutex _octalCodeMutex; + OctalCodePtr _rootOctalCode { nullptr }; + std::vector _endNodes; NodeType_t _nodeType; }; diff --git a/libraries/octree/src/OctreeHeadlessViewer.cpp b/libraries/octree/src/OctreeHeadlessViewer.cpp index 4a0a76cd02..28d3794a54 100644 --- a/libraries/octree/src/OctreeHeadlessViewer.cpp +++ b/libraries/octree/src/OctreeHeadlessViewer.cpp @@ -78,12 +78,12 @@ void OctreeHeadlessViewer::queryOctree() { } const JurisdictionMap& map = (jurisdictions)[nodeUUID]; - unsigned char* rootCode = map.getRootOctalCode(); + auto rootCode = map.getRootOctalCode(); if (!rootCode) { return; } - voxelDetailsForCode(rootCode, rootDetails); + voxelDetailsForCode(rootCode.get(), rootDetails); foundRootDetails = true; }); @@ -146,7 +146,7 @@ void OctreeHeadlessViewer::queryOctree() { } const JurisdictionMap& map = (jurisdictions)[nodeUUID]; - unsigned char* rootCode = map.getRootOctalCode(); + auto rootCode = map.getRootOctalCode(); if (!rootCode) { if (wantExtraDebugging) { @@ -154,7 +154,7 @@ void OctreeHeadlessViewer::queryOctree() { } return; } - voxelDetailsForCode(rootCode, rootDetails); + voxelDetailsForCode(rootCode.get(), rootDetails); foundRootDetails = true; }); diff --git a/libraries/octree/src/OctreeSceneStats.cpp b/libraries/octree/src/OctreeSceneStats.cpp index 578d35b687..fd770ccd67 100644 --- a/libraries/octree/src/OctreeSceneStats.cpp +++ b/libraries/octree/src/OctreeSceneStats.cpp @@ -110,29 +110,21 @@ void OctreeSceneStats::copyFromOther(const OctreeSceneStats& other) { _treesRemoved = other._treesRemoved; // before copying the jurisdictions, delete any current values... - if (_jurisdictionRoot) { - delete[] _jurisdictionRoot; - _jurisdictionRoot = NULL; - } - for (size_t i = 0; i < _jurisdictionEndNodes.size(); i++) { - if (_jurisdictionEndNodes[i]) { - delete[] _jurisdictionEndNodes[i]; - } - } + _jurisdictionRoot = nullptr; _jurisdictionEndNodes.clear(); // Now copy the values from the other if (other._jurisdictionRoot) { - auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(other._jurisdictionRoot)); - _jurisdictionRoot = new unsigned char[bytes]; - memcpy(_jurisdictionRoot, other._jurisdictionRoot, bytes); + auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(other._jurisdictionRoot.get())); + _jurisdictionRoot = OctalCodePtr(new unsigned char[bytes]); + memcpy(_jurisdictionRoot.get(), other._jurisdictionRoot.get(), bytes); } for (size_t i = 0; i < other._jurisdictionEndNodes.size(); i++) { - unsigned char* endNodeCode = other._jurisdictionEndNodes[i]; + auto& endNodeCode = other._jurisdictionEndNodes[i]; if (endNodeCode) { - auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode)); - unsigned char* endNodeCodeCopy = new unsigned char[bytes]; - memcpy(endNodeCodeCopy, endNodeCode, bytes); + auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode.get())); + auto endNodeCodeCopy = OctalCodePtr(new unsigned char[bytes]); + memcpy(endNodeCodeCopy.get(), endNodeCode.get(), bytes); _jurisdictionEndNodes.push_back(endNodeCodeCopy); } } @@ -162,37 +154,15 @@ void OctreeSceneStats::sceneStarted(bool isFullScene, bool isMoving, OctreeEleme _isFullScene = isFullScene; _isMoving = isMoving; - if (_jurisdictionRoot) { - delete[] _jurisdictionRoot; - _jurisdictionRoot = NULL; - } + _jurisdictionRoot = nullptr; + // clear existing endNodes before copying new ones... - for (size_t i=0; i < _jurisdictionEndNodes.size(); i++) { - if (_jurisdictionEndNodes[i]) { - delete[] _jurisdictionEndNodes[i]; - } - } _jurisdictionEndNodes.clear(); // setup jurisdictions if (jurisdictionMap) { - unsigned char* jurisdictionRoot = jurisdictionMap->getRootOctalCode(); - if (jurisdictionRoot) { - auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(jurisdictionRoot)); - _jurisdictionRoot = new unsigned char[bytes]; - memcpy(_jurisdictionRoot, jurisdictionRoot, bytes); - } - - // copy new endNodes... - for (int i = 0; i < jurisdictionMap->getEndNodeCount(); i++) { - unsigned char* endNodeCode = jurisdictionMap->getEndNodeOctalCode(i); - if (endNodeCode) { - auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode)); - unsigned char* endNodeCodeCopy = new unsigned char[bytes]; - memcpy(endNodeCodeCopy, endNodeCode, bytes); - _jurisdictionEndNodes.push_back(endNodeCodeCopy); - } - } + _jurisdictionRoot = jurisdictionMap->getRootOctalCode(); + _jurisdictionEndNodes = jurisdictionMap->getEndNodeOctalCodes(); } } @@ -270,15 +240,7 @@ void OctreeSceneStats::reset() { _existsInPacketBitsWritten = 0; _treesRemoved = 0; - if (_jurisdictionRoot) { - delete[] _jurisdictionRoot; - _jurisdictionRoot = NULL; - } - for (size_t i = 0; i < _jurisdictionEndNodes.size(); i++) { - if (_jurisdictionEndNodes[i]) { - delete[] _jurisdictionEndNodes[i]; - } - } + _jurisdictionRoot = nullptr; _jurisdictionEndNodes.clear(); } @@ -418,9 +380,9 @@ int OctreeSceneStats::packIntoPacket() { // add the root jurisdiction if (_jurisdictionRoot) { // copy the - int bytes = (int)bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(_jurisdictionRoot)); + int bytes = (int)bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(_jurisdictionRoot.get())); _statsPacket->writePrimitive(bytes); - _statsPacket->write(reinterpret_cast(_jurisdictionRoot), bytes); + _statsPacket->write(reinterpret_cast(_jurisdictionRoot.get()), bytes); // if and only if there's a root jurisdiction, also include the end elements int endNodeCount = (int)_jurisdictionEndNodes.size(); @@ -428,10 +390,10 @@ int OctreeSceneStats::packIntoPacket() { _statsPacket->writePrimitive(endNodeCount); for (int i=0; i < endNodeCount; i++) { - unsigned char* endNodeCode = _jurisdictionEndNodes[i]; - auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode)); + auto& endNodeCode = _jurisdictionEndNodes[i]; + auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode.get())); _statsPacket->writePrimitive(bytes); - _statsPacket->write(reinterpret_cast(endNodeCode), bytes); + _statsPacket->write(reinterpret_cast(endNodeCode.get()), bytes); } } else { int bytes = 0; @@ -500,17 +462,7 @@ int OctreeSceneStats::unpackFromPacket(ReceivedMessage& packet) { packet.readPrimitive(&_existsInPacketBitsWritten); packet.readPrimitive(&_treesRemoved); // before allocating new juridiction, clean up existing ones - if (_jurisdictionRoot) { - delete[] _jurisdictionRoot; - _jurisdictionRoot = NULL; - } - - // clear existing endNodes before copying new ones... - for (size_t i = 0; i < _jurisdictionEndNodes.size(); i++) { - if (_jurisdictionEndNodes[i]) { - delete[] _jurisdictionEndNodes[i]; - } - } + _jurisdictionRoot = nullptr; _jurisdictionEndNodes.clear(); // read the root jurisdiction @@ -518,11 +470,11 @@ int OctreeSceneStats::unpackFromPacket(ReceivedMessage& packet) { packet.readPrimitive(&bytes); if (bytes == 0) { - _jurisdictionRoot = NULL; + _jurisdictionRoot = nullptr; _jurisdictionEndNodes.clear(); } else { - _jurisdictionRoot = new unsigned char[bytes]; - packet.read(reinterpret_cast(_jurisdictionRoot), bytes); + _jurisdictionRoot = OctalCodePtr(new unsigned char[bytes]); + packet.read(reinterpret_cast(_jurisdictionRoot.get()), bytes); // if and only if there's a root jurisdiction, also include the end elements _jurisdictionEndNodes.clear(); @@ -535,8 +487,8 @@ int OctreeSceneStats::unpackFromPacket(ReceivedMessage& packet) { packet.readPrimitive(&bytes); - unsigned char* endNodeCode = new unsigned char[bytes]; - packet.read(reinterpret_cast(endNodeCode), bytes); + auto endNodeCode = OctalCodePtr(new unsigned char[bytes]); + packet.read(reinterpret_cast(endNodeCode.get()), bytes); _jurisdictionEndNodes.push_back(endNodeCode); } diff --git a/libraries/octree/src/OctreeSceneStats.h b/libraries/octree/src/OctreeSceneStats.h index 11aa0b82b2..a888ad2485 100644 --- a/libraries/octree/src/OctreeSceneStats.h +++ b/libraries/octree/src/OctreeSceneStats.h @@ -20,6 +20,7 @@ #include "JurisdictionMap.h" #include "OctreePacketData.h" #include "SequenceNumberStats.h" +#include "OctalCode.h" #define GREENISH 0x40ff40d0 #define YELLOWISH 0xffef40c0 @@ -143,10 +144,10 @@ public: const char* getItemValue(Item item); /// Returns OctCode for root element of the jurisdiction of this particular octree server - unsigned char* getJurisdictionRoot() const { return _jurisdictionRoot; } + OctalCodePtr getJurisdictionRoot() const { return _jurisdictionRoot; } /// Returns list of OctCodes for end elements of the jurisdiction of this particular octree server - const std::vector& getJurisdictionEndNodes() const { return _jurisdictionEndNodes; } + const std::vector& getJurisdictionEndNodes() const { return _jurisdictionEndNodes; } bool isMoving() const { return _isMoving; } bool isFullScene() const { return _isFullScene; } @@ -277,8 +278,8 @@ private: static const int MAX_ITEM_VALUE_LENGTH = 128; char _itemValueBuffer[MAX_ITEM_VALUE_LENGTH]; - unsigned char* _jurisdictionRoot; - std::vector _jurisdictionEndNodes; + OctalCodePtr _jurisdictionRoot; + std::vector _jurisdictionEndNodes; }; /// Map between element IDs and their reported OctreeSceneStats. Typically used by classes that need to know which elements sent diff --git a/libraries/shared/src/OctalCode.cpp b/libraries/shared/src/OctalCode.cpp index 1fa18903cb..802ddbbb64 100644 --- a/libraries/shared/src/OctalCode.cpp +++ b/libraries/shared/src/OctalCode.cpp @@ -304,14 +304,14 @@ bool isAncestorOf(const unsigned char* possibleAncestor, const unsigned char* po return true; } -unsigned char* hexStringToOctalCode(const QString& input) { +std::shared_ptr hexStringToOctalCode(const QString& input) { const int HEX_NUMBER_BASE = 16; const int HEX_BYTE_SIZE = 2; int stringIndex = 0; int byteArrayIndex = 0; // allocate byte array based on half of string length - unsigned char* bytes = new unsigned char[(input.length()) / HEX_BYTE_SIZE]; + auto bytes = std::shared_ptr(new unsigned char[(input.length()) / HEX_BYTE_SIZE]); // loop through the string - 2 bytes at a time converting // it to decimal equivalent and store in byte array @@ -321,15 +321,14 @@ unsigned char* hexStringToOctalCode(const QString& input) { if (!ok) { break; } - bytes[byteArrayIndex] = (unsigned char)value; + bytes.get()[byteArrayIndex] = (unsigned char)value; stringIndex += HEX_BYTE_SIZE; byteArrayIndex++; } // something went wrong if (!ok) { - delete[] bytes; - return NULL; + return nullptr; } return bytes; } diff --git a/libraries/shared/src/OctalCode.h b/libraries/shared/src/OctalCode.h index 09766b685a..e8892fb40b 100644 --- a/libraries/shared/src/OctalCode.h +++ b/libraries/shared/src/OctalCode.h @@ -12,9 +12,10 @@ #ifndef hifi_OctalCode_h #define hifi_OctalCode_h -#include #include +#include + const int BITS_IN_OCTAL = 3; const int NUMBER_OF_COLORS = 3; // RGB! const int SIZE_OF_COLOR_DATA = NUMBER_OF_COLORS * sizeof(unsigned char); // size in bytes @@ -22,6 +23,8 @@ const int RED_INDEX = 0; const int GREEN_INDEX = 1; const int BLUE_INDEX = 2; +using OctalCodePtr = std::shared_ptr; + void printOctalCode(const unsigned char* octalCode); size_t bytesRequiredForCodeLength(unsigned char threeBitCodes); int branchIndexWithDescendant(const unsigned char* ancestorOctalCode, const unsigned char* descendantOctalCode); @@ -58,6 +61,6 @@ typedef enum { OctalCodeComparison compareOctalCodes(const unsigned char* code1, const unsigned char* code2); QString octalCodeToHexString(const unsigned char* octalCode); -unsigned char* hexStringToOctalCode(const QString& input); +std::shared_ptr hexStringToOctalCode(const QString& input); #endif // hifi_OctalCode_h From e819ab8475bfed2a6365defee8f0b4fa3fb9ecae Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 11 May 2016 09:59:02 -0700 Subject: [PATCH 0070/1237] Add mutex protection around octal code getters --- libraries/octree/src/JurisdictionMap.cpp | 26 +++++++++++++++++++---- libraries/octree/src/JurisdictionMap.h | 9 ++++---- libraries/octree/src/OctreeSceneStats.cpp | 11 ++++------ libraries/shared/src/OctalCode.cpp | 2 +- libraries/shared/src/OctalCode.h | 3 ++- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/libraries/octree/src/JurisdictionMap.cpp b/libraries/octree/src/JurisdictionMap.cpp index 86de7467d3..30a8de1bda 100644 --- a/libraries/octree/src/JurisdictionMap.cpp +++ b/libraries/octree/src/JurisdictionMap.cpp @@ -70,7 +70,12 @@ void JurisdictionMap::copyContents(const OctalCodePtr& rootCodeIn, const std::ve void JurisdictionMap::copyContents(const JurisdictionMap& other) { _nodeType = other._nodeType; - init(other.getRootOctalCode(), other.getEndNodeOctalCodes()); + OctalCodePtr rootOctalCode; + OctalCodePtrList endNodes; + + std::tie(rootOctalCode, endNodes) = other.getRootOctalCodeAndEndNodes(); + + init(rootOctalCode, endNodes); } JurisdictionMap::~JurisdictionMap() { @@ -120,6 +125,20 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe } } +std::tuple JurisdictionMap::getRootOctalCodeAndEndNodes() const { + std::lock_guard lock(_octalCodeMutex); + return std::tuple>(_rootOctalCode, _endNodes); +} + +OctalCodePtr JurisdictionMap::getRootOctalCode() const { + std::lock_guard lock(_octalCodeMutex); + return _rootOctalCode; +} + +OctalCodePtrList JurisdictionMap::getEndNodeOctalCodes() const { + std::lock_guard lock(_octalCodeMutex); + return _endNodes; +} void JurisdictionMap::init(OctalCodePtr rootOctalCode, const std::vector& endNodes) { std::lock_guard lock(_octalCodeMutex); @@ -160,7 +179,7 @@ bool JurisdictionMap::readFromFile(const char* filename) { qCDebug(octree) << "rootCode=" << rootCode; std::lock_guard lock(_octalCodeMutex); - _rootOctalCode = std::shared_ptr(hexStringToOctalCode(rootCode)); + _rootOctalCode = hexStringToOctalCode(rootCode); printOctalCode(_rootOctalCode.get()); settings.beginGroup("endNodes"); @@ -195,11 +214,10 @@ void JurisdictionMap::displayDebugDetails() const { bool JurisdictionMap::writeToFile(const char* filename) { - std::lock_guard lock(_octalCodeMutex); - QString settingsFile(filename); QSettings settings(settingsFile, QSettings::IniFormat); + std::lock_guard lock(_octalCodeMutex); QString rootNodeValue = octalCodeToHexString(_rootOctalCode.get()); diff --git a/libraries/octree/src/JurisdictionMap.h b/libraries/octree/src/JurisdictionMap.h index 0deedb41e1..3b08fb221c 100644 --- a/libraries/octree/src/JurisdictionMap.h +++ b/libraries/octree/src/JurisdictionMap.h @@ -51,10 +51,11 @@ public: bool writeToFile(const char* filename); bool readFromFile(const char* filename); - OctalCodePtr getRootOctalCode() const { return _rootOctalCode; } - std::vector getEndNodeOctalCodes() const { return _endNodes; } + std::tuple JurisdictionMap::getRootOctalCodeAndEndNodes() const; + OctalCodePtr getRootOctalCode() const; + OctalCodePtrList getEndNodeOctalCodes() const; - void copyContents(const OctalCodePtr& rootCodeIn, const std::vector& endNodesIn); + void copyContents(const OctalCodePtr& rootCodeIn, const OctalCodePtrList& endNodesIn); int unpackFromPacket(ReceivedMessage& message); std::unique_ptr packIntoPacket(); @@ -73,7 +74,7 @@ private: mutable std::mutex _octalCodeMutex; OctalCodePtr _rootOctalCode { nullptr }; - std::vector _endNodes; + OctalCodePtrList _endNodes; NodeType_t _nodeType; }; diff --git a/libraries/octree/src/OctreeSceneStats.cpp b/libraries/octree/src/OctreeSceneStats.cpp index fd770ccd67..ca2bff32a4 100644 --- a/libraries/octree/src/OctreeSceneStats.cpp +++ b/libraries/octree/src/OctreeSceneStats.cpp @@ -154,15 +154,12 @@ void OctreeSceneStats::sceneStarted(bool isFullScene, bool isMoving, OctreeEleme _isFullScene = isFullScene; _isMoving = isMoving; - _jurisdictionRoot = nullptr; - - // clear existing endNodes before copying new ones... - _jurisdictionEndNodes.clear(); - // setup jurisdictions if (jurisdictionMap) { - _jurisdictionRoot = jurisdictionMap->getRootOctalCode(); - _jurisdictionEndNodes = jurisdictionMap->getEndNodeOctalCodes(); + std::tie(_jurisdictionRoot, _jurisdictionEndNodes) = jurisdictionMap->getRootOctalCodeAndEndNodes(); + } else { + _jurisdictionRoot = nullptr; + _jurisdictionEndNodes.clear(); } } diff --git a/libraries/shared/src/OctalCode.cpp b/libraries/shared/src/OctalCode.cpp index 802ddbbb64..85b6c2b632 100644 --- a/libraries/shared/src/OctalCode.cpp +++ b/libraries/shared/src/OctalCode.cpp @@ -304,7 +304,7 @@ bool isAncestorOf(const unsigned char* possibleAncestor, const unsigned char* po return true; } -std::shared_ptr hexStringToOctalCode(const QString& input) { +OctalCodePtr hexStringToOctalCode(const QString& input) { const int HEX_NUMBER_BASE = 16; const int HEX_BYTE_SIZE = 2; int stringIndex = 0; diff --git a/libraries/shared/src/OctalCode.h b/libraries/shared/src/OctalCode.h index e8892fb40b..7d1424d954 100644 --- a/libraries/shared/src/OctalCode.h +++ b/libraries/shared/src/OctalCode.h @@ -24,6 +24,7 @@ const int GREEN_INDEX = 1; const int BLUE_INDEX = 2; using OctalCodePtr = std::shared_ptr; +using OctalCodePtrList = std::vector; void printOctalCode(const unsigned char* octalCode); size_t bytesRequiredForCodeLength(unsigned char threeBitCodes); @@ -61,6 +62,6 @@ typedef enum { OctalCodeComparison compareOctalCodes(const unsigned char* code1, const unsigned char* code2); QString octalCodeToHexString(const unsigned char* octalCode); -std::shared_ptr hexStringToOctalCode(const QString& input); +OctalCodePtr hexStringToOctalCode(const QString& input); #endif // hifi_OctalCode_h From 35f147f55703987d91678530d0356e348c370680 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 11 May 2016 10:37:54 -0700 Subject: [PATCH 0071/1237] Cleanup use of OctalCodePtrList and add allocateOctalCodePtr --- libraries/octree/src/JurisdictionMap.cpp | 14 +++++++------- libraries/octree/src/JurisdictionMap.h | 4 ++-- libraries/octree/src/OctreeSceneStats.cpp | 8 ++++---- libraries/octree/src/OctreeSceneStats.h | 2 +- libraries/shared/src/OctalCode.cpp | 6 +++++- libraries/shared/src/OctalCode.h | 1 + 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/libraries/octree/src/JurisdictionMap.cpp b/libraries/octree/src/JurisdictionMap.cpp index 30a8de1bda..7eb976690b 100644 --- a/libraries/octree/src/JurisdictionMap.cpp +++ b/libraries/octree/src/JurisdictionMap.cpp @@ -63,7 +63,7 @@ JurisdictionMap::JurisdictionMap(const JurisdictionMap& other) : _rootOctalCode( copyContents(other); } -void JurisdictionMap::copyContents(const OctalCodePtr& rootCodeIn, const std::vector& endNodesIn) { +void JurisdictionMap::copyContents(const OctalCodePtr& rootCodeIn, const OctalCodePtrList& endNodesIn) { init(rootCodeIn, endNodesIn); } @@ -83,10 +83,10 @@ JurisdictionMap::~JurisdictionMap() { JurisdictionMap::JurisdictionMap(NodeType_t type) : _rootOctalCode(nullptr) { _nodeType = type; - auto rootCode = std::shared_ptr(new unsigned char[1], std::default_delete()); + OctalCodePtr rootCode = allocateOctalCodePtr(1); *rootCode = 0; - std::vector emptyEndNodes; + OctalCodePtrList emptyEndNodes; init(rootCode, emptyEndNodes); } @@ -127,7 +127,7 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe std::tuple JurisdictionMap::getRootOctalCodeAndEndNodes() const { std::lock_guard lock(_octalCodeMutex); - return std::tuple>(_rootOctalCode, _endNodes); + return std::tuple(_rootOctalCode, _endNodes); } OctalCodePtr JurisdictionMap::getRootOctalCode() const { @@ -140,7 +140,7 @@ OctalCodePtrList JurisdictionMap::getEndNodeOctalCodes() const { return _endNodes; } -void JurisdictionMap::init(OctalCodePtr rootOctalCode, const std::vector& endNodes) { +void JurisdictionMap::init(OctalCodePtr rootOctalCode, const OctalCodePtrList& endNodes) { std::lock_guard lock(_octalCodeMutex); _rootOctalCode = rootOctalCode; _endNodes = endNodes; @@ -291,7 +291,7 @@ int JurisdictionMap::unpackFromPacket(ReceivedMessage& message) { _endNodes.clear(); if (bytes > 0 && bytes <= message.getBytesLeftToRead()) { - _rootOctalCode = std::shared_ptr(new unsigned char[bytes], std::default_delete()); + _rootOctalCode = allocateOctalCodePtr(bytes); message.read(reinterpret_cast(_rootOctalCode.get()), bytes); // if and only if there's a root jurisdiction, also include the end nodes @@ -303,7 +303,7 @@ int JurisdictionMap::unpackFromPacket(ReceivedMessage& message) { message.readPrimitive(&bytes); if (bytes <= message.getBytesLeftToRead()) { - auto endNodeCode = std::shared_ptr(new unsigned char[bytes], std::default_delete()); + auto endNodeCode = allocateOctalCodePtr(bytes); message.read(reinterpret_cast(endNodeCode.get()), bytes); // if the endNodeCode was 0 length then don't add it diff --git a/libraries/octree/src/JurisdictionMap.h b/libraries/octree/src/JurisdictionMap.h index 3b08fb221c..995b88e3ff 100644 --- a/libraries/octree/src/JurisdictionMap.h +++ b/libraries/octree/src/JurisdictionMap.h @@ -42,8 +42,8 @@ public: // application constructors JurisdictionMap(const char* filename); -// JurisdictionMap(unsigned char* rootOctalCode, const std::vector& endNodes); JurisdictionMap(const char* rootHextString, const char* endNodesHextString); + ~JurisdictionMap(); Area isMyJurisdiction(const unsigned char* nodeOctalCode, int childIndex) const; @@ -70,7 +70,7 @@ public: private: void copyContents(const JurisdictionMap& other); // use assignment instead - void init(OctalCodePtr rootOctalCode, const std::vector& endNodes); + void init(OctalCodePtr rootOctalCode, const OctalCodePtrList& endNodes); mutable std::mutex _octalCodeMutex; OctalCodePtr _rootOctalCode { nullptr }; diff --git a/libraries/octree/src/OctreeSceneStats.cpp b/libraries/octree/src/OctreeSceneStats.cpp index ca2bff32a4..f164cd2d48 100644 --- a/libraries/octree/src/OctreeSceneStats.cpp +++ b/libraries/octree/src/OctreeSceneStats.cpp @@ -116,14 +116,14 @@ void OctreeSceneStats::copyFromOther(const OctreeSceneStats& other) { // Now copy the values from the other if (other._jurisdictionRoot) { auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(other._jurisdictionRoot.get())); - _jurisdictionRoot = OctalCodePtr(new unsigned char[bytes]); + _jurisdictionRoot = allocateOctalCodePtr(bytes); memcpy(_jurisdictionRoot.get(), other._jurisdictionRoot.get(), bytes); } for (size_t i = 0; i < other._jurisdictionEndNodes.size(); i++) { auto& endNodeCode = other._jurisdictionEndNodes[i]; if (endNodeCode) { auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode.get())); - auto endNodeCodeCopy = OctalCodePtr(new unsigned char[bytes]); + auto endNodeCodeCopy = allocateOctalCodePtr(bytes); memcpy(endNodeCodeCopy.get(), endNodeCode.get(), bytes); _jurisdictionEndNodes.push_back(endNodeCodeCopy); } @@ -470,7 +470,7 @@ int OctreeSceneStats::unpackFromPacket(ReceivedMessage& packet) { _jurisdictionRoot = nullptr; _jurisdictionEndNodes.clear(); } else { - _jurisdictionRoot = OctalCodePtr(new unsigned char[bytes]); + _jurisdictionRoot = allocateOctalCodePtr(bytes); packet.read(reinterpret_cast(_jurisdictionRoot.get()), bytes); // if and only if there's a root jurisdiction, also include the end elements @@ -484,7 +484,7 @@ int OctreeSceneStats::unpackFromPacket(ReceivedMessage& packet) { packet.readPrimitive(&bytes); - auto endNodeCode = OctalCodePtr(new unsigned char[bytes]); + auto endNodeCode = allocateOctalCodePtr(bytes); packet.read(reinterpret_cast(endNodeCode.get()), bytes); _jurisdictionEndNodes.push_back(endNodeCode); diff --git a/libraries/octree/src/OctreeSceneStats.h b/libraries/octree/src/OctreeSceneStats.h index a888ad2485..e7d697f785 100644 --- a/libraries/octree/src/OctreeSceneStats.h +++ b/libraries/octree/src/OctreeSceneStats.h @@ -147,7 +147,7 @@ public: OctalCodePtr getJurisdictionRoot() const { return _jurisdictionRoot; } /// Returns list of OctCodes for end elements of the jurisdiction of this particular octree server - const std::vector& getJurisdictionEndNodes() const { return _jurisdictionEndNodes; } + const OctalCodePtrList& getJurisdictionEndNodes() const { return _jurisdictionEndNodes; } bool isMoving() const { return _isMoving; } bool isFullScene() const { return _isFullScene; } diff --git a/libraries/shared/src/OctalCode.cpp b/libraries/shared/src/OctalCode.cpp index 85b6c2b632..d6b9759619 100644 --- a/libraries/shared/src/OctalCode.cpp +++ b/libraries/shared/src/OctalCode.cpp @@ -304,6 +304,10 @@ bool isAncestorOf(const unsigned char* possibleAncestor, const unsigned char* po return true; } +OctalCodePtr allocateOctalCodePtr(size_t size) { + return OctalCodePtr(new unsigned char[size], std::default_delete()); +} + OctalCodePtr hexStringToOctalCode(const QString& input) { const int HEX_NUMBER_BASE = 16; const int HEX_BYTE_SIZE = 2; @@ -311,7 +315,7 @@ OctalCodePtr hexStringToOctalCode(const QString& input) { int byteArrayIndex = 0; // allocate byte array based on half of string length - auto bytes = std::shared_ptr(new unsigned char[(input.length()) / HEX_BYTE_SIZE]); + auto bytes = allocateOctalCodePtr(input.length() / HEX_BYTE_SIZE); // loop through the string - 2 bytes at a time converting // it to decimal equivalent and store in byte array diff --git a/libraries/shared/src/OctalCode.h b/libraries/shared/src/OctalCode.h index 7d1424d954..50381cb0d7 100644 --- a/libraries/shared/src/OctalCode.h +++ b/libraries/shared/src/OctalCode.h @@ -61,6 +61,7 @@ typedef enum { OctalCodeComparison compareOctalCodes(const unsigned char* code1, const unsigned char* code2); +OctalCodePtr allocateOctalCodePtr(size_t size); QString octalCodeToHexString(const unsigned char* octalCode); OctalCodePtr hexStringToOctalCode(const QString& input); From 5fc18eafda6f4d5614bfbbbeecd04a30b8bc5445 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 11 May 2016 10:43:22 -0700 Subject: [PATCH 0072/1237] Rename OctalCodePtr related functions --- libraries/octree/src/JurisdictionMap.cpp | 10 +++++----- libraries/octree/src/JurisdictionMap.h | 3 ++- libraries/octree/src/OctreeSceneStats.cpp | 10 +++++----- libraries/shared/src/OctalCode.cpp | 4 ++-- libraries/shared/src/OctalCode.h | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/libraries/octree/src/JurisdictionMap.cpp b/libraries/octree/src/JurisdictionMap.cpp index 7eb976690b..1349b48b8d 100644 --- a/libraries/octree/src/JurisdictionMap.cpp +++ b/libraries/octree/src/JurisdictionMap.cpp @@ -73,7 +73,7 @@ void JurisdictionMap::copyContents(const JurisdictionMap& other) { OctalCodePtr rootOctalCode; OctalCodePtrList endNodes; - std::tie(rootOctalCode, endNodes) = other.getRootOctalCodeAndEndNodes(); + std::tie(rootOctalCode, endNodes) = other.getRootAndEndNodeOctalCodes(); init(rootOctalCode, endNodes); } @@ -83,7 +83,7 @@ JurisdictionMap::~JurisdictionMap() { JurisdictionMap::JurisdictionMap(NodeType_t type) : _rootOctalCode(nullptr) { _nodeType = type; - OctalCodePtr rootCode = allocateOctalCodePtr(1); + OctalCodePtr rootCode = createOctalCodePtr(1); *rootCode = 0; OctalCodePtrList emptyEndNodes; @@ -125,7 +125,7 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe } } -std::tuple JurisdictionMap::getRootOctalCodeAndEndNodes() const { +std::tuple JurisdictionMap::getRootAndEndNodeOctalCodes() const { std::lock_guard lock(_octalCodeMutex); return std::tuple(_rootOctalCode, _endNodes); } @@ -291,7 +291,7 @@ int JurisdictionMap::unpackFromPacket(ReceivedMessage& message) { _endNodes.clear(); if (bytes > 0 && bytes <= message.getBytesLeftToRead()) { - _rootOctalCode = allocateOctalCodePtr(bytes); + _rootOctalCode = createOctalCodePtr(bytes); message.read(reinterpret_cast(_rootOctalCode.get()), bytes); // if and only if there's a root jurisdiction, also include the end nodes @@ -303,7 +303,7 @@ int JurisdictionMap::unpackFromPacket(ReceivedMessage& message) { message.readPrimitive(&bytes); if (bytes <= message.getBytesLeftToRead()) { - auto endNodeCode = allocateOctalCodePtr(bytes); + auto endNodeCode = createOctalCodePtr(bytes); message.read(reinterpret_cast(endNodeCode.get()), bytes); // if the endNodeCode was 0 length then don't add it diff --git a/libraries/octree/src/JurisdictionMap.h b/libraries/octree/src/JurisdictionMap.h index 995b88e3ff..2e39447eb0 100644 --- a/libraries/octree/src/JurisdictionMap.h +++ b/libraries/octree/src/JurisdictionMap.h @@ -51,7 +51,8 @@ public: bool writeToFile(const char* filename); bool readFromFile(const char* filename); - std::tuple JurisdictionMap::getRootOctalCodeAndEndNodes() const; + // Provide an atomic way to get both the rootOctalCode and endNodeOctalCodes. + std::tuple JurisdictionMap::getRootAndEndNodeOctalCodes() const; OctalCodePtr getRootOctalCode() const; OctalCodePtrList getEndNodeOctalCodes() const; diff --git a/libraries/octree/src/OctreeSceneStats.cpp b/libraries/octree/src/OctreeSceneStats.cpp index f164cd2d48..fdaa6b4928 100644 --- a/libraries/octree/src/OctreeSceneStats.cpp +++ b/libraries/octree/src/OctreeSceneStats.cpp @@ -116,14 +116,14 @@ void OctreeSceneStats::copyFromOther(const OctreeSceneStats& other) { // Now copy the values from the other if (other._jurisdictionRoot) { auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(other._jurisdictionRoot.get())); - _jurisdictionRoot = allocateOctalCodePtr(bytes); + _jurisdictionRoot = createOctalCodePtr(bytes); memcpy(_jurisdictionRoot.get(), other._jurisdictionRoot.get(), bytes); } for (size_t i = 0; i < other._jurisdictionEndNodes.size(); i++) { auto& endNodeCode = other._jurisdictionEndNodes[i]; if (endNodeCode) { auto bytes = bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(endNodeCode.get())); - auto endNodeCodeCopy = allocateOctalCodePtr(bytes); + auto endNodeCodeCopy = createOctalCodePtr(bytes); memcpy(endNodeCodeCopy.get(), endNodeCode.get(), bytes); _jurisdictionEndNodes.push_back(endNodeCodeCopy); } @@ -156,7 +156,7 @@ void OctreeSceneStats::sceneStarted(bool isFullScene, bool isMoving, OctreeEleme // setup jurisdictions if (jurisdictionMap) { - std::tie(_jurisdictionRoot, _jurisdictionEndNodes) = jurisdictionMap->getRootOctalCodeAndEndNodes(); + std::tie(_jurisdictionRoot, _jurisdictionEndNodes) = jurisdictionMap->getRootAndEndNodeOctalCodes(); } else { _jurisdictionRoot = nullptr; _jurisdictionEndNodes.clear(); @@ -470,7 +470,7 @@ int OctreeSceneStats::unpackFromPacket(ReceivedMessage& packet) { _jurisdictionRoot = nullptr; _jurisdictionEndNodes.clear(); } else { - _jurisdictionRoot = allocateOctalCodePtr(bytes); + _jurisdictionRoot = createOctalCodePtr(bytes); packet.read(reinterpret_cast(_jurisdictionRoot.get()), bytes); // if and only if there's a root jurisdiction, also include the end elements @@ -484,7 +484,7 @@ int OctreeSceneStats::unpackFromPacket(ReceivedMessage& packet) { packet.readPrimitive(&bytes); - auto endNodeCode = allocateOctalCodePtr(bytes); + auto endNodeCode = createOctalCodePtr(bytes); packet.read(reinterpret_cast(endNodeCode.get()), bytes); _jurisdictionEndNodes.push_back(endNodeCode); diff --git a/libraries/shared/src/OctalCode.cpp b/libraries/shared/src/OctalCode.cpp index d6b9759619..1a0a19bf44 100644 --- a/libraries/shared/src/OctalCode.cpp +++ b/libraries/shared/src/OctalCode.cpp @@ -304,7 +304,7 @@ bool isAncestorOf(const unsigned char* possibleAncestor, const unsigned char* po return true; } -OctalCodePtr allocateOctalCodePtr(size_t size) { +OctalCodePtr createOctalCodePtr(size_t size) { return OctalCodePtr(new unsigned char[size], std::default_delete()); } @@ -315,7 +315,7 @@ OctalCodePtr hexStringToOctalCode(const QString& input) { int byteArrayIndex = 0; // allocate byte array based on half of string length - auto bytes = allocateOctalCodePtr(input.length() / HEX_BYTE_SIZE); + auto bytes = createOctalCodePtr(input.length() / HEX_BYTE_SIZE); // loop through the string - 2 bytes at a time converting // it to decimal equivalent and store in byte array diff --git a/libraries/shared/src/OctalCode.h b/libraries/shared/src/OctalCode.h index 50381cb0d7..3fd9a49390 100644 --- a/libraries/shared/src/OctalCode.h +++ b/libraries/shared/src/OctalCode.h @@ -61,7 +61,7 @@ typedef enum { OctalCodeComparison compareOctalCodes(const unsigned char* code1, const unsigned char* code2); -OctalCodePtr allocateOctalCodePtr(size_t size); +OctalCodePtr createOctalCodePtr(size_t size); QString octalCodeToHexString(const unsigned char* octalCode); OctalCodePtr hexStringToOctalCode(const QString& input); From 368eded8eec6bdd897b731bf13c03425483454d2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 11 May 2016 10:45:38 -0700 Subject: [PATCH 0073/1237] Remove unnecessary type in Jurisdictionmap --- libraries/octree/src/JurisdictionMap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/octree/src/JurisdictionMap.cpp b/libraries/octree/src/JurisdictionMap.cpp index 1349b48b8d..128a4aa162 100644 --- a/libraries/octree/src/JurisdictionMap.cpp +++ b/libraries/octree/src/JurisdictionMap.cpp @@ -99,7 +99,7 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe qCDebug(octree, "JurisdictionMap::JurisdictionMap(const char* rootHexCode=[%p] %s, const char* endNodesHexCodes=[%p] %s)", rootHexCode, rootHexCode, endNodesHexCodes, endNodesHexCodes); - _rootOctalCode = std::shared_ptr(hexStringToOctalCode(QString(rootHexCode))); + _rootOctalCode = hexStringToOctalCode(QString(rootHexCode)); qCDebug(octree, "JurisdictionMap::JurisdictionMap() _rootOctalCode=%p octalCode=", _rootOctalCode.get()); myDebugPrintOctalCode(_rootOctalCode.get(), true); From 1bec38584b52cf7770d991012f2e94d06aa3e03a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 11 May 2016 11:11:52 -0700 Subject: [PATCH 0074/1237] Remove class qualification from header file --- libraries/octree/src/JurisdictionMap.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/octree/src/JurisdictionMap.h b/libraries/octree/src/JurisdictionMap.h index 2e39447eb0..b5a311c3d9 100644 --- a/libraries/octree/src/JurisdictionMap.h +++ b/libraries/octree/src/JurisdictionMap.h @@ -52,7 +52,7 @@ public: bool readFromFile(const char* filename); // Provide an atomic way to get both the rootOctalCode and endNodeOctalCodes. - std::tuple JurisdictionMap::getRootAndEndNodeOctalCodes() const; + std::tuple getRootAndEndNodeOctalCodes() const; OctalCodePtr getRootOctalCode() const; OctalCodePtrList getEndNodeOctalCodes() const; From 475d881b043a2842806c6f3c29b8186ff829994b Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 11 May 2016 11:31:51 -0700 Subject: [PATCH 0075/1237] Fix logging of shared_ptr --- libraries/octree/src/JurisdictionMap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/octree/src/JurisdictionMap.cpp b/libraries/octree/src/JurisdictionMap.cpp index 128a4aa162..5d5d50f4ad 100644 --- a/libraries/octree/src/JurisdictionMap.cpp +++ b/libraries/octree/src/JurisdictionMap.cpp @@ -119,7 +119,7 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe //printOctalCode(endNodeOctcode); _endNodes.push_back(endNodeOctcode); - qCDebug(octree, "JurisdictionMap::JurisdictionMap() endNodeOctcode=%p octalCode=", endNodeOctcode); + qCDebug(octree, "JurisdictionMap::JurisdictionMap() endNodeOctcode=%p octalCode=", endNodeOctcode.get()); myDebugPrintOctalCode(endNodeOctcode.get(), true); } From 013d902376b332df07a70b4aaee9642ee9d03498 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 11 May 2016 11:50:00 -0700 Subject: [PATCH 0076/1237] Add vector include to OctalCode.h --- libraries/shared/src/OctalCode.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/shared/src/OctalCode.h b/libraries/shared/src/OctalCode.h index 3fd9a49390..a0d86f32d2 100644 --- a/libraries/shared/src/OctalCode.h +++ b/libraries/shared/src/OctalCode.h @@ -12,6 +12,7 @@ #ifndef hifi_OctalCode_h #define hifi_OctalCode_h +#include #include #include From 0f440835584634627ea2b24ea2105d6945be1e05 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 17 May 2016 11:13:34 -0700 Subject: [PATCH 0077/1237] FIx bad roughness value read from the deferred buffer --- libraries/render-utils/src/DeferredBufferRead.slh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index fa166300ae..569063955d 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -101,7 +101,7 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { // Unpack the normal from the map frag.normal = normalize(frag.normalVal.xyz * 2.0 - vec3(1.0)); - frag.roughness = 2.0 * frag.normalVal.a; + frag.roughness = frag.normalVal.a; // Diffuse color and unpack the mode and the metallicness frag.diffuse = frag.diffuseVal.xyz; From b82356a249f00b6678bd161d6f88eb366bf2f39f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 17 May 2016 15:02:04 -0700 Subject: [PATCH 0078/1237] AvatarMixer: Clients will show incompatible version dialog For this to work, the server needs to send an empty AvatarIdentity packet back to the sender when it receives a packet mismatch error. This AvatarIdentity packet will be different then what the client expects and will trigger the incompatible version dialog. Previously, the avatar-mixer was just silently dropping incoming mismatched version packets. Causing the client to never get a response, and thus never showing the incompatible version dialog. --- assignment-client/src/avatars/AvatarMixer.cpp | 16 ++++++++++++++++ assignment-client/src/avatars/AvatarMixer.h | 3 ++- libraries/networking/src/LimitedNodeList.cpp | 7 ++++--- libraries/networking/src/LimitedNodeList.h | 4 +++- libraries/networking/src/udt/PacketHeaders.cpp | 4 +++- libraries/networking/src/udt/PacketHeaders.h | 2 +- 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index a109934d10..610c9bcc40 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -45,6 +45,9 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::AvatarData, this, "handleAvatarDataPacket"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); + + auto nodeList = DependencyManager::get(); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); } AvatarMixer::~AvatarMixer() { @@ -509,6 +512,19 @@ void AvatarMixer::domainSettingsRequestComplete() { _broadcastThread.start(); } +void AvatarMixer::handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID) { + // if this client is using packet versions we don't expect. + if ((type == PacketTypeEnum::Value::AvatarIdentity || type == PacketTypeEnum::Value::AvatarData) && !senderUUID.isNull()) { + // Echo an empty AvatarIdentity packet back to that client. + // This should trigger a version mismatch dialog on their side. + auto nodeList = DependencyManager::get(); + auto node = nodeList->nodeWithUUID(senderUUID); + if (node) { + auto poisonPacket = NLPacket::create(PacketType::AvatarIdentity, 0); + nodeList->sendPacket(std::move(poisonPacket), *node); + } + } +} void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { const QString AVATAR_MIXER_SETTINGS_KEY = "avatar_mixer"; diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index c7761a2cba..d1a9249c83 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -38,7 +38,8 @@ private slots: void handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode); void handleKillAvatarPacket(QSharedPointer message); void domainSettingsRequestComplete(); - + void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); + private: void broadcastAvatarData(); void parseDomainServerSettings(const QJsonObject& domainSettings); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 2c10d0627e..9efe51183e 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -176,9 +176,10 @@ bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { bool hasBeenOutput = false; QString senderString; + const HifiSockAddr& senderSockAddr = packet.getSenderSockAddr(); + QUuid sourceID; if (NON_SOURCED_PACKETS.contains(headerType)) { - const HifiSockAddr& senderSockAddr = packet.getSenderSockAddr(); hasBeenOutput = versionDebugSuppressMap.contains(senderSockAddr, headerType); if (!hasBeenOutput) { @@ -186,7 +187,7 @@ bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { senderString = QString("%1:%2").arg(senderSockAddr.getAddress().toString()).arg(senderSockAddr.getPort()); } } else { - QUuid sourceID = NLPacket::sourceIDInHeader(packet); + sourceID = NLPacket::sourceIDInHeader(packet); hasBeenOutput = sourcedVersionDebugSuppressMap.contains(sourceID, headerType); @@ -201,7 +202,7 @@ bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { << senderString << "sent" << qPrintable(QString::number(headerVersion)) << "but" << qPrintable(QString::number(versionForPacketType(headerType))) << "expected."; - emit packetVersionMismatch(headerType); + emit packetVersionMismatch(headerType, senderSockAddr, sourceID); } return false; diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 0cbe9668b3..2ab8aaab39 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -236,7 +236,9 @@ public slots: signals: void dataSent(quint8 channelType, int bytes); - void packetVersionMismatch(PacketType type); + + // QUuid might be zero for non-sourced packet types. + void packetVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); void uuidChanged(const QUuid& ownerUUID, const QUuid& oldUUID); void nodeAdded(SharedNodePointer); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e4aab94090..81984521f8 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -48,9 +48,11 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: return VERSION_LIGHT_HAS_FALLOFF_RADIUS; + case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: - return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); + case PacketType::KillAvatar: + return static_cast(AvatarMixerPacketVersion::AbsoluteSixByteRotations); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetGetInfo: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e0d854ab71..4c2141dff4 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -175,7 +175,7 @@ const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, SoftAttachmentSupport, - AbsoluteFortyEightBitRotations + AbsoluteSixByteRotations }; #endif // hifi_PacketHeaders_h From 47d897f66967415d399e3ef5054dc311b522b634 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 17 May 2016 15:12:14 -0700 Subject: [PATCH 0079/1237] Fix processOctreeStats recreating JurisdictionMap for every stat packet --- interface/src/Application.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4d772d4b19..a60adeceb5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4372,8 +4372,11 @@ int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer serverType = "Entity"; } + bool found = false; + jurisdiction->withReadLock([&] { if (jurisdiction->find(nodeUUID) != jurisdiction->end()) { + found = true; return; } @@ -4384,15 +4387,18 @@ int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer qPrintable(serverType), (double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s); }); - // store jurisdiction details for later use - // This is bit of fiddling is because JurisdictionMap assumes it is the owner of the values used to construct it - // but OctreeSceneStats thinks it's just returning a reference to its contents. So we need to make a copy of the - // details from the OctreeSceneStats to construct the JurisdictionMap - JurisdictionMap jurisdictionMap; - jurisdictionMap.copyContents(octreeStats.getJurisdictionRoot(), octreeStats.getJurisdictionEndNodes()); - jurisdiction->withWriteLock([&] { - (*jurisdiction)[nodeUUID] = jurisdictionMap; - }); + + if (!found) { + // store jurisdiction details for later use + // This is bit of fiddling is because JurisdictionMap assumes it is the owner of the values used to construct it + // but OctreeSceneStats thinks it's just returning a reference to its contents. So we need to make a copy of the + // details from the OctreeSceneStats to construct the JurisdictionMap + JurisdictionMap jurisdictionMap; + jurisdictionMap.copyContents(octreeStats.getJurisdictionRoot(), octreeStats.getJurisdictionEndNodes()); + jurisdiction->withWriteLock([&] { + (*jurisdiction)[nodeUUID] = jurisdictionMap; + }); + } }); return statsMessageLength; From 68665e6b2579fa63e9cf0af6ce293af78afdf1dd Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 17 May 2016 15:12:45 -0700 Subject: [PATCH 0080/1237] Fix JurisdictionMap not initializing correctly --- libraries/octree/src/JurisdictionMap.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/octree/src/JurisdictionMap.cpp b/libraries/octree/src/JurisdictionMap.cpp index 5d5d50f4ad..fcbc085339 100644 --- a/libraries/octree/src/JurisdictionMap.cpp +++ b/libraries/octree/src/JurisdictionMap.cpp @@ -64,7 +64,14 @@ JurisdictionMap::JurisdictionMap(const JurisdictionMap& other) : _rootOctalCode( } void JurisdictionMap::copyContents(const OctalCodePtr& rootCodeIn, const OctalCodePtrList& endNodesIn) { - init(rootCodeIn, endNodesIn); + OctalCodePtr rootCode = rootCodeIn; + if (!rootCode) { + rootCode = createOctalCodePtr(1); + *rootCode = 0; + } + + OctalCodePtrList emptyEndNodes; + init(rootCode, endNodesIn); } void JurisdictionMap::copyContents(const JurisdictionMap& other) { From dc6e1afae668ddb683a9ce6b4413cee30a9b00e1 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 17 May 2016 16:34:30 -0700 Subject: [PATCH 0081/1237] Changed empty AvatarIdentity packet to AvatarData packet Just in-case it actually gets through, it will fail to be parsed by AvatarData::parseDataFromBuffer() due to it's size. AvatarData::hasIdentityChangedAfterParsing() has no such checks. --- assignment-client/src/avatars/AvatarMixer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 610c9bcc40..cc94e4f1b7 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -515,13 +515,13 @@ void AvatarMixer::domainSettingsRequestComplete() { void AvatarMixer::handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID) { // if this client is using packet versions we don't expect. if ((type == PacketTypeEnum::Value::AvatarIdentity || type == PacketTypeEnum::Value::AvatarData) && !senderUUID.isNull()) { - // Echo an empty AvatarIdentity packet back to that client. + // Echo an empty AvatarData packet back to that client. // This should trigger a version mismatch dialog on their side. auto nodeList = DependencyManager::get(); auto node = nodeList->nodeWithUUID(senderUUID); if (node) { - auto poisonPacket = NLPacket::create(PacketType::AvatarIdentity, 0); - nodeList->sendPacket(std::move(poisonPacket), *node); + auto emptyPacket = NLPacket::create(PacketType::AvatarData, 0); + nodeList->sendPacket(std::move(emptyPacket), *node); } } } From 2c6b0e5c95f4aa86c3096db047809def5bae66b7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 17 May 2016 17:32:09 -0700 Subject: [PATCH 0082/1237] Fix for linux warning. --- libraries/shared/src/GLMHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index e81958f407..e73e936fb9 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -151,7 +151,7 @@ int packOrientationQuatToSixBytes(unsigned char* buffer, const glm::quat& quatIn // ensure that the sign of the dropped component is always negative. glm::quat q = quatInput[largestComponent] > 0 ? -quatInput : quatInput; - const float MAGNITUDE = 1.0f / sqrt(2.0f); + const float MAGNITUDE = 1.0f / sqrtf(2.0f); const uint32_t NUM_BITS_PER_COMPONENT = 15; const uint32_t RANGE = (1 << NUM_BITS_PER_COMPONENT) - 1; From 17d41dd7b977c833e97961c64df3752ed574a73b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 18 May 2016 13:25:11 +1200 Subject: [PATCH 0083/1237] HiFi glyphs font file update with fixed up/down carats Update QML and HTML spinboxes accordingly. --- interface/resources/fonts/hifi-glyphs.ttf | Bin 23944 -> 24204 bytes .../resources/qml/controls-uit/SpinBox.qml | 6 +++--- .../qml/styles-uit/HifiConstants.qml | 2 +- scripts/system/html/edit-style.css | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 1b0d4f3fe61a60a3cf499f3557dd29bb924bc924..89d47670122b68a27ce80d77da0227390d817ac1 100644 GIT binary patch delta 960 zcmYL|TTGi}7{`Ba`yriL4&&T@UxC6X9V?WwavHFXL17!GaVYJ8!pE_&GDtz0GsrG> zu`Ih-{H88*F_Q~ixG-j&-iTg!VUR4*L?b3M1e3)qlPfc$$zm|=miQ!Z-v9g0`#gCM zzvt?g;`&!&1pyejjGiv9x3_(Px3?fT56CS|&0cQt3Iw}=yw097SLa4s2M9ml8=F<< z`~F?;0NoAHS*O&6`Tc$L5NrhuQ{lHJo$}rqBwUB?kkN-@vaxWhgYUqDoG& z3nCpT49zZH%zm2o9f;3>o$%b4S~l2R1kwZGms$1VJewj2qB?e<3+n8|AC;Rv=-&eo zou6A+Z2mCmJqX+dX)S&^0jW=3T^dfV`-|6f#NtOay!>L;g_qwI_2RtNAcn*N?T#4I zo{JG}SXWx}1O05X9Z%3he8};N2pN4`$w;yH!+?PorQg{-pl#@smOAQb;5a8}qsG*i>`sv~^VHQh96!-`@^rJe@6?&Lc2~EfqjP$u zwaHUcTixGv)-Nl$Q+fH;g2F>ZwqpC?k|UL+WzO=dilgqHq23nXG0iBMq(E?BP%D?N zXdcO+eJ<6lIA8zPc*NLb3>#OCJ4vRb_2iD^pHfCsK22>+T~Av~dy?);|04ZqhLjP> zxSx4Ab24)^>tI$m>+9^U?DunAIk!zIrbg3M)6Zs$dD6UL{#~}nL3vC5N0F2irB)eL zZYVnzmu1v))ABg?UY;-SL4Gj*8|!XCcfnra{q!Ph(L-CC?UHS?_;T@Gd$zsM?u=eZ z_#i7fpj@*?Gd#PQT8`q;?B#ztws|BR;$ZBT4%sS%uEh7K3Iy9c77k&K`68i_TZ5K delta 954 zcmY+CTTGjE7{-79wm`=y$I{ZnR|iUW=)K7P7OD@g{aX8H`CySUJ!PN#%MB3nK!=4oA=3+H&31??;Cw0 zZao&O2*ALbXz6Wh8|;pJxV;X+dBEP@(%Qy-5+T?HIC};>-k#69xxOTunxNSkHQJ~?aPn0CerHu;JAi({a9|F9h{X3#w%&9&R6ngcc2#W93zl5UQ zVMh&UJRah4j@o{thOnYNLUwe#jK@UJE7=>ndi5)fv|5jk2F}q$Gc8=8jdlWb(nU8H zDd!TW>7kcC`Z-T4jns0MGmJ36Afwok$i+z>`4k{iND(fIIl)OvIE9;PJd{#~ml`Ul zqzXTm8KQ$>>eMl9rDY6-aV7|Ig-NEEW`d)s>>o^>rkkcsv)3Fo|B@5V`NYz1`OI2sy<%Or z{%P~sZrYyP6YW0x4f~#yBgs;!R3~*v5$S>Sjl<-ab3AZt<=S(1o$omJ^X}#SU65le zxGl@_UeSn4=UQ_;DyH~Th(#0bo7H#yzF3|V&G!g)cq-h2_zeqPMEcZKR6zANU`&Ea*u7!ZTv68UOmL{{G$ z`A45LvlMwYO diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controls-uit/SpinBox.qml index 599d94c28d..5d78cfc5a8 100755 --- a/interface/resources/qml/controls-uit/SpinBox.qml +++ b/interface/resources/qml/controls-uit/SpinBox.qml @@ -56,7 +56,7 @@ SpinBox { incrementControl: HiFiGlyphs { id: incrementButton text: hifi.glyphs.caratUp - x: 6 + x: 10 y: 1 size: hifi.dimensions.spinnerSize color: styleData.upPressed ? (isLightColorScheme ? hifi.colors.black : hifi.colors.white) : hifi.colors.gray @@ -64,8 +64,8 @@ SpinBox { decrementControl: HiFiGlyphs { text: hifi.glyphs.caratDn - x: 6 - y: -3 + x: 10 + y: -1 size: hifi.dimensions.spinnerSize color: styleData.downPressed ? (isLightColorScheme ? hifi.colors.black : hifi.colors.white) : hifi.colors.gray } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 640fe8625b..16f150b2d9 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -141,7 +141,7 @@ Item { readonly property real textPadding: 8 readonly property real sliderHandleSize: 18 readonly property real sliderGrooveHeight: 8 - readonly property real spinnerSize: 42 + readonly property real spinnerSize: 50 readonly property real tablePadding: 12 readonly property real tableRowHeight: largeScreen ? 26 : 23 readonly property real tableHeaderHeight: 29 diff --git a/scripts/system/html/edit-style.css b/scripts/system/html/edit-style.css index 5eaa3c6497..4083e580b5 100644 --- a/scripts/system/html/edit-style.css +++ b/scripts/system/html/edit-style.css @@ -268,7 +268,7 @@ input[type=number]::-webkit-inner-spin-button { height: 100%; overflow: hidden; font-family: hifi-glyphs; - font-size: 50px; + font-size: 46px; color: #afafaf; cursor: pointer; /*background-color: #000000;*/ @@ -276,17 +276,17 @@ input[type=number]::-webkit-inner-spin-button { input[type=number]::-webkit-inner-spin-button:before, input[type=number]::-webkit-inner-spin-button:after { position:absolute; - left: -21px; + left: -19px; line-height: 8px; text-align: center; } input[type=number]::-webkit-inner-spin-button:before { content: "6"; - top: 5px; + top: 4px; } input[type=number]::-webkit-inner-spin-button:after { content: "5"; - bottom: 6px; + bottom: 4px; } input[type=number].hover-up::-webkit-inner-spin-button:before, From e1d885004f55b98681f94ebc4e92170cad584472 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 18 May 2016 13:59:13 +1200 Subject: [PATCH 0084/1237] Fix carat in drop-down boxes --- interface/resources/qml/controls-uit/ComboBox.qml | 2 +- scripts/system/html/edit-style.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index f99e18b12b..cd6dc8ede0 100755 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -91,7 +91,7 @@ FocusScope { HiFiGlyphs { anchors { top: parent.top - topMargin: -8 + topMargin: -11 horizontalCenter: parent.horizontalCenter } size: hifi.dimensions.spinnerSize diff --git a/scripts/system/html/edit-style.css b/scripts/system/html/edit-style.css index 4083e580b5..19d1cd95a9 100644 --- a/scripts/system/html/edit-style.css +++ b/scripts/system/html/edit-style.css @@ -613,7 +613,7 @@ hr { margin-right: -48px; position: relative; left: -12px; - top: -11px; + top: -9px; } .dropdown dd { From cf829f1babf58f4d442bdebdb8696f2c7f164fdf Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 17 May 2016 19:14:44 -0700 Subject: [PATCH 0085/1237] magic number --- interface/src/avatar/Avatar.cpp | 2 +- libraries/entities/src/SimulationOwner.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 95a3ef6132..efaed5b83a 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -220,7 +220,7 @@ void Avatar::updateAvatarEntities() { // there's no entity-server to tell us we're the simulation owner, so always set the // simulationOwner to the owningAvatarID and a high priority. - properties.setSimulationOwner(getID(), 129); + properties.setSimulationOwner(getID(), AVATAR_ENTITY_SIMULATION_PRIORITY); if (properties.getParentID() == AVATAR_SELF_ID) { properties.setParentID(getID()); diff --git a/libraries/entities/src/SimulationOwner.h b/libraries/entities/src/SimulationOwner.h index 1afec426d7..5f940bbe25 100644 --- a/libraries/entities/src/SimulationOwner.h +++ b/libraries/entities/src/SimulationOwner.h @@ -27,6 +27,7 @@ const quint8 RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1; // When poking objects with scripts an observer will bid at SCRIPT_EDIT priority. const quint8 SCRIPT_GRAB_SIMULATION_PRIORITY = 0x80; const quint8 SCRIPT_POKE_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY - 1; +const quint8 AVATAR_ENTITY_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY + 1; // PERSONAL priority (needs a better name) is the level at which a simulation observer owns its own avatar // which really just means: things that collide with it will be bid at a priority level one lower From 59e17c2d5f66e801c889349f114cb3c154e4e54e Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 17 May 2016 22:13:56 -0700 Subject: [PATCH 0086/1237] Fix abs of unsigned warning --- libraries/shared/src/GLMHelpers.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index bd8bffefd2..784e9ce5af 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -435,8 +435,8 @@ void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& seconda #ifndef NDEBUG const float MIN_LENGTH_SQUARED = 1.0e-6f; #endif - assert(fabsf(glm::length2(primaryAxis) > MIN_LENGTH_SQUARED)); - assert(fabsf(glm::length2(secondaryAxis) > MIN_LENGTH_SQUARED)); + assert(glm::length2(primaryAxis) > MIN_LENGTH_SQUARED); + assert(glm::length2(secondaryAxis) > MIN_LENGTH_SQUARED); uAxisOut = glm::normalize(primaryAxis); glm::vec3 normSecondary = glm::normalize(secondaryAxis); From 17b617f1e181e44ce9b5cfe76b9136186e0bcc31 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 May 2016 10:23:45 -0700 Subject: [PATCH 0087/1237] umbrella --- .../Home/kineticObjects/bench.json | 36 ++ .../Home/kineticObjects/blueChair.json | 37 -- .../kineticObjects/dressingRoomBricabrac.json | 577 ++++++++++++++++++ .../Home/kineticObjects/umbrella.json | 38 ++ .../Home/kineticObjects/wrapper.js | 142 +++-- .../DomainContent/Home/reset.js | 25 +- 6 files changed, 763 insertions(+), 92 deletions(-) create mode 100644 unpublishedScripts/DomainContent/Home/kineticObjects/bench.json delete mode 100644 unpublishedScripts/DomainContent/Home/kineticObjects/blueChair.json create mode 100644 unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json create mode 100644 unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/bench.json b/unpublishedScripts/DomainContent/Home/kineticObjects/bench.json new file mode 100644 index 0000000000..d7306155a6 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/bench.json @@ -0,0 +1,36 @@ +{ + "Entities": [{ + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/kineticObjects/bench/Bench.obj", + "created": "2016-05-17T19:27:31Z", + "dimensions": { + "x": 2.3647537231445312, + "y": 0.51260757446289062, + "z": 0.49234861135482788 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{733c3f02-a98a-4876-91dc-320a26a07090}", + "modelURL": "atp:/kineticObjects/bench/Bench.fbx", + "queryAACube": { + "scale": 2.4692578315734863, + "x": -1.2346289157867432, + "y": -1.2346289157867432, + "z": -1.2346289157867432 + }, + "rotation": { + "w": 0.88613712787628174, + "x": -0.0015716552734375, + "y": -0.46337074041366577, + "z": -1.52587890625e-05 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/blueChair.json b/unpublishedScripts/DomainContent/Home/kineticObjects/blueChair.json deleted file mode 100644 index 2556cd6223..0000000000 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/blueChair.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "Entities": [{ - "collisionsWillMove": 1, - "compoundShapeURL": "atp:/kineticObjects/blueChair/Comfy_Chair_Blue_hull.obj", - "created": "2016-03-29T17:37:52Z", - "dimensions": { - "x": 1.1764, - "y": 1.4557, - "z": 1.2657 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "id": "{51a44c3a-ec4a-4c79-8034-aeb5c45660b5}", - "modelURL": "atp:/kineticObjects/blueChair/Comfy_Chair_Blue.fbx", - "name": "home_model_comfyChair", - "queryAACube": { - "scale": 1.9147497415542603, - "x": -0.95737487077713013, - "y": -0.95737487077713013, - "z": -0.95737487077713013 - }, - "rotation": { - "w": 0.46746015548706055, - "x": -0.0017547607421875, - "y": 0.88400089740753174, - "z": 0.0024261474609375 - }, - "shapeType": "compound", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }], - "Version": 57 -} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json new file mode 100644 index 0000000000..8b19e16901 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json @@ -0,0 +1,577 @@ +{ + "Entities": [{ + "collisionsWillMove": 1, + "compoundShapeURL": "https://hifi-content.s3.amazonaws.com/DomainContent/Home/skully/573a170808bd0_vhacd_skully.obj", + "created": "2016-05-16T19:34:28Z", + "dimensions": { + "x": 0.19080814719200134, + "y": 0.20009028911590576, + "z": 0.21198928356170654 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -6, + "z": 0 + }, + "id": "{cef37b0a-bb3d-4549-a645-f3a934f82148}", + "modelURL": "https://hifi-content.s3.amazonaws.com/DomainContent/Home/skully/skully.fbx", + "position": { + "x": 0.254150390625, + "y": 0.4500732421875, + "z": 0.26517486572265625 + }, + "queryAACube": { + "scale": 0.34840109944343567, + "x": 0.079949840903282166, + "y": 0.27587270736694336, + "z": 0.090974316000938416 + }, + "rotation": { + "w": 0.11515986919403076, + "x": -0.087693572044372559, + "y": 0.88818192481994629, + "z": -0.43608760833740234 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/dressingRoom/trump.obj", + "created": "2016-05-18T00:02:46Z", + "dimensions": { + "x": 0.25387090444564819, + "y": 0.28642392158508301, + "z": 0.31468403339385986 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{6483a752-5ba0-4a02-bee5-3290781dfef8}", + "modelURL": "atp:/dressingRoom/trump.fbx", + "position": { + "x": 0.3887939453125, + "y": 0.879974365234375, + "z": 0.2674407958984375 + }, + "queryAACube": { + "scale": 0.49549484252929688, + "x": 0.14104652404785156, + "y": 0.63222694396972656, + "z": 0.019693374633789062 + }, + "rotation": { + "w": 0.58565652370452881, + "x": -0.039444565773010254, + "y": 0.73434042930603027, + "z": -0.34084075689315796 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/dressingRoom/dreads.obj", + "created": "2016-05-18T00:02:46Z", + "dimensions": { + "x": 0.30918264389038086, + "y": 0.31221473217010498, + "z": 0.29840445518493652 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{2f2fb50b-35f0-4818-9cfa-e29f7aeb7a1f}", + "modelURL": "atp:/dressingRoom/dreads.fbx", + "position": { + "x": 0.5389404296875, + "y": 0.52008056640625, + "z": 0.09856414794921875 + }, + "queryAACube": { + "scale": 0.53114700317382812, + "x": 0.27336692810058594, + "y": 0.25450706481933594, + "z": -0.16700935363769531 + }, + "rotation": { + "w": 0.85949492454528809, + "x": 0.051773905754089355, + "y": -0.41289389133453369, + "z": 0.29680323600769043 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/tophat/tophat.obj", + "created": "2016-05-16T16:59:22Z", + "dimensions": { + "x": 0.38495281338691711, + "y": 0.26857495307922363, + "z": 0.39648750424385071 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{8cff801b-cb50-47d2-a1e8-4f7ee245d2ee}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/tophat/tophat.fbx", + "position": { + "x": 0, + "y": 0.541900634765625, + "z": 0.4959869384765625 + }, + "queryAACube": { + "scale": 0.61442941427230835, + "x": -0.30721470713615417, + "y": 0.23468592762947083, + "z": 0.18877223134040833 + }, + "rotation": { + "w": 0.92727553844451904, + "x": -0.18382543325424194, + "y": 0.32610058784484863, + "z": 0.0003204345703125 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m2.obj", + "created": "2016-05-16T17:12:28Z", + "dimensions": { + "x": 0.094467177987098694, + "y": 0.079604864120483398, + "z": 0.029766213148832321 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{0805851a-4ef1-4075-b4b3-5692ca7d6352}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m2.fbx", + "position": { + "x": 0.0111083984375, + "y": 0.00689697265625, + "z": 0.45806884765625 + }, + "queryAACube": { + "scale": 0.12707087397575378, + "x": -0.052427038550376892, + "y": -0.056638464331626892, + "z": 0.39453339576721191 + }, + "rotation": { + "w": 0.84777605533599854, + "x": 0.43642330169677734, + "y": 0.26695656776428223, + "z": -0.13978791236877441 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/dressingRoom/rectangleFrames.obj", + "created": "2016-05-18T15:50:52Z", + "dimensions": { + "x": 0.18497957289218903, + "y": 0.048084259033203125, + "z": 0.14581345021724701 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{692df470-2a76-4d5b-971b-257291ff67f7}", + "modelURL": "atp:/dressingRoom/rectangleFrames.fbx", + "name": "rectangleFrames.fbx", + "position": { + "x": 0.3701171875, + "y": 0.062652587890625, + "z": 0.35735321044921875 + }, + "queryAACube": { + "scale": 0.24039779603481293, + "x": 0.24991828203201294, + "y": -0.057546310126781464, + "z": 0.23715430498123169 + }, + "rotation": { + "w": -0.084428191184997559, + "x": -0.10658425092697144, + "y": 0.9651484489440918, + "z": -0.2235599160194397 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b3.obj", + "created": "2016-05-16T17:10:17Z", + "dimensions": { + "x": 0.19054701924324036, + "y": 0.16880631446838379, + "z": 0.11410932987928391 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{32926ddc-7c3b-49ca-97da-56e69af211af}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b3.fbx", + "position": { + "x": 0.75, + "y": 0.0643310546875, + "z": 0 + }, + "queryAACube": { + "scale": 0.27897074818611145, + "x": 0.61051464080810547, + "y": -0.075154319405555725, + "z": -0.13948537409305573 + }, + "rotation": { + "w": -0.26207369565963745, + "x": 0.055374979972839355, + "y": 0.93298232555389404, + "z": 0.24034488201141357 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b1.obj", + "created": "2016-05-16T17:05:55Z", + "dimensions": { + "x": 0.18918535113334656, + "y": 0.15465652942657471, + "z": 0.11267256736755371 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{c412654f-9feb-489d-91f9-6f4eb369afe7}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b1.fbx", + "position": { + "x": 0.55712890625, + "y": 0.0443115234375, + "z": 0.0774078369140625 + }, + "queryAACube": { + "scale": 0.2690814733505249, + "x": 0.42258816957473755, + "y": -0.090229213237762451, + "z": -0.057132899761199951 + }, + "rotation": { + "w": -0.26680397987365723, + "x": 0.16276800632476807, + "y": 0.81954681873321533, + "z": 0.4802471399307251 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m3.obj", + "created": "2016-05-16T17:12:28Z", + "dimensions": { + "x": 0.15590831637382507, + "y": 0.057838320732116699, + "z": 0.033922493457794189 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{c2df6722-e970-4c36-bf42-b117e2bc13c4}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m3.fbx", + "position": { + "x": 0.1549072265625, + "y": 0.006439208984375, + "z": 0.34037017822265625 + }, + "queryAACube": { + "scale": 0.16971567273139954, + "x": 0.070049390196800232, + "y": -0.078418627381324768, + "z": 0.25551235675811768 + }, + "rotation": { + "w": -0.2230105996131897, + "x": 0.19484245777130127, + "y": 0.73751425743103027, + "z": 0.60689711570739746 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/dressingRoom/Elvis.obj", + "created": "2016-05-17T23:58:24Z", + "dimensions": { + "x": 0.22462925314903259, + "y": 0.32056856155395508, + "z": 0.32186508178710938 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{5fe0d835-e52b-432b-8ffe-2e7467613974}", + "modelURL": "atp:/dressingRoom/Elvis.fbx", + "position": { + "x": 0.6776123046875, + "y": 0.89739990234375, + "z": 0.036468505859375 + }, + "queryAACube": { + "scale": 0.50677376985549927, + "x": 0.42422541975975037, + "y": 0.64401304721832275, + "z": -0.21691837906837463 + }, + "rotation": { + "w": 0.76330208778381348, + "x": 0.26140224933624268, + "y": -0.43398183584213257, + "z": 0.40090024471282959 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/dressingRoom/roundFrames.obj", + "created": "2016-05-18T15:55:14Z", + "dimensions": { + "x": 0.18497957289218903, + "y": 0.061892151832580566, + "z": 0.14581345021724701 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{2706fd7c-9f99-4e11-aebe-4c7274d29d69}", + "modelURL": "atp:/dressingRoom/roundFrames.fbx", + "name": "roundFrames.fbx", + "position": { + "x": 0.1690673828125, + "y": 0.08319091796875, + "z": 0.50496673583984375 + }, + "queryAACube": { + "scale": 0.24353571236133575, + "x": 0.047299526631832123, + "y": -0.038576938211917877, + "z": 0.38319888710975647 + }, + "rotation": { + "w": -0.27809566259384155, + "x": -0.059800088405609131, + "y": 0.93838405609130859, + "z": -0.19615471363067627 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m1.obj", + "created": "2016-05-16T17:10:17Z", + "dimensions": { + "x": 0.10473950952291489, + "y": 0.044521570205688477, + "z": 0.032888296991586685 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{448c53c2-bf1e-44f5-b28f-06da33a4145d}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m1.fbx", + "position": { + "x": 0.29052734375, + "y": 0, + "z": 0.254638671875 + }, + "queryAACube": { + "scale": 0.11846592277288437, + "x": 0.23129437863826752, + "y": -0.059232961386442184, + "z": 0.19540570676326752 + }, + "rotation": { + "w": -0.24409854412078857, + "x": -0.17210650444030762, + "y": 0.76794075965881348, + "z": -0.56667429208755493 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b2.obj", + "created": "2016-05-16T17:05:55Z", + "dimensions": { + "x": 0.12604480981826782, + "y": 0.090894937515258789, + "z": 0.069697588682174683 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{33d3ba77-d2c9-4a5f-ba1c-f6f731d05221}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b2.fbx", + "position": { + "x": 0.4130859375, + "y": 0.030181884765625, + "z": 0.182342529296875 + }, + "queryAACube": { + "scale": 0.1703142374753952, + "x": 0.3279288113117218, + "y": -0.054975233972072601, + "z": 0.097185410559177399 + }, + "rotation": { + "w": -0.2856946587562561, + "x": 0.17711150646209717, + "y": 0.86263823509216309, + "z": 0.37792015075683594 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/snapback/snapbackFinal.OBJ", + "created": "2016-05-16T17:01:33Z", + "dimensions": { + "x": 0.19942393898963928, + "y": 0.12862896919250488, + "z": 0.30034130811691284 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{26e895b7-4a83-45fe-a3e9-5b6ba5f5717e}", + "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/snapback/snapback.fbx", + "position": { + "x": 0.7891845703125, + "y": 0.492218017578125, + "z": 0.0359649658203125 + }, + "queryAACube": { + "scale": 0.38277959823608398, + "x": 0.59779477119445801, + "y": 0.30082821846008301, + "z": -0.15542483329772949 + }, + "rotation": { + "w": -0.31783014535903931, + "x": -0.04985123872756958, + "y": 0.92556643486022949, + "z": -0.19945067167282104 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/dressingRoom/afro.obj", + "created": "2016-05-17T23:58:24Z", + "dimensions": { + "x": 0.35398837924003601, + "y": 0.33958616852760315, + "z": 0.35055956244468689 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{d163c628-8247-4d13-a465-c1692825b4fe}", + "modelURL": "atp:/dressingRoom/afro.fbx", + "position": { + "x": 0.1300048828125, + "y": 0.9853515625, + "z": 0.4675140380859375 + }, + "queryAACube": { + "scale": 0.60292500257492065, + "x": -0.17145761847496033, + "y": 0.68388903141021729, + "z": 0.16605153679847717 + }, + "rotation": { + "w": 0.73046457767486572, + "x": 0.14757001399993896, + "y": 0.58101773262023926, + "z": -0.32719922065734863 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json b/unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json new file mode 100644 index 0000000000..9d0386a339 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json @@ -0,0 +1,38 @@ +{ + "Entities": [{ + "collidesWith": "static,dynamic,kinematic,otherAvatar,", + "collisionMask": 23, + "collisionsWillMove": 1, + "compoundShapeURL": "atp:/kineticObjects/umbrellaopen_ch.obj", + "created": "2016-05-16T22:48:54Z", + "dimensions": { + "x": 1.2649545669555664, + "y": 1.0218980312347412, + "z": 1.2649545669555664 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -5, + "z": 0 + }, + "id": "{59f5cc21-c6e8-4deb-a1e2-0364e82fe062}", + "modelURL": "atp:/kineticObjects/umbrella_open.fbx", + "queryAACube": { + "scale": 2.0602173805236816, + "x": -1.0301086902618408, + "y": -1.0301086902618408, + "z": -1.0301086902618408 + }, + "rotation": { + "w": 0.66066992282867432, + "x": -0.2207522988319397, + "y": 0.64501416683197021, + "z": -0.31422901153564453 + }, + "shapeType": "compound", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js b/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js index 5b0e75dc94..5ddea20560 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js @@ -1,24 +1,23 @@ -print('HOME KINETIC INCLUDING WRAPPER') - -var BOOKS_URL = "atp:/kineticObjects/books.json" -var UPPER_BOOKSHELF_URL = "atp:/kineticObjects/upperBookShelf.json" -var LOWER_BOOKSHELF_URL = "atp:/kineticObjects/lowerBookShelf.json" +print('HOME KINETIC INCLUDING WRAPPER'); +var BOOKS_URL = "atp:/kineticObjects/books.json"; +var UPPER_BOOKSHELF_URL = "atp:/kineticObjects/upperBookShelf.json"; +var LOWER_BOOKSHELF_URL = "atp:/kineticObjects/lowerBookShelf.json"; var CHAIR_URL = 'atp:/kineticObjects/deskChair.json'; -var BLUE_CHAIR_URL = 'atp:/kineticObjects/blueChair.json'; - var FRUIT_BOWL_URL = "atp:/kineticObjects/fruit.json" - -var LIVING_ROOM_LAMP_URL = "atp:/kineticObjects/deskLamp.json" +var LIVING_ROOM_LAMP_URL = "atp:/kineticObjects/deskLamp.json"; var TRASHCAN_URL = "atp:/kineticObjects/trashcan.json" var BLOCKS_URL = "atp:/kineticObjects/blocks.json"; -var PLAYA_POSTER_URL = "atp:/kineticObjects/postersPlaya.json" -var CELL_POSTER_URL = "atp:/kineticObjects/postersCell.json" -var STUFF_ON_SHELVES_URL = "atp:/kineticObjects/stuff_on_shelves.json" -var JUNK_URL = "atp:/kineticObjects/junk.json" +var PLAYA_POSTER_URL = "atp:/kineticObjects/postersPlaya.json"; +var CELL_POSTER_URL = "atp:/kineticObjects/postersCell.json"; +var STUFF_ON_SHELVES_URL = "atp:/kineticObjects/stuff_on_shelves.json"; +var JUNK_URL = "atp:/kineticObjects/junk.json"; +var BRICABRAC_URL = "atp:/kineticObjects/dressingRoomBricabrac.json"; +var BENCH_URL = "atp:/kineticObjects/bench.json"; +var UMBRELLA_URL = "atp:/kineticObjects/umbrella.json"; FruitBowl = function(spawnLocation, spawnRotation) { - print('CREATE FRUIT BOWL') + print('CREATE FRUIT BOWL'); var created = []; function create() { @@ -42,7 +41,7 @@ FruitBowl = function(spawnLocation, spawnRotation) { } LivingRoomLamp = function(spawnLocation, spawnRotation) { - print('CREATE LIVING ROOM LAMP') + print('CREATE LIVING ROOM LAMP'); var created = []; function create() { @@ -65,7 +64,7 @@ LivingRoomLamp = function(spawnLocation, spawnRotation) { } UpperBookShelf = function(spawnLocation, spawnRotation) { - print('CREATE UPPER SHELF') + print('CREATE UPPER SHELF'); var created = []; function create() { @@ -89,7 +88,7 @@ UpperBookShelf = function(spawnLocation, spawnRotation) { LowerBookShelf = function(spawnLocation, spawnRotation) { - print('CREATE LOWER SHELF') + print('CREATE LOWER SHELF'); var created = []; function create() { @@ -112,7 +111,7 @@ LowerBookShelf = function(spawnLocation, spawnRotation) { } Chair = function(spawnLocation, spawnRotation) { - print('CREATE CHAIR') + print('CREATE CHAIR'); var created = []; function create() { @@ -134,32 +133,8 @@ Chair = function(spawnLocation, spawnRotation) { this.cleanup = cleanup; } -BlueChair = function(spawnLocation, spawnRotation) { - print('CREATE BLUE CHAIR') - var created = []; - - function create() { - var success = Clipboard.importEntities(BLUE_CHAIR_URL); - - if (success === true) { - created = Clipboard.pasteEntities(spawnLocation) - print('created ' + created); - } - }; - - function cleanup() { - created.forEach(function(obj) { - Entities.deleteEntity(obj); - }) - }; - - create(); - - this.cleanup = cleanup; -} - Trashcan = function(spawnLocation, spawnRotation) { - print('CREATE TRASHCAN') + print('CREATE TRASHCAN'); var created = []; function create() { @@ -183,7 +158,7 @@ Trashcan = function(spawnLocation, spawnRotation) { } Books = function(spawnLocation, spawnRotation) { - print('CREATE BOOKS') + print('CREATE BOOKS'); var created = []; function create() { @@ -206,7 +181,7 @@ Books = function(spawnLocation, spawnRotation) { } Blocks = function(spawnLocation, spawnRotation) { - print('EBL CREATE BLOCKS') + print('EBL CREATE BLOCKS'); var created = []; function create() { @@ -230,7 +205,7 @@ Blocks = function(spawnLocation, spawnRotation) { } PosterCell = function(spawnLocation, spawnRotation) { - print('CREATE CELL POSTER') + print('CREATE CELL POSTER'); var created = []; function create() { @@ -253,7 +228,7 @@ PosterCell = function(spawnLocation, spawnRotation) { } PosterPlaya = function(spawnLocation, spawnRotation) { - print('CREATE PLAYA POSTER') + print('CREATE PLAYA POSTER'); var created = []; function create() { @@ -276,7 +251,7 @@ PosterPlaya = function(spawnLocation, spawnRotation) { } StuffOnShelves = function(spawnLocation, spawnRotation) { - print('CREATE STUFF ON SHELVES') + print('CREATE STUFF ON SHELVES'); var created = []; function create() { @@ -299,7 +274,7 @@ StuffOnShelves = function(spawnLocation, spawnRotation) { } HomeJunk = function(spawnLocation, spawnRotation) { - print('HOME CREATE JUNK') + print('HOME CREATE JUNK'); var created = []; function create() { @@ -318,5 +293,74 @@ HomeJunk = function(spawnLocation, spawnRotation) { create(); + this.cleanup = cleanup; +} + +Bricabrac = function(spawnLocation, spawnRotation) { + print('HOME CREATE BRICABRAC'); + var created = []; + + function create() { + var success = Clipboard.importEntities(BRICABRAC_URL); + if (success === true) { + created = Clipboard.pasteEntities(spawnLocation) + print('created ' + created); + } + }; + + function cleanup() { + created.forEach(function(obj) { + Entities.deleteEntity(obj); + }) + }; + + create(); + + this.cleanup = cleanup; +} + +Bench = function(spawnLocation, spawnRotation) { + print('HOME CREATE BENCH'); + var created = []; + + function create() { + var success = Clipboard.importEntities(BENCH_URL); + if (success === true) { + created = Clipboard.pasteEntities(spawnLocation) + print('created ' + created); + } + }; + + function cleanup() { + created.forEach(function(obj) { + Entities.deleteEntity(obj); + }) + }; + + create(); + + this.cleanup = cleanup; +} + +Umbrella = function(spawnLocation, spawnRotation) { + print('HOME CREATE Umbrella'); + var created = []; + + function create() { + var success = Clipboard.importEntities(UMBRELLA_URL); + if (success === true) { + created = Clipboard.pasteEntities(spawnLocation) + print('created ' + created); + } + }; + + function cleanup() { + created.forEach(function(obj) { + Entities.deleteEntity(obj); + }) + }; + + create(); + this.cleanup = cleanup; } \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index f6e5a01c7c..31a4433b91 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -380,12 +380,6 @@ z: -79.8097 }); - var blueChair = new BlueChair({ - x: 1100.4821, - y: 459.9147, - z: -75.9071 - }); - var stuffOnShelves = new StuffOnShelves({ x: 1105.9432, @@ -422,6 +416,25 @@ y: 461, z: -73.3 }); + + var dressingRoomBricabrac = new Bricabrac({ + x: 1107.6450, + y: 460.4309, + z: -72.6496 + }); + + var bench = new Bench({ + x: 1100.1210, + y: 459.4552, + z: -75.4537 + }); + + var umbrella = new Umbrella({ + x: 1097.5510, + y: 459.5230, + z: -84.3897 + }); + print('HOME after creating kinetic entities'); }, From 10c412d8c7c2ff06573ec4f3d631f45bdf79a045 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 16 May 2016 18:43:59 -0700 Subject: [PATCH 0088/1237] Fix settings getting reset fixes bugzid:531 When a setting handle was created but never initialized/used, it would override the current value with the default value on destruction. --- libraries/shared/src/SettingInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/SettingInterface.cpp b/libraries/shared/src/SettingInterface.cpp index 6c5431a13e..a931875771 100644 --- a/libraries/shared/src/SettingInterface.cpp +++ b/libraries/shared/src/SettingInterface.cpp @@ -97,7 +97,7 @@ namespace Setting { } void Interface::deinit() { - if (privateInstance) { + if (_isInitialized && privateInstance) { // Save value to disk save(); From de36cd150e8d00dd82df9e6b002c4730fd6eaa4c Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 16 May 2016 19:48:20 -0700 Subject: [PATCH 0089/1237] Move runnings scripts setting storage So that it doesn't conflict with the "Settings" menu setting storage. Running script would clear those settings while storing its data This adds some backward compatible code to move the scripts settings to the new location. --- libraries/script-engine/src/ScriptEngines.cpp | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 70eb055d22..4fd680025a 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -256,7 +256,7 @@ QVariantList ScriptEngines::getRunning() { } -static const QString SETTINGS_KEY = "Settings"; +static const QString SETTINGS_KEY = "RunningScripts"; void ScriptEngines::loadDefaultScripts() { QUrl defaultScriptsLoc = defaultScriptsLocation(); @@ -281,6 +281,43 @@ void ScriptEngines::loadScripts() { // loads all saved scripts Settings settings; + + + // START of backward compatibility code + // This following if statement is only meant to update the settings file still using the old setting key. + // If you read that comment and it has been more than a couple months since it was merged, + // then by all means, feel free to remove it. + if (!settings.childGroups().contains(SETTINGS_KEY)) { + qWarning() << "Detected old script settings config, loading from previous location"; + const QString oldKey = "Settings"; + + // Load old scripts array from settings + int size = settings.beginReadArray(oldKey); + for (int i = 0; i < size; ++i) { + settings.setArrayIndex(i); + QString string = settings.value("script").toString(); + if (!string.isEmpty()) { + loadScript(string); + } + } + settings.endArray(); + + // Cleanup old scripts array from settings + settings.beginWriteArray(oldKey); + for (int i = 0; i < size; ++i) { + settings.setArrayIndex(i); + settings.remove(""); + } + settings.endArray(); + settings.beginGroup(oldKey); + settings.remove("size"); + settings.endGroup(); + + return; + } + // END of backward compatibility code + + int size = settings.beginReadArray(SETTINGS_KEY); for (int i = 0; i < size; ++i) { settings.setArrayIndex(i); From b48134e30cefb6d9fa00ae7bddfbd32bcbc68919 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 May 2016 11:40:45 -0700 Subject: [PATCH 0090/1237] WIP commit testing for joint mapping transmission --- interface/src/avatar/Avatar.cpp | 4 -- interface/src/avatar/Avatar.h | 2 - libraries/avatars/src/AvatarData.cpp | 67 ++++++++++++++++++++++------ libraries/avatars/src/AvatarData.h | 1 + 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 9ae636af36..820a0491d1 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -543,10 +543,6 @@ void Avatar::simulateAttachments(float deltaTime) { } } -void Avatar::updateJointMappings() { - // no-op; joint mappings come from skeleton model -} - float Avatar::getBoundingRadius() const { return getBounds().getLargestDimension() / 2.0f; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 2580ac1d37..288fc9d781 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -235,8 +235,6 @@ protected: virtual bool shouldRenderHead(const RenderArgs* renderArgs) const; virtual void fixupModelsInScene(); - virtual void updateJointMappings() override; - virtual void updatePalms(); render::ItemID _renderItemID{ render::Item::INVALID_ITEM_ID }; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 251d83c19b..4b7e5cfb3c 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -602,8 +602,19 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { sourceBuffer += unpackFloatFromByte(sourceBuffer, _headData->_pupilDilation, 1.0f); } // 1 byte + // joint rotations int numJoints = *sourceBuffer++; + + + // do not process any jointData until we've received a valid jointIndices hash from + // an earlier AvatarIdentity packet. Because if we do, we risk applying the joint data + // the wrong bones, resulting in a twisted avatar, An un-animated avatar is preferable to this. + bool skipJoints = false; + if (_networkJointIndexMap.empty()) { + skipJoints = true; + } + int bytesOfValidity = (int)ceil((float)numJoints / (float)BITS_IN_BYTE); minPossibleSize += bytesOfValidity; if (minPossibleSize > maxAvailableSize) { @@ -654,9 +665,13 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { for (int i = 0; i < numJoints; i++) { JointData& data = _jointData[i]; if (validRotations[i]) { - _hasNewJointRotations = true; - data.rotationSet = true; - sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); + if (skipJoints) { + sourceBuffer += COMPRESSED_QUATERNION_SIZE; + } else { + sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); + _hasNewJointRotations = true; + data.rotationSet = true; + } } } } // numJoints * 6 bytes @@ -684,7 +699,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } // 1 + bytesOfValidity bytes // each joint translation component is stored in 6 bytes. 1 byte for translationCompressionRadix - minPossibleSize += numValidJointTranslations * 6 + 1; + const size_t COMPRESSED_TRANSLATION_SIZE = 6; + minPossibleSize += numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE + 1; if (minPossibleSize > maxAvailableSize) { if (shouldLogError(now)) { qCDebug(avatars) << "Malformed AvatarData packet after JointData translation validity;" @@ -701,10 +717,13 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { for (int i = 0; i < numJoints; i++) { JointData& data = _jointData[i]; if (validTranslations[i]) { - sourceBuffer += - unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, translationCompressionRadix); - _hasNewJointTranslations = true; - data.translationSet = true; + if (skipJoints) { + sourceBuffer += COMPRESSED_TRANSLATION_SIZE; + } else { + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, translationCompressionRadix); + _hasNewJointTranslations = true; + data.translationSet = true; + } } } } // numJoints * 12 bytes @@ -966,14 +985,25 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { QDataStream packetStream(data); QUuid avatarUUID; - QUrl unusedModelURL; // legacy faceModel support QUrl skeletonModelURL; QVector attachmentData; QString displayName; - packetStream >> avatarUUID >> unusedModelURL >> skeletonModelURL >> attachmentData >> displayName; - + QHash networkJointIndices; + packetStream >> avatarUUID >> skeletonModelURL >> attachmentData >> displayName >> networkJointIndices; bool hasIdentityChanged = false; + if (!_jointIndices.empty() && _networkJointIndexMap.empty() && !networkJointIndices.empty()) { + // build networkJointIndexMap from _jointIndices and networkJointIndices. + _networkJointIndexMap.fill(networkJointIndices.size(), -1); + for (auto iter = networkJointIndices.cbegin(); iter != networkJointIndices.end(); ++iter) { + int jointIndex = getJointIndex(iter.key()); + _networkJointIndexMap[iter.value()] = jointIndex; + } + } + + // AJT: just got a new networkJointIndicesMap. + qCDebug(avatars) << "AJT: receiving networkJointIndices.size = " << networkJointIndices.size(); + if (_firstSkeletonCheck || (skeletonModelURL != _skeletonModelURL)) { setSkeletonModelURL(skeletonModelURL); hasIdentityChanged = true; @@ -999,9 +1029,10 @@ QByteArray AvatarData::identityByteArray() { QUrl emptyURL(""); const QUrl& urlToSend = _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; - QUrl unusedModelURL; // legacy faceModel support + // AJT: just got a sending networkJointIndices + qCDebug(avatars) << "AJT: sending _jointIndices.size = " << _jointIndices.size(); - identityStream << QUuid() << unusedModelURL << urlToSend << _attachmentData << _displayName; + identityStream << QUuid() << urlToSend << _attachmentData << _displayName << _jointIndices; return identityData; } @@ -1106,6 +1137,8 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) { void AvatarData::setJointMappingsFromNetworkReply() { QNetworkReply* networkReply = static_cast(sender()); + qCDebug(avatars) << "AJT: GOT HERE! finished fst network request"; + QByteArray line; while (!(line = networkReply->readLine()).isEmpty()) { line = line.trimmed(); @@ -1140,6 +1173,11 @@ void AvatarData::setJointMappingsFromNetworkReply() { _jointIndices.insert(_jointNames.at(i), i + 1); } + // now that we have the jointIndices send them to the AvatarMixer. + sendIdentityPacket(); + + qCDebug(avatars) << "AJT: _jointIndices.size = " << _jointIndices.size(); + networkReply->deleteLater(); } @@ -1180,6 +1218,9 @@ void AvatarData::sendIdentityPacket() { void AvatarData::updateJointMappings() { _jointIndices.clear(); _jointNames.clear(); + _networkJointIndexMap.clear(); + + qCDebug(avatars) << "AJT: GOT HERE! kicking off fst network request"; if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index a7b97ef4c0..43bc682bda 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -366,6 +366,7 @@ protected: float _displayNameAlpha; QHash _jointIndices; ///< 1-based, since zero is returned for missing keys + QVector _networkJointIndexMap; // maps network joint indices to local model joint indices. QStringList _jointNames; ///< in order of depth-first traversal quint64 _errorLogExpiry; ///< time in future when to log an error From 89f18aee3b54661075204d238e2da5136a4e4c30 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 18 May 2016 13:14:42 -0700 Subject: [PATCH 0091/1237] Simplifying the deferredbuffer clear and enableing z test for lighting pass --- .../src/DeferredLightingEffect.cpp | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 1d9ce65581..fd7826070d 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -149,17 +149,12 @@ void DeferredLightingEffect::prepare(RenderArgs* args) { batch.setFramebuffer(deferredFbo); - // Clear Color, Depth and Stencil + // Clear Color, Depth and Stencil for deferred buffer batch.clearFramebuffer( - gpu::Framebuffer::BUFFER_COLOR0 | + gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_COLOR1 | gpu::Framebuffer::BUFFER_COLOR2 | gpu::Framebuffer::BUFFER_DEPTH | gpu::Framebuffer::BUFFER_STENCIL, - vec4(vec3(0), 1), 1.0, 0.0, true); - - // clear the normal and specular buffers - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR1, glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), true); - const float MAX_SPECULAR_EXPONENT = 128.0f; - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR2, glm::vec4(0.0f, 0.0f, 0.0f, 1.0f / MAX_SPECULAR_EXPONENT), true); + vec4(vec3(0), 0), 1.0, 0.0, true); }); } @@ -469,9 +464,15 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo batch.setInputBuffer(0, mesh->getVertexBuffer()); batch.setInputFormat(mesh->getVertexFormat()); - auto& part = mesh->getPartBuffer().get(); - - batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); + { + auto& part = mesh->getPartBuffer().get(0); + batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); + } + // keep for debug + /* { + auto& part = mesh->getPartBuffer().get(1); + batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); + }*/ } } } @@ -548,14 +549,13 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo // Stencil test all the light passes for objects pixels only, not the background state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - + if (lightVolume) { state->setCullMode(gpu::State::CULL_BACK); - - // No need for z test since the depth buffer is not bound state->setDepthTest(true, false, gpu::LESS_EQUAL); - // TODO: We should bind the true depth buffer both as RT and texture for the depth test + state->setDepthTest(true, false, gpu::LESS_EQUAL); + // TODO: We should use DepthClamp and avoid changing geometry for inside /outside cases - state->setDepthClampEnable(true); + //state->setDepthClampEnable(true); // additive blending state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); @@ -662,10 +662,14 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { _spotLightMesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(sizeof(unsigned short) * indices, (gpu::Byte*) indexData), gpu::Element::INDEX_UINT16)); delete[] indexData; - model::Mesh::Part part(0, indices, 0, model::Mesh::TRIANGLES); - //DEBUG: model::Mesh::Part part(0, indices, 0, model::Mesh::LINE_STRIP); - _spotLightMesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(sizeof(part), (gpu::Byte*) &part), gpu::Element::PART_DRAWCALL)); + std::vector parts; + parts.push_back(model::Mesh::Part(0, indices, 0, model::Mesh::TRIANGLES)); + //DEBUG: + parts.push_back(model::Mesh::Part(0, indices, 0, model::Mesh::LINE_STRIP)); + + + _spotLightMesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); } return _spotLightMesh; } From d6d94b7d17c2ddb6375d410f4bb9ce048dd7ae17 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 May 2016 14:12:45 -0700 Subject: [PATCH 0092/1237] switch branches --- .../DomainContent/Home/kineticObjects/bench.json | 1 + .../Home/kineticObjects/dressingRoomBricabrac.json | 8 ++++---- .../DomainContent/Home/kineticObjects/umbrella.json | 5 +++-- unpublishedScripts/DomainContent/Home/reset.js | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/bench.json b/unpublishedScripts/DomainContent/Home/kineticObjects/bench.json index d7306155a6..6ad65af88d 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/bench.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/bench.json @@ -1,5 +1,6 @@ { "Entities": [{ + "name": "home_model_bench", "collisionsWillMove": 1, "compoundShapeURL": "atp:/kineticObjects/bench/Bench.obj", "created": "2016-05-17T19:27:31Z", diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json index 8b19e16901..215f2b3506 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json @@ -1,7 +1,7 @@ { "Entities": [{ "collisionsWillMove": 1, - "compoundShapeURL": "https://hifi-content.s3.amazonaws.com/DomainContent/Home/skully/573a170808bd0_vhacd_skully.obj", + "compoundShapeURL": "atp:/kineticObjects/skully/skullyhull.obj", "created": "2016-05-16T19:34:28Z", "dimensions": { "x": 0.19080814719200134, @@ -15,7 +15,7 @@ "z": 0 }, "id": "{cef37b0a-bb3d-4549-a645-f3a934f82148}", - "modelURL": "https://hifi-content.s3.amazonaws.com/DomainContent/Home/skully/skully.fbx", + "modelURL": "atp:/kineticObjects/skully/skully.fbx", "position": { "x": 0.254150390625, "y": 0.4500732421875, @@ -500,7 +500,7 @@ "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/snapback/snapbackFinal.OBJ", + "compoundShapeURL": "atp:/kineticObjects/snapback/snapback.obj", "created": "2016-05-16T17:01:33Z", "dimensions": { "x": 0.19942393898963928, @@ -514,7 +514,7 @@ "z": 0 }, "id": "{26e895b7-4a83-45fe-a3e9-5b6ba5f5717e}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/snapback/snapback.fbx", + "modelURL": "atp:/kineticObjects/snapback/snapback.fbx", "position": { "x": 0.7891845703125, "y": 0.492218017578125, diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json b/unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json index 9d0386a339..6ab8f11ddc 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/umbrella.json @@ -1,9 +1,10 @@ { "Entities": [{ + "name": "home_model_umbrella", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "atp:/kineticObjects/umbrellaopen_ch.obj", + "compoundShapeURL": "atp:/kineticObjects/umbrella/umbrellaopen_ch.obj", "created": "2016-05-16T22:48:54Z", "dimensions": { "x": 1.2649545669555664, @@ -17,7 +18,7 @@ "z": 0 }, "id": "{59f5cc21-c6e8-4deb-a1e2-0364e82fe062}", - "modelURL": "atp:/kineticObjects/umbrella_open.fbx", + "modelURL": "atp:/kineticObjects/umbrella/umbrella_open.fbx", "queryAACube": { "scale": 2.0602173805236816, "x": -1.0301086902618408, diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index 31a4433b91..2c56ee2141 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -418,9 +418,9 @@ }); var dressingRoomBricabrac = new Bricabrac({ - x: 1107.6450, + x: 1105.8, y: 460.4309, - z: -72.6496 + z: -72.6 }); var bench = new Bench({ From 551c6698835683a8dab27040904c9c1f711327fc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 09:24:15 +1200 Subject: [PATCH 0093/1237] Move FolderListModel to be stand-alone --- .../resources/qml/dialogs/FileDialog.qml | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index e8b6b9b2b7..4f6e7ef050 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -43,7 +43,7 @@ ModalWindow { // Set from OffscreenUi::getOpenFile() property alias caption: root.title; // Set from OffscreenUi::getOpenFile() - property alias dir: model.folder; + property alias dir: folderListModel.folder; // Set from OffscreenUi::getOpenFile() property alias filter: selectionType.filtersString; // Set from OffscreenUi::getOpenFile() @@ -110,7 +110,7 @@ ModalWindow { glyph: hifi.glyphs.levelUp width: height size: 30 - enabled: model.parentFolder && model.parentFolder !== "" + enabled: folderListModel.parentFolder && folderListModel.parentFolder !== "" onClicked: d.navigateUp(); } @@ -135,7 +135,7 @@ ModalWindow { TextField { id: currentDirectory - property var lastValidFolder: helper.urlToPath(model.folder) + property var lastValidFolder: helper.urlToPath(folderListModel.folder) height: homeButton.height anchors { top: parent.top @@ -161,7 +161,7 @@ ModalWindow { text = lastValidFolder; return } - model.folder = helper.pathToUrl(text); + folderListModel.folder = helper.pathToUrl(text); } } @@ -172,7 +172,7 @@ ModalWindow { property bool currentSelectionIsFolder; property var backStack: [] property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } - property var modelConnection: Connections { target: model; onFolderChanged: d.update(); } + property var modelConnection: Connections { target: model; onFolderChanged: d.update(); } // DJRTODO property var homeDestination: helper.home(); Component.onCompleted: update(); @@ -194,18 +194,38 @@ ModalWindow { } function navigateUp() { - if (model.parentFolder && model.parentFolder !== "") { - model.folder = model.parentFolder + if (folderListModel.parentFolder && folderListModel.parentFolder !== "") { + folderListModel.folder = folderListModel.parentFolder return true; } } function navigateHome() { - model.folder = homeDestination; + folderListModel.folder = homeDestination; return true; } } + FolderListModel { + id: folderListModel + nameFilters: selectionType.currentFilter + showDirsFirst: true + showDotAndDotDot: false + showFiles: !root.selectDirectory + // For some reason, declaring these bindings directly in the targets doesn't + // work for setting the initial state + Component.onCompleted: { + currentDirectory.lastValidFolder = Qt.binding(function() { return helper.urlToPath(folder); }); + upButton.enabled = Qt.binding(function() { return (parentFolder && parentFolder != "") ? true : false; }); + showFiles = !root.selectDirectory + } + onFolderChanged: { + fileTableView.selection.clear(); + fileTableView.selection.select(0); + fileTableView.currentRow = 0; + } + } + Table { id: fileTableView colorScheme: hifi.colorSchemes.light @@ -227,25 +247,7 @@ ModalWindow { sortIndicatorOrder: Qt.AscendingOrder sortIndicatorVisible: true - model: FolderListModel { - id: model - nameFilters: selectionType.currentFilter - showDirsFirst: true - showDotAndDotDot: false - showFiles: !root.selectDirectory - // For some reason, declaring these bindings directly in the targets doesn't - // work for setting the initial state - Component.onCompleted: { - currentDirectory.lastValidFolder = Qt.binding(function() { return helper.urlToPath(model.folder); }); - upButton.enabled = Qt.binding(function() { return (model.parentFolder && model.parentFolder != "") ? true : false; }); - showFiles = !root.selectDirectory - } - onFolderChanged: { - fileTableView.selection.clear(); - fileTableView.selection.select(0); - fileTableView.currentRow = 0; - } - } + model: folderListModel function updateSort() { model.sortReversed = sortIndicatorColumn == 0 @@ -343,7 +345,7 @@ ModalWindow { var isFolder = model.isFolder(row); var file = model.get(row, "fileURL"); if (isFolder) { - fileTableView.model.folder = file + fileTableView.model.folder = file; } else { okAction.trigger(); } From 3cc08cdcfce195bb0becd33ad9555060cd290846 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 16 May 2016 11:44:18 -0700 Subject: [PATCH 0094/1237] Breaking up GL agnostic code from 4.1 specific code --- cmake/macros/TargetNsight.cmake | 15 +- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 411 +----- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 611 +++------ .../gpu-gl/src/gpu/gl/GLBackendBuffer.cpp | 124 -- .../gpu-gl/src/gpu/gl/GLBackendInput.cpp | 175 +-- .../gpu-gl/src/gpu/gl/GLBackendOutput.cpp | 208 +-- .../gpu-gl/src/gpu/gl/GLBackendPipeline.cpp | 67 +- .../gpu-gl/src/gpu/gl/GLBackendQuery.cpp | 50 +- libraries/gpu-gl/src/gpu/gl/GLBackendShared.h | 68 - .../gpu-gl/src/gpu/gl/GLBackendState.cpp | 563 +-------- .../gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 604 +-------- .../gpu-gl/src/gpu/gl/GLBackendTransform.cpp | 74 -- libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp | 28 + libraries/gpu-gl/src/gpu/gl/GLBuffer.h | 57 + libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp | 39 + libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 76 ++ libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp | 48 + libraries/gpu-gl/src/gpu/gl/GLPipeline.h | 26 + libraries/gpu-gl/src/gpu/gl/GLQuery.cpp | 12 + libraries/gpu-gl/src/gpu/gl/GLQuery.h | 57 + libraries/gpu-gl/src/gpu/gl/GLShader.cpp | 182 +++ libraries/gpu-gl/src/gpu/gl/GLShader.h | 52 + .../gl/{GLBackendShader.cpp => GLShared.cpp} | 1124 ++++++++++------- libraries/gpu-gl/src/gpu/gl/GLShared.h | 158 +++ libraries/gpu-gl/src/gpu/gl/GLState.cpp | 233 ++++ libraries/gpu-gl/src/gpu/gl/GLState.h | 73 ++ ...{GLBackendShared.cpp => GLTexelFormat.cpp} | 70 +- libraries/gpu-gl/src/gpu/gl/GLTexelFormat.h | 32 + libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 292 +++++ libraries/gpu-gl/src/gpu/gl/GLTexture.h | 192 +++ ...tureTransfer.cpp => GLTextureTransfer.cpp} | 19 +- ...dTextureTransfer.h => GLTextureTransfer.h} | 15 +- libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp | 175 +++ libraries/gpu-gl/src/gpu/gl41/GLBackend.h | 96 ++ .../gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp | 62 + .../gpu-gl/src/gpu/gl41/GLBackendInput.cpp | 185 +++ .../gpu-gl/src/gpu/gl41/GLBackendOutput.cpp | 169 +++ .../gpu-gl/src/gpu/gl41/GLBackendQuery.cpp | 37 + .../gpu-gl/src/gpu/gl41/GLBackendTexture.cpp | 237 ++++ .../src/gpu/gl41/GLBackendTransform.cpp | 83 ++ libraries/gpu/CMakeLists.txt | 2 + libraries/gpu/src/gpu/Batch.cpp | 106 +- libraries/gpu/src/gpu/Forward.h | 7 + libraries/gpu/src/gpu/Resource.h | 31 +- 44 files changed, 3685 insertions(+), 3260 deletions(-) delete mode 100644 libraries/gpu-gl/src/gpu/gl/GLBackendBuffer.cpp delete mode 100644 libraries/gpu-gl/src/gpu/gl/GLBackendShared.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLBuffer.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLPipeline.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLQuery.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLQuery.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLShader.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLShader.h rename libraries/gpu-gl/src/gpu/gl/{GLBackendShader.cpp => GLShared.cpp} (57%) create mode 100644 libraries/gpu-gl/src/gpu/gl/GLShared.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLState.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLState.h rename libraries/gpu-gl/src/gpu/gl/{GLBackendShared.cpp => GLTexelFormat.cpp} (82%) create mode 100644 libraries/gpu-gl/src/gpu/gl/GLTexelFormat.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLTexture.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLTexture.h rename libraries/gpu-gl/src/gpu/gl/{GLBackendTextureTransfer.cpp => GLTextureTransfer.cpp} (84%) rename libraries/gpu-gl/src/gpu/gl/{GLBackendTextureTransfer.h => GLTextureTransfer.h} (78%) create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackend.h create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackendInput.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackendOutput.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackendQuery.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackendTexture.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl41/GLBackendTransform.cpp diff --git a/cmake/macros/TargetNsight.cmake b/cmake/macros/TargetNsight.cmake index 09b056d07a..44ca4eecbf 100644 --- a/cmake/macros/TargetNsight.cmake +++ b/cmake/macros/TargetNsight.cmake @@ -7,18 +7,21 @@ # macro(TARGET_NSIGHT) if (WIN32 AND USE_NSIGHT) - + # grab the global CHECKED_FOR_NSIGHT_ONCE property - get_property(NSIGHT_CHECKED GLOBAL PROPERTY CHECKED_FOR_NSIGHT_ONCE) + get_property(NSIGHT_UNAVAILABLE GLOBAL PROPERTY CHECKED_FOR_NSIGHT_ONCE) - if (NOT NSIGHT_CHECKED) + if (NOT NSIGHT_UNAVAILABLE) # try to find the Nsight package and add it to the build if we find it find_package(NSIGHT) - # set the global CHECKED_FOR_NSIGHT_ONCE property so that we only debug that we couldn't find it once - set_property(GLOBAL PROPERTY CHECKED_FOR_NSIGHT_ONCE TRUE) + # Cache the failure to find nsight, so that we don't check over and over + if (NOT NSIGHT_FOUND) + set_property(GLOBAL PROPERTY CHECKED_FOR_NSIGHT_ONCE TRUE) + endif() endif () - + + # try to find the Nsight package and add it to the build if we find it if (NSIGHT_FOUND) include_directories(${NSIGHT_INCLUDE_DIRS}) add_definitions(-DNSIGHT_FOUND) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 69699e673b..e5460c6641 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -16,17 +16,51 @@ #include #include +#include "../gl41/GLBackend.h" + #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" #endif #include -#include -#include "GLBackendShared.h" +#include +#include +#include "GLTexture.h" +#include "GLShader.h" using namespace gpu; using namespace gpu::gl; + +static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); +static bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + +Backend* GLBackend::createBackend() { + auto version = QOpenGLContextWrapper::currentContextVersion(); + + // FIXME provide a mechanism to override the backend for testing + // Where the gpuContext is initialized and where the TRUE Backend is created and assigned +#if 0 + GLBackend* result; + if (enableOpenGL45 && version >= 0x0405) { + result = new gpu::gl45::GLBackend; + } else { + result = new gpu::gl41::GLBackend; + } +#else + GLBackend* result = new gpu::gl41::GLBackend; +#endif + result->initInput(); + result->initTransform(); + gl::GLTexture::initTextureTransferHelper(); + return result; +} + + +bool GLBackend::makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { + return GLShader::makeProgram(shader, slotBindings); +} + GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = { (&::gpu::gl::GLBackend::do_draw), @@ -95,16 +129,13 @@ void GLBackend::init() { std::call_once(once, [] { TEXTURE_ID_RESOLVER = [](const Texture& texture)->uint32 { - auto object = Backend::getGPUObject(texture); + auto object = Backend::getGPUObject(texture); if (!object) { return 0; } - if (object->getSyncState() != GLTexture::Idle) { - if (object->_downsampleSource) { - return object->_downsampleSource->_texture; - } - return 0; + if (object->getSyncState() != GLSyncState::Idle) { + return object->_downsampleSource._texture; } return object->_texture; }; @@ -148,61 +179,11 @@ void GLBackend::init() { }); } -Context::Size GLBackend::getDedicatedMemory() { - static Context::Size dedicatedMemory { 0 }; - static std::once_flag once; - std::call_once(once, [&] { -#ifdef Q_OS_WIN - if (!dedicatedMemory && wglGetGPUIDsAMD && wglGetGPUInfoAMD) { - UINT maxCount = wglGetGPUIDsAMD(0, 0); - std::vector ids; - ids.resize(maxCount); - wglGetGPUIDsAMD(maxCount, &ids[0]); - GLuint memTotal; - wglGetGPUInfoAMD(ids[0], WGL_GPU_RAM_AMD, GL_UNSIGNED_INT, sizeof(GLuint), &memTotal); - dedicatedMemory = MB_TO_BYTES(memTotal); - } -#endif - - if (!dedicatedMemory) { - GLint atiGpuMemory[4]; - // not really total memory, but close enough if called early enough in the application lifecycle - glGetIntegerv(GL_TEXTURE_FREE_MEMORY_ATI, atiGpuMemory); - if (GL_NO_ERROR == glGetError()) { - dedicatedMemory = KB_TO_BYTES(atiGpuMemory[0]); - } - } - - if (!dedicatedMemory) { - GLint nvGpuMemory { 0 }; - glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &nvGpuMemory); - if (GL_NO_ERROR == glGetError()) { - dedicatedMemory = KB_TO_BYTES(nvGpuMemory); - } - } - - if (!dedicatedMemory) { - auto gpuIdent = GPUIdent::getInstance(); - if (gpuIdent && gpuIdent->isValid()) { - dedicatedMemory = MB_TO_BYTES(gpuIdent->getMemory()); - } - } - }); - - return dedicatedMemory; -} - -Backend* GLBackend::createBackend() { - return new GLBackend(); -} - GLBackend::GLBackend() { glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_uboAlignment); - initInput(); - initTransform(); - initTextureTransferHelper(); } + GLBackend::~GLBackend() { resetStages(); @@ -260,7 +241,7 @@ void GLBackend::renderPassTransfer(Batch& batch) { { // Sync the transform buffers PROFILE_RANGE("syncGPUTransform"); - _transform.transfer(batch); + transferTransformState(batch); } _inRenderTransferPass = false; @@ -355,164 +336,6 @@ void GLBackend::setupStereoSide(int side) { _transform.bindCurrentCamera(side); } - -void GLBackend::do_draw(Batch& batch, size_t paramOffset) { - Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; - GLenum mode = _primitiveToGLmode[primitiveType]; - uint32 numVertices = batch._params[paramOffset + 1]._uint; - uint32 startVertex = batch._params[paramOffset + 0]._uint; - - if (isStereo()) { - setupStereoSide(0); - glDrawArrays(mode, startVertex, numVertices); - setupStereoSide(1); - glDrawArrays(mode, startVertex, numVertices); - - _stats._DSNumTriangles += 2 * numVertices / 3; - _stats._DSNumDrawcalls += 2; - - } else { - glDrawArrays(mode, startVertex, numVertices); - _stats._DSNumTriangles += numVertices / 3; - _stats._DSNumDrawcalls++; - } - _stats._DSNumAPIDrawcalls++; - - (void) CHECK_GL_ERROR(); -} - -void GLBackend::do_drawIndexed(Batch& batch, size_t paramOffset) { - Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; - GLenum mode = _primitiveToGLmode[primitiveType]; - uint32 numIndices = batch._params[paramOffset + 1]._uint; - uint32 startIndex = batch._params[paramOffset + 0]._uint; - - GLenum glType = _elementTypeToGLType[_input._indexBufferType]; - - auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; - GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); - - if (isStereo()) { - setupStereoSide(0); - glDrawElements(mode, numIndices, glType, indexBufferByteOffset); - setupStereoSide(1); - glDrawElements(mode, numIndices, glType, indexBufferByteOffset); - - _stats._DSNumTriangles += 2 * numIndices / 3; - _stats._DSNumDrawcalls += 2; - } else { - glDrawElements(mode, numIndices, glType, indexBufferByteOffset); - _stats._DSNumTriangles += numIndices / 3; - _stats._DSNumDrawcalls++; - } - _stats._DSNumAPIDrawcalls++; - - (void) CHECK_GL_ERROR(); -} - -void GLBackend::do_drawInstanced(Batch& batch, size_t paramOffset) { - GLint numInstances = batch._params[paramOffset + 4]._uint; - Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; - GLenum mode = _primitiveToGLmode[primitiveType]; - uint32 numVertices = batch._params[paramOffset + 2]._uint; - uint32 startVertex = batch._params[paramOffset + 1]._uint; - - - if (isStereo()) { - GLint trueNumInstances = 2 * numInstances; - - setupStereoSide(0); - glDrawArraysInstancedARB(mode, startVertex, numVertices, numInstances); - setupStereoSide(1); - glDrawArraysInstancedARB(mode, startVertex, numVertices, numInstances); - - _stats._DSNumTriangles += (trueNumInstances * numVertices) / 3; - _stats._DSNumDrawcalls += trueNumInstances; - } else { - glDrawArraysInstancedARB(mode, startVertex, numVertices, numInstances); - _stats._DSNumTriangles += (numInstances * numVertices) / 3; - _stats._DSNumDrawcalls += numInstances; - } - _stats._DSNumAPIDrawcalls++; - - (void) CHECK_GL_ERROR(); -} - -void glbackend_glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices, GLsizei primcount, GLint basevertex, GLuint baseinstance) { -#if (GPU_INPUT_PROFILE == GPU_CORE_43) - glDrawElementsInstancedBaseVertexBaseInstance(mode, count, type, indices, primcount, basevertex, baseinstance); -#else - glDrawElementsInstanced(mode, count, type, indices, primcount); -#endif -} - -void GLBackend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { - GLint numInstances = batch._params[paramOffset + 4]._uint; - GLenum mode = _primitiveToGLmode[(Primitive)batch._params[paramOffset + 3]._uint]; - uint32 numIndices = batch._params[paramOffset + 2]._uint; - uint32 startIndex = batch._params[paramOffset + 1]._uint; - // FIXME glDrawElementsInstancedBaseVertexBaseInstance is only available in GL 4.3 - // and higher, so currently we ignore this field - uint32 startInstance = batch._params[paramOffset + 0]._uint; - GLenum glType = _elementTypeToGLType[_input._indexBufferType]; - - auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; - GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); - - if (isStereo()) { - GLint trueNumInstances = 2 * numInstances; - - setupStereoSide(0); - glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); - setupStereoSide(1); - glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); - - _stats._DSNumTriangles += (trueNumInstances * numIndices) / 3; - _stats._DSNumDrawcalls += trueNumInstances; - } else { - glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); - _stats._DSNumTriangles += (numInstances * numIndices) / 3; - _stats._DSNumDrawcalls += numInstances; - } - - _stats._DSNumAPIDrawcalls++; - - (void)CHECK_GL_ERROR(); -} - - -void GLBackend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { -#if (GPU_INPUT_PROFILE == GPU_CORE_43) - uint commandCount = batch._params[paramOffset + 0]._uint; - GLenum mode = _primitiveToGLmode[(Primitive)batch._params[paramOffset + 1]._uint]; - - glMultiDrawArraysIndirect(mode, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); - _stats._DSNumDrawcalls += commandCount; - _stats._DSNumAPIDrawcalls++; - -#else - // FIXME implement the slow path -#endif - (void)CHECK_GL_ERROR(); - -} - -void GLBackend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { -#if (GPU_INPUT_PROFILE == GPU_CORE_43) - uint commandCount = batch._params[paramOffset + 0]._uint; - GLenum mode = _primitiveToGLmode[(Primitive)batch._params[paramOffset + 1]._uint]; - GLenum indexType = _elementTypeToGLType[_input._indexBufferType]; - - glMultiDrawElementsIndirect(mode, indexType, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); - _stats._DSNumDrawcalls += commandCount; - _stats._DSNumAPIDrawcalls++; -#else - // FIXME implement the slow path -#endif - (void)CHECK_GL_ERROR(); -} - - void GLBackend::do_resetStages(Batch& batch, size_t paramOffset) { resetStages(); } @@ -543,41 +366,34 @@ void GLBackend::resetStages() { (void) CHECK_GL_ERROR(); } + +void GLBackend::do_pushProfileRange(Batch& batch, size_t paramOffset) { +#if defined(NSIGHT_FOUND) + auto name = batch._profileRanges.get(batch._params[paramOffset]._uint); + nvtxRangePush(name.c_str()); +#endif +} + +void GLBackend::do_popProfileRange(Batch& batch, size_t paramOffset) { +#if defined(NSIGHT_FOUND) + nvtxRangePop(); +#endif +} + // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API -#define ADD_COMMAND_GL(call) _commands.push_back(COMMAND_##call); _commandOffsets.push_back(_params.size()); - // As long as we don;t use several versions of shaders we can avoid this more complex code path // #define GET_UNIFORM_LOCATION(shaderUniformLoc) _pipeline._programShader->getUniformLocation(shaderUniformLoc, isStereo()); #define GET_UNIFORM_LOCATION(shaderUniformLoc) shaderUniformLoc - -void Batch::_glActiveBindTexture(GLenum unit, GLenum target, GLuint texture) { - // clean the cache on the texture unit we are going to use so the next call to setResourceTexture() at the same slot works fine - setResourceTexture(unit - GL_TEXTURE0, nullptr); - - ADD_COMMAND_GL(glActiveBindTexture); - _params.push_back(texture); - _params.push_back(target); - _params.push_back(unit); -} void GLBackend::do_glActiveBindTexture(Batch& batch, size_t paramOffset) { glActiveTexture(batch._params[paramOffset + 2]._uint); glBindTexture( GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._uint), batch._params[paramOffset + 0]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glUniform1i(GLint location, GLint v0) { - if (location < 0) { - return; - } - ADD_COMMAND_GL(glUniform1i); - _params.push_back(v0); - _params.push_back(location); + (void)CHECK_GL_ERROR(); } void GLBackend::do_glUniform1i(Batch& batch, size_t paramOffset) { @@ -591,17 +407,9 @@ void GLBackend::do_glUniform1i(Batch& batch, size_t paramOffset) { glUniform1f( GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._int), batch._params[paramOffset + 0]._int); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } -void Batch::_glUniform1f(GLint location, GLfloat v0) { - if (location < 0) { - return; - } - ADD_COMMAND_GL(glUniform1f); - _params.push_back(v0); - _params.push_back(location); -} void GLBackend::do_glUniform1f(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that @@ -613,15 +421,7 @@ void GLBackend::do_glUniform1f(Batch& batch, size_t paramOffset) { glUniform1f( GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._int), batch._params[paramOffset + 0]._float); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glUniform2f(GLint location, GLfloat v0, GLfloat v1) { - ADD_COMMAND_GL(glUniform2f); - - _params.push_back(v1); - _params.push_back(v0); - _params.push_back(location); + (void)CHECK_GL_ERROR(); } void GLBackend::do_glUniform2f(Batch& batch, size_t paramOffset) { @@ -635,16 +435,7 @@ void GLBackend::do_glUniform2f(Batch& batch, size_t paramOffset) { GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), batch._params[paramOffset + 1]._float, batch._params[paramOffset + 0]._float); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glUniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2) { - ADD_COMMAND_GL(glUniform3f); - - _params.push_back(v2); - _params.push_back(v1); - _params.push_back(v0); - _params.push_back(location); + (void)CHECK_GL_ERROR(); } void GLBackend::do_glUniform3f(Batch& batch, size_t paramOffset) { @@ -659,21 +450,9 @@ void GLBackend::do_glUniform3f(Batch& batch, size_t paramOffset) { batch._params[paramOffset + 2]._float, batch._params[paramOffset + 1]._float, batch._params[paramOffset + 0]._float); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } - -void Batch::_glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) { - ADD_COMMAND_GL(glUniform4f); - - _params.push_back(v3); - _params.push_back(v2); - _params.push_back(v1); - _params.push_back(v0); - _params.push_back(location); -} - - void GLBackend::do_glUniform4f(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that @@ -690,14 +469,6 @@ void GLBackend::do_glUniform4f(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void Batch::_glUniform3fv(GLint location, GLsizei count, const GLfloat* value) { - ADD_COMMAND_GL(glUniform3fv); - - const int VEC3_SIZE = 3 * sizeof(float); - _params.push_back(cacheData(count * VEC3_SIZE, value)); - _params.push_back(count); - _params.push_back(location); -} void GLBackend::do_glUniform3fv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that @@ -710,18 +481,9 @@ void GLBackend::do_glUniform3fv(Batch& batch, size_t paramOffset) { batch._params[paramOffset + 1]._uint, (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } - -void Batch::_glUniform4fv(GLint location, GLsizei count, const GLfloat* value) { - ADD_COMMAND_GL(glUniform4fv); - - const int VEC4_SIZE = 4 * sizeof(float); - _params.push_back(cacheData(count * VEC4_SIZE, value)); - _params.push_back(count); - _params.push_back(location); -} void GLBackend::do_glUniform4fv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that @@ -729,23 +491,15 @@ void GLBackend::do_glUniform4fv(Batch& batch, size_t paramOffset) { return; } updatePipeline(); - + GLint location = GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int); GLsizei count = batch._params[paramOffset + 1]._uint; const GLfloat* value = (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint); glUniform4fv(location, count, value); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } -void Batch::_glUniform4iv(GLint location, GLsizei count, const GLint* value) { - ADD_COMMAND_GL(glUniform4iv); - - const int VEC4_SIZE = 4 * sizeof(int); - _params.push_back(cacheData(count * VEC4_SIZE, value)); - _params.push_back(count); - _params.push_back(location); -} void GLBackend::do_glUniform4iv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that @@ -758,18 +512,9 @@ void GLBackend::do_glUniform4iv(Batch& batch, size_t paramOffset) { batch._params[paramOffset + 1]._uint, (const GLint*)batch.editData(batch._params[paramOffset + 0]._uint)); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } -void Batch::_glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - ADD_COMMAND_GL(glUniformMatrix4fv); - - const int MATRIX4_SIZE = 16 * sizeof(float); - _params.push_back(cacheData(count * MATRIX4_SIZE, value)); - _params.push_back(transpose); - _params.push_back(count); - _params.push_back(location); -} void GLBackend::do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that @@ -783,42 +528,20 @@ void GLBackend::do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) { batch._params[paramOffset + 2]._uint, batch._params[paramOffset + 1]._uint, (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } -void Batch::_glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) { - ADD_COMMAND_GL(glColor4f); - - _params.push_back(alpha); - _params.push_back(blue); - _params.push_back(green); - _params.push_back(red); -} void GLBackend::do_glColor4f(Batch& batch, size_t paramOffset) { glm::vec4 newColor( batch._params[paramOffset + 3]._float, batch._params[paramOffset + 2]._float, batch._params[paramOffset + 1]._float, - batch._params[paramOffset + 0]._float); + batch._params[paramOffset + 0]._float); if (_input._colorAttribute != newColor) { _input._colorAttribute = newColor; glVertexAttrib4fv(gpu::Stream::COLOR, &_input._colorAttribute.r); } - (void) CHECK_GL_ERROR(); -} - - -void GLBackend::do_pushProfileRange(Batch& batch, size_t paramOffset) { -#if defined(NSIGHT_FOUND) - auto name = batch._profileRanges.get(batch._params[paramOffset]._uint); - nvtxRangePush(name.c_str()); -#endif -} - -void GLBackend::do_popProfileRange(Batch& batch, size_t paramOffset) { -#if defined(NSIGHT_FOUND) - nvtxRangePop(); -#endif + (void)CHECK_GL_ERROR(); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 16bb1cc394..610672f44f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -8,8 +8,8 @@ // 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_gpu_GLBackend_h -#define hifi_gpu_GLBackend_h +#ifndef hifi_gpu_gl_GLBackend_h +#define hifi_gpu_gl_GLBackend_h #include #include @@ -26,325 +26,34 @@ #include #include - -#define GPU_CORE 1 -#define GPU_LEGACY 0 -#define GPU_CORE_41 410 -#define GPU_CORE_43 430 -#define GPU_CORE_MINIMUM GPU_CORE_41 -#define GPU_FEATURE_PROFILE GPU_CORE - -#if defined(__APPLE__) - -#define GPU_INPUT_PROFILE GPU_CORE_41 - -#else - -#define GPU_INPUT_PROFILE GPU_CORE_43 - -#endif - - -#if (GPU_INPUT_PROFILE == GPU_CORE_43) -// Deactivate SSBO for now, we've run into some issues -// on GL 4.3 capable GPUs not behaving as expected -//#define GPU_SSBO_DRAW_CALL_INFO -#endif +#include "GLShared.h" namespace gpu { namespace gl { -class GLTextureTransferHelper; - class GLBackend : public Backend { - // Context Backend static interface required friend class gpu::Context; static void init(); static Backend* createBackend(); - static bool makeProgram(Shader& shader, const Shader::BindingSet& bindings); + static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings); +protected: explicit GLBackend(bool syncCache); GLBackend(); public: - static Context::Size getDedicatedMemory(); + ~GLBackend(); - virtual ~GLBackend(); - - virtual void render(Batch& batch); + void render(Batch& batch) final; // This call synchronize the Full Backend cache with the current GLState // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync // the gpu::Backend state with the true gl state which has probably been messed up by these ugly naked gl calls // Let's try to avoid to do that as much as possible! - virtual void syncCache(); + void syncCache() final; // This is the ugly "download the pixels to sysmem for taking a snapshot" // Just avoid using it, it's ugly and will break performances - virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage); - - static void checkGLStackStable(std::function f); - - - - class GLBuffer : public GPUObject { - public: - const GLuint _buffer; - const GLuint _size; - const Stamp _stamp; - - GLBuffer(const Buffer& buffer, GLBuffer* original = nullptr); - ~GLBuffer(); - - virtual void transfer(); - - private: - bool getNextTransferBlock(GLintptr& outOffset, GLsizeiptr& outSize, size_t& currentPage) const; - - // The owning texture - const Buffer& _gpuBuffer; - }; - - static GLBuffer* syncGPUObject(const Buffer& buffer); - static GLuint getBufferID(const Buffer& buffer); - - class GLTexture : public GPUObject { - public: - // The public gl texture object - GLuint _texture{ 0 }; - - const Stamp _storageStamp; - Stamp _contentStamp { 0 }; - const GLenum _target; - const uint16 _maxMip; - const uint16 _minMip; - const bool _transferrable; - - struct DownsampleSource { - using Pointer = std::shared_ptr; - DownsampleSource(GLTexture& oldTexture); - ~DownsampleSource(); - const GLuint _texture; - const uint16 _minMip; - const uint16 _maxMip; - }; - - DownsampleSource::Pointer _downsampleSource; - - GLTexture(bool transferrable, const gpu::Texture& gpuTexture); - GLTexture(GLTexture& originalTexture, const gpu::Texture& gpuTexture); - ~GLTexture(); - - // Return a floating point value indicating how much of the allowed - // texture memory we are currently consuming. A value of 0 indicates - // no texture memory usage, while a value of 1 indicates all available / allowed memory - // is consumed. A value above 1 indicates that there is a problem. - static float getMemoryPressure(); - - void withPreservedTexture(std::function f); - - void createTexture(); - void allocateStorage(); - - GLuint size() const { return _size; } - GLuint virtualSize() const { return _virtualSize; } - - void updateSize(); - - enum SyncState { - // The texture is currently undergoing no processing, although it's content - // may be out of date, or it's storage may be invalid relative to the - // owning GPU texture - Idle, - // The texture has been queued for transfer to the GPU - Pending, - // The texture has been transferred to the GPU, but is awaiting - // any post transfer operations that may need to occur on the - // primary rendering thread - Transferred, - }; - - void setSyncState(SyncState syncState) { _syncState = syncState; } - SyncState getSyncState() const { return _syncState; } - - // Is the storage out of date relative to the gpu texture? - bool isInvalid() const; - - // Is the content out of date relative to the gpu texture? - bool isOutdated() const; - - // Is the texture in a state where it can be rendered with no work? - bool isReady() const; - - // Is this texture pushing us over the memory limit? - bool isOverMaxMemory() const; - - // Move the image bits from the CPU to the GPU - void transfer() const; - - // Execute any post-move operations that must occur only on the main thread - void postTransfer(); - - uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } - - static const size_t CUBE_NUM_FACES = 6; - static const GLenum CUBE_FACE_LAYOUT[6]; - - private: - friend class GLTextureTransferHelper; - - GLTexture(bool transferrable, const gpu::Texture& gpuTexture, bool init); - // at creation the true texture is created in GL - // it becomes public only when ready. - GLuint _privateTexture{ 0 }; - - const std::vector& getFaceTargets() const; - - void setSize(GLuint size); - - const GLuint _virtualSize; // theorical size as expected - GLuint _size { 0 }; // true size as reported by the gl api - - void transferMip(uint16_t mipLevel, uint8_t face = 0) const; - - // The owning texture - const Texture& _gpuTexture; - std::atomic _syncState { SyncState::Idle }; - }; - static GLTexture* syncGPUObject(const TexturePointer& texture, bool needTransfer = true); - static GLuint getTextureID(const TexturePointer& texture, bool sync = true); - - // very specific for now - static void syncSampler(const Sampler& sampler, Texture::Type type, const GLTexture* object); - - class GLShader : public GPUObject { - public: - enum Version { - Mono = 0, - - NumVersions - }; - - struct ShaderObject { - GLuint glshader{ 0 }; - GLuint glprogram{ 0 }; - GLint transformCameraSlot{ -1 }; - GLint transformObjectSlot{ -1 }; - }; - - using ShaderObjects = std::array< ShaderObject, NumVersions >; - - using UniformMapping = std::map; - using UniformMappingVersions = std::vector; - - GLShader(); - ~GLShader(); - - ShaderObjects _shaderObjects; - UniformMappingVersions _uniformMappings; - - GLuint getProgram(Version version = Mono) const { - return _shaderObjects[version].glprogram; - } - - GLint getUniformLocation(GLint srcLoc, Version version = Mono) { - // THIS will be used in the future PR as we grow the number of versions - // return _uniformMappings[version][srcLoc]; - return srcLoc; - } - - }; - static GLShader* syncGPUObject(const Shader& shader); - - class GLState : public GPUObject { - public: - class Command { - public: - virtual void run(GLBackend* backend) = 0; - Command() {} - virtual ~Command() {}; - }; - - template class Command1 : public Command { - public: - typedef void (GLBackend::*GLFunction)(T); - void run(GLBackend* backend) { (backend->*(_func))(_param); } - Command1(GLFunction func, T param) : _func(func), _param(param) {}; - GLFunction _func; - T _param; - }; - template class Command2 : public Command { - public: - typedef void (GLBackend::*GLFunction)(T, U); - void run(GLBackend* backend) { (backend->*(_func))(_param0, _param1); } - Command2(GLFunction func, T param0, U param1) : _func(func), _param0(param0), _param1(param1) {}; - GLFunction _func; - T _param0; - U _param1; - }; - - template class Command3 : public Command { - public: - typedef void (GLBackend::*GLFunction)(T, U, V); - void run(GLBackend* backend) { (backend->*(_func))(_param0, _param1, _param2); } - Command3(GLFunction func, T param0, U param1, V param2) : _func(func), _param0(param0), _param1(param1), _param2(param2) {}; - GLFunction _func; - T _param0; - U _param1; - V _param2; - }; - - typedef std::shared_ptr< Command > CommandPointer; - typedef std::vector< CommandPointer > Commands; - - Commands _commands; - Stamp _stamp; - State::Signature _signature; - - GLState(); - ~GLState(); - - // The state commands to reset to default, - static const Commands _resetStateCommands; - - friend class GLBackend; - }; - static GLState* syncGPUObject(const State& state); - - class GLPipeline : public GPUObject { - public: - GLShader* _program = 0; - GLState* _state = 0; - - GLPipeline(); - ~GLPipeline(); - }; - static GLPipeline* syncGPUObject(const Pipeline& pipeline); - - - class GLFramebuffer : public GPUObject { - public: - GLuint _fbo = 0; - std::vector _colorBuffers; - Stamp _depthStamp { 0 }; - std::vector _colorStamps; - - - GLFramebuffer(); - ~GLFramebuffer(); - }; - static GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer); - static GLuint getFramebufferID(const FramebufferPointer& framebuffer); - - class GLQuery : public GPUObject { - public: - GLuint _qo = 0; - GLuint64 _result = 0; - - GLQuery(); - ~GLQuery(); - }; - static GLQuery* syncGPUObject(const Query& query); - static GLuint getQueryID(const QueryPointer& query); + virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) final; static const int MAX_NUM_ATTRIBUTES = Stream::NUM_INPUT_SLOTS; @@ -362,70 +71,131 @@ public: static const int MAX_NUM_RESOURCE_TEXTURES = 16; size_t getMaxNumResourceTextures() const { return MAX_NUM_RESOURCE_TEXTURES; } + // Draw Stage + virtual void do_draw(Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexed(Batch& batch, size_t paramOffset) = 0; + virtual void do_drawInstanced(Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexedInstanced(Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndirect(Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) = 0; + + // Input Stage + virtual void do_setInputFormat(Batch& batch, size_t paramOffset) final; + virtual void do_setInputBuffer(Batch& batch, size_t paramOffset) final; + virtual void do_setIndexBuffer(Batch& batch, size_t paramOffset) final; + virtual void do_setIndirectBuffer(Batch& batch, size_t paramOffset) final; + virtual void do_generateTextureMips(Batch& batch, size_t paramOffset) final; + + // Transform Stage + virtual void do_setModelTransform(Batch& batch, size_t paramOffset) final; + virtual void do_setViewTransform(Batch& batch, size_t paramOffset) final; + virtual void do_setProjectionTransform(Batch& batch, size_t paramOffset) final; + virtual void do_setViewportTransform(Batch& batch, size_t paramOffset) final; + virtual void do_setDepthRangeTransform(Batch& batch, size_t paramOffset) final; + + // Uniform Stage + virtual void do_setUniformBuffer(Batch& batch, size_t paramOffset) final; + + // Resource Stage + virtual void do_setResourceTexture(Batch& batch, size_t paramOffset) final; + + // Pipeline Stage + virtual void do_setPipeline(Batch& batch, size_t paramOffset) final; + + // Output stage + virtual void do_setFramebuffer(Batch& batch, size_t paramOffset) final; + virtual void do_clearFramebuffer(Batch& batch, size_t paramOffset) final; + virtual void do_blit(Batch& batch, size_t paramOffset) = 0; + + // Query section + virtual void do_beginQuery(Batch& batch, size_t paramOffset) final; + virtual void do_endQuery(Batch& batch, size_t paramOffset) final; + virtual void do_getQuery(Batch& batch, size_t paramOffset) final; + + // Reset stages + virtual void do_resetStages(Batch& batch, size_t paramOffset) final; + + virtual void do_runLambda(Batch& batch, size_t paramOffset) final; + + virtual void do_startNamedCall(Batch& batch, size_t paramOffset) final; + virtual void do_stopNamedCall(Batch& batch, size_t paramOffset) final; + + virtual void do_pushProfileRange(Batch& batch, size_t paramOffset) final; + virtual void do_popProfileRange(Batch& batch, size_t paramOffset) final; + + // TODO: As long as we have gl calls explicitely issued from interface + // code, we need to be able to record and batch these calls. THe long + // term strategy is to get rid of any GL calls in favor of the HIFI GPU API + virtual void do_glActiveBindTexture(Batch& batch, size_t paramOffset) final; + + virtual void do_glUniform1i(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform1f(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform2f(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3f(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4f(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3fv(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4fv(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4iv(Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) final; + + virtual void do_glColor4f(Batch& batch, size_t paramOffset) final; + // The State setters called by the GLState::Commands when a new state is assigned - void do_setStateFillMode(int32 mode); - void do_setStateCullMode(int32 mode); - void do_setStateFrontFaceClockwise(bool isClockwise); - void do_setStateDepthClampEnable(bool enable); - void do_setStateScissorEnable(bool enable); - void do_setStateMultisampleEnable(bool enable); - void do_setStateAntialiasedLineEnable(bool enable); + virtual void do_setStateFillMode(int32 mode) final; + virtual void do_setStateCullMode(int32 mode) final; + virtual void do_setStateFrontFaceClockwise(bool isClockwise) final; + virtual void do_setStateDepthClampEnable(bool enable) final; + virtual void do_setStateScissorEnable(bool enable) final; + virtual void do_setStateMultisampleEnable(bool enable) final; + virtual void do_setStateAntialiasedLineEnable(bool enable) final; + virtual void do_setStateDepthBias(Vec2 bias) final; + virtual void do_setStateDepthTest(State::DepthTest test) final; + virtual void do_setStateStencil(State::StencilActivation activation, State::StencilTest frontTest, State::StencilTest backTest) final; + virtual void do_setStateAlphaToCoverageEnable(bool enable) final; + virtual void do_setStateSampleMask(uint32 mask) final; + virtual void do_setStateBlend(State::BlendFunction blendFunction) final; + virtual void do_setStateColorWriteMask(uint32 mask) final; + virtual void do_setStateBlendFactor(Batch& batch, size_t paramOffset) final; + virtual void do_setStateScissorRect(Batch& batch, size_t paramOffset) final; - void do_setStateDepthBias(Vec2 bias); - void do_setStateDepthTest(State::DepthTest test); - - void do_setStateStencil(State::StencilActivation activation, State::StencilTest frontTest, State::StencilTest backTest); - - void do_setStateAlphaToCoverageEnable(bool enable); - void do_setStateSampleMask(uint32 mask); - - void do_setStateBlend(State::BlendFunction blendFunction); - - void do_setStateColorWriteMask(uint32 mask); - protected: - static const size_t INVALID_OFFSET = (size_t)-1; - bool _inRenderTransferPass; + virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + + virtual GLuint getBufferID(const Buffer& buffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + + virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; + + virtual GLuint getQueryID(const QueryPointer& query) = 0; + virtual GLQuery* syncGPUObject(const Query& query) = 0; + + static const size_t INVALID_OFFSET = (size_t)-1; + bool _inRenderTransferPass { false }; + int32_t _uboAlignment { 0 }; + int _currentDraw { -1 }; void renderPassTransfer(Batch& batch); void renderPassDraw(Batch& batch); - void setupStereoSide(int side); - void initTextureTransferHelper(); - static void transferGPUObject(const TexturePointer& texture); + virtual void initInput() final; + virtual void killInput() final; + virtual void syncInputStateCache() final; + virtual void resetInputStage() final; + virtual void updateInput() = 0; - static std::shared_ptr _textureTransferHelper; - - // Draw Stage - void do_draw(Batch& batch, size_t paramOffset); - void do_drawIndexed(Batch& batch, size_t paramOffset); - void do_drawInstanced(Batch& batch, size_t paramOffset); - void do_drawIndexedInstanced(Batch& batch, size_t paramOffset); - void do_multiDrawIndirect(Batch& batch, size_t paramOffset); - void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset); - - // Input Stage - void do_setInputFormat(Batch& batch, size_t paramOffset); - void do_setInputBuffer(Batch& batch, size_t paramOffset); - void do_setIndexBuffer(Batch& batch, size_t paramOffset); - void do_setIndirectBuffer(Batch& batch, size_t paramOffset); - - void initInput(); - void killInput(); - void syncInputStateCache(); - void updateInput(); - void resetInputStage(); struct InputStageState { - bool _invalidFormat = true; + bool _invalidFormat { true }; Stream::FormatPointer _format; typedef std::bitset ActivationCache; - ActivationCache _attributeActivation; + ActivationCache _attributeActivation { 0 }; typedef std::bitset BuffersState; - BuffersState _invalidBuffers; + BuffersState _invalidBuffers { 0 }; Buffers _buffers; Offsets _bufferOffsets; @@ -435,39 +205,23 @@ protected: glm::vec4 _colorAttribute{ 0.0f }; BufferPointer _indexBuffer; - Offset _indexBufferOffset; - Type _indexBufferType; + Offset _indexBufferOffset { 0 }; + Type _indexBufferType { UINT32 }; BufferPointer _indirectBuffer; Offset _indirectBufferOffset{ 0 }; Offset _indirectBufferStride{ 0 }; - GLuint _defaultVAO; + GLuint _defaultVAO { 0 }; InputStageState() : - _invalidFormat(true), - _format(0), - _attributeActivation(0), - _invalidBuffers(0), - _buffers(_invalidBuffers.size(), BufferPointer(0)), + _buffers(_invalidBuffers.size()), _bufferOffsets(_invalidBuffers.size(), 0), _bufferStrides(_invalidBuffers.size(), 0), - _bufferVBOs(_invalidBuffers.size(), 0), - _indexBuffer(0), - _indexBufferOffset(0), - _indexBufferType(UINT32), - _defaultVAO(0) - {} + _bufferVBOs(_invalidBuffers.size(), 0) {} } _input; - // Transform Stage - void do_setModelTransform(Batch& batch, size_t paramOffset); - void do_setViewTransform(Batch& batch, size_t paramOffset); - void do_setProjectionTransform(Batch& batch, size_t paramOffset); - void do_setViewportTransform(Batch& batch, size_t paramOffset); - void do_setDepthRangeTransform(Batch& batch, size_t paramOffset); - - void initTransform(); + virtual void initTransform() = 0; void killTransform(); // Synchronize the state cache of this Backend with the actual real state of the GL Context void syncTransformStateCache(); @@ -505,153 +259,74 @@ protected: void preUpdate(size_t commandIndex, const StereoState& stereo); void update(size_t commandIndex, const StereoState& stereo) const; void bindCurrentCamera(int stereoSide) const; - void transfer(const Batch& batch) const; } _transform; - int32_t _uboAlignment{ 0 }; + virtual void transferTransformState(const Batch& batch) const = 0; - - // Uniform Stage - void do_setUniformBuffer(Batch& batch, size_t paramOffset); + struct UniformStageState { + std::array _buffers; + //Buffers _buffers { }; + } _uniform; void releaseUniformBuffer(uint32_t slot); void resetUniformStage(); - struct UniformStageState { - Buffers _buffers; - - UniformStageState(): - _buffers(MAX_NUM_UNIFORM_BUFFERS, nullptr) - {} - } _uniform; - // Resource Stage - void do_setResourceTexture(Batch& batch, size_t paramOffset); - // update resource cache and do the gl unbind call with the current gpu::Texture cached at slot s void releaseResourceTexture(uint32_t slot); void resetResourceStage(); + struct ResourceStageState { - Textures _textures; - + std::array _textures; + //Textures _textures { { MAX_NUM_RESOURCE_TEXTURES } }; int findEmptyTextureSlot() const; - - ResourceStageState(): - _textures(MAX_NUM_RESOURCE_TEXTURES, nullptr) - {} - } _resource; + size_t _commandIndex{ 0 }; - // Pipeline Stage - void do_setPipeline(Batch& batch, size_t paramOffset); - void do_setStateBlendFactor(Batch& batch, size_t paramOffset); - void do_setStateScissorRect(Batch& batch, size_t paramOffset); - // Standard update pipeline check that the current Program and current State or good to go for a void updatePipeline(); // Force to reset all the state fields indicated by the 'toBeReset" signature void resetPipelineState(State::Signature toBeReset); // Synchronize the state cache of this Backend with the actual real state of the GL Context void syncPipelineStateCache(); - // Grab the actual gl state into it's gpu::State equivalent. THis is used by the above call syncPipleineStateCache() - void getCurrentGLState(State::Data& state); void resetPipelineStage(); struct PipelineStageState { - PipelinePointer _pipeline; - GLuint _program; - GLShader* _programShader; - bool _invalidProgram; + GLuint _program { 0 }; + GLShader* _programShader { nullptr }; + bool _invalidProgram { false }; - State::Data _stateCache; - State::Signature _stateSignatureCache; + State::Data _stateCache { State::DEFAULT }; + State::Signature _stateSignatureCache { 0 }; - GLState* _state; - bool _invalidState = false; - - PipelineStageState() : - _pipeline(), - _program(0), - _programShader(nullptr), - _invalidProgram(false), - _stateCache(State::DEFAULT), - _stateSignatureCache(0), - _state(nullptr), - _invalidState(false) - {} + GLState* _state { nullptr }; + bool _invalidState { false }; } _pipeline; - // Output stage - void do_setFramebuffer(Batch& batch, size_t paramOffset); - void do_clearFramebuffer(Batch& batch, size_t paramOffset); - void do_blit(Batch& batch, size_t paramOffset); - void do_generateTextureMips(Batch& batch, size_t paramOffset); - // Synchronize the state cache of this Backend with the actual real state of the GL Context void syncOutputStateCache(); void resetOutputStage(); struct OutputStageState { - - FramebufferPointer _framebuffer = nullptr; - GLuint _drawFBO = 0; - - OutputStageState() {} + FramebufferPointer _framebuffer { nullptr }; + GLuint _drawFBO { 0 }; } _output; - // Query section - void do_beginQuery(Batch& batch, size_t paramOffset); - void do_endQuery(Batch& batch, size_t paramOffset); - void do_getQuery(Batch& batch, size_t paramOffset); - void resetQueryStage(); struct QueryStageState { }; - // Reset stages - void do_resetStages(Batch& batch, size_t paramOffset); - - void do_runLambda(Batch& batch, size_t paramOffset); - - void do_startNamedCall(Batch& batch, size_t paramOffset); - void do_stopNamedCall(Batch& batch, size_t paramOffset); - void resetStages(); - // TODO: As long as we have gl calls explicitely issued from interface - // code, we need to be able to record and batch these calls. THe long - // term strategy is to get rid of any GL calls in favor of the HIFI GPU API - void do_glActiveBindTexture(Batch& batch, size_t paramOffset); - - void do_glUniform1i(Batch& batch, size_t paramOffset); - void do_glUniform1f(Batch& batch, size_t paramOffset); - void do_glUniform2f(Batch& batch, size_t paramOffset); - void do_glUniform3f(Batch& batch, size_t paramOffset); - void do_glUniform4f(Batch& batch, size_t paramOffset); - void do_glUniform3fv(Batch& batch, size_t paramOffset); - void do_glUniform4fv(Batch& batch, size_t paramOffset); - void do_glUniform4iv(Batch& batch, size_t paramOffset); - void do_glUniformMatrix4fv(Batch& batch, size_t paramOffset); - - void do_glColor4f(Batch& batch, size_t paramOffset); - - void do_pushProfileRange(Batch& batch, size_t paramOffset); - void do_popProfileRange(Batch& batch, size_t paramOffset); - - int _currentDraw { -1 }; - typedef void (GLBackend::*CommandCall)(Batch&, size_t); static CommandCall _commandCalls[Batch::NUM_COMMANDS]; - + friend class GLState; }; } } -Q_DECLARE_LOGGING_CATEGORY(gpugllogging) - - #endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendBuffer.cpp deleted file mode 100644 index 0057b3d702..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendBuffer.cpp +++ /dev/null @@ -1,124 +0,0 @@ -// -// GLBackendBuffer.cpp -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 3/8/2015. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "GLBackend.h" -#include "GLBackendShared.h" - -using namespace gpu; -using namespace gpu::gl; - -GLuint allocateSingleBuffer() { - GLuint result; - glGenBuffers(1, &result); - (void)CHECK_GL_ERROR(); - return result; -} - -GLBackend::GLBuffer::GLBuffer(const Buffer& buffer, GLBuffer* original) : - _buffer(allocateSingleBuffer()), - _size((GLuint)buffer._sysmem.getSize()), - _stamp(buffer._sysmem.getStamp()), - _gpuBuffer(buffer) { - glBindBuffer(GL_ARRAY_BUFFER, _buffer); - if (GLEW_VERSION_4_4 || GLEW_ARB_buffer_storage) { - glBufferStorage(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); - (void)CHECK_GL_ERROR(); - } else { - glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); - (void)CHECK_GL_ERROR(); - } - glBindBuffer(GL_ARRAY_BUFFER, 0); - - if (original) { - glBindBuffer(GL_COPY_WRITE_BUFFER, _buffer); - glBindBuffer(GL_COPY_READ_BUFFER, original->_buffer); - glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, original->_size); - glBindBuffer(GL_COPY_WRITE_BUFFER, 0); - glBindBuffer(GL_COPY_READ_BUFFER, 0); - (void)CHECK_GL_ERROR(); - } - - Backend::setGPUObject(buffer, this); - Backend::incrementBufferGPUCount(); - Backend::updateBufferGPUMemoryUsage(0, _size); -} - -GLBackend::GLBuffer::~GLBuffer() { - if (_buffer != 0) { - glDeleteBuffers(1, &_buffer); - } - Backend::updateBufferGPUMemoryUsage(_size, 0); - Backend::decrementBufferGPUCount(); -} - -void GLBackend::GLBuffer::transfer() { - glBindBuffer(GL_ARRAY_BUFFER, _buffer); - (void)CHECK_GL_ERROR(); - GLintptr offset; - GLsizeiptr size; - size_t currentPage { 0 }; - auto data = _gpuBuffer.getSysmem().readData(); - while (getNextTransferBlock(offset, size, currentPage)) { - glBufferSubData(GL_ARRAY_BUFFER, offset, size, data + offset); - (void)CHECK_GL_ERROR(); - } - glBindBuffer(GL_ARRAY_BUFFER, 0); - (void)CHECK_GL_ERROR(); - _gpuBuffer._flags &= ~Buffer::DIRTY; -} - -bool GLBackend::GLBuffer::getNextTransferBlock(GLintptr& outOffset, GLsizeiptr& outSize, size_t& currentPage) const { - size_t pageCount = _gpuBuffer._pages.size(); - // Advance to the first dirty page - while (currentPage < pageCount && (0 == (Buffer::DIRTY & _gpuBuffer._pages[currentPage]))) { - ++currentPage; - } - - // If we got to the end, we're done - if (currentPage >= pageCount) { - return false; - } - - // Advance to the next clean page - outOffset = static_cast(currentPage * _gpuBuffer._pageSize); - while (currentPage < pageCount && (0 != (Buffer::DIRTY & _gpuBuffer._pages[currentPage]))) { - _gpuBuffer._pages[currentPage] &= ~Buffer::DIRTY; - ++currentPage; - } - outSize = static_cast((currentPage * _gpuBuffer._pageSize) - outOffset); - return true; -} - -GLBackend::GLBuffer* GLBackend::syncGPUObject(const Buffer& buffer) { - GLBuffer* object = Backend::getGPUObject(buffer); - - // Has the storage size changed? - if (!object || object->_stamp != buffer.getSysmem().getStamp()) { - object = new GLBuffer(buffer, object); - } - - if (0 != (buffer._flags & Buffer::DIRTY)) { - object->transfer(); - } - - return object; -} - - - -GLuint GLBackend::getBufferID(const Buffer& buffer) { - GLBuffer* bo = GLBackend::syncGPUObject(buffer); - if (bo) { - return bo->_buffer; - } else { - return 0; - } -} - diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp index 7849da42a0..448cc508eb 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "GLBackend.h" -#include "GLBackendShared.h" +#include "GLShared.h" using namespace gpu; using namespace gpu::gl; @@ -59,9 +59,6 @@ void GLBackend::do_setInputBuffer(Batch& batch, size_t paramOffset) { } } - - - void GLBackend::initInput() { if(!_input._defaultVAO) { glGenVertexArrays(1, &_input._defaultVAO); @@ -88,176 +85,6 @@ void GLBackend::syncInputStateCache() { glBindVertexArray(_input._defaultVAO); } -// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding -// Core 43 does :) -// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat -#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) -#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT -#else -#define SUPPORT_VERTEX_ATTRIB_FORMAT -#endif - -void GLBackend::updateInput() { -#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) - if (_input._invalidFormat) { - - InputStageState::ActivationCache newActivation; - - // Assign the vertex format required - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGLType[attrib._element.getType()]; - GLuint offset = attrib._offset;; - GLboolean isNormalized = attrib._element.isNormalized(); - - GLenum perLocationSize = attrib._element.getLocationSize(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - newActivation.set(slot + locNum); - glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); - glVertexAttribBinding(slot + locNum, attrib._channel); - } - glVertexBindingDivisor(attrib._channel, attrib._frequency); - } - (void) CHECK_GL_ERROR(); - } - - // Manage Activation what was and what is expected now - for (size_t i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - _input._attributeActivation.flip(i); - } - } - (void) CHECK_GL_ERROR(); - - _input._invalidFormat = false; - _stats._ISNumFormatChanges++; - } - - if (_input._invalidBuffers.any()) { - int numBuffers = _input._buffers.size(); - auto buffer = _input._buffers.data(); - auto vbo = _input._bufferVBOs.data(); - auto offset = _input._bufferOffsets.data(); - auto stride = _input._bufferStrides.data(); - - for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { - if (_input._invalidBuffers.test(bufferNum)) { - glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); - } - buffer++; - vbo++; - offset++; - stride++; - } - _input._invalidBuffers.reset(); - (void) CHECK_GL_ERROR(); - } -#else - if (_input._invalidFormat || _input._invalidBuffers.any()) { - - if (_input._invalidFormat) { - InputStageState::ActivationCache newActivation; - - _stats._ISNumFormatChanges++; - - // Check expected activation - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - uint8_t locationCount = attrib._element.getLocationCount(); - for (int i = 0; i < locationCount; ++i) { - newActivation.set(attrib._slot + i); - } - } - } - - // Manage Activation what was and what is expected now - for (unsigned int i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - (void) CHECK_GL_ERROR(); - - _input._attributeActivation.flip(i); - } - } - } - - // now we need to bind the buffers and assign the attrib pointers - if (_input._format) { - const Buffers& buffers = _input._buffers; - const Offsets& offsets = _input._bufferOffsets; - const Offsets& strides = _input._bufferStrides; - - const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); - auto& inputChannels = _input._format->getChannels(); - _stats._ISNumInputBufferChanges++; - - GLuint boundVBO = 0; - for (auto& channelIt : inputChannels) { - const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; - if ((channelIt).first < buffers.size()) { - int bufferNum = (channelIt).first; - - if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { - // GLuint vbo = gpu::GLBackend::getBufferID((*buffers[bufferNum])); - GLuint vbo = _input._bufferVBOs[bufferNum]; - if (boundVBO != vbo) { - glBindBuffer(GL_ARRAY_BUFFER, vbo); - (void) CHECK_GL_ERROR(); - boundVBO = vbo; - } - _input._invalidBuffers[bufferNum] = false; - - for (unsigned int i = 0; i < channel._slots.size(); i++) { - const Stream::Attribute& attrib = attributes.at(channel._slots[i]); - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGLType[attrib._element.getType()]; - // GLenum perLocationStride = strides[bufferNum]; - GLenum perLocationStride = attrib._element.getLocationSize(); - GLuint stride = (GLuint)strides[bufferNum]; - GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); - GLboolean isNormalized = attrib._element.isNormalized(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, - reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); - glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); - } - - // TODO: Support properly the IAttrib version - - (void) CHECK_GL_ERROR(); - } - } - } - } - } - // everything format related should be in sync now - _input._invalidFormat = false; - } -#endif -} - void GLBackend::resetInputStage() { // Reset index buffer _input._indexBufferType = UINT32; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp index f0a29a19ba..1d46078b5b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp @@ -9,180 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "GLBackend.h" +#include "GLShared.h" +#include "GLFramebuffer.h" #include -#include "GLBackendShared.h" - using namespace gpu; using namespace gpu::gl; -GLBackend::GLFramebuffer::GLFramebuffer() {} - -GLBackend::GLFramebuffer::~GLFramebuffer() { - if (_fbo != 0) { - glDeleteFramebuffers(1, &_fbo); - } -} - -GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffer) { - GLFramebuffer* object = Backend::getGPUObject(framebuffer); - - bool needsUpate { false }; - if (!object || - framebuffer.getDepthStamp() != object->_depthStamp || - framebuffer.getColorStamps() != object->_colorStamps) { - needsUpate = true; - } - - // If GPU object already created and in sync - if (!needsUpate) { - return object; - } else if (framebuffer.isEmpty()) { - // NO framebuffer definition yet so let's avoid thinking - return nullptr; - } - - // need to have a gpu object? - if (!object) { - // All is green, assign the gpuobject to the Framebuffer - object = new GLFramebuffer(); - Backend::setGPUObject(framebuffer, object); - glGenFramebuffers(1, &object->_fbo); - (void)CHECK_GL_ERROR(); - } - - if (needsUpate) { - GLint currentFBO = -1; - glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); - glBindFramebuffer(GL_FRAMEBUFFER, object->_fbo); - - GLTexture* gltexture = nullptr; - TexturePointer surface; - if (framebuffer.getColorStamps() != object->_colorStamps) { - if (framebuffer.hasColor()) { - object->_colorBuffers.clear(); - static const GLenum colorAttachments[] = { - GL_COLOR_ATTACHMENT0, - GL_COLOR_ATTACHMENT1, - GL_COLOR_ATTACHMENT2, - GL_COLOR_ATTACHMENT3, - GL_COLOR_ATTACHMENT4, - GL_COLOR_ATTACHMENT5, - GL_COLOR_ATTACHMENT6, - GL_COLOR_ATTACHMENT7, - GL_COLOR_ATTACHMENT8, - GL_COLOR_ATTACHMENT9, - GL_COLOR_ATTACHMENT10, - GL_COLOR_ATTACHMENT11, - GL_COLOR_ATTACHMENT12, - GL_COLOR_ATTACHMENT13, - GL_COLOR_ATTACHMENT14, - GL_COLOR_ATTACHMENT15 }; - - int unit = 0; - for (auto& b : framebuffer.getRenderBuffers()) { - surface = b._texture; - if (surface) { - gltexture = GLBackend::syncGPUObject(surface, false); // Grab the gltexture and don't transfer - } else { - gltexture = nullptr; - } - - if (gltexture) { - glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); - object->_colorBuffers.push_back(colorAttachments[unit]); - } else { - glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0); - } - unit++; - } - } - object->_colorStamps = framebuffer.getColorStamps(); - } - - GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; - if (!framebuffer.hasStencil()) { - attachement = GL_DEPTH_ATTACHMENT; - } else if (!framebuffer.hasDepth()) { - attachement = GL_STENCIL_ATTACHMENT; - } - - if (framebuffer.getDepthStamp() != object->_depthStamp) { - auto surface = framebuffer.getDepthStencilBuffer(); - if (framebuffer.hasDepthStencil() && surface) { - gltexture = GLBackend::syncGPUObject(surface, false); // Grab the gltexture and don't transfer - } - - if (gltexture) { - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); - } else { - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0); - } - object->_depthStamp = framebuffer.getDepthStamp(); - } - - - // Last but not least, define where we draw - if (!object->_colorBuffers.empty()) { - glDrawBuffers((GLsizei)object->_colorBuffers.size(), object->_colorBuffers.data()); - } else { - glDrawBuffer( GL_NONE ); - } - - // Now check for completness - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - - // restore the current framebuffer - if (currentFBO != -1) { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); - } - - bool result = false; - switch (status) { - case GL_FRAMEBUFFER_COMPLETE : - // Success ! - result = true; - break; - case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : - qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; - break; - case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT : - qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT."; - break; - case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER : - qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER."; - break; - case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER : - qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER."; - break; - case GL_FRAMEBUFFER_UNSUPPORTED : - qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; - break; - } - if (!result && object->_fbo) { - glDeleteFramebuffers(1, &object->_fbo); - return nullptr; - } - } - - return object; -} - - - -GLuint GLBackend::getFramebufferID(const FramebufferPointer& framebuffer) { - if (!framebuffer) { - return 0; - } - GLFramebuffer* object = GLBackend::syncGPUObject(*framebuffer); - if (object) { - return object->_fbo; - } else { - return 0; - } -} - void GLBackend::syncOutputStateCache() { GLint currentFBO; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); @@ -303,44 +137,6 @@ void GLBackend::do_clearFramebuffer(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GLBackend::do_blit(Batch& batch, size_t paramOffset) { - auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); - Vec4i srcvp; - for (auto i = 0; i < 4; ++i) { - srcvp[i] = batch._params[paramOffset + 1 + i]._int; - } - - auto dstframebuffer = batch._framebuffers.get(batch._params[paramOffset + 5]._uint); - Vec4i dstvp; - for (auto i = 0; i < 4; ++i) { - dstvp[i] = batch._params[paramOffset + 6 + i]._int; - } - - // Assign dest framebuffer if not bound already - auto newDrawFBO = getFramebufferID(dstframebuffer); - if (_output._drawFBO != newDrawFBO) { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, newDrawFBO); - } - - // always bind the read fbo - glBindFramebuffer(GL_READ_FRAMEBUFFER, getFramebufferID(srcframebuffer)); - - // Blit! - glBlitFramebuffer(srcvp.x, srcvp.y, srcvp.z, srcvp.w, - dstvp.x, dstvp.y, dstvp.z, dstvp.w, - GL_COLOR_BUFFER_BIT, GL_LINEAR); - - // Always clean the read fbo to 0 - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - - // Restore draw fbo if changed - if (_output._drawFBO != newDrawFBO) { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _output._drawFBO); - } - - (void) CHECK_GL_ERROR(); -} - void GLBackend::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) { auto readFBO = getFramebufferID(srcFramebuffer); if (srcFramebuffer && readFBO) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp index b66b672850..04f56ba0f5 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp @@ -9,54 +9,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "GLBackend.h" -#include "GLBackendShared.h" +#include "GLShared.h" +#include "GLPipeline.h" +#include "GLShader.h" +#include "GLState.h" +#include "GLBuffer.h" +#include "GLTexture.h" using namespace gpu; using namespace gpu::gl; -GLBackend::GLPipeline::GLPipeline() : - _program(nullptr), - _state(nullptr) -{} - -GLBackend::GLPipeline::~GLPipeline() { - _program = nullptr; - _state = nullptr; -} - -GLBackend::GLPipeline* GLBackend::syncGPUObject(const Pipeline& pipeline) { - GLPipeline* object = Backend::getGPUObject(pipeline); - - // If GPU object already created then good - if (object) { - return object; - } - - // No object allocated yet, let's see if it's worth it... - ShaderPointer shader = pipeline.getProgram(); - GLShader* programObject = GLBackend::syncGPUObject((*shader)); - if (programObject == nullptr) { - return nullptr; - } - - StatePointer state = pipeline.getState(); - GLState* stateObject = GLBackend::syncGPUObject((*state)); - if (stateObject == nullptr) { - return nullptr; - } - - // Program and state are valid, we can create the pipeline object - if (!object) { - object = new GLPipeline(); - Backend::setGPUObject(pipeline, object); - } - - object->_program = programObject; - object->_state = stateObject; - - return object; -} - void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { PipelinePointer pipeline = batch._pipelines.get(batch._params[paramOffset + 0]._uint); @@ -78,7 +40,7 @@ void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { _pipeline._state = nullptr; _pipeline._invalidState = true; } else { - auto pipelineObject = syncGPUObject((*pipeline)); + auto pipelineObject = GLPipeline::sync(*pipeline); if (!pipelineObject) { return; } @@ -155,14 +117,12 @@ void GLBackend::resetPipelineStage() { glUseProgram(0); } - void GLBackend::releaseUniformBuffer(uint32_t slot) { auto& buf = _uniform._buffers[slot]; if (buf) { - auto* object = Backend::getGPUObject(*buf); + auto* object = Backend::getGPUObject(*buf); if (object) { glBindBufferBase(GL_UNIFORM_BUFFER, slot, 0); // RELEASE - (void) CHECK_GL_ERROR(); } buf.reset(); @@ -181,9 +141,6 @@ void GLBackend::do_setUniformBuffer(Batch& batch, size_t paramOffset) { GLintptr rangeStart = batch._params[paramOffset + 1]._uint; GLsizeiptr rangeSize = batch._params[paramOffset + 0]._uint; - - - if (!uniformBuffer) { releaseUniformBuffer(slot); return; @@ -195,7 +152,7 @@ void GLBackend::do_setUniformBuffer(Batch& batch, size_t paramOffset) { } // Sync BufferObject - auto* object = GLBackend::syncGPUObject(*uniformBuffer); + auto* object = syncGPUObject(*uniformBuffer); if (object) { glBindBufferRange(GL_UNIFORM_BUFFER, slot, object->_buffer, rangeStart, rangeSize); @@ -210,19 +167,17 @@ void GLBackend::do_setUniformBuffer(Batch& batch, size_t paramOffset) { void GLBackend::releaseResourceTexture(uint32_t slot) { auto& tex = _resource._textures[slot]; if (tex) { - auto* object = Backend::getGPUObject(*tex); + auto* object = Backend::getGPUObject(*tex); if (object) { GLuint target = object->_target; glActiveTexture(GL_TEXTURE0 + slot); glBindTexture(target, 0); // RELEASE - (void) CHECK_GL_ERROR(); } tex.reset(); } } - void GLBackend::resetResourceStage() { for (uint32_t i = 0; i < _resource._textures.size(); i++) { releaseResourceTexture(i); @@ -246,7 +201,7 @@ void GLBackend::do_setResourceTexture(Batch& batch, size_t paramOffset) { _stats._RSNumTextureBounded++; // Always make sure the GLObject is in sync - GLTexture* object = GLBackend::syncGPUObject(resourceTexture); + GLTexture* object = syncGPUObject(resourceTexture); if (object) { GLuint to = object->_texture; GLuint target = object->_target; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp index 4f5b2ff1bf..344f380339 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp @@ -9,58 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "GLBackend.h" -#include "GLBackendShared.h" +#include "GLQuery.h" using namespace gpu; using namespace gpu::gl; -GLBackend::GLQuery::GLQuery() {} - -GLBackend::GLQuery::~GLQuery() { - if (_qo != 0) { - glDeleteQueries(1, &_qo); - } -} - -GLBackend::GLQuery* GLBackend::syncGPUObject(const Query& query) { - GLQuery* object = Backend::getGPUObject(query); - - // If GPU object already created and in sync - if (object) { - return object; - } - - // need to have a gpu object? - if (!object) { - GLuint qo; - glGenQueries(1, &qo); - (void) CHECK_GL_ERROR(); - GLuint64 result = -1; - - // All is green, assign the gpuobject to the Query - object = new GLQuery(); - object->_qo = qo; - object->_result = result; - Backend::setGPUObject(query, object); - } - - return object; -} - - - -GLuint GLBackend::getQueryID(const QueryPointer& query) { - if (!query) { - return 0; - } - GLQuery* object = GLBackend::syncGPUObject(*query); - if (object) { - return object->_qo; - } else { - return 0; - } -} - void GLBackend::do_beginQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); @@ -94,4 +47,3 @@ void GLBackend::do_getQuery(Batch& batch, size_t paramOffset) { void GLBackend::resetQueryStage() { } - diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendShared.h b/libraries/gpu-gl/src/gpu/gl/GLBackendShared.h deleted file mode 100644 index 0f7215f364..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendShared.h +++ /dev/null @@ -1,68 +0,0 @@ -// -// GLBackendShared.h -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 1/22/2014. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_gpu_GLBackend_Shared_h -#define hifi_gpu_GLBackend_Shared_h - -#include -#include -#include - -namespace gpu { namespace gl { - -static const GLenum _primitiveToGLmode[gpu::NUM_PRIMITIVES] = { - GL_POINTS, - GL_LINES, - GL_LINE_STRIP, - GL_TRIANGLES, - GL_TRIANGLE_STRIP, - GL_TRIANGLE_FAN, -}; - -static const GLenum _elementTypeToGLType[gpu::NUM_TYPES] = { - GL_FLOAT, - GL_INT, - GL_UNSIGNED_INT, - GL_HALF_FLOAT, - GL_SHORT, - GL_UNSIGNED_SHORT, - GL_BYTE, - GL_UNSIGNED_BYTE, - // Normalized values - GL_INT, - GL_UNSIGNED_INT, - GL_SHORT, - GL_UNSIGNED_SHORT, - GL_BYTE, - GL_UNSIGNED_BYTE -}; - -class GLTexelFormat { -public: - GLenum internalFormat; - GLenum format; - GLenum type; - - static GLTexelFormat evalGLTexelFormat(const gpu::Element& dstFormat) { - return evalGLTexelFormat(dstFormat, dstFormat); - } - static GLTexelFormat evalGLTexelFormatInternal(const gpu::Element& dstFormat); - - static GLTexelFormat evalGLTexelFormat(const gpu::Element& dstFormat, const gpu::Element& srcFormat); -}; - -bool checkGLError(const char* name = nullptr); -bool checkGLErrorDebug(const char* name = nullptr); - -} } - -#define CHECK_GL_ERROR() gpu::gl::checkGLErrorDebug(__FUNCTION__) - -#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp index 1c1e0b38a4..a42b0dca6f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp @@ -9,238 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "GLBackend.h" -#include "GLBackendShared.h" +#include "GLState.h" using namespace gpu; using namespace gpu::gl; -GLBackend::GLState::GLState() -{} - -GLBackend::GLState::~GLState() { -} - - -typedef GLBackend::GLState::Command Command; -typedef GLBackend::GLState::CommandPointer CommandPointer; -typedef GLBackend::GLState::Command1 Command1U; -typedef GLBackend::GLState::Command1 Command1I; -typedef GLBackend::GLState::Command1 Command1B; -typedef GLBackend::GLState::Command1 CommandDepthBias; -typedef GLBackend::GLState::Command1 CommandDepthTest; -typedef GLBackend::GLState::Command3 CommandStencil; -typedef GLBackend::GLState::Command1 CommandBlend; - -const GLBackend::GLState::Commands makeResetStateCommands(); -const GLBackend::GLState::Commands GLBackend::GLState::_resetStateCommands = makeResetStateCommands(); - - -// NOTE: This must stay in sync with the ordering of the State::Field enum -const GLBackend::GLState::Commands makeResetStateCommands() { - // Since State::DEFAULT is a static defined in another .cpp the initialisation order is random - // and we have a 50/50 chance that State::DEFAULT is not yet initialized. - // Since State::DEFAULT = State::Data() it is much easier to not use the actual State::DEFAULT - // but another State::Data object with a default initialization. - const State::Data DEFAULT = State::Data(); - - auto depthBiasCommand = std::make_shared(&GLBackend::do_setStateDepthBias, - Vec2(DEFAULT.depthBias, DEFAULT.depthBiasSlopeScale)); - auto stencilCommand = std::make_shared(&GLBackend::do_setStateStencil, DEFAULT.stencilActivation, - DEFAULT.stencilTestFront, DEFAULT.stencilTestBack); - - // The state commands to reset to default, - // WARNING depending on the order of the State::Field enum - return { - std::make_shared(&GLBackend::do_setStateFillMode, DEFAULT.fillMode), - std::make_shared(&GLBackend::do_setStateCullMode, DEFAULT.cullMode), - std::make_shared(&GLBackend::do_setStateFrontFaceClockwise, DEFAULT.frontFaceClockwise), - std::make_shared(&GLBackend::do_setStateDepthClampEnable, DEFAULT.depthClampEnable), - std::make_shared(&GLBackend::do_setStateScissorEnable, DEFAULT.scissorEnable), - std::make_shared(&GLBackend::do_setStateMultisampleEnable, DEFAULT.multisampleEnable), - std::make_shared(&GLBackend::do_setStateAntialiasedLineEnable, DEFAULT.antialisedLineEnable), - - // Depth bias has 2 fields in State but really one call in GLBackend - CommandPointer(depthBiasCommand), - CommandPointer(depthBiasCommand), - - std::make_shared(&GLBackend::do_setStateDepthTest, DEFAULT.depthTest), - - // Depth bias has 3 fields in State but really one call in GLBackend - CommandPointer(stencilCommand), - CommandPointer(stencilCommand), - CommandPointer(stencilCommand), - - std::make_shared(&GLBackend::do_setStateSampleMask, DEFAULT.sampleMask), - - std::make_shared(&GLBackend::do_setStateAlphaToCoverageEnable, DEFAULT.alphaToCoverageEnable), - - std::make_shared(&GLBackend::do_setStateBlend, DEFAULT.blendFunction), - - std::make_shared(&GLBackend::do_setStateColorWriteMask, DEFAULT.colorWriteMask) - }; -} - -void generateFillMode(GLBackend::GLState::Commands& commands, State::FillMode fillMode) { - commands.push_back(std::make_shared(&GLBackend::do_setStateFillMode, int32(fillMode))); -} - -void generateCullMode(GLBackend::GLState::Commands& commands, State::CullMode cullMode) { - commands.push_back(std::make_shared(&GLBackend::do_setStateCullMode, int32(cullMode))); -} - -void generateFrontFaceClockwise(GLBackend::GLState::Commands& commands, bool isClockwise) { - commands.push_back(std::make_shared(&GLBackend::do_setStateFrontFaceClockwise, isClockwise)); -} - -void generateDepthClampEnable(GLBackend::GLState::Commands& commands, bool enable) { - commands.push_back(std::make_shared(&GLBackend::do_setStateDepthClampEnable, enable)); -} - -void generateScissorEnable(GLBackend::GLState::Commands& commands, bool enable) { - commands.push_back(std::make_shared(&GLBackend::do_setStateScissorEnable, enable)); -} - -void generateMultisampleEnable(GLBackend::GLState::Commands& commands, bool enable) { - commands.push_back(std::make_shared(&GLBackend::do_setStateMultisampleEnable, enable)); -} - -void generateAntialiasedLineEnable(GLBackend::GLState::Commands& commands, bool enable) { - commands.push_back(std::make_shared(&GLBackend::do_setStateAntialiasedLineEnable, enable)); -} - -void generateDepthBias(GLBackend::GLState::Commands& commands, const State& state) { - commands.push_back(std::make_shared(&GLBackend::do_setStateDepthBias, Vec2(state.getDepthBias(), state.getDepthBiasSlopeScale()))); -} - -void generateDepthTest(GLBackend::GLState::Commands& commands, const State::DepthTest& test) { - commands.push_back(std::make_shared(&GLBackend::do_setStateDepthTest, int32(test.getRaw()))); -} - -void generateStencil(GLBackend::GLState::Commands& commands, const State& state) { - commands.push_back(std::make_shared(&GLBackend::do_setStateStencil, state.getStencilActivation(), state.getStencilTestFront(), state.getStencilTestBack())); -} - -void generateAlphaToCoverageEnable(GLBackend::GLState::Commands& commands, bool enable) { - commands.push_back(std::make_shared(&GLBackend::do_setStateAlphaToCoverageEnable, enable)); -} - -void generateSampleMask(GLBackend::GLState::Commands& commands, uint32 mask) { - commands.push_back(std::make_shared(&GLBackend::do_setStateSampleMask, mask)); -} - -void generateBlend(GLBackend::GLState::Commands& commands, const State& state) { - commands.push_back(std::make_shared(&GLBackend::do_setStateBlend, state.getBlendFunction())); -} - -void generateColorWriteMask(GLBackend::GLState::Commands& commands, uint32 mask) { - commands.push_back(std::make_shared(&GLBackend::do_setStateColorWriteMask, mask)); -} - -GLBackend::GLState* GLBackend::syncGPUObject(const State& state) { - GLState* object = Backend::getGPUObject(state); - - // If GPU object already created then good - if (object) { - return object; - } - - // Else allocate and create the GLState - if (!object) { - object = new GLState(); - Backend::setGPUObject(state, object); - } - - // here, we need to regenerate something so let's do it all - object->_commands.clear(); - object->_stamp = state.getStamp(); - object->_signature = state.getSignature(); - - bool depthBias = false; - bool stencilState = false; - - // go thorugh the list of state fields in the State and record the corresponding gl command - for (int i = 0; i < State::NUM_FIELDS; i++) { - if (state.getSignature()[i]) { - switch(i) { - case State::FILL_MODE: { - generateFillMode(object->_commands, state.getFillMode()); - break; - } - case State::CULL_MODE: { - generateCullMode(object->_commands, state.getCullMode()); - break; - } - case State::DEPTH_BIAS: - case State::DEPTH_BIAS_SLOPE_SCALE: { - depthBias = true; - break; - } - case State::FRONT_FACE_CLOCKWISE: { - generateFrontFaceClockwise(object->_commands, state.isFrontFaceClockwise()); - break; - } - case State::DEPTH_CLAMP_ENABLE: { - generateDepthClampEnable(object->_commands, state.isDepthClampEnable()); - break; - } - case State::SCISSOR_ENABLE: { - generateScissorEnable(object->_commands, state.isScissorEnable()); - break; - } - case State::MULTISAMPLE_ENABLE: { - generateMultisampleEnable(object->_commands, state.isMultisampleEnable()); - break; - } - case State::ANTIALISED_LINE_ENABLE: { - generateAntialiasedLineEnable(object->_commands, state.isAntialiasedLineEnable()); - break; - } - case State::DEPTH_TEST: { - generateDepthTest(object->_commands, state.getDepthTest()); - break; - } - - case State::STENCIL_ACTIVATION: - case State::STENCIL_TEST_FRONT: - case State::STENCIL_TEST_BACK: { - stencilState = true; - break; - } - - case State::SAMPLE_MASK: { - generateSampleMask(object->_commands, state.getSampleMask()); - break; - } - case State::ALPHA_TO_COVERAGE_ENABLE: { - generateAlphaToCoverageEnable(object->_commands, state.isAlphaToCoverageEnabled()); - break; - } - - case State::BLEND_FUNCTION: { - generateBlend(object->_commands, state); - break; - } - - case State::COLOR_WRITE_MASK: { - generateColorWriteMask(object->_commands, state.getColorWriteMask()); - break; - } - } - } - } - - if (depthBias) { - generateDepthBias(object->_commands, state); - } - - if (stencilState) { - generateStencil(object->_commands, state); - } - - return object; -} - - void GLBackend::resetPipelineState(State::Signature nextSignature) { auto currentNotSignature = ~_pipeline._stateSignatureCache; auto nextNotSignature = ~nextSignature; @@ -255,230 +28,6 @@ void GLBackend::resetPipelineState(State::Signature nextSignature) { } } -ComparisonFunction comparisonFuncFromGL(GLenum func) { - if (func == GL_NEVER) { - return NEVER; - } else if (func == GL_LESS) { - return LESS; - } else if (func == GL_EQUAL) { - return EQUAL; - } else if (func == GL_LEQUAL) { - return LESS_EQUAL; - } else if (func == GL_GREATER) { - return GREATER; - } else if (func == GL_NOTEQUAL) { - return NOT_EQUAL; - } else if (func == GL_GEQUAL) { - return GREATER_EQUAL; - } else if (func == GL_ALWAYS) { - return ALWAYS; - } - - return ALWAYS; -} - -State::StencilOp stencilOpFromGL(GLenum stencilOp) { - if (stencilOp == GL_KEEP) { - return State::STENCIL_OP_KEEP; - } else if (stencilOp == GL_ZERO) { - return State::STENCIL_OP_ZERO; - } else if (stencilOp == GL_REPLACE) { - return State::STENCIL_OP_REPLACE; - } else if (stencilOp == GL_INCR_WRAP) { - return State::STENCIL_OP_INCR_SAT; - } else if (stencilOp == GL_DECR_WRAP) { - return State::STENCIL_OP_DECR_SAT; - } else if (stencilOp == GL_INVERT) { - return State::STENCIL_OP_INVERT; - } else if (stencilOp == GL_INCR) { - return State::STENCIL_OP_INCR; - } else if (stencilOp == GL_DECR) { - return State::STENCIL_OP_DECR; - } - - return State::STENCIL_OP_KEEP; -} - -State::BlendOp blendOpFromGL(GLenum blendOp) { - if (blendOp == GL_FUNC_ADD) { - return State::BLEND_OP_ADD; - } else if (blendOp == GL_FUNC_SUBTRACT) { - return State::BLEND_OP_SUBTRACT; - } else if (blendOp == GL_FUNC_REVERSE_SUBTRACT) { - return State::BLEND_OP_REV_SUBTRACT; - } else if (blendOp == GL_MIN) { - return State::BLEND_OP_MIN; - } else if (blendOp == GL_MAX) { - return State::BLEND_OP_MAX; - } - - return State::BLEND_OP_ADD; -} - -State::BlendArg blendArgFromGL(GLenum blendArg) { - if (blendArg == GL_ZERO) { - return State::ZERO; - } else if (blendArg == GL_ONE) { - return State::ONE; - } else if (blendArg == GL_SRC_COLOR) { - return State::SRC_COLOR; - } else if (blendArg == GL_ONE_MINUS_SRC_COLOR) { - return State::INV_SRC_COLOR; - } else if (blendArg == GL_DST_COLOR) { - return State::DEST_COLOR; - } else if (blendArg == GL_ONE_MINUS_DST_COLOR) { - return State::INV_DEST_COLOR; - } else if (blendArg == GL_SRC_ALPHA) { - return State::SRC_ALPHA; - } else if (blendArg == GL_ONE_MINUS_SRC_ALPHA) { - return State::INV_SRC_ALPHA; - } else if (blendArg == GL_DST_ALPHA) { - return State::DEST_ALPHA; - } else if (blendArg == GL_ONE_MINUS_DST_ALPHA) { - return State::INV_DEST_ALPHA; - } else if (blendArg == GL_CONSTANT_COLOR) { - return State::FACTOR_COLOR; - } else if (blendArg == GL_ONE_MINUS_CONSTANT_COLOR) { - return State::INV_FACTOR_COLOR; - } else if (blendArg == GL_CONSTANT_ALPHA) { - return State::FACTOR_ALPHA; - } else if (blendArg == GL_ONE_MINUS_CONSTANT_ALPHA) { - return State::INV_FACTOR_ALPHA; - } - - return State::ONE; -} - -void GLBackend::getCurrentGLState(State::Data& state) { - { - GLint modes[2]; - glGetIntegerv(GL_POLYGON_MODE, modes); - if (modes[0] == GL_FILL) { - state.fillMode = State::FILL_FACE; - } else { - if (modes[0] == GL_LINE) { - state.fillMode = State::FILL_LINE; - } else { - state.fillMode = State::FILL_POINT; - } - } - } - { - if (glIsEnabled(GL_CULL_FACE)) { - GLint mode; - glGetIntegerv(GL_CULL_FACE_MODE, &mode); - state.cullMode = (mode == GL_FRONT ? State::CULL_FRONT : State::CULL_BACK); - } else { - state.cullMode = State::CULL_NONE; - } - } - { - GLint winding; - glGetIntegerv(GL_FRONT_FACE, &winding); - state.frontFaceClockwise = (winding == GL_CW); - state.depthClampEnable = glIsEnabled(GL_DEPTH_CLAMP); - state.scissorEnable = glIsEnabled(GL_SCISSOR_TEST); - state.multisampleEnable = glIsEnabled(GL_MULTISAMPLE); - state.antialisedLineEnable = glIsEnabled(GL_LINE_SMOOTH); - } - { - if (glIsEnabled(GL_POLYGON_OFFSET_FILL)) { - glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &state.depthBiasSlopeScale); - glGetFloatv(GL_POLYGON_OFFSET_UNITS, &state.depthBias); - } - } - { - GLboolean isEnabled = glIsEnabled(GL_DEPTH_TEST); - GLboolean writeMask; - glGetBooleanv(GL_DEPTH_WRITEMASK, &writeMask); - GLint func; - glGetIntegerv(GL_DEPTH_FUNC, &func); - - state.depthTest = State::DepthTest(isEnabled, writeMask, comparisonFuncFromGL(func)); - } - { - GLboolean isEnabled = glIsEnabled(GL_STENCIL_TEST); - - GLint frontWriteMask; - GLint frontReadMask; - GLint frontRef; - GLint frontFail; - GLint frontDepthFail; - GLint frontPass; - GLint frontFunc; - glGetIntegerv(GL_STENCIL_WRITEMASK, &frontWriteMask); - glGetIntegerv(GL_STENCIL_VALUE_MASK, &frontReadMask); - glGetIntegerv(GL_STENCIL_REF, &frontRef); - glGetIntegerv(GL_STENCIL_FAIL, &frontFail); - glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &frontDepthFail); - glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &frontPass); - glGetIntegerv(GL_STENCIL_FUNC, &frontFunc); - - GLint backWriteMask; - GLint backReadMask; - GLint backRef; - GLint backFail; - GLint backDepthFail; - GLint backPass; - GLint backFunc; - glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &backWriteMask); - glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &backReadMask); - glGetIntegerv(GL_STENCIL_BACK_REF, &backRef); - glGetIntegerv(GL_STENCIL_BACK_FAIL, &backFail); - glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &backDepthFail); - glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &backPass); - glGetIntegerv(GL_STENCIL_BACK_FUNC, &backFunc); - - state.stencilActivation = State::StencilActivation(isEnabled, frontWriteMask, backWriteMask); - state.stencilTestFront = State::StencilTest(frontRef, frontReadMask, comparisonFuncFromGL(frontFunc), stencilOpFromGL(frontFail), stencilOpFromGL(frontDepthFail), stencilOpFromGL(frontPass)); - state.stencilTestBack = State::StencilTest(backRef, backReadMask, comparisonFuncFromGL(backFunc), stencilOpFromGL(backFail), stencilOpFromGL(backDepthFail), stencilOpFromGL(backPass)); - } - { - GLint mask = 0xFFFFFFFF; - -#ifdef GPU_PROFILE_CORE - if (glIsEnabled(GL_SAMPLE_MASK)) { - glGetIntegerv(GL_SAMPLE_MASK, &mask); - state.sampleMask = mask; - } -#endif - state.sampleMask = mask; - } - { - state.alphaToCoverageEnable = glIsEnabled(GL_SAMPLE_ALPHA_TO_COVERAGE); - } - { - GLboolean isEnabled = glIsEnabled(GL_BLEND); - GLint srcRGB; - GLint srcA; - GLint dstRGB; - GLint dstA; - glGetIntegerv(GL_BLEND_SRC_RGB, &srcRGB); - glGetIntegerv(GL_BLEND_SRC_ALPHA, &srcA); - glGetIntegerv(GL_BLEND_DST_RGB, &dstRGB); - glGetIntegerv(GL_BLEND_DST_ALPHA, &dstA); - - GLint opRGB; - GLint opA; - glGetIntegerv(GL_BLEND_EQUATION_RGB, &opRGB); - glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &opA); - - state.blendFunction = State::BlendFunction(isEnabled, - blendArgFromGL(srcRGB), blendOpFromGL(opRGB), blendArgFromGL(dstRGB), - blendArgFromGL(srcA), blendOpFromGL(opA), blendArgFromGL(dstA)); - } - { - GLboolean mask[4]; - glGetBooleanv(GL_COLOR_WRITEMASK, mask); - state.colorWriteMask = (mask[0] ? State::WRITE_RED : 0) - | (mask[1] ? State::WRITE_GREEN : 0) - | (mask[2] ? State::WRITE_BLUE : 0) - | (mask[3] ? State::WRITE_ALPHA : 0); - } - - (void) CHECK_GL_ERROR(); -} - void GLBackend::syncPipelineStateCache() { State::Data state; @@ -500,21 +49,12 @@ void GLBackend::syncPipelineStateCache() { _pipeline._stateSignatureCache = signature; } -static GLenum GL_COMPARISON_FUNCTIONS[] = { - GL_NEVER, - GL_LESS, - GL_EQUAL, - GL_LEQUAL, - GL_GREATER, - GL_NOTEQUAL, - GL_GEQUAL, - GL_ALWAYS }; void GLBackend::do_setStateFillMode(int32 mode) { if (_pipeline._stateCache.fillMode != mode) { static GLenum GL_FILL_MODES[] = { GL_POINT, GL_LINE, GL_FILL }; glPolygonMode(GL_FRONT_AND_BACK, GL_FILL_MODES[mode]); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.fillMode = State::FillMode(mode); } @@ -530,7 +70,7 @@ void GLBackend::do_setStateCullMode(int32 mode) { glEnable(GL_CULL_FACE); glCullFace(GL_CULL_MODES[mode]); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.cullMode = State::CullMode(mode); } @@ -540,8 +80,8 @@ void GLBackend::do_setStateFrontFaceClockwise(bool isClockwise) { if (_pipeline._stateCache.frontFaceClockwise != isClockwise) { static GLenum GL_FRONT_FACES[] = { GL_CCW, GL_CW }; glFrontFace(GL_FRONT_FACES[isClockwise]); - (void) CHECK_GL_ERROR(); - + (void)CHECK_GL_ERROR(); + _pipeline._stateCache.frontFaceClockwise = isClockwise; } } @@ -553,7 +93,7 @@ void GLBackend::do_setStateDepthClampEnable(bool enable) { } else { glDisable(GL_DEPTH_CLAMP); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.depthClampEnable = enable; } @@ -566,7 +106,7 @@ void GLBackend::do_setStateScissorEnable(bool enable) { } else { glDisable(GL_SCISSOR_TEST); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.scissorEnable = enable; } @@ -579,7 +119,7 @@ void GLBackend::do_setStateMultisampleEnable(bool enable) { } else { glDisable(GL_MULTISAMPLE); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.multisampleEnable = enable; } @@ -592,14 +132,14 @@ void GLBackend::do_setStateAntialiasedLineEnable(bool enable) { } else { glDisable(GL_LINE_SMOOTH); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.antialisedLineEnable = enable; } } void GLBackend::do_setStateDepthBias(Vec2 bias) { - if ( (bias.x != _pipeline._stateCache.depthBias) || (bias.y != _pipeline._stateCache.depthBiasSlopeScale)) { + if ((bias.x != _pipeline._stateCache.depthBias) || (bias.y != _pipeline._stateCache.depthBiasSlopeScale)) { if ((bias.x != 0.0f) || (bias.y != 0.0f)) { glEnable(GL_POLYGON_OFFSET_FILL); glEnable(GL_POLYGON_OFFSET_LINE); @@ -610,10 +150,10 @@ void GLBackend::do_setStateDepthBias(Vec2 bias) { glDisable(GL_POLYGON_OFFSET_LINE); glDisable(GL_POLYGON_OFFSET_POINT); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); - _pipeline._stateCache.depthBias = bias.x; - _pipeline._stateCache.depthBiasSlopeScale = bias.y; + _pipeline._stateCache.depthBias = bias.x; + _pipeline._stateCache.depthBiasSlopeScale = bias.y; } } @@ -622,15 +162,15 @@ void GLBackend::do_setStateDepthTest(State::DepthTest test) { if (test.isEnabled()) { glEnable(GL_DEPTH_TEST); glDepthMask(test.getWriteMask()); - glDepthFunc(GL_COMPARISON_FUNCTIONS[test.getFunction()]); + glDepthFunc(COMPARISON_TO_GL[test.getFunction()]); } else { glDisable(GL_DEPTH_TEST); } if (CHECK_GL_ERROR()) { qDebug() << "DepthTest" << (test.isEnabled() ? "Enabled" : "Disabled") - << "Mask=" << (test.getWriteMask() ? "Write" : "no Write") - << "Func=" << test.getFunction() - << "Raw=" << test.getRaw(); + << "Mask=" << (test.getWriteMask() ? "Write" : "no Write") + << "Func=" << test.getFunction() + << "Raw=" << test.getRaw(); } _pipeline._stateCache.depthTest = test; @@ -638,7 +178,7 @@ void GLBackend::do_setStateDepthTest(State::DepthTest test) { } void GLBackend::do_setStateStencil(State::StencilActivation activation, State::StencilTest frontTest, State::StencilTest backTest) { - + if ((_pipeline._stateCache.stencilActivation != activation) || (_pipeline._stateCache.stencilTestFront != frontTest) || (_pipeline._stateCache.stencilTestBack != backTest)) { @@ -665,18 +205,18 @@ void GLBackend::do_setStateStencil(State::StencilActivation activation, State::S if (frontTest != backTest) { glStencilOpSeparate(GL_FRONT, STENCIL_OPS[frontTest.getFailOp()], STENCIL_OPS[frontTest.getPassOp()], STENCIL_OPS[frontTest.getDepthFailOp()]); - glStencilFuncSeparate(GL_FRONT, GL_COMPARISON_FUNCTIONS[frontTest.getFunction()], frontTest.getReference(), frontTest.getReadMask()); + glStencilFuncSeparate(GL_FRONT, COMPARISON_TO_GL[frontTest.getFunction()], frontTest.getReference(), frontTest.getReadMask()); glStencilOpSeparate(GL_BACK, STENCIL_OPS[backTest.getFailOp()], STENCIL_OPS[backTest.getPassOp()], STENCIL_OPS[backTest.getDepthFailOp()]); - glStencilFuncSeparate(GL_BACK, GL_COMPARISON_FUNCTIONS[backTest.getFunction()], backTest.getReference(), backTest.getReadMask()); + glStencilFuncSeparate(GL_BACK, COMPARISON_TO_GL[backTest.getFunction()], backTest.getReference(), backTest.getReadMask()); } else { glStencilOp(STENCIL_OPS[frontTest.getFailOp()], STENCIL_OPS[frontTest.getPassOp()], STENCIL_OPS[frontTest.getDepthFailOp()]); - glStencilFunc(GL_COMPARISON_FUNCTIONS[frontTest.getFunction()], frontTest.getReference(), frontTest.getReadMask()); + glStencilFunc(COMPARISON_TO_GL[frontTest.getFunction()], frontTest.getReference(), frontTest.getReadMask()); } } else { glDisable(GL_STENCIL_TEST); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.stencilActivation = activation; _pipeline._stateCache.stencilTestFront = frontTest; @@ -691,7 +231,7 @@ void GLBackend::do_setStateAlphaToCoverageEnable(bool enable) { } else { glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.alphaToCoverageEnable = enable; } @@ -699,15 +239,13 @@ void GLBackend::do_setStateAlphaToCoverageEnable(bool enable) { void GLBackend::do_setStateSampleMask(uint32 mask) { if (_pipeline._stateCache.sampleMask != mask) { -#ifdef GPU_CORE_PROFILE if (mask == 0xFFFFFFFF) { glDisable(GL_SAMPLE_MASK); } else { glEnable(GL_SAMPLE_MASK); glSampleMaski(0, mask); } - (void) CHECK_GL_ERROR(); -#endif + (void)CHECK_GL_ERROR(); _pipeline._stateCache.sampleMask = mask; } } @@ -717,40 +255,16 @@ void GLBackend::do_setStateBlend(State::BlendFunction function) { if (function.isEnabled()) { glEnable(GL_BLEND); - static GLenum GL_BLEND_OPS[] = { - GL_FUNC_ADD, - GL_FUNC_SUBTRACT, - GL_FUNC_REVERSE_SUBTRACT, - GL_MIN, - GL_MAX }; + glBlendEquationSeparate(BLEND_OPS_TO_GL[function.getOperationColor()], BLEND_OPS_TO_GL[function.getOperationAlpha()]); + (void)CHECK_GL_ERROR(); - glBlendEquationSeparate(GL_BLEND_OPS[function.getOperationColor()], GL_BLEND_OPS[function.getOperationAlpha()]); - (void) CHECK_GL_ERROR(); - static GLenum BLEND_ARGS[] = { - GL_ZERO, - GL_ONE, - GL_SRC_COLOR, - GL_ONE_MINUS_SRC_COLOR, - GL_SRC_ALPHA, - GL_ONE_MINUS_SRC_ALPHA, - GL_DST_ALPHA, - GL_ONE_MINUS_DST_ALPHA, - GL_DST_COLOR, - GL_ONE_MINUS_DST_COLOR, - GL_SRC_ALPHA_SATURATE, - GL_CONSTANT_COLOR, - GL_ONE_MINUS_CONSTANT_COLOR, - GL_CONSTANT_ALPHA, - GL_ONE_MINUS_CONSTANT_ALPHA, - }; - - glBlendFuncSeparate(BLEND_ARGS[function.getSourceColor()], BLEND_ARGS[function.getDestinationColor()], - BLEND_ARGS[function.getSourceAlpha()], BLEND_ARGS[function.getDestinationAlpha()]); + glBlendFuncSeparate(BLEND_ARGS_TO_GL[function.getSourceColor()], BLEND_ARGS_TO_GL[function.getDestinationColor()], + BLEND_ARGS_TO_GL[function.getSourceAlpha()], BLEND_ARGS_TO_GL[function.getDestinationAlpha()]); } else { glDisable(GL_BLEND); } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.blendFunction = function; } @@ -759,10 +273,10 @@ void GLBackend::do_setStateBlend(State::BlendFunction function) { void GLBackend::do_setStateColorWriteMask(uint32 mask) { if (_pipeline._stateCache.colorWriteMask != mask) { glColorMask(mask & State::ColorMask::WRITE_RED, - mask & State::ColorMask::WRITE_GREEN, - mask & State::ColorMask::WRITE_BLUE, - mask & State::ColorMask::WRITE_ALPHA ); - (void) CHECK_GL_ERROR(); + mask & State::ColorMask::WRITE_GREEN, + mask & State::ColorMask::WRITE_BLUE, + mask & State::ColorMask::WRITE_ALPHA); + (void)CHECK_GL_ERROR(); _pipeline._stateCache.colorWriteMask = mask; } @@ -771,12 +285,12 @@ void GLBackend::do_setStateColorWriteMask(uint32 mask) { void GLBackend::do_setStateBlendFactor(Batch& batch, size_t paramOffset) { Vec4 factor(batch._params[paramOffset + 0]._float, - batch._params[paramOffset + 1]._float, - batch._params[paramOffset + 2]._float, - batch._params[paramOffset + 3]._float); + batch._params[paramOffset + 1]._float, + batch._params[paramOffset + 2]._float, + batch._params[paramOffset + 3]._float); glBlendColor(factor.x, factor.y, factor.z, factor.w); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } void GLBackend::do_setStateScissorRect(Batch& batch, size_t paramOffset) { @@ -790,5 +304,6 @@ void GLBackend::do_setStateScissorRect(Batch& batch, size_t paramOffset) { } } glScissor(rect.x, rect.y, rect.z, rect.w); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } + diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index 952e5bad8f..6d9b5fd2c7 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -9,591 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "GLBackend.h" - -#include -#include -#include - -#include "GLBackendShared.h" - -#include "GLBackendTextureTransfer.h" +#include "GLTexture.h" using namespace gpu; using namespace gpu::gl; - -GLenum gpuToGLTextureType(const Texture& texture) { - switch (texture.getType()) { - case Texture::TEX_2D: - return GL_TEXTURE_2D; - break; - - case Texture::TEX_CUBE: - return GL_TEXTURE_CUBE_MAP; - break; - - default: - qFatal("Unsupported texture type"); - } - Q_UNREACHABLE(); - return GL_TEXTURE_2D; -} - -GLuint allocateSingleTexture() { - Backend::incrementTextureGPUCount(); - GLuint result; - glGenTextures(1, &result); - return result; -} - - -// FIXME placeholder for texture memory over-use -#define DEFAULT_MAX_MEMORY_MB 256 - -float GLBackend::GLTexture::getMemoryPressure() { - // Check for an explicit memory limit - auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); - - // If no memory limit has been set, use a percentage of the total dedicated memory - if (!availableTextureMemory) { - auto totalGpuMemory = GLBackend::getDedicatedMemory(); - - // If no limit has been explicitly set, and the dedicated memory can't be determined, - // just use a fallback fixed value of 256 MB - if (!totalGpuMemory) { - totalGpuMemory = MB_TO_BYTES(DEFAULT_MAX_MEMORY_MB); - } - - // Allow 75% of all available GPU memory to be consumed by textures - // FIXME overly conservative? - availableTextureMemory = (totalGpuMemory >> 2) * 3; - } - - // Return the consumed texture memory divided by the available texture memory. - auto consumedGpuMemory = Context::getTextureGPUMemoryUsage(); - return (float)consumedGpuMemory / (float)availableTextureMemory; -} - -GLBackend::GLTexture::DownsampleSource::DownsampleSource(GLTexture& oldTexture) : - _texture(oldTexture._privateTexture), - _minMip(oldTexture._minMip), - _maxMip(oldTexture._maxMip) -{ - // Take ownership of the GL texture - oldTexture._texture = oldTexture._privateTexture = 0; -} - -GLBackend::GLTexture::DownsampleSource::~DownsampleSource() { - if (_texture) { - Backend::decrementTextureGPUCount(); - glDeleteTextures(1, &_texture); - } -} - -const GLenum GLBackend::GLTexture::CUBE_FACE_LAYOUT[6] = { - GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, - GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, - GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z -}; - -static std::map _textureCountByMips; -static uint16 _currentMaxMipCount { 0 }; - -GLBackend::GLTexture::GLTexture(bool transferrable, const Texture& texture, bool init) : - _storageStamp(texture.getStamp()), - _target(gpuToGLTextureType(texture)), - _maxMip(texture.maxMip()), - _minMip(texture.minMip()), - _transferrable(transferrable), - _virtualSize(texture.evalTotalSize()), - _size(_virtualSize), - _gpuTexture(texture) -{ - Q_UNUSED(init); - - if (_transferrable) { - uint16 mipCount = usedMipLevels(); - _currentMaxMipCount = std::max(_currentMaxMipCount, mipCount); - if (!_textureCountByMips.count(mipCount)) { - _textureCountByMips[mipCount] = 1; - } else { - ++_textureCountByMips[mipCount]; - } - } else { - withPreservedTexture([&] { - createTexture(); - }); - _contentStamp = _gpuTexture.getDataStamp(); - postTransfer(); - } - - Backend::updateTextureGPUMemoryUsage(0, _size); - Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); -} - -// Create the texture and allocate storage -GLBackend::GLTexture::GLTexture(bool transferrable, const Texture& texture) : - GLTexture(transferrable, texture, true) -{ - Backend::setGPUObject(texture, this); -} - -// Create the texture and copy from the original higher resolution version -GLBackend::GLTexture::GLTexture(GLTexture& originalTexture, const gpu::Texture& texture) : - GLTexture(originalTexture._transferrable, texture, true) -{ - if (!originalTexture._texture) { - qFatal("Invalid original texture"); - } - Q_ASSERT(_minMip >= originalTexture._minMip); - // Our downsampler takes ownership of the texture - _downsampleSource = std::make_shared(originalTexture); - _texture = _downsampleSource->_texture; - - // Set the GPU object last because that implicitly destroys the originalTexture object - Backend::setGPUObject(texture, this); -} - -GLBackend::GLTexture::~GLTexture() { - if (_privateTexture != 0) { - Backend::decrementTextureGPUCount(); - glDeleteTextures(1, &_privateTexture); - } - - if (_transferrable) { - uint16 mipCount = usedMipLevels(); - Q_ASSERT(_textureCountByMips.count(mipCount)); - auto& numTexturesForMipCount = _textureCountByMips[mipCount]; - --numTexturesForMipCount; - if (0 == numTexturesForMipCount) { - _textureCountByMips.erase(mipCount); - if (mipCount == _currentMaxMipCount) { - _currentMaxMipCount = _textureCountByMips.rbegin()->first; - } - } - } - - Backend::updateTextureGPUMemoryUsage(_size, 0); - Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); -} - -const std::vector& GLBackend::GLTexture::getFaceTargets() const { - static std::vector cubeFaceTargets { - GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, - GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, - GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z - }; - static std::vector faceTargets { - GL_TEXTURE_2D - }; - switch (_target) { - case GL_TEXTURE_2D: - return faceTargets; - case GL_TEXTURE_CUBE_MAP: - return cubeFaceTargets; - default: - Q_UNREACHABLE(); - break; - } - Q_UNREACHABLE(); - return faceTargets; -} - -void GLBackend::GLTexture::withPreservedTexture(std::function f) { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; - - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; - - default: - qFatal("Unsupported texture type"); - } - (void)CHECK_GL_ERROR(); - - f(); - - glBindTexture(_target, boundTex); - (void)CHECK_GL_ERROR(); -} - -void GLBackend::GLTexture::createTexture() { - _privateTexture = allocateSingleTexture(); - - glBindTexture(_target, _privateTexture); - (void)CHECK_GL_ERROR(); - - allocateStorage(); - (void)CHECK_GL_ERROR(); - - syncSampler(_gpuTexture.getSampler(), _gpuTexture.getType(), this); - (void)CHECK_GL_ERROR(); -} - -void GLBackend::GLTexture::allocateStorage() { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuTexture.getTexelFormat()); - glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); - (void)CHECK_GL_ERROR(); - glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - (void)CHECK_GL_ERROR(); - if (GLEW_VERSION_4_2 && !_gpuTexture.getTexelFormat().isCompressed()) { - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuTexture.evalMipDimensions(_minMip); - glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); - (void)CHECK_GL_ERROR(); - } else { - for (uint16_t l = _minMip; l <= _maxMip; l++) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuTexture.evalMipDimensions(l); - for (GLenum target : getFaceTargets()) { - glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); - (void)CHECK_GL_ERROR(); - } - } - } -} - - -void GLBackend::GLTexture::setSize(GLuint size) { - Backend::updateTextureGPUMemoryUsage(_size, size); - _size = size; -} - -void GLBackend::GLTexture::updateSize() { - setSize(_virtualSize); - if (!_texture) { - return; - } - - if (_gpuTexture.getTexelFormat().isCompressed()) { - GLenum proxyType = GL_TEXTURE_2D; - GLuint numFaces = 1; - if (_gpuTexture.getType() == gpu::Texture::TEX_CUBE) { - proxyType = CUBE_FACE_LAYOUT[0]; - numFaces = CUBE_NUM_FACES; - } - GLint gpuSize{ 0 }; - glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); - (void)CHECK_GL_ERROR(); - - if (gpuSize) { - for (GLuint level = _minMip; level < _maxMip; level++) { - GLint levelSize{ 0 }; - glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); - levelSize *= numFaces; - - if (levelSize <= 0) { - break; - } - gpuSize += levelSize; - } - (void)CHECK_GL_ERROR(); - setSize(gpuSize); - return; - } - } -} - -bool GLBackend::GLTexture::isInvalid() const { - return _storageStamp < _gpuTexture.getStamp(); -} - -bool GLBackend::GLTexture::isOutdated() const { - return GLTexture::Idle == _syncState && _contentStamp < _gpuTexture.getDataStamp(); -} - -bool GLBackend::GLTexture::isOverMaxMemory() const { - // FIXME switch to using the max mip count used from the previous frame - if (usedMipLevels() < _currentMaxMipCount) { - return false; - } - Q_ASSERT(usedMipLevels() == _currentMaxMipCount); - - if (getMemoryPressure() < 1.0f) { - return false; - } - - return true; -} - -bool GLBackend::GLTexture::isReady() const { - // If we have an invalid texture, we're never ready - if (isInvalid()) { - return false; - } - - // If we're out of date, but the transfer is in progress, report ready - // as a special case - auto syncState = _syncState.load(); - - if (isOutdated()) { - return Idle != syncState; - } - - if (Idle != syncState) { - return false; - } - - return true; -} - -// Move content bits from the CPU to the GPU for a given mip / face -void GLBackend::GLTexture::transferMip(uint16_t mipLevel, uint8_t face) const { - auto mip = _gpuTexture.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuTexture.getTexelFormat(), mip->getFormat()); - //GLenum target = getFaceTargets()[face]; - GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; - auto size = _gpuTexture.evalMipDimensions(mipLevel); - glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - (void)CHECK_GL_ERROR(); -} - -// This should never happen on the main thread -// Move content bits from the CPU to the GPU -void GLBackend::GLTexture::transfer() const { - PROFILE_RANGE(__FUNCTION__); - //qDebug() << "Transferring texture: " << _privateTexture; - // Need to update the content of the GPU object from the source sysmem of the texture - if (_contentStamp >= _gpuTexture.getDataStamp()) { - return; - } - - glBindTexture(_target, _privateTexture); - (void)CHECK_GL_ERROR(); - - if (_downsampleSource) { - GLuint fbo { 0 }; - glGenFramebuffers(1, &fbo); - (void)CHECK_GL_ERROR(); - glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); - (void)CHECK_GL_ERROR(); - // Find the distance between the old min mip and the new one - uint16 mipOffset = _minMip - _downsampleSource->_minMip; - for (uint16 i = _minMip; i <= _maxMip; ++i) { - uint16 targetMip = i - _minMip; - uint16 sourceMip = targetMip + mipOffset; - Vec3u dimensions = _gpuTexture.evalMipDimensions(i); - for (GLenum target : getFaceTargets()) { - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, _downsampleSource->_texture, sourceMip); - (void)CHECK_GL_ERROR(); - glCopyTexSubImage2D(target, targetMip, 0, 0, 0, 0, dimensions.x, dimensions.y); - (void)CHECK_GL_ERROR(); - } - } - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - glDeleteFramebuffers(1, &fbo); - } else { - // GO through the process of allocating the correct storage and/or update the content - switch (_gpuTexture.getType()) { - case Texture::TEX_2D: - { - for (uint16_t i = _minMip; i <= _maxMip; ++i) { - if (_gpuTexture.isStoredMipFaceAvailable(i)) { - transferMip(i); - } - } - } - break; - - case Texture::TEX_CUBE: - // transfer pixels from each faces - for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuTexture.isStoredMipFaceAvailable(i, f)) { - transferMip(i, f); - } - } - } - break; - - default: - qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuTexture.getType() << " not supported"; - break; - } - } - if (_gpuTexture.isAutogenerateMips()) { - glGenerateMipmap(_target); - (void)CHECK_GL_ERROR(); - } -} - -// Do any post-transfer operations that might be required on the main context / rendering thread -void GLBackend::GLTexture::postTransfer() { - setSyncState(GLTexture::Idle); - - // The public gltexture becaomes available - _texture = _privateTexture; - - _downsampleSource.reset(); - - // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory - switch (_gpuTexture.getType()) { - case Texture::TEX_2D: - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuTexture.isStoredMipFaceAvailable(i)) { - _gpuTexture.notifyMipFaceGPULoaded(i); - } - } - break; - - case Texture::TEX_CUBE: - // transfer pixels from each faces - for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuTexture.isStoredMipFaceAvailable(i, f)) { - _gpuTexture.notifyMipFaceGPULoaded(i, f); - } - } - } - break; - - default: - qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuTexture.getType() << " not supported"; - break; - } -} - -GLBackend::GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer, bool needTransfer) { - const Texture& texture = *texturePointer; - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - // If the object hasn't been created, or the object definition is out of date, drop and re-create - GLTexture* object = Backend::getGPUObject(texture); - - // Create the texture if need be (force re-creation if the storage stamp changes - // for easier use of immutable storage) - if (!object || object->isInvalid()) { - // This automatically any previous texture - object = new GLTexture(needTransfer, texture); - } - - // Object maybe doens't neet to be tranasferred after creation - if (!object->_transferrable) { - return object; - } - - // If we just did a transfer, return the object after doing post-transfer work - if (GLTexture::Transferred == object->getSyncState()) { - object->postTransfer(); - return object; - } - - if (object->isReady()) { - // Do we need to reduce texture memory usage? - if (object->isOverMaxMemory() && texturePointer->incremementMinMip()) { - // This automatically destroys the old texture - object = new GLTexture(*object, texture); - _textureTransferHelper->transferTexture(texturePointer); - } - } else if (object->isOutdated()) { - // Object might be outdated, if so, start the transfer - // (outdated objects that are already in transfer will have reported 'true' for ready() - _textureTransferHelper->transferTexture(texturePointer); - } - - return object; -} - -std::shared_ptr GLBackend::_textureTransferHelper; - -void GLBackend::initTextureTransferHelper() { - _textureTransferHelper = std::make_shared(); -} - -GLuint GLBackend::getTextureID(const TexturePointer& texture, bool sync) { - if (!texture) { - return 0; - } - GLTexture* object { nullptr }; - if (sync) { - object = GLBackend::syncGPUObject(texture); - } else { - object = Backend::getGPUObject(*texture); - } - if (object) { - if (object->getSyncState() == GLTexture::Idle) { - return object->_texture; - } else if (object->_downsampleSource) { - return object->_downsampleSource->_texture; - } else { - return 0; - } - } else { - return 0; - } -} - -void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GLTexture* object) { - if (!object) return; - - class GLFilterMode { - public: - GLint minFilter; - GLint magFilter; - }; - static const GLFilterMode filterModes[Sampler::NUM_FILTERS] = { - { GL_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_POINT, - { GL_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR, - { GL_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT, - { GL_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR, - - { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_MIP_POINT, - { GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_MAG_POINT_MIP_LINEAR, - { GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT, - { GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR }, //FILTER_MIN_POINT_MAG_MIP_LINEAR, - { GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_MIP_POINT, - { GL_LINEAR_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR, - { GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR_MIP_POINT, - { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_MIP_LINEAR, - { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR } //FILTER_ANISOTROPIC, - }; - - auto fm = filterModes[sampler.getFilter()]; - glTexParameteri(object->_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); - glTexParameteri(object->_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); - - static const GLenum comparisonFuncs[NUM_COMPARISON_FUNCS] = { - GL_NEVER, - GL_LESS, - GL_EQUAL, - GL_LEQUAL, - GL_GREATER, - GL_NOTEQUAL, - GL_GEQUAL, - GL_ALWAYS }; - - if (sampler.doComparison()) { - glTexParameteri(object->_target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); - glTexParameteri(object->_target, GL_TEXTURE_COMPARE_FUNC, comparisonFuncs[sampler.getComparisonFunction()]); - } else { - glTexParameteri(object->_target, GL_TEXTURE_COMPARE_MODE, GL_NONE); - } - - static const GLenum wrapModes[Sampler::NUM_WRAP_MODES] = { - GL_REPEAT, // WRAP_REPEAT, - GL_MIRRORED_REPEAT, // WRAP_MIRROR, - GL_CLAMP_TO_EDGE, // WRAP_CLAMP, - GL_CLAMP_TO_BORDER, // WRAP_BORDER, - GL_MIRROR_CLAMP_TO_EDGE_EXT }; // WRAP_MIRROR_ONCE, - - glTexParameteri(object->_target, GL_TEXTURE_WRAP_S, wrapModes[sampler.getWrapModeU()]); - glTexParameteri(object->_target, GL_TEXTURE_WRAP_T, wrapModes[sampler.getWrapModeV()]); - glTexParameteri(object->_target, GL_TEXTURE_WRAP_R, wrapModes[sampler.getWrapModeW()]); - - glTexParameterfv(object->_target, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); - glTexParameteri(object->_target, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); - glTexParameterf(object->_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); - glTexParameterf(object->_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); - glTexParameterf(object->_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); -} - void GLBackend::do_generateTextureMips(Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); if (!resourceTexture) { @@ -601,28 +21,10 @@ void GLBackend::do_generateTextureMips(Batch& batch, size_t paramOffset) { } // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = GLBackend::syncGPUObject(resourceTexture, false); + GLTexture* object = syncGPUObject(resourceTexture, false); if (!object) { return; } - // IN 4.1 we still need to find an available slot - auto freeSlot = _resource.findEmptyTextureSlot(); - auto bindingSlot = (freeSlot < 0 ? 0 : freeSlot); - glActiveTexture(GL_TEXTURE0 + bindingSlot); - glBindTexture(object->_target, object->_texture); - - glGenerateMipmap(object->_target); - - if (freeSlot < 0) { - // If had to use slot 0 then restore state - GLTexture* boundObject = GLBackend::syncGPUObject(_resource._textures[0]); - if (boundObject) { - glBindTexture(boundObject->_target, boundObject->_texture); - } - } else { - // clean up - glBindTexture(object->_target, 0); - } - (void)CHECK_GL_ERROR(); + object->generateMips(); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp index 7c60eac10e..6120df0bfc 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp @@ -9,7 +9,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "GLBackend.h" -#include "GLBackendShared.h" using namespace gpu; using namespace gpu::gl; @@ -51,26 +50,11 @@ void GLBackend::do_setDepthRangeTransform(Batch& batch, size_t paramOffset) { } } -void GLBackend::initTransform() { - glGenBuffers(1, &_transform._objectBuffer); - glGenBuffers(1, &_transform._cameraBuffer); - glGenBuffers(1, &_transform._drawCallInfoBuffer); -#ifndef GPU_SSBO_DRAW_CALL_INFO - glGenTextures(1, &_transform._objectBufferTexture); -#endif - size_t cameraSize = sizeof(TransformStageState::CameraBufferElement); - while (_transform._cameraUboSize < cameraSize) { - _transform._cameraUboSize += _uboAlignment; - } -} - void GLBackend::killTransform() { glDeleteBuffers(1, &_transform._objectBuffer); glDeleteBuffers(1, &_transform._cameraBuffer); glDeleteBuffers(1, &_transform._drawCallInfoBuffer); -#ifndef GPU_SSBO_DRAW_CALL_INFO glDeleteTextures(1, &_transform._objectBufferTexture); -#endif } void GLBackend::syncTransformStateCache() { @@ -118,64 +102,6 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo _invalidView = _invalidProj = _invalidViewport = false; } -void GLBackend::TransformStageState::transfer(const Batch& batch) const { - // FIXME not thread safe - static std::vector bufferData; - if (!_cameras.empty()) { - bufferData.resize(_cameraUboSize * _cameras.size()); - for (size_t i = 0; i < _cameras.size(); ++i) { - memcpy(bufferData.data() + (_cameraUboSize * i), &_cameras[i], sizeof(CameraBufferElement)); - } - glBindBuffer(GL_UNIFORM_BUFFER, _cameraBuffer); - glBufferData(GL_UNIFORM_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); - glBindBuffer(GL_UNIFORM_BUFFER, 0); - } - - if (!batch._objects.empty()) { - auto byteSize = batch._objects.size() * sizeof(Batch::TransformObject); - bufferData.resize(byteSize); - memcpy(bufferData.data(), batch._objects.data(), byteSize); - -#ifdef GPU_SSBO_DRAW_CALL_INFO - glBindBuffer(GL_SHADER_STORAGE_BUFFER, _objectBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); -#else - glBindBuffer(GL_TEXTURE_BUFFER, _objectBuffer); - glBufferData(GL_TEXTURE_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); - glBindBuffer(GL_TEXTURE_BUFFER, 0); -#endif - } - - if (!batch._namedData.empty()) { - bufferData.clear(); - for (auto& data : batch._namedData) { - auto currentSize = bufferData.size(); - auto bytesToCopy = data.second.drawCallInfos.size() * sizeof(Batch::DrawCallInfo); - bufferData.resize(currentSize + bytesToCopy); - memcpy(bufferData.data() + currentSize, data.second.drawCallInfos.data(), bytesToCopy); - _drawCallInfoOffsets[data.first] = (GLvoid*)currentSize; - } - - glBindBuffer(GL_ARRAY_BUFFER, _drawCallInfoBuffer); - glBufferData(GL_ARRAY_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); - } - -#ifdef GPU_SSBO_DRAW_CALL_INFO - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _objectBuffer); -#else - glActiveTexture(GL_TEXTURE0 + TRANSFORM_OBJECT_SLOT); - glBindTexture(GL_TEXTURE_BUFFER, _objectBufferTexture); - glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, _objectBuffer); -#endif - - CHECK_GL_ERROR(); - - // Make sure the current Camera offset is unknown before render Draw - _currentCameraOffset = INVALID_OFFSET; -} - void GLBackend::TransformStageState::update(size_t commandIndex, const StereoState& stereo) const { size_t offset = INVALID_OFFSET; while ((_camerasItr != _cameraOffsets.end()) && (commandIndex >= (*_camerasItr).first)) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp new file mode 100644 index 0000000000..cd0f86a410 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp @@ -0,0 +1,28 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLBuffer.h" + +using namespace gpu; +using namespace gpu::gl; + +GLBuffer::~GLBuffer() { + glDeleteBuffers(1, &_id); + Backend::decrementBufferGPUCount(); + Backend::updateBufferGPUMemoryUsage(_size, 0); +} + +GLBuffer::GLBuffer(const Buffer& buffer, GLuint id) : + GLObject(buffer, id), + _size((GLuint)buffer._sysmem.getSize()), + _stamp(buffer._sysmem.getStamp()) +{ + Backend::incrementBufferGPUCount(); + Backend::updateBufferGPUMemoryUsage(0, _size); +} + diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h new file mode 100644 index 0000000000..4783541b11 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h @@ -0,0 +1,57 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLBuffer_h +#define hifi_gpu_gl_GLBuffer_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLBuffer : public GLObject { +public: + template + static GLBufferType* sync(const Buffer& buffer) { + GLBufferType* object = Backend::getGPUObject(buffer); + + // Has the storage size changed? + if (!object || object->_stamp != buffer.getSysmem().getStamp()) { + object = new GLBufferType(buffer, object); + } + + if (0 != (buffer._flags & Buffer::DIRTY)) { + object->transfer(); + } + + return object; + } + + template + static GLuint getId(const Buffer& buffer) { + GLBuffer* bo = sync(buffer); + if (bo) { + return bo->_buffer; + } else { + return 0; + } + } + + const GLuint& _buffer { _id }; + const GLuint _size; + const Stamp _stamp; + + ~GLBuffer(); + + virtual void transfer() = 0; + +protected: + GLBuffer(const Buffer& buffer, GLuint id); +}; + +} } + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp new file mode 100644 index 0000000000..91f7fbd494 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -0,0 +1,39 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLFramebuffer.h" + +using namespace gpu; +using namespace gpu::gl; + + +bool GLFramebuffer::checkStatus(GLenum target) const { + bool result = false; + switch (_status) { + case GL_FRAMEBUFFER_COMPLETE: + // Success ! + result = true; + break; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; + break; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT."; + break; + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER."; + break; + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER."; + break; + case GL_FRAMEBUFFER_UNSUPPORTED: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; + break; + } + return result; +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h new file mode 100644 index 0000000000..d54c181c20 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -0,0 +1,76 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLFramebuffer_h +#define hifi_gpu_gl_GLFramebuffer_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLFramebuffer : public GLObject { +public: + template + static GLFramebufferType* sync(const Framebuffer& framebuffer) { + GLFramebufferType* object = Backend::getGPUObject(framebuffer); + + bool needsUpate { false }; + if (!object || + framebuffer.getDepthStamp() != object->_depthStamp || + framebuffer.getColorStamps() != object->_colorStamps) { + needsUpate = true; + } + + // If GPU object already created and in sync + if (!needsUpate) { + return object; + } else if (framebuffer.isEmpty()) { + // NO framebuffer definition yet so let's avoid thinking + return nullptr; + } + + // need to have a gpu object? + if (!object) { + // All is green, assign the gpuobject to the Framebuffer + object = new GLFramebufferType(framebuffer); + Backend::setGPUObject(framebuffer, object); + (void)CHECK_GL_ERROR(); + } + + object->update(); + return object; + } + + template + static GLuint getId(const Framebuffer& framebuffer) { + GLFramebufferType* fbo = sync(framebuffer); + if (fbo) { + return fbo->_id; + } else { + return 0; + } + } + + const GLuint& _fbo { _id }; + std::vector _colorBuffers; + Stamp _depthStamp { 0 }; + std::vector _colorStamps; + +protected: + GLenum _status { GL_FRAMEBUFFER_COMPLETE }; + virtual void update() = 0; + bool checkStatus(GLenum target) const; + + GLFramebuffer(const Framebuffer& framebuffer, GLuint id) : GLObject(framebuffer, id) {} + ~GLFramebuffer() { if (_id) { glDeleteFramebuffers(1, &_id); } }; + +}; + +} } + + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp new file mode 100644 index 0000000000..19cf798b19 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp @@ -0,0 +1,48 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLPipeline.h" + +#include "GLShader.h" +#include "GLState.h" + +using namespace gpu; +using namespace gpu::gl; + +GLPipeline* GLPipeline::sync(const Pipeline& pipeline) { + GLPipeline* object = Backend::getGPUObject(pipeline); + + // If GPU object already created then good + if (object) { + return object; + } + + // No object allocated yet, let's see if it's worth it... + ShaderPointer shader = pipeline.getProgram(); + GLShader* programObject = GLShader::sync(*shader); + if (programObject == nullptr) { + return nullptr; + } + + StatePointer state = pipeline.getState(); + GLState* stateObject = GLState::sync(*state); + if (stateObject == nullptr) { + return nullptr; + } + + // Program and state are valid, we can create the pipeline object + if (!object) { + object = new GLPipeline(); + Backend::setGPUObject(pipeline, object); + } + + object->_program = programObject; + object->_state = stateObject; + + return object; +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.h b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h new file mode 100644 index 0000000000..9ade2bb830 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLPipeline_h +#define hifi_gpu_gl_GLPipeline_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLPipeline : public GPUObject { +public: + static GLPipeline* sync(const Pipeline& pipeline); + + GLShader* _program { nullptr }; + GLState* _state { nullptr }; +}; + +} } + + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLQuery.cpp new file mode 100644 index 0000000000..b358db40c9 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLQuery.cpp @@ -0,0 +1,12 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLQuery.h" + +using namespace gpu; +using namespace gpu::gl; diff --git a/libraries/gpu-gl/src/gpu/gl/GLQuery.h b/libraries/gpu-gl/src/gpu/gl/GLQuery.h new file mode 100644 index 0000000000..7b14b227ab --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLQuery.h @@ -0,0 +1,57 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLQuery_h +#define hifi_gpu_gl_GLQuery_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLQuery : public GLObject { + using Parent = gpu::gl::GLObject; +public: + template + static GLQueryType* sync(const Query& query) { + GLQueryType* object = Backend::getGPUObject(query); + + // need to have a gpu object? + if (!object) { + // All is green, assign the gpuobject to the Query + object = new GLQueryType(query); + (void)CHECK_GL_ERROR(); + Backend::setGPUObject(query, object); + } + + return object; + } + + template + static GLuint getId(const QueryPointer& query) { + if (!query) { + return 0; + } + + GLQuery* object = sync(*query); + if (!object) { + return 0; + } + + return object->_qo; + } + + const GLuint& _qo { _id }; + GLuint64 _result { (GLuint64)-1 }; + +protected: + GLQuery(const Query& query, GLuint id) : Parent(query, id) {} + ~GLQuery() { if (_id) { glDeleteQueries(1, &_id); } } +}; + +} } + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLShader.cpp b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp new file mode 100644 index 0000000000..c89bc88899 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp @@ -0,0 +1,182 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLShader.h" +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +GLShader::GLShader() { +} + +GLShader::~GLShader() { + for (auto& so : _shaderObjects) { + if (so.glshader != 0) { + glDeleteShader(so.glshader); + } + if (so.glprogram != 0) { + glDeleteProgram(so.glprogram); + } + } +} + +GLShader* compileBackendShader(const Shader& shader) { + // Any GLSLprogram ? normally yes... + const std::string& shaderSource = shader.getSource().getCode(); + + // GLSL version + const std::string glslVersion = { + "#version 410 core" + }; + + // Shader domain + const int NUM_SHADER_DOMAINS = 2; + const GLenum SHADER_DOMAINS[NUM_SHADER_DOMAINS] = { + GL_VERTEX_SHADER, + GL_FRAGMENT_SHADER + }; + GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; + + // Domain specific defines + const std::string domainDefines[NUM_SHADER_DOMAINS] = { + "#define GPU_VERTEX_SHADER", + "#define GPU_PIXEL_SHADER" + }; + + // Versions specific of the shader + const std::string versionDefines[GLShader::NumVersions] = { + "" + }; + + GLShader::ShaderObjects shaderObjects; + + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& shaderObject = shaderObjects[version]; + + std::string shaderDefines = glslVersion + "\n" + domainDefines[shader.getType()] + "\n" + versionDefines[version]; + + bool result = compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram); + if (!result) { + return nullptr; + } + } + + // So far so good, the shader is created successfully + GLShader* object = new GLShader(); + object->_shaderObjects = shaderObjects; + + return object; +} + +GLShader* compileBackendProgram(const Shader& program) { + if (!program.isProgram()) { + return nullptr; + } + + GLShader::ShaderObjects programObjects; + + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& programObject = programObjects[version]; + + // Let's go through every shaders and make sure they are ready to go + std::vector< GLuint > shaderGLObjects; + for (auto subShader : program.getShaders()) { + auto object = GLShader::sync(*subShader); + if (object) { + shaderGLObjects.push_back(object->_shaderObjects[version].glshader); + } else { + qCDebug(gpugllogging) << "GLShader::compileBackendProgram - One of the shaders of the program is not compiled?"; + return nullptr; + } + } + + GLuint glprogram = compileProgram(shaderGLObjects); + if (glprogram == 0) { + return nullptr; + } + + programObject.glprogram = glprogram; + + makeProgramBindings(programObject); + } + + // So far so good, the program versions have all been created successfully + GLShader* object = new GLShader(); + object->_shaderObjects = programObjects; + + return object; +} + +GLShader* GLShader::sync(const Shader& shader) { + GLShader* object = Backend::getGPUObject(shader); + + // If GPU object already created then good + if (object) { + return object; + } + // need to have a gpu object? + if (shader.isProgram()) { + GLShader* tempObject = compileBackendProgram(shader); + if (tempObject) { + object = tempObject; + Backend::setGPUObject(shader, object); + } + } else if (shader.isDomain()) { + GLShader* tempObject = compileBackendShader(shader); + if (tempObject) { + object = tempObject; + Backend::setGPUObject(shader, object); + } + } + + return object; +} + +bool GLShader::makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { + + // First make sure the Shader has been compiled + GLShader* object = sync(shader); + if (!object) { + return false; + } + + // Apply bindings to all program versions and generate list of slots from default version + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& shaderObject = object->_shaderObjects[version]; + if (shaderObject.glprogram) { + Shader::SlotSet buffers; + makeUniformBlockSlots(shaderObject.glprogram, slotBindings, buffers); + + Shader::SlotSet uniforms; + Shader::SlotSet textures; + Shader::SlotSet samplers; + makeUniformSlots(shaderObject.glprogram, slotBindings, uniforms, textures, samplers); + + Shader::SlotSet inputs; + makeInputSlots(shaderObject.glprogram, slotBindings, inputs); + + Shader::SlotSet outputs; + makeOutputSlots(shaderObject.glprogram, slotBindings, outputs); + + // Define the public slots only from the default version + if (version == 0) { + shader.defineSlots(uniforms, buffers, textures, samplers, inputs, outputs); + } else { + GLShader::UniformMapping mapping; + for (auto srcUniform : shader.getUniforms()) { + mapping[srcUniform._location] = uniforms.findLocation(srcUniform._name); + } + object->_uniformMappings.push_back(mapping); + } + } + } + + + return true; +} + diff --git a/libraries/gpu-gl/src/gpu/gl/GLShader.h b/libraries/gpu-gl/src/gpu/gl/GLShader.h new file mode 100644 index 0000000000..ca583e6d74 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLShader.h @@ -0,0 +1,52 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLShader_h +#define hifi_gpu_gl_GLShader_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLShader : public GPUObject { +public: + static GLShader* sync(const Shader& shader); + static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings); + + enum Version { + Mono = 0, + NumVersions + }; + + using ShaderObject = gpu::gl::ShaderObject; + using ShaderObjects = std::array< ShaderObject, NumVersions >; + + using UniformMapping = std::map; + using UniformMappingVersions = std::vector; + + GLShader(); + ~GLShader(); + + ShaderObjects _shaderObjects; + UniformMappingVersions _uniformMappings; + + GLuint getProgram(Version version = Mono) const { + return _shaderObjects[version].glprogram; + } + + GLint getUniformLocation(GLint srcLoc, Version version = Mono) { + // THIS will be used in the future PR as we grow the number of versions + // return _uniformMappings[version][srcLoc]; + return srcLoc; + } + +}; + +} } + + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendShader.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp similarity index 57% rename from libraries/gpu-gl/src/gpu/gl/GLBackendShader.cpp rename to libraries/gpu-gl/src/gpu/gl/GLShared.cpp index 8ebf7751d1..17152733d1 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -1,34 +1,648 @@ // -// GLBackendShader.cpp -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 2/28/2015. -// Copyright 2014 High Fidelity, Inc. +// Created by Bradley Austin Davis on 2016/05/14 +// Copyright 2013-2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" -#include "GLBackendShared.h" +#include "GLShared.h" -using namespace gpu; -using namespace gpu::gl; +#include -GLBackend::GLShader::GLShader() -{ -} +#include +#include -GLBackend::GLShader::~GLShader() { - for (auto& so : _shaderObjects) { - if (so.glshader != 0) { - glDeleteShader(so.glshader); - } - if (so.glprogram != 0) { - glDeleteProgram(so.glprogram); +Q_LOGGING_CATEGORY(gpugllogging, "hifi.gpu.gl") + +namespace gpu { namespace gl { + +bool checkGLError(const char* name) { + GLenum error = glGetError(); + if (!error) { + return false; + } else { + switch (error) { + case GL_INVALID_ENUM: + qCDebug(gpugllogging) << "GLBackend::" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; + break; + case GL_INVALID_VALUE: + qCDebug(gpugllogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; + break; + case GL_INVALID_OPERATION: + qCDebug(gpugllogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; + break; + case GL_INVALID_FRAMEBUFFER_OPERATION: + qCDebug(gpugllogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; + break; + case GL_OUT_OF_MEMORY: + qCDebug(gpugllogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; + break; + case GL_STACK_UNDERFLOW: + qCDebug(gpugllogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to underflow."; + break; + case GL_STACK_OVERFLOW: + qCDebug(gpugllogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to overflow."; + break; } + return true; } } +bool checkGLErrorDebug(const char* name) { +#ifdef DEBUG + return checkGLError(name); +#else + Q_UNUSED(name); + return false; +#endif +} + +gpu::Size getDedicatedMemory() { + static Size dedicatedMemory { 0 }; + static std::once_flag once; + std::call_once(once, [&] { +#ifdef Q_OS_WIN + if (!dedicatedMemory && wglGetGPUIDsAMD && wglGetGPUInfoAMD) { + UINT maxCount = wglGetGPUIDsAMD(0, 0); + std::vector ids; + ids.resize(maxCount); + wglGetGPUIDsAMD(maxCount, &ids[0]); + GLuint memTotal; + wglGetGPUInfoAMD(ids[0], WGL_GPU_RAM_AMD, GL_UNSIGNED_INT, sizeof(GLuint), &memTotal); + dedicatedMemory = MB_TO_BYTES(memTotal); + } +#endif + + if (!dedicatedMemory) { + GLint atiGpuMemory[4]; + // not really total memory, but close enough if called early enough in the application lifecycle + glGetIntegerv(GL_TEXTURE_FREE_MEMORY_ATI, atiGpuMemory); + if (GL_NO_ERROR == glGetError()) { + dedicatedMemory = KB_TO_BYTES(atiGpuMemory[0]); + } + } + + if (!dedicatedMemory) { + GLint nvGpuMemory { 0 }; + glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &nvGpuMemory); + if (GL_NO_ERROR == glGetError()) { + dedicatedMemory = KB_TO_BYTES(nvGpuMemory); + } + } + + if (!dedicatedMemory) { + auto gpuIdent = GPUIdent::getInstance(); + if (gpuIdent && gpuIdent->isValid()) { + dedicatedMemory = MB_TO_BYTES(gpuIdent->getMemory()); + } + } + }); + + return dedicatedMemory; +} + + + + +ComparisonFunction comparisonFuncFromGL(GLenum func) { + if (func == GL_NEVER) { + return NEVER; + } else if (func == GL_LESS) { + return LESS; + } else if (func == GL_EQUAL) { + return EQUAL; + } else if (func == GL_LEQUAL) { + return LESS_EQUAL; + } else if (func == GL_GREATER) { + return GREATER; + } else if (func == GL_NOTEQUAL) { + return NOT_EQUAL; + } else if (func == GL_GEQUAL) { + return GREATER_EQUAL; + } else if (func == GL_ALWAYS) { + return ALWAYS; + } + + return ALWAYS; +} + +State::StencilOp stencilOpFromGL(GLenum stencilOp) { + if (stencilOp == GL_KEEP) { + return State::STENCIL_OP_KEEP; + } else if (stencilOp == GL_ZERO) { + return State::STENCIL_OP_ZERO; + } else if (stencilOp == GL_REPLACE) { + return State::STENCIL_OP_REPLACE; + } else if (stencilOp == GL_INCR_WRAP) { + return State::STENCIL_OP_INCR_SAT; + } else if (stencilOp == GL_DECR_WRAP) { + return State::STENCIL_OP_DECR_SAT; + } else if (stencilOp == GL_INVERT) { + return State::STENCIL_OP_INVERT; + } else if (stencilOp == GL_INCR) { + return State::STENCIL_OP_INCR; + } else if (stencilOp == GL_DECR) { + return State::STENCIL_OP_DECR; + } + + return State::STENCIL_OP_KEEP; +} + +State::BlendOp blendOpFromGL(GLenum blendOp) { + if (blendOp == GL_FUNC_ADD) { + return State::BLEND_OP_ADD; + } else if (blendOp == GL_FUNC_SUBTRACT) { + return State::BLEND_OP_SUBTRACT; + } else if (blendOp == GL_FUNC_REVERSE_SUBTRACT) { + return State::BLEND_OP_REV_SUBTRACT; + } else if (blendOp == GL_MIN) { + return State::BLEND_OP_MIN; + } else if (blendOp == GL_MAX) { + return State::BLEND_OP_MAX; + } + + return State::BLEND_OP_ADD; +} + +State::BlendArg blendArgFromGL(GLenum blendArg) { + if (blendArg == GL_ZERO) { + return State::ZERO; + } else if (blendArg == GL_ONE) { + return State::ONE; + } else if (blendArg == GL_SRC_COLOR) { + return State::SRC_COLOR; + } else if (blendArg == GL_ONE_MINUS_SRC_COLOR) { + return State::INV_SRC_COLOR; + } else if (blendArg == GL_DST_COLOR) { + return State::DEST_COLOR; + } else if (blendArg == GL_ONE_MINUS_DST_COLOR) { + return State::INV_DEST_COLOR; + } else if (blendArg == GL_SRC_ALPHA) { + return State::SRC_ALPHA; + } else if (blendArg == GL_ONE_MINUS_SRC_ALPHA) { + return State::INV_SRC_ALPHA; + } else if (blendArg == GL_DST_ALPHA) { + return State::DEST_ALPHA; + } else if (blendArg == GL_ONE_MINUS_DST_ALPHA) { + return State::INV_DEST_ALPHA; + } else if (blendArg == GL_CONSTANT_COLOR) { + return State::FACTOR_COLOR; + } else if (blendArg == GL_ONE_MINUS_CONSTANT_COLOR) { + return State::INV_FACTOR_COLOR; + } else if (blendArg == GL_CONSTANT_ALPHA) { + return State::FACTOR_ALPHA; + } else if (blendArg == GL_ONE_MINUS_CONSTANT_ALPHA) { + return State::INV_FACTOR_ALPHA; + } + + return State::ONE; +} + +void getCurrentGLState(State::Data& state) { + { + GLint modes[2]; + glGetIntegerv(GL_POLYGON_MODE, modes); + if (modes[0] == GL_FILL) { + state.fillMode = State::FILL_FACE; + } else { + if (modes[0] == GL_LINE) { + state.fillMode = State::FILL_LINE; + } else { + state.fillMode = State::FILL_POINT; + } + } + } + { + if (glIsEnabled(GL_CULL_FACE)) { + GLint mode; + glGetIntegerv(GL_CULL_FACE_MODE, &mode); + state.cullMode = (mode == GL_FRONT ? State::CULL_FRONT : State::CULL_BACK); + } else { + state.cullMode = State::CULL_NONE; + } + } + { + GLint winding; + glGetIntegerv(GL_FRONT_FACE, &winding); + state.frontFaceClockwise = (winding == GL_CW); + state.depthClampEnable = glIsEnabled(GL_DEPTH_CLAMP); + state.scissorEnable = glIsEnabled(GL_SCISSOR_TEST); + state.multisampleEnable = glIsEnabled(GL_MULTISAMPLE); + state.antialisedLineEnable = glIsEnabled(GL_LINE_SMOOTH); + } + { + if (glIsEnabled(GL_POLYGON_OFFSET_FILL)) { + glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &state.depthBiasSlopeScale); + glGetFloatv(GL_POLYGON_OFFSET_UNITS, &state.depthBias); + } + } + { + GLboolean isEnabled = glIsEnabled(GL_DEPTH_TEST); + GLboolean writeMask; + glGetBooleanv(GL_DEPTH_WRITEMASK, &writeMask); + GLint func; + glGetIntegerv(GL_DEPTH_FUNC, &func); + + state.depthTest = State::DepthTest(isEnabled, writeMask, comparisonFuncFromGL(func)); + } + { + GLboolean isEnabled = glIsEnabled(GL_STENCIL_TEST); + + GLint frontWriteMask; + GLint frontReadMask; + GLint frontRef; + GLint frontFail; + GLint frontDepthFail; + GLint frontPass; + GLint frontFunc; + glGetIntegerv(GL_STENCIL_WRITEMASK, &frontWriteMask); + glGetIntegerv(GL_STENCIL_VALUE_MASK, &frontReadMask); + glGetIntegerv(GL_STENCIL_REF, &frontRef); + glGetIntegerv(GL_STENCIL_FAIL, &frontFail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &frontDepthFail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &frontPass); + glGetIntegerv(GL_STENCIL_FUNC, &frontFunc); + + GLint backWriteMask; + GLint backReadMask; + GLint backRef; + GLint backFail; + GLint backDepthFail; + GLint backPass; + GLint backFunc; + glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &backWriteMask); + glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &backReadMask); + glGetIntegerv(GL_STENCIL_BACK_REF, &backRef); + glGetIntegerv(GL_STENCIL_BACK_FAIL, &backFail); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &backDepthFail); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &backPass); + glGetIntegerv(GL_STENCIL_BACK_FUNC, &backFunc); + + state.stencilActivation = State::StencilActivation(isEnabled, frontWriteMask, backWriteMask); + state.stencilTestFront = State::StencilTest(frontRef, frontReadMask, comparisonFuncFromGL(frontFunc), stencilOpFromGL(frontFail), stencilOpFromGL(frontDepthFail), stencilOpFromGL(frontPass)); + state.stencilTestBack = State::StencilTest(backRef, backReadMask, comparisonFuncFromGL(backFunc), stencilOpFromGL(backFail), stencilOpFromGL(backDepthFail), stencilOpFromGL(backPass)); + } + { + GLint mask = 0xFFFFFFFF; + if (glIsEnabled(GL_SAMPLE_MASK)) { + glGetIntegerv(GL_SAMPLE_MASK, &mask); + state.sampleMask = mask; + } + state.sampleMask = mask; + } + { + state.alphaToCoverageEnable = glIsEnabled(GL_SAMPLE_ALPHA_TO_COVERAGE); + } + { + GLboolean isEnabled = glIsEnabled(GL_BLEND); + GLint srcRGB; + GLint srcA; + GLint dstRGB; + GLint dstA; + glGetIntegerv(GL_BLEND_SRC_RGB, &srcRGB); + glGetIntegerv(GL_BLEND_SRC_ALPHA, &srcA); + glGetIntegerv(GL_BLEND_DST_RGB, &dstRGB); + glGetIntegerv(GL_BLEND_DST_ALPHA, &dstA); + + GLint opRGB; + GLint opA; + glGetIntegerv(GL_BLEND_EQUATION_RGB, &opRGB); + glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &opA); + + state.blendFunction = State::BlendFunction(isEnabled, + blendArgFromGL(srcRGB), blendOpFromGL(opRGB), blendArgFromGL(dstRGB), + blendArgFromGL(srcA), blendOpFromGL(opA), blendArgFromGL(dstA)); + } + { + GLboolean mask[4]; + glGetBooleanv(GL_COLOR_WRITEMASK, mask); + state.colorWriteMask = (mask[0] ? State::WRITE_RED : 0) + | (mask[1] ? State::WRITE_GREEN : 0) + | (mask[2] ? State::WRITE_BLUE : 0) + | (mask[3] ? State::WRITE_ALPHA : 0); + } + + (void)CHECK_GL_ERROR(); +} + + +class ElementResource { +public: + gpu::Element _element; + uint16 _resource; + + ElementResource(Element&& elem, uint16 resource) : _element(elem), _resource(resource) {} +}; + +ElementResource getFormatFromGLUniform(GLenum gltype) { + switch (gltype) { + case GL_FLOAT: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + /* + case GL_DOUBLE: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + */ + case GL_INT: return ElementResource(Element(SCALAR, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC2: return ElementResource(Element(VEC2, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC3: return ElementResource(Element(VEC3, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC4: return ElementResource(Element(VEC4, gpu::INT32, UNIFORM), Resource::BUFFER); + + case GL_UNSIGNED_INT: return ElementResource(Element(SCALAR, gpu::UINT32, UNIFORM), Resource::BUFFER); +#if defined(Q_OS_WIN) + case GL_UNSIGNED_INT_VEC2: return ElementResource(Element(VEC2, gpu::UINT32, UNIFORM), Resource::BUFFER); + case GL_UNSIGNED_INT_VEC3: return ElementResource(Element(VEC3, gpu::UINT32, UNIFORM), Resource::BUFFER); + case GL_UNSIGNED_INT_VEC4: return ElementResource(Element(VEC4, gpu::UINT32, UNIFORM), Resource::BUFFER); +#endif + + case GL_BOOL: return ElementResource(Element(SCALAR, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC2: return ElementResource(Element(VEC2, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC3: return ElementResource(Element(VEC3, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC4: return ElementResource(Element(VEC4, gpu::BOOL, UNIFORM), Resource::BUFFER); + + + case GL_FLOAT_MAT2: return ElementResource(Element(gpu::MAT2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_MAT3: return ElementResource(Element(MAT3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_MAT4: return ElementResource(Element(MAT4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + + /* {GL_FLOAT_MAT2x3 mat2x3}, + {GL_FLOAT_MAT2x4 mat2x4}, + {GL_FLOAT_MAT3x2 mat3x2}, + {GL_FLOAT_MAT3x4 mat3x4}, + {GL_FLOAT_MAT4x2 mat4x2}, + {GL_FLOAT_MAT4x3 mat4x3}, + {GL_DOUBLE_MAT2 dmat2}, + {GL_DOUBLE_MAT3 dmat3}, + {GL_DOUBLE_MAT4 dmat4}, + {GL_DOUBLE_MAT2x3 dmat2x3}, + {GL_DOUBLE_MAT2x4 dmat2x4}, + {GL_DOUBLE_MAT3x2 dmat3x2}, + {GL_DOUBLE_MAT3x4 dmat3x4}, + {GL_DOUBLE_MAT4x2 dmat4x2}, + {GL_DOUBLE_MAT4x3 dmat4x3}, + */ + + case GL_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D); + case GL_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D); + + case GL_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_3D); + case GL_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_CUBE); + +#if defined(Q_OS_WIN) + case GL_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); +#endif + + case GL_SAMPLER_2D_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D); +#if defined(Q_OS_WIN) + case GL_SAMPLER_CUBE_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_CUBE); + + case GL_SAMPLER_2D_ARRAY_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D_ARRAY); +#endif + + // {GL_SAMPLER_1D_SHADOW sampler1DShadow}, + // {GL_SAMPLER_1D_ARRAY_SHADOW sampler1DArrayShadow}, + + // {GL_SAMPLER_BUFFER samplerBuffer}, + // {GL_SAMPLER_2D_RECT sampler2DRect}, + // {GL_SAMPLER_2D_RECT_SHADOW sampler2DRectShadow}, + +#if defined(Q_OS_WIN) + case GL_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D); + case GL_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D); + case GL_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_3D); + case GL_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_CUBE); + + case GL_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); + + // {GL_INT_SAMPLER_BUFFER isamplerBuffer}, + // {GL_INT_SAMPLER_2D_RECT isampler2DRect}, + + case GL_UNSIGNED_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D); + case GL_UNSIGNED_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D); + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_UNSIGNED_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_3D); + case GL_UNSIGNED_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_CUBE); + + case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); +#endif + // {GL_UNSIGNED_INT_SAMPLER_BUFFER usamplerBuffer}, + // {GL_UNSIGNED_INT_SAMPLER_2D_RECT usampler2DRect}, + /* + {GL_IMAGE_1D image1D}, + {GL_IMAGE_2D image2D}, + {GL_IMAGE_3D image3D}, + {GL_IMAGE_2D_RECT image2DRect}, + {GL_IMAGE_CUBE imageCube}, + {GL_IMAGE_BUFFER imageBuffer}, + {GL_IMAGE_1D_ARRAY image1DArray}, + {GL_IMAGE_2D_ARRAY image2DArray}, + {GL_IMAGE_2D_MULTISAMPLE image2DMS}, + {GL_IMAGE_2D_MULTISAMPLE_ARRAY image2DMSArray}, + {GL_INT_IMAGE_1D iimage1D}, + {GL_INT_IMAGE_2D iimage2D}, + {GL_INT_IMAGE_3D iimage3D}, + {GL_INT_IMAGE_2D_RECT iimage2DRect}, + {GL_INT_IMAGE_CUBE iimageCube}, + {GL_INT_IMAGE_BUFFER iimageBuffer}, + {GL_INT_IMAGE_1D_ARRAY iimage1DArray}, + {GL_INT_IMAGE_2D_ARRAY iimage2DArray}, + {GL_INT_IMAGE_2D_MULTISAMPLE iimage2DMS}, + {GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY iimage2DMSArray}, + {GL_UNSIGNED_INT_IMAGE_1D uimage1D}, + {GL_UNSIGNED_INT_IMAGE_2D uimage2D}, + {GL_UNSIGNED_INT_IMAGE_3D uimage3D}, + {GL_UNSIGNED_INT_IMAGE_2D_RECT uimage2DRect}, + {GL_UNSIGNED_INT_IMAGE_CUBE uimageCube},+ [0] {_name="fInnerRadius" _location=0 _element={_semantic=15 '\xf' _dimension=0 '\0' _type=0 '\0' } } gpu::Shader::Slot + + {GL_UNSIGNED_INT_IMAGE_BUFFER uimageBuffer}, + {GL_UNSIGNED_INT_IMAGE_1D_ARRAY uimage1DArray}, + {GL_UNSIGNED_INT_IMAGE_2D_ARRAY uimage2DArray}, + {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE uimage2DMS}, + {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY uimage2DMSArray}, + {GL_UNSIGNED_INT_ATOMIC_COUNTER atomic_uint} + */ + default: + return ElementResource(Element(), Resource::BUFFER); + } + +}; + +int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers) { + GLint uniformsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); + + for (int i = 0; i < uniformsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + GLint location = glGetUniformLocation(glprogram, name); + const GLint INVALID_UNIFORM_LOCATION = -1; + + // Try to make sense of the gltype + auto elementResource = getFormatFromGLUniform(type); + + // The uniform as a standard var type + if (location != INVALID_UNIFORM_LOCATION) { + // Let's make sure the name doesn't contains an array element + std::string sname(name); + auto foundBracket = sname.find_first_of('['); + if (foundBracket != std::string::npos) { + // std::string arrayname = sname.substr(0, foundBracket); + + if (sname[foundBracket + 1] == '0') { + sname = sname.substr(0, foundBracket); + } else { + // skip this uniform since it's not the first element of an array + continue; + } + } + + if (elementResource._resource == Resource::BUFFER) { + uniforms.insert(Shader::Slot(sname, location, elementResource._element, elementResource._resource)); + } else { + // For texture/Sampler, the location is the actual binding value + GLint binding = -1; + glGetUniformiv(glprogram, location, &binding); + + auto requestedBinding = slotBindings.find(std::string(sname)); + if (requestedBinding != slotBindings.end()) { + if (binding != (*requestedBinding)._location) { + binding = (*requestedBinding)._location; + glProgramUniform1i(glprogram, location, binding); + } + } + + textures.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); + samplers.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); + } + } + } + + return uniformsCount; +} + +const GLint UNUSED_SLOT = -1; +bool isUnusedSlot(GLint binding) { + return (binding == UNUSED_SLOT); +} + +int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers) { + GLint buffersCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORM_BLOCKS, &buffersCount); + + // fast exit + if (buffersCount == 0) { + return 0; + } + + GLint maxNumUniformBufferSlots = 0; + glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxNumUniformBufferSlots); + std::vector uniformBufferSlotMap(maxNumUniformBufferSlots, -1); + + for (int i = 0; i < buffersCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLint binding = -1; + + glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); + glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, name); + glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_BINDING, &binding); + glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); + + GLuint blockIndex = glGetUniformBlockIndex(glprogram, name); + + // CHeck if there is a requested binding for this block + auto requestedBinding = slotBindings.find(std::string(name)); + if (requestedBinding != slotBindings.end()) { + // If yes force it + if (binding != (*requestedBinding)._location) { + binding = (*requestedBinding)._location; + glUniformBlockBinding(glprogram, blockIndex, binding); + } + } else if (binding == 0) { + // If no binding was assigned then just do it finding a free slot + auto slotIt = std::find_if(uniformBufferSlotMap.begin(), uniformBufferSlotMap.end(), isUnusedSlot); + if (slotIt != uniformBufferSlotMap.end()) { + binding = slotIt - uniformBufferSlotMap.begin(); + glUniformBlockBinding(glprogram, blockIndex, binding); + } else { + // This should neve happen, an active ubo cannot find an available slot among the max available?! + binding = -1; + } + } + // If binding is valid record it + if (binding >= 0) { + uniformBufferSlotMap[binding] = blockIndex; + } + + Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); + buffers.insert(Shader::Slot(name, binding, element, Resource::BUFFER)); + } + return buffersCount; +} + +int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs) { + GLint inputsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_ATTRIBUTES, &inputsCount); + + for (int i = 0; i < inputsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + + GLint binding = glGetAttribLocation(glprogram, name); + + auto elementResource = getFormatFromGLUniform(type); + inputs.insert(Shader::Slot(name, binding, elementResource._element, -1)); + } + + return inputsCount; +} + +int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs) { + /* GLint outputsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_, &outputsCount); + + for (int i = 0; i < inputsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + + auto element = getFormatFromGLUniform(type); + outputs.insert(Shader::Slot(name, i, element)); + } + */ + return 0; //inputsCount; +} + + bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject) { if (shaderSource.empty()) { qCDebug(gpugllogging) << "GLShader::compileShader - no GLSL shader source code ? so failed to create"; @@ -206,7 +820,7 @@ GLuint compileProgram(const std::vector& glshaders) { } -void makeProgramBindings(GLBackend::GLShader::ShaderObject& shaderObject) { +void makeProgramBindings(ShaderObject& shaderObject) { if (!shaderObject.glprogram) { return; } @@ -294,475 +908,9 @@ void makeProgramBindings(GLBackend::GLShader::ShaderObject& shaderObject) { (void)CHECK_GL_ERROR(); } -GLBackend::GLShader* compileBackendShader(const Shader& shader) { - // Any GLSLprogram ? normally yes... - const std::string& shaderSource = shader.getSource().getCode(); - // GLSL version - const std::string glslVersion = { - "#version 410 core" - }; +} } - // Shader domain - const int NUM_SHADER_DOMAINS = 2; - const GLenum SHADER_DOMAINS[NUM_SHADER_DOMAINS] = { - GL_VERTEX_SHADER, - GL_FRAGMENT_SHADER - }; - GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; +using namespace gpu; - // Domain specific defines - const std::string domainDefines[NUM_SHADER_DOMAINS] = { - "#define GPU_VERTEX_SHADER", - "#define GPU_PIXEL_SHADER" - }; - - // Versions specific of the shader - const std::string versionDefines[GLBackend::GLShader::NumVersions] = { - "" - }; - - GLBackend::GLShader::ShaderObjects shaderObjects; - - for (int version = 0; version < GLBackend::GLShader::NumVersions; version++) { - auto& shaderObject = shaderObjects[version]; - - std::string shaderDefines = glslVersion + "\n" + domainDefines[shader.getType()] + "\n" + versionDefines[version]; - - bool result = compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram); - if (!result) { - return nullptr; - } - } - - // So far so good, the shader is created successfully - GLBackend::GLShader* object = new GLBackend::GLShader(); - object->_shaderObjects = shaderObjects; - - return object; -} - -GLBackend::GLShader* compileBackendProgram(const Shader& program) { - if (!program.isProgram()) { - return nullptr; - } - - GLBackend::GLShader::ShaderObjects programObjects; - - for (int version = 0; version < GLBackend::GLShader::NumVersions; version++) { - auto& programObject = programObjects[version]; - - // Let's go through every shaders and make sure they are ready to go - std::vector< GLuint > shaderGLObjects; - for (auto subShader : program.getShaders()) { - auto object = GLBackend::syncGPUObject(*subShader); - if (object) { - shaderGLObjects.push_back(object->_shaderObjects[version].glshader); - } else { - qCDebug(gpugllogging) << "GLShader::compileBackendProgram - One of the shaders of the program is not compiled?"; - return nullptr; - } - } - - GLuint glprogram = compileProgram(shaderGLObjects); - if (glprogram == 0) { - return nullptr; - } - - programObject.glprogram = glprogram; - - makeProgramBindings(programObject); - } - - // So far so good, the program versions have all been created successfully - GLBackend::GLShader* object = new GLBackend::GLShader(); - object->_shaderObjects = programObjects; - - return object; -} - -GLBackend::GLShader* GLBackend::syncGPUObject(const Shader& shader) { - GLShader* object = Backend::getGPUObject(shader); - - // If GPU object already created then good - if (object) { - return object; - } - // need to have a gpu object? - if (shader.isProgram()) { - GLShader* tempObject = compileBackendProgram(shader); - if (tempObject) { - object = tempObject; - Backend::setGPUObject(shader, object); - } - } else if (shader.isDomain()) { - GLShader* tempObject = compileBackendShader(shader); - if (tempObject) { - object = tempObject; - Backend::setGPUObject(shader, object); - } - } - - return object; -} - -class ElementResource { -public: - gpu::Element _element; - uint16 _resource; - - ElementResource(Element&& elem, uint16 resource) : _element(elem), _resource(resource) {} -}; - -ElementResource getFormatFromGLUniform(GLenum gltype) { - switch (gltype) { - case GL_FLOAT: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); -/* - case GL_DOUBLE: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_DOUBLE_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_DOUBLE_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_DOUBLE_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); -*/ - case GL_INT: return ElementResource(Element(SCALAR, gpu::INT32, UNIFORM), Resource::BUFFER); - case GL_INT_VEC2: return ElementResource(Element(VEC2, gpu::INT32, UNIFORM), Resource::BUFFER); - case GL_INT_VEC3: return ElementResource(Element(VEC3, gpu::INT32, UNIFORM), Resource::BUFFER); - case GL_INT_VEC4: return ElementResource(Element(VEC4, gpu::INT32, UNIFORM), Resource::BUFFER); - - case GL_UNSIGNED_INT: return ElementResource(Element(SCALAR, gpu::UINT32, UNIFORM), Resource::BUFFER); -#if defined(Q_OS_WIN) - case GL_UNSIGNED_INT_VEC2: return ElementResource(Element(VEC2, gpu::UINT32, UNIFORM), Resource::BUFFER); - case GL_UNSIGNED_INT_VEC3: return ElementResource(Element(VEC3, gpu::UINT32, UNIFORM), Resource::BUFFER); - case GL_UNSIGNED_INT_VEC4: return ElementResource(Element(VEC4, gpu::UINT32, UNIFORM), Resource::BUFFER); -#endif - - case GL_BOOL: return ElementResource(Element(SCALAR, gpu::BOOL, UNIFORM), Resource::BUFFER); - case GL_BOOL_VEC2: return ElementResource(Element(VEC2, gpu::BOOL, UNIFORM), Resource::BUFFER); - case GL_BOOL_VEC3: return ElementResource(Element(VEC3, gpu::BOOL, UNIFORM), Resource::BUFFER); - case GL_BOOL_VEC4: return ElementResource(Element(VEC4, gpu::BOOL, UNIFORM), Resource::BUFFER); - - - case GL_FLOAT_MAT2: return ElementResource(Element(gpu::MAT2, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_MAT3: return ElementResource(Element(MAT3, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_MAT4: return ElementResource(Element(MAT4, gpu::FLOAT, UNIFORM), Resource::BUFFER); - -/* {GL_FLOAT_MAT2x3 mat2x3}, - {GL_FLOAT_MAT2x4 mat2x4}, - {GL_FLOAT_MAT3x2 mat3x2}, - {GL_FLOAT_MAT3x4 mat3x4}, - {GL_FLOAT_MAT4x2 mat4x2}, - {GL_FLOAT_MAT4x3 mat4x3}, - {GL_DOUBLE_MAT2 dmat2}, - {GL_DOUBLE_MAT3 dmat3}, - {GL_DOUBLE_MAT4 dmat4}, - {GL_DOUBLE_MAT2x3 dmat2x3}, - {GL_DOUBLE_MAT2x4 dmat2x4}, - {GL_DOUBLE_MAT3x2 dmat3x2}, - {GL_DOUBLE_MAT3x4 dmat3x4}, - {GL_DOUBLE_MAT4x2 dmat4x2}, - {GL_DOUBLE_MAT4x3 dmat4x3}, - */ - - case GL_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D); - case GL_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D); - - case GL_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_3D); - case GL_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_CUBE); - -#if defined(Q_OS_WIN) - case GL_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); - case GL_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D_ARRAY); - case GL_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D_ARRAY); - case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); -#endif - - case GL_SAMPLER_2D_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D); -#if defined(Q_OS_WIN) - case GL_SAMPLER_CUBE_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_CUBE); - - case GL_SAMPLER_2D_ARRAY_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D_ARRAY); -#endif - -// {GL_SAMPLER_1D_SHADOW sampler1DShadow}, - // {GL_SAMPLER_1D_ARRAY_SHADOW sampler1DArrayShadow}, - -// {GL_SAMPLER_BUFFER samplerBuffer}, -// {GL_SAMPLER_2D_RECT sampler2DRect}, - // {GL_SAMPLER_2D_RECT_SHADOW sampler2DRectShadow}, - -#if defined(Q_OS_WIN) - case GL_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D); - case GL_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D); - case GL_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); - case GL_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_3D); - case GL_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_CUBE); - - case GL_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); - case GL_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); - case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); - - // {GL_INT_SAMPLER_BUFFER isamplerBuffer}, - // {GL_INT_SAMPLER_2D_RECT isampler2DRect}, - - case GL_UNSIGNED_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D); - case GL_UNSIGNED_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D); - case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); - case GL_UNSIGNED_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_3D); - case GL_UNSIGNED_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_CUBE); - - case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); - case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); - case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); -#endif -// {GL_UNSIGNED_INT_SAMPLER_BUFFER usamplerBuffer}, -// {GL_UNSIGNED_INT_SAMPLER_2D_RECT usampler2DRect}, -/* - {GL_IMAGE_1D image1D}, - {GL_IMAGE_2D image2D}, - {GL_IMAGE_3D image3D}, - {GL_IMAGE_2D_RECT image2DRect}, - {GL_IMAGE_CUBE imageCube}, - {GL_IMAGE_BUFFER imageBuffer}, - {GL_IMAGE_1D_ARRAY image1DArray}, - {GL_IMAGE_2D_ARRAY image2DArray}, - {GL_IMAGE_2D_MULTISAMPLE image2DMS}, - {GL_IMAGE_2D_MULTISAMPLE_ARRAY image2DMSArray}, - {GL_INT_IMAGE_1D iimage1D}, - {GL_INT_IMAGE_2D iimage2D}, - {GL_INT_IMAGE_3D iimage3D}, - {GL_INT_IMAGE_2D_RECT iimage2DRect}, - {GL_INT_IMAGE_CUBE iimageCube}, - {GL_INT_IMAGE_BUFFER iimageBuffer}, - {GL_INT_IMAGE_1D_ARRAY iimage1DArray}, - {GL_INT_IMAGE_2D_ARRAY iimage2DArray}, - {GL_INT_IMAGE_2D_MULTISAMPLE iimage2DMS}, - {GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY iimage2DMSArray}, - {GL_UNSIGNED_INT_IMAGE_1D uimage1D}, - {GL_UNSIGNED_INT_IMAGE_2D uimage2D}, - {GL_UNSIGNED_INT_IMAGE_3D uimage3D}, - {GL_UNSIGNED_INT_IMAGE_2D_RECT uimage2DRect}, - {GL_UNSIGNED_INT_IMAGE_CUBE uimageCube},+ [0] {_name="fInnerRadius" _location=0 _element={_semantic=15 '\xf' _dimension=0 '\0' _type=0 '\0' } } gpu::Shader::Slot - - {GL_UNSIGNED_INT_IMAGE_BUFFER uimageBuffer}, - {GL_UNSIGNED_INT_IMAGE_1D_ARRAY uimage1DArray}, - {GL_UNSIGNED_INT_IMAGE_2D_ARRAY uimage2DArray}, - {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE uimage2DMS}, - {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY uimage2DMSArray}, - {GL_UNSIGNED_INT_ATOMIC_COUNTER atomic_uint} -*/ - default: - return ElementResource(Element(), Resource::BUFFER); - } - -}; - - -int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, - Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers) { - GLint uniformsCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); - - for (int i = 0; i < uniformsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - GLint location = glGetUniformLocation(glprogram, name); - const GLint INVALID_UNIFORM_LOCATION = -1; - - // Try to make sense of the gltype - auto elementResource = getFormatFromGLUniform(type); - - // The uniform as a standard var type - if (location != INVALID_UNIFORM_LOCATION) { - // Let's make sure the name doesn't contains an array element - std::string sname(name); - auto foundBracket = sname.find_first_of('['); - if (foundBracket != std::string::npos) { - // std::string arrayname = sname.substr(0, foundBracket); - - if (sname[foundBracket + 1] == '0') { - sname = sname.substr(0, foundBracket); - } else { - // skip this uniform since it's not the first element of an array - continue; - } - } - - if (elementResource._resource == Resource::BUFFER) { - uniforms.insert(Shader::Slot(sname, location, elementResource._element, elementResource._resource)); - } else { - // For texture/Sampler, the location is the actual binding value - GLint binding = -1; - glGetUniformiv(glprogram, location, &binding); - - auto requestedBinding = slotBindings.find(std::string(sname)); - if (requestedBinding != slotBindings.end()) { - if (binding != (*requestedBinding)._location) { - binding = (*requestedBinding)._location; - glProgramUniform1i(glprogram, location, binding); - } - } - - textures.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); - samplers.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); - } - } - } - - return uniformsCount; -} - -const GLint UNUSED_SLOT = -1; -bool isUnusedSlot(GLint binding) { - return (binding == UNUSED_SLOT); -} - -int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers) { - GLint buffersCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_UNIFORM_BLOCKS, &buffersCount); - - // fast exit - if (buffersCount == 0) { - return 0; - } - - GLint maxNumUniformBufferSlots = 0; - glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxNumUniformBufferSlots); - std::vector uniformBufferSlotMap(maxNumUniformBufferSlots, -1); - - for (int i = 0; i < buffersCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLint binding = -1; - - glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); - glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, name); - glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_BINDING, &binding); - glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); - - GLuint blockIndex = glGetUniformBlockIndex(glprogram, name); - - // CHeck if there is a requested binding for this block - auto requestedBinding = slotBindings.find(std::string(name)); - if (requestedBinding != slotBindings.end()) { - // If yes force it - if (binding != (*requestedBinding)._location) { - binding = (*requestedBinding)._location; - glUniformBlockBinding(glprogram, blockIndex, binding); - } - } else if (binding == 0) { - // If no binding was assigned then just do it finding a free slot - auto slotIt = std::find_if(uniformBufferSlotMap.begin(), uniformBufferSlotMap.end(), isUnusedSlot); - if (slotIt != uniformBufferSlotMap.end()) { - binding = slotIt - uniformBufferSlotMap.begin(); - glUniformBlockBinding(glprogram, blockIndex, binding); - } else { - // This should neve happen, an active ubo cannot find an available slot among the max available?! - binding = -1; - } - } - // If binding is valid record it - if (binding >= 0) { - uniformBufferSlotMap[binding] = blockIndex; - } - - Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); - buffers.insert(Shader::Slot(name, binding, element, Resource::BUFFER)); - } - return buffersCount; -} - -int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs) { - GLint inputsCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_ATTRIBUTES, &inputsCount); - - for (int i = 0; i < inputsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - - GLint binding = glGetAttribLocation(glprogram, name); - - auto elementResource = getFormatFromGLUniform(type); - inputs.insert(Shader::Slot(name, binding, elementResource._element, -1)); - } - - return inputsCount; -} - -int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs) { - /* GLint outputsCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_, &outputsCount); - - for (int i = 0; i < inputsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - - auto element = getFormatFromGLUniform(type); - outputs.insert(Shader::Slot(name, i, element)); - } - */ - return 0; //inputsCount; -} - -bool GLBackend::makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { - - // First make sure the Shader has been compiled - GLShader* object = GLBackend::syncGPUObject(shader); - if (!object) { - return false; - } - - // Apply bindings to all program versions and generate list of slots from default version - for (int version = 0; version < GLBackend::GLShader::NumVersions; version++) { - auto& shaderObject = object->_shaderObjects[version]; - if (shaderObject.glprogram) { - Shader::SlotSet buffers; - makeUniformBlockSlots(shaderObject.glprogram, slotBindings, buffers); - - Shader::SlotSet uniforms; - Shader::SlotSet textures; - Shader::SlotSet samplers; - makeUniformSlots(shaderObject.glprogram, slotBindings, uniforms, textures, samplers); - - Shader::SlotSet inputs; - makeInputSlots(shaderObject.glprogram, slotBindings, inputs); - - Shader::SlotSet outputs; - makeOutputSlots(shaderObject.glprogram, slotBindings, outputs); - - // Define the public slots only from the default version - if (version == 0) { - shader.defineSlots(uniforms, buffers, textures, samplers, inputs, outputs); - } else { - GLShader::UniformMapping mapping; - for (auto srcUniform : shader.getUniforms()) { - mapping[srcUniform._location] = uniforms.findLocation(srcUniform._name); - } - object->_uniformMappings.push_back(mapping); - } - } - } - - - return true; -} diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.h b/libraries/gpu-gl/src/gpu/gl/GLShared.h new file mode 100644 index 0000000000..5d90badc1c --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.h @@ -0,0 +1,158 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_GLShared_h +#define hifi_gpu_GLShared_h + +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(gpugllogging) + +namespace gpu { namespace gl { + +gpu::Size getDedicatedMemory(); +ComparisonFunction comparisonFuncFromGL(GLenum func); +State::StencilOp stencilOpFromGL(GLenum stencilOp); +State::BlendOp blendOpFromGL(GLenum blendOp); +State::BlendArg blendArgFromGL(GLenum blendArg); +void getCurrentGLState(State::Data& state); + +struct ShaderObject { + GLuint glshader { 0 }; + GLuint glprogram { 0 }; + GLint transformCameraSlot { -1 }; + GLint transformObjectSlot { -1 }; +}; + +int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers); +int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers); +int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs); +int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs); +bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject); +GLuint compileProgram(const std::vector& glshaders); +void makeProgramBindings(ShaderObject& shaderObject); + +enum GLSyncState { + // The object is currently undergoing no processing, although it's content + // may be out of date, or it's storage may be invalid relative to the + // owning GPU object + Idle, + // The object has been queued for transfer to the GPU + Pending, + // The object has been transferred to the GPU, but is awaiting + // any post transfer operations that may need to occur on the + // primary rendering thread + Transferred, +}; + +static const GLenum BLEND_OPS_TO_GL[State::NUM_BLEND_OPS] = { + GL_FUNC_ADD, + GL_FUNC_SUBTRACT, + GL_FUNC_REVERSE_SUBTRACT, + GL_MIN, + GL_MAX +}; + +static const GLenum BLEND_ARGS_TO_GL[State::NUM_BLEND_ARGS] = { + GL_ZERO, + GL_ONE, + GL_SRC_COLOR, + GL_ONE_MINUS_SRC_COLOR, + GL_SRC_ALPHA, + GL_ONE_MINUS_SRC_ALPHA, + GL_DST_ALPHA, + GL_ONE_MINUS_DST_ALPHA, + GL_DST_COLOR, + GL_ONE_MINUS_DST_COLOR, + GL_SRC_ALPHA_SATURATE, + GL_CONSTANT_COLOR, + GL_ONE_MINUS_CONSTANT_COLOR, + GL_CONSTANT_ALPHA, + GL_ONE_MINUS_CONSTANT_ALPHA, +}; + +static const GLenum COMPARISON_TO_GL[gpu::NUM_COMPARISON_FUNCS] = { + GL_NEVER, + GL_LESS, + GL_EQUAL, + GL_LEQUAL, + GL_GREATER, + GL_NOTEQUAL, + GL_GEQUAL, + GL_ALWAYS +}; + +static const GLenum PRIMITIVE_TO_GL[gpu::NUM_PRIMITIVES] = { + GL_POINTS, + GL_LINES, + GL_LINE_STRIP, + GL_TRIANGLES, + GL_TRIANGLE_STRIP, + GL_TRIANGLE_FAN, +}; + +static const GLenum ELEMENT_TYPE_TO_GL[gpu::NUM_TYPES] = { + GL_FLOAT, + GL_INT, + GL_UNSIGNED_INT, + GL_HALF_FLOAT, + GL_SHORT, + GL_UNSIGNED_SHORT, + GL_BYTE, + GL_UNSIGNED_BYTE, + // Normalized values + GL_INT, + GL_UNSIGNED_INT, + GL_SHORT, + GL_UNSIGNED_SHORT, + GL_BYTE, + GL_UNSIGNED_BYTE +}; + +bool checkGLError(const char* name = nullptr); +bool checkGLErrorDebug(const char* name = nullptr); + +template +struct GLObject : public GPUObject { +public: + GLObject(const GPUType& gpuObject, GLuint id) : _gpuObject(gpuObject), _id(id) {} + + virtual ~GLObject() { } + + // Used by derived classes and helpers to ensure the actual GL object exceeds the lifetime of `this` + GLuint takeOwnership() { + GLuint result = _id; + const_cast(_id) = 0; + return result; + } + + const GPUType& _gpuObject; + const GLuint _id; +}; + +class GlBuffer; +class GLFramebuffer; +class GLPipeline; +class GLQuery; +class GLState; +class GLShader; +class GLTexture; +class GLTextureTransferHelper; + +} } // namespace gpu::gl + +#define CHECK_GL_ERROR() gpu::gl::checkGLErrorDebug(__FUNCTION__) + +#endif + + + diff --git a/libraries/gpu-gl/src/gpu/gl/GLState.cpp b/libraries/gpu-gl/src/gpu/gl/GLState.cpp new file mode 100644 index 0000000000..8cb2efa7b4 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLState.cpp @@ -0,0 +1,233 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLState.h" +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +typedef GLState::Command Command; +typedef GLState::CommandPointer CommandPointer; +typedef GLState::Command1 Command1U; +typedef GLState::Command1 Command1I; +typedef GLState::Command1 Command1B; +typedef GLState::Command1 CommandDepthBias; +typedef GLState::Command1 CommandDepthTest; +typedef GLState::Command3 CommandStencil; +typedef GLState::Command1 CommandBlend; + +const GLState::Commands makeResetStateCommands(); + +// NOTE: This must stay in sync with the ordering of the State::Field enum +const GLState::Commands makeResetStateCommands() { + // Since State::DEFAULT is a static defined in another .cpp the initialisation order is random + // and we have a 50/50 chance that State::DEFAULT is not yet initialized. + // Since State::DEFAULT = State::Data() it is much easier to not use the actual State::DEFAULT + // but another State::Data object with a default initialization. + const State::Data DEFAULT = State::Data(); + + auto depthBiasCommand = std::make_shared(&GLBackend::do_setStateDepthBias, + Vec2(DEFAULT.depthBias, DEFAULT.depthBiasSlopeScale)); + auto stencilCommand = std::make_shared(&GLBackend::do_setStateStencil, DEFAULT.stencilActivation, + DEFAULT.stencilTestFront, DEFAULT.stencilTestBack); + + // The state commands to reset to default, + // WARNING depending on the order of the State::Field enum + return { + std::make_shared(&GLBackend::do_setStateFillMode, DEFAULT.fillMode), + std::make_shared(&GLBackend::do_setStateCullMode, DEFAULT.cullMode), + std::make_shared(&GLBackend::do_setStateFrontFaceClockwise, DEFAULT.frontFaceClockwise), + std::make_shared(&GLBackend::do_setStateDepthClampEnable, DEFAULT.depthClampEnable), + std::make_shared(&GLBackend::do_setStateScissorEnable, DEFAULT.scissorEnable), + std::make_shared(&GLBackend::do_setStateMultisampleEnable, DEFAULT.multisampleEnable), + std::make_shared(&GLBackend::do_setStateAntialiasedLineEnable, DEFAULT.antialisedLineEnable), + + // Depth bias has 2 fields in State but really one call in GLBackend + CommandPointer(depthBiasCommand), + CommandPointer(depthBiasCommand), + + std::make_shared(&GLBackend::do_setStateDepthTest, DEFAULT.depthTest), + + // Depth bias has 3 fields in State but really one call in GLBackend + CommandPointer(stencilCommand), + CommandPointer(stencilCommand), + CommandPointer(stencilCommand), + + std::make_shared(&GLBackend::do_setStateSampleMask, DEFAULT.sampleMask), + + std::make_shared(&GLBackend::do_setStateAlphaToCoverageEnable, DEFAULT.alphaToCoverageEnable), + + std::make_shared(&GLBackend::do_setStateBlend, DEFAULT.blendFunction), + + std::make_shared(&GLBackend::do_setStateColorWriteMask, DEFAULT.colorWriteMask) + }; +} + +const GLState::Commands GLState::_resetStateCommands = makeResetStateCommands(); + + +void generateFillMode(GLState::Commands& commands, State::FillMode fillMode) { + commands.push_back(std::make_shared(&GLBackend::do_setStateFillMode, int32(fillMode))); +} + +void generateCullMode(GLState::Commands& commands, State::CullMode cullMode) { + commands.push_back(std::make_shared(&GLBackend::do_setStateCullMode, int32(cullMode))); +} + +void generateFrontFaceClockwise(GLState::Commands& commands, bool isClockwise) { + commands.push_back(std::make_shared(&GLBackend::do_setStateFrontFaceClockwise, isClockwise)); +} + +void generateDepthClampEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateDepthClampEnable, enable)); +} + +void generateScissorEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateScissorEnable, enable)); +} + +void generateMultisampleEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateMultisampleEnable, enable)); +} + +void generateAntialiasedLineEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateAntialiasedLineEnable, enable)); +} + +void generateDepthBias(GLState::Commands& commands, const State& state) { + commands.push_back(std::make_shared(&GLBackend::do_setStateDepthBias, Vec2(state.getDepthBias(), state.getDepthBiasSlopeScale()))); +} + +void generateDepthTest(GLState::Commands& commands, const State::DepthTest& test) { + commands.push_back(std::make_shared(&GLBackend::do_setStateDepthTest, int32(test.getRaw()))); +} + +void generateStencil(GLState::Commands& commands, const State& state) { + commands.push_back(std::make_shared(&GLBackend::do_setStateStencil, state.getStencilActivation(), state.getStencilTestFront(), state.getStencilTestBack())); +} + +void generateAlphaToCoverageEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateAlphaToCoverageEnable, enable)); +} + +void generateSampleMask(GLState::Commands& commands, uint32 mask) { + commands.push_back(std::make_shared(&GLBackend::do_setStateSampleMask, mask)); +} + +void generateBlend(GLState::Commands& commands, const State& state) { + commands.push_back(std::make_shared(&GLBackend::do_setStateBlend, state.getBlendFunction())); +} + +void generateColorWriteMask(GLState::Commands& commands, uint32 mask) { + commands.push_back(std::make_shared(&GLBackend::do_setStateColorWriteMask, mask)); +} + +GLState* GLState::sync(const State& state) { + GLState* object = Backend::getGPUObject(state); + + // If GPU object already created then good + if (object) { + return object; + } + + // Else allocate and create the GLState + if (!object) { + object = new GLState(); + Backend::setGPUObject(state, object); + } + + // here, we need to regenerate something so let's do it all + object->_commands.clear(); + object->_stamp = state.getStamp(); + object->_signature = state.getSignature(); + + bool depthBias = false; + bool stencilState = false; + + // go thorugh the list of state fields in the State and record the corresponding gl command + for (int i = 0; i < State::NUM_FIELDS; i++) { + if (state.getSignature()[i]) { + switch (i) { + case State::FILL_MODE: { + generateFillMode(object->_commands, state.getFillMode()); + break; + } + case State::CULL_MODE: { + generateCullMode(object->_commands, state.getCullMode()); + break; + } + case State::DEPTH_BIAS: + case State::DEPTH_BIAS_SLOPE_SCALE: { + depthBias = true; + break; + } + case State::FRONT_FACE_CLOCKWISE: { + generateFrontFaceClockwise(object->_commands, state.isFrontFaceClockwise()); + break; + } + case State::DEPTH_CLAMP_ENABLE: { + generateDepthClampEnable(object->_commands, state.isDepthClampEnable()); + break; + } + case State::SCISSOR_ENABLE: { + generateScissorEnable(object->_commands, state.isScissorEnable()); + break; + } + case State::MULTISAMPLE_ENABLE: { + generateMultisampleEnable(object->_commands, state.isMultisampleEnable()); + break; + } + case State::ANTIALISED_LINE_ENABLE: { + generateAntialiasedLineEnable(object->_commands, state.isAntialiasedLineEnable()); + break; + } + case State::DEPTH_TEST: { + generateDepthTest(object->_commands, state.getDepthTest()); + break; + } + + case State::STENCIL_ACTIVATION: + case State::STENCIL_TEST_FRONT: + case State::STENCIL_TEST_BACK: { + stencilState = true; + break; + } + + case State::SAMPLE_MASK: { + generateSampleMask(object->_commands, state.getSampleMask()); + break; + } + case State::ALPHA_TO_COVERAGE_ENABLE: { + generateAlphaToCoverageEnable(object->_commands, state.isAlphaToCoverageEnabled()); + break; + } + + case State::BLEND_FUNCTION: { + generateBlend(object->_commands, state); + break; + } + + case State::COLOR_WRITE_MASK: { + generateColorWriteMask(object->_commands, state.getColorWriteMask()); + break; + } + } + } + } + + if (depthBias) { + generateDepthBias(object->_commands, state); + } + + if (stencilState) { + generateStencil(object->_commands, state); + } + + return object; +} + diff --git a/libraries/gpu-gl/src/gpu/gl/GLState.h b/libraries/gpu-gl/src/gpu/gl/GLState.h new file mode 100644 index 0000000000..82635db893 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLState.h @@ -0,0 +1,73 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLState_h +#define hifi_gpu_gl_GLState_h + +#include "GLShared.h" + +#include + +namespace gpu { namespace gl { + +class GLBackend; +class GLState : public GPUObject { +public: + static GLState* sync(const State& state); + + class Command { + public: + virtual void run(GLBackend* backend) = 0; + Command() {} + virtual ~Command() {}; + }; + + template class Command1 : public Command { + public: + typedef void (GLBackend::*GLFunction)(T); + void run(GLBackend* backend) { (backend->*(_func))(_param); } + Command1(GLFunction func, T param) : _func(func), _param(param) {}; + GLFunction _func; + T _param; + }; + template class Command2 : public Command { + public: + typedef void (GLBackend::*GLFunction)(T, U); + void run(GLBackend* backend) { (backend->*(_func))(_param0, _param1); } + Command2(GLFunction func, T param0, U param1) : _func(func), _param0(param0), _param1(param1) {}; + GLFunction _func; + T _param0; + U _param1; + }; + + template class Command3 : public Command { + public: + typedef void (GLBackend::*GLFunction)(T, U, V); + void run(GLBackend* backend) { (backend->*(_func))(_param0, _param1, _param2); } + Command3(GLFunction func, T param0, U param1, V param2) : _func(func), _param0(param0), _param1(param1), _param2(param2) {}; + GLFunction _func; + T _param0; + U _param1; + V _param2; + }; + + typedef std::shared_ptr< Command > CommandPointer; + typedef std::vector< CommandPointer > Commands; + + Commands _commands; + Stamp _stamp; + State::Signature _signature; + + // The state commands to reset to default, + static const Commands _resetStateCommands; + + friend class GLBackend; +}; + +} } + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp similarity index 82% rename from libraries/gpu-gl/src/gpu/gl/GLBackendShared.cpp rename to libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index deb48be1ec..4bff5c87bd 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -1,59 +1,15 @@ // -// Created by Bradley Austin Davis on 2016/05/14 +// Created by Bradley Austin Davis on 2016/05/15 // Copyright 2013-2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackendShared.h" -#include +#include "GLTexelFormat.h" -Q_DECLARE_LOGGING_CATEGORY(gpugllogging) -Q_LOGGING_CATEGORY(gpugllogging, "hifi.gpu.gl") - -namespace gpu { namespace gl { - -bool checkGLError(const char* name) { - GLenum error = glGetError(); - if (!error) { - return false; - } else { - switch (error) { - case GL_INVALID_ENUM: - qCDebug(gpugllogging) << "GLBackend::" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; - break; - case GL_INVALID_VALUE: - qCDebug(gpugllogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; - break; - case GL_INVALID_OPERATION: - qCDebug(gpugllogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; - break; - case GL_INVALID_FRAMEBUFFER_OPERATION: - qCDebug(gpugllogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; - break; - case GL_OUT_OF_MEMORY: - qCDebug(gpugllogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; - break; - case GL_STACK_UNDERFLOW: - qCDebug(gpugllogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to underflow."; - break; - case GL_STACK_OVERFLOW: - qCDebug(gpugllogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to overflow."; - break; - } - return true; - } -} - -bool checkGLErrorDebug(const char* name) { -#ifdef DEBUG - return checkGLError(name); -#else - Q_UNUSED(name); - return false; -#endif -} +using namespace gpu; +using namespace gpu::gl; GLTexelFormat GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { @@ -68,7 +24,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E switch (dstFormat.getDimension()) { case gpu::SCALAR: { texel.format = GL_RED; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { case gpu::RGB: @@ -96,7 +52,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::VEC2: { texel.format = GL_RG; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { case gpu::RGB: @@ -113,7 +69,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::VEC3: { texel.format = GL_RGB; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { case gpu::RGB: @@ -135,7 +91,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::VEC4: { texel.format = GL_RGBA; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (srcFormat.getSemantic()) { case gpu::BGRA: @@ -205,7 +161,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E switch (dstFormat.getDimension()) { case gpu::SCALAR: { texel.format = GL_RED; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { case gpu::COMPRESSED_R: { @@ -340,7 +296,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::VEC2: { texel.format = GL_RG; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { case gpu::RGB: @@ -357,7 +313,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::VEC3: { texel.format = GL_RGB; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { case gpu::RGB: @@ -382,7 +338,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::VEC4: { texel.format = GL_RGBA; - texel.type = _elementTypeToGLType[dstFormat.getType()]; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { case gpu::RGB: @@ -468,5 +424,3 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E return texel; } } - -} } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.h b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.h new file mode 100644 index 0000000000..bc3ec55066 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.h @@ -0,0 +1,32 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLTexelFormat_h +#define hifi_gpu_gl_GLTexelFormat_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLTexelFormat { +public: + GLenum internalFormat; + GLenum format; + GLenum type; + + static GLTexelFormat evalGLTexelFormat(const Element& dstFormat) { + return evalGLTexelFormat(dstFormat, dstFormat); + } + static GLTexelFormat evalGLTexelFormatInternal(const Element& dstFormat); + + static GLTexelFormat evalGLTexelFormat(const Element& dstFormat, const Element& srcFormat); +}; + +} } + + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp new file mode 100644 index 0000000000..47fc583d77 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -0,0 +1,292 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLTexture.h" + +#include + +#include "GLTextureTransfer.h" + +using namespace gpu; +using namespace gpu::gl; + +std::shared_ptr GLTexture::_textureTransferHelper; +static std::map _textureCountByMips; +static uint16 _currentMaxMipCount { 0 }; + +// FIXME placeholder for texture memory over-use +#define DEFAULT_MAX_MEMORY_MB 256 + +const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { + GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, + GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, + GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z +}; + +const GLenum GLTexture::WRAP_MODES[Sampler::NUM_WRAP_MODES] = { + GL_REPEAT, // WRAP_REPEAT, + GL_MIRRORED_REPEAT, // WRAP_MIRROR, + GL_CLAMP_TO_EDGE, // WRAP_CLAMP, + GL_CLAMP_TO_BORDER, // WRAP_BORDER, + GL_MIRROR_CLAMP_TO_EDGE_EXT // WRAP_MIRROR_ONCE, +}; + +const GLFilterMode GLTexture::FILTER_MODES[Sampler::NUM_FILTERS] = { + { GL_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_POINT, + { GL_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR, + { GL_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT, + { GL_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR, + + { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_MIP_POINT, + { GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_MAG_POINT_MIP_LINEAR, + { GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT, + { GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR }, //FILTER_MIN_POINT_MAG_MIP_LINEAR, + { GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_MIP_POINT, + { GL_LINEAR_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR, + { GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR_MIP_POINT, + { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_MIP_LINEAR, + { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR } //FILTER_ANISOTROPIC, +}; + +GLenum GLTexture::getGLTextureType(const Texture& texture) { + switch (texture.getType()) { + case Texture::TEX_2D: + return GL_TEXTURE_2D; + break; + + case Texture::TEX_CUBE: + return GL_TEXTURE_CUBE_MAP; + break; + + default: + qFatal("Unsupported texture type"); + } + Q_UNREACHABLE(); + return GL_TEXTURE_2D; +} + + +const std::vector& GLTexture::getFaceTargets(GLenum target) { + static std::vector cubeFaceTargets { + GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, + GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, + GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z + }; + static std::vector faceTargets { + GL_TEXTURE_2D + }; + switch (target) { + case GL_TEXTURE_2D: + return faceTargets; + case GL_TEXTURE_CUBE_MAP: + return cubeFaceTargets; + default: + Q_UNREACHABLE(); + break; + } + Q_UNREACHABLE(); + return faceTargets; +} + +float GLTexture::getMemoryPressure() { + // Check for an explicit memory limit + auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); + + // If no memory limit has been set, use a percentage of the total dedicated memory + if (!availableTextureMemory) { + auto totalGpuMemory = gpu::gl::getDedicatedMemory(); + + // If no limit has been explicitly set, and the dedicated memory can't be determined, + // just use a fallback fixed value of 256 MB + if (!totalGpuMemory) { + totalGpuMemory = MB_TO_BYTES(DEFAULT_MAX_MEMORY_MB); + } + + // Allow 75% of all available GPU memory to be consumed by textures + // FIXME overly conservative? + availableTextureMemory = (totalGpuMemory >> 2) * 3; + } + + // Return the consumed texture memory divided by the available texture memory. + auto consumedGpuMemory = Context::getTextureGPUMemoryUsage(); + return (float)consumedGpuMemory / (float)availableTextureMemory; +} + +GLTexture::DownsampleSource::DownsampleSource(GLTexture* oldTexture) : + _texture(oldTexture ? oldTexture->takeOwnership() : 0), + _minMip(oldTexture ? oldTexture->_minMip : 0), + _maxMip(oldTexture ? oldTexture->_maxMip : 0) +{ +} + +GLTexture::DownsampleSource::~DownsampleSource() { + if (_texture) { + glDeleteTextures(1, &_texture); + Backend::decrementTextureGPUCount(); + } +} + +GLTexture::GLTexture(const gpu::Texture& texture, GLuint id, GLTexture* originalTexture, bool transferrable) : + GLObject(texture, id), + _storageStamp(texture.getStamp()), + _target(getGLTextureType(texture)), + _maxMip(texture.maxMip()), + _minMip(texture.minMip()), + _virtualSize(texture.evalTotalSize()), + _transferrable(transferrable), + _downsampleSource(originalTexture) +{ + if (_transferrable) { + uint16 mipCount = usedMipLevels(); + _currentMaxMipCount = std::max(_currentMaxMipCount, mipCount); + if (!_textureCountByMips.count(mipCount)) { + _textureCountByMips[mipCount] = 1; + } else { + ++_textureCountByMips[mipCount]; + } + } + Backend::incrementTextureGPUCount(); + Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); +} + + +// Create the texture and allocate storage +GLTexture::GLTexture(const Texture& texture, GLuint id, bool transferrable) : + GLTexture(texture, id, nullptr, transferrable) +{ + // FIXME, do during allocation + //Backend::updateTextureGPUMemoryUsage(0, _size); + Backend::setGPUObject(texture, this); +} + +// Create the texture and copy from the original higher resolution version +GLTexture::GLTexture(const gpu::Texture& texture, GLuint id, GLTexture* originalTexture) : + GLTexture(texture, id, originalTexture, originalTexture->_transferrable) +{ + Q_ASSERT(_minMip >= originalTexture->_minMip); + // Set the GPU object last because that implicitly destroys the originalTexture object + Backend::setGPUObject(texture, this); +} + +GLTexture::~GLTexture() { + if (_transferrable) { + uint16 mipCount = usedMipLevels(); + Q_ASSERT(_textureCountByMips.count(mipCount)); + auto& numTexturesForMipCount = _textureCountByMips[mipCount]; + --numTexturesForMipCount; + if (0 == numTexturesForMipCount) { + _textureCountByMips.erase(mipCount); + if (mipCount == _currentMaxMipCount) { + _currentMaxMipCount = _textureCountByMips.rbegin()->first; + } + } + } + + Backend::decrementTextureGPUCount(); + Backend::updateTextureGPUMemoryUsage(_size, 0); + Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); +} + +void GLTexture::createTexture() { + withPreservedTexture([&] { + allocateStorage(); + (void)CHECK_GL_ERROR(); + syncSampler(); + (void)CHECK_GL_ERROR(); + }); +} + +void GLTexture::setSize(GLuint size) const { + Backend::updateTextureGPUMemoryUsage(_size, size); + const_cast(_size) = size; +} + +bool GLTexture::isInvalid() const { + return _storageStamp < _gpuObject.getStamp(); +} + +bool GLTexture::isOutdated() const { + return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); +} + +bool GLTexture::isOverMaxMemory() const { + // FIXME switch to using the max mip count used from the previous frame + if (usedMipLevels() < _currentMaxMipCount) { + return false; + } + Q_ASSERT(usedMipLevels() == _currentMaxMipCount); + + if (getMemoryPressure() < 1.0f) { + return false; + } + + return true; +} + +bool GLTexture::isReady() const { + // If we have an invalid texture, we're never ready + if (isInvalid()) { + return false; + } + + // If we're out of date, but the transfer is in progress, report ready + // as a special case + auto syncState = _syncState.load(); + + if (isOutdated()) { + return Idle != syncState; + } + + if (Idle != syncState) { + return false; + } + + return true; +} + + +// Do any post-transfer operations that might be required on the main context / rendering thread +void GLTexture::postTransfer() { + setSyncState(GLSyncState::Idle); + ++_transferCount; + + //// The public gltexture becaomes available + //_id = _privateTexture; + + _downsampleSource.reset(); + + // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory + switch (_gpuObject.getType()) { + case Texture::TEX_2D: + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i)) { + _gpuObject.notifyMipFaceGPULoaded(i); + } + } + break; + + case Texture::TEX_CUBE: + // transfer pixels from each faces + for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + _gpuObject.notifyMipFaceGPULoaded(i, f); + } + } + } + break; + + default: + qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; + break; + } +} + +void GLTexture::initTextureTransferHelper() { + _textureTransferHelper = std::make_shared(); +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h new file mode 100644 index 0000000000..fa09fb49f2 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -0,0 +1,192 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLTexture_h +#define hifi_gpu_gl_GLTexture_h + +#include "GLShared.h" +#include "GLTextureTransfer.h" + +namespace gpu { namespace gl { + +struct GLFilterMode { + GLint minFilter; + GLint magFilter; +}; + +class GLTexture : public GLObject { +public: + static void initTextureTransferHelper(); + static std::shared_ptr _textureTransferHelper; + + template + static GLTextureType* sync(const TexturePointer& texturePointer, bool needTransfer) { + const Texture& texture = *texturePointer; + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + // If the object hasn't been created, or the object definition is out of date, drop and re-create + GLTextureType* object = Backend::getGPUObject(texture); + + // Create the texture if need be (force re-creation if the storage stamp changes + // for easier use of immutable storage) + if (!object || object->isInvalid()) { + // This automatically any previous texture + object = new GLTextureType(texture, needTransfer); + if (!object->_transferrable) { + object->createTexture(); + object->_contentStamp = texture.getDataStamp(); + object->postTransfer(); + } + } + + // Object maybe doens't neet to be tranasferred after creation + if (!object->_transferrable) { + return object; + } + + // If we just did a transfer, return the object after doing post-transfer work + if (GLSyncState::Transferred == object->getSyncState()) { + object->postTransfer(); + return object; + } + + if (object->isReady()) { + // Do we need to reduce texture memory usage? + if (object->isOverMaxMemory() && texturePointer->incremementMinMip()) { + // WARNING, this code path will essentially `delete this`, + // so no dereferencing of this instance should be done past this point + object = new GLTextureType(texture, object); + _textureTransferHelper->transferTexture(texturePointer); + } + } else if (object->isOutdated()) { + // Object might be outdated, if so, start the transfer + // (outdated objects that are already in transfer will have reported 'true' for ready() + _textureTransferHelper->transferTexture(texturePointer); + } + + return object; + } + + template + static GLuint getId(const TexturePointer& texture, bool shouldSync) { + if (!texture) { + return 0; + } + GLTextureType* object { nullptr }; + if (shouldSync) { + object = sync(texture, shouldSync); + } else { + object = Backend::getGPUObject(*texture); + } + if (!object) { + return 0; + } + + GLuint result = object->_id; + + // Don't return textures that are in transfer state + if ((object->getSyncState() != GLSyncState::Idle) || + // Don't return transferrable textures that have never completed transfer + (!object->_transferrable || 0 != object->_transferCount)) { + // Will be either 0 or the original texture being downsampled. + result = object->_downsampleSource._texture; + } + + return result; + } + + ~GLTexture(); + + const GLuint& _texture { _id }; + const Stamp _storageStamp; + const GLenum _target; + const uint16 _maxMip; + const uint16 _minMip; + const GLuint _virtualSize; // theoretical size as expected + Stamp _contentStamp { 0 }; + const bool _transferrable; + Size _transferCount { 0 }; + + struct DownsampleSource { + using Pointer = std::shared_ptr; + DownsampleSource() : _texture(0), _minMip(0), _maxMip(0) {} + DownsampleSource(GLTexture* originalTexture); + ~DownsampleSource(); + void reset() const { const_cast(_texture) = 0; } + const GLuint _texture { 0 }; + const uint16 _minMip { 0 }; + const uint16 _maxMip { 0 }; + } _downsampleSource; + + GLuint size() const { return _size; } + GLSyncState getSyncState() const { return _syncState; } + + // Is the storage out of date relative to the gpu texture? + bool isInvalid() const; + + // Is the content out of date relative to the gpu texture? + bool isOutdated() const; + + // Is the texture in a state where it can be rendered with no work? + bool isReady() const; + + // Execute any post-move operations that must occur only on the main thread + void postTransfer(); + + bool isOverMaxMemory() const; + +protected: + static const size_t CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[6]; + static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; + static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; + + static const std::vector& getFaceTargets(GLenum textureType); + + static GLenum getGLTextureType(const Texture& texture); + // Return a floating point value indicating how much of the allowed + // texture memory we are currently consuming. A value of 0 indicates + // no texture memory usage, while a value of 1 indicates all available / allowed memory + // is consumed. A value above 1 indicates that there is a problem. + static float getMemoryPressure(); + + + const GLuint _size { 0 }; // true size as reported by the gl api + std::atomic _syncState { GLSyncState::Idle }; + + GLTexture(const Texture& texture, GLuint id, bool transferrable); + GLTexture(const Texture& texture, GLuint id, GLTexture* originalTexture); + + void setSyncState(GLSyncState syncState) { _syncState = syncState; } + uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } + + void createTexture(); + + virtual void allocateStorage() const = 0; + virtual void updateSize() const = 0; + virtual void transfer() const = 0; + virtual void syncSampler() const = 0; + virtual void generateMips() const = 0; + virtual void withPreservedTexture(std::function f) const = 0; + +protected: + void setSize(GLuint size) const; + +private: + + GLTexture(const gpu::Texture& gpuTexture, GLuint id, GLTexture* originalTexture, bool transferrable); + + friend class GLTextureTransferHelper; + friend class GLBackend; +}; + +} } + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTextureTransfer.cpp b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp similarity index 84% rename from libraries/gpu-gl/src/gpu/gl/GLBackendTextureTransfer.cpp rename to libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp index a9635e8307..ca2e7061f5 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTextureTransfer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp @@ -5,21 +5,18 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackendTextureTransfer.h" +#include "GLTextureTransfer.h" #ifdef THREADED_TEXTURE_TRANSFER #include #include #endif - -#include "GLBackendShared.h" +#include "GLShared.h" using namespace gpu; using namespace gpu::gl; -#include "GLBackend.h" - GLTextureTransferHelper::GLTextureTransferHelper() { #ifdef THREADED_TEXTURE_TRANSFER _canvas = QSharedPointer(new OffscreenGLCanvas(), &QObject::deleteLater); @@ -45,7 +42,7 @@ GLTextureTransferHelper::~GLTextureTransferHelper() { } void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { - GLBackend::GLTexture* object = Backend::getGPUObject(*texturePointer); + GLTexture* object = Backend::getGPUObject(*texturePointer); Backend::incrementTextureGPUTransferCount(); #ifdef THREADED_TEXTURE_TRANSFER GLsync fence { 0 }; @@ -53,14 +50,14 @@ void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texture //glFlush(); TextureTransferPackage package { texturePointer, fence }; - object->setSyncState(GLBackend::GLTexture::Pending); + object->setSyncState(GLSyncState::Pending); queueItem(package); #else object->withPreservedTexture([&] { do_transfer(*object); }); object->_contentStamp = texturePointer->getDataStamp(); - object->setSyncState(GLBackend::GLTexture::Transferred); + object->setSyncState(GLSyncState::Transferred); #endif } @@ -78,7 +75,7 @@ void GLTextureTransferHelper::shutdown() { #endif } -void GLTextureTransferHelper::do_transfer(GLBackend::GLTexture& texture) { +void GLTextureTransferHelper::do_transfer(GLTexture& texture) { texture.createTexture(); texture.transfer(); texture.updateSize(); @@ -99,7 +96,7 @@ bool GLTextureTransferHelper::processQueueItems(const Queue& messages) { package.fence = 0; } - GLBackend::GLTexture* object = Backend::getGPUObject(*texturePointer); + GLTexture* object = Backend::getGPUObject(*texturePointer); do_transfer(*object); glBindTexture(object->_target, 0); @@ -108,7 +105,7 @@ bool GLTextureTransferHelper::processQueueItems(const Queue& messages) { glDeleteSync(writeSync); object->_contentStamp = texturePointer->getDataStamp(); - object->setSyncState(GLBackend::GLTexture::Transferred); + object->setSyncState(GLSyncState::Transferred); } return true; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTextureTransfer.h b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h similarity index 78% rename from libraries/gpu-gl/src/gpu/gl/GLBackendTextureTransfer.h rename to libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h index f344827e53..deb470c572 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTextureTransfer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h @@ -5,12 +5,16 @@ // 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_gpu_gl_GLTextureTransfer_h +#define hifi_gpu_gl_GLTextureTransfer_h #include -#include +#include + #include -#include "GLBackendShared.h" -#include "GLBackend.h" + +#include "GLShared.h" +#include "GLTexture.h" #ifdef Q_OS_WIN #define THREADED_TEXTURE_TRANSFER @@ -27,6 +31,7 @@ struct TextureTransferPackage { class GLTextureTransferHelper : public GenericQueueThread { public: + using Pointer = std::shared_ptr; GLTextureTransferHelper(); ~GLTextureTransferHelper(); void transferTexture(const gpu::TexturePointer& texturePointer); @@ -36,10 +41,12 @@ protected: void setup() override; void shutdown() override; bool processQueueItems(const Queue& messages) override; - void do_transfer(GLBackend::GLTexture& texturePointer); + void do_transfer(GLTexture& texturePointer); private: QSharedPointer _canvas; }; } } + +#endif \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp new file mode 100644 index 0000000000..e46c739593 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp @@ -0,0 +1,175 @@ +// +// Created by Sam Gateau on 10/27/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLBackend.h" + +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(gpugl41logging, "hifi.gpu.gl41") + +using namespace gpu; +using namespace gpu::gl41; + +void GLBackend::do_draw(Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 1]._uint; + uint32 startVertex = batch._params[paramOffset + 0]._uint; + + if (isStereo()) { + setupStereoSide(0); + glDrawArrays(mode, startVertex, numVertices); + setupStereoSide(1); + glDrawArrays(mode, startVertex, numVertices); + + _stats._DSNumTriangles += 2 * numVertices / 3; + _stats._DSNumDrawcalls += 2; + + } else { + glDrawArrays(mode, startVertex, numVertices); + _stats._DSNumTriangles += numVertices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GLBackend::do_drawIndexed(Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numIndices = batch._params[paramOffset + 1]._uint; + uint32 startIndex = batch._params[paramOffset + 0]._uint; + + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { + setupStereoSide(0); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + setupStereoSide(1); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + + _stats._DSNumTriangles += 2 * numIndices / 3; + _stats._DSNumDrawcalls += 2; + } else { + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + _stats._DSNumTriangles += numIndices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GLBackend::do_drawInstanced(Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 2]._uint; + uint32 startVertex = batch._params[paramOffset + 1]._uint; + + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + + setupStereoSide(0); + glDrawArraysInstancedARB(mode, startVertex, numVertices, numInstances); + setupStereoSide(1); + glDrawArraysInstancedARB(mode, startVertex, numVertices, numInstances); + + _stats._DSNumTriangles += (trueNumInstances * numVertices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + glDrawArraysInstancedARB(mode, startVertex, numVertices, numInstances); + _stats._DSNumTriangles += (numInstances * numVertices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void glbackend_glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices, GLsizei primcount, GLint basevertex, GLuint baseinstance) { +#if (GPU_INPUT_PROFILE == GPU_CORE_43) + glDrawElementsInstancedBaseVertexBaseInstance(mode, count, type, indices, primcount, basevertex, baseinstance); +#else + glDrawElementsInstanced(mode, count, type, indices, primcount); +#endif +} + +void GLBackend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 3]._uint]; + uint32 numIndices = batch._params[paramOffset + 2]._uint; + uint32 startIndex = batch._params[paramOffset + 1]._uint; + // FIXME glDrawElementsInstancedBaseVertexBaseInstance is only available in GL 4.3 + // and higher, so currently we ignore this field + uint32 startInstance = batch._params[paramOffset + 0]._uint; + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + + setupStereoSide(0); + glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + setupStereoSide(1); + glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + + _stats._DSNumTriangles += (trueNumInstances * numIndices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + _stats._DSNumTriangles += (numInstances * numIndices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + + _stats._DSNumAPIDrawcalls++; + + (void)CHECK_GL_ERROR(); +} + + +void GLBackend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { +#if (GPU_INPUT_PROFILE == GPU_CORE_43) + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + + glMultiDrawArraysIndirect(mode, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; + +#else + // FIXME implement the slow path +#endif + (void)CHECK_GL_ERROR(); + +} + +void GLBackend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { +#if (GPU_INPUT_PROFILE == GPU_CORE_43) + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + GLenum indexType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + glMultiDrawElementsIndirect(mode, indexType, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; +#else + // FIXME implement the slow path +#endif + (void)CHECK_GL_ERROR(); +} diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackend.h b/libraries/gpu-gl/src/gpu/gl41/GLBackend.h new file mode 100644 index 0000000000..8a0f4bb912 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackend.h @@ -0,0 +1,96 @@ +// +// GLBackend.h +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 10/27/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_41_GLBackend_h +#define hifi_gpu_41_GLBackend_h + +#include + +#include "../gl/GLBackend.h" +#include "../gl/GLTexture.h" + +#define GPU_CORE_41 410 +#define GPU_CORE_43 430 + +#ifdef Q_OS_MAC +#define GPU_INPUT_PROFILE GPU_CORE_41 +#else +#define GPU_INPUT_PROFILE GPU_CORE_43 +#endif + +namespace gpu { namespace gl41 { + +class GLBackend : public gl::GLBackend { + using Parent = gl::GLBackend; + // Context Backend static interface required + friend class Context; + +public: + explicit GLBackend(bool syncCache) : Parent(syncCache) {} + GLBackend() : Parent() {} + + class GLTexture : public gpu::gl::GLTexture { + using Parent = gpu::gl::GLTexture; + GLuint allocate(); + public: + GLTexture(const Texture& buffer, bool transferrable); + GLTexture(const Texture& buffer, GLTexture* original); + + protected: + void transferMip(uint16_t mipLevel, uint8_t face = 0) const; + void allocateStorage() const override; + void updateSize() const override; + void transfer() const override; + void syncSampler() const override; + void generateMips() const override; + void withPreservedTexture(std::function f) const override; + }; + + +protected: + GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; + gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; + + GLuint getBufferID(const Buffer& buffer) override; + gl::GLBuffer* syncGPUObject(const Buffer& buffer) override; + + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + + GLuint getQueryID(const QueryPointer& query) override; + gl::GLQuery* syncGPUObject(const Query& query) override; + + // Draw Stage + void do_draw(Batch& batch, size_t paramOffset) override; + void do_drawIndexed(Batch& batch, size_t paramOffset) override; + void do_drawInstanced(Batch& batch, size_t paramOffset) override; + void do_drawIndexedInstanced(Batch& batch, size_t paramOffset) override; + void do_multiDrawIndirect(Batch& batch, size_t paramOffset) override; + void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) override; + + // Input Stage + void updateInput() override; + + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void transferTransformState(const Batch& batch) const override; + void initTransform() override; + void updateTransform(const Batch& batch); + void resetTransformStage(); + + // Output stage + void do_blit(Batch& batch, size_t paramOffset) override; +}; + +} } + +Q_DECLARE_LOGGING_CATEGORY(gpugl41logging) + + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp new file mode 100644 index 0000000000..9c6b8b8124 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp @@ -0,0 +1,62 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLBackend.h" +#include "../gl/GLBuffer.h" + +using namespace gpu; +using namespace gpu::gl41; + +class GLBuffer : public gl::GLBuffer { + using Parent = gpu::gl::GLBuffer; + static GLuint allocate() { + GLuint result; + glGenBuffers(1, &result); + return result; + } + +public: + GLBuffer(const Buffer& buffer, GLBuffer* original) : Parent(buffer, allocate()) { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + if (original) { + glBindBuffer(GL_COPY_WRITE_BUFFER, _buffer); + glBindBuffer(GL_COPY_READ_BUFFER, original->_buffer); + glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, original->_size); + glBindBuffer(GL_COPY_WRITE_BUFFER, 0); + glBindBuffer(GL_COPY_READ_BUFFER, 0); + (void)CHECK_GL_ERROR(); + } + Backend::setGPUObject(buffer, this); + } + + void transfer() override { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + (void)CHECK_GL_ERROR(); + Size offset; + Size size; + Size currentPage { 0 }; + auto data = _gpuObject.getSysmem().readData(); + while (_gpuObject.getNextTransferBlock(offset, size, currentPage)) { + glBufferSubData(GL_ARRAY_BUFFER, offset, size, data + offset); + (void)CHECK_GL_ERROR(); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + (void)CHECK_GL_ERROR(); + _gpuObject._flags &= ~Buffer::DIRTY; + } +}; + +GLuint GLBackend::getBufferID(const Buffer& buffer) { + return GLBuffer::getId(buffer); +} + +gl::GLBuffer* GLBackend::syncGPUObject(const Buffer& buffer) { + return GLBuffer::sync(buffer); +} diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GLBackendInput.cpp new file mode 100644 index 0000000000..0486d1cfd2 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackendInput.cpp @@ -0,0 +1,185 @@ +// +// GLBackendInput.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl41; + +// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding +// Core 43 does :) +// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat +#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) +#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT +#else +#define SUPPORT_VERTEX_ATTRIB_FORMAT +#endif + +void GLBackend::updateInput() { +#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) + if (_input._invalidFormat) { + + InputStageState::ActivationCache newActivation; + + // Assign the vertex format required + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = _elementTypeToGLType[attrib._element.getType()]; + GLuint offset = attrib._offset;; + GLboolean isNormalized = attrib._element.isNormalized(); + + GLenum perLocationSize = attrib._element.getLocationSize(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + newActivation.set(slot + locNum); + glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); + glVertexAttribBinding(slot + locNum, attrib._channel); + } + glVertexBindingDivisor(attrib._channel, attrib._frequency); + } + (void) CHECK_GL_ERROR(); + } + + // Manage Activation what was and what is expected now + for (size_t i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + _input._attributeActivation.flip(i); + } + } + (void) CHECK_GL_ERROR(); + + _input._invalidFormat = false; + _stats._ISNumFormatChanges++; + } + + if (_input._invalidBuffers.any()) { + int numBuffers = _input._buffers.size(); + auto buffer = _input._buffers.data(); + auto vbo = _input._bufferVBOs.data(); + auto offset = _input._bufferOffsets.data(); + auto stride = _input._bufferStrides.data(); + + for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { + if (_input._invalidBuffers.test(bufferNum)) { + glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); + } + buffer++; + vbo++; + offset++; + stride++; + } + _input._invalidBuffers.reset(); + (void) CHECK_GL_ERROR(); + } +#else + if (_input._invalidFormat || _input._invalidBuffers.any()) { + + if (_input._invalidFormat) { + InputStageState::ActivationCache newActivation; + + _stats._ISNumFormatChanges++; + + // Check expected activation + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + uint8_t locationCount = attrib._element.getLocationCount(); + for (int i = 0; i < locationCount; ++i) { + newActivation.set(attrib._slot + i); + } + } + } + + // Manage Activation what was and what is expected now + for (unsigned int i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + (void) CHECK_GL_ERROR(); + + _input._attributeActivation.flip(i); + } + } + } + + // now we need to bind the buffers and assign the attrib pointers + if (_input._format) { + const Buffers& buffers = _input._buffers; + const Offsets& offsets = _input._bufferOffsets; + const Offsets& strides = _input._bufferStrides; + + const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); + auto& inputChannels = _input._format->getChannels(); + _stats._ISNumInputBufferChanges++; + + GLuint boundVBO = 0; + for (auto& channelIt : inputChannels) { + const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; + if ((channelIt).first < buffers.size()) { + int bufferNum = (channelIt).first; + + if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { + // GLuint vbo = gpu::GLBackend::getBufferID((*buffers[bufferNum])); + GLuint vbo = _input._bufferVBOs[bufferNum]; + if (boundVBO != vbo) { + glBindBuffer(GL_ARRAY_BUFFER, vbo); + (void) CHECK_GL_ERROR(); + boundVBO = vbo; + } + _input._invalidBuffers[bufferNum] = false; + + for (unsigned int i = 0; i < channel._slots.size(); i++) { + const Stream::Attribute& attrib = attributes.at(channel._slots[i]); + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; + // GLenum perLocationStride = strides[bufferNum]; + GLenum perLocationStride = attrib._element.getLocationSize(); + GLuint stride = (GLuint)strides[bufferNum]; + GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); + GLboolean isNormalized = attrib._element.isNormalized(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, + reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); + glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); + } + + // TODO: Support properly the IAttrib version + + (void) CHECK_GL_ERROR(); + } + } + } + } + } + // everything format related should be in sync now + _input._invalidFormat = false; + } +#endif +} + diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GLBackendOutput.cpp new file mode 100644 index 0000000000..a34a0aaebd --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackendOutput.cpp @@ -0,0 +1,169 @@ +// +// GLBackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLBackend.h" + +#include + +#include "../gl/GLFramebuffer.h" +#include "../gl/GLTexture.h" + +namespace gpu { namespace gl41 { + +class GLFramebuffer : public gl::GLFramebuffer { + using Parent = gl::GLFramebuffer; + static GLuint allocate() { + GLuint result; + glGenFramebuffers(1, &result); + return result; + } +public: + void update() override { + GLint currentFBO = -1; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); + glBindFramebuffer(GL_FRAMEBUFFER, _fbo); + gl::GLTexture* gltexture = nullptr; + TexturePointer surface; + if (_gpuObject.getColorStamps() != _colorStamps) { + if (_gpuObject.hasColor()) { + _colorBuffers.clear(); + static const GLenum colorAttachments[] = { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2, + GL_COLOR_ATTACHMENT3, + GL_COLOR_ATTACHMENT4, + GL_COLOR_ATTACHMENT5, + GL_COLOR_ATTACHMENT6, + GL_COLOR_ATTACHMENT7, + GL_COLOR_ATTACHMENT8, + GL_COLOR_ATTACHMENT9, + GL_COLOR_ATTACHMENT10, + GL_COLOR_ATTACHMENT11, + GL_COLOR_ATTACHMENT12, + GL_COLOR_ATTACHMENT13, + GL_COLOR_ATTACHMENT14, + GL_COLOR_ATTACHMENT15 }; + + int unit = 0; + for (auto& b : _gpuObject.getRenderBuffers()) { + surface = b._texture; + if (surface) { + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + } else { + gltexture = nullptr; + } + + if (gltexture) { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + _colorBuffers.push_back(colorAttachments[unit]); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0); + } + unit++; + } + } + _colorStamps = _gpuObject.getColorStamps(); + } + + GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; + if (!_gpuObject.hasStencil()) { + attachement = GL_DEPTH_ATTACHMENT; + } else if (!_gpuObject.hasDepth()) { + attachement = GL_STENCIL_ATTACHMENT; + } + + if (_gpuObject.getDepthStamp() != _depthStamp) { + auto surface = _gpuObject.getDepthStencilBuffer(); + if (_gpuObject.hasDepthStencil() && surface) { + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + } + + if (gltexture) { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0); + } + _depthStamp = _gpuObject.getDepthStamp(); + } + + + // Last but not least, define where we draw + if (!_colorBuffers.empty()) { + glDrawBuffers((GLsizei)_colorBuffers.size(), _colorBuffers.data()); + } else { + glDrawBuffer(GL_NONE); + } + + // Now check for completness + _status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + // restore the current framebuffer + if (currentFBO != -1) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); + } + + checkStatus(GL_DRAW_FRAMEBUFFER); + } + + +public: + GLFramebuffer(const gpu::Framebuffer& framebuffer) + : Parent(framebuffer, allocate()) { } +}; + +gl::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffer) { + return GLFramebuffer::sync(framebuffer); +} + +GLuint GLBackend::getFramebufferID(const FramebufferPointer& framebuffer) { + return framebuffer ? GLFramebuffer::getId(*framebuffer) : 0; +} + +void GLBackend::do_blit(Batch& batch, size_t paramOffset) { + auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); + Vec4i srcvp; + for (auto i = 0; i < 4; ++i) { + srcvp[i] = batch._params[paramOffset + 1 + i]._int; + } + + auto dstframebuffer = batch._framebuffers.get(batch._params[paramOffset + 5]._uint); + Vec4i dstvp; + for (auto i = 0; i < 4; ++i) { + dstvp[i] = batch._params[paramOffset + 6 + i]._int; + } + + // Assign dest framebuffer if not bound already + auto newDrawFBO = getFramebufferID(dstframebuffer); + if (_output._drawFBO != newDrawFBO) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, newDrawFBO); + } + + // always bind the read fbo + glBindFramebuffer(GL_READ_FRAMEBUFFER, getFramebufferID(srcframebuffer)); + + // Blit! + glBlitFramebuffer(srcvp.x, srcvp.y, srcvp.z, srcvp.w, + dstvp.x, dstvp.y, dstvp.z, dstvp.w, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + + // Always clean the read fbo to 0 + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + // Restore draw fbo if changed + if (_output._drawFBO != newDrawFBO) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _output._drawFBO); + } + + (void) CHECK_GL_ERROR(); +} + + +} } diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl41/GLBackendQuery.cpp new file mode 100644 index 0000000000..4915adbc1d --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackendQuery.cpp @@ -0,0 +1,37 @@ +// +// GLBackendQuery.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 7/7/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 "GLBackend.h" + +#include "../gl/GLQuery.h" + +using namespace gpu; +using namespace gpu::gl41; + +class GLQuery : public gpu::gl::GLQuery { + using Parent = gpu::gl::GLBuffer; +public: + static GLuint allocateQuery() { + GLuint result; + glGenQueries(1, &result); + return result; + } + + GLQuery(const Query& query) + : gl::GLQuery(query, allocateQuery()) { } +}; + +gl::GLQuery* GLBackend::syncGPUObject(const Query& query) { + return GLQuery::sync(query); +} + +GLuint GLBackend::getQueryID(const QueryPointer& query) { + return GLQuery::getId(query); +} diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GLBackendTexture.cpp new file mode 100644 index 0000000000..f5e8531ddc --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackendTexture.cpp @@ -0,0 +1,237 @@ +// +// GLBackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLBackend.h" + +#include +#include +#include + +#include "../gl/GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl41; + +using GLTexelFormat = gl::GLTexelFormat; +using GLTexture = GLBackend::GLTexture; + +GLuint GLTexture::allocate() { + Backend::incrementTextureGPUCount(); + GLuint result; + glGenTextures(1, &result); + return result; +} + +GLuint GLBackend::getTextureID(const TexturePointer& texture, bool transfer) { + return GLTexture::getId(texture, transfer); +} + +gl::GLTexture* GLBackend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GLTexture::sync(texture, transfer); +} + +GLTexture::GLTexture(const Texture& texture, bool transferrable) : gl::GLTexture(texture, allocate(), transferrable) {} + +GLTexture::GLTexture(const Texture& texture, GLTexture* original) : gl::GLTexture(texture, allocate(), original) {} + +void GLBackend::GLTexture::withPreservedTexture(std::function f) const { + GLint boundTex = -1; + switch (_target) { + case GL_TEXTURE_2D: + glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); + break; + + case GL_TEXTURE_CUBE_MAP: + glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); + break; + + default: + qFatal("Unsupported texture type"); + } + (void)CHECK_GL_ERROR(); + + glBindTexture(_target, _texture); + f(); + glBindTexture(_target, boundTex); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::GLTexture::generateMips() const { + withPreservedTexture([&] { + glGenerateMipmap(_target); + }); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::GLTexture::allocateStorage() const { + gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); + (void)CHECK_GL_ERROR(); + glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + (void)CHECK_GL_ERROR(); + if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); + glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); + } else { + for (uint16_t l = _minMip; l <= _maxMip; l++) { + // Get the mip level dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(l); + for (GLenum target : getFaceTargets(_target)) { + glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); + (void)CHECK_GL_ERROR(); + } + } + } +} + +void GLBackend::GLTexture::updateSize() const { + setSize(_virtualSize); + if (!_id) { + return; + } + + if (_gpuObject.getTexelFormat().isCompressed()) { + GLenum proxyType = GL_TEXTURE_2D; + GLuint numFaces = 1; + if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { + proxyType = CUBE_FACE_LAYOUT[0]; + numFaces = (GLuint)CUBE_NUM_FACES; + } + GLint gpuSize{ 0 }; + glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); + (void)CHECK_GL_ERROR(); + + if (gpuSize) { + for (GLuint level = _minMip; level < _maxMip; level++) { + GLint levelSize{ 0 }; + glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); + levelSize *= numFaces; + + if (levelSize <= 0) { + break; + } + gpuSize += levelSize; + } + (void)CHECK_GL_ERROR(); + setSize(gpuSize); + return; + } + } +} + +// Move content bits from the CPU to the GPU for a given mip / face +void GLBackend::GLTexture::transferMip(uint16_t mipLevel, uint8_t face) const { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + //GLenum target = getFaceTargets()[face]; + GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; + auto size = _gpuObject.evalMipDimensions(mipLevel); + glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + (void)CHECK_GL_ERROR(); +} + +// This should never happen on the main thread +// Move content bits from the CPU to the GPU +void GLBackend::GLTexture::transfer() const { + PROFILE_RANGE(__FUNCTION__); + //qDebug() << "Transferring texture: " << _privateTexture; + // Need to update the content of the GPU object from the source sysmem of the texture + if (_contentStamp >= _gpuObject.getDataStamp()) { + return; + } + + glBindTexture(_target, _id); + (void)CHECK_GL_ERROR(); + + if (_downsampleSource._texture) { + GLuint fbo { 0 }; + glGenFramebuffers(1, &fbo); + (void)CHECK_GL_ERROR(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + (void)CHECK_GL_ERROR(); + // Find the distance between the old min mip and the new one + uint16 mipOffset = _minMip - _downsampleSource._minMip; + for (uint16 i = _minMip; i <= _maxMip; ++i) { + uint16 targetMip = i - _minMip; + uint16 sourceMip = targetMip + mipOffset; + Vec3u dimensions = _gpuObject.evalMipDimensions(i); + for (GLenum target : getFaceTargets(_target)) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, _downsampleSource._texture, sourceMip); + (void)CHECK_GL_ERROR(); + glCopyTexSubImage2D(target, targetMip, 0, 0, 0, 0, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); + } + } + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + } else { + // GO through the process of allocating the correct storage and/or update the content + switch (_gpuObject.getType()) { + case Texture::TEX_2D: + { + for (uint16_t i = _minMip; i <= _maxMip; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i)) { + transferMip(i); + } + } + } + break; + + case Texture::TEX_CUBE: + // transfer pixels from each faces + for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + transferMip(i, f); + } + } + } + break; + + default: + qCWarning(gpugl41logging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; + break; + } + } + if (_gpuObject.isAutogenerateMips()) { + glGenerateMipmap(_target); + (void)CHECK_GL_ERROR(); + } +} + +void GLBackend::GLTexture::syncSampler() const { + const Sampler& sampler = _gpuObject.getSampler(); + Texture::Type type = _gpuObject.getType(); + auto object = this; + + const auto& fm = FILTER_MODES[sampler.getFilter()]; + glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); + glTexParameteri(_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); + + if (sampler.doComparison()) { + glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); + glTexParameteri(_target, GL_TEXTURE_COMPARE_FUNC, gl::COMPARISON_TO_GL[sampler.getComparisonFunction()]); + } else { + glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_NONE); + } + + glTexParameteri(_target, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); + glTexParameteri(_target, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); + glTexParameteri(_target, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + + glTexParameterfv(_target, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); + glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); +} + diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl41/GLBackendTransform.cpp new file mode 100644 index 0000000000..6601eef86e --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl41/GLBackendTransform.cpp @@ -0,0 +1,83 @@ +// +// GLBackendTransform.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl41; + +void GLBackend::initTransform() { + glGenBuffers(1, &_transform._objectBuffer); + glGenBuffers(1, &_transform._cameraBuffer); + glGenBuffers(1, &_transform._drawCallInfoBuffer); + glGenTextures(1, &_transform._objectBufferTexture); + size_t cameraSize = sizeof(TransformStageState::CameraBufferElement); + while (_transform._cameraUboSize < cameraSize) { + _transform._cameraUboSize += _uboAlignment; + } +} + +void GLBackend::transferTransformState(const Batch& batch) const { + // FIXME not thread safe + static std::vector bufferData; + if (!_transform._cameras.empty()) { + bufferData.resize(_transform._cameraUboSize * _transform._cameras.size()); + for (size_t i = 0; i < _transform._cameras.size(); ++i) { + memcpy(bufferData.data() + (_transform._cameraUboSize * i), &_transform._cameras[i], sizeof(TransformStageState::CameraBufferElement)); + } + glBindBuffer(GL_UNIFORM_BUFFER, _transform._cameraBuffer); + glBufferData(GL_UNIFORM_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_UNIFORM_BUFFER, 0); + } + + if (!batch._objects.empty()) { + auto byteSize = batch._objects.size() * sizeof(Batch::TransformObject); + bufferData.resize(byteSize); + memcpy(bufferData.data(), batch._objects.data(), byteSize); + +#ifdef GPU_SSBO_DRAW_CALL_INFO + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _transform._objectBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); +#else + glBindBuffer(GL_TEXTURE_BUFFER, _transform._objectBuffer); + glBufferData(GL_TEXTURE_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_TEXTURE_BUFFER, 0); +#endif + } + + if (!batch._namedData.empty()) { + bufferData.clear(); + for (auto& data : batch._namedData) { + auto currentSize = bufferData.size(); + auto bytesToCopy = data.second.drawCallInfos.size() * sizeof(Batch::DrawCallInfo); + bufferData.resize(currentSize + bytesToCopy); + memcpy(bufferData.data() + currentSize, data.second.drawCallInfos.data(), bytesToCopy); + _transform._drawCallInfoOffsets[data.first] = (GLvoid*)currentSize; + } + + glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); + glBufferData(GL_ARRAY_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + +#ifdef GPU_SSBO_DRAW_CALL_INFO + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _transform._objectBuffer); +#else + glActiveTexture(GL_TEXTURE0 + TRANSFORM_OBJECT_SLOT); + glBindTexture(GL_TEXTURE_BUFFER, _transform._objectBufferTexture); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, _transform._objectBuffer); +#endif + + CHECK_GL_ERROR(); + + // Make sure the current Camera offset is unknown before render Draw + _transform._currentCameraOffset = INVALID_OFFSET; +} diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index ae1e9b4427..ecddeb07ad 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -2,3 +2,5 @@ set(TARGET_NAME gpu) AUTOSCRIBE_SHADER_LIB(gpu) setup_hifi_library() link_hifi_libraries(shared) + +target_nsight() diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 18e923831f..9126f11acf 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -10,9 +10,10 @@ // #include "Batch.h" -#include #include +#include + #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" @@ -481,3 +482,106 @@ void Batch::popProfileRange() { ADD_COMMAND(popProfileRange); #endif } + +#define GL_TEXTURE0 0x84C0 + +void Batch::_glActiveBindTexture(uint32 unit, uint32 target, uint32 texture) { + // clean the cache on the texture unit we are going to use so the next call to setResourceTexture() at the same slot works fine + setResourceTexture(unit - GL_TEXTURE0, nullptr); + + ADD_COMMAND(glActiveBindTexture); + _params.push_back(texture); + _params.push_back(target); + _params.push_back(unit); +} + +void Batch::_glUniform1i(int32 location, int32 v0) { + if (location < 0) { + return; + } + ADD_COMMAND(glUniform1i); + _params.push_back(v0); + _params.push_back(location); +} + +void Batch::_glUniform1f(int32 location, float v0) { + if (location < 0) { + return; + } + ADD_COMMAND(glUniform1f); + _params.push_back(v0); + _params.push_back(location); +} + +void Batch::_glUniform2f(int32 location, float v0, float v1) { + ADD_COMMAND(glUniform2f); + + _params.push_back(v1); + _params.push_back(v0); + _params.push_back(location); +} + +void Batch::_glUniform3f(int32 location, float v0, float v1, float v2) { + ADD_COMMAND(glUniform3f); + + _params.push_back(v2); + _params.push_back(v1); + _params.push_back(v0); + _params.push_back(location); +} + +void Batch::_glUniform4f(int32 location, float v0, float v1, float v2, float v3) { + ADD_COMMAND(glUniform4f); + + _params.push_back(v3); + _params.push_back(v2); + _params.push_back(v1); + _params.push_back(v0); + _params.push_back(location); +} + +void Batch::_glUniform3fv(int32 location, int count, const float* value) { + ADD_COMMAND(glUniform3fv); + + const int VEC3_SIZE = 3 * sizeof(float); + _params.push_back(cacheData(count * VEC3_SIZE, value)); + _params.push_back(count); + _params.push_back(location); +} + +void Batch::_glUniform4fv(int32 location, int count, const float* value) { + ADD_COMMAND(glUniform4fv); + + const int VEC4_SIZE = 4 * sizeof(float); + _params.push_back(cacheData(count * VEC4_SIZE, value)); + _params.push_back(count); + _params.push_back(location); +} + +void Batch::_glUniform4iv(int32 location, int count, const int32* value) { + ADD_COMMAND(glUniform4iv); + + const int VEC4_SIZE = 4 * sizeof(int); + _params.push_back(cacheData(count * VEC4_SIZE, value)); + _params.push_back(count); + _params.push_back(location); +} + +void Batch::_glUniformMatrix4fv(int32 location, int count, uint8 transpose, const float* value) { + ADD_COMMAND(glUniformMatrix4fv); + + const int MATRIX4_SIZE = 16 * sizeof(float); + _params.push_back(cacheData(count * MATRIX4_SIZE, value)); + _params.push_back(transpose); + _params.push_back(count); + _params.push_back(location); +} + +void Batch::_glColor4f(float red, float green, float blue, float alpha) { + ADD_COMMAND(glColor4f); + + _params.push_back(alpha); + _params.push_back(blue); + _params.push_back(green); + _params.push_back(red); +} \ No newline at end of file diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index 621ccee2f9..83645f73e3 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -84,6 +84,13 @@ namespace gpu { namespace gl { class GLBuffer; + } + + namespace gl41 { + class GLBackend; + } + + namespace gl45 { class GLBackend; } } diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 21164955d6..10c83dfb0e 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -184,8 +184,34 @@ public: return append(sizeof(T) * t.size(), reinterpret_cast(&t[0])); } + bool getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage) const { + Size pageCount = _pages.size(); + // Advance to the first dirty page + while (currentPage < pageCount && (0 == (Buffer::DIRTY & _pages[currentPage]))) { + ++currentPage; + } + + // If we got to the end, we're done + if (currentPage >= pageCount) { + return false; + } + + // Advance to the next clean page + outOffset = static_cast(currentPage * _pageSize); + while (currentPage < pageCount && (0 != (Buffer::DIRTY & _pages[currentPage]))) { + _pages[currentPage] &= ~Buffer::DIRTY; + ++currentPage; + } + outSize = static_cast((currentPage * _pageSize) - outOffset); + return true; + } + const GPUObjectPointer gpuObject {}; + // Access the sysmem object, limited to ourselves and GPUObject derived classes + const Sysmem& getSysmem() const { return _sysmem; } + // FIXME find a better access mechanism for clearing this + mutable uint8_t _flags; protected: void markDirty(Size offset, Size bytes); @@ -194,21 +220,18 @@ protected: markDirty(sizeof(T) * index, sizeof(T) * count); } - // Access the sysmem object, limited to ourselves and GPUObject derived classes - const Sysmem& getSysmem() const { return _sysmem; } Sysmem& editSysmem() { return _sysmem; } Byte* editData() { return editSysmem().editData(); } Size getRequiredPageCount() const; Size _end { 0 }; - mutable uint8_t _flags; mutable PageFlags _pages; const Size _pageSize; Sysmem _sysmem; // FIXME find a more generic way to do this. - friend class gl::GLBackend; + friend class gl::GLBuffer; friend class BufferView; }; From d50681243c1c14f09b03ce1a7b650c525a7eeae3 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 18 May 2016 14:49:30 -0700 Subject: [PATCH 0095/1237] bricabrac, bench, no blue chair, umbrella --- .../kineticObjects/dressingRoomBricabrac.json | 43 +++++++++++++------ .../DomainContent/Home/reset.js | 4 +- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json index 215f2b3506..aa4c944427 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json @@ -1,5 +1,6 @@ { "Entities": [{ + "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, "compoundShapeURL": "atp:/kineticObjects/skully/skullyhull.obj", "created": "2016-05-16T19:34:28Z", @@ -37,6 +38,7 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, "compoundShapeURL": "atp:/dressingRoom/trump.obj", "created": "2016-05-18T00:02:46Z", @@ -74,6 +76,7 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, "compoundShapeURL": "atp:/dressingRoom/dreads.obj", "created": "2016-05-18T00:02:46Z", @@ -111,10 +114,11 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/tophat/tophat.obj", + "compoundShapeURL": "atp:/kineticObjects/tophat/tophat.obj", "created": "2016-05-16T16:59:22Z", "dimensions": { "x": 0.38495281338691711, @@ -128,7 +132,7 @@ "z": 0 }, "id": "{8cff801b-cb50-47d2-a1e8-4f7ee245d2ee}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/tophat/tophat.fbx", + "modelURL": "atp:/kineticObjects/tophat/tophat.fbx", "position": { "x": 0, "y": 0.541900634765625, @@ -150,10 +154,11 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m2.obj", + "compoundShapeURL": "atp:/kineticObjects/hair/m2.obj", "created": "2016-05-16T17:12:28Z", "dimensions": { "x": 0.094467177987098694, @@ -167,7 +172,7 @@ "z": 0 }, "id": "{0805851a-4ef1-4075-b4b3-5692ca7d6352}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m2.fbx", + "modelURL": "atp:/kineticObjects/hair/m3.fbx", "position": { "x": 0.0111083984375, "y": 0.00689697265625, @@ -189,6 +194,7 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, "compoundShapeURL": "atp:/dressingRoom/rectangleFrames.obj", "created": "2016-05-18T15:50:52Z", @@ -227,10 +233,11 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b3.obj", + "compoundShapeURL": "atp:/kineticObjects/hair/b3.obj", "created": "2016-05-16T17:10:17Z", "dimensions": { "x": 0.19054701924324036, @@ -244,7 +251,7 @@ "z": 0 }, "id": "{32926ddc-7c3b-49ca-97da-56e69af211af}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b3.fbx", + "modelURL": "atp:/kineticObjects/hair/b3.fbx", "position": { "x": 0.75, "y": 0.0643310546875, @@ -266,10 +273,11 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b1.obj", + "compoundShapeURL": "atp:/kineticObjects/hair/b1.obj", "created": "2016-05-16T17:05:55Z", "dimensions": { "x": 0.18918535113334656, @@ -283,7 +291,7 @@ "z": 0 }, "id": "{c412654f-9feb-489d-91f9-6f4eb369afe7}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b1.fbx", + "modelURL": "atp:/kineticObjects/hair/b1.fbx", "position": { "x": 0.55712890625, "y": 0.0443115234375, @@ -305,10 +313,11 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m3.obj", + "compoundShapeURL": "atp:/kineticObjects/hair/m3.obj", "created": "2016-05-16T17:12:28Z", "dimensions": { "x": 0.15590831637382507, @@ -322,7 +331,7 @@ "z": 0 }, "id": "{c2df6722-e970-4c36-bf42-b117e2bc13c4}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m3.fbx", + "modelURL": "atp:/kineticObjects/hair/m3.fbx", "position": { "x": 0.1549072265625, "y": 0.006439208984375, @@ -344,6 +353,7 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, "compoundShapeURL": "atp:/dressingRoom/Elvis.obj", "created": "2016-05-17T23:58:24Z", @@ -381,6 +391,7 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, "compoundShapeURL": "atp:/dressingRoom/roundFrames.obj", "created": "2016-05-18T15:55:14Z", @@ -419,10 +430,11 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m1.obj", + "compoundShapeURL": "atp:/kineticObjects/hair/m1.obj", "created": "2016-05-16T17:10:17Z", "dimensions": { "x": 0.10473950952291489, @@ -436,7 +448,7 @@ "z": 0 }, "id": "{448c53c2-bf1e-44f5-b28f-06da33a4145d}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/m1.fbx", + "modelURL": "atp:/kineticObjects/hair/m1.fbx", "position": { "x": 0.29052734375, "y": 0, @@ -458,10 +470,11 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, - "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b2.obj", + "compoundShapeURL": "atp:/kineticObjects/hair/b2.obj", "created": "2016-05-16T17:05:55Z", "dimensions": { "x": 0.12604480981826782, @@ -475,7 +488,7 @@ "z": 0 }, "id": "{33d3ba77-d2c9-4a5f-ba1c-f6f731d05221}", - "modelURL": "http://hifi-content.s3.amazonaws.com/DomainContent/Home/JJAssets/Stache%26Beard_Home/b2.fbx", + "modelURL": "atp:/kineticObjects/hair/b3.fbx", "position": { "x": 0.4130859375, "y": 0.030181884765625, @@ -497,6 +510,7 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionsWillMove": 1, @@ -536,6 +550,7 @@ "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { + "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, "compoundShapeURL": "atp:/dressingRoom/afro.obj", "created": "2016-05-17T23:58:24Z", diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index 2c56ee2141..bd8e4383e0 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -418,8 +418,8 @@ }); var dressingRoomBricabrac = new Bricabrac({ - x: 1105.8, - y: 460.4309, + x: 1106.8, + y: 460.3909, z: -72.6 }); From c48fce4f5ab1cebf53e191d40dbd9b41d218faf4 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 May 2016 15:24:10 -0700 Subject: [PATCH 0096/1237] WIP commit, socket errors when AvatarIdentity is larger then MTU --- assignment-client/src/avatars/AvatarMixer.cpp | 4 +- libraries/avatars/src/AvatarData.cpp | 48 ++++++++++------- libraries/avatars/src/AvatarData.h | 14 ++++- libraries/avatars/src/AvatarHashMap.cpp | 52 ++++++------------- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index cc94e4f1b7..46599396ca 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -417,7 +417,9 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes AvatarData& avatar = nodeData->getAvatar(); // parse the identity packet and update the change timestamp if appropriate - if (avatar.hasIdentityChangedAfterParsing(message->getMessage())) { + AvatarData::Identity identity; + AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity); + if (avatar.processAvatarIdentity(identity)) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->flagIdentityChange(); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 4b7e5cfb3c..abf3f52ed6 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -612,7 +612,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // the wrong bones, resulting in a twisted avatar, An un-animated avatar is preferable to this. bool skipJoints = false; if (_networkJointIndexMap.empty()) { - skipJoints = true; + qCDebug(avatars) << "AJT: parseAvatarDataPacket _networkJointIndexMap.size = " << _networkJointIndexMap.size(); + skipJoints = false; } int bytesOfValidity = (int)ceil((float)numJoints / (float)BITS_IN_BYTE); @@ -726,7 +727,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } } } - } // numJoints * 12 bytes + } // numJoints * 6 bytes #ifdef WANT_DEBUG if (numValidJointRotations > 15) { @@ -981,42 +982,45 @@ void AvatarData::clearJointsData() { } } -bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { +void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut) { QDataStream packetStream(data); + packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName >> identityOut.jointIndices; + + // AJT: just got a new networkJointIndicesMap. + qCDebug(avatars) << "AJT: receiving identity.jointIndices.size = " << identityOut.jointIndices.size(); +} + +bool AvatarData::processAvatarIdentity(const Identity& identity) { - QUuid avatarUUID; - QUrl skeletonModelURL; - QVector attachmentData; - QString displayName; - QHash networkJointIndices; - packetStream >> avatarUUID >> skeletonModelURL >> attachmentData >> displayName >> networkJointIndices; bool hasIdentityChanged = false; - if (!_jointIndices.empty() && _networkJointIndexMap.empty() && !networkJointIndices.empty()) { + qCDebug(avatars) << "AJT: processAvatarIdentity got here!"; + + if (!_jointIndices.empty() && _networkJointIndexMap.empty() && !identity.jointIndices.empty()) { // build networkJointIndexMap from _jointIndices and networkJointIndices. - _networkJointIndexMap.fill(networkJointIndices.size(), -1); - for (auto iter = networkJointIndices.cbegin(); iter != networkJointIndices.end(); ++iter) { + _networkJointIndexMap.fill(identity.jointIndices.size(), -1); + for (auto iter = identity.jointIndices.cbegin(); iter != identity.jointIndices.end(); ++iter) { int jointIndex = getJointIndex(iter.key()); _networkJointIndexMap[iter.value()] = jointIndex; + qCDebug(avatars) << "AJT: mapping " << iter.value() << " -> " << jointIndex; } } - // AJT: just got a new networkJointIndicesMap. - qCDebug(avatars) << "AJT: receiving networkJointIndices.size = " << networkJointIndices.size(); + qCDebug(avatars) << "AJT: processAvatarIdentity got here!"; - if (_firstSkeletonCheck || (skeletonModelURL != _skeletonModelURL)) { - setSkeletonModelURL(skeletonModelURL); + if (_firstSkeletonCheck || (identity.skeletonModelURL != _skeletonModelURL)) { + setSkeletonModelURL(identity.skeletonModelURL); hasIdentityChanged = true; _firstSkeletonCheck = false; } - if (displayName != _displayName) { - setDisplayName(displayName); + if (identity.displayName != _displayName) { + setDisplayName(identity.displayName); hasIdentityChanged = true; } - if (attachmentData != _attachmentData) { - setAttachmentData(attachmentData); + if (identity.attachmentData != _attachmentData) { + setAttachmentData(identity.attachmentData); hasIdentityChanged = true; } @@ -1204,6 +1208,8 @@ void AvatarData::sendIdentityPacket() { QByteArray identityData = identityByteArray(); + qCDebug(avatars) << "AJT: sendIdentityPacket() size = " << identityData.size(); + auto packetList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); packetList->write(identityData); nodeList->eachMatchingNode( @@ -1213,6 +1219,8 @@ void AvatarData::sendIdentityPacket() { [&](const SharedNodePointer& node) { nodeList->sendPacketList(std::move(packetList), *node); }); + + qCDebug(avatars) << "AJT: sendIdentityPacket() done!"; } void AvatarData::updateJointMappings() { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 43bc682bda..feabfad544 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -280,7 +280,19 @@ public: const HeadData* getHeadData() const { return _headData; } - bool hasIdentityChangedAfterParsing(const QByteArray& data); + struct Identity { + QUuid uuid; + QUrl skeletonModelURL; + QVector attachmentData; + QString displayName; + QHash jointIndices; + }; + + static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); + + // returns true if identity has changed, false otherwise. + bool processAvatarIdentity(const Identity& identity); + QByteArray identityByteArray(); const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; } diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index f14e2b0ad3..1b1cb14e41 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -50,26 +50,26 @@ AvatarSharedPointer AvatarHashMap::newSharedAvatar() { AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { qCDebug(avatars) << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap."; - + auto avatar = newSharedAvatar(); avatar->setSessionUUID(sessionUUID); avatar->setOwningAvatarMixer(mixerWeakPointer); - + _avatarHash.insert(sessionUUID, avatar); emit avatarAddedEvent(sessionUUID); - + return avatar; } AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { QWriteLocker locker(&_hashLock); - + auto avatar = _avatarHash.value(sessionUUID); - + if (!avatar) { avatar = addAvatar(sessionUUID, mixerWeakPointer); } - + return avatar; } @@ -86,14 +86,14 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer mess // only add them if mixerWeakPointer points to something (meaning that mixer is still around) while (message->getBytesLeftToRead()) { QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - + int positionBeforeRead = message->getPosition(); QByteArray byteArray = message->readWithoutCopy(message->getBytesLeftToRead()); - + if (sessionUUID != _lastOwnerSessionUUID) { auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); - + // have the matching (or new) avatar parse the data from the packet int bytesRead = avatar->parseDataFromBuffer(byteArray); message->seek(positionBeforeRead + bytesRead); @@ -107,33 +107,13 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer mess } void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode) { - // setup a data stream to parse the packet - QDataStream identityStream(message->getMessage()); + AvatarData::Identity identity; + AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity); - QUuid sessionUUID; - - while (!identityStream.atEnd()) { + // mesh URL for a UUID, find avatar in our list + auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); - QUrl faceMeshURL, skeletonURL; - QVector attachmentData; - QString displayName; - identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName; - - // mesh URL for a UUID, find avatar in our list - auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); - - if (avatar->getSkeletonModelURL().isEmpty() || (avatar->getSkeletonModelURL() != skeletonURL)) { - avatar->setSkeletonModelURL(skeletonURL); // Will expand "" to default and so will not continuously fire - } - - if (avatar->getAttachmentData() != attachmentData) { - avatar->setAttachmentData(attachmentData); - } - - if (avatar->getDisplayName() != displayName) { - avatar->setDisplayName(displayName); - } - } + avatar->processAvatarIdentity(identity); } void AvatarHashMap::processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode) { @@ -144,9 +124,9 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S void AvatarHashMap::removeAvatar(const QUuid& sessionUUID) { QWriteLocker locker(&_hashLock); - + auto removedAvatar = _avatarHash.take(sessionUUID); - + if (removedAvatar) { handleRemovedAvatar(removedAvatar); } From cf49c8e2bfbcafe5ef95265f5416610eed7935fc Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 6 May 2016 11:11:50 -0700 Subject: [PATCH 0097/1237] Add getUserAgent function that includes plugin information --- interface/src/Application.cpp | 34 ++++++++++++++++++++++++++++++++++ interface/src/Application.h | 3 +++ 2 files changed, 37 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5f08877ae2..edc6227bfc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1057,6 +1057,40 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : firstRun.set(false); } +QString Application::getUserAgent() { + if (QThread::currentThread() != thread()) { + QString userAgent; + + QMetaObject::invokeMethod(this, "getUserAgent", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, userAgent)); + + return userAgent; + } + + QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " + + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; + + auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; + + // For each plugin, add to userAgent + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + foreach(auto dp, displayPlugins) { + if (dp->isActive() && dp->isHmd()) { + userAgent += " " + formatPluginName(dp->getName()); + } + } + + // For each plugin, add to userAgent + auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); + foreach(auto ip, inputPlugins) { + if (ip->isActive()) { + userAgent += " " + formatPluginName(ip->getName()); + } + } + + return userAgent; +} + + void Application::checkChangeCursor() { QMutexLocker locker(&_changeCursorLock); diff --git a/interface/src/Application.h b/interface/src/Application.h index 558190c8d1..5616722c92 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -112,6 +112,9 @@ public: QString getPreviousScriptLocation(); void setPreviousScriptLocation(const QString& previousScriptLocation); + // Return an HTTP User-Agent string with OS and device information. + Q_INVOKABLE QString getUserAgent(); + void initializeGL(); void initializeUi(); void paintGL(); From dd093e3fcdc2716f22d8b3013bbb08a0e8fd3071 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 6 May 2016 15:25:19 -0700 Subject: [PATCH 0098/1237] Make AccountManager a DependencyManager singleton --- assignment-client/src/AssignmentClient.cpp | 10 +++-- domain-server/src/DomainGatekeeper.cpp | 2 +- domain-server/src/DomainServer.cpp | 45 ++++++++++--------- interface/src/Application.cpp | 17 +++---- interface/src/DiscoverabilityManager.cpp | 12 ++--- interface/src/Menu.cpp | 6 +-- interface/src/main.cpp | 12 ++--- .../scripting/AccountScriptingInterface.cpp | 12 ++--- .../GlobalServicesScriptingInterface.cpp | 18 ++++---- interface/src/ui/LoginDialog.cpp | 15 ++++--- interface/src/ui/Snapshot.cpp | 9 ++-- libraries/networking/src/AccountManager.cpp | 10 ----- libraries/networking/src/AccountManager.h | 3 ++ libraries/networking/src/AddressManager.cpp | 7 ++- libraries/networking/src/DomainHandler.cpp | 6 +-- libraries/networking/src/NodeList.cpp | 20 ++++----- .../src/OAuthNetworkAccessManager.cpp | 6 +-- .../networking/src/UserActivityLogger.cpp | 5 ++- .../script-engine/src/XMLHttpRequestClass.cpp | 6 +-- libraries/ui/src/OffscreenUi.cpp | 4 +- libraries/ui/src/Tooltip.cpp | 10 ++--- 21 files changed, 118 insertions(+), 117 deletions(-) diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 0455377d89..4fc8975262 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -51,6 +51,8 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri LogUtils::init(); QSettings::setDefaultFormat(QSettings::IniFormat); + + DependencyManager::set(); auto scriptableAvatar = DependencyManager::set(); auto addressManager = DependencyManager::set(); @@ -116,7 +118,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri _requestTimer.start(ASSIGNMENT_REQUEST_INTERVAL_MSECS); // connections to AccountManager for authentication - connect(&AccountManager::getInstance(), &AccountManager::authRequired, + connect(DependencyManager::get().data(), &AccountManager::authRequired, this, &AssignmentClient::handleAuthenticationRequest); // Create Singleton objects on main thread @@ -309,13 +311,13 @@ void AssignmentClient::handleAuthenticationRequest() { QString username = sysEnvironment.value(DATA_SERVER_USERNAME_ENV); QString password = sysEnvironment.value(DATA_SERVER_PASSWORD_ENV); - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); if (!username.isEmpty() && !password.isEmpty()) { // ask the account manager to log us in from the env variables - accountManager.requestAccessToken(username, password); + accountManager->requestAccessToken(username, password); } else { - qCWarning(assigmnentclient) << "Authentication was requested against" << qPrintable(accountManager.getAuthURL().toString()) + qCWarning(assigmnentclient) << "Authentication was requested against" << qPrintable(accountManager->getAuthURL().toString()) << "but both or one of" << qPrintable(DATA_SERVER_USERNAME_ENV) << "/" << qPrintable(DATA_SERVER_PASSWORD_ENV) << "are not set. Unable to authenticate."; diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 94e79416a5..919ac37ee9 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -485,7 +485,7 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { qDebug() << "Requesting public key for user" << username; - AccountManager::getInstance().sendRequest(USER_PUBLIC_KEY_PATH.arg(username), + DependencyManager::get()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, callbackParams); } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index a5c485ebe2..f37954c4e6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -77,7 +78,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : // make sure we have a fresh AccountManager instance // (need this since domain-server can restart itself and maintain static variables) - AccountManager::getInstance(true); + DependencyManager::set(); auto args = arguments(); @@ -195,8 +196,8 @@ bool DomainServer::optionallySetupOAuth() { _oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL; } - AccountManager& accountManager = AccountManager::getInstance(); - accountManager.setAuthURL(_oauthProviderURL); + auto accountManager = DependencyManager::get(); + accountManager->setAuthURL(_oauthProviderURL); _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); @@ -239,7 +240,7 @@ void DomainServer::optionallyGetTemporaryName(const QStringList& arguments) { // we've been asked to grab a temporary name from the API // so fire off that request now - auto& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); // get callbacks for temporary domain result JSONCallbackParameters callbackParameters; @@ -248,8 +249,8 @@ void DomainServer::optionallyGetTemporaryName(const QStringList& arguments) { callbackParameters.errorCallbackReceiver = this; callbackParameters.errorCallbackMethod = "handleTempDomainError"; - accountManager.sendRequest("/api/v1/domains/temporary", AccountManagerAuth::None, - QNetworkAccessManager::PostOperation, callbackParameters); + accountManager->sendRequest("/api/v1/domains/temporary", AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParameters); } } @@ -397,7 +398,7 @@ bool DomainServer::resetAccountManagerAccessToken() { << "at keypath metaverse.access_token or in your ENV at key DOMAIN_SERVER_ACCESS_TOKEN"; // clear any existing access token from AccountManager - AccountManager::getInstance().setAccessTokenForCurrentAuthURL(QString()); + DependencyManager::get()->setAccessTokenForCurrentAuthURL(QString()); return false; } @@ -407,7 +408,7 @@ bool DomainServer::resetAccountManagerAccessToken() { } // give this access token to the AccountManager - AccountManager::getInstance().setAccessTokenForCurrentAuthURL(accessToken); + DependencyManager::get()->setAccessTokenForCurrentAuthURL(accessToken); return true; @@ -499,17 +500,17 @@ void DomainServer::setupICEHeartbeatForFullNetworking() { limitedNodeList->startSTUNPublicSocketUpdate(); // to send ICE heartbeats we'd better have a private key locally with an uploaded public key - auto& accountManager = AccountManager::getInstance(); - auto domainID = accountManager.getAccountInfo().getDomainID(); + auto accountManager = DependencyManager::get(); + auto domainID = accountManager->getAccountInfo().getDomainID(); // if we have an access token and we don't have a private key or the current domain ID has changed // we should generate a new keypair - if (!accountManager.getAccountInfo().hasPrivateKey() || domainID != limitedNodeList->getSessionUUID()) { - accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + if (!accountManager->getAccountInfo().hasPrivateKey() || domainID != limitedNodeList->getSessionUUID()) { + accountManager->generateNewDomainKeypair(limitedNodeList->getSessionUUID()); } // hookup to the signal from account manager that tells us when keypair is available - connect(&accountManager, &AccountManager::newKeypair, this, &DomainServer::handleKeypairChange); + connect(accountManager.data(), &AccountManager::newKeypair, this, &DomainServer::handleKeypairChange); if (!_iceHeartbeatTimer) { // setup a timer to heartbeat with the ice-server every so often @@ -962,9 +963,9 @@ void DomainServer::setupPendingAssignmentCredits() { void DomainServer::sendPendingTransactionsToServer() { - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); - if (accountManager.hasValidAccessToken()) { + if (accountManager->hasValidAccessToken()) { // enumerate the pending transactions and send them to the server to complete payment TransactionHash::iterator i = _pendingAssignmentCredits.begin(); @@ -975,7 +976,7 @@ void DomainServer::sendPendingTransactionsToServer() { transactionCallbackParams.jsonCallbackMethod = "transactionJSONCallback"; while (i != _pendingAssignmentCredits.end()) { - accountManager.sendRequest("api/v1/transactions", + accountManager->sendRequest("api/v1/transactions", AccountManagerAuth::Required, QNetworkAccessManager::PostOperation, transactionCallbackParams, i.value()->postJson().toJson()); @@ -1073,7 +1074,7 @@ void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) { QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); - AccountManager::getInstance().sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), + DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, JSONCallbackParameters(), @@ -1103,7 +1104,7 @@ void DomainServer::sendICEServerAddressToMetaverseAPI() { static const QString DOMAIN_ICE_ADDRESS_UPDATE = "/api/v1/domains/%1/ice_server_address"; - AccountManager::getInstance().sendRequest(DOMAIN_ICE_ADDRESS_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), + DependencyManager::get()->sendRequest(DOMAIN_ICE_ADDRESS_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, @@ -1123,15 +1124,15 @@ void DomainServer::handleFailedICEServerAddressUpdate(QNetworkReply& requestRepl void DomainServer::sendHeartbeatToIceServer() { if (!_iceServerSocket.getAddress().isNull()) { - auto& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); auto limitedNodeList = DependencyManager::get(); - if (!accountManager.getAccountInfo().hasPrivateKey()) { + if (!accountManager->getAccountInfo().hasPrivateKey()) { qWarning() << "Cannot send an ice-server heartbeat without a private key for signature."; qWarning() << "Waiting for keypair generation to complete before sending ICE heartbeat."; if (!limitedNodeList->getSessionUUID().isNull()) { - accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + accountManager->generateNewDomainKeypair(limitedNodeList->getSessionUUID()); } else { qWarning() << "Attempting to send ICE server heartbeat with no domain ID. This is not supported"; } @@ -2101,7 +2102,7 @@ void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer(); - AccountManager::getInstance().generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + DependencyManager::get()->generateNewDomainKeypair(limitedNodeList->getSessionUUID()); // reset our number of heartbeat denials _numHeartbeatDenials = 0; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index edc6227bfc..3fffc4d9f3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -402,6 +402,7 @@ bool setupEssentials(int& argc, char** argv) { Setting::init(); // Set dependencies + DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -656,15 +657,15 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); // connect to appropriate slots on AccountManager - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); auto dialogsManager = DependencyManager::get(); - connect(&accountManager, &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); - connect(&accountManager, &AccountManager::usernameChanged, this, &Application::updateWindowTitle); + connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); + connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); // set the account manager's root URL and trigger a login request if we don't have the access token - accountManager.setIsAgent(true); - accountManager.setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); + accountManager->setIsAgent(true); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. @@ -4169,7 +4170,7 @@ void Application::updateWindowTitle() const { auto nodeList = DependencyManager::get(); QString connectionStatus = nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED) "; - QString username = AccountManager::getInstance().getAccountInfo().getUsername(); + QString username = DependencyManager::get()->getAccountInfo().getUsername(); QString currentPlaceName = DependencyManager::get()->getHost(); if (currentPlaceName.isEmpty()) { @@ -4809,8 +4810,8 @@ void Application::takeSnapshot() { QString fileName = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); - AccountManager& accountManager = AccountManager::getInstance(); - if (!accountManager.isLoggedIn()) { + auto accountManager = DependencyManager::get(); + if (!accountManager->isLoggedIn()) { return; } diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index a8b0a265c9..83f87f82ba 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -35,9 +35,9 @@ const QString API_USER_HEARTBEAT_PATH = "/api/v1/user/heartbeat"; const QString SESSION_ID_KEY = "session_id"; void DiscoverabilityManager::updateLocation() { - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); - if (_mode.get() != Discoverability::None && accountManager.isLoggedIn()) { + if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) { auto addressManager = DependencyManager::get(); DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); @@ -98,7 +98,7 @@ void DiscoverabilityManager::updateLocation() { apiPath = API_USER_LOCATION_PATH; } - accountManager.sendRequest(apiPath, AccountManagerAuth::Required, + accountManager->sendRequest(apiPath, AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, callbackParameters, QJsonDocument(rootObject).toJson()); @@ -116,7 +116,7 @@ void DiscoverabilityManager::updateLocation() { heartbeatObject[SESSION_ID_KEY] = QJsonValue(); } - accountManager.sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, + accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, QJsonDocument(heartbeatObject).toJson()); } @@ -131,8 +131,8 @@ void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply } void DiscoverabilityManager::removeLocation() { - AccountManager& accountManager = AccountManager::getInstance(); - accountManager.sendRequest(API_USER_LOCATION_PATH, AccountManagerAuth::Required, QNetworkAccessManager::DeleteOperation); + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(API_USER_LOCATION_PATH, AccountManagerAuth::Required, QNetworkAccessManager::DeleteOperation); } void DiscoverabilityManager::setDiscoverabilityMode(Discoverability::Mode discoverabilityMode) { diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 05bb03bf86..8b69bb8022 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -54,7 +54,7 @@ Menu* Menu::getInstance() { Menu::Menu() { auto dialogsManager = DependencyManager::get(); - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); // File/Application menu ---------------------------------- MenuWrapper* fileMenu = addMenu("File"); @@ -64,9 +64,9 @@ Menu::Menu() { addActionToQMenuAndActionHash(fileMenu, MenuOption::Login); // connect to the appropriate signal of the AccountManager so that we can change the Login/Logout menu item - connect(&accountManager, &AccountManager::profileChanged, + connect(accountManager.data(), &AccountManager::profileChanged, dialogsManager.data(), &DialogsManager::toggleLoginDialog); - connect(&accountManager, &AccountManager::logoutComplete, + connect(accountManager.data(), &AccountManager::logoutComplete, dialogsManager.data(), &DialogsManager::toggleLoginDialog); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 1726d47045..13f9470fda 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -144,11 +144,11 @@ int main(int argc, const char* argv[]) { // If we failed the OpenGLVersion check, log it. if (override) { - auto& accountManager = AccountManager::getInstance(); - if (accountManager.isLoggedIn()) { + auto accountManager = DependencyManager::get(); + if (accountManager->isLoggedIn()) { UserActivityLogger::getInstance().insufficientGLVersion(glData); } else { - QObject::connect(&AccountManager::getInstance(), &AccountManager::loginComplete, [glData](){ + QObject::connect(accountManager.data(), &AccountManager::loginComplete, [glData](){ static bool loggedInsufficientGL = false; if (!loggedInsufficientGL) { UserActivityLogger::getInstance().insufficientGLVersion(glData); @@ -168,9 +168,9 @@ int main(int argc, const char* argv[]) { QObject::connect(&server, &QLocalServer::newConnection, &app, &Application::handleLocalServerConnection, Qt::DirectConnection); #ifdef HAS_BUGSPLAT - AccountManager& accountManager = AccountManager::getInstance(); - crashReporter.mpSender.setDefaultUserName(qPrintable(accountManager.getAccountInfo().getUsername())); - QObject::connect(&accountManager, &AccountManager::usernameChanged, &app, [&crashReporter](const QString& newUsername) { + auto accountManager = DependencyManager::get(); + crashReporter.mpSender.setDefaultUserName(qPrintable(accountManager->getAccountInfo().getUsername())); + QObject::connect(accountManager.data(), &AccountManager::usernameChanged, &app, [&crashReporter](const QString& newUsername) { crashReporter.mpSender.setDefaultUserName(qPrintable(newUsername)); }); diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp index 1b6d52ac2a..377dbe8e35 100644 --- a/interface/src/scripting/AccountScriptingInterface.cpp +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -13,20 +13,22 @@ #include "AccountScriptingInterface.h" +#include + AccountScriptingInterface* AccountScriptingInterface::getInstance() { static AccountScriptingInterface sharedInstance; return &sharedInstance; } bool AccountScriptingInterface::isLoggedIn() { - AccountManager& accountManager = AccountManager::getInstance(); - return accountManager.isLoggedIn(); + auto accountManager = DependencyManager::get(); + return accountManager->isLoggedIn(); } QString AccountScriptingInterface::getUsername() { - AccountManager& accountManager = AccountManager::getInstance(); - if (accountManager.isLoggedIn()) { - return accountManager.getAccountInfo().getUsername(); + auto accountManager = DependencyManager::get(); + if (accountManager->isLoggedIn()) { + return accountManager->getAccountInfo().getUsername(); } else { return "Unknown user"; } diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.cpp b/interface/src/scripting/GlobalServicesScriptingInterface.cpp index e8d63a6d99..d7e5bae3f8 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.cpp +++ b/interface/src/scripting/GlobalServicesScriptingInterface.cpp @@ -17,10 +17,10 @@ #include "GlobalServicesScriptingInterface.h" GlobalServicesScriptingInterface::GlobalServicesScriptingInterface() { - AccountManager& accountManager = AccountManager::getInstance(); - connect(&accountManager, &AccountManager::usernameChanged, this, &GlobalServicesScriptingInterface::myUsernameChanged); - connect(&accountManager, &AccountManager::logoutComplete, this, &GlobalServicesScriptingInterface::loggedOut); - connect(&accountManager, &AccountManager::loginComplete, this, &GlobalServicesScriptingInterface::connected); + auto accountManager = DependencyManager::get(); + connect(accountManager.data(), &AccountManager::usernameChanged, this, &GlobalServicesScriptingInterface::myUsernameChanged); + connect(accountManager.data(), &AccountManager::logoutComplete, this, &GlobalServicesScriptingInterface::loggedOut); + connect(accountManager.data(), &AccountManager::loginComplete, this, &GlobalServicesScriptingInterface::connected); _downloading = false; QTimer* checkDownloadTimer = new QTimer(this); @@ -34,10 +34,10 @@ GlobalServicesScriptingInterface::GlobalServicesScriptingInterface() { } GlobalServicesScriptingInterface::~GlobalServicesScriptingInterface() { - AccountManager& accountManager = AccountManager::getInstance(); - disconnect(&accountManager, &AccountManager::usernameChanged, this, &GlobalServicesScriptingInterface::myUsernameChanged); - disconnect(&accountManager, &AccountManager::logoutComplete, this, &GlobalServicesScriptingInterface::loggedOut); - disconnect(&accountManager, &AccountManager::loginComplete, this, &GlobalServicesScriptingInterface::connected); + auto accountManager = DependencyManager::get(); + disconnect(accountManager.data(), &AccountManager::usernameChanged, this, &GlobalServicesScriptingInterface::myUsernameChanged); + disconnect(accountManager.data(), &AccountManager::logoutComplete, this, &GlobalServicesScriptingInterface::loggedOut); + disconnect(accountManager.data(), &AccountManager::loginComplete, this, &GlobalServicesScriptingInterface::connected); } GlobalServicesScriptingInterface* GlobalServicesScriptingInterface::getInstance() { @@ -46,7 +46,7 @@ GlobalServicesScriptingInterface* GlobalServicesScriptingInterface::getInstance( } const QString& GlobalServicesScriptingInterface::getUsername() const { - return AccountManager::getInstance().getAccountInfo().getUsername(); + return DependencyManager::get()->getAccountInfo().getUsername(); } void GlobalServicesScriptingInterface::loggedOut() { diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 60d19df355..80d52b7a07 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -24,14 +24,15 @@ HIFI_QML_DEF(LoginDialog) LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent), _rootUrl(NetworkingConstants::METAVERSE_SERVER_URL.toString()) { - connect(&AccountManager::getInstance(), &AccountManager::loginComplete, + auto accountManager = DependencyManager::get(); + connect(accountManager.data(), &AccountManager::loginComplete, this, &LoginDialog::handleLoginCompleted); - connect(&AccountManager::getInstance(), &AccountManager::loginFailed, + connect(accountManager.data(), &AccountManager::loginFailed, this, &LoginDialog::handleLoginFailed); } void LoginDialog::toggleAction() { - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); QAction* loginAction = Menu::getInstance()->getActionForOption(MenuOption::Login); Q_CHECK_PTR(loginAction); static QMetaObject::Connection connection; @@ -39,10 +40,10 @@ void LoginDialog::toggleAction() { disconnect(connection); } - if (accountManager.isLoggedIn()) { + if (accountManager->isLoggedIn()) { // change the menu item to logout - loginAction->setText("Logout " + accountManager.getAccountInfo().getUsername()); - connection = connect(loginAction, &QAction::triggered, &accountManager, &AccountManager::logout); + loginAction->setText("Logout " + accountManager->getAccountInfo().getUsername()); + connection = connect(loginAction, &QAction::triggered, accountManager.data(), &AccountManager::logout); } else { // change the menu item to login loginAction->setText("Login"); @@ -78,7 +79,7 @@ QString LoginDialog::rootUrl() const { void LoginDialog::login(const QString& username, const QString& password) { qDebug() << "Attempting to login " << username; setStatusText("Logging in..."); - AccountManager::getInstance().requestAccessToken(username, password); + DependencyManager::get()->requestAccessToken(username, password); } void LoginDialog::openUrl(const QString& url) { diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index e35039df3d..b8be2bb8c4 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -92,7 +92,7 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { QUrl currentURL = DependencyManager::get()->currentAddress(); shot.setText(URL, currentURL.toString()); - QString username = AccountManager::getInstance().getAccountInfo().getUsername(); + QString username = DependencyManager::get()->getAccountInfo().getUsername(); // normalize username, replace all non alphanumeric with '-' username.replace(QRegExp("[^A-Za-z0-9_]"), "-"); @@ -144,14 +144,15 @@ const QString SUCCESS_LABEL_TEMPLATE = "Success!!! Go check out your image ...(); + if (accountManager->getAccountInfo().getDiscourseApiKey().isEmpty()) { OffscreenUi::warning(nullptr, "", "Your Discourse API key is missing, you cannot share snapshots. Please try to relog."); return QString(); } QHttpPart apiKeyPart; apiKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"api_key\"")); - apiKeyPart.setBody(AccountManager::getInstance().getAccountInfo().getDiscourseApiKey().toLatin1()); + apiKeyPart.setBody(accountManager->getAccountInfo().getDiscourseApiKey().toLatin1()); QString filename = fileUrl.toLocalFile(); qDebug() << filename; @@ -206,7 +207,7 @@ QString SnapshotUploader::sendForumPost(const QString& snapshotPath, const QStri QUrl forumUrl(FORUM_POST_URL); QUrlQuery query; - query.addQueryItem("api_key", AccountManager::getInstance().getAccountInfo().getDiscourseApiKey()); + query.addQueryItem("api_key", DependencyManager::get()->getAccountInfo().getDiscourseApiKey()); query.addQueryItem("topic_id", FORUM_REPLY_TO_TOPIC); query.addQueryItem("raw", FORUM_POST_TEMPLATE.arg(snapshotPath, notes)); forumUrl.setQuery(query); diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 8c23844f4e..324301a9d6 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -36,16 +36,6 @@ const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; -AccountManager& AccountManager::getInstance(bool forceReset) { - static std::unique_ptr sharedInstance(new AccountManager()); - - if (forceReset) { - sharedInstance.reset(new AccountManager()); - } - - return *sharedInstance; -} - Q_DECLARE_METATYPE(OAuthAccessToken) Q_DECLARE_METATYPE(DataServerAccountInfo) Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 108b49f678..7306eaef14 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -20,6 +20,9 @@ #include "NetworkAccessManager.h" #include "DataServerAccountInfo.h" +#include "SharedUtil.h" + +#include class JSONCallbackParameters { public: diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 27647d2694..a97f4df35d 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -374,7 +374,7 @@ void AddressManager::attemptPlaceNameLookup(const QString& lookupString, const Q // remember how this lookup was triggered for history storage handling later requestParams.insert(LOOKUP_TRIGGER_KEY, static_cast(trigger)); - AccountManager::getInstance().sendRequest(GET_PLACE.arg(placeName), + DependencyManager::get()->sendRequest(GET_PLACE.arg(placeName), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, apiCallbackParameters(), @@ -397,7 +397,7 @@ void AddressManager::attemptDomainIDLookup(const QString& lookupString, const QS // remember how this lookup was triggered for history storage handling later requestParams.insert(LOOKUP_TRIGGER_KEY, static_cast(trigger)); - AccountManager::getInstance().sendRequest(GET_DOMAIN_ID.arg(domainID), + DependencyManager::get()->sendRequest(GET_DOMAIN_ID.arg(domainID), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, apiCallbackParameters(), @@ -577,7 +577,6 @@ bool AddressManager::setHost(const QString& host, LookupTrigger trigger, quint16 return false; } - bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, LookupTrigger trigger) { bool hostChanged = setHost(hostname, trigger, port); @@ -600,7 +599,7 @@ void AddressManager::goToUser(const QString& username) { requestParams.insert(LOOKUP_TRIGGER_KEY, static_cast(LookupTrigger::UserInput)); // this is a username - pull the captured name and lookup that user's location - AccountManager::getInstance().sendRequest(GET_USER_LOCATION.arg(formattedUsername), + DependencyManager::get()->sendRequest(GET_USER_LOCATION.arg(formattedUsername), AccountManagerAuth::Optional, QNetworkAccessManager::GetOperation, apiCallbackParameters(), diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index b2eb0cd680..44ce63e6c6 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -371,10 +371,10 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); if (!_hasCheckedForAccessToken) { - accountManager.checkAndSignalForAccessToken(); + accountManager->checkAndSignalForAccessToken(); _hasCheckedForAccessToken = true; } @@ -382,7 +382,7 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer= CONNECTION_DENIALS_FOR_KEYPAIR_REGEN) { - accountManager.generateNewUserKeypair(); + accountManager->generateNewUserKeypair(); _connectionDenialsSinceKeypairRegen = 0; } } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 25dfe884db..482d0366fd 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -80,16 +80,16 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // send a ping punch immediately connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer); - auto &accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); // assume that we may need to send a new DS check in anytime a new keypair is generated - connect(&accountManager, &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn); + connect(accountManager.data(), &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn); // clear out NodeList when login is finished - connect(&accountManager, &AccountManager::loginComplete , this, &NodeList::reset); + connect(accountManager.data(), &AccountManager::loginComplete , this, &NodeList::reset); // clear our NodeList when logout is requested - connect(&accountManager, &AccountManager::logoutComplete , this, &NodeList::reset); + connect(accountManager.data(), &AccountManager::logoutComplete , this, &NodeList::reset); // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); @@ -273,7 +273,7 @@ void NodeList::sendDomainServerCheckIn() { } // check if we're missing a keypair we need to verify ourselves with the domain-server - auto& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); const QUuid& connectionToken = _domainHandler.getConnectionToken(); // we assume that we're on the same box as the DS if it has the same local address and @@ -283,10 +283,10 @@ void NodeList::sendDomainServerCheckIn() { bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull() && !localhostDomain; - if (requiresUsernameSignature && !accountManager.getAccountInfo().hasPrivateKey()) { + if (requiresUsernameSignature && !accountManager->getAccountInfo().hasPrivateKey()) { qWarning() << "A keypair is required to present a username signature to the domain-server" << "but no keypair is present. Waiting for keypair generation to complete."; - accountManager.generateNewUserKeypair(); + accountManager->generateNewUserKeypair(); // don't send the check in packet - wait for the keypair first return; @@ -318,12 +318,12 @@ void NodeList::sendDomainServerCheckIn() { packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); if (!_domainHandler.isConnected()) { - DataServerAccountInfo& accountInfo = accountManager.getAccountInfo(); + DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); packetStream << accountInfo.getUsername(); // if this is a connect request, and we can present a username signature, send it along - if (requiresUsernameSignature && accountManager.getAccountInfo().hasPrivateKey()) { - const QByteArray& usernameSignature = accountManager.getAccountInfo().getUsernameSignature(connectionToken); + if (requiresUsernameSignature && accountManager->getAccountInfo().hasPrivateKey()) { + const QByteArray& usernameSignature = accountManager->getAccountInfo().getUsernameSignature(connectionToken); packetStream << usernameSignature; } } diff --git a/libraries/networking/src/OAuthNetworkAccessManager.cpp b/libraries/networking/src/OAuthNetworkAccessManager.cpp index fa6f3b8340..92e7a2ff4f 100644 --- a/libraries/networking/src/OAuthNetworkAccessManager.cpp +++ b/libraries/networking/src/OAuthNetworkAccessManager.cpp @@ -32,14 +32,14 @@ OAuthNetworkAccessManager* OAuthNetworkAccessManager::getInstance() { QNetworkReply* OAuthNetworkAccessManager::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest& req, QIODevice* outgoingData) { - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); - if (accountManager.hasValidAccessToken() + if (accountManager->hasValidAccessToken() && req.url().host() == NetworkingConstants::METAVERSE_SERVER_URL.host()) { QNetworkRequest authenticatedRequest(req); authenticatedRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); authenticatedRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, - accountManager.getAccountInfo().getAccessToken().authorizationHeaderValue()); + accountManager->getAccountInfo().getAccessToken().authorizationHeaderValue()); return QNetworkAccessManager::createRequest(op, authenticatedRequest, outgoingData); } else { diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 0d7690840d..83c6eb304e 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -17,6 +17,7 @@ #include "NetworkLogging.h" #include "UserActivityLogger.h" +#include static const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; @@ -34,7 +35,7 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall return; } - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); // Adding the action name @@ -59,7 +60,7 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall params.errorCallbackMethod = "requestError"; } - accountManager.sendRequest(USER_ACTIVITY_URL, + accountManager->sendRequest(USER_ACTIVITY_URL, AccountManagerAuth::Optional, QNetworkAccessManager::PostOperation, params, NULL, multipart); diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 9a6f81b19f..15b2576331 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -140,11 +140,11 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a _async = async; if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) { - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); - if (accountManager.hasValidAccessToken()) { + if (accountManager->hasValidAccessToken()) { QUrlQuery urlQuery(_url.query()); - urlQuery.addQueryItem("access_token", accountManager.getAccountInfo().getAccessToken().token); + urlQuery.addQueryItem("access_token", accountManager->getAccountInfo().getAccessToken().token); _url.setQuery(urlQuery); } diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 0c1ad69d72..4fb25e3e3f 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -53,8 +53,8 @@ QString fixupHifiUrl(const QString& urlString) { QUrl url(urlString); QUrlQuery query(url); if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { - AccountManager& accountManager = AccountManager::getInstance(); - query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token); + auto accountManager = DependencyManager::get(); + query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager->getAccountInfo().getAccessToken().token); url.setQuery(query.query()); return url.toString(); } diff --git a/libraries/ui/src/Tooltip.cpp b/libraries/ui/src/Tooltip.cpp index 56ba8a95b9..3c0902b378 100644 --- a/libraries/ui/src/Tooltip.cpp +++ b/libraries/ui/src/Tooltip.cpp @@ -84,16 +84,16 @@ void Tooltip::requestHyperlinkImage() { // should the network link be removed from UI at a later date. // we possibly have a valid place name - so ask the API for the associated info - AccountManager& accountManager = AccountManager::getInstance(); + auto accountManager = DependencyManager::get(); JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "handleAPIResponse"; - accountManager.sendRequest(GET_PLACE.arg(_title), - AccountManagerAuth::None, - QNetworkAccessManager::GetOperation, - callbackParams); + accountManager->sendRequest(GET_PLACE.arg(_title), + AccountManagerAuth::None, + QNetworkAccessManager::GetOperation, + callbackParams); } } } From bfb4bb089638ab61955638fcccda07b654c116fe Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 6 May 2016 15:33:52 -0700 Subject: [PATCH 0099/1237] Add userAgentGetter to AccountManager for custom UserAgent --- libraries/networking/src/AccountManager.cpp | 12 +++++++----- libraries/networking/src/AccountManager.h | 9 ++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 324301a9d6..9080e3cc53 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -69,7 +69,8 @@ QJsonObject AccountManager::dataObjectFromResponse(QNetworkReply &requestReply) } } -AccountManager::AccountManager() : +AccountManager::AccountManager(UserAgentGetter userAgentGetter) : + _userAgentGetter(userAgentGetter), _authURL(), _pendingCallbackMap() { @@ -212,8 +213,9 @@ void AccountManager::sendRequest(const QString& path, QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest; - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - + + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + QUrl requestURL = _authURL; if (path.startsWith("/")) { @@ -463,7 +465,7 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest request; - request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + request.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); QUrl grantURL = _authURL; grantURL.setPath("/oauth/token"); @@ -533,7 +535,7 @@ void AccountManager::requestProfile() { profileURL.setPath("/api/v1/user/profile"); QNetworkRequest profileRequest(profileURL); - profileRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + profileRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); profileRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, _accountInfo.getAccessToken().authorizationHeaderValue()); QNetworkReply* profileReply = networkAccessManager.get(profileRequest); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 7306eaef14..d3c855e001 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -52,10 +52,12 @@ Q_DECLARE_METATYPE(AccountManagerAuth::Type); const QByteArray ACCESS_TOKEN_AUTHORIZATION_HEADER = "Authorization"; -class AccountManager : public QObject { +using UserAgentGetter = std::function; + +class AccountManager : public QObject, public Dependency { Q_OBJECT public: - static AccountManager& getInstance(bool forceReset = false); + AccountManager(UserAgentGetter = []() -> QString { return HIGH_FIDELITY_USER_AGENT; }); Q_INVOKABLE void sendRequest(const QString& path, AccountManagerAuth::Type authType, @@ -112,7 +114,6 @@ private slots: void generateNewKeypair(bool isUserKeypair = true, const QUuid& domainID = QUuid()); private: - AccountManager(); AccountManager(AccountManager const& other) = delete; void operator=(AccountManager const& other) = delete; @@ -122,6 +123,8 @@ private: void passSuccessToCallback(QNetworkReply* reply); void passErrorToCallback(QNetworkReply* reply); + UserAgentGetter _userAgentGetter; + QUrl _authURL; QMap _pendingCallbackMap; From ce6ae4043277a6cd1204bd1d2cf1ed279637283f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 6 May 2016 15:59:59 -0700 Subject: [PATCH 0100/1237] Replace foreach use with for() --- interface/src/Application.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3fffc4d9f3..9ad24526c1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1074,15 +1074,13 @@ QString Application::getUserAgent() { // For each plugin, add to userAgent auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - foreach(auto dp, displayPlugins) { + for (auto& dp : displayPlugins) { if (dp->isActive() && dp->isHmd()) { userAgent += " " + formatPluginName(dp->getName()); } } - - // For each plugin, add to userAgent auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); - foreach(auto ip, inputPlugins) { + for (auto& ip : inputPlugins) { if (ip->isActive()) { userAgent += " " + formatPluginName(ip->getName()); } From 79ce64aa3afdf028cef04b697fc57a453bf9e287 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 10 May 2016 15:36:22 -0700 Subject: [PATCH 0101/1237] Fix compilation error in DomainServer.cpp --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index f37954c4e6..0aab6b7e31 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1209,7 +1209,7 @@ void DomainServer::sendHeartbeatToIceServer() { auto plaintext = QByteArray::fromRawData(_iceServerHeartbeatPacket->getPayload(), _iceServerHeartbeatPacket->getPayloadSize()); // generate a signature for the plaintext data in the packet - auto signature = accountManager.getAccountInfo().signPlaintext(plaintext); + auto signature = accountManager->getAccountInfo().signPlaintext(plaintext); // pack the signature with the data heartbeatDataStream << signature; From e622c17f6b184ec109098cbc98e3f743cf3124d2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 17 May 2016 15:22:52 -0700 Subject: [PATCH 0102/1237] Pull default UserAgentGetter out of func signature --- libraries/networking/src/AccountManager.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index d3c855e001..d9a5ad8d83 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -53,11 +53,12 @@ Q_DECLARE_METATYPE(AccountManagerAuth::Type); const QByteArray ACCESS_TOKEN_AUTHORIZATION_HEADER = "Authorization"; using UserAgentGetter = std::function; +auto DEFAULT_USER_AGENT_GETTER = []() -> QString { return HIGH_FIDELITY_USER_AGENT; }; class AccountManager : public QObject, public Dependency { Q_OBJECT public: - AccountManager(UserAgentGetter = []() -> QString { return HIGH_FIDELITY_USER_AGENT; }); + AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER); Q_INVOKABLE void sendRequest(const QString& path, AccountManagerAuth::Type authType, From 01944f51004dbe8ecf71557e2923e0afe3f55de0 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 17 May 2016 15:23:44 -0700 Subject: [PATCH 0103/1237] Remove unused DependencyManager include --- interface/src/scripting/AccountScriptingInterface.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp index 377dbe8e35..1328197195 100644 --- a/interface/src/scripting/AccountScriptingInterface.cpp +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -13,8 +13,6 @@ #include "AccountScriptingInterface.h" -#include - AccountScriptingInterface* AccountScriptingInterface::getInstance() { static AccountScriptingInterface sharedInstance; return &sharedInstance; From 64c89a5dde45d83fb785ce40d8e66b07bc69a204 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 18 May 2016 08:34:26 -0700 Subject: [PATCH 0104/1237] Fix AccountManager not compiling on Windows --- libraries/networking/src/AccountManager.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index d9a5ad8d83..89a2240bbb 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -53,7 +53,8 @@ Q_DECLARE_METATYPE(AccountManagerAuth::Type); const QByteArray ACCESS_TOKEN_AUTHORIZATION_HEADER = "Authorization"; using UserAgentGetter = std::function; -auto DEFAULT_USER_AGENT_GETTER = []() -> QString { return HIGH_FIDELITY_USER_AGENT; }; + +const auto DEFAULT_USER_AGENT_GETTER = []() -> QString { return HIGH_FIDELITY_USER_AGENT; }; class AccountManager : public QObject, public Dependency { Q_OBJECT From 135fa8c2aaa6fb8987d7fe5eda9b16518ecb8ec0 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 May 2016 15:41:21 -0700 Subject: [PATCH 0105/1237] Moved jointIndices transmission behind #ifdef --- libraries/avatars/src/AvatarData.cpp | 40 ++++++++++++---------------- libraries/avatars/src/AvatarData.h | 4 +++ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index abf3f52ed6..09ea34a5c4 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -611,10 +611,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // an earlier AvatarIdentity packet. Because if we do, we risk applying the joint data // the wrong bones, resulting in a twisted avatar, An un-animated avatar is preferable to this. bool skipJoints = false; +#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET if (_networkJointIndexMap.empty()) { - qCDebug(avatars) << "AJT: parseAvatarDataPacket _networkJointIndexMap.size = " << _networkJointIndexMap.size(); - skipJoints = false; + skipJoints = true; } +#endif int bytesOfValidity = (int)ceil((float)numJoints / (float)BITS_IN_BYTE); minPossibleSize += bytesOfValidity; @@ -984,29 +985,28 @@ void AvatarData::clearJointsData() { void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut) { QDataStream packetStream(data); - packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName >> identityOut.jointIndices; - // AJT: just got a new networkJointIndicesMap. - qCDebug(avatars) << "AJT: receiving identity.jointIndices.size = " << identityOut.jointIndices.size(); +#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET + packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName >> identityOut.jointIndices; +#else + packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName; +#endif } bool AvatarData::processAvatarIdentity(const Identity& identity) { bool hasIdentityChanged = false; - qCDebug(avatars) << "AJT: processAvatarIdentity got here!"; - + #ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET if (!_jointIndices.empty() && _networkJointIndexMap.empty() && !identity.jointIndices.empty()) { // build networkJointIndexMap from _jointIndices and networkJointIndices. _networkJointIndexMap.fill(identity.jointIndices.size(), -1); for (auto iter = identity.jointIndices.cbegin(); iter != identity.jointIndices.end(); ++iter) { int jointIndex = getJointIndex(iter.key()); _networkJointIndexMap[iter.value()] = jointIndex; - qCDebug(avatars) << "AJT: mapping " << iter.value() << " -> " << jointIndex; } } - - qCDebug(avatars) << "AJT: processAvatarIdentity got here!"; +#endif if (_firstSkeletonCheck || (identity.skeletonModelURL != _skeletonModelURL)) { setSkeletonModelURL(identity.skeletonModelURL); @@ -1033,10 +1033,11 @@ QByteArray AvatarData::identityByteArray() { QUrl emptyURL(""); const QUrl& urlToSend = _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; - // AJT: just got a sending networkJointIndices - qCDebug(avatars) << "AJT: sending _jointIndices.size = " << _jointIndices.size(); - +#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET identityStream << QUuid() << urlToSend << _attachmentData << _displayName << _jointIndices; +#else + identityStream << QUuid() << urlToSend << _attachmentData << _displayName; +#endif return identityData; } @@ -1141,8 +1142,6 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) { void AvatarData::setJointMappingsFromNetworkReply() { QNetworkReply* networkReply = static_cast(sender()); - qCDebug(avatars) << "AJT: GOT HERE! finished fst network request"; - QByteArray line; while (!(line = networkReply->readLine()).isEmpty()) { line = line.trimmed(); @@ -1180,8 +1179,6 @@ void AvatarData::setJointMappingsFromNetworkReply() { // now that we have the jointIndices send them to the AvatarMixer. sendIdentityPacket(); - qCDebug(avatars) << "AJT: _jointIndices.size = " << _jointIndices.size(); - networkReply->deleteLater(); } @@ -1208,8 +1205,6 @@ void AvatarData::sendIdentityPacket() { QByteArray identityData = identityByteArray(); - qCDebug(avatars) << "AJT: sendIdentityPacket() size = " << identityData.size(); - auto packetList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); packetList->write(identityData); nodeList->eachMatchingNode( @@ -1219,16 +1214,15 @@ void AvatarData::sendIdentityPacket() { [&](const SharedNodePointer& node) { nodeList->sendPacketList(std::move(packetList), *node); }); - - qCDebug(avatars) << "AJT: sendIdentityPacket() done!"; } void AvatarData::updateJointMappings() { _jointIndices.clear(); _jointNames.clear(); - _networkJointIndexMap.clear(); - qCDebug(avatars) << "AJT: GOT HERE! kicking off fst network request"; +#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET + _networkJointIndexMap.clear(); +#endif if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index feabfad544..5af66fae4e 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -285,7 +285,9 @@ public: QUrl skeletonModelURL; QVector attachmentData; QString displayName; +#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET QHash jointIndices; +#endif }; static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); @@ -378,7 +380,9 @@ protected: float _displayNameAlpha; QHash _jointIndices; ///< 1-based, since zero is returned for missing keys +#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET QVector _networkJointIndexMap; // maps network joint indices to local model joint indices. +#endif QStringList _jointNames; ///< in order of depth-first traversal quint64 _errorLogExpiry; ///< time in future when to log an error From e792e8eecfcf7e4565090720fd332c55d285f222 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 May 2016 16:05:16 -0700 Subject: [PATCH 0106/1237] Fix for identity packet pong --- libraries/avatars/src/AvatarData.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 09ea34a5c4..e5fe31217f 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -606,7 +606,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // joint rotations int numJoints = *sourceBuffer++; - // do not process any jointData until we've received a valid jointIndices hash from // an earlier AvatarIdentity packet. Because if we do, we risk applying the joint data // the wrong bones, resulting in a twisted avatar, An un-animated avatar is preferable to this. @@ -1034,22 +1033,21 @@ QByteArray AvatarData::identityByteArray() { const QUrl& urlToSend = _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; #ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - identityStream << QUuid() << urlToSend << _attachmentData << _displayName << _jointIndices; + identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName << _jointIndices; #else - identityStream << QUuid() << urlToSend << _attachmentData << _displayName; + identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName; #endif return identityData; } - void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { const QUrl& expanded = skeletonModelURL.isEmpty() ? AvatarData::defaultFullAvatarModelUrl() : skeletonModelURL; if (expanded == _skeletonModelURL) { return; } _skeletonModelURL = expanded; - qCDebug(avatars) << "Changing skeleton model for avatar to" << _skeletonModelURL.toString(); + qCDebug(avatars) << "Changing skeleton model for avatar" << getSessionUUID() << "to" << _skeletonModelURL.toString(); updateJointMappings(); } From eb80990c1096247c8dd191e7dcd5e7e798ebeaa8 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 May 2016 16:08:56 -0700 Subject: [PATCH 0107/1237] Another fix for avatarIdentity pong --- libraries/avatars/src/AvatarData.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index e5fe31217f..f956305c3f 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1174,9 +1174,6 @@ void AvatarData::setJointMappingsFromNetworkReply() { _jointIndices.insert(_jointNames.at(i), i + 1); } - // now that we have the jointIndices send them to the AvatarMixer. - sendIdentityPacket(); - networkReply->deleteLater(); } From b4df7cada73dc8f8b5ec6b7ea52af38fc3424100 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 18 May 2016 16:16:31 -0700 Subject: [PATCH 0108/1237] don't mouse-grab things through an overlay --- scripts/system/controllers/grab.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index a7e08451bc..ef39e95880 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -316,7 +316,12 @@ Grabber.prototype.pressEvent = function(event) { return; } - if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) { + if (event.isLeftButton !== true || event.isRightButton === true || event.isMiddleButton === true) { + return; + } + + if (Overlays.getOverlayAtPoint(Reticle.position) > 0) { + // the mouse is pointing at an overlay; don't look for entities underneath the overlay. return; } From c480dcfddd740a939c60e0ef477256591612e8b2 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 16:26:54 -0700 Subject: [PATCH 0109/1237] Check thread validity after event processing --- libraries/script-engine/src/ScriptEngine.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 15e896fac4..5f68dcfbf8 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -294,13 +294,6 @@ void ScriptEngine::waitTillDoneRunning() { auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { - // NOTE: This will be called on the main application thread from stopAllScripts. - // The application thread will need to continue to process events, because - // the scripts will likely need to marshall messages across to the main thread, e.g. - // if they access Settings or Menu in any of their shutdown code. So: - // Process events for the main application thread, allowing invokeMethod calls to pass between threads. - QCoreApplication::processEvents(); - // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; @@ -326,6 +319,17 @@ void ScriptEngine::waitTillDoneRunning() { } } + // NOTE: This will be called on the main application thread from stopAllScripts. + // The application thread will need to continue to process events, because + // the scripts will likely need to marshall messages across to the main thread, e.g. + // if they access Settings or Menu in any of their shutdown code. So: + // Process events for the main application thread, allowing invokeMethod calls to pass between threads. + QCoreApplication::processEvents(); + // In some cases (debugging), processEvents may give the thread enough time to shut down, so recheck it. + if (!thread()) { + break; + } + // Avoid a pure busy wait QThread::yieldCurrentThread(); } From 862a88cf292cdb2cd1e6712cf156db0d6793f427 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 11:29:21 +1200 Subject: [PATCH 0110/1237] Insert an intermediary ListModel suitable for sorting and drives --- .../resources/qml/dialogs/FileDialog.qml | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 4f6e7ef050..fbe8df79c0 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -184,7 +184,7 @@ ModalWindow { return; } - currentSelectionUrl = fileTableView.model.get(row, "fileURL"); + currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); currentSelectionIsFolder = fileTableView.model.isFolder(row); if (root.selectDirectory || !currentSelectionIsFolder) { currentSelection.text = helper.urlToPath(currentSelectionUrl); @@ -220,12 +220,49 @@ ModalWindow { showFiles = !root.selectDirectory } onFolderChanged: { + fileTableModel.update(); fileTableView.selection.clear(); fileTableView.selection.select(0); fileTableView.currentRow = 0; } } + ListModel { + id: fileTableModel + + // FolderListModel has a couple of problems: + // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 + // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 + // + // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with + // drive information when viewing at the computer level. + + property var folder + + onFolderChanged: folderListModel.folder = folder; + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function update() { + var i; + clear(); + for (i = 0; i < folderListModel.count; i++) { + append({ + fileName: folderListModel.get(i, "fileName"), + fileModified: folderListModel.get(i, "fileModified"), + fileSize: folderListModel.get(i, "fileSize"), + filePath: folderListModel.get(i, "filePath"), + fileIsDir: folderListModel.get(i, "fileIsDir") + }); + } + } + } + Table { id: fileTableView colorScheme: hifi.colorSchemes.light @@ -247,7 +284,7 @@ ModalWindow { sortIndicatorOrder: Qt.AscendingOrder sortIndicatorVisible: true - model: folderListModel + model: fileTableModel function updateSort() { model.sortReversed = sortIndicatorColumn == 0 @@ -284,12 +321,17 @@ ModalWindow { } size: hifi.fontSizes.tableText color: hifi.colors.baseGrayHighlight - font.family: fileTableView.model.get(styleData.row, "fileIsDir") ? firaSansSemiBold.name : firaSansRegular.name + font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) + ? firaSansSemiBold.name : firaSansRegular.name function getText() { + if (styleData.row === -1) { + return styleData.value; + } + switch (styleData.column) { - case 1: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; - case 2: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); + case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); default: return styleData.value; } } @@ -343,9 +385,9 @@ ModalWindow { function navigateToCurrentRow() { var row = fileTableView.currentRow var isFolder = model.isFolder(row); - var file = model.get(row, "fileURL"); + var file = model.get(row).filePath; if (isFolder) { - fileTableView.model.folder = file; + fileTableView.model.folder = helper.pathToUrl(file); } else { okAction.trigger(); } @@ -360,7 +402,7 @@ ModalWindow { var newPrefix = prefix + event.text.toLowerCase(); var matchedIndex = -1; for (var i = 0; i < model.count; ++i) { - var name = model.get(i, "fileName").toLowerCase(); + var name = model.get(i).fileName.toLowerCase(); if (0 === name.indexOf(newPrefix)) { matchedIndex = i; break; @@ -401,7 +443,6 @@ ModalWindow { } break; } - } } From 0294066668af90f684cffe1bf852b238a8549e1e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 May 2016 16:41:50 -0700 Subject: [PATCH 0111/1237] Removed pupilData and translationRadix from AvatarData packet. --- interface/src/avatar/MyAvatar.cpp | 4 ---- interface/src/ui/PreferencesDialog.cpp | 5 ----- libraries/avatars/src/AvatarData.cpp | 28 +++++++------------------- libraries/avatars/src/HeadData.cpp | 6 ++---- libraries/avatars/src/HeadData.h | 22 +++++++++----------- 5 files changed, 18 insertions(+), 47 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ddc0407f14..6d1d80b7f6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -670,8 +670,6 @@ void MyAvatar::saveData() { settings.setValue("headPitch", getHead()->getBasePitch()); - settings.setValue("pupilDilation", getHead()->getPupilDilation()); - settings.setValue("leanScale", _leanScale); settings.setValue("scale", _targetScale); @@ -778,8 +776,6 @@ void MyAvatar::loadData() { getHead()->setBasePitch(loadSetting(settings, "headPitch", 0.0f)); - getHead()->setPupilDilation(loadSetting(settings, "pupilDilation", 0.0f)); - _leanScale = loadSetting(settings, "leanScale", 0.05f); _targetScale = loadSetting(settings, "scale", 1.0f); setScale(glm::vec3(_targetScale)); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 9b1146340e..cb68b36c24 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -144,11 +144,6 @@ void setupPreferences() { preference->setStep(1); preferences->addPreference(preference); } - { - auto getter = [=]()->float { return myAvatar->getHead()->getPupilDilation(); }; - auto setter = [=](float value) { myAvatar->getHead()->setPupilDilation(value); }; - preferences->addPreference(new SliderPreference(AVATAR_TUNING, "Pupil dilation", getter, setter)); - } { auto getter = []()->float { return DependencyManager::get()->getEyeClosingThreshold(); }; auto setter = [](float value) { DependencyManager::get()->setEyeClosingThreshold(value); }; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index f956305c3f..9d010e3dc4 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -219,9 +219,6 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { destinationBuffer += _headData->_blendshapeCoefficients.size() * sizeof(float); } - // pupil dilation - destinationBuffer += packFloatToByte(destinationBuffer, _headData->_pupilDilation, 1.0f); - // joint rotation data *destinationBuffer++ = _jointData.size(); unsigned char* validityPosition = destinationBuffer; @@ -306,15 +303,11 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { } } - if (validityBit != 0) { *destinationBuffer++ = validity; } - // TODO -- automatically pick translationCompressionRadix - int translationCompressionRadix = 12; - - *destinationBuffer++ = translationCompressionRadix; + const int TRANSLATION_COMPRESSION_RADIX = 12; validityBit = 0; validity = *validityPosition++; @@ -322,7 +315,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { const JointData& data = _jointData[ i ]; if (validity & (1 << validityBit)) { destinationBuffer += - packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, translationCompressionRadix); + packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); } if (++validityBit == BITS_IN_BYTE) { validityBit = 0; @@ -335,7 +328,6 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { qDebug() << "AvatarData::toByteArray" << cullSmallChanges << sendAll << "rotations:" << rotationSentCount << "translations:" << translationSentCount << "largest:" << maxTranslationDimension - << "radix:" << translationCompressionRadix << "size:" << (beforeRotations - startPosition) << "+" << (beforeTranslations - beforeRotations) << "+" @@ -408,7 +400,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // audioLoudness = 4 // } // + 1 byte for varying data - // + 1 byte for pupilSize // + 1 byte for numJoints (0) // = 39 bytes int minPossibleSize = 39; @@ -598,11 +589,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } } // 1 + bitItemsDataSize bytes - { // pupil dilation - sourceBuffer += unpackFloatFromByte(sourceBuffer, _headData->_pupilDilation, 1.0f); - } // 1 byte - - // joint rotations int numJoints = *sourceBuffer++; @@ -650,6 +636,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } // 1 + bytesOfValidity bytes // each joint rotation is stored in 6 bytes. + const size_t COMPRESSED_QUATERNION_SIZE = 6; minPossibleSize += numValidJointRotations * COMPRESSED_QUATERNION_SIZE; if (minPossibleSize > maxAvailableSize) { @@ -699,9 +686,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } } // 1 + bytesOfValidity bytes - // each joint translation component is stored in 6 bytes. 1 byte for translationCompressionRadix + // each joint translation component is stored in 6 bytes. const size_t COMPRESSED_TRANSLATION_SIZE = 6; - minPossibleSize += numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE + 1; + minPossibleSize += numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE; if (minPossibleSize > maxAvailableSize) { if (shouldLogError(now)) { qCDebug(avatars) << "Malformed AvatarData packet after JointData translation validity;" @@ -712,7 +699,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { return maxAvailableSize; } - int translationCompressionRadix = *sourceBuffer++; + const int TRANSLATION_COMPRESSION_RADIX = 12; { // joint data for (int i = 0; i < numJoints; i++) { @@ -721,7 +708,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { if (skipJoints) { sourceBuffer += COMPRESSED_TRANSLATION_SIZE; } else { - sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, translationCompressionRadix); + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); _hasNewJointTranslations = true; data.translationSet = true; } @@ -733,7 +720,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { if (numValidJointRotations > 15) { qDebug() << "RECEIVING -- rotations:" << numValidJointRotations << "translations:" << numValidJointTranslations - << "radix:" << translationCompressionRadix << "size:" << (int)(sourceBuffer - startPosition); } #endif diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index b98112d6e0..1aee85b2cd 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -43,10 +43,9 @@ HeadData::HeadData(AvatarData* owningAvatar) : _averageLoudness(0.0f), _browAudioLift(0.0f), _audioAverageLoudness(0.0f), - _pupilDilation(0.0f), _owningAvatar(owningAvatar) { - + } glm::quat HeadData::getRawOrientation() const { @@ -72,7 +71,7 @@ void HeadData::setOrientation(const glm::quat& orientation) { glm::vec3 newFront = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FRONT); bodyOrientation = bodyOrientation * glm::angleAxis(atan2f(-newFront.x, -newFront.z), glm::vec3(0.0f, 1.0f, 0.0f)); _owningAvatar->setOrientation(bodyOrientation); - + // the rest goes to the head glm::vec3 eulers = glm::degrees(safeEulerAngles(glm::inverse(bodyOrientation) * orientation)); _basePitch = eulers.x; @@ -186,4 +185,3 @@ void HeadData::fromJson(const QJsonObject& json) { } } } - diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index fef77c6f8f..535aa12847 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -34,7 +34,7 @@ class HeadData { public: explicit HeadData(AvatarData* owningAvatar); virtual ~HeadData() { }; - + // degrees float getBaseYaw() const { return _baseYaw; } void setBaseYaw(float yaw) { _baseYaw = glm::clamp(yaw, MIN_HEAD_YAW, MAX_HEAD_YAW); } @@ -42,7 +42,7 @@ public: void setBasePitch(float pitch) { _basePitch = glm::clamp(pitch, MIN_HEAD_PITCH, MAX_HEAD_PITCH); } float getBaseRoll() const { return _baseRoll; } void setBaseRoll(float roll) { _baseRoll = glm::clamp(roll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); } - + virtual void setFinalYaw(float finalYaw) { _baseYaw = finalYaw; } virtual void setFinalPitch(float finalPitch) { _basePitch = finalPitch; } virtual void setFinalRoll(float finalRoll) { _baseRoll = finalRoll; } @@ -64,26 +64,23 @@ public: void setBlendshape(QString name, float val); const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } void setBlendshapeCoefficients(const QVector& blendshapeCoefficients) { _blendshapeCoefficients = blendshapeCoefficients; } - - float getPupilDilation() const { return _pupilDilation; } - void setPupilDilation(float pupilDilation) { _pupilDilation = pupilDilation; } - + const glm::vec3& getLookAtPosition() const { return _lookAtPosition; } void setLookAtPosition(const glm::vec3& lookAtPosition) { _lookAtPosition = lookAtPosition; } - - + + float getLeanSideways() const { return _leanSideways; } float getLeanForward() const { return _leanForward; } float getTorsoTwist() const { return _torsoTwist; } virtual float getFinalLeanSideways() const { return _leanSideways; } virtual float getFinalLeanForward() const { return _leanForward; } - + void setLeanSideways(float leanSideways) { _leanSideways = leanSideways; } void setLeanForward(float leanForward) { _leanForward = leanForward; } void setTorsoTwist(float torsoTwist) { _torsoTwist = torsoTwist; } - + friend class AvatarData; - + QJsonObject toJson() const; void fromJson(const QJsonObject& json); @@ -106,9 +103,8 @@ protected: float _browAudioLift; float _audioAverageLoudness; QVector _blendshapeCoefficients; - float _pupilDilation; AvatarData* _owningAvatar; - + private: // privatize copy ctor and assignment operator so copies of this object cannot be made HeadData(const HeadData&); From 8025a3f14cb7602204c02e08a7c5cafaa1ab2eb5 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 16:35:07 -0700 Subject: [PATCH 0112/1237] Prevent crash from script timers on shutdown --- libraries/script-engine/src/ScriptEngine.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 15e896fac4..67b87ae5e0 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -979,6 +979,11 @@ void ScriptEngine::updateMemoryCost(const qint64& deltaSize) { } void ScriptEngine::timerFired() { + if (DependencyManager::get()->isStopped()) { + qCDebug(scriptengine) << "Script.timerFired() while shutting down is ignored... parent script:" << getFilename(); + return; // bail early + } + QTimer* callingTimer = reinterpret_cast(sender()); CallbackData timerData = _timerFunctionMap.value(callingTimer); From 2c9b0f7233f00938addc2031b84972f865a30d64 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 17:31:47 -0700 Subject: [PATCH 0113/1237] Rm failing VrMenu assertion --- libraries/ui/src/VrMenu.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 2d6113ad63..a0a1399c78 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -236,5 +236,4 @@ void VrMenu::removeAction(QAction* action) { QQuickMenuBase* qmlItem = reinterpret_cast(item); bool invokeResult = QMetaObject::invokeMethod(menu, "removeItem", Qt::DirectConnection, Q_ARG(QQuickMenuBase*, qmlItem)); - Q_ASSERT(invokeResult); } From 107b1b830eb156077546016e2042ad0867a24679 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 17:46:12 -0700 Subject: [PATCH 0114/1237] Stop ScriptCache callbacks when scripts are stopped --- libraries/script-engine/src/ScriptCache.cpp | 43 ++++++++++++--------- libraries/script-engine/src/ScriptEngines.h | 2 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 3ebd3d53ce..2114289095 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "ScriptCache.h" + #include #include #include @@ -20,7 +22,7 @@ #include #include -#include "ScriptCache.h" +#include "ScriptEngines.h" #include "ScriptEngineLogging.h" ScriptCache::ScriptCache(QObject* parent) { @@ -78,22 +80,25 @@ void ScriptCache::scriptDownloaded() { QList scriptUsers = _scriptUsers.values(url); _scriptUsers.remove(url); - if (req->getResult() == ResourceRequest::Success) { - auto scriptContents = req->getData(); - _scriptCache[url] = scriptContents; - lock.unlock(); - qCDebug(scriptengine) << "Done downloading script at:" << url.toString(); + if (!DependencyManager::get()->isStopped()) { + if (req->getResult() == ResourceRequest::Success) { + auto scriptContents = req->getData(); + _scriptCache[url] = scriptContents; + lock.unlock(); + qCDebug(scriptengine) << "Done downloading script at:" << url.toString(); - foreach(ScriptUser* user, scriptUsers) { - user->scriptContentsAvailable(url, scriptContents); - } - } else { - lock.unlock(); - qCWarning(scriptengine) << "Error loading script from URL " << url; - foreach(ScriptUser* user, scriptUsers) { - user->errorInLoadingScript(url); + foreach(ScriptUser* user, scriptUsers) { + user->scriptContentsAvailable(url, scriptContents); + } + } else { + lock.unlock(); + qCWarning(scriptengine) << "Error loading script from URL " << url; + foreach(ScriptUser* user, scriptUsers) { + user->errorInLoadingScript(url); + } } } + req->deleteLater(); } @@ -162,9 +167,11 @@ void ScriptCache::scriptContentAvailable() { } } - foreach(contentAvailableCallback thisCallback, allCallbacks) { - thisCallback(url.toString(), scriptContent, true, success); - } req->deleteLater(); -} + if (!DependencyManager::get()->isStopped()) { + foreach(contentAvailableCallback thisCallback, allCallbacks) { + thisCallback(url.toString(), scriptContent, true, success); + } + } +} diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 72bf7d529e..a9c273b2a7 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -68,6 +68,7 @@ public: // Called at shutdown time void shutdownScripting(); + bool isStopped() const { return _isStopped; } signals: void scriptCountChanged(); @@ -86,7 +87,6 @@ protected: void onScriptEngineLoaded(const QString& scriptFilename); void onScriptEngineError(const QString& scriptFilename); void launchScriptEngine(ScriptEngine* engine); - bool isStopped() const { return _isStopped; } QReadWriteLock _scriptEnginesHashLock; QHash _scriptEnginesHash; From de1204c42d9bff5b4496da56595901fbbe62a5d3 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 May 2016 18:19:28 -0700 Subject: [PATCH 0115/1237] Enable transmission of jointIndices --- libraries/avatars/src/AvatarData.cpp | 10 +++++++--- libraries/avatars/src/AvatarData.h | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9d010e3dc4..b70c021f61 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -37,7 +37,7 @@ #include "AvatarLogging.h" -#define WANT_DEBUG +//#define WANT_DEBUG quint64 DEFAULT_FILTERED_LOG_EXPIRY = 2 * USECS_PER_SECOND; @@ -984,11 +984,15 @@ bool AvatarData::processAvatarIdentity(const Identity& identity) { #ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET if (!_jointIndices.empty() && _networkJointIndexMap.empty() && !identity.jointIndices.empty()) { + // build networkJointIndexMap from _jointIndices and networkJointIndices. - _networkJointIndexMap.fill(identity.jointIndices.size(), -1); + _networkJointIndexMap.fill(-1, identity.jointIndices.size()); for (auto iter = identity.jointIndices.cbegin(); iter != identity.jointIndices.end(); ++iter) { int jointIndex = getJointIndex(iter.key()); - _networkJointIndexMap[iter.value()] = jointIndex; + int networkJointIndex = iter.value(); + if (networkJointIndex >= 0 && networkJointIndex < identity.jointIndices.size()) { + _networkJointIndexMap[networkJointIndex - 1] = jointIndex; + } } } #endif diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 5af66fae4e..1a1a07410d 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -128,6 +128,8 @@ enum KeyState { DELETE_KEY_DOWN }; +#define TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET + class QDataStream; class AttachmentData; From 87b102ae0f0c1ebc7e2b771fd9f1b899271b0f99 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 18:19:44 -0700 Subject: [PATCH 0116/1237] Fix -Wunused-result from invokeResult --- libraries/ui/src/VrMenu.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index a0a1399c78..c2732197d3 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -234,6 +234,5 @@ void VrMenu::removeAction(QAction* action) { QObject* menu = item->parent(); // Proxy QuickItem requests through the QML layer QQuickMenuBase* qmlItem = reinterpret_cast(item); - bool invokeResult = QMetaObject::invokeMethod(menu, "removeItem", Qt::DirectConnection, - Q_ARG(QQuickMenuBase*, qmlItem)); + QMetaObject::invokeMethod(menu, "removeItem", Qt::DirectConnection, Q_ARG(QQuickMenuBase*, qmlItem)); } From e1971f5ed34f7bb692e49d1c53066277149cc2a9 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 18:58:28 -0700 Subject: [PATCH 0117/1237] Fix shadow bounds checking --- libraries/render-utils/src/RenderShadowTask.cpp | 9 ++++++--- libraries/render-utils/src/Shadow.slh | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index f695e2d04c..5680660122 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -52,17 +52,19 @@ void RenderShadowMap::run(const render::SceneContextPointer& sceneContext, const batch.setFramebuffer(fbo); batch.clearFramebuffer( gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_DEPTH, - vec4(vec3(1.0, 1.0, 1.0), 1.0), 1.0, 0, true); + vec4(vec3(1.0, 1.0, 1.0), 0.0), 1.0, 0, true); batch.setProjectionTransform(shadow.getProjection()); batch.setViewTransform(shadow.getView()); auto shadowPipeline = _shapePlumber->pickPipeline(args, ShapeKey()); auto shadowSkinnedPipeline = _shapePlumber->pickPipeline(args, ShapeKey::Builder().withSkinned()); - args->_pipeline = shadowPipeline; - batch.setPipeline(shadowPipeline->pipeline); std::vector skinnedShapeKeys{}; + + // Iterate through all inShapes and render the unskinned + args->_pipeline = shadowPipeline; + batch.setPipeline(shadowPipeline->pipeline); for (auto items : inShapes) { if (items.first.isSkinned()) { skinnedShapeKeys.push_back(items.first); @@ -71,6 +73,7 @@ void RenderShadowMap::run(const render::SceneContextPointer& sceneContext, const } } + // Reiterate to render the skinned args->_pipeline = shadowSkinnedPipeline; batch.setPipeline(shadowSkinnedPipeline->pipeline); for (const auto& key : skinnedShapeKeys) { diff --git a/libraries/render-utils/src/Shadow.slh b/libraries/render-utils/src/Shadow.slh index 43a20057cd..7b86b9b660 100755 --- a/libraries/render-utils/src/Shadow.slh +++ b/libraries/render-utils/src/Shadow.slh @@ -87,7 +87,8 @@ float evalShadowAttenuationPCF(vec4 position, vec4 shadowTexcoord) { float evalShadowAttenuation(vec4 position) { vec4 shadowTexcoord = evalShadowTexcoord(position); if (shadowTexcoord.x < 0.0 || shadowTexcoord.x > 1.0 || - shadowTexcoord.y < 0.0 || shadowTexcoord.y > 1.0) { + shadowTexcoord.y < 0.0 || shadowTexcoord.y > 1.0 || + shadowTexcoord.z < 0.0 || shadowTexcoord.z > 1.0) { // If a point is not in the map, do not attenuate return 1.0; } From 917fe2ae6a0154ac16a1b3b8fcd6d5ea947218f0 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 18 May 2016 19:09:25 -0700 Subject: [PATCH 0118/1237] Give AC audio searcher invalid avatar URL --- .../audioExamples/acAudioSearching/ACAudioSearchAndInject.js | 1 + 1 file changed, 1 insertion(+) diff --git a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index de7baba267..381a7ee902 100644 --- a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -40,6 +40,7 @@ var DEFAULT_SOUND_DATA = { Script.include("../../libraries/utils.js"); Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList. +Avatar.skeletonModelURL = "http://invalid-url"; function ignore() {} function debug() { // Display the arguments not just [Object object]. //print.apply(null, [].map.call(arguments, JSON.stringify)); From 585467094c58e177c59e9da30b18e5b77bcf8d7b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 14:16:00 +1200 Subject: [PATCH 0119/1237] Sort files case-insensitively, directories first --- .../resources/qml/dialogs/FileDialog.qml | 64 ++++++++++++++++--- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index fbe8df79c0..e7d96cedf9 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -238,6 +238,8 @@ ModalWindow { // drive information when viewing at the computer level. property var folder + property int sortOrder: Qt.AscendingOrder + property int sortColumn: 0 onFolderChanged: folderListModel.folder = folder; @@ -249,16 +251,61 @@ ModalWindow { } function update() { - var i; + var dataFields = ["fileName", "fileModified", "fileSize"], + sortFields = ["fileNameSort", "fileModified", "fileSize"], + dataField = dataFields[sortColumn], + sortField = sortFields[sortColumn], + sortValue, + fileName, + fileIsDir, + comparisonFunction, + lower, + middle, + upper, + rows = 0, + i; clear(); + + comparisonFunction = sortOrder === Qt.AscendingOrder + ? function(a, b) { return a < b; } + : function(a, b) { return a > b; } + for (i = 0; i < folderListModel.count; i++) { - append({ - fileName: folderListModel.get(i, "fileName"), - fileModified: folderListModel.get(i, "fileModified"), + fileName = folderListModel.get(i, "fileName"); + fileIsDir = folderListModel.get(i, "fileIsDir"); + + sortValue = folderListModel.get(i, dataField); + + if (dataField === "fileName") { + // Directories first by prefixing a "*". + // Case-insensitive. + sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); + } + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (comparisonFunction(sortValue, get(middle)[sortField])) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + fileModified: (fileIsDir ? new Date(0) : folderListModel.get(i, "fileModified")), fileSize: folderListModel.get(i, "fileSize"), filePath: folderListModel.get(i, "filePath"), - fileIsDir: folderListModel.get(i, "fileIsDir") + fileIsDir: fileIsDir, + fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() }); + + rows++; } } } @@ -287,10 +334,9 @@ ModalWindow { model: fileTableModel function updateSort() { - model.sortReversed = sortIndicatorColumn == 0 - ? (sortIndicatorOrder == Qt.DescendingOrder) - : (sortIndicatorOrder == Qt.AscendingOrder); // Date and size fields have opposite sense - model.sortField = sortIndicatorColumn + 1; + model.sortOrder = sortIndicatorOrder; + model.sortColumn = sortIndicatorColumn; + model.update(); } onSortIndicatorColumnChanged: { updateSort(); } From 41311bc3df3859be96291d7a06fb44605f98146b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 17:13:07 +1200 Subject: [PATCH 0120/1237] Finish decoupling FolderListModel --- .../resources/qml/dialogs/FileDialog.qml | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index e7d96cedf9..5674123b4d 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -43,7 +43,7 @@ ModalWindow { // Set from OffscreenUi::getOpenFile() property alias caption: root.title; // Set from OffscreenUi::getOpenFile() - property alias dir: folderListModel.folder; + property alias dir: fileTableModel.folder; // Set from OffscreenUi::getOpenFile() property alias filter: selectionType.filtersString; // Set from OffscreenUi::getOpenFile() @@ -75,8 +75,9 @@ ModalWindow { // HACK: The following two lines force the model to initialize properly such that: // - Selecting a different drive at the initial screen updates the path displayed. // - The go-up button works properly from the initial screen. + var initialFolder = currentDirectory.lastValidFolder; root.dir = helper.pathToUrl(drivesSelector.currentText); - root.dir = helper.pathToUrl(currentDirectory.lastValidFolder); + root.dir = helper.pathToUrl(initialFolder); iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } @@ -110,7 +111,7 @@ ModalWindow { glyph: hifi.glyphs.levelUp width: height size: 30 - enabled: folderListModel.parentFolder && folderListModel.parentFolder !== "" + enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" onClicked: d.navigateUp(); } @@ -135,7 +136,7 @@ ModalWindow { TextField { id: currentDirectory - property var lastValidFolder: helper.urlToPath(folderListModel.folder) + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) height: homeButton.height anchors { top: parent.top @@ -161,7 +162,7 @@ ModalWindow { text = lastValidFolder; return } - folderListModel.folder = helper.pathToUrl(text); + fileTableModel.folder = helper.pathToUrl(text); } } @@ -172,15 +173,13 @@ ModalWindow { property bool currentSelectionIsFolder; property var backStack: [] property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } - property var modelConnection: Connections { target: model; onFolderChanged: d.update(); } // DJRTODO + property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } property var homeDestination: helper.home(); - Component.onCompleted: update(); function update() { var row = fileTableView.currentRow; - if (row === -1 && root.selectDirectory) { - currentSelectionUrl = fileTableView.model.folder; - currentSelectionIsFolder = true; + + if (row === -1) { return; } @@ -189,19 +188,21 @@ ModalWindow { if (root.selectDirectory || !currentSelectionIsFolder) { currentSelection.text = helper.urlToPath(currentSelectionUrl); } else { - currentSelection.text = "" + currentSelection.text = ""; } } function navigateUp() { - if (folderListModel.parentFolder && folderListModel.parentFolder !== "") { - folderListModel.folder = folderListModel.parentFolder + if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { + fileTableModel.folder = fileTableModel.parentFolder return true; + } else if (true) { + } } function navigateHome() { - folderListModel.folder = homeDestination; + fileTableModel.folder = homeDestination; return true; } } @@ -215,15 +216,11 @@ ModalWindow { // For some reason, declaring these bindings directly in the targets doesn't // work for setting the initial state Component.onCompleted: { - currentDirectory.lastValidFolder = Qt.binding(function() { return helper.urlToPath(folder); }); - upButton.enabled = Qt.binding(function() { return (parentFolder && parentFolder != "") ? true : false; }); + currentDirectory.lastValidFolder = helper.urlToPath(folder); showFiles = !root.selectDirectory } onFolderChanged: { - fileTableModel.update(); - fileTableView.selection.clear(); - fileTableView.selection.select(0); - fileTableView.currentRow = 0; + fileTableModel.update(); // Update once the data from the folder change is available. } } @@ -240,8 +237,12 @@ ModalWindow { property var folder property int sortOrder: Qt.AscendingOrder property int sortColumn: 0 + property string parentFolder: folderListModel.parentFolder - onFolderChanged: folderListModel.folder = folder; + onFolderChanged: { + folderListModel.folder = folder; + currentDirectory.lastValidFolder = helper.urlToPath(folder); + } function isFolder(row) { if (row === -1) { From 7efcad38d2a0816fdac09fdccfd97b601b877097 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 18 May 2016 22:29:07 -0700 Subject: [PATCH 0121/1237] PR feedback --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 8 ++-- libraries/gpu-gl/src/gpu/gl/GLQuery.cpp | 12 ------ libraries/gpu-gl/src/gpu/gl/GLShared.h | 8 ---- libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 2 +- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 7 ++++ .../gpu-gl/src/gpu/gl/GLTextureTransfer.cpp | 1 + .../gpu-gl/src/gpu/gl/GLTextureTransfer.h | 1 - .../gl41/{GLBackend.cpp => GL41Backend.cpp} | 14 +++---- .../gpu/gl41/{GLBackend.h => GL41Backend.h} | 18 ++++----- ...ackendBuffer.cpp => GL41BackendBuffer.cpp} | 14 +++---- ...LBackendInput.cpp => GL41BackendInput.cpp} | 10 ++--- ...ackendOutput.cpp => GL41BackendOutput.cpp} | 22 +++++------ ...LBackendQuery.cpp => GL41BackendQuery.cpp} | 16 ++++---- ...kendTexture.cpp => GL41BackendTexture.cpp} | 39 +++++++++---------- ...Transform.cpp => GL41BackendTransform.cpp} | 8 ++-- libraries/gpu/src/gpu/Forward.h | 4 +- 16 files changed, 84 insertions(+), 100 deletions(-) delete mode 100644 libraries/gpu-gl/src/gpu/gl/GLQuery.cpp rename libraries/gpu-gl/src/gpu/gl41/{GLBackend.cpp => GL41Backend.cpp} (93%) rename libraries/gpu-gl/src/gpu/gl41/{GLBackend.h => GL41Backend.h} (86%) rename libraries/gpu-gl/src/gpu/gl41/{GLBackendBuffer.cpp => GL41BackendBuffer.cpp} (81%) rename libraries/gpu-gl/src/gpu/gl41/{GLBackendInput.cpp => GL41BackendInput.cpp} (96%) rename libraries/gpu-gl/src/gpu/gl41/{GLBackendOutput.cpp => GL41BackendOutput.cpp} (86%) rename libraries/gpu-gl/src/gpu/gl41/{GLBackendQuery.cpp => GL41BackendQuery.cpp} (62%) rename libraries/gpu-gl/src/gpu/gl41/{GLBackendTexture.cpp => GL41BackendTexture.cpp} (86%) rename libraries/gpu-gl/src/gpu/gl41/{GLBackendTransform.cpp => GL41BackendTransform.cpp} (95%) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index e5460c6641..4d264995ae 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -16,7 +16,7 @@ #include #include -#include "../gl41/GLBackend.h" +#include "../gl41/GL41Backend.h" #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" @@ -36,11 +36,11 @@ static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); static bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); Backend* GLBackend::createBackend() { - auto version = QOpenGLContextWrapper::currentContextVersion(); +#if 0 // FIXME provide a mechanism to override the backend for testing // Where the gpuContext is initialized and where the TRUE Backend is created and assigned -#if 0 + auto version = QOpenGLContextWrapper::currentContextVersion(); GLBackend* result; if (enableOpenGL45 && version >= 0x0405) { result = new gpu::gl45::GLBackend; @@ -48,7 +48,7 @@ Backend* GLBackend::createBackend() { result = new gpu::gl41::GLBackend; } #else - GLBackend* result = new gpu::gl41::GLBackend; + GLBackend* result = new gpu::gl41::GL41Backend; #endif result->initInput(); result->initTransform(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLQuery.cpp deleted file mode 100644 index b358db40c9..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLQuery.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/05/15 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "GLQuery.h" - -using namespace gpu; -using namespace gpu::gl; diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.h b/libraries/gpu-gl/src/gpu/gl/GLShared.h index 5d90badc1c..3220eafef4 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.h +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.h @@ -128,13 +128,6 @@ public: virtual ~GLObject() { } - // Used by derived classes and helpers to ensure the actual GL object exceeds the lifetime of `this` - GLuint takeOwnership() { - GLuint result = _id; - const_cast(_id) = 0; - return result; - } - const GPUType& _gpuObject; const GLuint _id; }; @@ -146,7 +139,6 @@ class GLQuery; class GLState; class GLShader; class GLTexture; -class GLTextureTransferHelper; } } // namespace gpu::gl diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 47fc583d77..ed931437b7 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -182,7 +182,7 @@ GLTexture::~GLTexture() { if (0 == numTexturesForMipCount) { _textureCountByMips.erase(mipCount); if (mipCount == _currentMaxMipCount) { - _currentMaxMipCount = _textureCountByMips.rbegin()->first; + _currentMaxMipCount = (_textureCountByMips.empty() ? 0 : _textureCountByMips.rbegin()->first); } } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index fa09fb49f2..df2d38e2f3 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -102,6 +102,13 @@ public: return result; } + // Used by derived classes and helpers to ensure the actual GL object exceeds the lifetime of `this` + GLuint takeOwnership() { + GLuint result = _id; + const_cast(_id) = 0; + return result; + } + ~GLTexture(); const GLuint& _texture { _id }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp index ca2e7061f5..7acb736063 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp @@ -13,6 +13,7 @@ #endif #include "GLShared.h" +#include "GLTexture.h" using namespace gpu; using namespace gpu::gl; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h index deb470c572..078ab40ee3 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h @@ -14,7 +14,6 @@ #include #include "GLShared.h" -#include "GLTexture.h" #ifdef Q_OS_WIN #define THREADED_TEXTURE_TRANSFER diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp similarity index 93% rename from libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp rename to libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp index e46c739593..93d87ee6e4 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp @@ -5,7 +5,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" +#include "GL41Backend.h" #include #include @@ -18,7 +18,7 @@ Q_LOGGING_CATEGORY(gpugl41logging, "hifi.gpu.gl41") using namespace gpu; using namespace gpu::gl41; -void GLBackend::do_draw(Batch& batch, size_t paramOffset) { +void GL41Backend::do_draw(Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; uint32 numVertices = batch._params[paramOffset + 1]._uint; @@ -43,7 +43,7 @@ void GLBackend::do_draw(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GLBackend::do_drawIndexed(Batch& batch, size_t paramOffset) { +void GL41Backend::do_drawIndexed(Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; uint32 numIndices = batch._params[paramOffset + 1]._uint; @@ -72,7 +72,7 @@ void GLBackend::do_drawIndexed(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GLBackend::do_drawInstanced(Batch& batch, size_t paramOffset) { +void GL41Backend::do_drawInstanced(Batch& batch, size_t paramOffset) { GLint numInstances = batch._params[paramOffset + 4]._uint; Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -108,7 +108,7 @@ void glbackend_glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsize #endif } -void GLBackend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { +void GL41Backend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { GLint numInstances = batch._params[paramOffset + 4]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 3]._uint]; uint32 numIndices = batch._params[paramOffset + 2]._uint; @@ -143,7 +143,7 @@ void GLBackend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { } -void GLBackend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { +void GL41Backend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { #if (GPU_INPUT_PROFILE == GPU_CORE_43) uint commandCount = batch._params[paramOffset + 0]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; @@ -159,7 +159,7 @@ void GLBackend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { } -void GLBackend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { +void GL41Backend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { #if (GPU_INPUT_PROFILE == GPU_CORE_43) uint commandCount = batch._params[paramOffset + 0]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h similarity index 86% rename from libraries/gpu-gl/src/gpu/gl41/GLBackend.h rename to libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 8a0f4bb912..5695ba080e 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -1,5 +1,5 @@ // -// GLBackend.h +// GL41Backend.h // libraries/gpu/src/gpu // // Created by Sam Gateau on 10/27/2014. @@ -8,8 +8,8 @@ // 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_gpu_41_GLBackend_h -#define hifi_gpu_41_GLBackend_h +#ifndef hifi_gpu_41_GL41Backend_h +#define hifi_gpu_41_GL41Backend_h #include @@ -27,21 +27,21 @@ namespace gpu { namespace gl41 { -class GLBackend : public gl::GLBackend { +class GL41Backend : public gl::GLBackend { using Parent = gl::GLBackend; // Context Backend static interface required friend class Context; public: - explicit GLBackend(bool syncCache) : Parent(syncCache) {} - GLBackend() : Parent() {} + explicit GL41Backend(bool syncCache) : Parent(syncCache) {} + GL41Backend() : Parent() {} - class GLTexture : public gpu::gl::GLTexture { + class GL41Texture : public gpu::gl::GLTexture { using Parent = gpu::gl::GLTexture; GLuint allocate(); public: - GLTexture(const Texture& buffer, bool transferrable); - GLTexture(const Texture& buffer, GLTexture* original); + GL41Texture(const Texture& buffer, bool transferrable); + GL41Texture(const Texture& buffer, GL41Texture* original); protected: void transferMip(uint16_t mipLevel, uint8_t face = 0) const; diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp similarity index 81% rename from libraries/gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp rename to libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp index 9c6b8b8124..e56d671891 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp @@ -5,13 +5,13 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" +#include "GL41Backend.h" #include "../gl/GLBuffer.h" using namespace gpu; using namespace gpu::gl41; -class GLBuffer : public gl::GLBuffer { +class GL41Buffer : public gl::GLBuffer { using Parent = gpu::gl::GLBuffer; static GLuint allocate() { GLuint result; @@ -20,7 +20,7 @@ class GLBuffer : public gl::GLBuffer { } public: - GLBuffer(const Buffer& buffer, GLBuffer* original) : Parent(buffer, allocate()) { + GL41Buffer(const Buffer& buffer, GL41Buffer* original) : Parent(buffer, allocate()) { glBindBuffer(GL_ARRAY_BUFFER, _buffer); glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); @@ -53,10 +53,10 @@ public: } }; -GLuint GLBackend::getBufferID(const Buffer& buffer) { - return GLBuffer::getId(buffer); +GLuint GL41Backend::getBufferID(const Buffer& buffer) { + return GL41Buffer::getId(buffer); } -gl::GLBuffer* GLBackend::syncGPUObject(const Buffer& buffer) { - return GLBuffer::sync(buffer); +gl::GLBuffer* GL41Backend::syncGPUObject(const Buffer& buffer) { + return GL41Buffer::sync(buffer); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp similarity index 96% rename from libraries/gpu-gl/src/gpu/gl41/GLBackendInput.cpp rename to libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp index 0486d1cfd2..1b0caa7345 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp @@ -1,5 +1,5 @@ // -// GLBackendInput.cpp +// GL41BackendInput.cpp // libraries/gpu/src/gpu // // Created by Sam Gateau on 3/8/2015. @@ -8,7 +8,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" +#include "GL41Backend.h" using namespace gpu; using namespace gpu::gl41; @@ -22,7 +22,7 @@ using namespace gpu::gl41; #define SUPPORT_VERTEX_ATTRIB_FORMAT #endif -void GLBackend::updateInput() { +void GL41Backend::updateInput() { #if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) if (_input._invalidFormat) { @@ -36,7 +36,7 @@ void GLBackend::updateInput() { GLuint slot = attrib._slot; GLuint count = attrib._element.getLocationScalarCount(); uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGLType[attrib._element.getType()]; + GLenum type = _elementTypeToGL41Type[attrib._element.getType()]; GLuint offset = attrib._offset;; GLboolean isNormalized = attrib._element.isNormalized(); @@ -142,7 +142,7 @@ void GLBackend::updateInput() { int bufferNum = (channelIt).first; if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { - // GLuint vbo = gpu::GLBackend::getBufferID((*buffers[bufferNum])); + // GLuint vbo = gpu::GL41Backend::getBufferID((*buffers[bufferNum])); GLuint vbo = _input._bufferVBOs[bufferNum]; if (boundVBO != vbo) { glBindBuffer(GL_ARRAY_BUFFER, vbo); diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp similarity index 86% rename from libraries/gpu-gl/src/gpu/gl41/GLBackendOutput.cpp rename to libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index a34a0aaebd..53c2c75394 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -1,5 +1,5 @@ // -// GLBackendTexture.cpp +// GL41BackendTexture.cpp // libraries/gpu/src/gpu // // Created by Sam Gateau on 1/19/2015. @@ -8,7 +8,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" +#include "GL41Backend.h" #include @@ -17,7 +17,7 @@ namespace gpu { namespace gl41 { -class GLFramebuffer : public gl::GLFramebuffer { +class GL41Framebuffer : public gl::GLFramebuffer { using Parent = gl::GLFramebuffer; static GLuint allocate() { GLuint result; @@ -56,7 +56,7 @@ public: for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -83,7 +83,7 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -115,19 +115,19 @@ public: public: - GLFramebuffer(const gpu::Framebuffer& framebuffer) + GL41Framebuffer(const gpu::Framebuffer& framebuffer) : Parent(framebuffer, allocate()) { } }; -gl::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffer) { - return GLFramebuffer::sync(framebuffer); +gl::GLFramebuffer* GL41Backend::syncGPUObject(const Framebuffer& framebuffer) { + return GL41Framebuffer::sync(framebuffer); } -GLuint GLBackend::getFramebufferID(const FramebufferPointer& framebuffer) { - return framebuffer ? GLFramebuffer::getId(*framebuffer) : 0; +GLuint GL41Backend::getFramebufferID(const FramebufferPointer& framebuffer) { + return framebuffer ? GL41Framebuffer::getId(*framebuffer) : 0; } -void GLBackend::do_blit(Batch& batch, size_t paramOffset) { +void GL41Backend::do_blit(Batch& batch, size_t paramOffset) { auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); Vec4i srcvp; for (auto i = 0; i < 4; ++i) { diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp similarity index 62% rename from libraries/gpu-gl/src/gpu/gl41/GLBackendQuery.cpp rename to libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp index 4915adbc1d..3c6109bbdf 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp @@ -1,5 +1,5 @@ // -// GLBackendQuery.cpp +// GL41BackendQuery.cpp // libraries/gpu/src/gpu // // Created by Sam Gateau on 7/7/2015. @@ -8,14 +8,14 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" +#include "GL41Backend.h" #include "../gl/GLQuery.h" using namespace gpu; using namespace gpu::gl41; -class GLQuery : public gpu::gl::GLQuery { +class GL41Query : public gpu::gl::GLQuery { using Parent = gpu::gl::GLBuffer; public: static GLuint allocateQuery() { @@ -24,14 +24,14 @@ public: return result; } - GLQuery(const Query& query) + GL41Query(const Query& query) : gl::GLQuery(query, allocateQuery()) { } }; -gl::GLQuery* GLBackend::syncGPUObject(const Query& query) { - return GLQuery::sync(query); +gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) { + return GL41Query::sync(query); } -GLuint GLBackend::getQueryID(const QueryPointer& query) { - return GLQuery::getId(query); +GLuint GL41Backend::getQueryID(const QueryPointer& query) { + return GL41Query::getId(query); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp similarity index 86% rename from libraries/gpu-gl/src/gpu/gl41/GLBackendTexture.cpp rename to libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index f5e8531ddc..326a63c01a 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -1,5 +1,5 @@ // -// GLBackendTexture.cpp +// GL41BackendTexture.cpp // libraries/gpu/src/gpu // // Created by Sam Gateau on 1/19/2015. @@ -8,7 +8,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" +#include "GL41Backend.h" #include #include @@ -19,29 +19,29 @@ using namespace gpu; using namespace gpu::gl41; -using GLTexelFormat = gl::GLTexelFormat; -using GLTexture = GLBackend::GLTexture; +using GL41TexelFormat = gl::GLTexelFormat; +using GL41Texture = GL41Backend::GL41Texture; -GLuint GLTexture::allocate() { +GLuint GL41Texture::allocate() { Backend::incrementTextureGPUCount(); GLuint result; glGenTextures(1, &result); return result; } -GLuint GLBackend::getTextureID(const TexturePointer& texture, bool transfer) { - return GLTexture::getId(texture, transfer); +GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { + return GL41Texture::getId(texture, transfer); } -gl::GLTexture* GLBackend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GLTexture::sync(texture, transfer); +gl::GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GL41Texture::sync(texture, transfer); } -GLTexture::GLTexture(const Texture& texture, bool transferrable) : gl::GLTexture(texture, allocate(), transferrable) {} +GL41Texture::GL41Texture(const Texture& texture, bool transferrable) : gl::GLTexture(texture, allocate(), transferrable) {} -GLTexture::GLTexture(const Texture& texture, GLTexture* original) : gl::GLTexture(texture, allocate(), original) {} +GL41Texture::GL41Texture(const Texture& texture, GL41Texture* original) : gl::GLTexture(texture, allocate(), original) {} -void GLBackend::GLTexture::withPreservedTexture(std::function f) const { +void GL41Backend::GL41Texture::withPreservedTexture(std::function f) const { GLint boundTex = -1; switch (_target) { case GL_TEXTURE_2D: @@ -63,14 +63,14 @@ void GLBackend::GLTexture::withPreservedTexture(std::function f) const (void)CHECK_GL_ERROR(); } -void GLBackend::GLTexture::generateMips() const { +void GL41Backend::GL41Texture::generateMips() const { withPreservedTexture([&] { glGenerateMipmap(_target); }); (void)CHECK_GL_ERROR(); } -void GLBackend::GLTexture::allocateStorage() const { +void GL41Backend::GL41Texture::allocateStorage() const { gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); (void)CHECK_GL_ERROR(); @@ -93,7 +93,7 @@ void GLBackend::GLTexture::allocateStorage() const { } } -void GLBackend::GLTexture::updateSize() const { +void GL41Backend::GL41Texture::updateSize() const { setSize(_virtualSize); if (!_id) { return; @@ -129,7 +129,7 @@ void GLBackend::GLTexture::updateSize() const { } // Move content bits from the CPU to the GPU for a given mip / face -void GLBackend::GLTexture::transferMip(uint16_t mipLevel, uint8_t face) const { +void GL41Backend::GL41Texture::transferMip(uint16_t mipLevel, uint8_t face) const { auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); //GLenum target = getFaceTargets()[face]; @@ -141,7 +141,7 @@ void GLBackend::GLTexture::transferMip(uint16_t mipLevel, uint8_t face) const { // This should never happen on the main thread // Move content bits from the CPU to the GPU -void GLBackend::GLTexture::transfer() const { +void GL41Backend::GL41Texture::transfer() const { PROFILE_RANGE(__FUNCTION__); //qDebug() << "Transferring texture: " << _privateTexture; // Need to update the content of the GPU object from the source sysmem of the texture @@ -208,11 +208,8 @@ void GLBackend::GLTexture::transfer() const { } } -void GLBackend::GLTexture::syncSampler() const { +void GL41Backend::GL41Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); - Texture::Type type = _gpuObject.getType(); - auto object = this; - const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); glTexParameteri(_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); diff --git a/libraries/gpu-gl/src/gpu/gl41/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp similarity index 95% rename from libraries/gpu-gl/src/gpu/gl41/GLBackendTransform.cpp rename to libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp index 6601eef86e..89b5db34c0 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GLBackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp @@ -1,5 +1,5 @@ // -// GLBackendTransform.cpp +// GL41BackendTransform.cpp // libraries/gpu/src/gpu // // Created by Sam Gateau on 3/8/2015. @@ -8,12 +8,12 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GLBackend.h" +#include "GL41Backend.h" using namespace gpu; using namespace gpu::gl41; -void GLBackend::initTransform() { +void GL41Backend::initTransform() { glGenBuffers(1, &_transform._objectBuffer); glGenBuffers(1, &_transform._cameraBuffer); glGenBuffers(1, &_transform._drawCallInfoBuffer); @@ -24,7 +24,7 @@ void GLBackend::initTransform() { } } -void GLBackend::transferTransformState(const Batch& batch) const { +void GL41Backend::transferTransformState(const Batch& batch) const { // FIXME not thread safe static std::vector bufferData; if (!_transform._cameras.empty()) { diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index 83645f73e3..7fe6739ff7 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -87,11 +87,11 @@ namespace gpu { } namespace gl41 { - class GLBackend; + class GL41Backend; } namespace gl45 { - class GLBackend; + class GL45Backend; } } From 53956c8210b1f93363e5c3f12deee002bb4234e7 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 19 May 2016 07:50:06 -0700 Subject: [PATCH 0122/1237] reduce log spam --- scripts/system/controllers/handControllerPointer.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 921c55b7b2..78b7c4eb84 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -186,7 +186,9 @@ function updateSeeking() { averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); if (!lookAt2D) { - print('Cannot seek without lookAt position'); + // FIXME - determine if this message is useful but make it so it doesn't spam the + // log in the case that it is happening + //print('Cannot seek without lookAt position'); return; } // E.g., if parallel to location in HUD var copy = Reticle.position; @@ -420,7 +422,9 @@ function update() { var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { - print('Controller is parallel to HUD'); + // FIXME - determine if this message is useful but make it so it doesn't spam the + // log in the case that it is happening + //print('Controller is parallel to HUD'); return turnOffVisualization(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); From 2d7118dacc87260faca4f232727823ca7b8985d2 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 19 May 2016 10:21:00 -0700 Subject: [PATCH 0123/1237] clening up the code --- libraries/render-utils/src/DeferredLightingEffect.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index fd7826070d..0a562de3ba 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -468,11 +468,6 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo auto& part = mesh->getPartBuffer().get(0); batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); } - // keep for debug - /* { - auto& part = mesh->getPartBuffer().get(1); - batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); - }*/ } } } @@ -555,7 +550,6 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo state->setDepthTest(true, false, gpu::LESS_EQUAL); // TODO: We should use DepthClamp and avoid changing geometry for inside /outside cases - //state->setDepthClampEnable(true); // additive blending state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); @@ -665,8 +659,7 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { std::vector parts; parts.push_back(model::Mesh::Part(0, indices, 0, model::Mesh::TRIANGLES)); - //DEBUG: - parts.push_back(model::Mesh::Part(0, indices, 0, model::Mesh::LINE_STRIP)); + parts.push_back(model::Mesh::Part(0, indices, 0, model::Mesh::LINE_STRIP)); // outline version _spotLightMesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); From 7ea7bd3e1dacd21702452421cc0737a89fd1aea7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 19 May 2016 10:21:04 -0700 Subject: [PATCH 0124/1237] set created-time for imported entities to 'now' --- libraries/entities/src/EntityTree.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 30c37d46b1..97d4e91acd 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1376,6 +1376,10 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra item->globalizeProperties(properties, "Cannot find %3 parent of %2 %1", args->root); } } + + // set creation time to "now" for imported entities + properties.setCreated(usecTimestampNow()); + properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity // queue the packet to send to the server From bc29bcf0d2ce6fc706d952b58f5c41c13669f0fc Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 19 May 2016 11:18:30 -0700 Subject: [PATCH 0125/1237] light switch swap --- .../Home/switches/livingRoomDiscLights.js | 26 +++++--------- .../Home/switches/livingRoomFan.js | 35 +++++++------------ 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomDiscLights.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomDiscLights.js index eef64fd3f8..6fb2ea538a 100644 --- a/unpublishedScripts/DomainContent/Home/switches/livingRoomDiscLights.js +++ b/unpublishedScripts/DomainContent/Home/switches/livingRoomDiscLights.js @@ -10,7 +10,8 @@ // (function() { - + var ON_MODEL_URL = "atp:/switches/lightswitch_on.fbx"; + var OFF_MODEL_URL = "atp:/switches/lightswitch_off.fbx"; var SEARCH_RADIUS = 100; var _this; @@ -142,6 +143,9 @@ setEntityCustomData('home-switch', _this.entityID, { state: 'on' }); + Entities.editEntity(this.entityID, { + modelURL: ON_MODEL_URL + }); } else { glowLights.forEach(function(glowLight) { @@ -156,9 +160,12 @@ setEntityCustomData('home-switch', this.entityID, { state: 'off' }); + + Entities.editEntity(this.entityID, { + modelURL: OFF_MODEL_URL + }); } - this.flipSwitch(); Audio.playSound(this.switchSound, { volume: 0.5, position: this.position @@ -166,21 +173,6 @@ }, - flipSwitch: function() { - var rotation = Entities.getEntityProperties(this.entityID, "rotation").rotation; - var axis = { - x: 0, - y: 1, - z: 0 - }; - var dQ = Quat.angleAxis(180, axis); - rotation = Quat.multiply(rotation, dQ); - - Entities.editEntity(this.entityID, { - rotation: rotation - }); - }, - preload: function(entityID) { this.entityID = entityID; setEntityCustomData('grabbableKey', this.entityID, { diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js index 6005503cf3..c1f0c0f112 100644 --- a/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js +++ b/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js @@ -11,7 +11,7 @@ (function() { - var FAN_SOUND_ENTITY_NAME ="home_sfx_ceiling_fan" + var FAN_SOUND_ENTITY_NAME = "home_sfx_ceiling_fan" var SEARCH_RADIUS = 100; var _this; var utilitiesScript = Script.resolvePath('../utils.js'); @@ -74,16 +74,16 @@ if (!_this.fanSoundEntity) { return; } - print('HOME FAN OFF 2') + print('HOME FAN OFF 2') var soundUserData = getEntityCustomData("soundKey", _this.fanSoundEntity); if (!soundUserData) { print("NO SOUND USER DATA! RETURNING."); return; } - print('HOME FAN OFF 3') + print('HOME FAN OFF 3') soundUserData.volume = 0.0; setEntityCustomData("soundKey", _this.fanSoundEntity, soundUserData); - print('HOME FAN OFF 4') + print('HOME FAN OFF 4') }, findFan: function() { @@ -105,7 +105,7 @@ var fan = null; print('HOME LOOKING FOR A FAN') - print('HOME TOTAL ENTITIES:: ' +entities.length) + print('HOME TOTAL ENTITIES:: ' + entities.length) entities.forEach(function(entity) { var props = Entities.getEntityProperties(entity); if (props.name === FAN_SOUND_ENTITY_NAME) { @@ -132,7 +132,9 @@ setEntityCustomData('home-switch', this.entityID, { state: 'on' }); - + Entities.editEntity(this.entityID, { + modelURL: ON_MODEL_URL + }) } else { this.fanRotationOff(); this.fanSoundOff(); @@ -140,9 +142,13 @@ setEntityCustomData('home-switch', this.entityID, { state: 'off' }); + + Entities.editEntity(this.entityID, { + modelURL: OFF_MODEL_URL + }) } - this.flipSwitch(); + Audio.playSound(this.switchSound, { volume: 0.5, position: this.position @@ -150,21 +156,6 @@ }, - flipSwitch: function() { - var rotation = Entities.getEntityProperties(this.entityID, "rotation").rotation; - var axis = { - x: 0, - y: 1, - z: 0 - }; - var dQ = Quat.angleAxis(180, axis); - rotation = Quat.multiply(rotation, dQ); - - Entities.editEntity(this.entityID, { - rotation: rotation - }); - }, - preload: function(entityID) { this.entityID = entityID; setEntityCustomData('grabbableKey', this.entityID, { From 9a4b30a50bb0c3178de83b6e53fbbf2d090cb4b5 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 May 2016 12:52:32 -0700 Subject: [PATCH 0126/1237] Fix AvatarInputs phantom movement --- interface/src/Application.cpp | 16 ++++++++-------- interface/src/Application.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9e3bd0c69c..cf14d01771 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1510,17 +1510,17 @@ void Application::paintGL() { renderArgs._context->syncCache(); } - if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { + auto inputs = AvatarInputs::getInstance(); + if (inputs->mirrorVisible()) { PerformanceTimer perfTimer("Mirror"); auto primaryFbo = DependencyManager::get()->getPrimaryFramebuffer(); renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); - auto inputs = AvatarInputs::getInstance(); _mirrorViewRect.moveTo(inputs->x(), inputs->y()); - renderRearViewMirror(&renderArgs, _mirrorViewRect); + renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); renderArgs._blitFramebuffer.reset(); renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; @@ -1839,6 +1839,9 @@ bool Application::event(QEvent* event) { idle(); return true; } else if ((int)event->type() == (int)Paint) { + // NOTE: This must be updated as close to painting as possible, + // or AvatarInputs will mysteriously move to the bottom-right + AvatarInputs::getInstance()->update(); justPresented = true; paintGL(); return true; @@ -2670,9 +2673,6 @@ void Application::idle() { firstIdle = false; connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); _overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays)); - } else { - // FIXME: AvatarInputs are positioned incorrectly if instantiated before the first paint - AvatarInputs::getInstance()->update(); } PROFILE_RANGE(__FUNCTION__); @@ -4101,7 +4101,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se activeRenderingThread = nullptr; } -void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region) { +void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { auto originalViewport = renderArgs->_viewport; // Grab current viewport to reset it at the end @@ -4111,7 +4111,7 @@ void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& regi auto myAvatar = getMyAvatar(); // bool eyeRelativeCamera = false; - if (!AvatarInputs::getInstance()->mirrorZoomed()) { + if (!isZoomed) { _mirrorCamera.setPosition(myAvatar->getChestPosition() + myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 5616722c92..28dbcead47 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -342,7 +342,7 @@ private: glm::vec3 getSunDirection() const; - void renderRearViewMirror(RenderArgs* renderArgs, const QRect& region); + void renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed); int sendNackPackets(); From 8381e74fb3478750362de176aa0710b369469c0f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 19 May 2016 13:56:40 -0700 Subject: [PATCH 0127/1237] OpenVRDispalyPlugin: fix one-frame lag in resetSensors. resetSensors() should take place immediately, so that the getHeadPose() after the reset should be identity. (or close to it) --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 38719fdca5..1f6b11f862 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -122,7 +122,19 @@ void OpenVrDisplayPlugin::customizeContext() { void OpenVrDisplayPlugin::resetSensors() { Lock lock(_poseMutex); glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + + glm::mat4 oldSensorResetMat = _sensorResetMat; _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + + glm::mat4 undoRedoMat = _sensorResetMat * glm::inverse(oldSensorResetMat); + + // update the device poses, by undoing the previous sensorResetMatrix then applying the new one. + for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + _trackedDevicePoseMat4[i] = undoRedoMat * _trackedDevicePoseMat4[i]; + _trackedDeviceLinearVelocities[i] = transformVectorFast(undoRedoMat, _trackedDeviceLinearVelocities[i]); + _trackedDeviceAngularVelocities[i] = transformVectorFast(undoRedoMat, _trackedDeviceAngularVelocities[i]); + } + _currentRenderFrameInfo.renderPose = _trackedDevicePoseMat4[vr::k_unTrackedDeviceIndex_Hmd]; } From 4e862941cb9307ebbcfb88c1a14e463c62a854c4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 19 May 2016 14:51:56 -0700 Subject: [PATCH 0128/1237] fix a race when restarting scripts -- avoid the old not-yet-stopped script from being considered the restart script --- libraries/script-engine/src/ScriptEngine.h | 3 +++ libraries/script-engine/src/ScriptEngines.cpp | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 80978e4527..b576e2a37f 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -146,6 +146,8 @@ public: bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget + bool isStopping() const { return _isStopping; } + void flagAsStopping() { _isStopping = true; } bool isDebuggable() const { return _debuggable; } @@ -189,6 +191,7 @@ protected: QString _parentURL; std::atomic _isFinished { false }; std::atomic _isRunning { false }; + std::atomic _isStopping { false }; int _evaluatesPending { 0 }; bool _isInitialized { false }; QHash _timerFunctionMap; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 70eb055d22..5a2beb901d 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -351,6 +351,7 @@ void ScriptEngines::stopAllScripts(bool restart) { reloadScript(scriptName); }); } + it.value()->flagAsStopping(); QMetaObject::invokeMethod(it.value(), "stop"); //it.value()->stop(); qCDebug(scriptengine) << "stopping script..." << it.key(); @@ -369,6 +370,7 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { if (_scriptEnginesHash.contains(scriptURL)) { ScriptEngine* scriptEngine = _scriptEnginesHash[scriptURL]; if (restart) { + scriptEngine->flagAsStopping(); auto scriptCache = DependencyManager::get(); scriptCache->deleteScript(scriptURL); connect(scriptEngine, &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) { @@ -454,7 +456,7 @@ ScriptEngine* ScriptEngines::getScriptEngine(const QUrl& rawScriptURL) { QReadLocker lock(&_scriptEnginesHashLock); const QUrl scriptURL = normalizeScriptURL(rawScriptURL); auto it = _scriptEnginesHash.find(scriptURL); - if (it != _scriptEnginesHash.end()) { + if (it != _scriptEnginesHash.end() && !it.value()->isStopping()) { result = it.value(); } } From 7f131e2c4fe06eb2d2faf3901ebcece4ec3c14ec Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 19 May 2016 15:00:08 -0700 Subject: [PATCH 0129/1237] Reset avatar as well as displayPlugin when "driving" --- interface/src/avatar/MyAvatar.cpp | 6 ++++-- interface/src/avatar/MyAvatar.h | 2 +- interface/src/ui/OverlayConductor.cpp | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a3cf1f9f4f..183a24f252 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -234,7 +234,7 @@ QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) { return AvatarData::toByteArray(cullSmallChanges, sendAll); } -void MyAvatar::reset(bool andRecenter) { +void MyAvatar::reset(bool andRecenter, bool andReload) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "reset", Q_ARG(bool, andRecenter)); @@ -244,7 +244,9 @@ void MyAvatar::reset(bool andRecenter) { // Reset dynamic state. _wasPushing = _isPushing = _isBraking = false; _follow.deactivate(); - _skeletonModel->reset(); + if (andReload) { + _skeletonModel->reset(); + } getHead()->reset(); _targetVelocity = glm::vec3(0.0f); setThrust(glm::vec3(0.0f)); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 66e6af340e..d7af6b5683 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -94,7 +94,7 @@ public: AudioListenerMode getAudioListenerModeCamera() const { return FROM_CAMERA; } AudioListenerMode getAudioListenerModeCustom() const { return CUSTOM; } - Q_INVOKABLE void reset(bool andRecenter = false); + Q_INVOKABLE void reset(bool andRecenter = false, bool andReload = true); void update(float deltaTime); virtual void postUpdate(float deltaTime) override; void preDisplaySide(RenderArgs* renderArgs); diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index b44ea91a60..1ceceb741e 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -86,6 +86,7 @@ void OverlayConductor::updateMode() { _mode = FLAT; // Seems appropriate to let things reset, below, after the following. // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. qApp->getActiveDisplayPlugin()->resetSensors(); + myAvatar->reset(true, false); } if (_wantsOverlays) { qDebug() << "flipping" << !nowDriving; From dac043a52d5c7f47255373e10cad89446d3297be Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 May 2016 10:49:38 +1200 Subject: [PATCH 0130/1237] Provide navigation up to the PC level to display drives in table --- .../resources/qml/dialogs/FileDialog.qml | 87 ++++++++++++++++--- 1 file changed, 74 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 5674123b4d..0e936ff0a3 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -194,10 +194,8 @@ ModalWindow { function navigateUp() { if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { - fileTableModel.folder = fileTableModel.parentFolder + fileTableModel.folder = fileTableModel.parentFolder; return true; - } else if (true) { - } } @@ -222,6 +220,42 @@ ModalWindow { onFolderChanged: { fileTableModel.update(); // Update once the data from the folder change is available. } + + function getItem(index, field) { + return get(index, field); + } + } + + ListModel { + // Emulates FolderListModel but contains drive data. + id: driveListModel + + property int count: 1 + + Component.onCompleted: initialize(); + + function initialize() { + var drive, + i; + + count = drives.length; + + for (i = 0; i < count; i++) { + drive = drives[i].slice(0, -1); // Remove trailing "/". + append({ + fileName: drive, + fileModified: new Date(0), + fileSize: 0, + filePath: drive + "/", + fileIsDir: true, + fileNameSort: drive.toLowerCase() + }); + } + } + + function getItem(index, field) { + return get(index)[field]; + } } ListModel { @@ -237,10 +271,37 @@ ModalWindow { property var folder property int sortOrder: Qt.AscendingOrder property int sortColumn: 0 - property string parentFolder: folderListModel.parentFolder + property var model: folderListModel + property string parentFolder: calculateParentFolder(); + + readonly property string rootFolder: "file:///" + + function calculateParentFolder() { + if (model === folderListModel) { + if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { + return rootFolder; + } else { + return folderListModel.parentFolder; + } + } else { + return ""; + } + } onFolderChanged: { - folderListModel.folder = folder; + if (folder === rootFolder) { + model = driveListModel; + update(); + } else { + var needsUpdate = model === driveListModel && folder === folderListModel.folder; + + model = folderListModel; + folderListModel.folder = folder; + + if (needsUpdate) { + update(); + } + } currentDirectory.lastValidFolder = helper.urlToPath(folder); } @@ -265,18 +326,18 @@ ModalWindow { upper, rows = 0, i; + clear(); comparisonFunction = sortOrder === Qt.AscendingOrder ? function(a, b) { return a < b; } : function(a, b) { return a > b; } - for (i = 0; i < folderListModel.count; i++) { - fileName = folderListModel.get(i, "fileName"); - fileIsDir = folderListModel.get(i, "fileIsDir"); - - sortValue = folderListModel.get(i, dataField); + for (i = 0; i < model.count; i++) { + fileName = model.getItem(i, "fileName"); + fileIsDir = model.getItem(i, "fileIsDir"); + sortValue = model.getItem(i, dataField); if (dataField === "fileName") { // Directories first by prefixing a "*". // Case-insensitive. @@ -299,9 +360,9 @@ ModalWindow { insert(lower, { fileName: fileName, - fileModified: (fileIsDir ? new Date(0) : folderListModel.get(i, "fileModified")), - fileSize: folderListModel.get(i, "fileSize"), - filePath: folderListModel.get(i, "filePath"), + fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), + fileSize: model.getItem(i, "fileSize"), + filePath: model.getItem(i, "filePath"), fileIsDir: fileIsDir, fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() }); From 9ad488ba7b1d1ce3709fbbba79ad8a2ef5e04821 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 19 May 2016 16:08:44 -0700 Subject: [PATCH 0131/1237] fix method name to match coding standard --- libraries/script-engine/src/ScriptEngine.h | 2 +- libraries/script-engine/src/ScriptEngines.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index b576e2a37f..512e79fb95 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -147,7 +147,7 @@ public: bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget bool isStopping() const { return _isStopping; } - void flagAsStopping() { _isStopping = true; } + void setIsStopping() { _isStopping = true; } bool isDebuggable() const { return _debuggable; } diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 5a2beb901d..65e08938b3 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -351,7 +351,7 @@ void ScriptEngines::stopAllScripts(bool restart) { reloadScript(scriptName); }); } - it.value()->flagAsStopping(); + it.value()->setIsStopping(); QMetaObject::invokeMethod(it.value(), "stop"); //it.value()->stop(); qCDebug(scriptengine) << "stopping script..." << it.key(); @@ -370,7 +370,7 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { if (_scriptEnginesHash.contains(scriptURL)) { ScriptEngine* scriptEngine = _scriptEnginesHash[scriptURL]; if (restart) { - scriptEngine->flagAsStopping(); + scriptEngine->setIsStopping(); auto scriptCache = DependencyManager::get(); scriptCache->deleteScript(scriptURL); connect(scriptEngine, &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) { From 3f5ed4bef8603eae9bb4a08e9912953057244c1d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 19 May 2016 16:15:33 -0700 Subject: [PATCH 0132/1237] set isStopping on other calls to stop --- libraries/script-engine/src/ScriptEngines.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 65e08938b3..8b57487db0 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -160,6 +160,7 @@ void ScriptEngines::shutdownScripting() { scriptEngine->disconnect(this); // Gracefully stop the engine's scripting thread + scriptEngine->setIsStopping(); scriptEngine->stop(); // We need to wait for the engine to be done running before we proceed, because we don't @@ -353,7 +354,6 @@ void ScriptEngines::stopAllScripts(bool restart) { } it.value()->setIsStopping(); QMetaObject::invokeMethod(it.value(), "stop"); - //it.value()->stop(); qCDebug(scriptengine) << "stopping script..." << it.key(); } } @@ -370,13 +370,13 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { if (_scriptEnginesHash.contains(scriptURL)) { ScriptEngine* scriptEngine = _scriptEnginesHash[scriptURL]; if (restart) { - scriptEngine->setIsStopping(); auto scriptCache = DependencyManager::get(); scriptCache->deleteScript(scriptURL); connect(scriptEngine, &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) { reloadScript(scriptName); }); } + scriptEngine->setIsStopping(); scriptEngine->stop(); stoppedScript = true; qCDebug(scriptengine) << "stopping script..." << scriptURL; From 11aeaba114fc4970767d8640d1f7530116d6d19b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 May 2016 11:32:14 +1200 Subject: [PATCH 0133/1237] Capitalize drive letter of chosen file --- interface/resources/qml/dialogs/FileDialog.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 0e936ff0a3..56b7cb0505 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -176,6 +176,14 @@ ModalWindow { property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } property var homeDestination: helper.home(); + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + function update() { var row = fileTableView.currentRow; @@ -186,7 +194,7 @@ ModalWindow { currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); currentSelectionIsFolder = fileTableView.model.isFolder(row); if (root.selectDirectory || !currentSelectionIsFolder) { - currentSelection.text = helper.urlToPath(currentSelectionUrl); + currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); } else { currentSelection.text = ""; } From 13a057513a3e04c0659904f947834524bbba91e0 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 19 May 2016 16:46:17 -0700 Subject: [PATCH 0134/1237] Removed jointIndices transmission experiment --- libraries/avatars/src/AvatarData.cpp | 58 +++------------------------- libraries/avatars/src/AvatarData.h | 8 ---- 2 files changed, 6 insertions(+), 60 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index b70c021f61..ab7647b9fc 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -592,16 +592,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // joint rotations int numJoints = *sourceBuffer++; - // do not process any jointData until we've received a valid jointIndices hash from - // an earlier AvatarIdentity packet. Because if we do, we risk applying the joint data - // the wrong bones, resulting in a twisted avatar, An un-animated avatar is preferable to this. - bool skipJoints = false; -#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - if (_networkJointIndexMap.empty()) { - skipJoints = true; - } -#endif - int bytesOfValidity = (int)ceil((float)numJoints / (float)BITS_IN_BYTE); minPossibleSize += bytesOfValidity; if (minPossibleSize > maxAvailableSize) { @@ -653,13 +643,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { for (int i = 0; i < numJoints; i++) { JointData& data = _jointData[i]; if (validRotations[i]) { - if (skipJoints) { - sourceBuffer += COMPRESSED_QUATERNION_SIZE; - } else { - sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); - _hasNewJointRotations = true; - data.rotationSet = true; - } + sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); + _hasNewJointRotations = true; + data.rotationSet = true; } } } // numJoints * 6 bytes @@ -705,13 +691,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { for (int i = 0; i < numJoints; i++) { JointData& data = _jointData[i]; if (validTranslations[i]) { - if (skipJoints) { - sourceBuffer += COMPRESSED_TRANSLATION_SIZE; - } else { - sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); - _hasNewJointTranslations = true; - data.translationSet = true; - } + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); + _hasNewJointTranslations = true; + data.translationSet = true; } } } // numJoints * 6 bytes @@ -971,32 +953,13 @@ void AvatarData::clearJointsData() { void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut) { QDataStream packetStream(data); -#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName >> identityOut.jointIndices; -#else packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName; -#endif } bool AvatarData::processAvatarIdentity(const Identity& identity) { bool hasIdentityChanged = false; - #ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - if (!_jointIndices.empty() && _networkJointIndexMap.empty() && !identity.jointIndices.empty()) { - - // build networkJointIndexMap from _jointIndices and networkJointIndices. - _networkJointIndexMap.fill(-1, identity.jointIndices.size()); - for (auto iter = identity.jointIndices.cbegin(); iter != identity.jointIndices.end(); ++iter) { - int jointIndex = getJointIndex(iter.key()); - int networkJointIndex = iter.value(); - if (networkJointIndex >= 0 && networkJointIndex < identity.jointIndices.size()) { - _networkJointIndexMap[networkJointIndex - 1] = jointIndex; - } - } - } -#endif - if (_firstSkeletonCheck || (identity.skeletonModelURL != _skeletonModelURL)) { setSkeletonModelURL(identity.skeletonModelURL); hasIdentityChanged = true; @@ -1021,12 +984,7 @@ QByteArray AvatarData::identityByteArray() { QDataStream identityStream(&identityData, QIODevice::Append); QUrl emptyURL(""); const QUrl& urlToSend = _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; - -#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName << _jointIndices; -#else identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName; -#endif return identityData; } @@ -1205,10 +1163,6 @@ void AvatarData::updateJointMappings() { _jointIndices.clear(); _jointNames.clear(); -#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - _networkJointIndexMap.clear(); -#endif - if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest = QNetworkRequest(_skeletonModelURL); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 1a1a07410d..817d8aef09 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -128,8 +128,6 @@ enum KeyState { DELETE_KEY_DOWN }; -#define TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - class QDataStream; class AttachmentData; @@ -287,9 +285,6 @@ public: QUrl skeletonModelURL; QVector attachmentData; QString displayName; -#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - QHash jointIndices; -#endif }; static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); @@ -382,9 +377,6 @@ protected: float _displayNameAlpha; QHash _jointIndices; ///< 1-based, since zero is returned for missing keys -#ifdef TRANSMIT_JOINT_INDICES_IN_IDENTITY_PACKET - QVector _networkJointIndexMap; // maps network joint indices to local model joint indices. -#endif QStringList _jointNames; ///< in order of depth-first traversal quint64 _errorLogExpiry; ///< time in future when to log an error From 6ecbdc6b9a5ec1e67701f00464d729403de11a80 Mon Sep 17 00:00:00 2001 From: Geenz Date: Thu, 19 May 2016 20:03:05 -0400 Subject: [PATCH 0135/1237] Modify evalLightAttenuation to "fade" edges of lights. This provides a more attractive light falloff around the bounds of a light - removing harsh edges. This also adds a new parameter to evalLightAttenuation - the unnormalized light vector. --- libraries/model/src/model/Light.slh | 9 ++++++--- libraries/render-utils/src/point_light.slf | 4 ++-- libraries/render-utils/src/spot_light.slf | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 7cc4691d63..51c78b9a4d 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -98,10 +98,13 @@ float getLightShowContour(Light l) { return l._control.w; } -float evalLightAttenuation(Light l, float d) { - float radius = getLightRadius(l); +float evalLightAttenuation(Light l, float d, vec3 ulightvec) { + float radius = getLightSquareRadius(l); float denom = d / radius + 1.0; - float attenuation = min(1.0, 1.0 / (denom * denom)); + float attenuation = 1.0 / (denom * denom); + // "Fade" the edges of light sources to make things look a bit more attractive. + // Note: this tends to look a bit odd at lower exponents. + attenuation *= clamp(0, 1, mix(0, 1, getLightCutoffSquareRadius(l) - dot(ulightvec, ulightvec))); return attenuation; } diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index fc72f094e7..581aa8c250 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -66,8 +66,8 @@ void main(void) { vec3 fragEyeDir = normalize(fragEyeVector.xyz); vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); - // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance); + // Eval attenuation + float radialAttenuation = evalLightAttenuation(light, fragLightDistance, fragLightVec); // Final Lighting color vec3 fragColor = (shading.w * frag.diffuse + shading.xyz); diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 4191ba3f63..1cb6ebb878 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -74,7 +74,7 @@ void main(void) { vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance); + float radialAttenuation = evalLightAttenuation(light, fragLightDistance, fragLightVec); float angularAttenuation = evalLightSpotAttenuation(light, cosSpotAngle); // Final Lighting color From a54e8206faeb7c689bae0e3731ca90a58d3e298c Mon Sep 17 00:00:00 2001 From: Geenz Date: Thu, 19 May 2016 20:10:49 -0400 Subject: [PATCH 0136/1237] Switch back to using the light's radius + spacing --- libraries/model/src/model/Light.slh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 51c78b9a4d..6211505090 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -99,12 +99,14 @@ float getLightShowContour(Light l) { } float evalLightAttenuation(Light l, float d, vec3 ulightvec) { - float radius = getLightSquareRadius(l); + float radius = getLightRadius(l); float denom = d / radius + 1.0; float attenuation = 1.0 / (denom * denom); + // "Fade" the edges of light sources to make things look a bit more attractive. // Note: this tends to look a bit odd at lower exponents. attenuation *= clamp(0, 1, mix(0, 1, getLightCutoffSquareRadius(l) - dot(ulightvec, ulightvec))); + return attenuation; } From 75532cda084c34c09b759eb120d00bfcc7d61ea5 Mon Sep 17 00:00:00 2001 From: Geenz Date: Thu, 19 May 2016 20:52:28 -0400 Subject: [PATCH 0137/1237] Document evalLightAttenuation's parameters. --- libraries/model/src/model/Light.slh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 6211505090..46eb352d16 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -98,6 +98,9 @@ float getLightShowContour(Light l) { return l._control.w; } +// Light is the light source its self, d is the light's distance, and ulightvec is the unnormalized light vector. +// Note that ulightvec should be calculated as light position - fragment position. +// Additionally, length(light position) != dot(light position, light position). float evalLightAttenuation(Light l, float d, vec3 ulightvec) { float radius = getLightRadius(l); float denom = d / radius + 1.0; From a00ce2599ce9a1bc35a427f27d1af2387dcbca6a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 19 May 2016 17:54:10 -0700 Subject: [PATCH 0138/1237] cleanup --- .../DomainContent/Home/switches/livingRoomFan.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js index c1f0c0f112..fa8d0968a0 100644 --- a/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js +++ b/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js @@ -9,7 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { +(function() { + var ON_MODEL_URL="atp:/switches/fanswitch_on.fbx"; + var OFF_MODEL_URL="atp:/switches/fanswitch_off.fbx"; var FAN_SOUND_ENTITY_NAME = "home_sfx_ceiling_fan" var SEARCH_RADIUS = 100; @@ -129,12 +131,15 @@ if (this._switch.state === 'off') { this.fanRotationOn(); this.fanSoundOn(); + setEntityCustomData('home-switch', this.entityID, { state: 'on' }); + Entities.editEntity(this.entityID, { modelURL: ON_MODEL_URL - }) + }); + } else { this.fanRotationOff(); this.fanSoundOff(); From e8d7f9614aed57bad25f940c82282c25b91ed92e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 May 2016 15:51:17 -0700 Subject: [PATCH 0139/1237] refresh API info during re-connect - case 570 --- libraries/networking/src/AddressManager.cpp | 44 ++++++++++++++++++--- libraries/networking/src/AddressManager.h | 9 ++++- libraries/networking/src/DomainHandler.cpp | 18 +++------ libraries/networking/src/DomainHandler.h | 10 ++--- libraries/networking/src/NodeList.cpp | 14 +++++-- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index a97f4df35d..35a9d9293d 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -144,12 +144,21 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // 4. domain network address (IP or dns resolvable hostname) // use our regex'ed helpers to figure out what we're supposed to do with this - if (!handleUsername(lookupUrl.authority())) { + if (handleUsername(lookupUrl.authority())) { + // handled a username for lookup + + // in case we're failing to connect to where we thought this user was + // store their username as previous lookup so we can refresh their location via API + _previousLookup = lookupUrl; + } else { // we're assuming this is either a network address or global place name // check if it is a network address first bool hostChanged; if (handleNetworkAddress(lookupUrl.host() - + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + // a network address lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); // If the host changed then we have already saved to history if (hostChanged) { @@ -165,10 +174,16 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info + _previousLookup = lookupUrl; + // no place name - this is probably a domain ID // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + // store this place name as the previous lookup in case we fail to connect and want to refresh API info + _previousLookup = lookupUrl; + // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger); @@ -180,9 +195,13 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); + // a path lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); + // if this is a relative path then handle it as a relative viewpoint handlePath(lookupUrl.path(), trigger, true); emit lookupResultsFinished(); + return true; } @@ -276,7 +295,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; - emit possibleDomainChangeRequired(domainHostname, domainPort); + emit possibleDomainChangeRequired(domainHostname, domainPort, domainID); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); @@ -309,7 +328,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty()) { - handlePath(overridePath, trigger); + // make sure we don't re-handle an overriden path if this was a refresh of info from API + if (trigger != LookupTrigger::AttemptedRefresh) { + handlePath(overridePath, trigger); + } } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; @@ -586,7 +608,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress); - emit possibleDomainChangeRequired(hostname, port); + emit possibleDomainChangeRequired(hostname, port, QUuid()); return hostChanged; } @@ -606,6 +628,13 @@ void AddressManager::goToUser(const QString& username) { QByteArray(), nullptr, requestParams); } +void AddressManager::refreshPreviousLookup() { + // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history) + if (!_previousLookup.isEmpty()) { + handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh); + } +} + void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } @@ -617,7 +646,10 @@ void AddressManager::copyPath() { void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { // if we're cold starting and this is called for the first address (from settings) we don't do anything - if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) { + if (trigger != LookupTrigger::StartupFromSettings + && trigger != LookupTrigger::DomainPathResponse + && trigger != LookupTrigger::AttemptedRefresh) { + if (trigger == LookupTrigger::Back) { // we're about to push to the forward stack // if it's currently empty emit our signal to say that going forward is now possible diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index dd0dbd9f38..0780cfb2c2 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -48,7 +48,8 @@ public: Forward, StartupFromSettings, DomainPathResponse, - Internal + Internal, + AttemptedRefresh }; bool isConnected(); @@ -88,6 +89,8 @@ public slots: void goToUser(const QString& username); + void refreshPreviousLookup(); + void storeCurrentAddress(); void copyAddress(); @@ -98,7 +101,7 @@ signals: void lookupResultIsOffline(); void lookupResultIsNotFound(); - void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort); + void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID); void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID); void locationChangeRequired(const glm::vec3& newPosition, @@ -150,6 +153,8 @@ private: quint64 _lastBackPush = 0; QString _newHostLookupPath; + + QUrl _previousLookup; }; #endif // hifi_AddressManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 44ce63e6c6..b3c3a28829 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -28,16 +28,8 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), - _uuid(), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), - _assignmentUUID(), - _connectionToken(), - _iceDomainID(), - _iceClientID(), - _iceServerSockAddr(), _icePeer(this), - _isConnected(false), - _settingsObject(), _settingsTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -105,7 +97,7 @@ void DomainHandler::hardReset() { softReset(); qCDebug(networking) << "Hard reset in NodeList DomainHandler."; - _iceDomainID = QUuid(); + _pendingDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); _sockAddr.clear(); @@ -140,7 +132,7 @@ void DomainHandler::setUUID(const QUuid& uuid) { } } -void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { +void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded @@ -169,6 +161,8 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { // grab the port by reading the string after the colon _sockAddr.setPort(port); } + + _pendingDomainID = domainID; } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { @@ -179,7 +173,7 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - _iceDomainID = id; + _pendingDomainID = id; HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); @@ -341,7 +335,7 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation); - if (_icePeer.getUUID() != _iceDomainID) { + if (_icePeer.getUUID() != _pendingDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; _icePeer.reset(); } else { diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 03141d8fef..c6269191d2 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -58,8 +58,8 @@ public: const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - - const QUuid& getICEDomainID() const { return _iceDomainID; } + + const QUuid& getPendingDomainID() const { return _pendingDomainID; } const QUuid& getICEClientID() const { return _iceClientID; } @@ -85,7 +85,7 @@ public: void softReset(); public slots: - void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); + void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid()); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); void processSettingsPacketList(QSharedPointer packetList); @@ -126,11 +126,11 @@ private: HifiSockAddr _sockAddr; QUuid _assignmentUUID; QUuid _connectionToken; - QUuid _iceDomainID; + QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection QUuid _iceClientID; HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; - bool _isConnected; + bool _isConnected { false }; QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 482d0366fd..831f0a4995 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, - &_domainHandler, &DomainHandler::setHostnameAndPort); + &_domainHandler, &DomainHandler::setSocketAndID); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); @@ -344,6 +344,14 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } + + if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { + // if we aren't connected to the domain-server, and we have an ID + // (that we presume belongs to a domain in the HF Metaverse) + // we request connection information for the domain every so often to make sure what we have is up to date + + DependencyManager::get()->refreshPreviousLookup(); + } } void NodeList::handleDSPathQuery(const QString& newPath) { @@ -451,7 +459,7 @@ void NodeList::handleICEConnectionToDomainServer() { LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), - _domainHandler.getICEDomainID()); + _domainHandler.getPendingDomainID()); } } @@ -464,7 +472,7 @@ void NodeList::pingPunchForDomainServer() { if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" - << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); + << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID()); } else { if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat From d3304ee65c52dd589cc04ed194768e17f30650df Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 May 2016 18:01:13 -0700 Subject: [PATCH 0140/1237] make some overriding methods as override --- assignment-client/src/entities/EntityServer.h | 4 ++-- tests/controllers/src/main.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 1685f08e01..0486a97ede 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -60,8 +60,8 @@ public: virtual void trackViewerGone(const QUuid& sessionID) override; public slots: - virtual void nodeAdded(SharedNodePointer node); - virtual void nodeKilled(SharedNodePointer node); + virtual void nodeAdded(SharedNodePointer node) override; + virtual void nodeKilled(SharedNodePointer node) override; void pruneDeletedEntities(); protected: diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index e978dd9a38..3a5b4a4a4d 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -89,7 +89,7 @@ public: virtual GLWidget* getPrimaryWidget() override { return nullptr; } virtual MainWindow* getPrimaryWindow() override { return nullptr; } virtual QOpenGLContext* getPrimaryContext() override { return nullptr; } - virtual ui::Menu* getPrimaryMenu() { return nullptr; } + virtual ui::Menu* getPrimaryMenu() override { return nullptr; } virtual bool isForeground() override { return true; } virtual const DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } }; From b95ba8141c59723e525fbc600ac8abcfa8872ce3 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 19 May 2016 20:24:44 -0700 Subject: [PATCH 0141/1237] AvatarData packet overhaul, uses a structure instead of raw memcpy --- libraries/avatars/src/AvatarData.cpp | 454 +++++++++++---------------- libraries/avatars/src/AvatarData.h | 2 + libraries/shared/src/Packed.h | 12 + 3 files changed, 199 insertions(+), 269 deletions(-) create mode 100644 libraries/shared/src/Packed.h diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index ab7647b9fc..4379a0d72c 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -48,6 +48,38 @@ const glm::vec3 DEFAULT_LOCAL_AABOX_SCALE(1.0f); const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData"; +namespace AvatarDataPacket { + + PACKED_BEGIN struct Header { + float position[3]; // skeletal model's position + float globalPosition[3]; // avatar's position + uint16_t localOrientation[3]; // avatar's local euler angles (degrees, compressed) relative to the thing it's attached to + uint16_t scale; // (compressed) 'ratio' encoding uses sign bit as flag. + float lookAtPosition[3]; // world space position that eyes are focusing on. + float audioLoudness; // current loundess of microphone + uint8_t flags; + } PACKED_END; + const size_t HEADER_SIZE = 49; + + PACKED_BEGIN struct ParentInfo { + uint8_t parentUUID[16]; // rfc 4122 encoded + uint16_t parentJointIndex; + } PACKED_END; + const size_t PARENT_INFO_SIZE = 18; + + PACKED_BEGIN struct FaceTrackerInfo { + float leftEyeBlink; + float rightEyeBlink; + float averageLoudness; + float browAudioLift; + uint8_t numBlendshapeCoefficients; + // float blendshapeCoefficients[numBlendshapeCoefficients]; + } PACKED_END; + const size_t FACE_TRACKER_INFO_SIZE = 17; +} + +#define ASSERT(COND) do { if (!(COND)) { int* bad = nullptr; *bad = 0xbad; } } while(0) + AvatarData::AvatarData() : SpatiallyNestable(NestableType::Avatar, QUuid()), _handPosition(0.0f), @@ -68,6 +100,10 @@ AvatarData::AvatarData() : setBodyPitch(0.0f); setBodyYaw(-90.0f); setBodyRoll(0.0f); + + ASSERT(sizeof AvatarDataPacket::Header == AvatarDataPacket::HEADER_SIZE); + ASSERT(sizeof AvatarDataPacket::ParentInfo == AvatarDataPacket::PARENT_INFO_SIZE); + ASSERT(sizeof AvatarDataPacket::FaceTrackerInfo == AvatarDataPacket::FACE_TRACKER_INFO_SIZE); } AvatarData::~AvatarData() { @@ -141,81 +177,67 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); unsigned char* startPosition = destinationBuffer; - const glm::vec3& position = getLocalPosition(); - memcpy(destinationBuffer, &position, sizeof(position)); - destinationBuffer += sizeof(position); + auto header = reinterpret_cast(destinationBuffer); + header->position[0] = getLocalPosition().x; + header->position[1] = getLocalPosition().y; + header->position[2] = getLocalPosition().z; + header->globalPosition[0] = _globalPosition.x; + header->globalPosition[1] = _globalPosition.y; + header->globalPosition[2] = _globalPosition.z; - memcpy(destinationBuffer, &_globalPosition, sizeof(_globalPosition)); - destinationBuffer += sizeof(_globalPosition); - - // Body rotation glm::vec3 bodyEulerAngles = glm::degrees(safeEulerAngles(getLocalOrientation())); - destinationBuffer += packFloatAngleToTwoByte(destinationBuffer, bodyEulerAngles.y); - destinationBuffer += packFloatAngleToTwoByte(destinationBuffer, bodyEulerAngles.x); - destinationBuffer += packFloatAngleToTwoByte(destinationBuffer, bodyEulerAngles.z); + packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 0), bodyEulerAngles.y); + packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 1), bodyEulerAngles.x); + packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 2), bodyEulerAngles.z); + packFloatRatioToTwoByte((uint8_t*)(&header->scale), _targetScale); + header->lookAtPosition[0] = _headData->_lookAtPosition.x; + header->lookAtPosition[1] = _headData->_lookAtPosition.y; + header->lookAtPosition[2] = _headData->_lookAtPosition.z; + header->audioLoudness = _headData->_audioLoudness; - // Body scale - destinationBuffer += packFloatRatioToTwoByte(destinationBuffer, _targetScale); - - // Lookat Position - memcpy(destinationBuffer, &_headData->_lookAtPosition, sizeof(_headData->_lookAtPosition)); - destinationBuffer += sizeof(_headData->_lookAtPosition); - - // Instantaneous audio loudness (used to drive facial animation) - memcpy(destinationBuffer, &_headData->_audioLoudness, sizeof(float)); - destinationBuffer += sizeof(float); - - // bitMask of less than byte wide items - unsigned char bitItems = 0; - - // key state - setSemiNibbleAt(bitItems,KEY_STATE_START_BIT,_keyState); + setSemiNibbleAt(header->flags, KEY_STATE_START_BIT, _keyState); // hand state bool isFingerPointing = _handState & IS_FINGER_POINTING_FLAG; - setSemiNibbleAt(bitItems, HAND_STATE_START_BIT, _handState & ~IS_FINGER_POINTING_FLAG); + setSemiNibbleAt(header->flags, HAND_STATE_START_BIT, _handState & ~IS_FINGER_POINTING_FLAG); if (isFingerPointing) { - setAtBit(bitItems, HAND_STATE_FINGER_POINTING_BIT); + setAtBit(header->flags, HAND_STATE_FINGER_POINTING_BIT); } // faceshift state if (_headData->_isFaceTrackerConnected) { - setAtBit(bitItems, IS_FACESHIFT_CONNECTED); + setAtBit(header->flags, IS_FACESHIFT_CONNECTED); } // eye tracker state if (_headData->_isEyeTrackerConnected) { - setAtBit(bitItems, IS_EYE_TRACKER_CONNECTED); + setAtBit(header->flags, IS_EYE_TRACKER_CONNECTED); } // referential state QUuid parentID = getParentID(); if (!parentID.isNull()) { - setAtBit(bitItems, HAS_REFERENTIAL); + setAtBit(header->flags, HAS_REFERENTIAL); } - *destinationBuffer++ = bitItems; + destinationBuffer += sizeof(AvatarDataPacket::Header); if (!parentID.isNull()) { + auto parentInfo = reinterpret_cast(destinationBuffer); QByteArray referentialAsBytes = parentID.toRfc4122(); - memcpy(destinationBuffer, referentialAsBytes.data(), referentialAsBytes.size()); - destinationBuffer += referentialAsBytes.size(); - memcpy(destinationBuffer, &_parentJointIndex, sizeof(_parentJointIndex)); - destinationBuffer += sizeof(_parentJointIndex); + memcpy(parentInfo->parentUUID, referentialAsBytes.data(), referentialAsBytes.size()); + parentInfo->parentJointIndex = _parentJointIndex; + destinationBuffer += sizeof(AvatarDataPacket::ParentInfo); } // If it is connected, pack up the data if (_headData->_isFaceTrackerConnected) { - memcpy(destinationBuffer, &_headData->_leftEyeBlink, sizeof(float)); - destinationBuffer += sizeof(float); + auto faceTrackerInfo = reinterpret_cast(destinationBuffer); - memcpy(destinationBuffer, &_headData->_rightEyeBlink, sizeof(float)); - destinationBuffer += sizeof(float); + faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink; + faceTrackerInfo->rightEyeBlink = _headData->_rightEyeBlink; + faceTrackerInfo->averageLoudness = _headData->_averageLoudness; + faceTrackerInfo->browAudioLift = _headData->_browAudioLift; + faceTrackerInfo->numBlendshapeCoefficients = _headData->_blendshapeCoefficients.size(); + destinationBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo); - memcpy(destinationBuffer, &_headData->_averageLoudness, sizeof(float)); - destinationBuffer += sizeof(float); - - memcpy(destinationBuffer, &_headData->_browAudioLift, sizeof(float)); - destinationBuffer += sizeof(float); - - *destinationBuffer++ = _headData->_blendshapeCoefficients.size(); - memcpy(destinationBuffer, _headData->_blendshapeCoefficients.data(), - _headData->_blendshapeCoefficients.size() * sizeof(float)); + // followed by a variable number of float coefficients + memcpy(destinationBuffer, _headData->_blendshapeCoefficients.data(), _headData->_blendshapeCoefficients.size() * sizeof(float)); destinationBuffer += _headData->_blendshapeCoefficients.size() * sizeof(float); } @@ -377,6 +399,16 @@ bool AvatarData::shouldLogError(const quint64& now) { return false; } +#define PACKET_READ_CHECK(ITEM_NAME, SIZE_TO_READ) \ + if ((endPosition - sourceBuffer) < (int)SIZE_TO_READ) { \ + if (shouldLogError(now)) { \ + qCWarning(avatars) << "AvatarData packet too small, attempting to read " << \ + #ITEM_NAME << ", only " << (endPosition - sourceBuffer) << \ + " bytes left, " << getSessionUUID(); \ + } \ + return buffer.size(); \ + } + // read data in packet starting at byte offset and return number of bytes parsed int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { @@ -386,124 +418,76 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } const unsigned char* startPosition = reinterpret_cast(buffer.data()); + const unsigned char* endPosition = startPosition + buffer.size(); const unsigned char* sourceBuffer = startPosition; quint64 now = usecTimestampNow(); - // The absolute minimum size of the update data is as follows: - // 36 bytes of "plain old data" { - // position = 12 bytes - // bodyYaw = 2 (compressed float) - // bodyPitch = 2 (compressed float) - // bodyRoll = 2 (compressed float) - // targetScale = 2 (compressed float) - // lookAt = 12 - // audioLoudness = 4 - // } - // + 1 byte for varying data - // + 1 byte for numJoints (0) - // = 39 bytes - int minPossibleSize = 39; + PACKET_READ_CHECK(Header, sizeof(AvatarDataPacket::Header)); + auto header = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(AvatarDataPacket::Header); - int maxAvailableSize = buffer.size(); - if (minPossibleSize > maxAvailableSize) { + glm::vec3 position = glm::vec3(header->position[0], header->position[1], header->position[2]); + _globalPosition = glm::vec3(header->globalPosition[0], header->globalPosition[1], header->globalPosition[2]); + if (glm::isnan(position.x) || glm::isnan(position.y) || glm::isnan(position.z)) { if (shouldLogError(now)) { - qCDebug(avatars) << "Malformed AvatarData packet at the start; " - << " displayName = '" << _displayName << "'" - << " minPossibleSize = " << minPossibleSize - << " maxAvailableSize = " << maxAvailableSize; + qCWarning(avatars) << "Discard AvatarData packet: position NaN, uuid " << getSessionUUID(); } - // this packet is malformed so we report all bytes as consumed - return maxAvailableSize; + return buffer.size(); + } + setLocalPosition(position); + + float pitch, yaw, roll; + unpackFloatAngleFromTwoByte(header->localOrientation + 0, &yaw); + unpackFloatAngleFromTwoByte(header->localOrientation + 1, &pitch); + unpackFloatAngleFromTwoByte(header->localOrientation + 2, &roll); + if (glm::isnan(yaw) || glm::isnan(pitch) || glm::isnan(roll)) { + if (shouldLogError(now)) { + qCWarning(avatars) << "Discard AvatarData packet: localOriention is NaN, uuid " << getSessionUUID(); + } + return buffer.size(); } - { // Body world position, rotation, and scale - // position - glm::vec3 position; - memcpy(&position, sourceBuffer, sizeof(position)); - sourceBuffer += sizeof(position); + glm::quat currentOrientation = getLocalOrientation(); + glm::vec3 newEulerAngles(pitch, yaw, roll); + glm::quat newOrientation = glm::quat(glm::radians(newEulerAngles)); + if (currentOrientation != newOrientation) { + _hasNewJointRotations = true; + setLocalOrientation(newOrientation); + } - memcpy(&_globalPosition, sourceBuffer, sizeof(_globalPosition)); - sourceBuffer += sizeof(_globalPosition); - - if (glm::isnan(position.x) || glm::isnan(position.y) || glm::isnan(position.z)) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Discard nan AvatarData::position; displayName = '" << _displayName << "'"; - } - return maxAvailableSize; + float scale; + unpackFloatRatioFromTwoByte((uint8_t*)&header->scale, scale); + if (glm::isnan(scale)) { + if (shouldLogError(now)) { + qCWarning(avatars) << "Discard AvatarData packet: scale NaN, uuid " << getSessionUUID(); } - setLocalPosition(position); + return buffer.size(); + } + _targetScale = std::max(MIN_AVATAR_SCALE, std::min(MAX_AVATAR_SCALE, scale)); - // rotation (NOTE: This needs to become a quaternion to save two bytes) - float yaw, pitch, roll; - sourceBuffer += unpackFloatAngleFromTwoByte((uint16_t*) sourceBuffer, &yaw); - sourceBuffer += unpackFloatAngleFromTwoByte((uint16_t*) sourceBuffer, &pitch); - sourceBuffer += unpackFloatAngleFromTwoByte((uint16_t*) sourceBuffer, &roll); - if (glm::isnan(yaw) || glm::isnan(pitch) || glm::isnan(roll)) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Discard nan AvatarData::yaw,pitch,roll; displayName = '" << _displayName << "'"; - } - return maxAvailableSize; + glm::vec3 lookAt = glm::vec3(header->lookAtPosition[0], header->lookAtPosition[1], header->lookAtPosition[2]); + if (glm::isnan(lookAt.x) || glm::isnan(lookAt.y) || glm::isnan(lookAt.z)) { + if (shouldLogError(now)) { + qCWarning(avatars) << "Discard AvatarData packet: lookAtPosition is NaN, uuid " << getSessionUUID(); } + return buffer.size(); + } + _headData->_lookAtPosition = lookAt; - // TODO is this safe? will the floats not exactly match? - // Andrew says: - // Yes, there is a possibility that the transmitted will not quite match the extracted despite being originally - // extracted from the exact same quaternion. I followed the code through and it appears the risk is that the - // avatar's SkeletonModel might fall into the CPU expensive part of Model::updateClusterMatrices() when otherwise it - // would not have required it. However, we know we can update many simultaneously animating avatars, and most - // avatars will be moving constantly anyway, so I don't think we need to worry. - glm::quat currentOrientation = getLocalOrientation(); - glm::vec3 newEulerAngles(pitch, yaw, roll); - glm::quat newOrientation = glm::quat(glm::radians(newEulerAngles)); - if (currentOrientation != newOrientation) { - _hasNewJointRotations = true; - setLocalOrientation(newOrientation); + float audioLoudness = header->audioLoudness; + if (glm::isnan(audioLoudness)) { + if (shouldLogError(now)) { + qCWarning(avatars) << "Discard AvatarData packet: audioLoudness is NaN, uuid " << getSessionUUID(); } - - // scale - float scale; - sourceBuffer += unpackFloatRatioFromTwoByte(sourceBuffer, scale); - if (glm::isnan(scale)) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Discard nan AvatarData::scale; displayName = '" << _displayName << "'"; - } - return maxAvailableSize; - } - _targetScale = std::max(MIN_AVATAR_SCALE, std::min(MAX_AVATAR_SCALE, scale)); - } // 20 bytes - - { // Lookat Position - glm::vec3 lookAt; - memcpy(&lookAt, sourceBuffer, sizeof(lookAt)); - sourceBuffer += sizeof(lookAt); - if (glm::isnan(lookAt.x) || glm::isnan(lookAt.y) || glm::isnan(lookAt.z)) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Discard nan AvatarData::lookAt; displayName = '" << _displayName << "'"; - } - return maxAvailableSize; - } - _headData->_lookAtPosition = lookAt; - } // 12 bytes - - { // AudioLoudness - // Instantaneous audio loudness (used to drive facial animation) - float audioLoudness; - memcpy(&audioLoudness, sourceBuffer, sizeof(float)); - sourceBuffer += sizeof(float); - if (glm::isnan(audioLoudness)) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Discard nan AvatarData::audioLoudness; displayName = '" << _displayName << "'"; - } - return maxAvailableSize; - } - _headData->_audioLoudness = audioLoudness; - } // 4 bytes + return buffer.size(); + } + _headData->_audioLoudness = audioLoudness; { // bitFlags and face data - unsigned char bitItems = *sourceBuffer++; + uint8_t bitItems = header->flags; // key state, stored as a semi-nibble in the bitItems - _keyState = (KeyState)getSemiNibbleAt(bitItems,KEY_STATE_START_BIT); + _keyState = (KeyState)getSemiNibbleAt(bitItems, KEY_STATE_START_BIT); // hand state, stored as a semi-nibble plus a bit in the bitItems // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split @@ -520,95 +504,48 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { bool hasReferential = oneAtBit(bitItems, HAS_REFERENTIAL); if (hasReferential) { - const int sizeOfPackedUuid = 16; - QByteArray referentialAsBytes((const char*)sourceBuffer, sizeOfPackedUuid); - _parentID = QUuid::fromRfc4122(referentialAsBytes); - sourceBuffer += sizeOfPackedUuid; - memcpy(&_parentJointIndex, sourceBuffer, sizeof(_parentJointIndex)); - sourceBuffer += sizeof(_parentJointIndex); + PACKET_READ_CHECK(ParentInfo, sizeof(AvatarDataPacket::ParentInfo)); + auto parentInfo = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(AvatarDataPacket::ParentInfo); + + const size_t RFC_4122_SIZE = 16; + QByteArray byteArray((const char*)parentInfo->parentUUID, RFC_4122_SIZE); + _parentID = QUuid::fromRfc4122(byteArray); + _parentJointIndex = parentInfo->parentJointIndex; } else { _parentID = QUuid(); } if (_headData->_isFaceTrackerConnected) { - float leftEyeBlink, rightEyeBlink, averageLoudness, browAudioLift; - minPossibleSize += sizeof(leftEyeBlink) + sizeof(rightEyeBlink) + sizeof(averageLoudness) + sizeof(browAudioLift); - minPossibleSize++; // one byte for blendDataSize - if (minPossibleSize > maxAvailableSize) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Malformed AvatarData packet after BitItems;" - << " displayName = '" << _displayName << "'" - << " minPossibleSize = " << minPossibleSize - << " maxAvailableSize = " << maxAvailableSize; - } - return maxAvailableSize; - } - // unpack face data - memcpy(&leftEyeBlink, sourceBuffer, sizeof(float)); - sourceBuffer += sizeof(float); + PACKET_READ_CHECK(FaceTrackerInfo, sizeof(AvatarDataPacket::FaceTrackerInfo)); + auto faceTrackerInfo = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo); - memcpy(&rightEyeBlink, sourceBuffer, sizeof(float)); - sourceBuffer += sizeof(float); + _headData->_leftEyeBlink = faceTrackerInfo->leftEyeBlink; + _headData->_rightEyeBlink = faceTrackerInfo->rightEyeBlink; + _headData->_averageLoudness = faceTrackerInfo->averageLoudness; + _headData->_browAudioLift = faceTrackerInfo->browAudioLift; - memcpy(&averageLoudness, sourceBuffer, sizeof(float)); - sourceBuffer += sizeof(float); - - memcpy(&browAudioLift, sourceBuffer, sizeof(float)); - sourceBuffer += sizeof(float); - - if (glm::isnan(leftEyeBlink) || glm::isnan(rightEyeBlink) - || glm::isnan(averageLoudness) || glm::isnan(browAudioLift)) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Discard nan AvatarData::faceData; displayName = '" << _displayName << "'"; - } - return maxAvailableSize; - } - _headData->_leftEyeBlink = leftEyeBlink; - _headData->_rightEyeBlink = rightEyeBlink; - _headData->_averageLoudness = averageLoudness; - _headData->_browAudioLift = browAudioLift; - - int numCoefficients = (int)(*sourceBuffer++); - int blendDataSize = numCoefficients * sizeof(float); - minPossibleSize += blendDataSize; - if (minPossibleSize > maxAvailableSize) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Malformed AvatarData packet after Blendshapes;" - << " displayName = '" << _displayName << "'" - << " minPossibleSize = " << minPossibleSize - << " maxAvailableSize = " << maxAvailableSize; - } - return maxAvailableSize; - } - - _headData->_blendshapeCoefficients.resize(numCoefficients); - memcpy(_headData->_blendshapeCoefficients.data(), sourceBuffer, blendDataSize); - sourceBuffer += numCoefficients * sizeof(float); - - //bitItemsDataSize = 4 * sizeof(float) + 1 + blendDataSize; + int numCoefficients = faceTrackerInfo->numBlendshapeCoefficients; + const int coefficientsSize = sizeof(float) * numCoefficients; + PACKET_READ_CHECK(FaceTrackerCoefficients, coefficientsSize); + _headData->_blendshapeCoefficients.resize(numCoefficients); // make sure there's room for the copy! + memcpy(_headData->_blendshapeCoefficients.data(), sourceBuffer, coefficientsSize); + sourceBuffer += coefficientsSize; } - } // 1 + bitItemsDataSize bytes + } - // joint rotations + PACKET_READ_CHECK(NumJoints, sizeof(uint8_t)); int numJoints = *sourceBuffer++; - int bytesOfValidity = (int)ceil((float)numJoints / (float)BITS_IN_BYTE); - minPossibleSize += bytesOfValidity; - if (minPossibleSize > maxAvailableSize) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Malformed AvatarData packet after JointValidityBits;" - << " displayName = '" << _displayName << "'" - << " minPossibleSize = " << minPossibleSize - << " maxAvailableSize = " << maxAvailableSize; - } - return maxAvailableSize; - } - int numValidJointRotations = 0; _jointData.resize(numJoints); + const int bytesOfValidity = (int)ceil((float)numJoints / (float)BITS_IN_BYTE); + PACKET_READ_CHECK(JointRotationValidityBits, bytesOfValidity); + + int numValidJointRotations = 0; QVector validRotations; validRotations.resize(numJoints); - { // rotation validity bits unsigned char validity = 0; int validityBit = 0; @@ -623,39 +560,26 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { validRotations[i] = valid; validityBit = (validityBit + 1) % BITS_IN_BYTE; } - } // 1 + bytesOfValidity bytes - - // each joint rotation is stored in 6 bytes. - - const size_t COMPRESSED_QUATERNION_SIZE = 6; - minPossibleSize += numValidJointRotations * COMPRESSED_QUATERNION_SIZE; - if (minPossibleSize > maxAvailableSize) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Malformed AvatarData packet after JointData rotation validity;" - << " displayName = '" << _displayName << "'" - << " minPossibleSize = " << minPossibleSize - << " maxAvailableSize = " << maxAvailableSize; - } - return maxAvailableSize; } - { // joint data - for (int i = 0; i < numJoints; i++) { - JointData& data = _jointData[i]; - if (validRotations[i]) { - sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); - _hasNewJointRotations = true; - data.rotationSet = true; - } + // each joint rotation is stored in 6 bytes. + const int COMPRESSED_QUATERNION_SIZE = 6; + PACKET_READ_CHECK(JointRotations, numValidJointRotations * COMPRESSED_QUATERNION_SIZE); + for (int i = 0; i < numJoints; i++) { + JointData& data = _jointData[i]; + if (validRotations[i]) { + sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); + _hasNewJointRotations = true; + data.rotationSet = true; } - } // numJoints * 6 bytes + } + + PACKET_READ_CHECK(JointTranslationValidityBits, bytesOfValidity); - // joint translations // get translation validity bits -- these indicate which translations were packed int numValidJointTranslations = 0; QVector validTranslations; validTranslations.resize(numJoints); - { // translation validity bits unsigned char validity = 0; int validityBit = 0; @@ -673,30 +597,18 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } // 1 + bytesOfValidity bytes // each joint translation component is stored in 6 bytes. - const size_t COMPRESSED_TRANSLATION_SIZE = 6; - minPossibleSize += numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE; - if (minPossibleSize > maxAvailableSize) { - if (shouldLogError(now)) { - qCDebug(avatars) << "Malformed AvatarData packet after JointData translation validity;" - << " displayName = '" << _displayName << "'" - << " minPossibleSize = " << minPossibleSize - << " maxAvailableSize = " << maxAvailableSize; - } - return maxAvailableSize; - } - + const int COMPRESSED_TRANSLATION_SIZE = 6; + PACKET_READ_CHECK(JointTranslation, numValidJointTranslations * COMPRESSED_QUATERNION_SIZE); const int TRANSLATION_COMPRESSION_RADIX = 12; - { // joint data - for (int i = 0; i < numJoints; i++) { - JointData& data = _jointData[i]; - if (validTranslations[i]) { - sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); - _hasNewJointTranslations = true; - data.translationSet = true; - } + for (int i = 0; i < numJoints; i++) { + JointData& data = _jointData[i]; + if (validTranslations[i]) { + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); + _hasNewJointTranslations = true; + data.translationSet = true; } - } // numJoints * 6 bytes + } #ifdef WANT_DEBUG if (numValidJointRotations > 15) { @@ -707,6 +619,10 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { #endif int numBytesRead = sourceBuffer - startPosition; + + // AJT: Maybe make this a warning. + ASSERT(numBytesRead == buffer.size()); + _averageBytesReceived.updateAverage(numBytesRead); return numBytesRead; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 817d8aef09..dfb27411b8 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -53,6 +53,7 @@ typedef unsigned long long quint64; #include #include #include +#include #include "AABox.h" #include "HeadData.h" @@ -165,6 +166,7 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) public: + static const QString FRAME_NAME; static void fromFrame(const QByteArray& frameData, AvatarData& avatar); diff --git a/libraries/shared/src/Packed.h b/libraries/shared/src/Packed.h new file mode 100644 index 0000000000..3300634b96 --- /dev/null +++ b/libraries/shared/src/Packed.h @@ -0,0 +1,12 @@ +#ifndef hifi_Packed_h +#define hifi_Packed_h + +#if defined(_MSC_VER) +#define PACKED_BEGIN __pragma(pack(push, 1)) +#define PACKED_END __pragma(pack(pop)); +#else +#define PACKED_BEGIN +#define PACKED_END __attribute__((__packed__)); +#endif + +#endif From c73943ee19574a95bc9ecce380929dab71d4bffc Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 19 May 2016 20:32:08 -0700 Subject: [PATCH 0142/1237] macosx warning fixes --- libraries/avatars/src/AvatarData.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 4379a0d72c..7a20f24da8 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -101,9 +101,9 @@ AvatarData::AvatarData() : setBodyYaw(-90.0f); setBodyRoll(0.0f); - ASSERT(sizeof AvatarDataPacket::Header == AvatarDataPacket::HEADER_SIZE); - ASSERT(sizeof AvatarDataPacket::ParentInfo == AvatarDataPacket::PARENT_INFO_SIZE); - ASSERT(sizeof AvatarDataPacket::FaceTrackerInfo == AvatarDataPacket::FACE_TRACKER_INFO_SIZE); + ASSERT(sizeof(AvatarDataPacket::Header) == AvatarDataPacket::HEADER_SIZE); + ASSERT(sizeof(AvatarDataPacket::ParentInfo) == AvatarDataPacket::PARENT_INFO_SIZE); + ASSERT(sizeof(AvatarDataPacket::FaceTrackerInfo) == AvatarDataPacket::FACE_TRACKER_INFO_SIZE); } AvatarData::~AvatarData() { @@ -598,7 +598,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // each joint translation component is stored in 6 bytes. const int COMPRESSED_TRANSLATION_SIZE = 6; - PACKET_READ_CHECK(JointTranslation, numValidJointTranslations * COMPRESSED_QUATERNION_SIZE); + PACKET_READ_CHECK(JointTranslation, numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE); const int TRANSLATION_COMPRESSION_RADIX = 12; for (int i = 0; i < numJoints; i++) { From 8be6bc6460e18ab1647c12dd07218f91bf32c101 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 19 May 2016 22:00:22 -0700 Subject: [PATCH 0143/1237] Add missing overrides --- assignment-client/src/entities/EntityServer.h | 4 ++-- tests/controllers/src/main.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 1685f08e01..0486a97ede 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -60,8 +60,8 @@ public: virtual void trackViewerGone(const QUuid& sessionID) override; public slots: - virtual void nodeAdded(SharedNodePointer node); - virtual void nodeKilled(SharedNodePointer node); + virtual void nodeAdded(SharedNodePointer node) override; + virtual void nodeKilled(SharedNodePointer node) override; void pruneDeletedEntities(); protected: diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index e978dd9a38..3a5b4a4a4d 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -89,7 +89,7 @@ public: virtual GLWidget* getPrimaryWidget() override { return nullptr; } virtual MainWindow* getPrimaryWindow() override { return nullptr; } virtual QOpenGLContext* getPrimaryContext() override { return nullptr; } - virtual ui::Menu* getPrimaryMenu() { return nullptr; } + virtual ui::Menu* getPrimaryMenu() override { return nullptr; } virtual bool isForeground() override { return true; } virtual const DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } }; From b2bbf72be2b10e0801cb281d9d96f52f2bf449bd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 May 2016 18:48:51 +1200 Subject: [PATCH 0144/1237] Replace drive drop-down plus text field with a paths drop-down --- .../resources/qml/dialogs/FileDialog.qml | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 56b7cb0505..78d5943479 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -68,16 +68,12 @@ ModalWindow { Component.onCompleted: { console.log("Helper " + helper + " drives " + drives) - drivesSelector.onCurrentTextChanged.connect(function(){ - root.dir = helper.pathToUrl(drivesSelector.currentText); - }) - // HACK: The following two lines force the model to initialize properly such that: - // - Selecting a different drive at the initial screen updates the path displayed. - // - The go-up button works properly from the initial screen. - var initialFolder = currentDirectory.lastValidFolder; - root.dir = helper.pathToUrl(drivesSelector.currentText); - root.dir = helper.pathToUrl(initialFolder); + // HACK: The following lines force the model to initialize properly such that the go-up button + // works properly from the initial screen. + var initialFolder = folderListModel.folder; + fileTableModel.folder = helper.pathToUrl(drives[0]); + fileTableModel.folder = initialFolder; iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } @@ -97,15 +93,6 @@ ModalWindow { } spacing: hifi.dimensions.contentSpacing.x - // FIXME implement back button - //VrControls.ButtonAwesome { - // id: backButton - // text: "\uf0a8" - // size: currentDirectory.height - // enabled: d.backStack.length != 0 - // MouseArea { anchors.fill: parent; onClicked: d.navigateBack() } - //} - GlyphButton { id: upButton glyph: hifi.glyphs.levelUp @@ -124,20 +111,10 @@ ModalWindow { enabled: d.homeDestination ? true : false onClicked: d.navigateHome(); } - - ComboBox { - id: drivesSelector - width: 62 - model: drives - visible: drives.length > 1 - currentIndex: 0 - } } - TextField { - id: currentDirectory - property var lastValidFolder: helper.urlToPath(fileTableModel.folder) - height: homeButton.height + ComboBox { + id: pathSelector anchors { top: parent.top topMargin: hifi.dimensions.contentMargin.y @@ -146,23 +123,54 @@ ModalWindow { right: parent.right } - function capitalizeDrive(path) { - // Consistently capitalize drive letter for Windows. - if (/[a-zA-Z]:/.test(path)) { - return path.charAt(0).toUpperCase() + path.slice(1); + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + if (folders[0] !== "") { + choices.push(folders[0]); + } + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + choices.reverse(); + + if (drives && drives.length > 1) { + choices.push("This PC"); + } + + if (choices.length > 0) { + pathSelector.model = choices; } - return path; } - onLastValidFolderChanged: text = capitalizeDrive(lastValidFolder); + onLastValidFolderChanged: { + var folder = d.capitalizeDrive(lastValidFolder); + calculatePathChoices(folder); + } - // FIXME add support auto-completion - onAccepted: { - if (!helper.validFolder(text)) { - text = lastValidFolder; - return + onCurrentTextChanged: { + var folder = currentText; + + if (/^[a-zA-z]:$/.test(folder)) { + folder = "file:///" + folder + "/"; + } else if (folder === "This PC") { + folder = "file:///"; + } else { + folder = helper.pathToUrl(folder); + } + + if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + fileTableModel.folder = folder; } - fileTableModel.folder = helper.pathToUrl(text); } } @@ -219,12 +227,10 @@ ModalWindow { showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory - // For some reason, declaring these bindings directly in the targets doesn't - // work for setting the initial state Component.onCompleted: { - currentDirectory.lastValidFolder = helper.urlToPath(folder); showFiles = !root.selectDirectory } + onFolderChanged: { fileTableModel.update(); // Update once the data from the folder change is available. } @@ -310,7 +316,6 @@ ModalWindow { update(); } } - currentDirectory.lastValidFolder = helper.urlToPath(folder); } function isFolder(row) { From 2026226fe2b09a7664fd5b4a7f97246c7029584c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 May 2016 13:18:02 -0700 Subject: [PATCH 0145/1237] Handle vsync without halving fps --- interface/src/Application.cpp | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cf14d01771..6190de5875 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1822,28 +1822,37 @@ bool Application::event(QEvent* event) { return false; } - static bool justPresented = false; + // Presentation/painting logic + // TODO: Decouple presentation and painting loops + static bool isPainting = false; if ((int)event->type() == (int)Present) { - if (justPresented) { - justPresented = false; - - // If presentation is hogging the main thread, repost as low priority to avoid hanging the GUI. + if (isPainting) { + // If painting (triggered by presentation) is hogging the main thread, + // repost as low priority to avoid hanging the GUI. // This has the effect of allowing presentation to exceed the paint budget by X times and - // only dropping every (1/X) frames, instead of every ceil(X) frames. + // only dropping every (1/X) frames, instead of every ceil(X) frames // (e.g. at a 60FPS target, painting for 17us would fall to 58.82FPS instead of 30FPS). removePostedEvents(this, Present); postEvent(this, new QEvent(static_cast(Present)), Qt::LowEventPriority); + isPainting = false; return true; } idle(); + + postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); + isPainting = true; + return true; } else if ((int)event->type() == (int)Paint) { // NOTE: This must be updated as close to painting as possible, // or AvatarInputs will mysteriously move to the bottom-right AvatarInputs::getInstance()->update(); - justPresented = true; + paintGL(); + + isPainting = false; + return true; } @@ -2658,9 +2667,6 @@ void Application::idle() { // Sync up the _renderedFrameIndex _renderedFrameIndex = displayPlugin->presentCount(); - // Request a paint ASAP - postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority + 1); - // Update the deadlock watchdog updateHeartbeat(); @@ -2687,8 +2693,6 @@ void Application::idle() { _keyboardDeviceHasFocus = true; } - - // We're going to execute idle processing, so restart the last idle timer _lastTimeUpdated.start(); From fff260c2b9bcadd9d1a0d5597dd34b895edc491d Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 20 May 2016 09:27:50 -0700 Subject: [PATCH 0146/1237] Guard against zero-sized gl buffer copy --- libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp index e56d671891..ac337550ca 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp @@ -25,7 +25,7 @@ public: glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); - if (original) { + if (original && original->_size) { glBindBuffer(GL_COPY_WRITE_BUFFER, _buffer); glBindBuffer(GL_COPY_READ_BUFFER, original->_buffer); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, original->_size); From 7866fcf78c9bd44a2e0e9979136e05ec2c56070e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 18 May 2016 23:23:11 -0700 Subject: [PATCH 0147/1237] trim high-point convex hulls --- .../physics/src/PhysicalEntitySimulation.cpp | 7 +++++++ libraries/physics/src/ShapeFactory.cpp | 18 ++++++++++++++++++ libraries/shared/src/ShapeInfo.cpp | 12 ++++++++++++ libraries/shared/src/ShapeInfo.h | 1 + 4 files changed, 38 insertions(+) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 3fbf8ffaf5..95ae45bbb2 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -217,6 +217,13 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re } else if (entity->isReadyToComputeShape()) { ShapeInfo shapeInfo; entity->computeShapeInfo(shapeInfo); + int numPoints = shapeInfo.getMaxNumPoints(); + const int MAX_HULL_POINTS = 42; + if (numPoints > MAX_HULL_POINTS) { + glm::vec3 p = entity->getPosition(); + qDebug() << "entity" << entity->getName() << "at <" << p.x << p.y << p.z << ">" + << "has convex hull with" << numPoints << "points"; + } btCollisionShape* shape = ObjectMotionState::getShapeManager()->getShape(shapeInfo); if (shape) { EntityMotionState* motionState = new EntityMotionState(shape, entity); diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index de3e9cc794..8641873540 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -10,6 +10,7 @@ // #include +#include #include // for MILLIMETERS_PER_METER @@ -64,6 +65,23 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin correctedPoint = (points[i] - center) * relativeScale + center; hull->addPoint(btVector3(correctedPoint[0], correctedPoint[1], correctedPoint[2]), false); } + + const int MAX_HULL_POINTS = 42; + if (points.size() > MAX_HULL_POINTS) { + // create hull approximation + btShapeHull* shapeHull = new btShapeHull(hull); + shapeHull->buildHull(margin); + btConvexHullShape* newHull = new btConvexHullShape(); + const btVector3* newPoints = shapeHull->getVertexPointer(); + for (int i = 0; i < shapeHull->numVertices(); ++i) { + newHull->addPoint(newPoints[i], false); + } + delete hull; + delete shapeHull; + hull = newHull; + qDebug() << "reduced hull with" << points.size() << "points down to" << hull->getNumPoints(); // TODO: remove after testing + } + hull->recalcLocalAabb(); return hull; } diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 1d0cd56b86..0974c88e73 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -99,6 +99,18 @@ uint32_t ShapeInfo::getNumSubShapes() const { } return 1; } + +int ShapeInfo::getMaxNumPoints() const { + int numPoints = 0; + for (int i = 0; i < _points.size(); ++i) { + int n = _points[i].size(); + if (n > numPoints) { + numPoints = n; + } + } + return numPoints; +} + float ShapeInfo::computeVolume() const { const float DEFAULT_VOLUME = 1.0f; float volume = DEFAULT_VOLUME; diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 79390b6680..71fa12d5b5 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -61,6 +61,7 @@ public: void clearPoints () { _points.clear(); } void appendToPoints (const QVector& newPoints) { _points << newPoints; } + int getMaxNumPoints() const; float computeVolume() const; From 9fae1a7edd7ea016257cda3f0239db1de900be25 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 19 May 2016 00:16:25 -0700 Subject: [PATCH 0148/1237] tweak log format --- libraries/physics/src/PhysicalEntitySimulation.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 95ae45bbb2..15296e15ab 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -221,8 +221,10 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re const int MAX_HULL_POINTS = 42; if (numPoints > MAX_HULL_POINTS) { glm::vec3 p = entity->getPosition(); - qDebug() << "entity" << entity->getName() << "at <" << p.x << p.y << p.z << ">" - << "has convex hull with" << numPoints << "points"; + qWarning().nospace() << "convex hull with " << numPoints + << " points for entity " << entity->getName() + << " at <" << p.x << ", " << p.y << ", " << p.z << ">" + << " will be reduced"; } btCollisionShape* shape = ObjectMotionState::getShapeManager()->getShape(shapeInfo); if (shape) { From 5a826d696a8438f9ea93a772211ae45c66b4bd50 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 19 May 2016 09:14:48 -0700 Subject: [PATCH 0149/1237] cleanup after code review --- libraries/physics/src/PhysicalEntitySimulation.cpp | 1 - libraries/physics/src/ShapeFactory.cpp | 10 ++++------ libraries/shared/src/ShapeInfo.h | 1 + 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 15296e15ab..3b271f6a81 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -218,7 +218,6 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re ShapeInfo shapeInfo; entity->computeShapeInfo(shapeInfo); int numPoints = shapeInfo.getMaxNumPoints(); - const int MAX_HULL_POINTS = 42; if (numPoints > MAX_HULL_POINTS) { glm::vec3 p = entity->getPosition(); qWarning().nospace() << "convex hull with " << numPoints diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 8641873540..8c1d44b273 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -66,18 +66,16 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin hull->addPoint(btVector3(correctedPoint[0], correctedPoint[1], correctedPoint[2]), false); } - const int MAX_HULL_POINTS = 42; if (points.size() > MAX_HULL_POINTS) { // create hull approximation - btShapeHull* shapeHull = new btShapeHull(hull); - shapeHull->buildHull(margin); + btShapeHull shapeHull(hull); + shapeHull.buildHull(margin); btConvexHullShape* newHull = new btConvexHullShape(); - const btVector3* newPoints = shapeHull->getVertexPointer(); - for (int i = 0; i < shapeHull->numVertices(); ++i) { + const btVector3* newPoints = shapeHull.getVertexPointer(); + for (int i = 0; i < shapeHull.numVertices(); ++i) { newHull->addPoint(newPoints[i], false); } delete hull; - delete shapeHull; hull = newHull; qDebug() << "reduced hull with" << points.size() << "points down to" << hull->getNumPoints(); // TODO: remove after testing } diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 71fa12d5b5..7c76703b77 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -21,6 +21,7 @@ #include "DoubleHashKey.h" const float MIN_SHAPE_OFFSET = 0.001f; // offsets less than 1mm will be ignored +const int MAX_HULL_POINTS = 42; enum ShapeType { SHAPE_TYPE_NONE, From f3fc00bc3a2056798cd8ed3505cd18d831d03c30 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 19 May 2016 09:20:44 -0700 Subject: [PATCH 0150/1237] add comment for why MAX_HULL_POINTS is 42 --- libraries/shared/src/ShapeInfo.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 7c76703b77..1632d22450 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -21,6 +21,9 @@ #include "DoubleHashKey.h" const float MIN_SHAPE_OFFSET = 0.001f; // offsets less than 1mm will be ignored + +// Bullet has a mesh generation util for convex shapes that we used to +// trim convex hulls with many points down to only 42 points. const int MAX_HULL_POINTS = 42; enum ShapeType { From 83b8ef8131a6d44b40d8e35b73921c789f08c6e3 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 19 May 2016 13:18:18 -0700 Subject: [PATCH 0151/1237] clean up stream formatting of vec3 and friends --- .../physics/src/PhysicalEntitySimulation.cpp | 8 ++-- libraries/shared/src/StreamUtils.cpp | 45 ++++++------------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 3b271f6a81..6806b3a398 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -219,11 +219,9 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re entity->computeShapeInfo(shapeInfo); int numPoints = shapeInfo.getMaxNumPoints(); if (numPoints > MAX_HULL_POINTS) { - glm::vec3 p = entity->getPosition(); - qWarning().nospace() << "convex hull with " << numPoints - << " points for entity " << entity->getName() - << " at <" << p.x << ", " << p.y << ", " << p.z << ">" - << " will be reduced"; + qWarning() << "convex hull with" << numPoints + << "points for entity" << entity->getName() + << "at" << entity->getPosition() << " will be reduced"; } btCollisionShape* shape = ObjectMotionState::getShapeManager()->getShape(shapeInfo); if (shape) { diff --git a/libraries/shared/src/StreamUtils.cpp b/libraries/shared/src/StreamUtils.cpp index d4378d82b3..876de2e698 100644 --- a/libraries/shared/src/StreamUtils.cpp +++ b/libraries/shared/src/StreamUtils.cpp @@ -23,7 +23,7 @@ void StreamUtil::dump(std::ostream& s, const QByteArray& buffer) { while (i < buffer.size()) { for(int j = 0; i < buffer.size() && j < row_size; ++j) { char byte = buffer[i]; - s << hex_digits[(byte >> 4) & 0x0f] << hex_digits[byte & 0x0f] << " "; + s << hex_digits[(byte >> 4) & 0x0f] << hex_digits[byte & 0x0f] << ' '; ++i; } s << "\n"; @@ -31,21 +31,21 @@ void StreamUtil::dump(std::ostream& s, const QByteArray& buffer) { } std::ostream& operator<<(std::ostream& s, const glm::vec3& v) { - s << "<" << v.x << " " << v.y << " " << v.z << ">"; + s << '(' << v.x << ' ' << v.y << ' ' << v.z << ')'; return s; } std::ostream& operator<<(std::ostream& s, const glm::quat& q) { - s << "<" << q.x << " " << q.y << " " << q.z << " " << q.w << ">"; + s << '(' << q.x << ' ' << q.y << ' ' << q.z << ' ' << q.w << ')'; return s; } std::ostream& operator<<(std::ostream& s, const glm::mat4& m) { - s << "["; + s << '['; for (int j = 0; j < 4; ++j) { - s << " " << m[0][j] << " " << m[1][j] << " " << m[2][j] << " " << m[3][j] << ";"; + s << ' ' << m[0][j] << ' ' << m[1][j] << ' ' << m[2][j] << ' ' << m[3][j] << ';'; } - s << " ]"; + s << " ]"; return s; } @@ -69,54 +69,37 @@ QDataStream& operator>>(QDataStream& in, glm::quat& quaternion) { #include QDebug& operator<<(QDebug& dbg, const glm::vec2& v) { - dbg.nospace() << "{type='glm::vec2'" - ", x=" << v.x << - ", y=" << v.y << - "}"; + dbg.nospace() << '(' << v.x << ", " << v.y << ')'; return dbg; } QDebug& operator<<(QDebug& dbg, const glm::vec3& v) { - dbg.nospace() << "{type='glm::vec3'" - ", x=" << v.x << - ", y=" << v.y << - ", z=" << v.z << - "}"; + dbg.nospace() << '(' << v.x << ", " << v.y << ", " << v.z << ')'; return dbg; } QDebug& operator<<(QDebug& dbg, const glm::vec4& v) { - dbg.nospace() << "{type='glm::vec4'" - ", x=" << v.x << - ", y=" << v.y << - ", z=" << v.z << - ", w=" << v.w << - "}"; + dbg.nospace() << '(' << v.x << ", " << v.y << ", " << v.z << ", " << v.w << ')'; return dbg; } QDebug& operator<<(QDebug& dbg, const glm::quat& q) { - dbg.nospace() << "{type='glm::quat'" - ", x=" << q.x << - ", y=" << q.y << - ", z=" << q.z << - ", w=" << q.w << - "}"; + dbg.nospace() << '(' << q.x << ", " << q.y << ", " << q.z << ", " << q.w << ')'; return dbg; } QDebug& operator<<(QDebug& dbg, const glm::mat4& m) { - dbg.nospace() << "{type='glm::mat4', ["; + dbg.nospace() << '['; for (int j = 0; j < 4; ++j) { dbg << ' ' << m[0][j] << ' ' << m[1][j] << ' ' << m[2][j] << ' ' << m[3][j] << ';'; } - return dbg << " ]}"; + return dbg << " ]"; } QDebug& operator<<(QDebug& dbg, const QVariantHash& v) { - dbg.nospace() << "["; + dbg.nospace() << "[ "; for (QVariantHash::const_iterator it = v.constBegin(); it != v.constEnd(); it++) { - dbg << it.key() << ":" << it.value(); + dbg << it.key() << ':' << it.value(); } return dbg << " ]"; } From aa58cad93e51ef078a3d6230c15d99b94cf5f86f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 09:54:54 -0700 Subject: [PATCH 0152/1237] code review --- libraries/script-engine/src/ScriptEngine.cpp | 7 +++++++ libraries/script-engine/src/ScriptEngine.h | 4 +++- libraries/script-engine/src/ScriptEngines.cpp | 5 +---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 67b87ae5e0..d8e0397347 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -938,6 +938,13 @@ void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { } void ScriptEngine::stop() { + _isStopping = true; // this can be done on any thread + + // marshal us over to the correct thread + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stop"); + return; + } if (!_isFinished) { _isFinished = true; emit runningStateChanged(); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 512e79fb95..6af6aef338 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -84,7 +84,7 @@ public: //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts - Q_INVOKABLE void stop(); + Q_INVOKABLE void stop(); // this can be called from any thread // Stop any evaluating scripts and wait for the scripting thread to finish. void waitTillDoneRunning(); @@ -146,6 +146,8 @@ public: bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget + + // these are used by code in ScriptEngines.cpp during the "reload all" operation bool isStopping() const { return _isStopping; } void setIsStopping() { _isStopping = true; } diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 8b57487db0..0d1f0de3c9 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -160,7 +160,6 @@ void ScriptEngines::shutdownScripting() { scriptEngine->disconnect(this); // Gracefully stop the engine's scripting thread - scriptEngine->setIsStopping(); scriptEngine->stop(); // We need to wait for the engine to be done running before we proceed, because we don't @@ -352,8 +351,7 @@ void ScriptEngines::stopAllScripts(bool restart) { reloadScript(scriptName); }); } - it.value()->setIsStopping(); - QMetaObject::invokeMethod(it.value(), "stop"); + it.value()->stop(); qCDebug(scriptengine) << "stopping script..." << it.key(); } } @@ -376,7 +374,6 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { reloadScript(scriptName); }); } - scriptEngine->setIsStopping(); scriptEngine->stop(); stoppedScript = true; qCDebug(scriptengine) << "stopping script..." << scriptURL; From 35065ab05ecb7384907f47a6360fda08c187cfd1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 10:12:38 -0700 Subject: [PATCH 0153/1237] remove unused setter --- libraries/script-engine/src/ScriptEngine.h | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 6af6aef338..2222503781 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -149,7 +149,6 @@ public: // these are used by code in ScriptEngines.cpp during the "reload all" operation bool isStopping() const { return _isStopping; } - void setIsStopping() { _isStopping = true; } bool isDebuggable() const { return _debuggable; } From 1ef0f8055bcd0f9b0a981bd24f16f6d6b8cb67fc Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 10:12:59 -0700 Subject: [PATCH 0154/1237] fix grammar in comment --- libraries/script-engine/src/ScriptEngine.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 2222503781..ef7e075021 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -147,7 +147,7 @@ public: bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget - // these are used by code in ScriptEngines.cpp during the "reload all" operation + // this is used by code in ScriptEngines.cpp during the "reload all" operation bool isStopping() const { return _isStopping; } bool isDebuggable() const { return _debuggable; } From 0353e562e4589e83322fd36a3e0f9c6c2f4e27a1 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 19 May 2016 16:55:13 -0700 Subject: [PATCH 0155/1237] entityProperties.html handles non object userData --- scripts/system/html/entityProperties.html | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index ae5684d6c8..e0bcf0efec 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -626,18 +626,19 @@ var parsedUserData = {} try { parsedUserData = JSON.parse(properties.userData); + + if ("grabbableKey" in parsedUserData) { + if ("grabbable" in parsedUserData["grabbableKey"]) { + elGrabbable.checked = parsedUserData["grabbableKey"].grabbable; + } + if ("wantsTrigger" in parsedUserData["grabbableKey"]) { + elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger; + } + if ("ignoreIK" in parsedUserData["grabbableKey"]) { + elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; + } + } } catch(e) {} - if ("grabbableKey" in parsedUserData) { - if ("grabbable" in parsedUserData["grabbableKey"]) { - elGrabbable.checked = parsedUserData["grabbableKey"].grabbable; - } - if ("wantsTrigger" in parsedUserData["grabbableKey"]) { - elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger; - } - if ("ignoreIK" in parsedUserData["grabbableKey"]) { - elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; - } - } elCollisionSoundURL.value = properties.collisionSoundURL; elLifetime.value = properties.lifetime; From 51c16739f2a68d7eaa33344b8b385b4919f2f5e9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 12:05:22 -0700 Subject: [PATCH 0156/1237] provide a getter for the place name in AddressManager --- libraries/networking/src/AddressManager.cpp | 5 +++++ libraries/networking/src/AddressManager.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index a97f4df35d..02962b636b 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -295,10 +295,13 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const // set our current root place name to the name that came back const QString PLACE_NAME_KEY = "name"; QString placeName = rootMap[PLACE_NAME_KEY].toString(); + if (!placeName.isEmpty()) { if (setHost(placeName, trigger)) { trigger = LookupTrigger::Internal; } + + _placeName = placeName; } else { if (setHost(domainIDString, trigger)) { trigger = LookupTrigger::Internal; @@ -580,7 +583,9 @@ bool AddressManager::setHost(const QString& host, LookupTrigger trigger, quint16 bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, LookupTrigger trigger) { bool hostChanged = setHost(hostname, trigger, port); + // clear any current place information _rootPlaceID = QUuid(); + _placeName.clear(); qCDebug(networking) << "Possible domain change required to connect to domain at" << hostname << "on" << port; diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index dd0dbd9f38..643924ff5c 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -58,6 +58,7 @@ public: const QString currentPath(bool withOrientation = true) const; const QUuid& getRootPlaceID() const { return _rootPlaceID; } + const QString& getPlaceName() const { return _placeName; } const QString& getHost() const { return _host; } @@ -141,6 +142,7 @@ private: QString _host; quint16 _port; + QString _placeName; QUuid _rootPlaceID; PositionGetter _positionGetter; OrientationGetter _orientationGetter; From 7110fe98eba05dd7928aef5cf259eb23dbb6ba62 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 12:13:22 -0700 Subject: [PATCH 0157/1237] associate incoming place name with DomainServerNodeData --- domain-server/src/DomainGatekeeper.cpp | 1 + domain-server/src/DomainServerNodeData.h | 5 +++++ domain-server/src/NodeConnectionData.cpp | 1 + domain-server/src/NodeConnectionData.h | 1 + libraries/networking/src/NodeList.cpp | 3 +++ libraries/networking/src/udt/PacketHeaders.cpp | 3 +++ 6 files changed, 14 insertions(+) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 919ac37ee9..76671461d3 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -105,6 +105,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer(node->getLinkedData()); nodeData->setSendingSockAddr(message->getSenderSockAddr()); nodeData->setNodeInterestSet(nodeConnection.interestList.toSet()); + nodeData->setPlaceName(nodeConnection.placeName); // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index cd68aa3006..3ef5415cba 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -56,6 +56,9 @@ public: void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue); void removeOverrideForKey(const QString& key, const QString& value); + + const QString& getPlaceName() { return _placeName; } + void setPlaceName(const QString& placeName) { _placeName; } private: QJsonObject overrideValuesIfNeeded(const QJsonObject& newStats); @@ -75,6 +78,8 @@ private: bool _isAuthenticated = true; NodeSet _nodeInterestSet; QString _nodeVersion; + + QString _placeName; }; #endif // hifi_DomainServerNodeData_h diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 80cb5950be..eabcfaacc6 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -19,6 +19,7 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c if (isConnectRequest) { dataStream >> newHeader.connectUUID; + dataStream >> newHeader.placeName; } dataStream >> newHeader.nodeType diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h index 6b3b8eb7c1..34119ffdab 100644 --- a/domain-server/src/NodeConnectionData.h +++ b/domain-server/src/NodeConnectionData.h @@ -27,6 +27,7 @@ public: HifiSockAddr localSockAddr; HifiSockAddr senderSockAddr; QList interestList; + QString placeName; }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 482d0366fd..cee6404755 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -312,6 +312,9 @@ void NodeList::sendDomainServerCheckIn() { // pack the connect UUID for this connect request packetStream << connectUUID; + + // pack the hostname information (so the domain-server can see which place name we came in on) + packetStream << DependencyManager::get()->getPlaceName(); } // pack our data to send to the domain-server diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e4aab94090..f6ad6b2970 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -58,6 +58,9 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AssetUpload: // Removal of extension from Asset requests return 18; + case PacketType::DomainConnectRequest: + // addition of referring hostname information + return 18; default: return 17; } From 3b2a9b7b98995298d44a316671c08331995e25b2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:02:34 -0700 Subject: [PATCH 0158/1237] fix set of place name on DomainServerNodeData --- domain-server/src/DomainServerNodeData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index 3ef5415cba..a14d7ff768 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -58,7 +58,7 @@ public: void removeOverrideForKey(const QString& key, const QString& value); const QString& getPlaceName() { return _placeName; } - void setPlaceName(const QString& placeName) { _placeName; } + void setPlaceName(const QString& placeName) { _placeName = placeName; } private: QJsonObject overrideValuesIfNeeded(const QJsonObject& newStats); From 5884e1eadd03e62d49c92a6dd7f7a222760dda56 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:03:18 -0700 Subject: [PATCH 0159/1237] rename metaverse heartbeat methods --- domain-server/src/DomainServer.cpp | 10 +++++----- domain-server/src/DomainServer.h | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 0aab6b7e31..e8199317f0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -462,7 +462,7 @@ void DomainServer::setupAutomaticNetworking() { nodeList->startSTUNPublicSocketUpdate(); } else { // send our heartbeat to data server so it knows what our network settings are - sendHeartbeatToDataServer(); + sendHeartbeatToMetaverse(); } } else { qDebug() << "Cannot enable domain-server automatic networking without a domain ID." @@ -471,7 +471,7 @@ void DomainServer::setupAutomaticNetworking() { return; } } else { - sendHeartbeatToDataServer(); + sendHeartbeatToMetaverse(); } qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; @@ -480,7 +480,7 @@ void DomainServer::setupAutomaticNetworking() { const int DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS = 15 * 1000; QTimer* dataHeartbeatTimer = new QTimer(this); - connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToDataServer())); + connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToMetaverse())); dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); } @@ -1029,11 +1029,11 @@ QJsonObject jsonForDomainSocketUpdate(const HifiSockAddr& socket) { const QString DOMAIN_UPDATE_AUTOMATIC_NETWORKING_KEY = "automatic_networking"; void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) { - sendHeartbeatToDataServer(newPublicSockAddr.getAddress().toString()); + sendHeartbeatToMetaverse(newPublicSockAddr.getAddress().toString()); } -void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) { +void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; auto nodeList = DependencyManager::get(); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 93bb5de494..fef3221b7d 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -71,7 +71,7 @@ private slots: void sendPendingTransactionsToServer(); void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr); - void sendHeartbeatToDataServer() { sendHeartbeatToDataServer(QString()); } + void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); } void sendHeartbeatToIceServer(); void handleConnectedNode(SharedNodePointer newNode); @@ -103,7 +103,7 @@ private: void setupAutomaticNetworking(); void setupICEHeartbeatForFullNetworking(); - void sendHeartbeatToDataServer(const QString& networkAddress); + void sendHeartbeatToMetaverse(const QString& networkAddress); void randomizeICEServerAddress(bool shouldTriggerHostLookup); From 7d2d60f2005e5f931bbcae12a1ab253360e80d8e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:08:24 -0700 Subject: [PATCH 0160/1237] split assigned and un-assigned nodes --- domain-server/src/DomainGatekeeper.cpp | 3 ++- domain-server/src/DomainServerNodeData.h | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 76671461d3..e974ab26f4 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -151,6 +151,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); + nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); @@ -283,7 +284,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // set the edit rights for this user newNode->setIsAllowedEditor(isAllowedEditor); newNode->setCanRez(canRez); - + // grab the linked data for our new node so we can set the username DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index a14d7ff768..f95403c779 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -59,6 +59,9 @@ public: const QString& getPlaceName() { return _placeName; } void setPlaceName(const QString& placeName) { _placeName = placeName; } + + bool wasAssigned() const { return _wasAssigned; }; + void setWasAssigned(bool wasAssigned) { _wasAssigned = wasAssigned; } private: QJsonObject overrideValuesIfNeeded(const QJsonObject& newStats); @@ -80,6 +83,8 @@ private: QString _nodeVersion; QString _placeName; + + bool _wasAssigned { false }; }; #endif // hifi_DomainServerNodeData_h From 962066c7d10fd9f5df126002f849b298477f0c06 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:19:38 -0700 Subject: [PATCH 0161/1237] send user hostname breakdown with heartbeat --- domain-server/src/DomainServer.cpp | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e8199317f0..75f1c9f6b6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1056,20 +1056,34 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { domainObject[RESTRICTED_ACCESS_FLAG] = _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); - // add the number of currently connected agent users - int numConnectedAuthedUsers = 0; + // figure out the breakdown of currently connected interface clients + int numConnectedUnassigned = 0; + QJsonObject userHostnames; - nodeList->eachNode([&numConnectedAuthedUsers](const SharedNodePointer& node){ - if (node->getLinkedData() && !static_cast(node->getLinkedData())->getUsername().isEmpty()) { - ++numConnectedAuthedUsers; + static const QString DEFAULT_HOSTNAME = "*"; + + nodeList->eachNode([&numConnectedUnassigned, &userHostnames](const SharedNodePointer& node) { + if (node->getLinkedData()) { + auto nodeData = static_cast(node->getLinkedData()); + + if (!nodeData->wasAssigned()) { + ++numConnectedUnassigned; + + // increment the count for this hostname (or the default if we don't have one) + auto hostname = nodeData->getPlaceName().isEmpty() ? DEFAULT_HOSTNAME : nodeData->getPlaceName(); + userHostnames[hostname] = userHostnames[hostname].toInt() + 1; + } } }); - const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; - const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; + static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; + static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; QJsonObject heartbeatObject; - heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedAuthedUsers; + heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; + heartbeatObject[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; + domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); From 5ab876114f86a715cc380c5214848a797dd9cf99 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:47:14 -0700 Subject: [PATCH 0162/1237] send hostname to DS with every DS packet to handle changes --- domain-server/src/DomainGatekeeper.cpp | 4 ++-- domain-server/src/DomainServer.cpp | 3 +++ domain-server/src/NodeConnectionData.cpp | 3 +-- libraries/networking/src/NodeList.cpp | 9 ++++----- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index e974ab26f4..61cc775e08 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -55,9 +55,9 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSize() == 0) { return; } - + QDataStream packetStream(message->getMessage()); - + // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr()); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 75f1c9f6b6..cfec72a24b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -678,6 +678,9 @@ void DomainServer::processListRequestPacket(QSharedPointer mess DomainServerNodeData* nodeData = reinterpret_cast(sendingNode->getLinkedData()); nodeData->setNodeInterestSet(nodeRequestData.interestList.toSet()); + // update the connecting hostname in case it has changed + nodeData->setPlaceName(nodeRequestData.placeName); + sendDomainListToNode(sendingNode, message->getSenderSockAddr()); } diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index eabcfaacc6..28f769298c 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -19,12 +19,11 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c if (isConnectRequest) { dataStream >> newHeader.connectUUID; - dataStream >> newHeader.placeName; } dataStream >> newHeader.nodeType >> newHeader.publicSockAddr >> newHeader.localSockAddr - >> newHeader.interestList; + >> newHeader.interestList >> newHeader.placeName; newHeader.senderSockAddr = senderSockAddr; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index cee6404755..c295ffc700 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -312,13 +312,12 @@ void NodeList::sendDomainServerCheckIn() { // pack the connect UUID for this connect request packetStream << connectUUID; - - // pack the hostname information (so the domain-server can see which place name we came in on) - packetStream << DependencyManager::get()->getPlaceName(); } - // pack our data to send to the domain-server - packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); + // pack our data to send to the domain-server including + // the hostname information (so the domain-server can see which place name we came in on) + packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList() + << DependencyManager::get()->getPlaceName(); if (!_domainHandler.isConnected()) { DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); From 33379824c8d0ad122d0e5435a9fa7c88ae59042b Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 15:15:22 -0700 Subject: [PATCH 0163/1237] clear place name if switching to a domain ID string --- libraries/networking/src/AddressManager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 02962b636b..36d9e621a9 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -306,6 +306,9 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const if (setHost(domainIDString, trigger)) { trigger = LookupTrigger::Internal; } + + // this isn't a place, so clear the place name + _placeName.clear(); } // check if we had a path to override the path returned From 76f4a25694f1d3adf82e92f0683a5ca759747878 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 11:40:46 -0700 Subject: [PATCH 0164/1237] move code that clears old scale and registration to after model is loaded --- .../src/RenderableModelEntityItem.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index a537ecd0f3..7b51283bac 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -48,13 +48,6 @@ RenderableModelEntityItem::~RenderableModelEntityItem() { void RenderableModelEntityItem::setModelURL(const QString& url) { auto& currentURL = getParsedModelURL(); - if (_model && (currentURL != url)) { - // The machinery for updateModelBounds will give existing models the opportunity to fix their translation/rotation/scale/registration. - // The first two are straightforward, but the latter two have guards to make sure they don't happen after they've already been set. - // Here we reset those guards. This doesn't cause the entity values to change -- it just allows the model to match once it comes in. - _model->setScaleToFit(false, getDimensions()); - _model->setSnapModelToRegistrationPoint(false, getRegistrationPoint()); - } ModelEntityItem::setModelURL(url); if (currentURL != getParsedModelURL() || !_model) { @@ -163,6 +156,14 @@ void RenderableModelEntityItem::remapTextures() { } void RenderableModelEntityItem::doInitialModelSimulation() { + // The machinery for updateModelBounds will give existing models the opportunity to fix their + // translation/rotation/scale/registration. The first two are straightforward, but the latter two have guards to + // make sure they don't happen after they've already been set. Here we reset those guards. This doesn't cause the + // entity values to change -- it just allows the model to match once it comes in. + _model->setScaleToFit(false, getDimensions()); + _model->setSnapModelToRegistrationPoint(false, getRegistrationPoint()); + + // now recalculate the bounds and registration _model->setScaleToFit(true, getDimensions()); _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); _model->setRotation(getRotation()); From c299aef8f214b39b5dd51acb831292be129d7f82 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 20 May 2016 12:15:30 -0700 Subject: [PATCH 0165/1237] fix 588, 558 --- scripts/system/controllers/handControllerPointer.js | 8 ++++++-- scripts/system/edit.js | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 78b7c4eb84..f4e4492a88 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -178,7 +178,10 @@ var NON_LINEAR_DIVISOR = 2; var MINIMUM_SEEK_DISTANCE = 0.01; function updateSeeking() { if (!Reticle.visible || isShakingMouse()) { - isSeeking = true; + if (!isSeeking) { + print('Start seeking mouse.'); + isSeeking = true; + } } // e.g., if we're about to turn it on with first movement. if (!isSeeking) { return; @@ -203,6 +206,7 @@ function updateSeeking() { } var okX = !updateDimension('x'), okY = !updateDimension('y'); // Evaluate both. Don't short-circuit. if (okX && okY) { + print('Finished seeking mouse'); isSeeking = false; } else { Reticle.setPosition(copy); // Not setReticlePosition @@ -444,7 +448,7 @@ function update() { updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); } -var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent. +var UPDATE_INTERVAL = 50; // milliseconds. Script.update is too frequent. var updater = Script.setInterval(update, UPDATE_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(updater); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index e84cdf7971..afbc679ec4 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -352,7 +352,9 @@ var toolBar = (function() { gridTool.setVisible(true); grid.setEnabled(true); propertiesTool.setVisible(true); - Window.setFocus(); + // Not sure what the following was meant to accomplish, but it currently causes + // everybody else to think that Interface has lost focus overall. fogbugzid:558 + // Window.setFocus(); } that.showTools(isActive); } From 770fab956fac0502b0a95d68f624d20d50bce533 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 May 2016 12:23:06 -0700 Subject: [PATCH 0166/1237] remove dead code --- interface/src/Application.cpp | 4 ---- interface/src/Menu.cpp | 3 --- interface/src/Menu.h | 1 - libraries/entities/src/EntityEditPacketSender.cpp | 4 +--- libraries/entities/src/EntityEditPacketSender.h | 4 ---- 5 files changed, 1 insertion(+), 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6190de5875..5ce869e5c9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3594,10 +3594,6 @@ void Application::update(float deltaTime) { int Application::sendNackPackets() { - if (Menu::getInstance()->isOptionChecked(MenuOption::DisableNackPackets)) { - return 0; - } - // iterates through all nodes in NodeList auto nodeList = DependencyManager::get(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b69bb8022..883c30a3b8 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -531,9 +531,6 @@ Menu::Menu() { // Developer > Network >>> MenuWrapper* networkMenu = developerMenu->addMenu("Network"); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableNackPackets, 0, false, - qApp->getEntityEditPacketSender(), - SLOT(toggleNackPackets())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 484be9f346..a58a103504 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -84,7 +84,6 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; - const QString DisableNackPackets = "Disable Entity NACK Packets"; const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 1e38c32964..ad936cc890 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -24,9 +24,7 @@ EntityEditPacketSender::EntityEditPacketSender() { } void EntityEditPacketSender::processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode) { - if (_shouldProcessNack) { - processNackPacket(*message, sendingNode); - } + processNackPacket(*message, sendingNode); } void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByteArray& buffer, qint64 clockSkew) { diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 26e4dd83ff..0492fc66f4 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -36,9 +36,5 @@ public: public slots: void processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode); - void toggleNackPackets() { _shouldProcessNack = !_shouldProcessNack; } - -private: - bool _shouldProcessNack = true; }; #endif // hifi_EntityEditPacketSender_h From 00bd3b2e8a60577f25d081078f4bfe1d98ea7dee Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 May 2016 13:43:08 -0700 Subject: [PATCH 0167/1237] remove more dead code --- interface/resources/qml/hifi/MenuOption.qml | 165 -------------------- 1 file changed, 165 deletions(-) delete mode 100644 interface/resources/qml/hifi/MenuOption.qml diff --git a/interface/resources/qml/hifi/MenuOption.qml b/interface/resources/qml/hifi/MenuOption.qml deleted file mode 100644 index 46cf5d9662..0000000000 --- a/interface/resources/qml/hifi/MenuOption.qml +++ /dev/null @@ -1,165 +0,0 @@ -import QtQuick 2.5 - -QtObject { - readonly property string aboutApp: "About Interface"; - readonly property string addRemoveFriends: "Add/Remove Friends..."; - readonly property string addressBar: "Show Address Bar"; - readonly property string animations: "Animations..."; - readonly property string animDebugDrawAnimPose: "Debug Draw Animation"; - readonly property string animDebugDrawDefaultPose: "Debug Draw Default Pose"; - readonly property string animDebugDrawPosition: "Debug Draw Position"; - readonly property string antialiasing: "Antialiasing"; - readonly property string assetMigration: "ATP Asset Migration"; - readonly property string assetServer: "Asset Server"; - readonly property string atmosphere: "Atmosphere"; - readonly property string attachments: "Attachments..."; - readonly property string audioNetworkStats: "Audio Network Stats"; - readonly property string audioNoiseReduction: "Audio Noise Reduction"; - readonly property string audioScope: "Show Scope"; - readonly property string audioScopeFiftyFrames: "Fifty"; - readonly property string audioScopeFiveFrames: "Five"; - readonly property string audioScopeFrames: "Display Frames"; - readonly property string audioScopePause: "Pause Scope"; - readonly property string audioScopeTwentyFrames: "Twenty"; - readonly property string audioStatsShowInjectedStreams: "Audio Stats Show Injected Streams"; - readonly property string audioTools: "Show Level Meter"; - readonly property string autoMuteAudio: "Auto Mute Microphone"; - readonly property string avatarReceiveStats: "Show Receive Stats"; - readonly property string back: "Back"; - readonly property string bandwidthDetails: "Bandwidth Details"; - readonly property string binaryEyelidControl: "Binary Eyelid Control"; - readonly property string bookmarkLocation: "Bookmark Location"; - readonly property string bookmarks: "Bookmarks"; - readonly property string cachesSize: "RAM Caches Size"; - readonly property string calibrateCamera: "Calibrate Camera"; - readonly property string cameraEntityMode: "Entity Mode"; - readonly property string centerPlayerInView: "Center Player In View"; - readonly property string chat: "Chat..."; - readonly property string collisions: "Collisions"; - readonly property string connexion: "Activate 3D Connexion Devices"; - readonly property string console_: "Console..."; - readonly property string controlWithSpeech: "Control With Speech"; - readonly property string copyAddress: "Copy Address to Clipboard"; - readonly property string copyPath: "Copy Path to Clipboard"; - readonly property string coupleEyelids: "Couple Eyelids"; - readonly property string crashInterface: "Crash Interface"; - readonly property string debugAmbientOcclusion: "Debug Ambient Occlusion"; - readonly property string decreaseAvatarSize: "Decrease Avatar Size"; - readonly property string deleteBookmark: "Delete Bookmark..."; - readonly property string disableActivityLogger: "Disable Activity Logger"; - readonly property string disableEyelidAdjustment: "Disable Eyelid Adjustment"; - readonly property string disableLightEntities: "Disable Light Entities"; - readonly property string disableNackPackets: "Disable Entity NACK Packets"; - readonly property string diskCacheEditor: "Disk Cache Editor"; - readonly property string displayCrashOptions: "Display Crash Options"; - readonly property string displayHandTargets: "Show Hand Targets"; - readonly property string displayModelBounds: "Display Model Bounds"; - readonly property string displayModelTriangles: "Display Model Triangles"; - readonly property string displayModelElementChildProxies: "Display Model Element Children"; - readonly property string displayModelElementProxy: "Display Model Element Bounds"; - readonly property string displayDebugTimingDetails: "Display Timing Details"; - readonly property string dontDoPrecisionPicking: "Don't Do Precision Picking"; - readonly property string dontRenderEntitiesAsScene: "Don't Render Entities as Scene"; - readonly property string echoLocalAudio: "Echo Local Audio"; - readonly property string echoServerAudio: "Echo Server Audio"; - readonly property string enable3DTVMode: "Enable 3DTV Mode"; - readonly property string enableCharacterController: "Enable avatar collisions"; - readonly property string expandMyAvatarSimulateTiming: "Expand /myAvatar/simulation"; - readonly property string expandMyAvatarTiming: "Expand /myAvatar"; - readonly property string expandOtherAvatarTiming: "Expand /otherAvatar"; - readonly property string expandPaintGLTiming: "Expand /paintGL"; - readonly property string expandUpdateTiming: "Expand /update"; - readonly property string faceshift: "Faceshift"; - readonly property string firstPerson: "First Person"; - readonly property string fivePointCalibration: "5 Point Calibration"; - readonly property string fixGaze: "Fix Gaze (no saccade)"; - readonly property string forward: "Forward"; - readonly property string frameTimer: "Show Timer"; - readonly property string fullscreenMirror: "Mirror"; - readonly property string help: "Help..."; - readonly property string increaseAvatarSize: "Increase Avatar Size"; - readonly property string independentMode: "Independent Mode"; - readonly property string inputMenu: "Avatar>Input Devices"; - readonly property string keyboardMotorControl: "Enable Keyboard Motor Control"; - readonly property string leapMotionOnHMD: "Leap Motion on HMD"; - readonly property string loadScript: "Open and Run Script File..."; - readonly property string loadScriptURL: "Open and Run Script from URL..."; - readonly property string lodTools: "LOD Tools"; - readonly property string login: "Login"; - readonly property string log: "Log"; - readonly property string logExtraTimings: "Log Extra Timing Details"; - readonly property string lowVelocityFilter: "Low Velocity Filter"; - readonly property string meshVisible: "Draw Mesh"; - readonly property string miniMirror: "Mini Mirror"; - readonly property string muteAudio: "Mute Microphone"; - readonly property string muteEnvironment: "Mute Environment"; - readonly property string muteFaceTracking: "Mute Face Tracking"; - readonly property string namesAboveHeads: "Names Above Heads"; - readonly property string noFaceTracking: "None"; - readonly property string octreeStats: "Entity Statistics"; - readonly property string onePointCalibration: "1 Point Calibration"; - readonly property string onlyDisplayTopTen: "Only Display Top Ten"; - readonly property string outputMenu: "Display"; - readonly property string packageModel: "Package Model..."; - readonly property string pair: "Pair"; - readonly property string physicsShowOwned: "Highlight Simulation Ownership"; - readonly property string physicsShowHulls: "Draw Collision Hulls"; - readonly property string pipelineWarnings: "Log Render Pipeline Warnings"; - readonly property string preferences: "General..."; - readonly property string quit: "Quit"; - readonly property string reloadAllScripts: "Reload All Scripts"; - readonly property string reloadContent: "Reload Content (Clears all caches)"; - readonly property string renderBoundingCollisionShapes: "Show Bounding Collision Shapes"; - readonly property string renderFocusIndicator: "Show Eye Focus"; - readonly property string renderLookAtTargets: "Show Look-at Targets"; - readonly property string renderLookAtVectors: "Show Look-at Vectors"; - readonly property string renderResolution: "Scale Resolution"; - readonly property string renderResolutionOne: "1"; - readonly property string renderResolutionTwoThird: "2/3"; - readonly property string renderResolutionHalf: "1/2"; - readonly property string renderResolutionThird: "1/3"; - readonly property string renderResolutionQuarter: "1/4"; - readonly property string renderAmbientLight: "Ambient Light"; - readonly property string renderAmbientLightGlobal: "Global"; - readonly property string renderAmbientLight0: "OLD_TOWN_SQUARE"; - readonly property string renderAmbientLight1: "GRACE_CATHEDRAL"; - readonly property string renderAmbientLight2: "EUCALYPTUS_GROVE"; - readonly property string renderAmbientLight3: "ST_PETERS_BASILICA"; - readonly property string renderAmbientLight4: "UFFIZI_GALLERY"; - readonly property string renderAmbientLight5: "GALILEOS_TOMB"; - readonly property string renderAmbientLight6: "VINE_STREET_KITCHEN"; - readonly property string renderAmbientLight7: "BREEZEWAY"; - readonly property string renderAmbientLight8: "CAMPUS_SUNSET"; - readonly property string renderAmbientLight9: "FUNSTON_BEACH_SUNSET"; - readonly property string resetAvatarSize: "Reset Avatar Size"; - readonly property string resetSensors: "Reset Sensors"; - readonly property string runningScripts: "Running Scripts..."; - readonly property string runTimingTests: "Run Timing Tests"; - readonly property string scriptEditor: "Script Editor..."; - readonly property string scriptedMotorControl: "Enable Scripted Motor Control"; - readonly property string showDSConnectTable: "Show Domain Connection Timing"; - readonly property string showBordersEntityNodes: "Show Entity Nodes"; - readonly property string showRealtimeEntityStats: "Show Realtime Entity Stats"; - readonly property string showWhosLookingAtMe: "Show Who's Looking at Me"; - readonly property string standingHMDSensorMode: "Standing HMD Sensor Mode"; - readonly property string simulateEyeTracking: "Simulate"; - readonly property string sMIEyeTracking: "SMI Eye Tracking"; - readonly property string stars: "Stars"; - readonly property string stats: "Stats"; - readonly property string stopAllScripts: "Stop All Scripts"; - readonly property string suppressShortTimings: "Suppress Timings Less than 10ms"; - readonly property string thirdPerson: "Third Person"; - readonly property string threePointCalibration: "3 Point Calibration"; - readonly property string throttleFPSIfNotFocus: "Throttle FPS If Not Focus"; // FIXME - this value duplicated in Basic2DWindowOpenGLDisplayPlugin.cpp - readonly property string toolWindow: "Tool Window"; - readonly property string transmitterDrive: "Transmitter Drive"; - readonly property string turnWithHead: "Turn using Head"; - readonly property string useAudioForMouth: "Use Audio for Mouth"; - readonly property string useCamera: "Use Camera"; - readonly property string velocityFilter: "Velocity Filter"; - readonly property string visibleToEveryone: "Everyone"; - readonly property string visibleToFriends: "Friends"; - readonly property string visibleToNoOne: "No one"; - readonly property string worldAxes: "World Axes"; -} - From 07562f72af18341b42069a12aa251c64e4e45e8e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 11 May 2016 17:23:24 -0700 Subject: [PATCH 0168/1237] Doing a pass over the input plugins and controller code --- interface/CMakeLists.txt | 4 - .../resources/controllers/spacemouse.json | 4 +- interface/src/Application.cpp | 55 ++--- interface/src/Menu.cpp | 13 +- interface/src/Menu.h | 2 +- .../controllers/src/controllers/Actions.cpp | 8 - .../controllers/src/controllers/Actions.h | 10 +- .../controllers/src/controllers/InputDevice.h | 4 +- .../src/controllers/StandardController.cpp | 6 - .../src/controllers/StandardController.h | 2 - .../src/controllers/StateController.h | 5 +- .../src/input-plugins/KeyboardMouseDevice.cpp | 7 +- .../src/input-plugins/KeyboardMouseDevice.h | 11 +- libraries/plugins/src/plugins/InputPlugin.h | 4 +- plugins/hifiNeuron/CMakeLists.txt | 11 +- plugins/hifiNeuron/src/NeuronPlugin.cpp | 118 ++------- plugins/hifiNeuron/src/NeuronPlugin.h | 5 +- plugins/hifiSdl2/src/Joystick.cpp | 11 +- plugins/hifiSdl2/src/Joystick.h | 15 +- plugins/hifiSdl2/src/SDL2Manager.cpp | 34 +-- plugins/hifiSdl2/src/SDL2Manager.h | 25 +- plugins/hifiSixense/src/SixenseManager.cpp | 18 +- plugins/hifiSixense/src/SixenseManager.h | 5 +- plugins/hifiSpacemouse/CMakeLists.txt | 18 ++ .../hifiSpacemouse/src}/SpacemouseManager.cpp | 230 +++++++++--------- .../hifiSpacemouse/src}/SpacemouseManager.h | 63 ++--- .../hifiSpacemouse/src/SpacemouseProvider.cpp | 45 ++++ plugins/hifiSpacemouse/src/plugin.json | 1 + .../oculus/src/OculusControllerManager.cpp | 30 ++- plugins/oculus/src/OculusControllerManager.h | 7 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 7 +- plugins/openvr/src/OpenVrHelpers.cpp | 7 + plugins/openvr/src/OpenVrHelpers.h | 3 +- plugins/openvr/src/ViveControllerManager.cpp | 17 +- plugins/openvr/src/ViveControllerManager.h | 20 +- tests/controllers/src/main.cpp | 4 +- 36 files changed, 333 insertions(+), 496 deletions(-) create mode 100644 plugins/hifiSpacemouse/CMakeLists.txt rename {libraries/input-plugins/src/input-plugins => plugins/hifiSpacemouse/src}/SpacemouseManager.cpp (95%) rename {libraries/input-plugins/src/input-plugins => plugins/hifiSpacemouse/src}/SpacemouseManager.h (77%) create mode 100644 plugins/hifiSpacemouse/src/SpacemouseProvider.cpp create mode 100644 plugins/hifiSpacemouse/src/plugin.json diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 4f58cf73db..4381f3321b 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -4,10 +4,6 @@ project(${TARGET_NAME}) # set a default root dir for each of our optional externals if it was not passed set(OPTIONAL_EXTERNALS "LeapMotion") -if(WIN32) - list(APPEND OPTIONAL_EXTERNALS "3DConnexionClient") -endif() - foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) string(TOUPPER ${EXTERNAL} ${EXTERNAL}_UPPERCASE) if (NOT ${${EXTERNAL}_UPPERCASE}_ROOT_DIR) diff --git a/interface/resources/controllers/spacemouse.json b/interface/resources/controllers/spacemouse.json index adcfef71a6..1480e1957d 100644 --- a/interface/resources/controllers/spacemouse.json +++ b/interface/resources/controllers/spacemouse.json @@ -2,11 +2,11 @@ "name": "Spacemouse to Standard", "channels": [ - { "from": "Spacemouse.TranslateX", "to": "Standard.RX" }, + { "from": "Spacemouse.TranslateX", "to": "Standard.LX" }, { "from": "Spacemouse.TranslateY", "to": "Standard.LY" }, { "from": "Spacemouse.TranslateZ", "to": "Standard.RY" }, - { "from": "Spacemouse.RotateZ", "to": "Standard.LX" }, + { "from": "Spacemouse.RotateZ", "to": "Standard.RX" }, { "from": "Spacemouse.LeftButton", "to": "Standard.LB" }, { "from": "Spacemouse.RightButton", "to": "Standard.RB" } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6190de5875..8ebb9a1f9d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -108,7 +108,6 @@ #include "audio/AudioScope.h" #include "avatar/AvatarManager.h" #include "CrashHandler.h" -#include "input-plugins/SpacemouseManager.h" #include "devices/DdeFaceTracker.h" #include "devices/EyeTracker.h" #include "devices/Faceshift.h" @@ -199,6 +198,7 @@ static const float PHYSICS_READY_RANGE = 3.0f; // how far from avatar to check f static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +static const QString INPUT_DEVICE_MENU_PREFIX = "Device: "; Setting::Handle maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS); const QHash Application::_acceptedExtensions { @@ -997,7 +997,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : RenderableWebEntityItem* webEntity = dynamic_cast(entity.get()); if (webEntity) { webEntity->setProxyWindow(_window->windowHandle()); - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->pluginFocusOutEvent(); } _keyboardFocusedItem = entityItemID; @@ -1121,7 +1121,7 @@ void Application::aboutToQuit() { emit beforeAboutToQuit(); foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - QString name = inputPlugin->getName(); + QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); QAction* action = Menu::getInstance()->getActionForOption(name); if (action->isChecked()) { inputPlugin->deactivate(); @@ -1440,8 +1440,7 @@ void Application::initializeUi() { // This will set up the input plugins UI _activeInputPlugins.clear(); foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - QString name = inputPlugin->getName(); - if (name == KeyboardMouseDevice::NAME) { + if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); } } @@ -1985,7 +1984,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } if (hasFocus()) { - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->keyPressEvent(event); } @@ -2319,7 +2318,7 @@ void Application::keyReleaseEvent(QKeyEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->keyReleaseEvent(event); } @@ -2351,7 +2350,7 @@ void Application::keyReleaseEvent(QKeyEvent* event) { void Application::focusOutEvent(QFocusEvent* event) { auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); foreach(auto inputPlugin, inputPlugins) { - QString name = inputPlugin->getName(); + QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); QAction* action = Menu::getInstance()->getActionForOption(name); if (action && action->isChecked()) { inputPlugin->pluginFocusOutEvent(); @@ -2438,7 +2437,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->mouseMoveEvent(event); } @@ -2475,7 +2474,7 @@ void Application::mousePressEvent(QMouseEvent* event) { if (hasFocus()) { - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->mousePressEvent(event); } @@ -2520,7 +2519,7 @@ void Application::mouseReleaseEvent(QMouseEvent* event) { } if (hasFocus()) { - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->mouseReleaseEvent(event); } @@ -2547,7 +2546,7 @@ void Application::touchUpdateEvent(QTouchEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->touchUpdateEvent(event); } } @@ -2565,7 +2564,7 @@ void Application::touchBeginEvent(QTouchEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->touchBeginEvent(event); } @@ -2582,7 +2581,7 @@ void Application::touchEndEvent(QTouchEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->touchEndEvent(event); } @@ -2598,7 +2597,7 @@ void Application::wheelEvent(QWheelEvent* event) const { return; } - if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { + if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->wheelEvent(event); } } @@ -2730,7 +2729,7 @@ void Application::idle() { getActiveDisplayPlugin()->idle(); auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); foreach(auto inputPlugin, inputPlugins) { - QString name = inputPlugin->getName(); + QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); QAction* action = Menu::getInstance()->getActionForOption(name); if (action && action->isChecked()) { inputPlugin->idle(); @@ -3373,22 +3372,18 @@ void Application::update(float deltaTime) { }; InputPluginPointer keyboardMousePlugin; - bool jointsCaptured = false; for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { keyboardMousePlugin = inputPlugin; } else if (inputPlugin->isActive()) { - inputPlugin->pluginUpdate(deltaTime, calibrationData, jointsCaptured); - if (inputPlugin->isJointController()) { - jointsCaptured = true; - } + inputPlugin->pluginUpdate(deltaTime, calibrationData); } } userInputMapper->update(deltaTime); if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { - keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData, jointsCaptured); + keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); } _controllerScriptingInterface->updateInputControllers(); @@ -5150,21 +5145,23 @@ void Application::updateDisplayMode() { Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } -static void addInputPluginToMenu(InputPluginPointer inputPlugin, bool active = false) { +static void addInputPluginToMenu(InputPluginPointer inputPlugin) { auto menu = Menu::getInstance(); - QString name = inputPlugin->getName(); + QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); Q_ASSERT(!menu->menuItemExists(MenuOption::InputMenu, name)); static QActionGroup* inputPluginGroup = nullptr; if (!inputPluginGroup) { inputPluginGroup = new QActionGroup(menu); + inputPluginGroup->setExclusive(false); } + auto parent = menu->getMenu(MenuOption::InputMenu); auto action = menu->addCheckableActionToQMenuAndActionHash(parent, - name, 0, active, qApp, + name, 0, true, qApp, SLOT(updateInputModes())); + inputPluginGroup->addAction(action); - inputPluginGroup->setExclusive(false); Q_ASSERT(menu->menuItemExists(MenuOption::InputMenu, name)); } @@ -5174,10 +5171,8 @@ void Application::updateInputModes() { auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); static std::once_flag once; std::call_once(once, [&] { - bool first = true; foreach(auto inputPlugin, inputPlugins) { - addInputPluginToMenu(inputPlugin, first); - first = false; + addInputPluginToMenu(inputPlugin); } }); auto offscreenUi = DependencyManager::get(); @@ -5185,7 +5180,7 @@ void Application::updateInputModes() { InputPluginList newInputPlugins; InputPluginList removedInputPlugins; foreach(auto inputPlugin, inputPlugins) { - QString name = inputPlugin->getName(); + QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); QAction* action = menu->getActionForOption(name); auto it = std::find(std::begin(_activeInputPlugins), std::end(_activeInputPlugins), inputPlugin); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b69bb8022..f946553b55 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -34,7 +34,6 @@ #include "avatar/AvatarManager.h" #include "devices/DdeFaceTracker.h" #include "devices/Faceshift.h" -#include "input-plugins/SpacemouseManager.h" #include "MainWindow.h" #include "render/DrawStatus.h" #include "scripting/MenuScriptingInterface.h" @@ -327,12 +326,6 @@ Menu::Menu() { connect(speechRecognizer.data(), SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool))); #endif - // Settings > Input Devices - MenuWrapper* inputModeMenu = addMenu(MenuOption::InputMenu, "Advanced"); - QActionGroup* inputModeGroup = new QActionGroup(inputModeMenu); - inputModeGroup->setExclusive(false); - - // Developer menu ---------------------------------- MenuWrapper* developerMenu = addMenu("Developer", "Developer"); @@ -410,6 +403,12 @@ Menu::Menu() { // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); + // Settings > Input Devices + MenuWrapper* inputModeMenu = addMenu(MenuOption::InputMenu, "Advanced"); + QActionGroup* inputModeGroup = new QActionGroup(inputModeMenu); + inputModeGroup->setExclusive(false); + + // Developer > Avatar > Face Tracking MenuWrapper* faceTrackingMenu = avatarDebugMenu->addMenu("Face Tracking"); { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 484be9f346..7230ac2157 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -114,7 +114,7 @@ namespace MenuOption { const QString Help = "Help..."; const QString IncreaseAvatarSize = "Increase Avatar Size"; const QString IndependentMode = "Independent Mode"; - const QString InputMenu = "Avatar>Input Devices"; + const QString InputMenu = "Developer>Avatar>Input Devices"; const QString ActionMotorControl = "Enable Default Motor Control"; const QString LeapMotionOnHMD = "Leap Motion on HMD"; const QString LoadScript = "Open and Run Script File..."; diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index dba856cbaa..79ff4ecbf8 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -121,16 +121,8 @@ namespace controller { return availableInputs; } - void ActionsDevice::update(float deltaTime, const InputCalibrationData& inpuCalibrationData, bool jointsCaptured) { - } - - void ActionsDevice::focusOutEvent() { - } - ActionsDevice::ActionsDevice() : InputDevice("Actions") { _deviceID = UserInputMapper::ACTIONS_DEVICE; } - ActionsDevice::~ActionsDevice() {} - } diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index efdc45cb3d..724d17d951 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -109,13 +109,11 @@ class ActionsDevice : public QObject, public InputDevice { Q_PROPERTY(QString name READ getName) public: - virtual EndpointPointer createEndpoint(const Input& input) const override; - virtual Input::NamedVector getAvailableInputs() const override; - virtual void update(float deltaTime, const InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; - virtual void focusOutEvent() override; - ActionsDevice(); - virtual ~ActionsDevice(); + + EndpointPointer createEndpoint(const Input& input) const override; + Input::NamedVector getAvailableInputs() const override; + }; } diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index 93247965bc..afb1f7d1f7 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -57,9 +57,9 @@ public: // Update call MUST be called once per simulation loop // It takes care of updating the action states and deltas - virtual void update(float deltaTime, const InputCalibrationData& inputCalibrationData, bool jointsCaptured) = 0; + virtual void update(float deltaTime, const InputCalibrationData& inputCalibrationData) {}; - virtual void focusOutEvent() = 0; + virtual void focusOutEvent() {}; int getDeviceID() { return _deviceID; } void setDeviceID(int deviceID) { _deviceID = deviceID; } diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index 5996cad5df..a9d317d407 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -22,12 +22,6 @@ StandardController::StandardController() : InputDevice("Standard") { _deviceID = UserInputMapper::STANDARD_DEVICE; } -StandardController::~StandardController() { -} - -void StandardController::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { -} - void StandardController::focusOutEvent() { _axisStateMap.clear(); _buttonPressedMap.clear(); diff --git a/libraries/controllers/src/controllers/StandardController.h b/libraries/controllers/src/controllers/StandardController.h index fee608f822..58c3ecaa30 100644 --- a/libraries/controllers/src/controllers/StandardController.h +++ b/libraries/controllers/src/controllers/StandardController.h @@ -28,11 +28,9 @@ public: virtual EndpointPointer createEndpoint(const Input& input) const override; virtual Input::NamedVector getAvailableInputs() const override; virtual QStringList getDefaultMappingConfigs() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; virtual void focusOutEvent() override; StandardController(); - virtual ~StandardController(); }; } diff --git a/libraries/controllers/src/controllers/StateController.h b/libraries/controllers/src/controllers/StateController.h index 57414c3ae8..c18c9df27c 100644 --- a/libraries/controllers/src/controllers/StateController.h +++ b/libraries/controllers/src/controllers/StateController.h @@ -35,10 +35,7 @@ public: const QString& getName() const { return _name; } // Device functions - virtual Input::NamedVector getAvailableInputs() const override; - - void update(float deltaTime, const InputCalibrationData& inputCalibrationData, bool jointsCaptured) override {} - void focusOutEvent() override {} + Input::NamedVector getAvailableInputs() const override; void setInputVariant(const QString& name, ReadLambda lambda); diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index 4c0240eaf7..915ec1db87 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -20,11 +20,10 @@ const QString KeyboardMouseDevice::NAME = "Keyboard/Mouse"; -void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { - +void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { - _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); + _inputDevice->update(deltaTime, inputCalibrationData); }); // For touch event, we need to check that the last event is not too long ago @@ -40,7 +39,7 @@ void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputC } } -void KeyboardMouseDevice::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void KeyboardMouseDevice::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { _axisStateMap.clear(); } diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h index 67d9ccb1b2..a66cc7060b 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h @@ -65,12 +65,11 @@ public: }; // Plugin functions - virtual bool isSupported() const override { return true; } - virtual bool isJointController() const override { return false; } - virtual const QString& getName() const override { return NAME; } + bool isSupported() const override { return true; } + const QString& getName() const override { return NAME; } - virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } - virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } + void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void keyPressEvent(QKeyEvent* event); void keyReleaseEvent(QKeyEvent* event); @@ -97,7 +96,7 @@ protected: // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void focusOutEvent() override; // Let's make it easy for Qt because we assume we love Qt forever diff --git a/libraries/plugins/src/plugins/InputPlugin.h b/libraries/plugins/src/plugins/InputPlugin.h index b45fa862c1..02ae5f58d5 100644 --- a/libraries/plugins/src/plugins/InputPlugin.h +++ b/libraries/plugins/src/plugins/InputPlugin.h @@ -18,10 +18,8 @@ namespace controller { class InputPlugin : public Plugin { public: - virtual bool isJointController() const = 0; - virtual void pluginFocusOutEvent() = 0; - virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) = 0; + virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) = 0; }; diff --git a/plugins/hifiNeuron/CMakeLists.txt b/plugins/hifiNeuron/CMakeLists.txt index 1cab2359a9..a9ed8cca6e 100644 --- a/plugins/hifiNeuron/CMakeLists.txt +++ b/plugins/hifiNeuron/CMakeLists.txt @@ -6,8 +6,11 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -set(TARGET_NAME hifiNeuron) -setup_hifi_plugin(Script Qml Widgets) -link_hifi_libraries(shared controllers ui plugins input-plugins) -target_neuron() +if (APPLE OR WIN32) + + set(TARGET_NAME hifiNeuron) + setup_hifi_plugin(Script Qml Widgets) + link_hifi_libraries(shared controllers ui plugins input-plugins) + target_neuron() +endif() diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 6e2f744173..41c0eb0d4e 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -25,9 +25,7 @@ Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") #define __OS_XUN__ 1 #define BOOL int -#ifdef HAVE_NEURON #include -#endif const QString NeuronPlugin::NAME = "Neuron"; const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; @@ -166,69 +164,6 @@ static controller::StandardPoseChannel neuronJointIndexToPoseIndexMap[NeuronJoin static glm::vec3 rightHandThumb1DefaultAbsTranslation(-2.155500650405884, -0.7610001564025879, 2.685631036758423); static glm::vec3 leftHandThumb1DefaultAbsTranslation(2.1555817127227783, -0.7603635787963867, 2.6856393814086914); -// default translations (cm) -static glm::vec3 neuronJointTranslations[NeuronJointIndex::Size] = { - {131.901, 95.6602, -27.9815}, - {-9.55907, -1.58772, 0.0760284}, - {0.0144232, -41.4683, -0.105322}, - {1.59348, -41.5875, -0.557237}, - {9.72077, -1.68926, -0.280643}, - {0.0886684, -43.1586, -0.0111596}, - {-2.98473, -44.0517, 0.0694456}, - {0.110967, 16.3959, 0.140463}, - {0.0500451, 10.0238, 0.0731921}, - {0.061568, 10.4352, 0.0583075}, - {0.0500606, 10.0217, 0.0711083}, - {0.0317731, 10.7176, 0.0779325}, - {-0.0204253, 9.71067, 0.131734}, - {-3.24245, 7.13584, 0.185638}, - {-13.0885, -0.0877601, 0.176065}, - {-27.2674, 0.0688724, 0.0272146}, - {-26.7673, 0.0301916, 0.0102847}, - {-2.56017, 0.195537, 3.20968}, - {-3.78796, 0, 0}, - {-2.63141, 0, 0}, - {-3.31579, 0.522947, 2.03495}, - {-5.36589, -0.0939789, 1.02771}, - {-3.72278, 0, 0}, - {-2.11074, 0, 0}, - {-3.47874, 0.532042, 0.778358}, - {-5.32194, -0.0864, 0.322863}, - {-4.06232, 0, 0}, - {-2.54653, 0, 0}, - {-3.46131, 0.553263, -0.132632}, - {-4.76716, -0.0227368, -0.492632}, - {-3.54073, 0, 0}, - {-2.45634, 0, 0}, - {-3.25137, 0.482779, -1.23613}, - {-4.25937, -0.0227368, -1.12168}, - {-2.83528, 0, 0}, - {-1.79166, 0, 0}, - {3.25624, 7.13148, -0.131575}, - {13.149, -0.052598, -0.125076}, - {27.2903, 0.00282644, -0.0181535}, - {26.6602, 0.000969969, -0.0487599}, - {2.56017, 0.195537, 3.20968}, - {3.78796, 0, 0}, - {2.63141, 0, 0}, - {3.31579, 0.522947, 2.03495}, - {5.36589, -0.0939789, 1.02771}, - {3.72278, 0, 0}, - {2.11074, 0, 0}, - {3.47874, 0.532042, 0.778358}, - {5.32194, -0.0864, 0.322863}, - {4.06232, 0, 0}, - {2.54653, 0, 0}, - {3.46131, 0.553263, -0.132632}, - {4.76716, -0.0227368, -0.492632}, - {3.54073, 0, 0}, - {2.45634, 0, 0}, - {3.25137, 0.482779, -1.23613}, - {4.25937, -0.0227368, -1.12168}, - {2.83528, 0, 0}, - {1.79166, 0, 0} -}; - static controller::StandardPoseChannel neuronJointIndexToPoseIndex(NeuronJointIndex i) { assert(i >= 0 && i < NeuronJointIndex::Size); if (i >= 0 && i < NeuronJointIndex::Size) { @@ -307,16 +242,13 @@ static const char* controllerJointName(controller::StandardPoseChannel i) { // convert between YXZ neuron euler angles in degrees to quaternion // this is the default setting in the Axis Neuron server. -static quat eulerToQuat(vec3 euler) { +static quat eulerToQuat(const vec3& e) { // euler.x and euler.y are swaped, WTF. - glm::vec3 e = glm::vec3(euler.y, euler.x, euler.z) * RADIANS_PER_DEGREE; - return (glm::angleAxis(e.y, Vectors::UNIT_Y) * - glm::angleAxis(e.x, Vectors::UNIT_X) * - glm::angleAxis(e.z, Vectors::UNIT_Z)); + return (glm::angleAxis(e.x * RADIANS_PER_DEGREE, Vectors::UNIT_Y) * + glm::angleAxis(e.y * RADIANS_PER_DEGREE, Vectors::UNIT_X) * + glm::angleAxis(e.z * RADIANS_PER_DEGREE, Vectors::UNIT_Z)); } -#ifdef HAVE_NEURON - // // neuronDataReader SDK callback functions // @@ -355,21 +287,6 @@ void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx // copy the data memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); - - } else { - qCWarning(inputplugins) << "NeuronPlugin: unsuported binary format, please enable displacements"; - - // enter mutex - std::lock_guard guard(neuronPlugin->_jointsMutex); - - if (neuronPlugin->_joints.size() != NeuronJointIndex::Size) { - neuronPlugin->_joints.resize(NeuronJointIndex::Size, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); - } - - for (int i = 0; i < NeuronJointIndex::Size; i++) { - neuronPlugin->_joints[i].euler = glm::vec3(); - neuronPlugin->_joints[i].pos = neuronJointTranslations[i]; - } } } else { static bool ONCE = false; @@ -435,26 +352,19 @@ static void SocketStatusChangedCallback(void* context, SOCKET_REF sender, Socket qCDebug(inputplugins) << "NeuronPlugin: socket status = " << message; } -#endif // #ifdef HAVE_NEURON - // // NeuronPlugin // bool NeuronPlugin::isSupported() const { -#ifdef HAVE_NEURON // Because it's a client/server network architecture, we can't tell // if the neuron is actually connected until we connect to the server. return true; -#else - return false; -#endif } bool NeuronPlugin::activate() { InputPlugin::activate(); -#ifdef HAVE_NEURON // register with userInputMapper auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); @@ -480,13 +390,9 @@ bool NeuronPlugin::activate() { BRRegisterAutoSyncParmeter(_socketRef, Cmd_CombinationMode); return true; } -#else - return false; -#endif } void NeuronPlugin::deactivate() { -#ifdef HAVE_NEURON // unregister from userInputMapper if (_inputDevice->_deviceID != controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); @@ -499,10 +405,9 @@ void NeuronPlugin::deactivate() { } InputPlugin::deactivate(); -#endif } -void NeuronPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void NeuronPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { std::vector joints; { // lock and copy @@ -548,16 +453,23 @@ QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { void NeuronPlugin::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const std::vector& joints, const std::vector& prevJoints) { for (size_t i = 0; i < joints.size(); i++) { + int poseIndex = neuronJointIndexToPoseIndex((NeuronJointIndex)i); glm::vec3 linearVel, angularVel; - glm::vec3 pos = joints[i].pos; - glm::quat rot = eulerToQuat(joints[i].euler); + const glm::vec3& pos = joints[i].pos; + const glm::vec3& rotEuler = joints[i].euler; + + if ((Vectors::ZERO == pos && Vectors::ZERO == rotEuler)) { + _poseStateMap[poseIndex] = controller::Pose(); + continue; + } + + glm::quat rot = eulerToQuat(rotEuler); if (i < prevJoints.size()) { linearVel = (pos - (prevJoints[i].pos * METERS_PER_CENTIMETER)) / deltaTime; // m/s // quat log imaginary part points along the axis of rotation, with length of one half the angle of rotation. glm::quat d = glm::log(rot * glm::inverse(eulerToQuat(prevJoints[i].euler))); angularVel = glm::vec3(d.x, d.y, d.z) / (0.5f * deltaTime); // radians/s } - int poseIndex = neuronJointIndexToPoseIndex((NeuronJointIndex)i); _poseStateMap[poseIndex] = controller::Pose(pos, rot, linearVel, angularVel); } diff --git a/plugins/hifiNeuron/src/NeuronPlugin.h b/plugins/hifiNeuron/src/NeuronPlugin.h index 99859dcacb..9ddd79c013 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.h +++ b/plugins/hifiNeuron/src/NeuronPlugin.h @@ -27,7 +27,6 @@ public: // Plugin functions virtual bool isSupported() const override; - virtual bool isJointController() const override { return true; } virtual const QString& getName() const override { return NAME; } const QString& getID() const override { return NEURON_ID_STRING; } @@ -35,7 +34,7 @@ public: virtual void deactivate() override; virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } - virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void saveSettings() const override; virtual void loadSettings() override; @@ -56,7 +55,7 @@ protected: // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override {}; + virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override {}; virtual void focusOutEvent() override {}; void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const std::vector& joints, const std::vector& prevJoints); diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index 9d195fd606..a109656489 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -15,7 +15,6 @@ const float CONTROLLER_THRESHOLD = 0.3f; -#ifdef HAVE_SDL2 const float MAX_AXIS = 32768.0f; Joystick::Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameController) : @@ -27,19 +26,15 @@ Joystick::Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameControl } -#endif - Joystick::~Joystick() { closeJoystick(); } void Joystick::closeJoystick() { -#ifdef HAVE_SDL2 SDL_GameControllerClose(_sdlGameController); -#endif } -void Joystick::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void Joystick::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { for (auto axisState : _axisStateMap) { if (fabsf(axisState.second) < CONTROLLER_THRESHOLD) { _axisStateMap[axisState.first] = 0.0f; @@ -52,8 +47,6 @@ void Joystick::focusOutEvent() { _buttonPressedMap.clear(); }; -#ifdef HAVE_SDL2 - void Joystick::handleAxisEvent(const SDL_ControllerAxisEvent& event) { SDL_GameControllerAxis axis = (SDL_GameControllerAxis) event.axis; _axisStateMap[makeInput((controller::StandardAxisChannel)axis).getChannel()] = (float)event.value / MAX_AXIS; @@ -69,8 +62,6 @@ void Joystick::handleButtonEvent(const SDL_ControllerButtonEvent& event) { } } -#endif - controller::Input::NamedVector Joystick::getAvailableInputs() const { using namespace controller; static const Input::NamedVector availableInputs{ diff --git a/plugins/hifiSdl2/src/Joystick.h b/plugins/hifiSdl2/src/Joystick.h index 08bf27b960..e2eaeaef8b 100644 --- a/plugins/hifiSdl2/src/Joystick.h +++ b/plugins/hifiSdl2/src/Joystick.h @@ -15,10 +15,8 @@ #include #include -#ifdef HAVE_SDL2 #include #undef main -#endif #include #include @@ -26,10 +24,7 @@ class Joystick : public QObject, public controller::InputDevice { Q_OBJECT Q_PROPERTY(QString name READ getName) - -#ifdef HAVE_SDL2 Q_PROPERTY(int instanceId READ getInstanceId) -#endif public: using Pointer = std::shared_ptr; @@ -39,33 +34,25 @@ public: // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void focusOutEvent() override; Joystick() : InputDevice("GamePad") {} ~Joystick(); -#ifdef HAVE_SDL2 Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameController); -#endif void closeJoystick(); -#ifdef HAVE_SDL2 void handleAxisEvent(const SDL_ControllerAxisEvent& event); void handleButtonEvent(const SDL_ControllerButtonEvent& event); -#endif -#ifdef HAVE_SDL2 int getInstanceId() const { return _instanceId; } -#endif private: -#ifdef HAVE_SDL2 SDL_GameController* _sdlGameController; SDL_Joystick* _sdlJoystick; SDL_JoystickID _instanceId; -#endif }; #endif // hifi_Joystick_h diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index 7091b20d21..0bdb68f830 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -16,7 +16,6 @@ #include "SDL2Manager.h" -#ifdef HAVE_SDL2 static_assert( (int)controller::A == (int)SDL_CONTROLLER_BUTTON_A && (int)controller::B == (int)SDL_CONTROLLER_BUTTON_B && @@ -40,28 +39,16 @@ static_assert( (int)controller::LT == (int)SDL_CONTROLLER_AXIS_TRIGGERLEFT && (int)controller::RT == (int)SDL_CONTROLLER_AXIS_TRIGGERRIGHT, "SDL2 equvalence: Enums and values from StandardControls.h are assumed to match enums from SDL_gamecontroller.h"); -#endif const QString SDL2Manager::NAME = "SDL2"; -#ifdef HAVE_SDL2 SDL_JoystickID SDL2Manager::getInstanceId(SDL_GameController* controller) { SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller); return SDL_JoystickInstanceID(joystick); } -#endif - -SDL2Manager::SDL2Manager() : -#ifdef HAVE_SDL2 -_openJoysticks(), -#endif -_isInitialized(false) -{ -} void SDL2Manager::init() { -#ifdef HAVE_SDL2 bool initSuccess = (SDL_Init(SDL_INIT_GAMECONTROLLER) == 0); if (initSuccess) { @@ -88,66 +75,50 @@ void SDL2Manager::init() { else { qDebug() << "Error initializing SDL2 Manager"; } -#endif } void SDL2Manager::deinit() { -#ifdef HAVE_SDL2 _openJoysticks.clear(); SDL_Quit(); -#endif } bool SDL2Manager::activate() { InputPlugin::activate(); -#ifdef HAVE_SDL2 auto userInputMapper = DependencyManager::get(); for (auto joystick : _openJoysticks) { userInputMapper->registerDevice(joystick); emit joystickAdded(joystick.get()); } return true; -#else - return false; -#endif } void SDL2Manager::deactivate() { -#ifdef HAVE_SDL2 auto userInputMapper = DependencyManager::get(); for (auto joystick : _openJoysticks) { userInputMapper->removeDevice(joystick->getDeviceID()); emit joystickRemoved(joystick.get()); } -#endif InputPlugin::deactivate(); } bool SDL2Manager::isSupported() const { -#ifdef HAVE_SDL2 return true; -#else - return false; -#endif } void SDL2Manager::pluginFocusOutEvent() { -#ifdef HAVE_SDL2 for (auto joystick : _openJoysticks) { joystick->focusOutEvent(); } -#endif } -void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { -#ifdef HAVE_SDL2 +void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (_isInitialized) { auto userInputMapper = DependencyManager::get(); for (auto joystick : _openJoysticks) { - joystick->update(deltaTime, inputCalibrationData, jointsCaptured); + joystick->update(deltaTime, inputCalibrationData); } PerformanceTimer perfTimer("SDL2Manager::update"); @@ -197,5 +168,4 @@ void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrati } } } -#endif } diff --git a/plugins/hifiSdl2/src/SDL2Manager.h b/plugins/hifiSdl2/src/SDL2Manager.h index f69e23ee98..a597a87aee 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.h +++ b/plugins/hifiSdl2/src/SDL2Manager.h @@ -12,9 +12,7 @@ #ifndef hifi__SDL2Manager_h #define hifi__SDL2Manager_h -#ifdef HAVE_SDL2 #include -#endif #include #include @@ -24,30 +22,26 @@ class SDL2Manager : public InputPlugin { Q_OBJECT public: - SDL2Manager(); - // Plugin functions - virtual bool isSupported() const override; - virtual bool isJointController() const override { return false; } - virtual const QString& getName() const override { return NAME; } + bool isSupported() const override; + const QString& getName() const override { return NAME; } - virtual void init() override; - virtual void deinit() override; + void init() override; + void deinit() override; /// Called when a plugin is being activated for use. May be called multiple times. - virtual bool activate() override; + bool activate() override; /// Called when a plugin is no longer being used. May be called multiple times. - virtual void deactivate() override; + void deactivate() override; - virtual void pluginFocusOutEvent() override; - virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + void pluginFocusOutEvent() override; + void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; signals: void joystickAdded(Joystick* joystick); void joystickRemoved(Joystick* joystick); private: -#ifdef HAVE_SDL2 SDL_JoystickID getInstanceId(SDL_GameController* controller); int axisInvalid() const { return SDL_CONTROLLER_AXIS_INVALID; } @@ -81,8 +75,7 @@ private: int buttonRelease() const { return SDL_RELEASED; } QMap _openJoysticks; -#endif - bool _isInitialized; + bool _isInitialized { false } ; static const QString NAME; }; diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index fdb7bb17fe..9ea79a8b96 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -134,12 +134,12 @@ void SixenseManager::setSixenseFilter(bool filter) { #endif } -void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { BAIL_IF_NOT_LOADED auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { - _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); + _inputDevice->update(deltaTime, inputCalibrationData); }); if (_inputDevice->_requestReset) { @@ -148,7 +148,7 @@ void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibr } } -void SixenseManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void SixenseManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { BAIL_IF_NOT_LOADED #ifdef HAVE_SIXENSE _buttonPressedMap.clear(); @@ -208,14 +208,10 @@ void SixenseManager::InputDevice::update(float deltaTime, const controller::Inpu _axisStateMap[left ? LY : RY] = data->joystick_y; _axisStateMap[left ? LT : RT] = data->trigger; - if (!jointsCaptured) { - // Rotation of Palm - glm::quat rotation(data->rot_quat[3], data->rot_quat[0], data->rot_quat[1], data->rot_quat[2]); - handlePoseEvent(deltaTime, inputCalibrationData, position, rotation, left); - rawPoses[i] = controller::Pose(position, rotation, Vectors::ZERO, Vectors::ZERO); - } else { - _poseStateMap.clear(); - } + // Rotation of Palm + glm::quat rotation(data->rot_quat[3], data->rot_quat[0], data->rot_quat[1], data->rot_quat[2]); + handlePoseEvent(deltaTime, inputCalibrationData, position, rotation, left); + rawPoses[i] = controller::Pose(position, rotation, Vectors::ZERO, Vectors::ZERO); } else { auto hand = left ? controller::StandardPoseChannel::LEFT_HAND : controller::StandardPoseChannel::RIGHT_HAND; _poseStateMap[hand] = controller::Pose(); diff --git a/plugins/hifiSixense/src/SixenseManager.h b/plugins/hifiSixense/src/SixenseManager.h index a46614b17a..6aec9fd4ad 100644 --- a/plugins/hifiSixense/src/SixenseManager.h +++ b/plugins/hifiSixense/src/SixenseManager.h @@ -28,7 +28,6 @@ class SixenseManager : public InputPlugin { public: // Plugin functions virtual bool isSupported() const override; - virtual bool isJointController() const override { return true; } virtual const QString& getName() const override { return NAME; } virtual const QString& getID() const override { return HYDRA_ID_STRING; } @@ -36,7 +35,7 @@ public: virtual void deactivate() override; virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } - virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void saveSettings() const override; virtual void loadSettings() override; @@ -61,7 +60,7 @@ private: // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void focusOutEvent() override; void handleButtonEvent(unsigned int buttons, bool left); diff --git a/plugins/hifiSpacemouse/CMakeLists.txt b/plugins/hifiSpacemouse/CMakeLists.txt new file mode 100644 index 0000000000..bcfb309a69 --- /dev/null +++ b/plugins/hifiSpacemouse/CMakeLists.txt @@ -0,0 +1,18 @@ +# +# Created by Bradley Austin Davis on 2016/05/11 +# Copyright 2013-2016 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html +# + +if(WIN32) + set(TARGET_NAME hifiSpacemouse) + find_package(3DCONNEXIONCLIENT) + if (3DCONNEXIONCLIENT_FOUND) + setup_hifi_plugin(Script Qml Widgets) + link_hifi_libraries(shared networking controllers ui plugins input-plugins) + target_include_directories(${TARGET_NAME} PUBLIC ${3DCONNEXIONCLIENT_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${3DCONNEXIONCLIENT_LIBRARIES}) + endif() +endif() diff --git a/libraries/input-plugins/src/input-plugins/SpacemouseManager.cpp b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp similarity index 95% rename from libraries/input-plugins/src/input-plugins/SpacemouseManager.cpp rename to plugins/hifiSpacemouse/src/SpacemouseManager.cpp index d946990319..8b584df366 100644 --- a/libraries/input-plugins/src/input-plugins/SpacemouseManager.cpp +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp @@ -17,13 +17,65 @@ #include #include -#include "../../../interface/src/Menu.h" +const QString SpacemouseManager::NAME { "Spacemouse" }; const float MAX_AXIS = 75.0f; // max forward = 2x speed +#define LOGITECH_VENDOR_ID 0x46d -static std::shared_ptr instance = std::make_shared(); +#ifndef RIDEV_DEVNOTIFY +#define RIDEV_DEVNOTIFY 0x00002000 +#endif -SpacemouseDevice::SpacemouseDevice() : InputDevice("Spacemouse") +const int TRACE_RIDI_DEVICENAME = 0; +const int TRACE_RIDI_DEVICEINFO = 0; + +#ifdef _WIN64 +typedef unsigned __int64 QWORD; +#endif + +bool Is3dmouseAttached(); + +std::shared_ptr instance; + +bool SpacemouseManager::isSupported() const { + return Is3dmouseAttached(); +} + +bool SpacemouseManager::activate() { + fLast3dmouseInputTime = 0; + + InitializeRawInput(GetActiveWindow()); + + QAbstractEventDispatcher::instance()->installNativeEventFilter(this); + + if (!instance) { + instance = std::make_shared(); + } + + if (instance->getDeviceID() == controller::Input::INVALID_DEVICE) { + auto userInputMapper = DependencyManager::get(); + userInputMapper->registerDevice(instance); + UserActivityLogger::getInstance().connectedDevice("controller", NAME); + } + return true; +} + +void SpacemouseManager::deactivate() { + QAbstractEventDispatcher::instance()->removeNativeEventFilter(this); + int deviceid = instance->getDeviceID(); + auto userInputMapper = DependencyManager::get(); + userInputMapper->removeDevice(deviceid); +} + +void SpacemouseManager::pluginFocusOutEvent() { + instance->focusOutEvent(); +} + +void SpacemouseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + +} + +SpacemouseDevice::SpacemouseDevice() : InputDevice(SpacemouseManager::NAME) { } @@ -111,71 +163,80 @@ controller::Input::NamedPair SpacemouseDevice::makePair(SpacemouseDevice::Positi return controller::Input::NamedPair(makeInput(axis), name); } -void SpacemouseDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void SpacemouseDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { // the update is done in the SpacemouseManager class. // for windows in the nativeEventFilter the inputmapper is connected or registed or removed when an 3Dconnnexion device is attached or detached // for osx the api will call DeviceAddedHandler or DeviceRemoveHandler when a 3Dconnexion device is attached or detached } -void SpacemouseManager::ManagerFocusOutEvent() { - instance->focusOutEvent(); -} - -void SpacemouseManager::init() { -} - -#ifdef HAVE_3DCONNEXIONCLIENT - #ifdef Q_OS_WIN #include -void SpacemouseManager::toggleSpacemouse(bool shouldEnable) { - if (shouldEnable) { - init(); - } - if (!shouldEnable && instance->getDeviceID() != controller::Input::INVALID_DEVICE) { - destroy(); - } +bool SpacemouseManager::nativeEventFilter(const QByteArray& eventType, void* message, long* result) { + MSG* msg = static_cast< MSG * >(message); + return RawInputEventFilter(message, result); } -void SpacemouseManager::init() { - if (Menu::getInstance()->isOptionChecked(MenuOption::Connexion)) { - fLast3dmouseInputTime = 0; - InitializeRawInput(GetActiveWindow()); +//Get an initialized array of PRAWINPUTDEVICE for the 3D devices +//pNumDevices returns the number of devices to register. Currently this is always 1. +static PRAWINPUTDEVICE GetDevicesToRegister(unsigned int* pNumDevices) { + // Array of raw input devices to register + static RAWINPUTDEVICE sRawInputDevices[] = { + { 0x01, 0x08, 0x00, 0x00 } // Usage Page = 0x01 Generic Desktop Page, Usage Id= 0x08 Multi-axis Controller + }; - QAbstractEventDispatcher::instance()->installNativeEventFilter(this); + if (pNumDevices) { + *pNumDevices = sizeof(sRawInputDevices) / sizeof(sRawInputDevices[0]); + } - if (instance->getDeviceID() != controller::Input::INVALID_DEVICE) { - auto userInputMapper = DependencyManager::get(); - userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", "Spacemouse"); + return sRawInputDevices; +} + + +//Detect the 3D mouse +bool Is3dmouseAttached() { + unsigned int numDevicesOfInterest = 0; + PRAWINPUTDEVICE devicesToRegister = GetDevicesToRegister(&numDevicesOfInterest); + + unsigned int nDevices = 0; + + if (::GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST)) != 0) { + return false; + } + + if (nDevices == 0) { + return false; + } + + std::vector rawInputDeviceList(nDevices); + if (::GetRawInputDeviceList(&rawInputDeviceList[0], &nDevices, sizeof(RAWINPUTDEVICELIST)) == static_cast(-1)) { + return false; + } + + for (unsigned int i = 0; i < nDevices; ++i) { + RID_DEVICE_INFO rdi = { sizeof(rdi) }; + unsigned int cbSize = sizeof(rdi); + + if (GetRawInputDeviceInfo(rawInputDeviceList[i].hDevice, RIDI_DEVICEINFO, &rdi, &cbSize) > 0) { + //skip non HID and non logitec (3DConnexion) devices + if (rdi.dwType != RIM_TYPEHID || rdi.hid.dwVendorId != LOGITECH_VENDOR_ID) { + continue; + } + + //check if devices matches Multi-axis Controller + for (unsigned int j = 0; j < numDevicesOfInterest; ++j) { + if (devicesToRegister[j].usUsage == rdi.hid.usUsage + && devicesToRegister[j].usUsagePage == rdi.hid.usUsagePage) { + return true; + } + } } - } + return false; } -void SpacemouseManager::destroy() { - QAbstractEventDispatcher::instance()->removeNativeEventFilter(this); - int deviceid = instance->getDeviceID(); - auto userInputMapper = DependencyManager::get(); - userInputMapper->removeDevice(deviceid); -} - -#define LOGITECH_VENDOR_ID 0x46d - -#ifndef RIDEV_DEVNOTIFY -#define RIDEV_DEVNOTIFY 0x00002000 -#endif - -const int TRACE_RIDI_DEVICENAME = 0; -const int TRACE_RIDI_DEVICEINFO = 0; - -#ifdef _WIN64 -typedef unsigned __int64 QWORD; -#endif - // object angular velocity per mouse tick 0.008 milliradians per second per count static const double k3dmouseAngularVelocity = 8.0e-6; // radians per second per count @@ -290,20 +351,9 @@ bool SpacemouseManager::RawInputEventFilter(void* msg, long* result) { return false; } -// Access the mouse parameters structure -I3dMouseParam& SpacemouseManager::MouseParams() { - return f3dMouseParams; -} - -// Access the mouse parameters structure -const I3dMouseParam& SpacemouseManager::MouseParams() const { - return f3dMouseParams; -} - //Called with the processed motion data when a 3D mouse event is received void SpacemouseManager::Move3d(HANDLE device, std::vector& motionData) { Q_UNUSED(device); - instance->cc_position = { motionData[0] * 1000, motionData[1] * 1000, motionData[2] * 1000 }; instance->cc_rotation = { motionData[3] * 1500, motionData[4] * 1500, motionData[5] * 1500 }; instance->handleAxisEvent(); @@ -321,62 +371,6 @@ void SpacemouseManager::On3dmouseKeyUp(HANDLE device, int virtualKeyCode) { instance->setButton(0); } -//Get an initialized array of PRAWINPUTDEVICE for the 3D devices -//pNumDevices returns the number of devices to register. Currently this is always 1. -static PRAWINPUTDEVICE GetDevicesToRegister(unsigned int* pNumDevices) { - // Array of raw input devices to register - static RAWINPUTDEVICE sRawInputDevices[] = { - { 0x01, 0x08, 0x00, 0x00 } // Usage Page = 0x01 Generic Desktop Page, Usage Id= 0x08 Multi-axis Controller - }; - - if (pNumDevices) { - *pNumDevices = sizeof(sRawInputDevices) / sizeof(sRawInputDevices[0]); - } - - return sRawInputDevices; -} - -//Detect the 3D mouse -bool SpacemouseManager::Is3dmouseAttached() { - unsigned int numDevicesOfInterest = 0; - PRAWINPUTDEVICE devicesToRegister = GetDevicesToRegister(&numDevicesOfInterest); - - unsigned int nDevices = 0; - - if (::GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST)) != 0) { - return false; - } - - if (nDevices == 0) { - return false; - } - - std::vector rawInputDeviceList(nDevices); - if (::GetRawInputDeviceList(&rawInputDeviceList[0], &nDevices, sizeof(RAWINPUTDEVICELIST)) == static_cast(-1)) { - return false; - } - - for (unsigned int i = 0; i < nDevices; ++i) { - RID_DEVICE_INFO rdi = { sizeof(rdi) }; - unsigned int cbSize = sizeof(rdi); - - if (GetRawInputDeviceInfo(rawInputDeviceList[i].hDevice, RIDI_DEVICEINFO, &rdi, &cbSize) > 0) { - //skip non HID and non logitec (3DConnexion) devices - if (rdi.dwType != RIM_TYPEHID || rdi.hid.dwVendorId != LOGITECH_VENDOR_ID) { - continue; - } - - //check if devices matches Multi-axis Controller - for (unsigned int j = 0; j < numDevicesOfInterest; ++j) { - if (devicesToRegister[j].usUsage == rdi.hid.usUsage - && devicesToRegister[j].usUsagePage == rdi.hid.usUsagePage) { - return true; - } - } - } - } - return false; -} // Initialize the window to recieve raw-input messages // This needs to be called initially so that Windows will send the messages from the 3D mouse to the window. @@ -942,5 +936,3 @@ void MessageHandler(unsigned int connection, unsigned int messageType, void *mes } #endif // __APPLE__ - -#endif diff --git a/libraries/input-plugins/src/input-plugins/SpacemouseManager.h b/plugins/hifiSpacemouse/src/SpacemouseManager.h similarity index 77% rename from libraries/input-plugins/src/input-plugins/SpacemouseManager.h rename to plugins/hifiSpacemouse/src/SpacemouseManager.h index 82c4fa8fb6..a9933902e5 100644 --- a/libraries/input-plugins/src/input-plugins/SpacemouseManager.h +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.h @@ -17,22 +17,8 @@ #include #include -#include "InputPlugin.h" +#include -#ifndef HAVE_3DCONNEXIONCLIENT -class SpacemouseManager : public QObject { - Q_OBJECT -public: - void ManagerFocusOutEvent(); - void init(); - void destroy() {}; - bool Is3dmouseAttached() { return false; }; - public slots: - void toggleSpacemouse(bool shouldEnable) {}; -}; -#endif - -#ifdef HAVE_3DCONNEXIONCLIENT // the windows connexion rawinput #ifdef Q_OS_WIN @@ -85,42 +71,26 @@ private: Speed fSpeed; }; -class SpacemouseManager : public QObject, public QAbstractNativeEventFilter { +class SpacemouseManager : public InputPlugin, public QAbstractNativeEventFilter { Q_OBJECT public: - SpacemouseManager() {}; + bool isSupported() const override; + const QString& getName() const override { return NAME; } + const QString& getID() const override { return NAME; } - void init(); - void destroy(); - bool Is3dmouseAttached(); + bool activate() override; + void deactivate() override; - SpacemouseManager* client; + void pluginFocusOutEvent() override; + void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; - void ManagerFocusOutEvent(); - - I3dMouseParam& MouseParams(); - const I3dMouseParam& MouseParams() const; - - virtual void Move3d(HANDLE device, std::vector& motionData); - virtual void On3dmouseKeyDown(HANDLE device, int virtualKeyCode); - virtual void On3dmouseKeyUp(HANDLE device, int virtualKeyCode); - - virtual bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) Q_DECL_OVERRIDE - { - MSG* msg = static_cast< MSG * >(message); - return RawInputEventFilter(message, result); - } - - public slots: - void toggleSpacemouse(bool shouldEnable); - -signals: - void Move3d(std::vector& motionData); - void On3dmouseKeyDown(int virtualKeyCode); - void On3dmouseKeyUp(int virtualKeyCode); + bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; private: + void Move3d(HANDLE device, std::vector& motionData); + void On3dmouseKeyDown(HANDLE device, int virtualKeyCode); + void On3dmouseKeyUp(HANDLE device, int virtualKeyCode); bool InitializeRawInput(HWND hwndTarget); bool RawInputEventFilter(void* msg, long* result); @@ -156,6 +126,9 @@ private: // use to calculate distance traveled since last event DWORD fLast3dmouseInputTime; + + static const QString NAME; + friend class SpacemouseDevice; }; // the osx connexion api @@ -176,8 +149,6 @@ public: #endif // __APPLE__ -#endif - // connnects to the userinputmapper class SpacemouseDevice : public QObject, public controller::InputDevice { @@ -214,7 +185,7 @@ public: virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void focusOutEvent() override; glm::vec3 cc_position; diff --git a/plugins/hifiSpacemouse/src/SpacemouseProvider.cpp b/plugins/hifiSpacemouse/src/SpacemouseProvider.cpp new file mode 100644 index 0000000000..c623f77d73 --- /dev/null +++ b/plugins/hifiSpacemouse/src/SpacemouseProvider.cpp @@ -0,0 +1,45 @@ +// +// Created by Bradley Austin Davis on 2015/10/25 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include +#include +#include + +#include +#include + +#include "SpacemouseManager.h" + +class SpacemouseProvider : public QObject, public InputProvider +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID InputProvider_iid FILE "plugin.json") + Q_INTERFACES(InputProvider) + +public: + SpacemouseProvider(QObject* parent = nullptr) : QObject(parent) {} + virtual ~SpacemouseProvider() {} + + virtual InputPluginList getInputPlugins() override { + static std::once_flag once; + std::call_once(once, [&] { + InputPluginPointer plugin(new SpacemouseManager()); + if (plugin->isSupported()) { + _inputPlugins.push_back(plugin); + } + }); + return _inputPlugins; + } + +private: + InputPluginList _inputPlugins; +}; + +#include "SpacemouseProvider.moc" diff --git a/plugins/hifiSpacemouse/src/plugin.json b/plugins/hifiSpacemouse/src/plugin.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/plugins/hifiSpacemouse/src/plugin.json @@ -0,0 +1 @@ +{} diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 50ef6b09a1..09ab6ec159 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -76,12 +76,12 @@ void OculusControllerManager::deactivate() { } } -void OculusControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void OculusControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { PerformanceTimer perfTimer("OculusControllerManager::TouchDevice::update"); if (_touch) { if (OVR_SUCCESS(ovr_GetInputState(_session, ovrControllerType_Touch, &_inputState))) { - _touch->update(deltaTime, inputCalibrationData, jointsCaptured); + _touch->update(deltaTime, inputCalibrationData); } else { qCWarning(oculus) << "Unable to read Oculus touch input state"; } @@ -89,7 +89,7 @@ void OculusControllerManager::pluginUpdate(float deltaTime, const controller::In if (_remote) { if (OVR_SUCCESS(ovr_GetInputState(_session, ovrControllerType_Remote, &_inputState))) { - _remote->update(deltaTime, inputCalibrationData, jointsCaptured); + _remote->update(deltaTime, inputCalibrationData); } else { qCWarning(oculus) << "Unable to read Oculus remote input state"; } @@ -158,7 +158,7 @@ QString OculusControllerManager::RemoteDevice::getDefaultMappingConfig() const { return MAPPING_JSON; } -void OculusControllerManager::RemoteDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void OculusControllerManager::RemoteDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { _buttonPressedMap.clear(); const auto& inputState = _parent._inputState; for (const auto& pair : BUTTON_MAP) { @@ -172,21 +172,19 @@ void OculusControllerManager::RemoteDevice::focusOutEvent() { _buttonPressedMap.clear(); } -void OculusControllerManager::TouchDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void OculusControllerManager::TouchDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { _poseStateMap.clear(); _buttonPressedMap.clear(); - if (!jointsCaptured) { - int numTrackedControllers = 0; - static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked & ovrStatus_PositionTracked; - auto tracking = ovr_GetTrackingState(_parent._session, 0, false); - ovr_for_each_hand([&](ovrHandType hand) { - ++numTrackedControllers; - if (REQUIRED_HAND_STATUS == (tracking.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { - handlePose(deltaTime, inputCalibrationData, hand, tracking.HandPoses[hand]); - } - }); - } + int numTrackedControllers = 0; + static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked & ovrStatus_PositionTracked; + auto tracking = ovr_GetTrackingState(_parent._session, 0, false); + ovr_for_each_hand([&](ovrHandType hand) { + ++numTrackedControllers; + if (REQUIRED_HAND_STATUS == (tracking.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { + handlePose(deltaTime, inputCalibrationData, hand, tracking.HandPoses[hand]); + } + }); using namespace controller; // Axes const auto& inputState = _parent._inputState; diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index 60969097f8..980e1286f8 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -24,14 +24,13 @@ class OculusControllerManager : public InputPlugin { public: // Plugin functions bool isSupported() const override; - bool isJointController() const override { return true; } const QString& getName() const override { return NAME; } bool activate() override; void deactivate() override; void pluginFocusOutEvent() override; - void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; private: class OculusInputDevice : public controller::InputDevice { @@ -49,7 +48,7 @@ private: controller::Input::NamedVector getAvailableInputs() const override; QString getDefaultMappingConfig() const override; - void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; friend class OculusControllerManager; @@ -62,7 +61,7 @@ private: controller::Input::NamedVector getAvailableInputs() const override; QString getDefaultMappingConfig() const override; - void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; private: diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 38719fdca5..dba8fca208 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -9,7 +9,6 @@ #include -#include #include #include #include @@ -30,10 +29,6 @@ Q_DECLARE_LOGGING_CATEGORY(displayplugins) const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here -static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); -static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); - - static vr::IVRCompositor* _compositor{ nullptr }; vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; mat4 _trackedDevicePoseMat4[vr::k_unMaxTrackedDeviceCount]; @@ -43,7 +38,7 @@ static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; bool OpenVrDisplayPlugin::isSupported() const { - return (enableDebugOpenVR || !isOculusPresent()) && vr::VR_IsHmdPresent(); + return openVrSupported(); } bool OpenVrDisplayPlugin::internalActivate() { diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 8ddf028dd2..8536ffd5d9 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -44,6 +45,12 @@ bool isOculusPresent() { return result; } +bool openVrSupported() { + static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); + static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + return (enableDebugOpenVR || !isOculusPresent()) && vr::VR_IsHmdPresent(); +} + vr::IVRSystem* acquireOpenVrSystem() { bool hmdPresent = vr::VR_IsHmdPresent(); if (hmdPresent) { diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 81896a2ce5..4b06ca0813 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -12,7 +12,8 @@ #include #include -bool isOculusPresent(); +bool openVrSupported(); + vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index fab383a955..5ca8f03ad7 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -50,11 +50,9 @@ static const QString MENU_PATH = MENU_PARENT + ">" + MENU_NAME; static const QString RENDER_CONTROLLERS = "Render Hand Controllers"; const QString ViveControllerManager::NAME = "OpenVR"; -static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); -static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); bool ViveControllerManager::isSupported() const { - return (enableDebugOpenVR || !isOculusPresent()) && vr::VR_IsHmdPresent(); + return openVrSupported(); } bool ViveControllerManager::activate() { @@ -214,12 +212,13 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& } -void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + _inputDevice->update(deltaTime, inputCalibrationData); auto userInputMapper = DependencyManager::get(); // because update mutates the internal state we need to lock userInputMapper->withLock([&, this]() { - _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); + _inputDevice->update(deltaTime, inputCalibrationData); }); if (_inputDevice->_trackedControllers == 0 && _registeredWithInputMapper) { @@ -235,7 +234,7 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu } } -void ViveControllerManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void ViveControllerManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { _poseStateMap.clear(); _buttonPressedMap.clear(); @@ -244,10 +243,8 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle auto leftHandDeviceIndex = _system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand); auto rightHandDeviceIndex = _system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_RightHand); - if (!jointsCaptured) { - handleHandController(deltaTime, leftHandDeviceIndex, inputCalibrationData, true); - handleHandController(deltaTime, rightHandDeviceIndex, inputCalibrationData, false); - } + handleHandController(deltaTime, leftHandDeviceIndex, inputCalibrationData, true); + handleHandController(deltaTime, rightHandDeviceIndex, inputCalibrationData, false); int numTrackedControllers = 0; if (leftHandDeviceIndex != vr::k_unTrackedDeviceIndexInvalid) { diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index d55d4e726c..672ad59cfe 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -31,17 +31,15 @@ namespace vr { class ViveControllerManager : public InputPlugin { Q_OBJECT public: - // Plugin functions - virtual bool isSupported() const override; - virtual bool isJointController() const override { return true; } + bool isSupported() const override; const QString& getName() const override { return NAME; } - virtual bool activate() override; - virtual void deactivate() override; + bool activate() override; + void deactivate() override; - virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } - virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } + void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void updateRendering(RenderArgs* args, render::ScenePointer scene, render::PendingChanges pendingChanges); @@ -53,10 +51,10 @@ private: InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) {} private: // Device functions - virtual controller::Input::NamedVector getAvailableInputs() const override; - virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; - virtual void focusOutEvent() override; + controller::Input::NamedVector getAvailableInputs() const override; + QString getDefaultMappingConfig() const override; + void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; + void focusOutEvent() override; void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); void handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand); diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index 3a5b4a4a4d..36ed566ea7 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -121,7 +121,7 @@ int main(int argc, char** argv) { }; foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - inputPlugin->pluginUpdate(delta, calibrationData, false); + inputPlugin->pluginUpdate(delta, calibrationData); } auto userInputMapper = DependencyManager::get(); @@ -144,7 +144,7 @@ int main(int argc, char** argv) { if (name == KeyboardMouseDevice::NAME) { userInputMapper->registerDevice(std::dynamic_pointer_cast(inputPlugin)->getInputDevice()); } - inputPlugin->pluginUpdate(0, calibrationData, false); + inputPlugin->pluginUpdate(0, calibrationData); } rootContext->setContextProperty("Controllers", new MyControllerScriptingInterface()); } From 73342b2758c1ae89188186a1b23b5e2b8b16664e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 18 May 2016 09:38:44 -0700 Subject: [PATCH 0169/1237] PR feedback --- plugins/hifiNeuron/src/NeuronPlugin.cpp | 2 +- plugins/hifiSpacemouse/src/SpacemouseManager.cpp | 6 ++++-- plugins/openvr/src/ViveControllerManager.cpp | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 41c0eb0d4e..0a4bc7f8d2 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -458,7 +458,7 @@ void NeuronPlugin::InputDevice::update(float deltaTime, const controller::InputC const glm::vec3& pos = joints[i].pos; const glm::vec3& rotEuler = joints[i].euler; - if ((Vectors::ZERO == pos && Vectors::ZERO == rotEuler)) { + if (Vectors::ZERO == pos && Vectors::ZERO == rotEuler) { _poseStateMap[poseIndex] = controller::Pose(); continue; } diff --git a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp index 8b584df366..3d9b93ff44 100644 --- a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp @@ -11,6 +11,10 @@ #include "SpacemouseManager.h" +#ifdef Q_OS_WIN +#include +#endif + #include #include @@ -171,8 +175,6 @@ void SpacemouseDevice::update(float deltaTime, const controller::InputCalibratio #ifdef Q_OS_WIN -#include - bool SpacemouseManager::nativeEventFilter(const QByteArray& eventType, void* message, long* result) { MSG* msg = static_cast< MSG * >(message); return RawInputEventFilter(message, result); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 5ca8f03ad7..12567b10d1 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -213,7 +213,6 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { - _inputDevice->update(deltaTime, inputCalibrationData); auto userInputMapper = DependencyManager::get(); // because update mutates the internal state we need to lock From d95d3ff3ac61c722a109fe50fe99859b31eee585 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 14:56:47 -0700 Subject: [PATCH 0170/1237] clean up debugging prints --- interface/src/avatar/Avatar.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index efaed5b83a..d12306a122 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -197,19 +197,16 @@ void Avatar::updateAvatarEntities() { QScriptEngine scriptEngine; entityTree->withWriteLock([&] { AvatarEntityMap avatarEntities = getAvatarEntityData(); - qDebug() << "---------------"; for (auto entityID : avatarEntities.keys()) { // see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties // and either add or update the entity. QByteArray jsonByteArray = avatarEntities.value(entityID); QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(jsonByteArray); if (!jsonProperties.isObject()) { - qDebug() << "got bad avatarEntity json"; + qCDebug(interfaceapp) << "got bad avatarEntity json" << QString(jsonByteArray.toHex()); continue; } - qDebug() << jsonProperties.toJson(); - QVariant variantProperties = jsonProperties.toVariant(); QVariantMap asMap = variantProperties.toMap(); QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); @@ -229,19 +226,14 @@ void Avatar::updateAvatarEntities() { EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { - qDebug() << "avatar-entities existing entity, element =" << entity->getElement().get(); if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); - qDebug() << "avatar-entities after entityTree->updateEntity(), element =" << entity->getElement().get(); } else { - qDebug() << "AVATAR-ENTITIES -- updateEntity failed: " << properties.getType(); success = false; } } else { - qDebug() << "avatar-entities new entity"; entity = entityTree->addEntity(entityID, properties); if (!entity) { - qDebug() << "AVATAR-ENTITIES -- addEntity failed: " << properties.getType(); success = false; } } @@ -1194,7 +1186,7 @@ void Avatar::setParentID(const QUuid& parentID) { if (success) { setTransform(beforeChangeTransform, success); if (!success) { - qDebug() << "Avatar::setParentID failed to reset avatar's location."; + qCDebug(interfaceapp) << "Avatar::setParentID failed to reset avatar's location."; } } } @@ -1209,7 +1201,7 @@ void Avatar::setParentJointIndex(quint16 parentJointIndex) { if (success) { setTransform(beforeChangeTransform, success); if (!success) { - qDebug() << "Avatar::setParentJointIndex failed to reset avatar's location."; + qCDebug(interfaceapp) << "Avatar::setParentJointIndex failed to reset avatar's location."; } } } From 2e30f0916cc9a365b5a93171527499e22f5385cb Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 20 May 2016 15:07:47 -0700 Subject: [PATCH 0171/1237] remove debug logging and add some comments --- libraries/physics/src/ShapeFactory.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 8c1d44b273..71b919b7ee 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -70,14 +70,15 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin // create hull approximation btShapeHull shapeHull(hull); shapeHull.buildHull(margin); + // we cannot copy Bullet shapes so we must create a new one... btConvexHullShape* newHull = new btConvexHullShape(); const btVector3* newPoints = shapeHull.getVertexPointer(); for (int i = 0; i < shapeHull.numVertices(); ++i) { newHull->addPoint(newPoints[i], false); } + // ...and delete the old one delete hull; hull = newHull; - qDebug() << "reduced hull with" << points.size() << "points down to" << hull->getNumPoints(); // TODO: remove after testing } hull->recalcLocalAabb(); From a24d63a39c3974b05839499524a05c07ca86d1b7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 15:45:00 -0700 Subject: [PATCH 0172/1237] make distance-grab work better when avatar is walking --- .../system/controllers/handControllerGrab.js | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ed4ac219c0..06549a38b5 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1112,8 +1112,8 @@ function MyController(hand) { Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame - var controllerPosition = Vec3.sum(MyAvatar.position, - Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation)); + var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); + var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar); var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); @@ -1161,7 +1161,7 @@ function MyController(hand) { this.turnOffVisualizations(); - this.previousControllerPosition = controllerPosition; + this.previousControllerPositionVSAvatar = controllerPositionVSAvatar; this.previousControllerRotation = controllerRotation; }; @@ -1179,8 +1179,8 @@ function MyController(hand) { Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame - var controllerPosition = Vec3.sum(MyAvatar.position, - Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation)); + var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); + var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar); var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); @@ -1197,7 +1197,8 @@ function MyController(hand) { } // scale delta controller hand movement by radius. - var handMoved = Vec3.multiply(Vec3.subtract(controllerPosition, this.previousControllerPosition), radius); + var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), + radius); // double delta controller rotation var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, @@ -1218,7 +1219,7 @@ function MyController(hand) { var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); // Update radialVelocity - var lastVelocity = Vec3.subtract(controllerPosition, this.previousControllerPosition); + var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaTime); var newRadialVelocity = Vec3.dot(lastVelocity, Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); @@ -1266,7 +1267,9 @@ function MyController(hand) { var clampedVector; var targetPosition; if (constraintData.axisStart !== false) { - clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, constraintData.axisStart, constraintData.axisEnd); + clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, + constraintData.axisStart, + constraintData.axisEnd); targetPosition = clampedVector; } else { targetPosition = { @@ -1309,7 +1312,7 @@ function MyController(hand) { print("continueDistanceHolding -- updateAction failed"); } - this.previousControllerPosition = controllerPosition; + this.previousControllerPositionVSAvatar = controllerPositionVSAvatar; this.previousControllerRotation = controllerRotation; }; From f2f911ef29017f4b75c9606361da0fda2e5700e9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 20 May 2016 16:15:15 -0700 Subject: [PATCH 0173/1237] remove blocks --- .../Home/kineticObjects/blocks.json | 778 ------------------ .../DomainContent/Home/reset.js | 6 - 2 files changed, 784 deletions(-) delete mode 100644 unpublishedScripts/DomainContent/Home/kineticObjects/blocks.json diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/blocks.json b/unpublishedScripts/DomainContent/Home/kineticObjects/blocks.json deleted file mode 100644 index 08331169de..0000000000 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/blocks.json +++ /dev/null @@ -1,778 +0,0 @@ -{ - "Entities": [{ - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{5371218c-e05b-49da-ac70-81f1f76c55ea}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_block", - "position": { - "x": 0.69732666015625, - "y": 0.1600341796875, - "z": 0.28265380859375 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.6540253758430481, - "y": 0.11673291027545929, - "z": 0.23935253918170929 - }, - "rotation": { - "w": 0.996917724609375, - "x": -0.0001678466796875, - "y": 0.078294038772583008, - "z": -0.0003204345703125 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{ccc1198a-a501-48b8-959a-68297258aea7}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_block", - "position": { - "x": 0.47344970703125, - "y": 0.06005859375, - "z": 0.30706787109375 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.4301484227180481, - "y": 0.01675732433795929, - "z": 0.2637665867805481 - }, - "rotation": { - "w": 0.70644688606262207, - "x": 0.70638585090637207, - "y": -0.030960559844970703, - "z": 0.031174182891845703 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{3aaf5dd5-16d8-4852-880d-8256de68de19}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_block", - "position": { - "x": 0.2613525390625, - "y": 0.01007080078125, - "z": 0.359466552734375 - }, - "queryAACube": { - "scale": 0.27386128902435303, - "x": 0.12442189455032349, - "y": -0.12685984373092651, - "z": 0.22253590822219849 - }, - "rotation": { - "w": -4.57763671875e-05, - "x": 0.35637450218200684, - "y": -4.57763671875e-05, - "z": 0.93432521820068359 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{0474a29f-c45b-4d42-ae95-0c0bd1e6c501}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_block", - "position": { - "x": 0.30487060546875, - "y": 0.01007080078125, - "z": 0.188262939453125 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.17651562392711639, - "y": -0.11828418076038361, - "z": 0.059907957911491394 - }, - "rotation": { - "w": 0.99496448040008545, - "x": 1.52587890625e-05, - "y": 0.10020601749420166, - "z": 0.0002288818359375 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{53e06851-8346-45ac-bdb5-0a74c99b5bd5}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_block", - "position": { - "x": 0.40277099609375, - "y": 0.035064697265625, - "z": 0.254364013671875 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": 0.25915694236755371, - "y": -0.10854937136173248, - "z": 0.11074994504451752 - }, - "rotation": { - "w": 0.70650792121887207, - "x": -0.031143665313720703, - "y": -0.031143665313720703, - "z": 0.70632481575012207 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{38bcb70d-e384-4b60-878a-e34d4830f045}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_block", - "position": { - "x": 0.54962158203125, - "y": 0.010040283203125, - "z": 0.294647216796875 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.42126661539077759, - "y": -0.11831469833850861, - "z": 0.16629223525524139 - }, - "rotation": { - "w": 0.999969482421875, - "x": -7.62939453125e-05, - "y": -0.0037384629249572754, - "z": 0.0001068115234375 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{fa8f8bbc-5bd0-4121-985d-75ce2f68eba1}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_block", - "position": { - "x": 0.52001953125, - "y": 0.01007080078125, - "z": 0 - }, - "queryAACube": { - "scale": 0.27386128902435303, - "x": 0.38308888673782349, - "y": -0.12685984373092651, - "z": -0.13693064451217651 - }, - "rotation": { - "w": 0.9861142635345459, - "x": -7.62939453125e-05, - "y": -0.16603344678878784, - "z": -7.62939453125e-05 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{d4b8582b-b707-453c-89c6-65e358da5cd7}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_block", - "position": { - "x": 0.66876220703125, - "y": 0, - "z": 0.012939453125 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.54040724039077759, - "y": -0.12835498154163361, - "z": -0.11541552841663361 - }, - "rotation": { - "w": 0.70589756965637207, - "x": 0.036270737648010254, - "y": -0.036331713199615479, - "z": -0.70647746324539185 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{3b5b53fb-7ee5-44eb-9b81-8de8a525c433}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_block", - "position": { - "x": 0.83819580078125, - "y": 0.135040283203125, - "z": 0.261627197265625 - }, - "queryAACube": { - "scale": 0.27386128902435303, - "x": 0.70126515626907349, - "y": -0.0018903613090515137, - "z": 0.12469655275344849 - }, - "rotation": { - "w": 0.69332420825958252, - "x": 0.13832306861877441, - "y": -0.13841456174850464, - "z": -0.69353783130645752 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{0d1f27e9-7e74-4263-9428-8c8f7aac94a6}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_block", - "position": { - "x": 0.61773681640625, - "y": 0.0350341796875, - "z": 0.265533447265625 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": 0.47412276268005371, - "y": -0.10857988893985748, - "z": 0.12191937863826752 - }, - "rotation": { - "w": 0.9998779296875, - "x": -4.57763671875e-05, - "y": -0.014206171035766602, - "z": 1.52587890625e-05 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{79ea518f-aac3-45ff-b22d-6d295b3c9e87}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_block", - "position": { - "x": 0.7188720703125, - "y": 0.08502197265625, - "z": 0.26397705078125 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": 0.57525801658630371, - "y": -0.058592095971107483, - "z": 0.12036298215389252 - }, - "rotation": { - "w": 0.99993896484375, - "x": -4.57763671875e-05, - "y": -0.0099946856498718262, - "z": -0.0001068115234375 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{ff470ff9-c889-4893-a25f-80895bff0e9a}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "position": { - "x": 0.74981689453125, - "y": 0.010040283203125, - "z": 0.258575439453125 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 0.61991310119628906, - "y": -0.11986352503299713, - "z": 0.12867163121700287 - }, - "rotation": { - "w": 0.99993896484375, - "x": -4.57763671875e-05, - "y": -0.010666072368621826, - "z": -0.0003814697265625 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{b5319f85-603d-436b-8bbe-fc9f798ca738}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_block", - "position": { - "x": 0.8824462890625, - "y": 0.0350341796875, - "z": 0.281097412109375 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": 0.73883223533630371, - "y": -0.10857988893985748, - "z": 0.13748334348201752 - }, - "rotation": { - "w": 0.999847412109375, - "x": -4.57763671875e-05, - "y": -0.016922235488891602, - "z": 1.52587890625e-05 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{944a4616-8dac-4d6a-a92b-49fa98514416}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "position": { - "x": 0.963623046875, - "y": 0.010009765625, - "z": 0.25885009765625 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 0.83371925354003906, - "y": -0.11989404261112213, - "z": 0.12894628942012787 - }, - "rotation": { - "w": 0.999847412109375, - "x": -4.57763671875e-05, - "y": -0.017379999160766602, - "z": 1.52587890625e-05 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{ea6a1038-7047-4a1e-bdbd-076d6e41508c}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "position": { - "x": 0.49188232421875, - "y": 0.010040283203125, - "z": 0.27752685546875 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 0.36197853088378906, - "y": -0.11986352503299713, - "z": 0.14762304723262787 - }, - "rotation": { - "w": 0.99798583984375, - "x": -4.57763671875e-05, - "y": -0.063248634338378906, - "z": 4.57763671875e-05 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{ff65f5dd-2d53-4127-86da-05156a42946d}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_block", - "position": { - "x": 0, - "y": 0.010101318359375, - "z": 0.353302001953125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": -0.04330126941204071, - "y": -0.03319995105266571, - "z": 0.3100007176399231 - }, - "rotation": { - "w": 0.55049967765808105, - "x": 0.44353401660919189, - "y": -0.44359505176544189, - "z": -0.55083543062210083 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{3a9acd14-f754-4c70-b294-9f622c000785}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_block", - "position": { - "x": 0.88006591796875, - "y": 0.010040283203125, - "z": 0.05718994140625 - }, - "queryAACube": { - "scale": 0.27386128902435303, - "x": 0.74313527345657349, - "y": -0.12689036130905151, - "z": -0.079740703105926514 - }, - "rotation": { - "w": 0.86965739727020264, - "x": -4.57763671875e-05, - "y": -0.4936751127243042, - "z": -4.57763671875e-05 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-03-23T21:29:36Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -10, - "z": 0 - }, - "velocity": { - "x": 0, - "y": -0.1, - "z": 0 - }, - "id": "{bb014301-247b-44d0-8b09-b830fea4439e}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_block", - "position": { - "x": 0.80487060546875, - "y": 0.010040283203125, - "z": 0.19500732421875 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.7615693211555481, - "y": -0.03326098620891571, - "z": 0.15170605480670929 - }, - "rotation": { - "w": 0.70440220832824707, - "x": 0.060776710510253906, - "y": -0.060868263244628906, - "z": -0.70455479621887207 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }], - "Version": 57 -} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index bd8e4383e0..b68486864a 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -344,12 +344,6 @@ createKineticEntities: function() { - var blocks = new Blocks({ - x: 1097.1383, - y: 460.3790, - z: -66.4895 - }); - var fruitBowl = new FruitBowl({ x: 1105.3185, y: 460.3221, From a48d26a59df2d88f467424d1d17420617aec7edb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 20 May 2016 16:18:40 -0700 Subject: [PATCH 0174/1237] individual switches --- .../Home/switches/livingRoomLightDown.js | 197 ++++++++++++++++++ .../Home/switches/livingRoomLightUp.js | 195 +++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js create mode 100644 unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js new file mode 100644 index 0000000000..86c9a70716 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js @@ -0,0 +1,197 @@ +// +// +// Created by The Content Team 4/10/216 +// Copyright 2016 High Fidelity, Inc. +// +// this finds lights and toggles thier visibility, and flips the emissive texture of some light models +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + + var SEARCH_RADIUS = 100; + + var _this; + var utilitiesScript = Script.resolvePath('../utils.js'); + Script.include(utilitiesScript); + Switch = function() { + _this = this; + this.switchSound = SoundCache.getSound("atp:/switches/lamp_switch_2.wav"); + }; + + Switch.prototype = { + prefix: 'hifi-home-living-room-disc-', + clickReleaseOnEntity: function(entityID, mouseEvent) { + if (!mouseEvent.isLeftButton) { + return; + } + this.toggleLights(); + }, + + startNearTrigger: function() { + this.toggleLights(); + }, + + modelEmitOn: function(glowDisc) { + var data = { + "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", + "Tex.CeilingLight.Emit": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-On-Diffuse.jpg", + "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" + } + + Entities.editEntity(glowDisc, { + textures: JSON.stringify(data) + }) + }, + + modelEmitOff: function(glowDisc) { + var data = { + "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", + "Tex.CeilingLight.Emit": "", + "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" + } + + Entities.editEntity(glowDisc, { + textures: JSON.stringify(data) + + }) + }, + + masterLightOn: function(masterLight) { + Entities.editEntity(masterLight, { + visible: true + }); + }, + + masterLightOff: function(masterLight) { + Entities.editEntity(masterLight, { + visible: false + }); + }, + + glowLightOn: function(glowLight) { + Entities.editEntity(glowLight, { + visible: true + }); + }, + + glowLightOff: function(glowLight) { + Entities.editEntity(glowLight, { + visible: false + }); + }, + + findGlowLights: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === _this.prefix + "light-glow") { + found.push(result); + } + }); + return found; + }, + + findMasterLights: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === _this.prefix + "light-master") { + found.push(result); + } + }); + return found; + }, + + findEmitModels: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === _this.prefix + "light-model") { + found.push(result); + } + }); + return found; + }, + + findSwitch: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === "hifi-home-living-room-light-switch-up") { + found.push(result); + } + }); + return found; + }, + + toggleLights: function() { + + var glowLights = this.findGlowLights(); + var masterLights = this.findMasterLights(); + var emitModels = this.findEmitModels(); + + glowLights.forEach(function(glowLight) { + // _this.glowLightOff(glowLight); + }); + + masterLights.forEach(function(masterLight) { + _this.masterLightOff(masterLight); + }); + + emitModels.forEach(function(emitModel) { + _this.modelEmitOff(emitModel); + }); + + + Audio.playSound(this.switchSound, { + volume: 0.5, + position: this.position + }); + + Entities.editEntity(this.entityID, { + position: { + x: 1103.9894, + y: 460.6867, + z: -75.5650 + } + }); + + + var otherSwitch = this.findSwitch(); + + print('other switch:: ' + otherSwitch) + + + var success = Entities.editEntity(otherSwitch.toString(), { + position: { + x: 1103.5823, + y: 460.6867, + z: -75.6313 + } + }) + print('edit success ' + success) + }, + + preload: function(entityID) { + this.entityID = entityID; + setEntityCustomData('grabbableKey', this.entityID, { + wantsTrigger: true + }); + + var properties = Entities.getEntityProperties(this.entityID); + + //The light switch is static, so just cache its position once + this.position = Entities.getEntityProperties(this.entityID, "position").position; + } + }; + + // entity scripts always need to return a newly constructed object of our type + return new Switch(); +}); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js new file mode 100644 index 0000000000..1be08a6fec --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js @@ -0,0 +1,195 @@ +// +// +// Created by The Content Team 4/10/216 +// Copyright 2016 High Fidelity, Inc. +// +// this finds lights and toggles thier visibility, and flips the emissive texture of some light models +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + + var SEARCH_RADIUS = 100; + + var _this; + var utilitiesScript = Script.resolvePath('../utils.js'); + Script.include(utilitiesScript); + Switch = function() { + _this = this; + this.switchSound = SoundCache.getSound("atp:/switches/lamp_switch_2.wav"); + }; + + Switch.prototype = { + prefix: 'hifi-home-living-room-disc-', + clickReleaseOnEntity: function(entityID, mouseEvent) { + if (!mouseEvent.isLeftButton) { + return; + } + this.toggleLights(); + }, + + startNearTrigger: function() { + this.toggleLights(); + }, + + modelEmitOn: function(glowDisc) { + var data = { + "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", + "Tex.CeilingLight.Emit": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-On-Diffuse.jpg", + "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" + } + + Entities.editEntity(glowDisc, { + textures: JSON.stringify(data) + }) + }, + + modelEmitOff: function(glowDisc) { + var data = { + "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", + "Tex.CeilingLight.Emit": "", + "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" + } + + Entities.editEntity(glowDisc, { + textures: JSON.stringify(data) + + }) + }, + + masterLightOn: function(masterLight) { + Entities.editEntity(masterLight, { + visible: true + }); + }, + + masterLightOff: function(masterLight) { + Entities.editEntity(masterLight, { + visible: false + }); + }, + + glowLightOn: function(glowLight) { + Entities.editEntity(glowLight, { + visible: true + }); + }, + + glowLightOff: function(glowLight) { + Entities.editEntity(glowLight, { + visible: false + }); + }, + + findGlowLights: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === _this.prefix + "light-glow") { + found.push(result); + } + }); + return found; + }, + + findMasterLights: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === _this.prefix + "light-master") { + found.push(result); + } + }); + return found; + }, + + findEmitModels: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === _this.prefix + "light-model") { + found.push(result); + } + }); + return found; + }, + + findSwitch: function() { + var found = []; + var results = Entities.findEntities(this.position, SEARCH_RADIUS); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + if (properties.name === "hifi-home-living-room-light-switch-down") { + found.push(result); + } + }); + return found; + }, + + toggleLights: function() { + + var glowLights = this.findGlowLights(); + var masterLights = this.findMasterLights(); + var emitModels = this.findEmitModels(); + + glowLights.forEach(function(glowLight) { + // _this.glowLightOff(glowLight); + }); + + masterLights.forEach(function(masterLight) { + _this.masterLightOff(masterLight); + }); + + emitModels.forEach(function(emitModel) { + _this.modelEmitOff(emitModel); + }); + + + Audio.playSound(this.switchSound, { + volume: 0.5, + position: this.position + }); + + Entities.editEntity(this.entityID, { + position: { + x: 1103.9894, + y: 460.6867, + z: -75.5650 + } + }); + + var otherSwitch = this.findSwitch(); + + print('other switch:: ' + otherSwitch) + + var success = Entities.editEntity(otherSwitch.toString(), { + position: { + x: 1103.5823, + y: 460.6867, + z: -75.6313 + } + }) + print('edit success ' + success) + }, + + preload: function(entityID) { + this.entityID = entityID; + setEntityCustomData('grabbableKey', this.entityID, { + wantsTrigger: true + }); + + var properties = Entities.getEntityProperties(this.entityID); + + //The light switch is static, so just cache its position once + this.position = Entities.getEntityProperties(this.entityID, "position").position; + } + }; + + // entity scripts always need to return a newly constructed object of our type + return new Switch(); +}); \ No newline at end of file From fdb0fa45597aeb9547cb39c865432510e10156d6 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 20 May 2016 16:21:52 -0700 Subject: [PATCH 0175/1237] add blocky start --- .../Home/blocky/arrangement1.json | 103 ++++++ .../Home/blocky/arrangement1A.json | 104 ++++++ .../Home/blocky/arrangement2.json | 179 ++++++++++ .../Home/blocky/arrangement2A.json | 174 ++++++++++ .../Home/blocky/arrangement3.json | 311 ++++++++++++++++++ .../Home/blocky/arrangement3A.json | 229 +++++++++++++ .../Home/blocky/arrangement4.json | 232 +++++++++++++ .../Home/blocky/arrangement4A.json | 203 ++++++++++++ .../Home/blocky/arrangement5.json | 254 ++++++++++++++ .../Home/blocky/arrangement5B.json | 203 ++++++++++++ .../Home/blocky/arrangement5a.json | 197 +++++++++++ .../Home/blocky/arrangement6.json | 238 ++++++++++++++ .../Home/blocky/arrangement6B.json | 236 +++++++++++++ .../DomainContent/Home/blocky/blocky.js | 227 +++++++++++++ .../Home/blocky/singleSpawner.js | 27 ++ .../DomainContent/Home/blocky/wrapper.js | 61 ++++ 16 files changed, 2978 insertions(+) create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement1.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement2.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement3.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement4.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement5.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement6.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/blocky.js create mode 100644 unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js create mode 100644 unpublishedScripts/DomainContent/Home/blocky/wrapper.js diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement1.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement1.json new file mode 100644 index 0000000000..754db172b6 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement1.json @@ -0,0 +1,103 @@ +{ + "Entities": [{ + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{e4fd5fc4-10b9-4d84-b6ba-b732cc7de7b7}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0147705078125, + "y": 0.5, + "z": 0 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.12884356081485748, + "y": 0.35638594627380371, + "z": -0.14361406862735748 + }, + "rotation": { + "w": -0.31651788949966431, + "x": -0.31587702035903931, + "y": -0.63225758075714111, + "z": 0.63265430927276611 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{1e310b85-7823-414b-8448-4435e056c07a}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0, + "z": 0.0084075927734375 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.14361406862735748, + "y": -0.14361406862735748, + "z": -0.13520647585391998 + }, + "rotation": { + "w": 0.34120702743530273, + "x": -0.34154266119003296, + "y": 0.61931788921356201, + "z": 0.61916530132293701 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{1f07cd0e-f9d1-46f3-b71a-ad7127d1a0b0}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0155029296875, + "y": 0.25, + "z": 0.0084686279296875 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.12811113893985748, + "y": 0.10638593137264252, + "z": -0.13514544069766998 + }, + "rotation": { + "w": 0.62478065490722656, + "x": -0.62526893615722656, + "y": -0.33089190721511841, + "z": -0.33040362596511841 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json new file mode 100644 index 0000000000..2d224c7d0a --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json @@ -0,0 +1,104 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{061cfb78-aece-414a-a56b-a83df3d3c188}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "parentID": "{adc9842c-db50-4bbf-8d90-8c12e5ad59ac}", + "position": { + "x": -0.0069605633616447449, + "y": -0.0044899135828018188, + "z": 0.24996849894523621 + }, + "queryAACube": { + "scale": 0.86168444156646729, + "x": 1098.883544921875, + "y": 460.20965576171875, + "z": -69.462593078613281 + }, + "rotation": { + "w": -6.8545341491699219e-05, + "x": -0.022976815700531006, + "y": 0.9996645450592041, + "z": 0.00011834502220153809 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{526b3076-78eb-438d-8566-ae0d0599029c}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "parentID": "{adc9842c-db50-4bbf-8d90-8c12e5ad59ac}", + "position": { + "x": 0.0026676803827285767, + "y": -0.016830384731292725, + "z": 0.4999808669090271 + }, + "queryAACube": { + "scale": 0.86168444156646729, + "x": 1098.8680419921875, + "y": 459.95965576171875, + "z": -69.462570190429688 + }, + "rotation": { + "w": 3.6507844924926758e-05, + "x": 0.99916994571685791, + "y": 0.040203869342803955, + "z": -0.0002717822790145874 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "angularDamping": 0, + "angularVelocity": { + "x": 0, + "y": 0.52359879016876221, + "z": 0 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{adc9842c-db50-4bbf-8d90-8c12e5ad59ac}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.14361406862735748, + "y": -0.14361406862735748, + "z": -0.14361406862735748 + }, + "rotation": { + "w": -0.69973748922348022, + "x": -0.69991332292556763, + "y": 0.10158070176839828, + "z": -0.10084760189056396 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement2.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement2.json new file mode 100644 index 0000000000..2a47e2c5d4 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement2.json @@ -0,0 +1,179 @@ +{ + "Entities": [{ + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{e4fd5fc4-10b9-4d84-b6ba-b732cc7de7b7}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0531005859375, + "y": 0.17498779296875, + "z": 0.10013580322265625 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.090513482689857483, + "y": 0.031373724341392517, + "z": -0.043478265404701233 + }, + "rotation": { + "w": 0.70144569873809814, + "x": 0.089502327144145966, + "y": 0.089585684239864349, + "z": 0.70138269662857056 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{82c627f6-d296-4ee9-9206-56aa06846013}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0238037109375, + "y": 0.5, + "z": 0 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": -0.01949755847454071, + "y": 0.4566987156867981, + "z": -0.04330126941204071 + }, + "rotation": { + "w": 0.00020232143288012594, + "x": 0.89600801467895508, + "y": 0.00046253428445197642, + "z": 0.44403755664825439 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": -0.007318682037293911, + "y": -0.0010040029883384705, + "z": -9.1018431703560054e-05 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{c07b3366-9fb2-4192-a2aa-963bbfa46de1}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0, + "z": 0.11705780029296875 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.13693064451217651, + "y": -0.13693064451217651, + "z": -0.019872844219207764 + }, + "rotation": { + "w": 0.52331775426864624, + "x": -0.52310466766357422, + "y": -0.47588011622428894, + "z": -0.47543454170227051 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{1fa9f95d-b040-4d52-8c50-6ceaaf794bba}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.024658203125, + "y": 0.3499755859375, + "z": 0.00504302978515625 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.10369677841663361, + "y": 0.22162060439586639, + "z": -0.12331195175647736 + }, + "rotation": { + "w": 0.69705569744110107, + "x": 0.69662469625473022, + "y": 0.11998884379863739, + "z": -0.12012538313865662 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": -0.0049706185236573219, + "y": 0.0010576546192169189, + "z": 0.00093202776042744517 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{02fb5b0d-17cb-44b2-ad41-9dbe24bcd606}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_block", + "position": { + "x": 0.1104736328125, + "y": 0, + "z": 0.097320556640625 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.026457011699676514, + "y": -0.13693064451217651, + "z": -0.039610087871551514 + }, + "rotation": { + "w": 0.58954423666000366, + "x": 0.58955228328704834, + "y": -0.39041024446487427, + "z": 0.39044272899627686 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json new file mode 100644 index 0000000000..42ae9059ac --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json @@ -0,0 +1,174 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{c170eefd-bd60-4d6b-a475-9c084f1c2de0}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_block", + "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", + "position": { + "x": 0.13519594073295593, + "y": 0.49943554401397705, + "z": 0.017574317753314972 + }, + "queryAACube": { + "scale": 0.82158386707305908, + "x": 1099.1766357421875, + "y": 460.05300903320312, + "z": -69.957748413085938 + }, + "rotation": { + "w": 0.7015533447265625, + "x": -0.70165455341339111, + "y": 0.08770480751991272, + "z": 0.088382631540298462 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{336bdac8-0229-460d-a9be-38efbc8b7e1f}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", + "position": { + "x": 0.083991743624210358, + "y": 0.49937903881072998, + "z": -0.082286134362220764 + }, + "queryAACube": { + "scale": 0.82158386707305908, + "x": 1099.06591796875, + "y": 460.05300903320312, + "z": -69.939018249511719 + }, + "rotation": { + "w": -0.6799309253692627, + "x": -0.68009138107299805, + "y": -0.19405338168144226, + "z": 0.19368152320384979 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{3ba19cc0-2572-4f42-9c21-1d9da982855c}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", + "position": { + "x": 0.10286395996809006, + "y": 0.32441270351409912, + "z": -0.029775351285934448 + }, + "queryAACube": { + "scale": 0.86168444156646729, + "x": 1099.0992431640625, + "y": 460.20794677734375, + "z": -69.975509643554688 + }, + "rotation": { + "w": 0.39181840419769287, + "x": -0.58902782201766968, + "y": 0.58839577436447144, + "z": -0.39155444502830505 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{7b4e39a0-2e4b-49da-a9c0-18bca0adb568}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", + "position": { + "x": 0.0086878137663006783, + "y": 0.14853793382644653, + "z": 0.00078312074765563011 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1099.113037109375, + "y": 460.42950439453125, + "z": -70.023597717285156 + }, + "rotation": { + "w": 0.57103759050369263, + "x": -0.57109135389328003, + "y": -0.41725897789001465, + "z": -0.41673195362091064 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": -0.0010035448940470815, + "y": 0.00021353544434532523, + "z": 0.00018817203817889094 + } + }, + { + "angularDamping": 0, + "angularVelocity": { + "x": 0, + "y": 0.2617993950843811, + "z": 0 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "queryAACube": { + "scale": 0.086602538824081421, + "x": -0.04330126941204071, + "y": -0.04330126941204071, + "z": -0.04330126941204071 + }, + "rotation": { + "w": 0.00035001290962100029, + "x": 0.68673843145370483, + "y": 0.0003638292255345732, + "z": 0.72690451145172119 + }, + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":false},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement3.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement3.json new file mode 100644 index 0000000000..990656eacd --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement3.json @@ -0,0 +1,311 @@ +{ + "Entities": [{ + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{099ee4ce-8cef-4885-b1a5-46c4efc9ec6d}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_block", + "position": { + "x": 0.0489501953125, + "y": 0.074981689453125, + "z": 0.1115264892578125 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.0056489259004592896, + "y": 0.03168042004108429, + "z": 0.06822521984577179 + }, + "rotation": { + "w": 0.28857278823852539, + "x": 0.28886386752128601, + "y": -0.64528524875640869, + "z": 0.64567047357559204 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": -7.4302828579675406e-05, + "y": 0.0010566487908363342, + "z": -0.00053769041551277041 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{83e6f44b-7444-4377-a89e-439a034e6cd3}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.18896484375, + "y": 0.075103759765625, + "z": 0.31917572021484375 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.14566357433795929, + "y": 0.03180249035358429, + "z": 0.27587443590164185 + }, + "rotation": { + "w": 0.00027349172160029411, + "x": 0.89839118719100952, + "y": 0.0002214395790360868, + "z": -0.43919605016708374 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 0.00010077288607135415, + "y": 0.0014465302228927612, + "z": 2.9299229936441407e-05 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:35:02Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{44f920b7-a2ba-4e65-a30a-27bc127f4570}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.121337890625, + "y": 0.075042724609375, + "z": 0.225860595703125 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.07803662121295929, + "y": 0.03174145519733429, + "z": 0.18255932629108429 + }, + "rotation": { + "w": 0.55899232625961304, + "x": -0.43351781368255615, + "y": 0.4330800473690033, + "z": -0.55859774351119995 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{f1b7be87-97d7-4c28-ba77-c431c3a248a4}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.2723388671875, + "y": 0.07513427734375, + "z": 0.44538116455078125 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.22903759777545929, + "y": 0.03183300793170929, + "z": 0.40207988023757935 + }, + "rotation": { + "w": 0.098181776702404022, + "x": 0.70029288530349731, + "y": 0.70022231340408325, + "z": 0.098178565502166748 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{82c627f6-d296-4ee9-9206-56aa06846013}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.327392578125, + "y": 0.07513427734375, + "z": 0.52436065673828125 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.2840912938117981, + "y": 0.03183300793170929, + "z": 0.48105937242507935 + }, + "rotation": { + "w": 0.68337905406951904, + "x": -0.18168261647224426, + "y": 0.18157178163528442, + "z": -0.68338578939437866 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{ae9bb770-d8a5-4a8f-9f07-512347c41fe7}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0238037109375, + "y": 0, + "z": 0.05812835693359375 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.11981035768985748, + "y": -0.14361406862735748, + "z": -0.085485711693763733 + }, + "rotation": { + "w": 7.7116979809943587e-05, + "x": 0.9558948278427124, + "y": 9.8852447990793735e-05, + "z": -0.29370918869972229 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{5dbfa65e-77f6-4646-864c-6ef99387bf21}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.074981689453125, + "z": 0 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": -0.04330126941204071, + "y": 0.03168042004108429, + "z": -0.04330126941204071 + }, + "rotation": { + "w": 0.70565086603164673, + "x": -0.70586901903152466, + "y": 0.043502748012542725, + "z": 0.043741695582866669 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:35:02Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{be46be67-9288-4eef-97be-1bc501bda8f0}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.1683349609375, + "y": 6.103515625e-05, + "z": 0.2762603759765625 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": 0.024720892310142517, + "y": -0.14355303347110748, + "z": 0.13264630734920502 + }, + "rotation": { + "w": 0.22011587023735046, + "x": 0.67208588123321533, + "y": -0.67190313339233398, + "z": -0.21999000012874603 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{eef1688c-5eba-4a57-8ece-39339fee5ffe}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.3221435546875, + "y": 0.000152587890625, + "z": 0.478668212890625 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": 0.17852948606014252, + "y": -0.14346148073673248, + "z": 0.33505415916442871 + }, + "rotation": { + "w": -0.00010969530558213592, + "x": 0.29818582534790039, + "y": 7.1413240220863372e-05, + "z": 0.95450782775878906 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json new file mode 100644 index 0000000000..4446c0f23a --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json @@ -0,0 +1,229 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{68b93994-05ce-49e8-acfc-113439b0016c}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_block", + "position": { + "x": 0.1077880859375, + "y": 0.075286865234375, + "z": 0.0563507080078125 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.06448681652545929, + "y": 0.03198559582233429, + "z": 0.01304943859577179 + }, + "rotation": { + "w": -0.48592910170555115, + "x": -0.48633205890655518, + "y": 0.51336151361465454, + "z": -0.51362788677215576 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{a55936ed-a7bb-4367-a08d-2c35610908ca}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.3470458984375, + "y": 0.075897216796875, + "z": 0.1311492919921875 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.3037446141242981, + "y": 0.03259594738483429, + "z": 0.08784802258014679 + }, + "rotation": { + "w": -0.00018426775932312012, + "x": -0.70232832431793213, + "y": -0.00030004978179931641, + "z": 0.71185272932052612 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:35:02Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{1f6d3b72-8e31-47d7-8306-61722554b8e5}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.23583984375, + "y": 0.075042724609375, + "z": 0.1005401611328125 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.19253857433795929, + "y": 0.03174145519733429, + "z": 0.05723889172077179 + }, + "rotation": { + "w": -0.38409394025802612, + "x": 0.59400159120559692, + "y": -0.5937197208404541, + "z": 0.38357573747634888 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{4f17bcac-91f2-4a0d-ae1d-00c6628ef578}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.583251953125, + "y": 0.07513427734375, + "z": 0.204864501953125 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.5399506688117981, + "y": 0.03183300793170929, + "z": 0.16156323254108429 + }, + "rotation": { + "w": -0.58473116159439087, + "x": 0.39768025279045105, + "y": -0.39757427573204041, + "z": 0.58470022678375244 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{5dfd5f47-3040-4d58-be0d-ce96a2962913}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.490966796875, + "y": 0.07513427734375, + "z": 0.1775970458984375 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.4476655125617981, + "y": 0.03183300793170929, + "z": 0.13429577648639679 + }, + "rotation": { + "w": 0.13917262852191925, + "x": -0.69330555200576782, + "y": -0.69324028491973877, + "z": 0.13919799029827118 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{a7497e99-914a-4a51-9d96-70198f7bbb0e}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.054931640625, + "y": 0, + "z": 0.03050994873046875 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.088682428002357483, + "y": -0.14361406862735748, + "z": -0.11310411989688873 + }, + "rotation": { + "w": -3.9517879486083984e-05, + "x": -0.80475461483001709, + "y": -0.00011920928955078125, + "z": 0.59360742568969727 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{3167fe1f-5d3e-49a5-aee4-b4a657eb93ff}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.074981689453125, + "z": 0 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": -0.04330126941204071, + "y": 0.03168042004108429, + "z": -0.04330126941204071 + }, + "rotation": { + "w": -0.65145671367645264, + "x": 0.65158241987228394, + "y": -0.2746637761592865, + "z": -0.274961918592453 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement4.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement4.json new file mode 100644 index 0000000000..6bb6c4067a --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement4.json @@ -0,0 +1,232 @@ +{ + "Entities": [{ + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{973dc65a-0200-4c27-b311-a16632abd336}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.060791015625, + "y": 0.439910888671875, + "z": 0.02478790283203125 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.067563965916633606, + "y": 0.31155592203140259, + "z": -0.10356707870960236 + }, + "rotation": { + "w": -0.36391082406044006, + "x": -0.60559296607971191, + "y": 0.60754096508026123, + "z": 0.3629324734210968 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": -0.00066184467868879437, + "y": 0.00055500119924545288, + "z": -0.0027390613686293364 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "dynamic": 0, + "id": "{ae9bb770-d8a5-4a8f-9f07-512347c41fe7}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0587158203125, + "y": 0, + "z": 0.06317138671875 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.084898248314857483, + "y": -0.14361406862735748, + "z": -0.080442681908607483 + }, + "rotation": { + "w": 0.62027901411056519, + "x": 0.62011599540710449, + "y": 0.33941084146499634, + "z": -0.33986839652061462 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 0.0011035117786377668, + "y": 0.00049801170825958252, + "z": -1.9822968170046806e-05 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{fc5d6d27-52b0-4334-b60b-84c27f466f76}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_block", + "position": { + "x": 0, + "y": 0.2999267578125, + "z": 0 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": -0.12990380823612213, + "y": 0.17002294957637787, + "z": -0.12990380823612213 + }, + "rotation": { + "w": -0.18395118415355682, + "x": -0.18451963365077972, + "y": -0.6821141242980957, + "z": 0.68325310945510864 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 3.076787106692791e-05, + "y": 0.00042589753866195679, + "z": -0.0022651664912700653 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{fa73da33-a242-4384-9795-317caae92c29}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0673828125, + "y": 0.149932861328125, + "z": 0.0480499267578125 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.069547832012176514, + "y": 0.013002216815948486, + "z": -0.088880717754364014 + }, + "rotation": { + "w": 0.88347971439361572, + "x": -0.00053799827583134174, + "y": 0.46846789121627808, + "z": -0.0010445250663906336 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 0.0007340551819652319, + "y": -0.00011105835437774658, + "z": -0.0010081863729283214 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{d83f649b-1dfd-4fe3-8f25-b472665ec28a}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.112548828125, + "y": 0.29986572265625, + "z": 0.043243408203125 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": -0.017354980111122131, + "y": 0.16996191442012787, + "z": -0.086660400032997131 + }, + "rotation": { + "w": 0.67243033647537231, + "x": 0.67039400339126587, + "y": -0.22182278335094452, + "z": 0.22181977331638336 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 0.0043582655489444733, + "y": 0.0005900263786315918, + "z": -0.0029577072709798813 + } + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{f1b7be87-97d7-4c28-ba77-c431c3a248a4}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0489501953125, + "y": 0.4798583984375, + "z": 0.02193450927734375 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.0056489259004592896, + "y": 0.4365571141242981, + "z": -0.02136676013469696 + }, + "rotation": { + "w": 0.049316942691802979, + "x": -0.046750579029321671, + "y": 0.70699954032897949, + "z": 0.7039417028427124 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": -0.0029058775398880243, + "y": 0.00084279477596282959, + "z": -0.0020738139282912016 + } + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json new file mode 100644 index 0000000000..df74a6aa64 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json @@ -0,0 +1,203 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{0aba0660-0991-47ab-b4d7-9c606f553e68}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", + "position": { + "x": 0.031455338001251221, + "y": -0.032536625862121582, + "z": -0.48027312755584717 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 1100.8177490234375, + "y": 461.0120849609375, + "z": -70.972358703613281 + }, + "rotation": { + "w": 0.0023152828216552734, + "x": -0.53879290819168091, + "y": 0.84243488311767578, + "z": -0.0008878018707036972 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{421d62c3-e5e5-4e51-9b14-46ce25a5cb11}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", + "position": { + "x": 0.017628543078899384, + "y": -0.0010632872581481934, + "z": -0.14994476735591888 + }, + "queryAACube": { + "scale": 0.82158386707305908, + "x": 1100.5284423828125, + "y": 460.40087890625, + "z": -71.226631164550781 + }, + "rotation": { + "w": 0.70702850818634033, + "x": -0.70705664157867432, + "y": -0.010112389922142029, + "z": 0.0089319320395588875 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{95792255-7fa6-4936-bb51-ce2f945dd5b9}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_block", + "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", + "position": { + "x": 0.023592084646224976, + "y": -0.084929108619689941, + "z": -0.29993364214897156 + }, + "queryAACube": { + "scale": 0.77942287921905518, + "x": 1100.482421875, + "y": 460.57196044921875, + "z": -71.256011962890625 + }, + "rotation": { + "w": -0.69225782155990601, + "x": -0.00045704841613769531, + "y": 0.00031775236129760742, + "z": 0.72165042161941528 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{4c3ba7ac-2122-4402-a05d-f70a32bfd9d8}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", + "position": { + "x": 0.05141855776309967, + "y": 0.034773021936416626, + "z": -0.29993501305580139 + }, + "queryAACube": { + "scale": 0.77942287921905518, + "x": 1100.59814453125, + "y": 460.57192993164062, + "z": -71.214653015136719 + }, + "rotation": { + "w": 0.68213790655136108, + "x": -0.0010509714484214783, + "y": -0.00042241811752319336, + "z": 0.73122292757034302 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{f04a15fe-4482-44f0-8b63-46ff8aa7538c}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", + "position": { + "x": 0.036200806498527527, + "y": -0.020915478467941284, + "z": -0.43993330001831055 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1100.5477294921875, + "y": 460.71658325195312, + "z": -71.227401733398438 + }, + "rotation": { + "w": -0.51840746402740479, + "x": -0.47963690757751465, + "y": 0.51959860324859619, + "z": -0.48085314035415649 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "angularDamping": 0, + "angularVelocity": { + "x": 0, + "y": -0.2617993950843811, + "z": 0 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:33:00Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.14361406862735748, + "y": -0.14361406862735748, + "z": -0.14361406862735748 + }, + "rotation": { + "w": 0.2521953284740448, + "x": 0.2517753541469574, + "y": 0.66056090593338013, + "z": -0.66080713272094727 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement5.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement5.json new file mode 100644 index 0000000000..d04f78b106 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement5.json @@ -0,0 +1,254 @@ +{ + "Entities": [ + { + "angularVelocity": { + "x": -0.0058977864682674408, + "y": 0.012016458436846733, + "z": -0.019001182168722153 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{5dbfa65e-77f6-4646-864c-6ef99387bf21}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.118896484375, + "y": 0.261627197265625, + "z": 0.5547332763671875 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.07559521496295929, + "y": 0.21832592785358429, + "z": 0.5114319920539856 + }, + "rotation": { + "w": 0.25579428672790527, + "x": 0.659473717212677, + "y": 0.65893793106079102, + "z": 0.25586038827896118 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 0.0021444200538098812, + "y": 0.0011220350861549377, + "z": 0.0011650724336504936 + } + }, + { + "angularVelocity": { + "x": 0.0021077222190797329, + "y": 0.0096995644271373749, + "z": -0.00475650979205966 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{18d0fe48-f5af-4baa-bc76-fd02b2cf507b}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_block", + "position": { + "x": 0.1376953125, + "y": 0.11163330078125, + "z": 0.54648590087890625 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 0.0077915042638778687, + "y": -0.018270507454872131, + "z": 0.41658210754394531 + }, + "rotation": { + "w": -0.16635751724243164, + "x": -0.16560617089271545, + "y": 0.68745934963226318, + "z": -0.68724048137664795 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{f61afc15-4813-4306-8db2-6fb9dc3baf33}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.2005615234375, + "y": 0.111602783203125, + "z": 0.3127593994140625 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": 0.072206541895866394, + "y": -0.016752198338508606, + "z": 0.18440441787242889 + }, + "rotation": { + "w": 0.5063401460647583, + "x": 0.5066148042678833, + "y": 0.4936140775680542, + "z": -0.4932173490524292 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "angularVelocity": { + "x": 0.0085022579878568649, + "y": 8.5791980382055044e-05, + "z": -0.0055606141686439514 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:59:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic": 0, + "id": "{050ba484-bd34-4819-a860-776f8d865a76}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.261627197265625, + "z": 0.6636199951171875 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": -0.04330126941204071, + "y": 0.21832592785358429, + "z": 0.6203187108039856 + }, + "rotation": { + "w": -0.11174160987138748, + "x": 0.6981397271156311, + "y": 0.69834953546524048, + "z": -0.11145715415477753 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 0.00021521201415453106, + "y": 0.0007597804069519043, + "z": 0.0016687994357198477 + } + }, + { + "angularVelocity": { + "x": 0.10312929749488831, + "y": 0.02031000517308712, + "z": 0.17793627083301544 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "dynamic":0, + "id": "{0a801214-14b7-4059-828a-61f18250a315}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.1876220703125, + "y": 0, + "z": 0 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.14432080090045929, + "y": -0.04330126941204071, + "z": -0.04330126941204071 + }, + "rotation": { + "w": 0.38613453507423401, + "x": 0.27518847584724426, + "y": 0.11528003960847855, + "z": 0.87285858392715454 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": -0.0028362239245325327, + "y": 0.0032131313346326351, + "z": 0.0032204082235693932 + } + }, + { + "angularVelocity": { + "x": 0.0060502123087644577, + "y": -0.0002331961877644062, + "z": -0.0040579927153885365 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{c0abdb26-3e55-422f-811a-306239ec4894}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0091552734375, + "y": 0.11163330078125, + "z": 0.663543701171875 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": -0.12074853479862213, + "y": -0.018270507454872131, + "z": 0.53363990783691406 + }, + "rotation": { + "w": 0.38818395137786865, + "x": -0.38840728998184204, + "y": 0.5908893346786499, + "z": 0.59101837873458862 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", + "velocity": { + "x": 0.00010580161324469373, + "y": 0.00069457292556762695, + "z": 0.00075469817966222763 + } + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json new file mode 100644 index 0000000000..634f1e6858 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json @@ -0,0 +1,203 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{e9a7eb61-bdfe-484d-963a-fdae21bc12e7}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", + "position": { + "x": -0.16834764182567596, + "y": 0.17657718062400818, + "z": -0.14808396995067596 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 1102.9293212890625, + "y": 460.51165771484375, + "z": -70.490364074707031 + }, + "rotation": { + "w": -0.72276210784912109, + "x": -0.67909777164459229, + "y": 0.11672055721282959, + "z": 0.054192394018173218 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:59:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{242a2da8-26e9-439f-a6bd-a5174d68b791}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", + "position": { + "x": 0.17733007669448853, + "y": -0.041430100798606873, + "z": -0.14965415000915527 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 1102.8548583984375, + "y": 460.51361083984375, + "z": -70.082504272460938 + }, + "rotation": { + "w": 0.45803076028823853, + "x": -0.54004138708114624, + "y": 0.45782959461212158, + "z": 0.53765928745269775 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{8d10e070-80b5-41a1-a98d-2fb60a73f98c}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", + "position": { + "x": -0.16358290612697601, + "y": 0.17843163013458252, + "z": 7.3954463005065918e-05 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1102.6942138671875, + "y": 460.10836791992188, + "z": -70.743125915527344 + }, + "rotation": { + "w": 0.50793719291687012, + "x": -0.0007769167423248291, + "y": -0.0016511008143424988, + "z": 0.86144548654556274 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{96880a75-218a-485b-91d3-be9741f320ed}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", + "position": { + "x": 0.16807788610458374, + "y": -0.044651858508586884, + "z": 0.00034449249505996704 + }, + "queryAACube": { + "scale": 0.77942287921905518, + "x": 1102.57421875, + "y": 460.10379028320312, + "z": -70.363555908203125 + }, + "rotation": { + "w": -0.00017070770263671875, + "x": -0.68149137496948242, + "y": -0.73185545206069946, + "z": -0.0021193623542785645 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{0fdd72df-a5f5-4f9c-b3ec-b79bc606b356}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", + "position": { + "x": 0.02161325141787529, + "y": 0.0003502964973449707, + "z": -0.14994175732135773 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 1102.9146728515625, + "y": 460.51358032226562, + "z": -70.236610412597656 + }, + "rotation": { + "w": 0.12661613523960114, + "x": -0.68729931116104126, + "y": 0.1313164234161377, + "z": 0.70317196846008301 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "angularDamping": 0, + "angularVelocity": { + "x": 0, + "y": -0.2617993950843811, + "z": 0 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_block", + "queryAACube": { + "scale": 0.25980761647224426, + "x": -0.12990380823612213, + "y": -0.12990380823612213, + "z": -0.12990380823612213 + }, + "rotation": { + "w": 0.21236260235309601, + "x": 0.20996685326099396, + "y": -0.67540425062179565, + "z": 0.67427384853363037 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json new file mode 100644 index 0000000000..3d12988bbc --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json @@ -0,0 +1,197 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{e5dcde81-ec21-49cd-bfc1-47a04678d620}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.118896484375, + "y": 0.1500244140625, + "z": 0.24393463134765625 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.07559521496295929, + "y": 0.10672314465045929, + "z": 0.20063336193561554 + }, + "rotation": { + "w": 0.25630581378936768, + "x": 0.66857409477233887, + "y": 0.65368127822875977, + "z": 0.24492251873016357 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{ecbc77d5-b578-453c-b502-f4b7331c9088}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0091552734375, + "y": 3.0517578125e-05, + "z": 0.35125732421875 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": -0.12074853479862213, + "y": -0.12987329065799713, + "z": 0.22135351598262787 + }, + "rotation": { + "w": 0.38817429542541504, + "x": -0.38844889402389526, + "y": 0.59087514877319336, + "z": 0.59099721908569336 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{35d2e036-2cfa-4e62-8cb7-81beda65963d}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.2061767578125, + "y": 0.148101806640625, + "z": 0 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.16287548840045929, + "y": 0.10480053722858429, + "z": -0.04330126941204071 + }, + "rotation": { + "w": -0.034055888652801514, + "x": 0.35255527496337891, + "y": -0.041519246995449066, + "z": 0.93420767784118652 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:59:13Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{e7edf689-89c9-4daf-b5e0-38c330f34c6e}", + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.1500244140625, + "z": 0.3527984619140625 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": -0.04330126941204071, + "y": 0.10672314465045929, + "z": 0.3094971776008606 + }, + "rotation": { + "w": -0.11314564943313599, + "x": 0.69872581958770752, + "y": 0.69771873950958252, + "z": -0.11012434959411621 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:46:06Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{764c37c5-892f-41b4-8a34-8e13561726f7}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.2005615234375, + "y": 0, + "z": 0.00042724609375 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": 0.072206541895866394, + "y": -0.12835498154163361, + "z": -0.12792773544788361 + }, + "rotation": { + "w": 0.5063401460647583, + "x": 0.5066148042678833, + "y": 0.4936140775680542, + "z": -0.4932783842086792 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{8b4d7620-0303-447c-815c-ed5a90ba33b3}", + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_block", + "position": { + "x": 0.1376953125, + "y": 3.0517578125e-05, + "z": 0.23415374755859375 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 0.0077915042638778687, + "y": -0.12987329065799713, + "z": 0.10424993932247162 + }, + "rotation": { + "w": -0.16896313428878784, + "x": -0.16664379835128784, + "y": 0.68758678436279297, + "z": -0.68624401092529297 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement6.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement6.json new file mode 100644 index 0000000000..17c88e8308 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement6.json @@ -0,0 +1,238 @@ +{ + "Entities": [{ + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{845bc4f0-f59b-4826-9910-e92ab0a82eca}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.118896484375, + "y": 6.103515625e-05, + "z": 0.16530609130859375 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.009458497166633606, + "y": -0.12829394638538361, + "z": 0.036951109766960144 + }, + "rotation": { + "w": 0.37096202373504639, + "x": -0.37190812826156616, + "y": 0.60173952579498291, + "z": 0.60161745548248291 + }, + "dynamic": 0, + + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{1af7f363-a1e4-4f2b-94af-fc8c592769e8}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.34423828125, + "y": 0.000152587890625, + "z": 0.4805755615234375 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": 0.21588329970836639, + "y": -0.12820239365100861, + "z": 0.35222059488296509 + }, + "rotation": { + "w": 0.66173803806304932, + "x": 0.66201269626617432, + "y": -0.24910354614257812, + "z": 0.24846267700195312 + }, + "dynamic": 0, + + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{f05ff17b-a9ef-4b67-9f1e-a0cdbac1d73a}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.1954345703125, + "y": 9.1552734375e-05, + "z": 0.2958984375 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": 0.067079588770866394, + "y": -0.12826342880725861, + "z": 0.16754345595836639 + }, + "rotation": { + "w": 0.61269545555114746, + "x": 0.61297011375427246, + "y": -0.35289537906646729, + "z": 0.35259020328521729 + }, + "dynamic": 0, + + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + + "id": "{d501c4f4-5ace-4337-a694-bf08230fd3ff}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.238037109375, + "y": 9.1552734375e-05, + "z": 0.35927581787109375 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": 0.10968212783336639, + "y": -0.12826342880725861, + "z": 0.23092083632946014 + }, + "rotation": { + "w": 0.61916530132293701, + "x": -0.61983668804168701, + "y": -0.34154266119003296, + "z": -0.34023040533065796 + }, + "dynamic": 0, + + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{b18bd985-1db5-40cf-b53b-d8765fac9112}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.13693064451217651, + "y": -0.13693064451217651, + "z": -0.13693064451217651 + }, + "rotation": { + "w": 0.67776000499725342, + "x": 0.67730224132537842, + "y": 0.20234990119934082, + "z": -0.20231938362121582 + }, + "dynamic": 0, + + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{0284ad28-9e2b-45f5-91b0-e00da9238b41}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0765380859375, + "y": 3.0517578125e-05, + "z": 0.09934234619140625 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.051816895604133606, + "y": -0.12832446396350861, + "z": -0.029012635350227356 + }, + "rotation": { + "w": 0.65304040908813477, + "x": -0.65294879674911499, + "y": -0.271351158618927, + "z": -0.271198570728302 + }, + "dynamic": 0, + + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionsWillMove": 1, + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "dynamic": 0, + "id": "{7090697d-bf3c-4edc-9090-0ef7e313151b}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_block", + "position": { + "x": 0.4200439453125, + "y": 0.00018310546875, + "z": 0.53116607666015625 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": 0.29168897867202759, + "y": -0.12817187607288361, + "z": 0.40281111001968384 + }, + "rotation": { + "w": 0.66921496391296387, + "x": 0.66921496391296387, + "y": -0.22810709476470947, + "z": 0.22853434085845947 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json new file mode 100644 index 0000000000..edced193a8 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json @@ -0,0 +1,236 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:37:13Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{5692ed0d-52ae-4bd9-8ef6-13a3d28dbcec}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "parentID": "{8c29606d-b071-4793-a5d5-26453bb12bf4}", + "position": { + "x": 0.076191246509552002, + "y": -0.0050119552761316299, + "z": -4.275888204574585e-05 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1104.9146728515625, + "y": 460.09011840820312, + "z": -69.708526611328125 + }, + "rotation": { + "w": -3.1195580959320068e-05, + "x": -0.99968332052230835, + "y": 0.019217833876609802, + "z": 0.0013828873634338379 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:28:29Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{41fdce7a-5c15-4335-a7d4-e6e42c419edb}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_block", + "parentID": "{8c29606d-b071-4793-a5d5-26453bb12bf4}", + "position": { + "x": 0.31622248888015747, + "y": -0.076068185269832611, + "z": -0.00023670494556427002 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1104.552734375, + "y": 460.09014892578125, + "z": -69.711776733398438 + }, + "rotation": { + "w": 0.9812014102935791, + "x": 2.8990209102630615e-05, + "y": 0.00053216516971588135, + "z": -0.19223560392856598 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{376ea748-6b7b-4ac4-9194-b0f118ac39dd}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "parentID": "{8c29606d-b071-4793-a5d5-26453bb12bf4}", + "position": { + "x": -0.22965218126773834, + "y": 0.0041047781705856323, + "z": 0.00015974044799804688 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1105.10009765625, + "y": 460.09011840820312, + "z": -69.742721557617188 + }, + "rotation": { + "w": -7.3388218879699707e-06, + "x": -0.99162417650222778, + "y": 0.1281534731388092, + "z": 0.00039628148078918457 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T21:28:38Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{d1359425-9129-4827-b881-e4802703a0ca}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "parentID": "{8c29606d-b071-4793-a5d5-26453bb12bf4}", + "position": { + "x": 0.23440317809581757, + "y": -0.035929545760154724, + "z": -0.00016139447689056396 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1104.6361083984375, + "y": 460.09011840820312, + "z": -69.792564392089844 + }, + "rotation": { + "w": 0.986641526222229, + "x": -0.00016996264457702637, + "y": -0.00021627545356750488, + "z": -0.16198150813579559 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{11b3c61c-abe3-48ac-9033-3f88d3efee00}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "parentID": "{8c29606d-b071-4793-a5d5-26453bb12bf4}", + "position": { + "x": -0.15134727954864502, + "y": 0.00060278922319412231, + "z": 0.00011575222015380859 + }, + "queryAACube": { + "scale": 0.77012991905212402, + "x": 1104.79541015625, + "y": 460.090087890625, + "z": -69.902366638183594 + }, + "rotation": { + "w": -0.00098252296447753906, + "x": -0.030791401863098145, + "y": 0.99939167499542236, + "z": 0.0002511441707611084 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:32:51Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{15c3aeae-1166-4cff-aa53-d732f20a1203}", + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "parentID": "{8c29606d-b071-4793-a5d5-26453bb12bf4}", + "position": { + "x": -0.35397925972938538, + "y": 0.020401254296302795, + "z": 0.00025978684425354004 + }, + "queryAACube": { + "scale": 0.82158386707305908, + "x": 1105.195068359375, + "y": 460.06436157226562, + "z": -69.863059997558594 + }, + "rotation": { + "w": 0.68755638599395752, + "x": -0.00055142492055892944, + "y": 0.00033529102802276611, + "z": -0.72594141960144043 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "angularDamping": 0, + "angularVelocity": { + "x": 0, + "y": -0.2617993950843811, + "z": 0 + }, + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "created": "2016-05-09T19:30:40Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{8c29606d-b071-4793-a5d5-26453bb12bf4}", + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.12835498154163361, + "y": -0.12835498154163361, + "z": -0.12835498154163361 + }, + "rotation": { + "w": 0.097722500562667847, + "x": 0.097280360758304596, + "y": 0.70029336214065552, + "z": -0.70041131973266602 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js new file mode 100644 index 0000000000..c9dc0a0678 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -0,0 +1,227 @@ +// this script creates a pile of playable blocks and displays a target arrangement when you trigger it + +var BLOCK_RED = { + url: 'atp:/kineticObjects/blocks/planky_red.fbx', + dimensions: { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + } +}; +var BLOCK_BLUE = { + url: 'atp:/kineticObjects/blocks/planky_blue.fbx', + dimensions: { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, +}; +var BLOCK_YELLOW = { + url: 'atp:/kineticObjects/blocks/planky_yellow.fbx', + dimensions: { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + } +}; +var BLOCK_GREEN = { + url: 'atp:/kineticObjects/blocks/planky_green.fbx', + dimensions: { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, +}; +var BLOCK_NATURAL = { + url: "atp:/kineticObjects/blocks/planky_natural.fbx", + dimensions: { + "x": 0.05, + "y": 0.05, + "z": 0.05 + } +}; + +var blocks = [ + BLOCK_RED, BLOCK_BLUE, BLOCK_YELLOW, BLOCK_GREEN, BLOCK_SMALL_CUBE +]; + +var arrangements = [{ + name: 'tall', + blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, ], + target: Script.resolvePath("arrangement1.json") +}, { + name: 'ostrich', + blocks: [BLOCK_RED, BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_NATURAL], + target: Script.resolvePath("arrangement2.json") +}, { + name: 'froglog', + blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], + target: Script.resolvePath("arrangement3.json") +}, { + name: 'allOneLeg', + blocks: [BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_BLUE, BLOCK_BLUE, BLOCK_NATURAL], + target: Script.resolvePath("arrangement4.json") +}, { + name: 'threeppl', + blocks: [BLOCK_BLUE BLOCK_YELLOW, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], + target: Script.resolvePath("arrangement5.json") +}, { + name: 'dominoes', + blocks: [BLOCK_RED, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW], + target: Script.resolvePath("arrangement6.json") +}] + +var PLAYABLE_BLOCKS_POSITION = { + x: 1097.6, + y: 460.5, + z: -66.22 +}; + +var TARGET_BLOCKS_POSITION = { + x: 1096.82, + y: 460.5, + z: -67.689 +}; + +(function() { + //#debug + print('BLOCK ENTITY SCRIPT') + var _this; + + function Blocky() { + _this = this; + } + + Blocky.prototype = { + debug: true, + playableBlocks: [], + targetBlocks: [], + preload: function(entityID) { + print('BLOCKY preload') + this.entityID = entityID; + }, + createTargetBlocks: function(arrangement) { + var created = []; + print('BLOCKY create target blocks') + + var created = []; + var success = Clipboard.importEntities(arrangement.target); + if (success === true) { + created = Clipboard.pasteEntities(TARGET_BLOCKS_POSITION) + print('created ' + created); + } + + this.targetBlocks = created; + print('BLOCKY TARGET BLOCKS:: ' + this.targetBlocks); + }, + createPlayableBlocks: function(arrangement) { + print('BLOCKY creating playable blocks'); + arrangement.blocks.forEach(function(block) { + var blockProps = { + name: "home_model_blocky_block", + type: 'Model', + collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true + } + }), + dimensions: block.dimensions, + modelURL: block.url, + shapeType: 'box', + velocity: { + x: 0, + y: -0.01, + z: 0, + }, + position: PLAYABLE_BLOCKS_POSITION + } + this.playableBlocks.push(Entities.addEntity(blockProps)); + + }) + }, + startNearTrigger: function() { + print('BLOCKY got a near trigger'); + this.advanceRound(); + }, + advanceRound: function() { + print('BLOCKY advance round'); + this.cleanup(); + var arrangement = arrangements[Math.floor(Math.random() * arrangements.length)]; + this.createTargetBlocks(arrangement); + + if (this.debug === true) { + this.debugCreatePlayableBlocks(); + } else { + this.createPlayableBlocks(arrangement); + + } + }, + cleanup: function() { + print('BLOCKY cleanup'); + this.targetBlocks.forEach(function(block) { + Entities.deleteEntity(block); + }); + this.playableBlocks.forEach(function(block) { + Entities.deleteEntity(block); + }); + this.targetBlocks = []; + this.playableBlocks = []; + }, + debugCreatePlayableBlocks: function() { + print('BLOCKY debug create'); + var howMany = 10; + var i; + for (i = 0; i < howMany; i++) { + var block = blocks[Math.floor(Math.random() * blocks.length)]; + var blockProps = { + name: "home_model_blocky_block", + type: 'Model', + collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true + } + }), + dimensions: block.dimensions, + modelURL: block.url, + shapeType: 'box', + velocity: { + x: 0, + y: -0.01, + z: 0, + }, + position: PLAYABLE_BLOCKS_POSITION + } + this.playableBlocks.push(Entities.addEntity(blockProps)); + } + }, + unload: function() { + this.cleanup(); + }, + clickReleaseOnEntity: function() { + print('BLOCKY click') + this.startNearTrigger(); + } + } + + return new Blocky(); +}) \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js b/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js new file mode 100644 index 0000000000..823d9ea67a --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js @@ -0,0 +1,27 @@ +// +// +// Created by The Content Team 4/10/216 +// Copyright 2016 High Fidelity, Inc. +// +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var blockyPath = Script.resolvePath('wrapper.js?'+Math.random()); +Script.include(blockyPath); +var center = Vec3.sum(Vec3.sum(MyAvatar.position, { + x: 0, + y: 0.5, + z: 0 +}), Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +var blocky = new BlockyGame(center, { + x: 0, + y: 0, + z: 0 +}); + +Script.scriptEnding.connect(function() { + blocky.cleanup() +}) \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/wrapper.js b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js new file mode 100644 index 0000000000..140c0753db --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js @@ -0,0 +1,61 @@ +// createPingPongGun.js +// +// Script Type: Entity Spawner +// Created by James B. Pollack on 9/30/2015 +// Copyright 2015 High Fidelity, Inc. +// +// This script creates a gun that shoots ping pong balls when you pull the trigger on a hand controller. +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +var RESETTER_POSITION = { + x: 1098.27, + y: 460.43, + z: -66.15 +}; + +BlockyGame = function(spawnPosition, spawnRotation) { + + var scriptURL = Script.resolvePath("blocky.js?" + Math.random()); + print('SCIRPT?!?' + scriptURL) + + var blockyProps = { + type: 'Box', + name:'home_box_blocky_resetter', + color: { + red: 0, + green: 0, + blue: 255 + }, + dimensions: { + x: 0.25, + y: 0.25, + z: 0.25 + }, + script: scriptURL, + userData: JSON.stringify({ + "grabbableKey": { + "wantsTrigger": true + }, + 'hifiHomeKey': { + 'reset': true + } + }), + dynamic: false, + position: spawnPosition + }; + + var blocky = Entities.addEntity(blockyProps); + + function cleanup() { + print('BLOCKY CLEANUP!') + Entities.deleteEntity(blocky); + } + + this.cleanup = cleanup; + + print('HOME CREATED BLOCKY GAME BLOCK 1') + +} \ No newline at end of file From 6568c0563de1c8b95f47be2b1a5dc0a7bdf7cc67 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 20 May 2016 16:43:55 -0700 Subject: [PATCH 0176/1237] Delay on entry/exit. --- interface/src/ui/OverlayConductor.cpp | 26 +++++++++++++++++++------- interface/src/ui/OverlayConductor.h | 1 + 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 1ceceb741e..e9dc766e73 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -69,27 +69,39 @@ void OverlayConductor::update(float dt) { void OverlayConductor::updateMode() { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); float speed = glm::length(myAvatar->getVelocity()); - bool nowDriving = _driving; const float MIN_DRIVING = 0.2f; const float MAX_NOT_DRIVING = 0.01f; - if (speed > MIN_DRIVING) { + const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; + const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; + int fixmeDiff; + bool nowDriving = _driving; // Assume current _driving mode unless... + if (speed > MIN_DRIVING) { // ... we're definitely moving... nowDriving = true; - } - else if (speed < MAX_NOT_DRIVING) { + } else if (speed < MAX_NOT_DRIVING) { // ... or definitely not. nowDriving = false; } + // Check that we're in this new mode for long enough to really trigger a transition. + if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. + _timeInPotentialMode = 0; + } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. + _timeInPotentialMode = usecTimestampNow(); + nowDriving = _driving; + } else if ((fixmeDiff = (usecTimestampNow() - _timeInPotentialMode)) < (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { + nowDriving = _driving; // Haven't accumulated enough time in new mode, but keep timing. + } else { // a real transition + _timeInPotentialMode = 0; + } + // If we're really in a transition if (nowDriving != _driving) { if (nowDriving) { _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); - } - else { // reset when coming out of driving + } else { // reset when coming out of driving _mode = FLAT; // Seems appropriate to let things reset, below, after the following. // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. qApp->getActiveDisplayPlugin()->resetSensors(); myAvatar->reset(true, false); } if (_wantsOverlays) { - qDebug() << "flipping" << !nowDriving; setEnabled(!nowDriving, false); } _driving = nowDriving; diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 02b2035b07..99f4b56584 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -32,6 +32,7 @@ private: Mode _mode { FLAT }; bool _enabled { false }; bool _driving { false }; + quint64 _timeInPotentialMode { 0 }; bool _wantsOverlays { true }; }; From 9a428947e4f9fa6c51e76d69d385dc2eec33b945 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 20 May 2016 16:50:17 -0700 Subject: [PATCH 0177/1237] Simplify code. --- interface/src/ui/OverlayConductor.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index e9dc766e73..fa74989f4f 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -73,7 +73,6 @@ void OverlayConductor::updateMode() { const float MAX_NOT_DRIVING = 0.01f; const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; - int fixmeDiff; bool nowDriving = _driving; // Assume current _driving mode unless... if (speed > MIN_DRIVING) { // ... we're definitely moving... nowDriving = true; @@ -85,14 +84,8 @@ void OverlayConductor::updateMode() { _timeInPotentialMode = 0; } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. _timeInPotentialMode = usecTimestampNow(); - nowDriving = _driving; - } else if ((fixmeDiff = (usecTimestampNow() - _timeInPotentialMode)) < (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { - nowDriving = _driving; // Haven't accumulated enough time in new mode, but keep timing. - } else { // a real transition - _timeInPotentialMode = 0; - } - // If we're really in a transition - if (nowDriving != _driving) { + } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { + _timeInPotentialMode = 0; // a real transition if (nowDriving) { _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); } else { // reset when coming out of driving @@ -105,7 +98,7 @@ void OverlayConductor::updateMode() { setEnabled(!nowDriving, false); } _driving = nowDriving; - } + } // Else haven't accumulated enough time in new mode, but keep timing. Mode newMode; if (qApp->isHMDMode()) { From 7f89ded241bd3692007c1289a1f4c8f3074c5399 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 20 May 2016 16:55:12 -0700 Subject: [PATCH 0178/1237] add blocky --- unpublishedScripts/DomainContent/Home/blocky/blocky.js | 2 +- .../DomainContent/Home/blocky/singleSpawner.js | 2 +- unpublishedScripts/DomainContent/Home/reset.js | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js index c9dc0a0678..cc185b96bf 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/blocky.js +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -93,7 +93,7 @@ var TARGET_BLOCKS_POSITION = { } Blocky.prototype = { - debug: true, + debug: false, playableBlocks: [], targetBlocks: [], preload: function(entityID) { diff --git a/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js b/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js index 823d9ea67a..4bc5ddd3a9 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js +++ b/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var blockyPath = Script.resolvePath('wrapper.js?'+Math.random()); +var blockyPath = Script.resolvePath('wrapper.js'); Script.include(blockyPath); var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index b68486864a..84320d11ff 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -338,6 +338,12 @@ z: -0.0013 }); + var blocky = new BlockyGame({ + x: 1098.27, + y: 460.43, + z: -66.15 + }) + print('HOME after creating scripted entities') }, @@ -428,7 +434,7 @@ y: 459.5230, z: -84.3897 }); - + print('HOME after creating kinetic entities'); }, From 6bc745ee017517bc625e22685026fb7f2cef3f63 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 20 May 2016 17:05:38 -0700 Subject: [PATCH 0179/1237] end of day --- unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js | 2 +- unpublishedScripts/DomainContent/Home/blocky/wrapper.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js b/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js index 4bc5ddd3a9..fc6650ac6a 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js +++ b/unpublishedScripts/DomainContent/Home/blocky/singleSpawner.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var blockyPath = Script.resolvePath('wrapper.js'); +var blockyPath = 'atp:/blocky/wrapper.js'; Script.include(blockyPath); var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, diff --git a/unpublishedScripts/DomainContent/Home/blocky/wrapper.js b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js index 140c0753db..2b56b716ec 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/wrapper.js +++ b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js @@ -18,8 +18,7 @@ var RESETTER_POSITION = { BlockyGame = function(spawnPosition, spawnRotation) { - var scriptURL = Script.resolvePath("blocky.js?" + Math.random()); - print('SCIRPT?!?' + scriptURL) + var scriptURL ="atp:/blocky/blocky.js"; var blockyProps = { type: 'Box', From 5a27d656c52d6524940d38a84d55799259c56e4b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 21 May 2016 13:03:58 +1200 Subject: [PATCH 0180/1237] Potential OSX fix plus extra logging --- interface/resources/qml/dialogs/FileDialog.qml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 78d5943479..b00ee76b5e 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -130,17 +130,23 @@ ModalWindow { choices = [], i, length; + console.log("####### folder parts: " + JSON.stringify(folders)); + if (folders[folders.length - 1] === "") { folders.pop(); } - if (folders[0] !== "") { - choices.push(folders[0]); - } + choices.push(folders[0]); for (i = 1, length = folders.length; i < length; i++) { choices.push(choices[i - 1] + "/" + folders[i]); } + + if (folders[0] === "") { + // Special handling for OSX root dir. + choices[0] = "/"; + } + choices.reverse(); if (drives && drives.length > 1) { @@ -154,6 +160,9 @@ ModalWindow { onLastValidFolderChanged: { var folder = d.capitalizeDrive(lastValidFolder); + + console.log("####### lastValidFolder: " + folder); + calculatePathChoices(folder); } From 637735bbc3e6099ed6648d31812a027d866ffcfb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 18:07:38 -0700 Subject: [PATCH 0181/1237] unmangle merge --- libraries/entities/src/EntityItemProperties.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 03dc4a0557..99285b4986 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -690,13 +690,11 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); -<<<<<<< HEAD - COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); - COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); -======= COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed); COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed); ->>>>>>> e012630db23d8aba81fae0721f69322220b21be2 + + COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); + COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); _lastEdited = usecTimestampNow(); } @@ -1592,13 +1590,11 @@ void EntityItemProperties::markAllChanged() { _queryAACubeChanged = true; -<<<<<<< HEAD - _clientOnlyChanged = true; - _owningAvatarIDChanged = true; -======= _flyingAllowedChanged = true; _ghostingAllowedChanged = true; ->>>>>>> e012630db23d8aba81fae0721f69322220b21be2 + + _clientOnlyChanged = true; + _owningAvatarIDChanged = true; } // The minimum bounding box for the entity. From a294170c74271ed1cb4b40a6e809ed2067bb5ce9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 21 May 2016 15:50:06 +1200 Subject: [PATCH 0182/1237] Fix not being able to teleport to user when users window moved right --- scripts/system/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index d935dd23ca..c010b7ea24 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -716,7 +716,7 @@ var usersWindow = (function () { if (clickedOverlay === windowPane) { - overlayX = event.x - WINDOW_MARGIN; + overlayX = event.x - windowPosition.x - WINDOW_MARGIN; overlayY = event.y - windowPosition.y + windowHeight - WINDOW_MARGIN - windowLineHeight; numLinesBefore = Math.round(overlayY / windowLineHeight); From 5211b6ad77084527724e37ab19247ee495bb2aed Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Sat, 21 May 2016 11:43:12 -0700 Subject: [PATCH 0183/1237] add blocky --- .../DomainContent/Home/blocky/blocky.js | 29 +++++++++++-------- .../DomainContent/Home/reset.js | 6 +++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js index cc185b96bf..0c4b252a21 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/blocky.js +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -42,33 +42,33 @@ var BLOCK_NATURAL = { }; var blocks = [ - BLOCK_RED, BLOCK_BLUE, BLOCK_YELLOW, BLOCK_GREEN, BLOCK_SMALL_CUBE + BLOCK_RED, BLOCK_BLUE, BLOCK_YELLOW, BLOCK_GREEN, BLOCK_NATURAL ]; var arrangements = [{ name: 'tall', - blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, ], - target: Script.resolvePath("arrangement1.json") + blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN], + target: "atp:/blocky/arrangement1A.json" }, { name: 'ostrich', blocks: [BLOCK_RED, BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_NATURAL], - target: Script.resolvePath("arrangement2.json") + target: "atp:/blocky/arrangement2A.json" }, { name: 'froglog', blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], - target: Script.resolvePath("arrangement3.json") + target: "atp:/blocky/arrangement3A.json" }, { name: 'allOneLeg', blocks: [BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_BLUE, BLOCK_BLUE, BLOCK_NATURAL], - target: Script.resolvePath("arrangement4.json") + target: "atp:/blocky/arrangement4A.json" }, { name: 'threeppl', - blocks: [BLOCK_BLUE BLOCK_YELLOW, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], - target: Script.resolvePath("arrangement5.json") + blocks: [BLOCK_BLUE, BLOCK_YELLOW, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], + target: "atp:/blocky/arrangement5B.json" }, { name: 'dominoes', blocks: [BLOCK_RED, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW], - target: Script.resolvePath("arrangement6.json") + target: "atp:/blocky/arrangement6B.json" }] var PLAYABLE_BLOCKS_POSITION = { @@ -115,8 +115,9 @@ var TARGET_BLOCKS_POSITION = { print('BLOCKY TARGET BLOCKS:: ' + this.targetBlocks); }, createPlayableBlocks: function(arrangement) { - print('BLOCKY creating playable blocks'); + print('BLOCKY creating playable blocks' + arrangement.blocks.length); arrangement.blocks.forEach(function(block) { + print('BLOCKY in a block loop') var blockProps = { name: "home_model_blocky_block", type: 'Model', @@ -145,9 +146,13 @@ var TARGET_BLOCKS_POSITION = { }, position: PLAYABLE_BLOCKS_POSITION } - this.playableBlocks.push(Entities.addEntity(blockProps)); - + var newBlock = Entities.addEntity(blockProps); + print('BLOCKY made a playable block' + newBlock) + _this.playableBlocks.push(newBlock); + print('BLOCKY pushing it into playable blocks'); }) + + print('BLOCKY after going through playable arrangement') }, startNearTrigger: function() { print('BLOCKY got a near trigger'); diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index 84320d11ff..bdc97fcbc6 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -43,6 +43,8 @@ var transformerPath = Script.resolvePath("atp:/dressingRoom/wrapper.js"); + var blockyPath = Script.resolvePath("atp:/blocky/wrapper.js"); + Script.include(utilsPath); Script.include(kineticPath); @@ -53,6 +55,7 @@ Script.include(cuckooClockPath); Script.include(pingPongGunPath); Script.include(transformerPath); + Script.include(blockyPath); var TRANSFORMER_URL_ROBOT = 'atp:/dressingRoom/simple_robot.fbx'; @@ -327,6 +330,7 @@ y: 179.0293, z: 89.9698 }); + print('home before cuckooClock') var cuckooClock = new MyCuckooClock({ x: 1105.5237, @@ -337,7 +341,7 @@ y: -57.0089, z: -0.0013 }); - + print('home after cuckooClock') var blocky = new BlockyGame({ x: 1098.27, y: 460.43, From 40e862cf9e624b50f91f267c87148ceeb7fef505 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 21 May 2016 16:53:01 -0700 Subject: [PATCH 0184/1237] quiet gcc 5 warnings --- .../octree/OctreeInboundPacketProcessor.cpp | 6 ++--- interface/src/Camera.h | 4 ++++ .../AssetMappingsScriptingInterface.cpp | 2 +- libraries/animation/src/AnimationCache.cpp | 2 +- libraries/audio/src/SoundCache.cpp | 2 +- .../src/controllers/UserInputMapper.cpp | 8 +++---- libraries/entities/src/EntityTypes.h | 2 +- libraries/fbx/src/FBXReader.cpp | 8 +++---- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 4 ++-- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 2 +- libraries/gpu/src/gpu/Texture.cpp | 2 +- libraries/networking/src/HifiSockAddr.cpp | 2 +- libraries/networking/src/Node.cpp | 6 ++--- libraries/networking/src/ReceivedMessage.cpp | 4 ++-- libraries/networking/src/udt/Packet.cpp | 2 +- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketList.cpp | 2 +- libraries/render-utils/src/Model.cpp | 6 ++--- libraries/script-engine/src/ScriptEngine.cpp | 2 +- libraries/shared/src/RegisteredMetaTypes.cpp | 22 +++++++++---------- libraries/shared/src/SimpleMovingAverage.h | 2 +- tests/render-utils/src/main.cpp | 4 ---- 22 files changed, 48 insertions(+), 48 deletions(-) diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp index c5d010871c..32f3de2ff6 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp @@ -153,7 +153,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer qDebug() << " maxSize=" << maxSize; qDebug("OctreeInboundPacketProcessor::processPacket() %hhu " "payload=%p payloadLength=%lld editData=%p payloadPosition=%lld maxSize=%d", - packetType, message->getRawMessage(), message->getSize(), editData, + (int)packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition(), maxSize); } @@ -191,7 +191,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer if (debugProcessPacket) { qDebug("OctreeInboundPacketProcessor::processPacket() DONE LOOPING FOR %hhu " "payload=%p payloadLength=%lld editData=%p payloadPosition=%lld", - packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition()); + (int)packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition()); } // Make sure our Node and NodeList knows we've heard from this node. @@ -208,7 +208,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer } trackInboundPacket(nodeUUID, sequence, transitTime, editsInPacket, processTime, lockWaitTime); } else { - qDebug("unknown packet ignored... packetType=%hhu", packetType); + qDebug("unknown packet ignored... packetType=%hhu", (int)packetType); } } diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 017bd742a4..486b98c100 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -29,6 +29,10 @@ enum CameraMode }; Q_DECLARE_METATYPE(CameraMode); + +#if defined(__GNUC__) && !defined(__clang__) +__attribute__((unused)) +#endif static int cameraModeId = qRegisterMetaType(); class Camera : public QObject { diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index 965b3a9e0c..f1198c9d5b 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -196,7 +196,7 @@ bool AssetMappingModel::isKnownFolder(QString path) const { return false; } -static int assetMappingModelMetatypeId = qRegisterMetaType("AssetMappingModel*"); +int assetMappingModelMetatypeId = qRegisterMetaType("AssetMappingModel*"); void AssetMappingModel::refresh() { qDebug() << "Refreshing asset mapping model"; diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 482c4211cb..7601fbc782 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -15,7 +15,7 @@ #include "AnimationCache.h" #include "AnimationLogging.h" -static int animationPointerMetaTypeId = qRegisterMetaType(); +int animationPointerMetaTypeId = qRegisterMetaType(); AnimationCache::AnimationCache(QObject* parent) : ResourceCache(parent) diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp index 96a2cee204..6b34c68959 100644 --- a/libraries/audio/src/SoundCache.cpp +++ b/libraries/audio/src/SoundCache.cpp @@ -14,7 +14,7 @@ #include "AudioLogging.h" #include "SoundCache.h" -static int soundPointerMetaTypeId = qRegisterMetaType(); +int soundPointerMetaTypeId = qRegisterMetaType(); SoundCache::SoundCache(QObject* parent) : ResourceCache(parent) diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index c0d3ff40c0..c1ee3ce36c 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -336,10 +336,10 @@ QVector UserInputMapper::getActionNames() const { return result; } -static int actionMetaTypeId = qRegisterMetaType(); -static int inputMetaTypeId = qRegisterMetaType(); -static int inputPairMetaTypeId = qRegisterMetaType(); -static int poseMetaTypeId = qRegisterMetaType("Pose"); +int actionMetaTypeId = qRegisterMetaType(); +int inputMetaTypeId = qRegisterMetaType(); +int inputPairMetaTypeId = qRegisterMetaType(); +int poseMetaTypeId = qRegisterMetaType("Pose"); QScriptValue inputToScriptValue(QScriptEngine* engine, const Input& input); void inputFromScriptValue(const QScriptValue& object, Input& input); diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index 3536327d18..fe37c9b5f7 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -69,7 +69,7 @@ private: /// named NameEntityItem and must of a static method called factory that takes an EnityItemID, and EntityItemProperties and return a newly /// constructed (heap allocated) instance of your type. e.g. The following prototype: // static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); -#define REGISTER_ENTITY_TYPE(x) static bool x##Registration = \ +#define REGISTER_ENTITY_TYPE(x) bool x##Registration = \ EntityTypes::registerEntityType(EntityTypes::x, #x, x##EntityItem::factory); /// Macro for registering entity types with an overloaded factory. Like using the REGISTER_ENTITY_TYPE macro: Make sure to add diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 65cae5343a..12cd0b0a91 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -39,7 +39,7 @@ using namespace std; -static int FBXGeometryPointerMetaTypeId = qRegisterMetaType(); +int FBXGeometryPointerMetaTypeId = qRegisterMetaType(); QStringList FBXGeometry::getJointNames() const { QStringList names; @@ -130,9 +130,9 @@ QString FBXGeometry::getModelNameOfMesh(int meshIndex) const { return QString(); } -static int fbxGeometryMetaTypeId = qRegisterMetaType(); -static int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); -static int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); +int fbxGeometryMetaTypeId = qRegisterMetaType(); +int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); +int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); glm::vec3 parseVec3(const QString& string) { diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index e90ef9e782..388ca26482 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -716,9 +716,9 @@ QQmlContext* OffscreenQmlSurface::getRootContext() { } Q_DECLARE_METATYPE(std::function); -static auto VoidLambdaType = qRegisterMetaType>(); +auto VoidLambdaType = qRegisterMetaType>(); Q_DECLARE_METATYPE(std::function); -static auto VariantLambdaType = qRegisterMetaType>(); +auto VariantLambdaType = qRegisterMetaType>(); void OffscreenQmlSurface::executeOnUiThread(std::function function, bool blocking ) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 4d264995ae..ece1ee730c 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -33,7 +33,7 @@ using namespace gpu::gl; static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); -static bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); Backend* GLBackend::createBackend() { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 3c8d39a838..f9a8f3b26b 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -22,7 +22,7 @@ using namespace gpu; -static int TexturePointerMetaTypeId = qRegisterMetaType(); +int TexturePointerMetaTypeId = qRegisterMetaType(); std::atomic Texture::_textureCPUCount{ 0 }; std::atomic Texture::_textureCPUMemoryUsage{ 0 }; diff --git a/libraries/networking/src/HifiSockAddr.cpp b/libraries/networking/src/HifiSockAddr.cpp index bc4c1a1599..bde0aca7b7 100644 --- a/libraries/networking/src/HifiSockAddr.cpp +++ b/libraries/networking/src/HifiSockAddr.cpp @@ -16,7 +16,7 @@ #include "HifiSockAddr.h" #include "NetworkLogging.h" -static int hifiSockAddrMetaTypeId = qRegisterMetaType(); +int hifiSockAddrMetaTypeId = qRegisterMetaType(); HifiSockAddr::HifiSockAddr() : _address(), diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index e4fe292223..1e1cec2413 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -22,9 +22,9 @@ const QString UNKNOWN_NodeType_t_NAME = "Unknown"; -static int NodePtrMetaTypeId = qRegisterMetaType("Node*"); -static int sharedPtrNodeMetaTypeId = qRegisterMetaType>("QSharedPointer"); -static int sharedNodePtrMetaTypeId = qRegisterMetaType("SharedNodePointer"); +int NodePtrMetaTypeId = qRegisterMetaType("Node*"); +int sharedPtrNodeMetaTypeId = qRegisterMetaType>("QSharedPointer"); +int sharedNodePtrMetaTypeId = qRegisterMetaType("SharedNodePointer"); namespace NodeType { QHash TypeNameHash; diff --git a/libraries/networking/src/ReceivedMessage.cpp b/libraries/networking/src/ReceivedMessage.cpp index 6ef2b7c7d4..cd3eb03473 100644 --- a/libraries/networking/src/ReceivedMessage.cpp +++ b/libraries/networking/src/ReceivedMessage.cpp @@ -14,8 +14,8 @@ #include "QSharedPointer" -static int receivedMessageMetaTypeId = qRegisterMetaType("ReceivedMessage*"); -static int sharedPtrReceivedMessageMetaTypeId = qRegisterMetaType>("QSharedPointer"); +int receivedMessageMetaTypeId = qRegisterMetaType("ReceivedMessage*"); +int sharedPtrReceivedMessageMetaTypeId = qRegisterMetaType>("QSharedPointer"); static const int HEAD_DATA_SIZE = 512; diff --git a/libraries/networking/src/udt/Packet.cpp b/libraries/networking/src/udt/Packet.cpp index d46cae2404..e852332317 100644 --- a/libraries/networking/src/udt/Packet.cpp +++ b/libraries/networking/src/udt/Packet.cpp @@ -19,7 +19,7 @@ using namespace udt; -static int packetMetaTypeId = qRegisterMetaType("Packet*"); +int packetMetaTypeId = qRegisterMetaType("Packet*"); using Key = uint64_t; static const std::array KEYS {{ diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index b04d582d6d..915c2f44ba 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -18,7 +18,7 @@ Q_DECLARE_METATYPE(PacketType); -static int packetTypeMetaTypeId = qRegisterMetaType(); +int packetTypeMetaTypeId = qRegisterMetaType(); const QSet NON_VERIFIED_PACKETS = QSet() << PacketType::NodeJsonStats << PacketType::EntityQuery diff --git a/libraries/networking/src/udt/PacketList.cpp b/libraries/networking/src/udt/PacketList.cpp index b12b7e6938..ea84548210 100644 --- a/libraries/networking/src/udt/PacketList.cpp +++ b/libraries/networking/src/udt/PacketList.cpp @@ -15,7 +15,7 @@ using namespace udt; -static int packetListMetaTypeId = qRegisterMetaType("PacketList*"); +int packetListMetaTypeId = qRegisterMetaType("PacketList*"); std::unique_ptr PacketList::create(PacketType packetType, QByteArray extendedHeader, bool isReliable, bool isOrdered) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 2fe4427333..9ef16b2daa 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -29,9 +29,9 @@ using namespace std; -static int nakedModelPointerTypeId = qRegisterMetaType(); -static int weakNetworkGeometryPointerTypeId = qRegisterMetaType >(); -static int vec3VectorTypeId = qRegisterMetaType >(); +int nakedModelPointerTypeId = qRegisterMetaType(); +int weakNetworkGeometryPointerTypeId = qRegisterMetaType >(); +int vec3VectorTypeId = qRegisterMetaType >(); float Model::FAKE_DIMENSION_PLACEHOLDER = -1.0f; #define HTTP_INVALID_COM "http://invalid.com" diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 7c3aad877e..c9d5ca35b0 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -65,7 +65,7 @@ static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) -static int functionSignatureMetaID = qRegisterMetaType(); +int functionSignatureMetaID = qRegisterMetaType(); static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ QString message = ""; diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 53fa8b30cf..8e6c1ef6ed 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -21,17 +21,17 @@ #include "RegisteredMetaTypes.h" -static int vec4MetaTypeId = qRegisterMetaType(); -static int vec3MetaTypeId = qRegisterMetaType(); -static int qVectorVec3MetaTypeId = qRegisterMetaType>(); -static int qVectorQuatMetaTypeId = qRegisterMetaType>(); -static int qVectorBoolMetaTypeId = qRegisterMetaType>(); -static int vec2MetaTypeId = qRegisterMetaType(); -static int quatMetaTypeId = qRegisterMetaType(); -static int xColorMetaTypeId = qRegisterMetaType(); -static int pickRayMetaTypeId = qRegisterMetaType(); -static int collisionMetaTypeId = qRegisterMetaType(); -static int qMapURLStringMetaTypeId = qRegisterMetaType>(); +int vec4MetaTypeId = qRegisterMetaType(); +int vec3MetaTypeId = qRegisterMetaType(); +int qVectorVec3MetaTypeId = qRegisterMetaType>(); +int qVectorQuatMetaTypeId = qRegisterMetaType>(); +int qVectorBoolMetaTypeId = qRegisterMetaType>(); +int vec2MetaTypeId = qRegisterMetaType(); +int quatMetaTypeId = qRegisterMetaType(); +int xColorMetaTypeId = qRegisterMetaType(); +int pickRayMetaTypeId = qRegisterMetaType(); +int collisionMetaTypeId = qRegisterMetaType(); +int qMapURLStringMetaTypeId = qRegisterMetaType>(); void registerMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, mat4toScriptValue, mat4FromScriptValue); diff --git a/libraries/shared/src/SimpleMovingAverage.h b/libraries/shared/src/SimpleMovingAverage.h index 296911ae3e..dd25705f7e 100644 --- a/libraries/shared/src/SimpleMovingAverage.h +++ b/libraries/shared/src/SimpleMovingAverage.h @@ -57,7 +57,7 @@ public: void addSample(T sample) { if (numSamples > 0) { - average = (sample * WEIGHTING) + (average * ONE_MINUS_WEIGHTING); + average = ((float)sample * WEIGHTING) + ((float)average * ONE_MINUS_WEIGHTING); } else { average = sample; } diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index db6598c43d..ccb91590c3 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -157,10 +157,6 @@ protected: //static const wchar_t* EXAMPLE_TEXT = L"\xC1y Hello 1.0\ny\xC1 line 2\n\xC1y"; static const glm::uvec2 QUAD_OFFSET(10, 10); -static const glm::vec3 COLORS[4] = { { 1.0, 1.0, 1.0 }, { 0.5, 1.0, 0.5 }, { - 1.0, 0.5, 0.5 }, { 0.5, 0.5, 1.0 } }; - - void testShaderBuild(const char* vs_src, const char * fs_src) { auto vs = gpu::Shader::createVertex(std::string(vs_src)); auto fs = gpu::Shader::createPixel(std::string(fs_src)); From d7142aee806e570ea07cef9c360ff89cc1c7ca89 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 21 May 2016 18:06:21 -0700 Subject: [PATCH 0185/1237] try to make osx clang happy, also --- .../src/octree/OctreeInboundPacketProcessor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp index 32f3de2ff6..d2fef4dfbd 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp @@ -153,7 +153,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer qDebug() << " maxSize=" << maxSize; qDebug("OctreeInboundPacketProcessor::processPacket() %hhu " "payload=%p payloadLength=%lld editData=%p payloadPosition=%lld maxSize=%d", - (int)packetType, message->getRawMessage(), message->getSize(), editData, + (unsigned char)packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition(), maxSize); } @@ -191,7 +191,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer if (debugProcessPacket) { qDebug("OctreeInboundPacketProcessor::processPacket() DONE LOOPING FOR %hhu " "payload=%p payloadLength=%lld editData=%p payloadPosition=%lld", - (int)packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition()); + (unsigned char)packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition()); } // Make sure our Node and NodeList knows we've heard from this node. @@ -208,7 +208,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer } trackInboundPacket(nodeUUID, sequence, transitTime, editsInPacket, processTime, lockWaitTime); } else { - qDebug("unknown packet ignored... packetType=%hhu", (int)packetType); + qDebug("unknown packet ignored... packetType=%hhu", (unsigned char)packetType); } } From cd1e910844ca3d7e9f6d0e722a6df07473fd9672 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 10 May 2016 09:27:01 -0700 Subject: [PATCH 0186/1237] Add a generic shape primitive --- .../src/EntityTreeRenderer.cpp | 10 +- .../src/RenderableBoxEntityItem.cpp | 76 ------ .../src/RenderableBoxEntityItem.h | 34 --- .../src/RenderableShapeEntityItem.cpp | 113 +++++++++ .../src/RenderableShapeEntityItem.h | 36 +++ .../src/RenderableSphereEntityItem.cpp | 80 ------ .../src/RenderableSphereEntityItem.h | 35 --- libraries/entities/src/BoxEntityItem.cpp | 103 -------- libraries/entities/src/BoxEntityItem.h | 64 ----- .../entities/src/EntityItemProperties.cpp | 15 +- libraries/entities/src/EntityItemProperties.h | 4 +- libraries/entities/src/EntityPropertyFlags.h | 2 + libraries/entities/src/EntityTypes.cpp | 10 +- libraries/entities/src/EntityTypes.h | 18 +- libraries/entities/src/ShapeEntityItem.cpp | 230 ++++++++++++++++++ libraries/entities/src/ShapeEntityItem.h | 103 ++++++++ libraries/entities/src/SphereEntityItem.cpp | 132 ---------- libraries/entities/src/SphereEntityItem.h | 70 ------ libraries/render-utils/src/GeometryCache.cpp | 41 ++-- libraries/render-utils/src/GeometryCache.h | 7 + .../developer/tests/entityEditStressTest.js | 164 +------------ scripts/developer/tests/entitySpawnTool.js | 184 ++++++++++++++ scripts/developer/tests/primitivesTest.js | 24 ++ scripts/system/html/entityProperties.html | 51 +++- tests/entities/src/main.cpp | 4 +- 25 files changed, 819 insertions(+), 791 deletions(-) delete mode 100644 libraries/entities-renderer/src/RenderableBoxEntityItem.cpp delete mode 100644 libraries/entities-renderer/src/RenderableBoxEntityItem.h create mode 100644 libraries/entities-renderer/src/RenderableShapeEntityItem.cpp create mode 100644 libraries/entities-renderer/src/RenderableShapeEntityItem.h delete mode 100644 libraries/entities-renderer/src/RenderableSphereEntityItem.cpp delete mode 100644 libraries/entities-renderer/src/RenderableSphereEntityItem.h delete mode 100644 libraries/entities/src/BoxEntityItem.cpp delete mode 100644 libraries/entities/src/BoxEntityItem.h create mode 100644 libraries/entities/src/ShapeEntityItem.cpp create mode 100644 libraries/entities/src/ShapeEntityItem.h delete mode 100644 libraries/entities/src/SphereEntityItem.cpp delete mode 100644 libraries/entities/src/SphereEntityItem.h create mode 100644 scripts/developer/tests/entitySpawnTool.js create mode 100644 scripts/developer/tests/primitivesTest.js diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index bec2fa9b8d..56f6438e70 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -29,17 +29,16 @@ #include "RenderableEntityItem.h" -#include "RenderableBoxEntityItem.h" #include "RenderableLightEntityItem.h" #include "RenderableModelEntityItem.h" #include "RenderableParticleEffectEntityItem.h" -#include "RenderableSphereEntityItem.h" #include "RenderableTextEntityItem.h" #include "RenderableWebEntityItem.h" #include "RenderableZoneEntityItem.h" #include "RenderableLineEntityItem.h" #include "RenderablePolyVoxEntityItem.h" #include "RenderablePolyLineEntityItem.h" +#include "RenderableShapeEntityItem.h" #include "EntitiesRendererLogging.h" #include "AddressManager.h" #include @@ -56,8 +55,6 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf _dontDoPrecisionPicking(false) { REGISTER_ENTITY_TYPE_WITH_FACTORY(Model, RenderableModelEntityItem::factory) - REGISTER_ENTITY_TYPE_WITH_FACTORY(Box, RenderableBoxEntityItem::factory) - REGISTER_ENTITY_TYPE_WITH_FACTORY(Sphere, RenderableSphereEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Light, RenderableLightEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Text, RenderableTextEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, RenderableWebEntityItem::factory) @@ -66,7 +63,10 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf REGISTER_ENTITY_TYPE_WITH_FACTORY(Line, RenderableLineEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(PolyVox, RenderablePolyVoxEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(PolyLine, RenderablePolyLineEntityItem::factory) - + REGISTER_ENTITY_TYPE_WITH_FACTORY(Shape, RenderableShapeEntityItem::factory) + REGISTER_ENTITY_TYPE_WITH_FACTORY(Box, RenderableShapeEntityItem::boxFactory) + REGISTER_ENTITY_TYPE_WITH_FACTORY(Sphere, RenderableShapeEntityItem::sphereFactory) + _currentHoverOverEntityID = UNKNOWN_ENTITY_ID; _currentClickingOnEntityID = UNKNOWN_ENTITY_ID; } diff --git a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp b/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp deleted file mode 100644 index e392450c08..0000000000 --- a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp +++ /dev/null @@ -1,76 +0,0 @@ -// -// RenderableBoxEntityItem.cpp -// libraries/entities-renderer/src/ -// -// Created by Brad Hefta-Gaub on 8/6/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "RenderableBoxEntityItem.h" - -#include - -#include - -#include -#include -#include - -#include -#include - -EntityItemPointer RenderableBoxEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { - EntityItemPointer entity{ new RenderableBoxEntityItem(entityID) }; - entity->setProperties(properties); - return entity; -} - -void RenderableBoxEntityItem::setUserData(const QString& value) { - if (value != getUserData()) { - BoxEntityItem::setUserData(value); - if (_procedural) { - _procedural->parse(value); - } - } -} - -void RenderableBoxEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderableBoxEntityItem::render"); - Q_ASSERT(getType() == EntityTypes::Box); - Q_ASSERT(args->_batch); - - if (!_procedural) { - _procedural.reset(new Procedural(this->getUserData())); - _procedural->_vertexSource = simple_vert; - _procedural->_fragmentSource = simple_frag; - _procedural->_state->setCullMode(gpu::State::CULL_NONE); - _procedural->_state->setDepthTest(true, true, gpu::LESS_EQUAL); - _procedural->_state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - } - - gpu::Batch& batch = *args->_batch; - glm::vec4 cubeColor(toGlm(getXColor()), getLocalRenderAlpha()); - - bool success; - auto transToCenter = getTransformToCenter(success); - if (!success) { - return; - } - - batch.setModelTransform(transToCenter); // we want to include the scale as well - if (_procedural->ready()) { - _procedural->prepare(batch, getPosition(), getDimensions()); - auto color = _procedural->getColor(cubeColor); - batch._glColor4f(color.r, color.g, color.b, color.a); - DependencyManager::get()->renderCube(batch); - } else { - DependencyManager::get()->renderSolidCubeInstance(batch, cubeColor); - } - static const auto triCount = DependencyManager::get()->getCubeTriangleCount(); - args->_details._trianglesRendered += (int)triCount; -} diff --git a/libraries/entities-renderer/src/RenderableBoxEntityItem.h b/libraries/entities-renderer/src/RenderableBoxEntityItem.h deleted file mode 100644 index 67f881dbd8..0000000000 --- a/libraries/entities-renderer/src/RenderableBoxEntityItem.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// RenderableBoxEntityItem.h -// libraries/entities-renderer/src/ -// -// Created by Brad Hefta-Gaub on 8/6/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_RenderableBoxEntityItem_h -#define hifi_RenderableBoxEntityItem_h - -#include -#include - -#include "RenderableEntityItem.h" - -class RenderableBoxEntityItem : public BoxEntityItem { -public: - static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); - RenderableBoxEntityItem(const EntityItemID& entityItemID) : BoxEntityItem(entityItemID) { } - - virtual void render(RenderArgs* args) override; - virtual void setUserData(const QString& value) override; - - SIMPLE_RENDERABLE() -private: - QSharedPointer _procedural; -}; - - -#endif // hifi_RenderableBoxEntityItem_h diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp new file mode 100644 index 0000000000..7d30b7a47c --- /dev/null +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -0,0 +1,113 @@ +// +// Created by Bradley Austin Davis on 2016/05/09 +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "RenderableShapeEntityItem.h" + +#include + +#include + +#include +#include +#include + +#include +#include + +// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 +// is a half unit sphere. However, the geometry cache renders a UNIT sphere, so we need to scale down. +static const float SPHERE_ENTITY_SCALE = 0.5f; + +static GeometryCache::Shape MAPPING[entity::NUM_SHAPES] = { + GeometryCache::Triangle, + GeometryCache::Quad, + GeometryCache::Circle, + GeometryCache::Cube, + GeometryCache::Sphere, + GeometryCache::Tetrahedron, + GeometryCache::Octahetron, + GeometryCache::Dodecahedron, + GeometryCache::Icosahedron, + GeometryCache::Torus, + GeometryCache::Cone, + GeometryCache::Cylinder, +}; + + +RenderableShapeEntityItem::Pointer RenderableShapeEntityItem::baseFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { + Pointer entity = std::make_shared(entityID); + entity->setProperties(properties); + return entity; +} + +EntityItemPointer RenderableShapeEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { + return baseFactory(entityID, properties); +} + +EntityItemPointer RenderableShapeEntityItem::boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { + auto result = baseFactory(entityID, properties); + result->setShape(entity::Cube); + return result; +} + +EntityItemPointer RenderableShapeEntityItem::sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { + auto result = baseFactory(entityID, properties); + result->setShape(entity::Sphere); + return result; +} + +void RenderableShapeEntityItem::setUserData(const QString& value) { + if (value != getUserData()) { + ShapeEntityItem::setUserData(value); + if (_procedural) { + _procedural->parse(value); + } + } +} + +void RenderableShapeEntityItem::render(RenderArgs* args) { + PerformanceTimer perfTimer("RenderableShapeEntityItem::render"); + //Q_ASSERT(getType() == EntityTypes::Shape); + Q_ASSERT(args->_batch); + + if (!_procedural) { + _procedural.reset(new Procedural(getUserData())); + _procedural->_vertexSource = simple_vert; + _procedural->_fragmentSource = simple_frag; + _procedural->_state->setCullMode(gpu::State::CULL_NONE); + _procedural->_state->setDepthTest(true, true, gpu::LESS_EQUAL); + _procedural->_state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + } + + gpu::Batch& batch = *args->_batch; + glm::vec4 color(toGlm(getXColor()), getLocalRenderAlpha()); + bool success; + Transform modelTransform = getTransformToCenter(success); + if (!success) { + return; + } + if (_shape != entity::Cube) { + modelTransform.postScale(SPHERE_ENTITY_SCALE); + } + batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation + if (_procedural->ready()) { + _procedural->prepare(batch, getPosition(), getDimensions()); + auto outColor = _procedural->getColor(color); + batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); + DependencyManager::get()->renderShape(batch, MAPPING[_shape]); + } else { + // FIXME, support instanced multi-shape rendering using multidraw indirect + DependencyManager::get()->renderSolidShapeInstance(batch, MAPPING[_shape], color); + } + + + static const auto triCount = DependencyManager::get()->getShapeTriangleCount(MAPPING[_shape]); + args->_details._trianglesRendered += (int)triCount; +} diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h new file mode 100644 index 0000000000..b18370b13c --- /dev/null +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -0,0 +1,36 @@ +// +// Created by Bradley Austin Davis on 2016/05/09 +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_RenderableShapeEntityItem_h +#define hifi_RenderableShapeEntityItem_h + +#include +#include + +#include "RenderableEntityItem.h" + +class RenderableShapeEntityItem : public ShapeEntityItem { + using Pointer = std::shared_ptr; + static Pointer baseFactory(const EntityItemID& entityID, const EntityItemProperties& properties); +public: + static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); + static EntityItemPointer boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties); + static EntityItemPointer sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties); + RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID) {} + + void render(RenderArgs* args) override; + void setUserData(const QString& value) override; + + SIMPLE_RENDERABLE(); + +private: + QSharedPointer _procedural; +}; + + +#endif // hifi_RenderableShapeEntityItem_h diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp deleted file mode 100644 index c3437b0e4a..0000000000 --- a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp +++ /dev/null @@ -1,80 +0,0 @@ -// -// RenderableSphereEntityItem.cpp -// interface/src -// -// Created by Brad Hefta-Gaub on 8/6/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "RenderableSphereEntityItem.h" - -#include - -#include - -#include -#include -#include - -#include -#include - -// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 -// is a half unit sphere. However, the geometry cache renders a UNIT sphere, so we need to scale down. -static const float SPHERE_ENTITY_SCALE = 0.5f; - - -EntityItemPointer RenderableSphereEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { - EntityItemPointer entity{ new RenderableSphereEntityItem(entityID) }; - entity->setProperties(properties); - return entity; -} - -void RenderableSphereEntityItem::setUserData(const QString& value) { - if (value != getUserData()) { - SphereEntityItem::setUserData(value); - if (_procedural) { - _procedural->parse(value); - } - } -} - -void RenderableSphereEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderableSphereEntityItem::render"); - Q_ASSERT(getType() == EntityTypes::Sphere); - Q_ASSERT(args->_batch); - - if (!_procedural) { - _procedural.reset(new Procedural(getUserData())); - _procedural->_vertexSource = simple_vert; - _procedural->_fragmentSource = simple_frag; - _procedural->_state->setCullMode(gpu::State::CULL_NONE); - _procedural->_state->setDepthTest(true, true, gpu::LESS_EQUAL); - _procedural->_state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - } - - gpu::Batch& batch = *args->_batch; - glm::vec4 sphereColor(toGlm(getXColor()), getLocalRenderAlpha()); - bool success; - Transform modelTransform = getTransformToCenter(success); - if (!success) { - return; - } - modelTransform.postScale(SPHERE_ENTITY_SCALE); - batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation - if (_procedural->ready()) { - _procedural->prepare(batch, getPosition(), getDimensions()); - auto color = _procedural->getColor(sphereColor); - batch._glColor4f(color.r, color.g, color.b, color.a); - DependencyManager::get()->renderSphere(batch); - } else { - DependencyManager::get()->renderSolidSphereInstance(batch, sphereColor); - } - static const auto triCount = DependencyManager::get()->getSphereTriangleCount(); - args->_details._trianglesRendered += (int)triCount; -} diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.h b/libraries/entities-renderer/src/RenderableSphereEntityItem.h deleted file mode 100644 index 5efe49854a..0000000000 --- a/libraries/entities-renderer/src/RenderableSphereEntityItem.h +++ /dev/null @@ -1,35 +0,0 @@ -// -// RenderableSphereEntityItem.h -// interface/src/entities -// -// Created by Brad Hefta-Gaub on 8/6/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_RenderableSphereEntityItem_h -#define hifi_RenderableSphereEntityItem_h - -#include -#include - -#include "RenderableEntityItem.h" - -class RenderableSphereEntityItem : public SphereEntityItem { -public: - static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); - RenderableSphereEntityItem(const EntityItemID& entityItemID) : SphereEntityItem(entityItemID) { } - - virtual void render(RenderArgs* args) override; - virtual void setUserData(const QString& value) override; - - SIMPLE_RENDERABLE(); - -private: - QSharedPointer _procedural; -}; - - -#endif // hifi_RenderableSphereEntityItem_h diff --git a/libraries/entities/src/BoxEntityItem.cpp b/libraries/entities/src/BoxEntityItem.cpp deleted file mode 100644 index bf02d383ab..0000000000 --- a/libraries/entities/src/BoxEntityItem.cpp +++ /dev/null @@ -1,103 +0,0 @@ -// -// BoxEntityItem.cpp -// libraries/entities/src -// -// Created by Brad Hefta-Gaub on 12/4/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#include - -#include - -#include "BoxEntityItem.h" -#include "EntitiesLogging.h" -#include "EntityItemProperties.h" -#include "EntityTree.h" -#include "EntityTreeElement.h" - -EntityItemPointer BoxEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { - EntityItemPointer entity { new BoxEntityItem(entityID) }; - entity->setProperties(properties); - return entity; -} - -BoxEntityItem::BoxEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { - _type = EntityTypes::Box; -} - -EntityItemProperties BoxEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { - EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class - - properties._color = getXColor(); - properties._colorChanged = false; - - return properties; -} - -bool BoxEntityItem::setProperties(const EntityItemProperties& properties) { - bool somethingChanged = false; - somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class - - SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); - - if (somethingChanged) { - bool wantDebug = false; - if (wantDebug) { - uint64_t now = usecTimestampNow(); - int elapsed = now - getLastEdited(); - qCDebug(entities) << "BoxEntityItem::setProperties() AFTER update... edited AGO=" << elapsed << - "now=" << now << " getLastEdited()=" << getLastEdited(); - } - setLastEdited(properties._lastEdited); - } - return somethingChanged; -} - -int BoxEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, - ReadBitstreamToTreeParams& args, - EntityPropertyFlags& propertyFlags, bool overwriteLocalData, - bool& somethingChanged) { - - int bytesRead = 0; - const unsigned char* dataAt = data; - - READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); - - return bytesRead; -} - - -// TODO: eventually only include properties changed since the params.lastViewFrustumSent time -EntityPropertyFlags BoxEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { - EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); - requestedProperties += PROP_COLOR; - return requestedProperties; -} - -void BoxEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, - EntityPropertyFlags& requestedProperties, - EntityPropertyFlags& propertyFlags, - EntityPropertyFlags& propertiesDidntFit, - int& propertyCount, - OctreeElement::AppendState& appendState) const { - - bool successPropertyFits = true; - - APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); -} - -void BoxEntityItem::debugDump() const { - quint64 now = usecTimestampNow(); - qCDebug(entities) << " BOX EntityItem id:" << getEntityItemID() << "---------------------------------------------"; - qCDebug(entities) << " color:" << _color[0] << "," << _color[1] << "," << _color[2]; - qCDebug(entities) << " position:" << debugTreeVector(getPosition()); - qCDebug(entities) << " dimensions:" << debugTreeVector(getDimensions()); - qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); -} - diff --git a/libraries/entities/src/BoxEntityItem.h b/libraries/entities/src/BoxEntityItem.h deleted file mode 100644 index 6196346b9a..0000000000 --- a/libraries/entities/src/BoxEntityItem.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// BoxEntityItem.h -// libraries/entities/src -// -// Created by Brad Hefta-Gaub on 12/4/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_BoxEntityItem_h -#define hifi_BoxEntityItem_h - -#include "EntityItem.h" - -class BoxEntityItem : public EntityItem { -public: - static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); - - BoxEntityItem(const EntityItemID& entityItemID); - - ALLOW_INSTANTIATION // This class can be instantiated - - // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; - virtual bool setProperties(const EntityItemProperties& properties); - - // TODO: eventually only include properties changed since the params.lastViewFrustumSent time - virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; - - virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, - EntityPropertyFlags& requestedProperties, - EntityPropertyFlags& propertyFlags, - EntityPropertyFlags& propertiesDidntFit, - int& propertyCount, - OctreeElement::AppendState& appendState) const; - - virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, - ReadBitstreamToTreeParams& args, - EntityPropertyFlags& propertyFlags, bool overwriteLocalData, - bool& somethingChanged); - - const rgbColor& getColor() const { return _color; } - xColor getXColor() const { xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; } - - void setColor(const rgbColor& value) { memcpy(_color, value, sizeof(_color)); } - void setColor(const xColor& value) { - _color[RED_INDEX] = value.red; - _color[GREEN_INDEX] = value.green; - _color[BLUE_INDEX] = value.blue; - } - - virtual ShapeType getShapeType() const { return SHAPE_TYPE_BOX; } - virtual bool shouldBePhysical() const { return !isDead(); } - - virtual void debugDump() const; - -protected: - rgbColor _color; -}; - -#endif // hifi_BoxEntityItem_h diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 99285b4986..f273507d0d 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -689,6 +689,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointRotations, qVectorQuat, setJointRotations); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); + COPY_PROPERTY_FROM_QSCRIPTVALUE(shape, QString, setShape); COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed); COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed); @@ -846,6 +847,8 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector); ADD_PROPERTY_TO_MAP(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector); + ADD_PROPERTY_TO_MAP(PROP_SHAPE, Shape, shape, QString); + ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_URL, Animation, animation, URL, url); ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_FPS, Animation, animation, FPS, fps); ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_FRAME_INDEX, Animation, animation, CurrentFrame, currentFrame); @@ -1141,7 +1144,9 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_STROKE_WIDTHS, properties.getStrokeWidths()); APPEND_ENTITY_PROPERTY(PROP_TEXTURES, properties.getTextures()); } - + if (properties.getType() == EntityTypes::Shape) { + APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); + } APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); @@ -1429,6 +1434,9 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_WIDTHS, QVector, setStrokeWidths); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_TEXTURES, QString, setTextures); } + if (properties.getType() == EntityTypes::Shape) { + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); + } READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); @@ -1915,6 +1923,7 @@ QList EntityItemProperties::listChangedProperties() { if (queryAACubeChanged()) { out += "queryAACube"; } + if (clientOnlyChanged()) { out += "clientOnly"; } @@ -1929,6 +1938,10 @@ QList EntityItemProperties::listChangedProperties() { out += "ghostingAllowed"; } + if (shapeChanged()) { + out += "shape"; + } + getAnimation().listChangedProperties(out); getKeyLight().listChangedProperties(out); getSkybox().listChangedProperties(out); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 59749cfef5..6018ba793f 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -50,7 +50,7 @@ const quint64 UNKNOWN_CREATED_TIME = 0; /// A collection of properties of an entity item used in the scripting API. Translates between the actual properties of an /// entity and a JavaScript style hash/QScriptValue storing a set of properties. Used in scripting to set/get the complete /// set of entity item properties via JavaScript hashes/QScriptValues -/// all units for position, dimensions, etc are in meter units +/// all units for SI units (meter, second, radian, etc) class EntityItemProperties { friend class EntityItem; // TODO: consider removing this friend relationship and use public methods friend class ModelEntityItem; // TODO: consider removing this friend relationship and use public methods @@ -64,6 +64,7 @@ class EntityItemProperties { friend class LineEntityItem; // TODO: consider removing this friend relationship and use public methods friend class PolyVoxEntityItem; // TODO: consider removing this friend relationship and use public methods friend class PolyLineEntityItem; // TODO: consider removing this friend relationship and use public methods + friend class ShapeEntityItem; // TODO: consider removing this friend relationship and use public methods public: EntityItemProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()); virtual ~EntityItemProperties() = default; @@ -195,6 +196,7 @@ public: DEFINE_PROPERTY_REF(PROP_PARENT_ID, ParentID, parentID, QUuid, UNKNOWN_ENTITY_ID); DEFINE_PROPERTY_REF(PROP_PARENT_JOINT_INDEX, ParentJointIndex, parentJointIndex, quint16, -1); DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube()); + DEFINE_PROPERTY_REF(PROP_SHAPE, Shape, shape, QString, "Sphere"); // these are used when bouncing location data into and out of scripts DEFINE_PROPERTY_REF(PROP_LOCAL_POSITION, LocalPosition, localPosition, glmVec3, ENTITY_ITEM_ZERO_VEC3); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 20c451eaae..36bb37c8f3 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -175,6 +175,8 @@ enum EntityPropertyList { PROP_CLIENT_ONLY, // doesn't go over wire PROP_OWNING_AVATAR_ID, // doesn't go over wire + PROP_SHAPE, + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityTypes.cpp b/libraries/entities/src/EntityTypes.cpp index 52c2242629..7b1133c2aa 100644 --- a/libraries/entities/src/EntityTypes.cpp +++ b/libraries/entities/src/EntityTypes.cpp @@ -18,17 +18,16 @@ #include "EntityItemProperties.h" #include "EntityTypes.h" -#include "BoxEntityItem.h" #include "LightEntityItem.h" #include "ModelEntityItem.h" #include "ParticleEffectEntityItem.h" -#include "SphereEntityItem.h" #include "TextEntityItem.h" #include "WebEntityItem.h" #include "ZoneEntityItem.h" #include "LineEntityItem.h" #include "PolyVoxEntityItem.h" #include "PolyLineEntityItem.h" +#include "ShapeEntityItem.h" QMap EntityTypes::_typeToNameMap; QMap EntityTypes::_nameToTypeMap; @@ -39,16 +38,17 @@ const QString ENTITY_TYPE_NAME_UNKNOWN = "Unknown"; // Register Entity the default implementations of entity types here... REGISTER_ENTITY_TYPE(Model) -REGISTER_ENTITY_TYPE(Box) REGISTER_ENTITY_TYPE(Web) -REGISTER_ENTITY_TYPE(Sphere) REGISTER_ENTITY_TYPE(Light) REGISTER_ENTITY_TYPE(Text) REGISTER_ENTITY_TYPE(ParticleEffect) REGISTER_ENTITY_TYPE(Zone) REGISTER_ENTITY_TYPE(Line) REGISTER_ENTITY_TYPE(PolyVox) -REGISTER_ENTITY_TYPE(PolyLine); +REGISTER_ENTITY_TYPE(PolyLine) +REGISTER_ENTITY_TYPE(Shape) +REGISTER_ENTITY_TYPE_WITH_FACTORY(Box, ShapeEntityItem::boxFactory) +REGISTER_ENTITY_TYPE_WITH_FACTORY(Sphere, ShapeEntityItem::sphereFactory) const QString& EntityTypes::getEntityTypeName(EntityType entityType) { QMap::iterator matchedTypeName = _typeToNameMap.find(entityType); diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index 3536327d18..be3be03713 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -48,7 +48,8 @@ public: Line, PolyVox, PolyLine, - LAST = PolyLine + Shape, + LAST = Shape } EntityType; static const QString& getEntityTypeName(EntityType entityType); @@ -72,6 +73,15 @@ private: #define REGISTER_ENTITY_TYPE(x) static bool x##Registration = \ EntityTypes::registerEntityType(EntityTypes::x, #x, x##EntityItem::factory); + +struct EntityRegistrationChecker { + EntityRegistrationChecker(bool result, const char* debugMessage) { + if (!result) { + qDebug() << debugMessage; + } + } +}; + /// Macro for registering entity types with an overloaded factory. Like using the REGISTER_ENTITY_TYPE macro: Make sure to add /// an element to the EntityType enum with your name. But unlike REGISTER_ENTITY_TYPE, your class can be named anything /// so long as you provide a static method passed to the macro, that takes an EnityItemID, and EntityItemProperties and @@ -79,9 +89,9 @@ private: // static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); #define REGISTER_ENTITY_TYPE_WITH_FACTORY(x,y) static bool x##Registration = \ EntityTypes::registerEntityType(EntityTypes::x, #x, y); \ - if (!x##Registration) { \ - qDebug() << "UNEXPECTED: REGISTER_ENTITY_TYPE_WITH_FACTORY(" #x "," #y ") FAILED.!"; \ - } + EntityRegistrationChecker x##RegistrationChecker( \ + x##Registration, \ + "UNEXPECTED: REGISTER_ENTITY_TYPE_WITH_FACTORY(" #x "," #y ") FAILED.!"); #endif // hifi_EntityTypes_h diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp new file mode 100644 index 0000000000..a24c7e1df5 --- /dev/null +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -0,0 +1,230 @@ +// +// Created by Bradley Austin Davis on 2016/05/09 +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include + +#include + +#include + +#include "EntitiesLogging.h" +#include "EntityItemProperties.h" +#include "EntityTree.h" +#include "EntityTreeElement.h" +#include "ShapeEntityItem.h" + +namespace entity { + static const std::vector shapeStrings { { + "Triangle", + "Quad", + "Circle", + "Cube", + "Sphere", + "Tetrahedron", + "Octahetron", + "Dodecahedron", + "Icosahedron", + "Torus", + "Cone", + "Cylinder" + } }; + + Shape shapeFromString(const ::QString& shapeString) { + for (size_t i = 0; i < shapeStrings.size(); ++i) { + if (shapeString.toLower() == shapeStrings[i].toLower()) { + return static_cast(i); + } + } + return Shape::Sphere; + } + + ::QString stringFromShape(Shape shape) { + return shapeStrings[shape]; + } +} + +ShapeEntityItem::Pointer ShapeEntityItem::baseFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { + Pointer entity { new ShapeEntityItem(entityID) }; + entity->setProperties(properties); + return entity; +} + +EntityItemPointer ShapeEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { + return baseFactory(entityID, properties); +} + +EntityItemPointer ShapeEntityItem::boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { + auto result = baseFactory(entityID, properties); + result->setShape(entity::Shape::Cube); + return result; +} + +EntityItemPointer ShapeEntityItem::sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { + auto result = baseFactory(entityID, properties); + result->setShape(entity::Shape::Sphere); + return result; +} + +// our non-pure virtual subclass for now... +ShapeEntityItem::ShapeEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { + _type = EntityTypes::Shape; + _volumeMultiplier *= PI / 6.0f; +} + +EntityItemProperties ShapeEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class + properties.setColor(getXColor()); + properties.setShape(entity::stringFromShape(getShape())); + return properties; +} + +void ShapeEntityItem::setShape(const entity::Shape& shape) { + _shape = shape; + switch (_shape) { + case entity::Shape::Cube: + _type = EntityTypes::Box; + break; + case entity::Shape::Sphere: + _type = EntityTypes::Sphere; + break; + default: + _type = EntityTypes::Shape; + break; + } +} + +bool ShapeEntityItem::setProperties(const EntityItemProperties& properties) { + bool somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class + + SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shape, setShape); + + if (somethingChanged) { + bool wantDebug = false; + if (wantDebug) { + uint64_t now = usecTimestampNow(); + int elapsed = now - getLastEdited(); + qCDebug(entities) << "ShapeEntityItem::setProperties() AFTER update... edited AGO=" << elapsed << + "now=" << now << " getLastEdited()=" << getLastEdited(); + } + setLastEdited(properties.getLastEdited()); + } + return somethingChanged; +} + +int ShapeEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData, + bool& somethingChanged) { + + int bytesRead = 0; + const unsigned char* dataAt = data; + + READ_ENTITY_PROPERTY(PROP_SHAPE, QString, setShape); + READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); + READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); + + return bytesRead; +} + + +// TODO: eventually only include properties changed since the params.lastViewFrustumSent time +EntityPropertyFlags ShapeEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { + EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); + requestedProperties += PROP_SHAPE; + requestedProperties += PROP_COLOR; + requestedProperties += PROP_ALPHA; + return requestedProperties; +} + +void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const { + + bool successPropertyFits = true; + APPEND_ENTITY_PROPERTY(PROP_SHAPE, entity::stringFromShape(getShape())); + APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); + APPEND_ENTITY_PROPERTY(PROP_COLOR, getAlpha()); +} + +// This value specifes how the shape should be treated by physics calculations. +// For now, all polys will act as spheres +ShapeType ShapeEntityItem::getShapeType() const { + return SHAPE_TYPE_ELLIPSOID; +} + +void ShapeEntityItem::setColor(const rgbColor& value) { + memcpy(_color, value, sizeof(rgbColor)); +} + +xColor ShapeEntityItem::getXColor() const { + return xColor { _color[0], _color[1], _color[2] }; +} + +void ShapeEntityItem::setColor(const xColor& value) { + setColor(rgbColor { value.red, value.green, value.blue }); +} + +QColor ShapeEntityItem::getQColor() const { + auto& color = getColor(); + return QColor(color[0], color[1], color[2], (int)(getAlpha() * 255)); +} + +void ShapeEntityItem::setColor(const QColor& value) { + setColor(rgbColor { (uint8_t)value.red(), (uint8_t)value.green(), (uint8_t)value.blue() }); + setAlpha(value.alpha()); +} + +bool ShapeEntityItem::supportsDetailedRayIntersection() const { + return _shape == entity::Sphere; +} + +bool ShapeEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + bool& keepSearching, OctreeElementPointer& element, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, + void** intersectedObject, bool precisionPicking) const { + // determine the ray in the frame of the entity transformed from a unit sphere + glm::mat4 entityToWorldMatrix = getEntityToWorldMatrix(); + glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); + glm::vec3 entityFrameOrigin = glm::vec3(worldToEntityMatrix * glm::vec4(origin, 1.0f)); + glm::vec3 entityFrameDirection = glm::normalize(glm::vec3(worldToEntityMatrix * glm::vec4(direction, 0.0f))); + + float localDistance; + // NOTE: unit sphere has center of 0,0,0 and radius of 0.5 + if (findRaySphereIntersection(entityFrameOrigin, entityFrameDirection, glm::vec3(0.0f), 0.5f, localDistance)) { + // determine where on the unit sphere the hit point occured + glm::vec3 entityFrameHitAt = entityFrameOrigin + (entityFrameDirection * localDistance); + // then translate back to work coordinates + glm::vec3 hitAt = glm::vec3(entityToWorldMatrix * glm::vec4(entityFrameHitAt, 1.0f)); + distance = glm::distance(origin, hitAt); + bool success; + surfaceNormal = glm::normalize(hitAt - getCenterPosition(success)); + if (!success) { + return false; + } + return true; + } + return false; +} + +void ShapeEntityItem::debugDump() const { + quint64 now = usecTimestampNow(); + qCDebug(entities) << "SHAPE EntityItem id:" << getEntityItemID() << "---------------------------------------------"; + qCDebug(entities) << " shape:" << stringFromShape(_shape); + qCDebug(entities) << " color:" << _color[0] << "," << _color[1] << "," << _color[2]; + qCDebug(entities) << " position:" << debugTreeVector(getPosition()); + qCDebug(entities) << " dimensions:" << debugTreeVector(getDimensions()); + qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); +} + diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h new file mode 100644 index 0000000000..f3cb2abd66 --- /dev/null +++ b/libraries/entities/src/ShapeEntityItem.h @@ -0,0 +1,103 @@ +// +// Created by Bradley Austin Davis on 2016/05/09 +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ShapeEntityItem_h +#define hifi_ShapeEntityItem_h + +#include "EntityItem.h" + +namespace entity { + enum Shape { + Triangle, + Quad, + Circle, + Cube, + Sphere, + Tetrahedron, + Octahetron, + Dodecahedron, + Icosahedron, + Torus, + Cone, + Cylinder, + NUM_SHAPES, + }; + + Shape shapeFromString(const ::QString& shapeString); + ::QString stringFromShape(Shape shape); +} + + +class ShapeEntityItem : public EntityItem { + using Pointer = std::shared_ptr; + static Pointer baseFactory(const EntityItemID& entityID, const EntityItemProperties& properties); +public: + static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); + static EntityItemPointer sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties); + static EntityItemPointer boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties); + + ShapeEntityItem(const EntityItemID& entityItemID); + + void pureVirtualFunctionPlaceHolder() override { }; + // Triggers warnings on OSX + //ALLOW_INSTANTIATION + + // methods for getting/setting all properties of an entity + EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; + bool setProperties(const EntityItemProperties& properties) override; + + EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; + + void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const override; + + int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData, + bool& somethingChanged) override; + + entity::Shape getShape() const { return _shape; } + void setShape(const entity::Shape& shape); + void setShape(const QString& shape) { setShape(entity::shapeFromString(shape)); } + + float getAlpha() const { return _alpha; }; + void setAlpha(float alpha) { _alpha = alpha; } + + const rgbColor& getColor() const { return _color; } + void setColor(const rgbColor& value); + + xColor getXColor() const; + void setColor(const xColor& value); + + QColor getQColor() const; + void setColor(const QColor& value); + + ShapeType getShapeType() const override; + bool shouldBePhysical() const override { return !isDead(); } + + bool supportsDetailedRayIntersection() const override; + bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + bool& keepSearching, OctreeElementPointer& element, float& distance, + BoxFace& face, glm::vec3& surfaceNormal, + void** intersectedObject, bool precisionPicking) const override; + + void debugDump() const override; + +protected: + + float _alpha { 1 }; + rgbColor _color; + entity::Shape _shape { entity::Shape::Sphere }; +}; + +#endif // hifi_ShapeEntityItem_h diff --git a/libraries/entities/src/SphereEntityItem.cpp b/libraries/entities/src/SphereEntityItem.cpp deleted file mode 100644 index 7ad7b39f20..0000000000 --- a/libraries/entities/src/SphereEntityItem.cpp +++ /dev/null @@ -1,132 +0,0 @@ -// -// SphereEntityItem.cpp -// libraries/entities/src -// -// Created by Brad Hefta-Gaub on 12/4/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#include - -#include - -#include -#include - -#include "EntitiesLogging.h" -#include "EntityItemProperties.h" -#include "EntityTree.h" -#include "EntityTreeElement.h" -#include "SphereEntityItem.h" - -EntityItemPointer SphereEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { - EntityItemPointer entity { new SphereEntityItem(entityID) }; - entity->setProperties(properties); - return entity; -} - -// our non-pure virtual subclass for now... -SphereEntityItem::SphereEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { - _type = EntityTypes::Sphere; - _volumeMultiplier *= PI / 6.0f; -} - -EntityItemProperties SphereEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { - EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class - properties.setColor(getXColor()); - return properties; -} - -bool SphereEntityItem::setProperties(const EntityItemProperties& properties) { - bool somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class - - SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); - - if (somethingChanged) { - bool wantDebug = false; - if (wantDebug) { - uint64_t now = usecTimestampNow(); - int elapsed = now - getLastEdited(); - qCDebug(entities) << "SphereEntityItem::setProperties() AFTER update... edited AGO=" << elapsed << - "now=" << now << " getLastEdited()=" << getLastEdited(); - } - setLastEdited(properties.getLastEdited()); - } - return somethingChanged; -} - -int SphereEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, - ReadBitstreamToTreeParams& args, - EntityPropertyFlags& propertyFlags, bool overwriteLocalData, - bool& somethingChanged) { - - int bytesRead = 0; - const unsigned char* dataAt = data; - - READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); - - return bytesRead; -} - - -// TODO: eventually only include properties changed since the params.lastViewFrustumSent time -EntityPropertyFlags SphereEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { - EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); - requestedProperties += PROP_COLOR; - return requestedProperties; -} - -void SphereEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, - EntityPropertyFlags& requestedProperties, - EntityPropertyFlags& propertyFlags, - EntityPropertyFlags& propertiesDidntFit, - int& propertyCount, - OctreeElement::AppendState& appendState) const { - - bool successPropertyFits = true; - APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); -} - -bool SphereEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - bool& keepSearching, OctreeElementPointer& element, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { - // determine the ray in the frame of the entity transformed from a unit sphere - glm::mat4 entityToWorldMatrix = getEntityToWorldMatrix(); - glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); - glm::vec3 entityFrameOrigin = glm::vec3(worldToEntityMatrix * glm::vec4(origin, 1.0f)); - glm::vec3 entityFrameDirection = glm::normalize(glm::vec3(worldToEntityMatrix * glm::vec4(direction, 0.0f))); - - float localDistance; - // NOTE: unit sphere has center of 0,0,0 and radius of 0.5 - if (findRaySphereIntersection(entityFrameOrigin, entityFrameDirection, glm::vec3(0.0f), 0.5f, localDistance)) { - // determine where on the unit sphere the hit point occured - glm::vec3 entityFrameHitAt = entityFrameOrigin + (entityFrameDirection * localDistance); - // then translate back to work coordinates - glm::vec3 hitAt = glm::vec3(entityToWorldMatrix * glm::vec4(entityFrameHitAt, 1.0f)); - distance = glm::distance(origin, hitAt); - bool success; - surfaceNormal = glm::normalize(hitAt - getCenterPosition(success)); - if (!success) { - return false; - } - return true; - } - return false; -} - - -void SphereEntityItem::debugDump() const { - quint64 now = usecTimestampNow(); - qCDebug(entities) << "SHPERE EntityItem id:" << getEntityItemID() << "---------------------------------------------"; - qCDebug(entities) << " color:" << _color[0] << "," << _color[1] << "," << _color[2]; - qCDebug(entities) << " position:" << debugTreeVector(getPosition()); - qCDebug(entities) << " dimensions:" << debugTreeVector(getDimensions()); - qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); -} - diff --git a/libraries/entities/src/SphereEntityItem.h b/libraries/entities/src/SphereEntityItem.h deleted file mode 100644 index fda5eab009..0000000000 --- a/libraries/entities/src/SphereEntityItem.h +++ /dev/null @@ -1,70 +0,0 @@ -// -// SphereEntityItem.h -// libraries/entities/src -// -// Created by Brad Hefta-Gaub on 12/4/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_SphereEntityItem_h -#define hifi_SphereEntityItem_h - -#include "EntityItem.h" - -class SphereEntityItem : public EntityItem { -public: - static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); - - SphereEntityItem(const EntityItemID& entityItemID); - - ALLOW_INSTANTIATION // This class can be instantiated - - // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; - virtual bool setProperties(const EntityItemProperties& properties); - - virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; - - virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, - EntityPropertyFlags& requestedProperties, - EntityPropertyFlags& propertyFlags, - EntityPropertyFlags& propertiesDidntFit, - int& propertyCount, - OctreeElement::AppendState& appendState) const; - - virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, - ReadBitstreamToTreeParams& args, - EntityPropertyFlags& propertyFlags, bool overwriteLocalData, - bool& somethingChanged); - - const rgbColor& getColor() const { return _color; } - xColor getXColor() const { xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; } - - void setColor(const rgbColor& value) { memcpy(_color, value, sizeof(_color)); } - void setColor(const xColor& value) { - _color[RED_INDEX] = value.red; - _color[GREEN_INDEX] = value.green; - _color[BLUE_INDEX] = value.blue; - } - - virtual ShapeType getShapeType() const { return SHAPE_TYPE_SPHERE; } - virtual bool shouldBePhysical() const { return !isDead(); } - - virtual bool supportsDetailedRayIntersection() const { return true; } - virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - bool& keepSearching, OctreeElementPointer& element, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const; - - virtual void debugDump() const; - -protected: - - rgbColor _color; -}; - -#endif // hifi_SphereEntityItem_h diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 15bf44744c..6852d17882 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -112,10 +112,12 @@ void GeometryCache::ShapeData::drawWireInstances(gpu::Batch& batch, size_t count } } +// The golden ratio +static const float PHI = 1.61803398874f; + const VertexVector& icosahedronVertices() { - static const float phi = (1.0f + sqrtf(5.0f)) / 2.0f; - static const float a = 0.5f; - static const float b = 1.0f / (2.0f * phi); + static const float a = 1; + static const float b = PHI / 2.0f; static const VertexVector vertices{ // vec3(0, b, -a), vec3(-b, a, 0), vec3(b, a, 0), // @@ -143,11 +145,10 @@ const VertexVector& icosahedronVertices() { } const VertexVector& tetrahedronVertices() { - static const float a = 1.0f / sqrtf(2.0f); - static const auto A = vec3(0, 1, a); - static const auto B = vec3(0, -1, a); - static const auto C = vec3(1, 0, -a); - static const auto D = vec3(-1, 0, -a); + static const auto A = vec3(1, 1, 1); + static const auto B = vec3(1, -1, -1); + static const auto C = vec3(-1, 1, -1); + static const auto D = vec3(-1, -1, 1); static const VertexVector vertices{ A, B, C, D, B, A, @@ -356,7 +357,7 @@ void GeometryCache::buildShapes() { for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { auto triangleVertexIndex = j; auto vertexIndex = triangleStartIndex + triangleVertexIndex; - vertices.push_back(glm::normalize(originalVertices[vertexIndex])); + vertices.push_back(originalVertices[vertexIndex]); vertices.push_back(faceNormal); } } @@ -437,7 +438,7 @@ void GeometryCache::buildShapes() { for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { auto triangleVertexIndex = j; auto vertexIndex = triangleStartIndex + triangleVertexIndex; - vertices.push_back(glm::normalize(originalVertices[vertexIndex])); + vertices.push_back(originalVertices[vertexIndex]); vertices.push_back(faceNormal); indices.push_back((uint16_t)(vertexIndex + startingIndex)); } @@ -1801,10 +1802,10 @@ uint32_t toCompactColor(const glm::vec4& color) { static const size_t INSTANCE_COLOR_BUFFER = 0; -void renderInstances(const std::string& name, gpu::Batch& batch, const glm::vec4& color, bool isWire, +void renderInstances(gpu::Batch& batch, const glm::vec4& color, bool isWire, const render::ShapePipelinePointer& pipeline, GeometryCache::Shape shape) { // Add pipeline to name - std::string instanceName = name + std::to_string(std::hash()(pipeline)); + std::string instanceName = (isWire ? "wire_shapes_" : "solid_shapes_") + std::to_string(shape) + "_" + std::to_string(std::hash()(pipeline)); // Add color to named buffer { @@ -1826,14 +1827,16 @@ void renderInstances(const std::string& name, gpu::Batch& batch, const glm::vec4 }); } +void GeometryCache::renderSolidShapeInstance(gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { + renderInstances(batch, color, false, pipeline, shape); +} + void GeometryCache::renderSolidSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { - static const std::string INSTANCE_NAME = __FUNCTION__; - renderInstances(INSTANCE_NAME, batch, color, false, pipeline, GeometryCache::Sphere); + renderInstances(batch, color, false, pipeline, GeometryCache::Sphere); } void GeometryCache::renderWireSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { - static const std::string INSTANCE_NAME = __FUNCTION__; - renderInstances(INSTANCE_NAME, batch, color, true, pipeline, GeometryCache::Sphere); + renderInstances(batch, color, true, pipeline, GeometryCache::Sphere); } // Enable this in a debug build to cause 'box' entities to iterate through all the @@ -1841,8 +1844,6 @@ void GeometryCache::renderWireSphereInstance(gpu::Batch& batch, const glm::vec4& //#define DEBUG_SHAPES void GeometryCache::renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { - static const std::string INSTANCE_NAME = __FUNCTION__; - #ifdef DEBUG_SHAPES static auto startTime = usecTimestampNow(); renderInstances(INSTANCE_NAME, batch, color, pipeline, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { @@ -1876,11 +1877,11 @@ void GeometryCache::renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& } }); #else - renderInstances(INSTANCE_NAME, batch, color, false, pipeline, GeometryCache::Cube); + renderInstances(batch, color, false, pipeline, GeometryCache::Cube); #endif } void GeometryCache::renderWireCubeInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { static const std::string INSTANCE_NAME = __FUNCTION__; - renderInstances(INSTANCE_NAME, batch, color, true, pipeline, GeometryCache::Cube); + renderInstances(batch, color, true, pipeline, GeometryCache::Cube); } diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index c4531aa102..7fa543abe2 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -163,6 +163,13 @@ public: void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); + void renderSolidShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), + const render::ShapePipelinePointer& pipeline = _simplePipeline); + void renderSolidShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec3& color, + const render::ShapePipelinePointer& pipeline = _simplePipeline) { + renderSolidShapeInstance(batch, shape, glm::vec4(color, 1.0f), pipeline); + } + void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simplePipeline); void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec3& color, diff --git a/scripts/developer/tests/entityEditStressTest.js b/scripts/developer/tests/entityEditStressTest.js index 2d3c8ad0e1..8fa06a968d 100644 --- a/scripts/developer/tests/entityEditStressTest.js +++ b/scripts/developer/tests/entityEditStressTest.js @@ -15,161 +15,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var NUM_ENTITIES = 20000; // number of entities to spawn -var ENTITY_SPAWN_LIMIT = 1000; -var ENTITY_SPAWN_INTERVAL = 0.1; +Script.include("./entitySpawnTool.js"); -var UPDATE_INTERVAL = 0.05; // Re-randomize the entity's position every x seconds / ms -var ENTITY_LIFETIME = 30; // Entity timeout (when/if we crash, we need the entities to delete themselves) -var KEEPALIVE_INTERVAL = 5; // Refreshes the timeout every X seconds - -var RADIUS = 5.0; // Spawn within this radius (square) -var Y_OFFSET = 1.5; // Spawn at an offset below the avatar -var TEST_ENTITY_NAME = "EntitySpawnTest"; - -(function () { - this.makeEntity = function (properties) { - var entity = Entities.addEntity(properties); - // print("spawning entity: " + JSON.stringify(properties)); - - return { - update: function (properties) { - Entities.editEntity(entity, properties); - }, - destroy: function () { - Entities.deleteEntity(entity) - }, - getAge: function () { - return Entities.getEntityProperties(entity).age; - } - }; - } - - this.randomPositionXZ = function (center, radius) { - return { - x: center.x + (Math.random() * radius * 2.0) - radius, - y: center.y, - z: center.z + (Math.random() * radius * 2.0) - radius - }; - } - this.randomColor = function () { - var shade = Math.floor(Math.random() * 255); - var hue = Math.floor(Math.random() * (255 - shade)); - - return { - red: shade + hue, - green: shade, - blue: shade - }; - } - this.randomDimensions = function () { - return { - x: 0.1 + Math.random() * 0.5, - y: 0.1 + Math.random() * 0.1, - z: 0.1 + Math.random() * 0.5 - }; - } -})(); - -(function () { - var entities = []; - var entitiesToCreate = 0; - var entitiesSpawned = 0; - - - function clear () { - var ids = Entities.findEntities(MyAvatar.position, 50); - var that = this; - ids.forEach(function(id) { - var properties = Entities.getEntityProperties(id); - if (properties.name == TEST_ENTITY_NAME) { - Entities.deleteEntity(id); - } - }, this); - } - - function createEntities () { - print("Creating " + NUM_ENTITIES + " entities (UPDATE_INTERVAL = " + UPDATE_INTERVAL + ", KEEPALIVE_INTERVAL = " + KEEPALIVE_INTERVAL + ")"); - entitiesToCreate = NUM_ENTITIES; - Script.update.connect(spawnEntities); - } - - var spawnTimer = 0.0; - function spawnEntities (dt) { - if (entitiesToCreate <= 0) { - Script.update.disconnect(spawnEntities); - print("Finished spawning entities"); - } - else if ((spawnTimer -= dt) < 0.0){ - spawnTimer = ENTITY_SPAWN_INTERVAL; - - var n = Math.min(entitiesToCreate, ENTITY_SPAWN_LIMIT); - print("Spawning " + n + " entities (" + (entitiesSpawned += n) + ")"); - - entitiesToCreate -= n; - - var center = MyAvatar.position; - center.y -= Y_OFFSET; - - for (; n > 0; --n) { - entities.push(makeEntity({ - type: "Box", - name: TEST_ENTITY_NAME, - position: randomPositionXZ(center, RADIUS), - color: randomColor(), - dimensions: randomDimensions(), - lifetime: ENTITY_LIFETIME - })); - } - } - } - - function despawnEntities () { - print("despawning entities"); - entities.forEach(function (entity) { - entity.destroy(); - }); - entities = []; - } - - var keepAliveTimer = 0.0; - var updateTimer = 0.0; - - // Runs the following entity updates: - // a) refreshes the timeout interval every KEEPALIVE_INTERVAL seconds, and - // b) re-randomizes its position every UPDATE_INTERVAL seconds. - // This should be sufficient to crash the client until the entity tree bug is fixed (and thereafter if it shows up again). - function updateEntities (dt) { - var updateLifetime = ((keepAliveTimer -= dt) < 0.0) ? ((keepAliveTimer = KEEPALIVE_INTERVAL), true) : false; - var updateProperties = ((updateTimer -= dt) < 0.0) ? ((updateTimer = UPDATE_INTERVAL), true) : false; - - if (updateLifetime || updateProperties) { - var center = MyAvatar.position; - center.y -= Y_OFFSET; - - entities.forEach((updateLifetime && updateProperties && function (entity) { - entity.update({ - lifetime: entity.getAge() + ENTITY_LIFETIME, - position: randomPositionXZ(center, RADIUS) - }); - }) || (updateLifetime && function (entity) { - entity.update({ - lifetime: entity.getAge() + ENTITY_LIFETIME - }); - }) || (updateProperties && function (entity) { - entity.update({ - position: randomPositionXZ(center, RADIUS) - }); - }) || null, this); - } - } - - function init () { - Script.update.disconnect(init); - clear(); - createEntities(); - Script.update.connect(updateEntities); - Script.scriptEnding.connect(despawnEntities); - } - Script.update.connect(init); -})(); \ No newline at end of file +ENTITY_SPAWNER({ + count: 20000, + spawnLimit: 1000, + spawnInterval: 0.1, + updateInterval: 0.05 +}); diff --git a/scripts/developer/tests/entitySpawnTool.js b/scripts/developer/tests/entitySpawnTool.js new file mode 100644 index 0000000000..d88933b867 --- /dev/null +++ b/scripts/developer/tests/entitySpawnTool.js @@ -0,0 +1,184 @@ +// entityEditStressTest.js +// +// Created by Seiji Emery on 8/31/15 +// Copyright 2015 High Fidelity, Inc. +// +// Stress tests the client + server-side entity trees by spawning huge numbers of entities in +// close proximity to your avatar and updating them continuously (ie. applying position edits), +// with the intent of discovering crashes and other bugs related to the entity, scripting, +// rendering, networking, and/or physics subsystems. +// +// This script was originally created to find + diagnose an a clientside crash caused by improper +// locking of the entity tree, but can be reused for other purposes. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +ENTITY_SPAWNER = function (properties) { + properties = properties || {}; + var RADIUS = properties.radius || 5.0; // Spawn within this radius (square) + var Y_OFFSET = properties.yOffset || 1.5; // Spawn at an offset below the avatar + var TEST_ENTITY_NAME = properties.entityName || "EntitySpawnTest"; + + var NUM_ENTITIES = properties.count || 1000; // number of entities to spawn + var ENTITY_SPAWN_LIMIT = properties.spawnLimit || 100; + var ENTITY_SPAWN_INTERVAL = properties.spawnInterval || properties.interval || 1.0; + + var UPDATE_INTERVAL = properties.updateInterval || properties.interval || 0.1; // Re-randomize the entity's position every x seconds / ms + var ENTITY_LIFETIME = properties.lifetime || 30; // Entity timeout (when/if we crash, we need the entities to delete themselves) + var KEEPALIVE_INTERVAL = properties.keepAlive || 5; // Refreshes the timeout every X seconds + var UPDATES = properties.updates || false + var SHAPES = properties.shapes || ["Icosahedron", "Tetrahedron", "Cube", "Sphere" ]; + + function makeEntity(properties) { + var entity = Entities.addEntity(properties); + // print("spawning entity: " + JSON.stringify(properties)); + + return { + update: function (properties) { + Entities.editEntity(entity, properties); + }, + destroy: function () { + Entities.deleteEntity(entity) + }, + getAge: function () { + return Entities.getEntityProperties(entity).age; + } + }; + } + + function randomPositionXZ(center, radius) { + return { + x: center.x + (Math.random() * radius * 2.0) - radius, + y: center.y, + z: center.z + (Math.random() * radius * 2.0) - radius + }; + } + + function randomPosition(center, radius) { + return { + x: center.x + (Math.random() * radius * 2.0) - radius, + y: center.y + (Math.random() * radius * 2.0) - radius, + z: center.z + (Math.random() * radius * 2.0) - radius + }; + } + + + function randomColor() { + return { + red: Math.floor(Math.random() * 255), + green: Math.floor(Math.random() * 255), + blue: Math.floor(Math.random() * 255), + }; + } + + function randomDimensions() { + return { + x: 0.1 + Math.random() * 0.5, + y: 0.1 + Math.random() * 0.1, + z: 0.1 + Math.random() * 0.5 + }; + } + + var entities = []; + var entitiesToCreate = 0; + var entitiesSpawned = 0; + var spawnTimer = 0.0; + var keepAliveTimer = 0.0; + var updateTimer = 0.0; + + function clear () { + var ids = Entities.findEntities(MyAvatar.position, 50); + var that = this; + ids.forEach(function(id) { + var properties = Entities.getEntityProperties(id); + if (properties.name == TEST_ENTITY_NAME) { + Entities.deleteEntity(id); + } + }, this); + } + + function createEntities () { + print("Creating " + NUM_ENTITIES + " entities (UPDATE_INTERVAL = " + UPDATE_INTERVAL + ", KEEPALIVE_INTERVAL = " + KEEPALIVE_INTERVAL + ")"); + entitiesToCreate = NUM_ENTITIES; + Script.update.connect(spawnEntities); + } + + function spawnEntities (dt) { + if (entitiesToCreate <= 0) { + Script.update.disconnect(spawnEntities); + print("Finished spawning entities"); + } + else if ((spawnTimer -= dt) < 0.0){ + spawnTimer = ENTITY_SPAWN_INTERVAL; + + var n = Math.min(entitiesToCreate, ENTITY_SPAWN_LIMIT); + print("Spawning " + n + " entities (" + (entitiesSpawned += n) + ")"); + + entitiesToCreate -= n; + + var center = MyAvatar.position; + center.y -= Y_OFFSET; + + for (; n > 0; --n) { + entities.push(makeEntity({ + type: "Shape", + shape: SHAPES[n % SHAPES.length], + name: TEST_ENTITY_NAME, + position: randomPosition(center, RADIUS), + color: randomColor(), + dimensions: randomDimensions(), + lifetime: ENTITY_LIFETIME + })); + } + } + } + + function despawnEntities () { + print("despawning entities"); + entities.forEach(function (entity) { + entity.destroy(); + }); + entities = []; + } + + // Runs the following entity updates: + // a) refreshes the timeout interval every KEEPALIVE_INTERVAL seconds, and + // b) re-randomizes its position every UPDATE_INTERVAL seconds. + // This should be sufficient to crash the client until the entity tree bug is fixed (and thereafter if it shows up again). + function updateEntities (dt) { + var updateLifetime = ((keepAliveTimer -= dt) < 0.0) ? ((keepAliveTimer = KEEPALIVE_INTERVAL), true) : false; + var updateProperties = ((updateTimer -= dt) < 0.0) ? ((updateTimer = UPDATE_INTERVAL), true) : false; + + if (updateLifetime || updateProperties) { + var center = MyAvatar.position; + center.y -= Y_OFFSET; + + entities.forEach((updateLifetime && updateProperties && function (entity) { + entity.update({ + lifetime: entity.getAge() + ENTITY_LIFETIME, + position: randomPosition(center, RADIUS) + }); + }) || (updateLifetime && function (entity) { + entity.update({ + lifetime: entity.getAge() + ENTITY_LIFETIME + }); + }) || (updateProperties && function (entity) { + entity.update({ + position: randomPosition(center, RADIUS) + }); + }) || null, this); + } + } + + function init () { + Script.update.disconnect(init); + clear(); + createEntities(); + Script.update.connect(updateEntities); + Script.scriptEnding.connect(despawnEntities); + } + + Script.update.connect(init); +}; \ No newline at end of file diff --git a/scripts/developer/tests/primitivesTest.js b/scripts/developer/tests/primitivesTest.js new file mode 100644 index 0000000000..e401963a83 --- /dev/null +++ b/scripts/developer/tests/primitivesTest.js @@ -0,0 +1,24 @@ +// entityEditStressTest.js +// +// Created by Seiji Emery on 8/31/15 +// Copyright 2015 High Fidelity, Inc. +// +// Stress tests the client + server-side entity trees by spawning huge numbers of entities in +// close proximity to your avatar and updating them continuously (ie. applying position edits), +// with the intent of discovering crashes and other bugs related to the entity, scripting, +// rendering, networking, and/or physics subsystems. +// +// This script was originally created to find + diagnose an a clientside crash caused by improper +// locking of the entity tree, but can be reused for other purposes. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("./entitySpawnTool.js"); + + +ENTITY_SPAWNER({ + updateInterval: 2.0 +}) + diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index efe7e6cc65..4a3b5a14a4 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -25,6 +25,7 @@ var ICON_FOR_TYPE = { Box: "V", Sphere: "n", + Shape: "n", ParticleEffect: "", Model: "", Web: "q", @@ -403,6 +404,10 @@ var elColorGreen = document.getElementById("property-color-green"); var elColorBlue = document.getElementById("property-color-blue"); + var elShapeSections = document.querySelectorAll(".shape-section"); + allSections.push(elShapeSections); + var elShape = document.getElementById("property-shape"); + var elLightSections = document.querySelectorAll(".light-section"); allSections.push(elLightSections); var elLightSpotLight = document.getElementById("property-light-spot-light"); @@ -666,7 +671,18 @@ elHyperlinkSections[i].style.display = 'table'; } - if (properties.type == "Box" || properties.type == "Sphere" || properties.type == "ParticleEffect") { + if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere") { + for (var i = 0; i < elShapeSections.length; i++) { + elShapeSections[i].style.display = 'table'; + } + } else { + for (var i = 0; i < elShapeSections.length; i++) { + console.log("Hiding shape section " + elShapeSections[i]) + elShapeSections[i].style.display = 'none'; + } + } + + if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere" || properties.type == "ParticleEffect") { for (var i = 0; i < elColorSections.length; i++) { elColorSections[i].style.display = 'table'; } @@ -958,6 +974,8 @@ elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); + elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); + elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); @@ -1344,6 +1362,36 @@
+
+ M +
+ +
+ + +
+
+ + +
+ -
M
diff --git a/tests/entities/src/main.cpp b/tests/entities/src/main.cpp index c0e21276d8..792ef7d9c6 100644 --- a/tests/entities/src/main.cpp +++ b/tests/entities/src/main.cpp @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include #include @@ -155,7 +155,7 @@ int main(int argc, char** argv) { QFile file(getTestResourceDir() + "packet.bin"); if (!file.open(QIODevice::ReadOnly)) return -1; QByteArray packet = file.readAll(); - EntityItemPointer item = BoxEntityItem::factory(EntityItemID(), EntityItemProperties()); + EntityItemPointer item = ShapeEntityItem::boxFactory(EntityItemID(), EntityItemProperties()); ReadBitstreamToTreeParams params; params.bitstreamVersion = 33; From 180f4ba4f509e2c70d6f86905d5e4c49ecd704c9 Mon Sep 17 00:00:00 2001 From: Geenz Date: Sun, 22 May 2016 21:02:28 -0400 Subject: [PATCH 0187/1237] Tweaked light attenuation formula some more. Keeping the ulightvec parameter for now - I want to revisit this later. --- libraries/model/src/model/Light.slh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 46eb352d16..de15e50908 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -106,9 +106,11 @@ float evalLightAttenuation(Light l, float d, vec3 ulightvec) { float denom = d / radius + 1.0; float attenuation = 1.0 / (denom * denom); + float cutoff = getLightCutoffRadius(l); + // "Fade" the edges of light sources to make things look a bit more attractive. // Note: this tends to look a bit odd at lower exponents. - attenuation *= clamp(0, 1, mix(0, 1, getLightCutoffSquareRadius(l) - dot(ulightvec, ulightvec))); + attenuation *= min(1, max(0, -(d - cutoff))); return attenuation; } From ff3fca3dc345c048009fc8ba03263850734aef9f Mon Sep 17 00:00:00 2001 From: Geenz Date: Sun, 22 May 2016 21:50:48 -0400 Subject: [PATCH 0188/1237] Remove no longer necessary light vector parameter. --- libraries/model/src/model/Light.slh | 6 ++---- libraries/render-utils/src/point_light.slf | 2 +- libraries/render-utils/src/spot_light.slf | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index de15e50908..7cb745ff53 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -98,10 +98,8 @@ float getLightShowContour(Light l) { return l._control.w; } -// Light is the light source its self, d is the light's distance, and ulightvec is the unnormalized light vector. -// Note that ulightvec should be calculated as light position - fragment position. -// Additionally, length(light position) != dot(light position, light position). -float evalLightAttenuation(Light l, float d, vec3 ulightvec) { +// Light is the light source its self, d is the light's distance calculated as length(unnormalized light vector). +float evalLightAttenuation(Light l, float d) { float radius = getLightRadius(l); float denom = d / radius + 1.0; float attenuation = 1.0 / (denom * denom); diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 581aa8c250..8c9ff2c8ad 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -67,7 +67,7 @@ void main(void) { vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance, fragLightVec); + float radialAttenuation = evalLightAttenuation(light, fragLightDistance); // Final Lighting color vec3 fragColor = (shading.w * frag.diffuse + shading.xyz); diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 1cb6ebb878..63e87e95c0 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -74,7 +74,7 @@ void main(void) { vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance, fragLightVec); + float radialAttenuation = evalLightAttenuation(light, fragLightDistance4); float angularAttenuation = evalLightSpotAttenuation(light, cosSpotAngle); // Final Lighting color From 2c703e963cdb9780c241381948806416deffdd6a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 22 May 2016 17:41:50 -0700 Subject: [PATCH 0189/1237] More shapes --- .../src/RenderableShapeEntityItem.cpp | 4 +- libraries/entities/src/ShapeEntityItem.cpp | 2 +- libraries/entities/src/ShapeEntityItem.h | 2 +- libraries/render-utils/src/GeometryCache.cpp | 651 +++++++++--------- libraries/render-utils/src/GeometryCache.h | 4 +- scripts/system/html/entityProperties.html | 6 +- tests/gpu-test/CMakeLists.txt | 4 +- tests/gpu-test/src/TestHelpers.cpp | 20 + tests/gpu-test/src/TestHelpers.h | 33 + tests/gpu-test/src/TestInstancedShapes.cpp | 84 +++ tests/gpu-test/src/TestInstancedShapes.h | 23 + tests/gpu-test/src/TestShapes.cpp | 48 ++ tests/gpu-test/src/TestShapes.h | 22 + tests/gpu-test/src/TestWindow.cpp | 180 +++++ tests/gpu-test/src/TestWindow.h | 53 ++ tests/gpu-test/src/main.cpp | 539 +++------------ 16 files changed, 895 insertions(+), 780 deletions(-) create mode 100644 tests/gpu-test/src/TestHelpers.cpp create mode 100644 tests/gpu-test/src/TestHelpers.h create mode 100644 tests/gpu-test/src/TestInstancedShapes.cpp create mode 100644 tests/gpu-test/src/TestInstancedShapes.h create mode 100644 tests/gpu-test/src/TestShapes.cpp create mode 100644 tests/gpu-test/src/TestShapes.h create mode 100644 tests/gpu-test/src/TestWindow.cpp create mode 100644 tests/gpu-test/src/TestWindow.h diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 7d30b7a47c..c93ae252e3 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -30,7 +30,7 @@ static GeometryCache::Shape MAPPING[entity::NUM_SHAPES] = { GeometryCache::Cube, GeometryCache::Sphere, GeometryCache::Tetrahedron, - GeometryCache::Octahetron, + GeometryCache::Octahedron, GeometryCache::Dodecahedron, GeometryCache::Icosahedron, GeometryCache::Torus, @@ -93,7 +93,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { if (!success) { return; } - if (_shape != entity::Cube) { + if (_shape == entity::Sphere) { modelTransform.postScale(SPHERE_ENTITY_SCALE); } batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index a24c7e1df5..2527dedab2 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -27,7 +27,7 @@ namespace entity { "Cube", "Sphere", "Tetrahedron", - "Octahetron", + "Octahedron", "Dodecahedron", "Icosahedron", "Torus", diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index f3cb2abd66..2ae4ae2ca1 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -19,7 +19,7 @@ namespace entity { Cube, Sphere, Tetrahedron, - Octahetron, + Octahedron, Dodecahedron, Icosahedron, Torus, diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 6852d17882..02aca4216e 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -51,8 +51,8 @@ static gpu::Stream::FormatPointer INSTANCED_SOLID_STREAM_FORMAT; static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec3) * 2; // vertices and normals static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3); -static const gpu::Type SHAPE_INDEX_TYPE = gpu::UINT16; -static const uint SHAPE_INDEX_SIZE = sizeof(gpu::uint16); +static const gpu::Type SHAPE_INDEX_TYPE = gpu::UINT32; +static const uint SHAPE_INDEX_SIZE = sizeof(gpu::uint32); void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices) { vertexBuffer->append(vertices); @@ -112,102 +112,7 @@ void GeometryCache::ShapeData::drawWireInstances(gpu::Batch& batch, size_t count } } -// The golden ratio -static const float PHI = 1.61803398874f; - -const VertexVector& icosahedronVertices() { - static const float a = 1; - static const float b = PHI / 2.0f; - - static const VertexVector vertices{ // - vec3(0, b, -a), vec3(-b, a, 0), vec3(b, a, 0), // - vec3(0, b, a), vec3(b, a, 0), vec3(-b, a, 0), // - vec3(0, b, a), vec3(-a, 0, b), vec3(0, -b, a), // - vec3(0, b, a), vec3(0, -b, a), vec3(a, 0, b), // - vec3(0, b, -a), vec3(a, 0, -b), vec3(0, -b, -a),// - vec3(0, b, -a), vec3(0, -b, -a), vec3(-a, 0, -b), // - vec3(0, -b, a), vec3(-b, -a, 0), vec3(b, -a, 0), // - vec3(0, -b, -a), vec3(b, -a, 0), vec3(-b, -a, 0), // - vec3(-b, a, 0), vec3(-a, 0, -b), vec3(-a, 0, b), // - vec3(-b, -a, 0), vec3(-a, 0, b), vec3(-a, 0, -b), // - vec3(b, a, 0), vec3(a, 0, b), vec3(a, 0, -b), // - vec3(b, -a, 0), vec3(a, 0, -b), vec3(a, 0, b), // - vec3(0, b, a), vec3(-b, a, 0), vec3(-a, 0, b), // - vec3(0, b, a), vec3(a, 0, b), vec3(b, a, 0), // - vec3(0, b, -a), vec3(-a, 0, -b), vec3(-b, a, 0), // - vec3(0, b, -a), vec3(b, a, 0), vec3(a, 0, -b), // - vec3(0, -b, -a), vec3(-b, -a, 0), vec3(-a, 0, -b), // - vec3(0, -b, -a), vec3(a, 0, -b), vec3(b, -a, 0), // - vec3(0, -b, a), vec3(-a, 0, b), vec3(-b, -a, 0), // - vec3(0, -b, a), vec3(b, -a, 0), vec3(a, 0, b) - }; // - return vertices; -} - -const VertexVector& tetrahedronVertices() { - static const auto A = vec3(1, 1, 1); - static const auto B = vec3(1, -1, -1); - static const auto C = vec3(-1, 1, -1); - static const auto D = vec3(-1, -1, 1); - static const VertexVector vertices{ - A, B, C, - D, B, A, - C, D, A, - C, B, D, - }; - return vertices; -} - -static const size_t TESSELTATION_MULTIPLIER = 4; static const size_t ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT = 3; -static const size_t VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER = 2; - - -VertexVector tesselate(const VertexVector& startingTriangles, int count) { - VertexVector triangles = startingTriangles; - if (0 != (triangles.size() % 3)) { - throw std::runtime_error("Bad number of vertices for tesselation"); - } - - for (size_t i = 0; i < triangles.size(); ++i) { - triangles[i] = glm::normalize(triangles[i]); - } - - VertexVector newTriangles; - while (count) { - newTriangles.clear(); - // Tesselation takes one triangle and makes it into 4 triangles - // See https://en.wikipedia.org/wiki/Space-filling_tree#/media/File:Space_Filling_Tree_Tri_iter_1_2_3.png - newTriangles.reserve(triangles.size() * TESSELTATION_MULTIPLIER); - for (size_t i = 0; i < triangles.size(); i += VERTICES_PER_TRIANGLE) { - const vec3& a = triangles[i]; - const vec3& b = triangles[i + 1]; - const vec3& c = triangles[i + 2]; - vec3 ab = glm::normalize(a + b); - vec3 bc = glm::normalize(b + c); - vec3 ca = glm::normalize(c + a); - - newTriangles.push_back(a); - newTriangles.push_back(ab); - newTriangles.push_back(ca); - - newTriangles.push_back(b); - newTriangles.push_back(bc); - newTriangles.push_back(ab); - - newTriangles.push_back(c); - newTriangles.push_back(ca); - newTriangles.push_back(bc); - - newTriangles.push_back(ab); - newTriangles.push_back(bc); - newTriangles.push_back(ca); - } - triangles.swap(newTriangles); - --count; - } - return triangles; -} size_t GeometryCache::getShapeTriangleCount(Shape shape) { return _shapes[shape]._indexCount / VERTICES_PER_TRIANGLE; @@ -221,6 +126,324 @@ size_t GeometryCache::getCubeTriangleCount() { return getShapeTriangleCount(Cube); } +using Index = uint32_t; +using IndexPair = uint64_t; +using IndexPairs = std::unordered_set; + +template +using Face = std::array; + +template +using FaceVector = std::vector>; + +template +struct Solid { + VertexVector vertices; + FaceVector faces; + + Solid& fitDimension(float newMaxDimension) { + float maxDimension = 0; + for (const auto& vertex : vertices) { + maxDimension = std::max(maxDimension, std::max(std::max(vertex.x, vertex.y), vertex.z)); + } + float multiplier = newMaxDimension / maxDimension; + for (auto& vertex : vertices) { + vertex *= multiplier; + } + return *this; + } + + vec3 getFaceNormal(size_t faceIndex) const { + vec3 result; + const auto& face = faces[faceIndex]; + for (size_t i = 0; i < N; ++i) { + result += vertices[face[i]]; + } + result /= N; + return glm::normalize(result); + } +}; + +template +static size_t triangulatedFaceTriangleCount() { + return N - 2; +} + +template +static size_t triangulatedFaceIndexCount() { + return triangulatedFaceTriangleCount() * VERTICES_PER_TRIANGLE; +} + +static IndexPair indexToken(Index a, Index b) { + if (a > b) { + std::swap(a, b); + } + return (((IndexPair)a) << 32) | ((IndexPair)b); +} + +static Solid<3> tesselate(Solid<3> solid, int count) { + float length = glm::length(solid.vertices[0]); + for (int i = 0; i < count; ++i) { + Solid<3> result { solid.vertices, {} }; + result.vertices.reserve(solid.vertices.size() + solid.faces.size() * 3); + for (size_t f = 0; f < solid.faces.size(); ++f) { + Index baseVertex = (Index)result.vertices.size(); + const Face<3>& oldFace = solid.faces[f]; + const vec3& a = solid.vertices[oldFace[0]]; + const vec3& b = solid.vertices[oldFace[1]]; + const vec3& c = solid.vertices[oldFace[2]]; + vec3 ab = glm::normalize(a + b) * length; + vec3 bc = glm::normalize(b + c) * length; + vec3 ca = glm::normalize(c + a) * length; + result.vertices.push_back(ab); + result.vertices.push_back(bc); + result.vertices.push_back(ca); + result.faces.push_back(Face<3>{ { oldFace[0], baseVertex, baseVertex + 2 } }); + result.faces.push_back(Face<3>{ { baseVertex, oldFace[1], baseVertex + 1 } }); + result.faces.push_back(Face<3>{ { baseVertex + 1, oldFace[2], baseVertex + 2 } }); + result.faces.push_back(Face<3>{ { baseVertex, baseVertex + 1, baseVertex + 2 } }); + } + solid = result; + } + return solid; +} + +template +void setupFlatShape(GeometryCache::ShapeData& shapeData, const Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); + VertexVector vertices; + IndexVector solidIndices, wireIndices; + IndexPairs wireSeenIndices; + + size_t faceCount = shape.faces.size(); + size_t faceIndexCount = triangulatedFaceIndexCount(); + + vertices.reserve(N * faceCount * 2); + solidIndices.reserve(faceIndexCount * faceCount); + + for (size_t f = 0; f < faceCount; ++f) { + const Face& face = shape.faces[f]; + // Compute the face normal + vec3 faceNormal = shape.getFaceNormal(f); + + // Create the vertices for the face + for (Index i = 0; i < N; ++i) { + Index originalIndex = face[i]; + vertices.push_back(shape.vertices[originalIndex]); + vertices.push_back(faceNormal); + } + + // Create the wire indices for unseen edges + for (Index i = 0; i < N; ++i) { + Index a = i; + Index b = (i + 1) % N; + auto token = indexToken(face[a], face[b]); + if (0 == wireSeenIndices.count(token)) { + wireSeenIndices.insert(token); + wireIndices.push_back(a + baseVertex); + wireIndices.push_back(b + baseVertex); + } + } + + // Create the solid face indices + for (Index i = 0; i < N - 2; ++i) { + solidIndices.push_back(0 + baseVertex); + solidIndices.push_back(i + 1 + baseVertex); + solidIndices.push_back(i + 2 + baseVertex); + } + baseVertex += (Index)N; + } + + shapeData.setupVertices(vertexBuffer, vertices); + shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); +} + +template +void setupSmoothShape(GeometryCache::ShapeData& shapeData, const Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); + + VertexVector vertices; + vertices.reserve(shape.vertices.size() * 2); + for (const auto& vertex : shape.vertices) { + vertices.push_back(vertex); + vertices.push_back(vertex); + } + + IndexVector solidIndices, wireIndices; + IndexPairs wireSeenIndices; + + size_t faceCount = shape.faces.size(); + size_t faceIndexCount = triangulatedFaceIndexCount(); + + solidIndices.reserve(faceIndexCount * faceCount); + + for (size_t f = 0; f < faceCount; ++f) { + const Face& face = shape.faces[f]; + // Create the wire indices for unseen edges + for (Index i = 0; i < N; ++i) { + Index a = face[i]; + Index b = face[(i + 1) % N]; + auto token = indexToken(a, b); + if (0 == wireSeenIndices.count(token)) { + wireSeenIndices.insert(token); + wireIndices.push_back(a + baseVertex); + wireIndices.push_back(b + baseVertex); + } + } + + // Create the solid face indices + for (Index i = 0; i < N - 2; ++i) { + solidIndices.push_back(face[i] + baseVertex); + solidIndices.push_back(face[i + 1] + baseVertex); + solidIndices.push_back(face[i + 2] + baseVertex); + } + } + + shapeData.setupVertices(vertexBuffer, vertices); + shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); +} + +// The golden ratio +static const float PHI = 1.61803398874f; + +static const Solid<3>& tetrahedron() { + static const auto A = vec3(1, 1, 1); + static const auto B = vec3(1, -1, -1); + static const auto C = vec3(-1, 1, -1); + static const auto D = vec3(-1, -1, 1); + static const Solid<3> TETRAHEDRON = Solid<3>{ + { A, B, C, D }, + FaceVector<3>{ + Face<3> { { 0, 1, 2 } }, + Face<3> { { 3, 1, 0 } }, + Face<3> { { 2, 3, 0 } }, + Face<3> { { 2, 1, 3 } }, + } + }.fitDimension(0.5f); + return TETRAHEDRON; +} + +static const Solid<4>& cube() { + static const auto A = vec3(1, 1, 1); + static const auto B = vec3(-1, 1, 1); + static const auto C = vec3(-1, 1, -1); + static const auto D = vec3(1, 1, -1); + static const Solid<4> CUBE = Solid<4>{ + { A, B, C, D, -A, -B, -C, -D }, + FaceVector<4>{ + Face<4> { { 3, 2, 1, 0 } }, + Face<4> { { 0, 1, 7, 6 } }, + Face<4> { { 1, 2, 4, 7 } }, + Face<4> { { 2, 3, 5, 4 } }, + Face<4> { { 3, 0, 6, 5 } }, + Face<4> { { 4, 5, 6, 7 } }, + } + }.fitDimension(0.5f); + return CUBE; +} + +static const Solid<3>& octahedron() { + static const auto A = vec3(0, 1, 0); + static const auto B = vec3(0, -1, 0); + static const auto C = vec3(0, 0, 1); + static const auto D = vec3(0, 0, -1); + static const auto E = vec3(1, 0, 0); + static const auto F = vec3(-1, 0, 0); + static const Solid<3> OCTAHEDRON = Solid<3>{ + { A, B, C, D, E, F}, + FaceVector<3> { + Face<3> { { 0, 2, 4, } }, + Face<3> { { 0, 4, 3, } }, + Face<3> { { 0, 3, 5, } }, + Face<3> { { 0, 5, 2, } }, + Face<3> { { 1, 4, 2, } }, + Face<3> { { 1, 3, 4, } }, + Face<3> { { 1, 5, 3, } }, + Face<3> { { 1, 2, 5, } }, + } + }.fitDimension(0.5f); + return OCTAHEDRON; +} + +static const Solid<5>& dodecahedron() { + static const float P = PHI; + static const float IP = 1.0f / PHI; + static const vec3 A = vec3(IP, P, 0); + static const vec3 B = vec3(-IP, P, 0); + static const vec3 C = vec3(-1, 1, 1); + static const vec3 D = vec3(0, IP, P); + static const vec3 E = vec3(1, 1, 1); + static const vec3 F = vec3(1, 1, -1); + static const vec3 G = vec3(-1, 1, -1); + static const vec3 H = vec3(-P, 0, IP); + static const vec3 I = vec3(0, -IP, P); + static const vec3 J = vec3(P, 0, IP); + + static const Solid<5> DODECAHEDRON = Solid<5>{ + { + A, B, C, D, E, F, G, H, I, J, + -A, -B, -C, -D, -E, -F, -G, -H, -I, -J, + }, + FaceVector<5> { + Face<5> { { 0, 1, 2, 3, 4 } }, + Face<5> { { 0, 5, 18, 6, 1 } }, + Face<5> { { 1, 6, 19, 7, 2 } }, + Face<5> { { 2, 7, 15, 8, 3 } }, + Face<5> { { 3, 8, 16, 9, 4 } }, + Face<5> { { 4, 9, 17, 5, 0 } }, + Face<5> { { 14, 13, 12, 11, 10 } }, + Face<5> { { 11, 16, 8, 15, 10 } }, + Face<5> { { 12, 17, 9, 16, 11 } }, + Face<5> { { 13, 18, 5, 17, 12 } }, + Face<5> { { 14, 19, 6, 18, 13 } }, + Face<5> { { 10, 15, 7, 19, 14 } }, + } + }.fitDimension(0.5f); + return DODECAHEDRON; +} + +static const Solid<3>& icosahedron() { + static const float N = 1.0f / PHI; + static const float P = 1.0f; + static const auto A = vec3(N, P, 0); + static const auto B = vec3(-N, P, 0); + static const auto C = vec3(0, N, P); + static const auto D = vec3(P, 0, N); + static const auto E = vec3(P, 0, -N); + static const auto F = vec3(0, N, -P); + + static const Solid<3> ICOSAHEDRON = Solid<3> { + { + A, B, C, D, E, F, + -A, -B, -C, -D, -E, -F, + }, + FaceVector<3> { + Face<3> { { 1, 2, 0 } }, + Face<3> { { 2, 3, 0 } }, + Face<3> { { 3, 4, 0 } }, + Face<3> { { 4, 5, 0 } }, + Face<3> { { 5, 1, 0 } }, + + Face<3> { { 1, 10, 2 } }, + Face<3> { { 11, 2, 10 } }, + Face<3> { { 2, 11, 3 } }, + Face<3> { { 7, 3, 11 } }, + Face<3> { { 3, 7, 4 } }, + Face<3> { { 8, 4, 7 } }, + Face<3> { { 4, 8, 5 } }, + Face<3> { { 9, 5, 8 } }, + Face<3> { { 5, 9, 1 } }, + Face<3> { { 10, 1, 9 } }, + + Face<3> { { 8, 7, 6 } }, + Face<3> { { 9, 8, 6 } }, + Face<3> { { 10, 9, 6 } }, + Face<3> { { 11, 10, 6 } }, + Face<3> { { 7, 11, 6 } }, + } + }.fitDimension(0.5f); + return ICOSAHEDRON; +} // FIXME solids need per-face vertices, but smooth shaded // components do not. Find a way to support using draw elements @@ -230,229 +453,28 @@ size_t GeometryCache::getCubeTriangleCount() { void GeometryCache::buildShapes() { auto vertexBuffer = std::make_shared(); auto indexBuffer = std::make_shared(); - size_t startingIndex = 0; - // Cube - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Cube]; - VertexVector vertices; - // front - vertices.push_back(vec3(1, 1, 1)); - vertices.push_back(vec3(0, 0, 1)); - vertices.push_back(vec3(-1, 1, 1)); - vertices.push_back(vec3(0, 0, 1)); - vertices.push_back(vec3(-1, -1, 1)); - vertices.push_back(vec3(0, 0, 1)); - vertices.push_back(vec3(1, -1, 1)); - vertices.push_back(vec3(0, 0, 1)); - - // right - vertices.push_back(vec3(1, 1, 1)); - vertices.push_back(vec3(1, 0, 0)); - vertices.push_back(vec3(1, -1, 1)); - vertices.push_back(vec3(1, 0, 0)); - vertices.push_back(vec3(1, -1, -1)); - vertices.push_back(vec3(1, 0, 0)); - vertices.push_back(vec3(1, 1, -1)); - vertices.push_back(vec3(1, 0, 0)); - - // top - vertices.push_back(vec3(1, 1, 1)); - vertices.push_back(vec3(0, 1, 0)); - vertices.push_back(vec3(1, 1, -1)); - vertices.push_back(vec3(0, 1, 0)); - vertices.push_back(vec3(-1, 1, -1)); - vertices.push_back(vec3(0, 1, 0)); - vertices.push_back(vec3(-1, 1, 1)); - vertices.push_back(vec3(0, 1, 0)); - - // left - vertices.push_back(vec3(-1, 1, 1)); - vertices.push_back(vec3(-1, 0, 0)); - vertices.push_back(vec3(-1, 1, -1)); - vertices.push_back(vec3(-1, 0, 0)); - vertices.push_back(vec3(-1, -1, -1)); - vertices.push_back(vec3(-1, 0, 0)); - vertices.push_back(vec3(-1, -1, 1)); - vertices.push_back(vec3(-1, 0, 0)); - - // bottom - vertices.push_back(vec3(-1, -1, -1)); - vertices.push_back(vec3(0, -1, 0)); - vertices.push_back(vec3(1, -1, -1)); - vertices.push_back(vec3(0, -1, 0)); - vertices.push_back(vec3(1, -1, 1)); - vertices.push_back(vec3(0, -1, 0)); - vertices.push_back(vec3(-1, -1, 1)); - vertices.push_back(vec3(0, -1, 0)); - - // back - vertices.push_back(vec3(1, -1, -1)); - vertices.push_back(vec3(0, 0, -1)); - vertices.push_back(vec3(-1, -1, -1)); - vertices.push_back(vec3(0, 0, -1)); - vertices.push_back(vec3(-1, 1, -1)); - vertices.push_back(vec3(0, 0, -1)); - vertices.push_back(vec3(1, 1, -1)); - vertices.push_back(vec3(0, 0, -1)); - - static const size_t VERTEX_FORMAT_SIZE = 2; - static const size_t VERTEX_OFFSET = 0; - - for (size_t i = 0; i < vertices.size(); ++i) { - auto vertexIndex = i; - // Make a unit cube by having the vertices (at index N) - // while leaving the normals (at index N + 1) alone - if (VERTEX_OFFSET == vertexIndex % VERTEX_FORMAT_SIZE) { - vertices[vertexIndex] *= 0.5f; - } - } - shapeData.setupVertices(_shapeVertices, vertices); - - IndexVector indices{ - 0, 1, 2, 2, 3, 0, // front - 4, 5, 6, 6, 7, 4, // right - 8, 9, 10, 10, 11, 8, // top - 12, 13, 14, 14, 15, 12, // left - 16, 17, 18, 18, 19, 16, // bottom - 20, 21, 22, 22, 23, 20 // back - }; - for (auto& index : indices) { - index += (uint16_t)startingIndex; - } - - IndexVector wireIndices{ - 0, 1, 1, 2, 2, 3, 3, 0, // front - 20, 21, 21, 22, 22, 23, 23, 20, // back - 0, 23, 1, 22, 2, 21, 3, 20 // sides - }; - - for (size_t i = 0; i < wireIndices.size(); ++i) { - indices[i] += (uint16_t)startingIndex; - } - - shapeData.setupIndices(_shapeIndices, indices, wireIndices); - } - + setupFlatShape(_shapes[Cube], cube(), _shapeVertices, _shapeIndices); // Tetrahedron - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Tetrahedron]; - size_t vertexCount = 4; - VertexVector vertices; - { - VertexVector originalVertices = tetrahedronVertices(); - vertexCount = originalVertices.size(); - vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); - for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) { - auto triangleStartIndex = i; - vec3 faceNormal; - for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - faceNormal += originalVertices[vertexIndex]; - } - faceNormal = glm::normalize(faceNormal); - for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - vertices.push_back(originalVertices[vertexIndex]); - vertices.push_back(faceNormal); - } - } - } - shapeData.setupVertices(_shapeVertices, vertices); - - IndexVector indices; - for (size_t i = 0; i < vertexCount; i += VERTICES_PER_TRIANGLE) { - auto triangleStartIndex = i; - for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - indices.push_back((uint16_t)(vertexIndex + startingIndex)); - } - } - - IndexVector wireIndices{ - 0, 1, 1, 2, 2, 0, - 0, 3, 1, 3, 2, 3, - }; - - for (size_t i = 0; i < wireIndices.size(); ++i) { - wireIndices[i] += (uint16_t)startingIndex; - } - - shapeData.setupIndices(_shapeIndices, indices, wireIndices); - } - + setupFlatShape(_shapes[Tetrahedron], tetrahedron(), _shapeVertices, _shapeIndices); + // Icosahedron + setupFlatShape(_shapes[Icosahedron], icosahedron(), _shapeVertices, _shapeIndices); + // Octahedron + setupFlatShape(_shapes[Octahedron], octahedron(), _shapeVertices, _shapeIndices); + // Dodecahedron + setupFlatShape(_shapes[Dodecahedron], dodecahedron(), _shapeVertices, _shapeIndices); + // Sphere // FIXME this uses way more vertices than required. Should find a way to calculate the indices // using shared vertices for better vertex caching - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Sphere]; - VertexVector vertices; - IndexVector indices; - { - VertexVector originalVertices = tesselate(icosahedronVertices(), ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT); - vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); - for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) { - auto triangleStartIndex = i; - for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - const auto& vertex = originalVertices[i + j]; - // Spheres use the same values for vertices and normals - vertices.push_back(vertex); - vertices.push_back(vertex); - indices.push_back((uint16_t)(vertexIndex + startingIndex)); - } - } - } - - shapeData.setupVertices(_shapeVertices, vertices); - // FIXME don't use solid indices for wire drawing. - shapeData.setupIndices(_shapeIndices, indices, indices); - } - - // Icosahedron - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Icosahedron]; - - VertexVector vertices; - IndexVector indices; - { - const VertexVector& originalVertices = icosahedronVertices(); - vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); - for (size_t i = 0; i < originalVertices.size(); i += 3) { - auto triangleStartIndex = i; - vec3 faceNormal; - for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - faceNormal += originalVertices[vertexIndex]; - } - faceNormal = glm::normalize(faceNormal); - for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - vertices.push_back(originalVertices[vertexIndex]); - vertices.push_back(faceNormal); - indices.push_back((uint16_t)(vertexIndex + startingIndex)); - } - } - } - - shapeData.setupVertices(_shapeVertices, vertices); - // FIXME don't use solid indices for wire drawing. - shapeData.setupIndices(_shapeIndices, indices, indices); - } + Solid<3> sphere = icosahedron(); + sphere = tesselate(sphere, ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT); + sphere.fitDimension(1.0f); + setupSmoothShape(_shapes[Sphere], sphere, _shapeVertices, _shapeIndices); // Line - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; { + Index baseVertex = (Index)(_shapeVertices->getSize() / SHAPE_VERTEX_STRIDE); ShapeData& shapeData = _shapes[Line]; shapeData.setupVertices(_shapeVertices, VertexVector{ vec3(-0.5, 0, 0), vec3(-0.5f, 0, 0), @@ -460,9 +482,8 @@ void GeometryCache::buildShapes() { }); IndexVector wireIndices; // Only two indices - wireIndices.push_back(0 + (uint16_t)startingIndex); - wireIndices.push_back(1 + (uint16_t)startingIndex); - + wireIndices.push_back(0 + baseVertex); + wireIndices.push_back(1 + baseVertex); shapeData.setupIndices(_shapeIndices, IndexVector(), wireIndices); } diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 7fa543abe2..a2f79de029 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -121,8 +121,8 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) { seed); } +using IndexVector = std::vector; using VertexVector = std::vector; -using IndexVector = std::vector; /// Stores cached geometry. class GeometryCache : public Dependency { @@ -137,7 +137,7 @@ public: Cube, Sphere, Tetrahedron, - Octahetron, + Octahedron, Dodecahedron, Icosahedron, Torus, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 4a3b5a14a4..c611fc5b38 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1368,10 +1368,12 @@
diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt index 4fc6143ff5..21ae9c5a99 100644 --- a/tests/gpu-test/CMakeLists.txt +++ b/tests/gpu-test/CMakeLists.txt @@ -4,4 +4,6 @@ AUTOSCRIBE_SHADER_LIB(gpu model render-utils) setup_hifi_project(Quick Gui OpenGL Script Widgets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(networking gl gpu gpu-gl procedural shared fbx model model-networking animation script-engine render render-utils ) -package_libraries_for_deployment() \ No newline at end of file +package_libraries_for_deployment() + +target_nsight() diff --git a/tests/gpu-test/src/TestHelpers.cpp b/tests/gpu-test/src/TestHelpers.cpp new file mode 100644 index 0000000000..75586da904 --- /dev/null +++ b/tests/gpu-test/src/TestHelpers.cpp @@ -0,0 +1,20 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestHelpers.h" + +gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings) { + auto vs = gpu::Shader::createVertex(vertexShaderSrc); + auto fs = gpu::Shader::createPixel(fragmentShaderSrc); + auto shader = gpu::Shader::createProgram(vs, fs); + if (!gpu::Shader::makeProgram(*shader, bindings)) { + printf("Could not compile shader\n"); + exit(-1); + } + return shader; +} diff --git a/tests/gpu-test/src/TestHelpers.h b/tests/gpu-test/src/TestHelpers.h new file mode 100644 index 0000000000..fd8989f628 --- /dev/null +++ b/tests/gpu-test/src/TestHelpers.h @@ -0,0 +1,33 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +class GpuTestBase { +public: + virtual ~GpuTestBase() {} + virtual bool isReady() const { return true; } + virtual size_t getTestCount() const { return 1; } + virtual void renderTest(size_t test, RenderArgs* args) = 0; +}; + +uint32_t toCompactColor(const glm::vec4& color); +gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings); + diff --git a/tests/gpu-test/src/TestInstancedShapes.cpp b/tests/gpu-test/src/TestInstancedShapes.cpp new file mode 100644 index 0000000000..6a98ee58b9 --- /dev/null +++ b/tests/gpu-test/src/TestInstancedShapes.cpp @@ -0,0 +1,84 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestInstancedShapes.h" + +gpu::Stream::FormatPointer& getInstancedSolidStreamFormat(); + +static const size_t TYPE_COUNT = 4; +static const size_t ITEM_COUNT = 50; +static const float SHAPE_INTERVAL = (PI * 2.0f) / ITEM_COUNT; +static const float ITEM_INTERVAL = SHAPE_INTERVAL / TYPE_COUNT; + +static GeometryCache::Shape SHAPE[TYPE_COUNT] = { + GeometryCache::Icosahedron, + GeometryCache::Cube, + GeometryCache::Sphere, + GeometryCache::Tetrahedron, +}; + +const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA }; + + +TestInstancedShapes::TestInstancedShapes() { + auto geometryCache = DependencyManager::get(); + colorBuffer = std::make_shared(); + + static const float ITEM_RADIUS = 20; + static const vec3 ITEM_TRANSLATION { 0, 0, -ITEM_RADIUS }; + for (size_t i = 0; i < TYPE_COUNT; ++i) { + GeometryCache::Shape shape = SHAPE[i]; + GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; + //indirectCommand._count + float startingInterval = ITEM_INTERVAL * i; + std::vector typeTransforms; + for (size_t j = 0; j < ITEM_COUNT; ++j) { + float theta = j * SHAPE_INTERVAL + startingInterval; + auto transform = glm::rotate(mat4(), theta, Vectors::UP); + transform = glm::rotate(transform, (randFloat() - 0.5f) * PI / 4.0f, Vectors::UNIT_X); + transform = glm::translate(transform, ITEM_TRANSLATION); + transform = glm::scale(transform, vec3(randFloat() / 2.0f + 0.5f)); + typeTransforms.push_back(transform); + auto color = vec4 { randomColorValue(64), randomColorValue(64), randomColorValue(64), 255 }; + color /= 255.0f; + colors.push_back(color); + colorBuffer->append(toCompactColor(color)); + } + transforms.push_back(typeTransforms); + } +} + +void TestInstancedShapes::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(batch); + batch.setInputFormat(getInstancedSolidStreamFormat()); + for (size_t i = 0; i < TYPE_COUNT; ++i) { + GeometryCache::Shape shape = SHAPE[i]; + GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; + + std::string namedCall = __FUNCTION__ + std::to_string(i); + + //batch.addInstanceModelTransforms(transforms[i]); + for (size_t j = 0; j < ITEM_COUNT; ++j) { + batch.setModelTransform(transforms[i][j]); + batch.setupNamedCalls(namedCall, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData&) { + batch.setInputBuffer(gpu::Stream::COLOR, gpu::BufferView(colorBuffer, i * ITEM_COUNT * 4, colorBuffer->getSize(), COLOR_ELEMENT)); + shapeData.drawInstances(batch, ITEM_COUNT); + }); + } + + //for (size_t j = 0; j < ITEM_COUNT; ++j) { + // batch.setModelTransform(transforms[j + i * ITEM_COUNT]); + // shapeData.draw(batch); + //} + } +} + diff --git a/tests/gpu-test/src/TestInstancedShapes.h b/tests/gpu-test/src/TestInstancedShapes.h new file mode 100644 index 0000000000..b509a13e60 --- /dev/null +++ b/tests/gpu-test/src/TestInstancedShapes.h @@ -0,0 +1,23 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include "TestHelpers.h" + +class TestInstancedShapes : public GpuTestBase { + + std::vector> transforms; + std::vector colors; + gpu::BufferPointer colorBuffer; + gpu::BufferView instanceXfmView; +public: + TestInstancedShapes(); + void renderTest(size_t testId, RenderArgs* args) override; +}; + diff --git a/tests/gpu-test/src/TestShapes.cpp b/tests/gpu-test/src/TestShapes.cpp new file mode 100644 index 0000000000..253d89cf61 --- /dev/null +++ b/tests/gpu-test/src/TestShapes.cpp @@ -0,0 +1,48 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestShapes.h" + +static const size_t TYPE_COUNT = 6; + +static GeometryCache::Shape SHAPE[TYPE_COUNT] = { + GeometryCache::Cube, + GeometryCache::Tetrahedron, + GeometryCache::Octahedron, + GeometryCache::Dodecahedron, + GeometryCache::Icosahedron, + GeometryCache::Sphere, +}; + +void TestShapes::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(batch); + + // Render unlit cube + sphere + static auto startSecs = secTimestampNow(); + float seconds = secTimestampNow() - startSecs; + seconds /= 4.0f; + batch.setModelTransform(Transform()); + batch._glColor4f(0.8f, 0.25f, 0.25f, 1.0f); + + bool wire = (seconds - floorf(seconds) > 0.5f); + int shapeIndex = ((int)seconds) % TYPE_COUNT; + if (wire) { + geometryCache->renderWireShape(batch, SHAPE[shapeIndex]); + } else { + geometryCache->renderShape(batch, SHAPE[shapeIndex]); + } + + batch.setModelTransform(Transform().setScale(1.01f)); + batch._glColor4f(1, 1, 1, 1); + geometryCache->renderWireCube(batch); +} + + + diff --git a/tests/gpu-test/src/TestShapes.h b/tests/gpu-test/src/TestShapes.h new file mode 100644 index 0000000000..606d3a45f7 --- /dev/null +++ b/tests/gpu-test/src/TestShapes.h @@ -0,0 +1,22 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include "TestHelpers.h" + +class TestShapes : public GpuTestBase { + + std::vector> transforms; + std::vector colors; + gpu::BufferPointer colorBuffer; + gpu::BufferView instanceXfmView; +public: + void renderTest(size_t testId, RenderArgs* args) override; +}; + diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp new file mode 100644 index 0000000000..4fe25e989d --- /dev/null +++ b/tests/gpu-test/src/TestWindow.cpp @@ -0,0 +1,180 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestWindow.h" + +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#ifdef DEFERRED_LIGHTING +extern void initDeferredPipelines(render::ShapePlumber& plumber); +extern void initStencilPipeline(gpu::PipelinePointer& pipeline); +#endif + +TestWindow::TestWindow() { + setSurfaceType(QSurface::OpenGLSurface); + + + auto timer = new QTimer(this); + timer->setInterval(5); + connect(timer, &QTimer::timeout, [&] { draw(); }); + timer->start(); + + connect(qApp, &QCoreApplication::aboutToQuit, [this, timer] { + timer->stop(); + _aboutToQuit = true; + }); + +#ifdef DEFERRED_LIGHTING + _light->setType(model::Light::SUN); + _light->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset::OLD_TOWN_SQUARE); + _light->setIntensity(1.0f); + _light->setAmbientIntensity(0.5f); + _light->setColor(vec3(1.0f)); + _light->setPosition(vec3(1, 1, 1)); + _renderContext->args = _renderArgs; +#endif + + QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); + format.setOption(QSurfaceFormat::DebugContext); + //format.setSwapInterval(0); + setFormat(format); + _glContext.setFormat(format); + _glContext.create(); + _glContext.makeCurrent(this); + show(); +} + +void TestWindow::initGl() { + _glContext.makeCurrent(this); + gpu::Context::init(); + _renderArgs->_context = std::make_shared(); + _glContext.makeCurrent(this); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + resize(QSize(800, 600)); + + setupDebugLogger(this); +#ifdef DEFERRED_LIGHTING + auto deferredLightingEffect = DependencyManager::get(); + deferredLightingEffect->init(); + deferredLightingEffect->setGlobalLight(_light); + initDeferredPipelines(*_shapePlumber); + initStencilPipeline(_opaquePipeline); +#endif +} + +void TestWindow::resizeWindow(const QSize& size) { + _size = size; + _renderArgs->_viewport = ivec4(0, 0, _size.width(), _size.height()); + auto fboCache = DependencyManager::get(); + if (fboCache) { + fboCache->setFrameBufferSize(_size); + } +} + +void TestWindow::beginFrame() { + _renderArgs->_context->syncCache(); + +#ifdef DEFERRED_LIGHTING + auto deferredLightingEffect = DependencyManager::get(); + deferredLightingEffect->prepare(_renderArgs); +#else + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 0.0f, 0.1f, 0.2f, 1.0f }); + batch.clearDepthFramebuffer(1e4); + batch.setViewportTransform({ 0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio() }); + }); +#endif + + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.setViewportTransform(_renderArgs->_viewport); + batch.setStateScissorRect(_renderArgs->_viewport); + batch.setProjectionTransform(_projectionMatrix); + }); +} + +void TestWindow::endFrame() { +#ifdef DEFERRED_LIGHTING + RenderArgs* args = _renderContext->args; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + auto deferredFboColorDepthStencil = DependencyManager::get()->getDeferredFramebufferDepthColor(); + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + batch.setFramebuffer(deferredFboColorDepthStencil); + batch.setPipeline(_opaquePipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(0, nullptr); + }); + + auto deferredLightingEffect = DependencyManager::get(); + deferredLightingEffect->render(_renderContext); + + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + PROFILE_RANGE_BATCH(batch, "blit"); + // Blit to screen + auto framebufferCache = DependencyManager::get(); + auto framebuffer = framebufferCache->getLightingFramebuffer(); + batch.blit(framebuffer, _renderArgs->_viewport, nullptr, _renderArgs->_viewport); + }); +#endif + + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.resetStages(); + }); + _glContext.swapBuffers(this); +} + +void TestWindow::draw() { + if (_aboutToQuit) { + return; + } + + // Attempting to draw before we're visible and have a valid size will + // produce GL errors. + if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) { + return; + } + + if (!_glContext.makeCurrent(this)) { + return; + } + + static std::once_flag once; + std::call_once(once, [&] { initGl(); }); + beginFrame(); + + renderFrame(); + + endFrame(); +} + +void TestWindow::resizeEvent(QResizeEvent* ev) { + resizeWindow(ev->size()); + float fov_degrees = 60.0f; + float aspect_ratio = (float)_size.width() / _size.height(); + float near_clip = 0.1f; + float far_clip = 1000.0f; + _projectionMatrix = glm::perspective(glm::radians(fov_degrees), aspect_ratio, near_clip, far_clip); +} diff --git a/tests/gpu-test/src/TestWindow.h b/tests/gpu-test/src/TestWindow.h new file mode 100644 index 0000000000..b7f8df48f5 --- /dev/null +++ b/tests/gpu-test/src/TestWindow.h @@ -0,0 +1,53 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#define DEFERRED_LIGHTING + +class TestWindow : public QWindow { +protected: + QOpenGLContextWrapper _glContext; + QSize _size; + glm::mat4 _projectionMatrix; + bool _aboutToQuit { false }; + +#ifdef DEFERRED_LIGHTING + // Prepare the ShapePipelines + render::ShapePlumberPointer _shapePlumber { std::make_shared() }; + render::RenderContextPointer _renderContext { std::make_shared() }; + gpu::PipelinePointer _opaquePipeline; + model::LightPointer _light { std::make_shared() }; +#endif + + RenderArgs* _renderArgs { new RenderArgs() }; + + TestWindow(); + virtual void initGl(); + virtual void renderFrame() = 0; + +private: + void resizeWindow(const QSize& size); + + void beginFrame(); + void endFrame(); + void draw(); + void resizeEvent(QResizeEvent* ev) override; +}; + diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index b80539b33a..e672fe3c86 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -42,238 +42,64 @@ #include #include +#include #include +#include +#include +#include +#include -#include "unlit_frag.h" -#include "unlit_vert.h" +#include +#include -class RateCounter { - std::vector times; - QElapsedTimer timer; -public: - RateCounter() { - timer.start(); - } +#include +#include +#include +#include +#include +#include +#include +#include - void reset() { - times.clear(); - } +#include "TestWindow.h" +#include "TestInstancedShapes.h" +#include "TestShapes.h" - unsigned int count() const { - return (unsigned int)times.size() - 1; - } - - float elapsed() const { - if (times.size() < 1) { - return 0.0f; - } - float elapsed = *times.rbegin() - *times.begin(); - return elapsed; - } - - void increment() { - times.push_back(timer.elapsed() / 1000.0f); - } - - float rate() const { - if (elapsed() == 0.0f) { - return NAN; - } - return (float) count() / elapsed(); - } -}; - -uint32_t toCompactColor(const glm::vec4& color); +using namespace render; -const char* VERTEX_SHADER = R"SHADER( - -layout(location = 0) in vec4 inPosition; -layout(location = 3) in vec2 inTexCoord0; - -struct TransformObject { - mat4 _model; - mat4 _modelInverse; -}; - -layout(location=15) in ivec2 _drawCallInfo; - -uniform samplerBuffer transformObjectBuffer; - -TransformObject getTransformObject() { - int offset = 8 * _drawCallInfo.x; - TransformObject object; - object._model[0] = texelFetch(transformObjectBuffer, offset); - object._model[1] = texelFetch(transformObjectBuffer, offset + 1); - object._model[2] = texelFetch(transformObjectBuffer, offset + 2); - object._model[3] = texelFetch(transformObjectBuffer, offset + 3); - - object._modelInverse[0] = texelFetch(transformObjectBuffer, offset + 4); - object._modelInverse[1] = texelFetch(transformObjectBuffer, offset + 5); - object._modelInverse[2] = texelFetch(transformObjectBuffer, offset + 6); - object._modelInverse[3] = texelFetch(transformObjectBuffer, offset + 7); - - return object; -} - -struct TransformCamera { - mat4 _view; - mat4 _viewInverse; - mat4 _projectionViewUntranslated; - mat4 _projection; - mat4 _projectionInverse; - vec4 _viewport; -}; - -layout(std140) uniform transformCameraBuffer { - TransformCamera _camera; -}; - -TransformCamera getTransformCamera() { - return _camera; -} - -// the interpolated normal -out vec2 _texCoord0; - -void main(void) { - _texCoord0 = inTexCoord0.st; - - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - { // transformModelToClipPos - vec4 eyeWAPos; - { // _transformModelToEyeWorldAlignedPos - highp mat4 _mv = obj._model; - _mv[3].xyz -= cam._viewInverse[3].xyz; - highp vec4 _eyeWApos = (_mv * inPosition); - eyeWAPos = _eyeWApos; - } - gl_Position = cam._projectionViewUntranslated * eyeWAPos; - } - -})SHADER"; - -const char* FRAGMENT_SHADER = R"SHADER( - -uniform sampler2D originalTexture; - -in vec2 _texCoord0; - -layout(location = 0) out vec4 _fragColor0; - -void main(void) { - //_fragColor0 = vec4(_texCoord0, 0.0, 1.0); - _fragColor0 = texture(originalTexture, _texCoord0); -} -)SHADER"; +using TestBuilder = std::function; +using TestBuilders = std::list; -gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings) { - auto vs = gpu::Shader::createVertex(vertexShaderSrc); - auto fs = gpu::Shader::createPixel(fragmentShaderSrc); - auto shader = gpu::Shader::createProgram(vs, fs); - if (!gpu::Shader::makeProgram(*shader, bindings)) { - printf("Could not compile shader\n"); - exit(-1); - } - return shader; -} +#define INTERACTIVE -float getSeconds(quint64 start = 0) { - auto usecs = usecTimestampNow() - start; - auto msecs = usecs / USECS_PER_MSEC; - float seconds = (float)msecs / MSECS_PER_SECOND; - return seconds; -} - -static const size_t TYPE_COUNT = 4; -static GeometryCache::Shape SHAPE[TYPE_COUNT] = { - GeometryCache::Icosahedron, - GeometryCache::Cube, - GeometryCache::Sphere, - GeometryCache::Tetrahedron, - //GeometryCache::Line, -}; - -gpu::Stream::FormatPointer& getInstancedSolidStreamFormat(); - -// Creates an OpenGL window that renders a simple unlit scene using the gpu library and GeometryCache -// Should eventually get refactored into something that supports multiple gpu backends. -class QTestWindow : public QWindow { - Q_OBJECT - - QOpenGLContextWrapper _qGlContext; - QSize _size; - - gpu::ContextPointer _context; - gpu::PipelinePointer _pipeline; - glm::mat4 _projectionMatrix; - RateCounter fps; - QTime _time; +class MyTestWindow : public TestWindow { + using Parent = TestWindow; + TestBuilders _testBuilders; + GpuTestBase* _currentTest { nullptr }; + size_t _currentTestId { 0 }; + size_t _currentMaxTests { 0 }; glm::mat4 _camera; + QTime _time; -protected: - void renderText(); - -private: - void resizeWindow(const QSize& size) { - _size = size; - } - -public: - QTestWindow() { - setSurfaceType(QSurface::OpenGLSurface); - - QSurfaceFormat format; - // Qt Quick may need a depth and stencil buffer. Always make sure these are available. - format.setDepthBufferSize(16); - format.setStencilBufferSize(8); - setGLFormatVersion(format); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - format.setOption(QSurfaceFormat::DebugContext); - //format.setSwapInterval(0); - - setFormat(format); - - _qGlContext.setFormat(format); - _qGlContext.create(); - - show(); - makeCurrent(); - setupDebugLogger(this); - - gpu::Context::init(); - _context = std::make_shared(); - makeCurrent(); - auto shader = makeShader(unlit_vert, unlit_frag, gpu::Shader::BindingSet{}); - auto state = std::make_shared(); - state->setMultisampleEnable(true); - state->setDepthTest(gpu::State::DepthTest { true }); - _pipeline = gpu::Pipeline::create(shader, state); - - - - // Clear screen - gpu::Batch batch; - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 1.0, 0.0, 0.5, 1.0 }); - _context->render(batch); - - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - resize(QSize(800, 600)); - + void initGl() override { + Parent::initGl(); +#ifdef INTERACTIVE _time.start(); - } - - virtual ~QTestWindow() { +#endif + updateCamera(); + _testBuilders = TestBuilders({ + //[this] { return new TestFbx(_shapePlumber); }, + [] { return new TestShapes(); }, + }); } void updateCamera() { - float t = _time.elapsed() * 1e-4f; + float t = 0; +#ifdef INTERACTIVE + t = _time.elapsed() * 1e-3f; +#endif glm::vec3 unitscale { 1.0f }; glm::vec3 up { 0.0f, 1.0f, 0.0f }; @@ -283,263 +109,64 @@ public: static const vec3 camera_focus(0); static const vec3 camera_up(0, 1, 0); _camera = glm::inverse(glm::lookAt(camera_position, camera_focus, up)); + + ViewFrustum frustum; + frustum.setPosition(camera_position); + frustum.setOrientation(glm::quat_cast(_camera)); + frustum.setProjection(_projectionMatrix); + _renderArgs->setViewFrustum(frustum); } + void renderFrame() override { + updateCamera(); - void drawFloorGrid(gpu::Batch& batch) { - auto geometryCache = DependencyManager::get(); - // Render grid on xz plane (not the optimal way to do things, but w/e) - // Note: GeometryCache::renderGrid will *not* work, as it is apparenly unaffected by batch rotations and renders xy only - static const std::string GRID_INSTANCE = "Grid"; - static auto compactColor1 = toCompactColor(vec4 { 0.35f, 0.25f, 0.15f, 1.0f }); - static auto compactColor2 = toCompactColor(vec4 { 0.15f, 0.25f, 0.35f, 1.0f }); - static std::vector transforms; - static gpu::BufferPointer colorBuffer; - if (!transforms.empty()) { - transforms.reserve(200); - colorBuffer = std::make_shared(); - for (int i = 0; i < 100; ++i) { - { - glm::mat4 transform = glm::translate(mat4(), vec3(0, -1, -50 + i)); - transform = glm::scale(transform, vec3(100, 1, 1)); - transforms.push_back(transform); - colorBuffer->append(compactColor1); - } + while ((!_currentTest || (_currentTestId >= _currentMaxTests)) && !_testBuilders.empty()) { + if (_currentTest) { + delete _currentTest; + _currentTest = nullptr; + } - { - glm::mat4 transform = glm::mat4_cast(quat(vec3(0, PI / 2.0f, 0))); - transform = glm::translate(transform, vec3(0, -1, -50 + i)); - transform = glm::scale(transform, vec3(100, 1, 1)); - transforms.push_back(transform); - colorBuffer->append(compactColor2); - } + _currentTest = _testBuilders.front()(); + _testBuilders.pop_front(); + + if (_currentTest) { + _currentMaxTests = _currentTest->getTestCount(); + _currentTestId = 0; } } - auto pipeline = geometryCache->getSimplePipeline(); - for (auto& transform : transforms) { - batch.setModelTransform(transform); - batch.setupNamedCalls(GRID_INSTANCE, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { - batch.setViewTransform(_camera); - batch.setPipeline(_pipeline); - geometryCache->renderWireShapeInstances(batch, GeometryCache::Line, data.count(), colorBuffer); - }); - } - } - void drawSimpleShapes(gpu::Batch& batch) { - auto geometryCache = DependencyManager::get(); - static const size_t ITEM_COUNT = 1000; - static const float SHAPE_INTERVAL = (PI * 2.0f) / ITEM_COUNT; - static const float ITEM_INTERVAL = SHAPE_INTERVAL / TYPE_COUNT; - - static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; - static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; - static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA }; - - static std::vector transforms; - static std::vector colors; - static gpu::BufferPointer colorBuffer; - static gpu::BufferView colorView; - static gpu::BufferView instanceXfmView; - if (!colorBuffer) { - colorBuffer = std::make_shared(); - - static const float ITEM_RADIUS = 20; - static const vec3 ITEM_TRANSLATION { 0, 0, -ITEM_RADIUS }; - for (size_t i = 0; i < TYPE_COUNT; ++i) { - GeometryCache::Shape shape = SHAPE[i]; - GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; - //indirectCommand._count - float startingInterval = ITEM_INTERVAL * i; - for (size_t j = 0; j < ITEM_COUNT; ++j) { - float theta = j * SHAPE_INTERVAL + startingInterval; - auto transform = glm::rotate(mat4(), theta, Vectors::UP); - transform = glm::rotate(transform, (randFloat() - 0.5f) * PI / 4.0f, Vectors::UNIT_X); - transform = glm::translate(transform, ITEM_TRANSLATION); - transform = glm::scale(transform, vec3(randFloat() / 2.0f + 0.5f)); - transforms.push_back(transform); - auto color = vec4 { randomColorValue(64), randomColorValue(64), randomColorValue(64), 255 }; - color /= 255.0f; - colors.push_back(color); - colorBuffer->append(toCompactColor(color)); - } - } - colorView = gpu::BufferView(colorBuffer, COLOR_ELEMENT); - } - - batch.setViewTransform(_camera); - batch.setPipeline(_pipeline); - batch.setInputFormat(getInstancedSolidStreamFormat()); - for (size_t i = 0; i < TYPE_COUNT; ++i) { - GeometryCache::Shape shape = SHAPE[i]; - GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; - batch.setInputBuffer(gpu::Stream::COLOR, colorView); - for (size_t j = 0; j < ITEM_COUNT; ++j) { - batch.setModelTransform(transforms[j]); - shapeData.draw(batch); - } - } - } - - void drawCenterShape(gpu::Batch& batch) { - // Render unlit cube + sphere - static auto startUsecs = usecTimestampNow(); - float seconds = getSeconds(startUsecs); - seconds /= 4.0f; - batch.setModelTransform(Transform()); - batch._glColor4f(0.8f, 0.25f, 0.25f, 1.0f); - - bool wire = (seconds - floorf(seconds) > 0.5f); - auto geometryCache = DependencyManager::get(); - int shapeIndex = ((int)seconds) % TYPE_COUNT; - if (wire) { - geometryCache->renderWireShape(batch, SHAPE[shapeIndex]); - } else { - geometryCache->renderShape(batch, SHAPE[shapeIndex]); - } - - batch.setModelTransform(Transform().setScale(2.05f)); - batch._glColor4f(1, 1, 1, 1); - geometryCache->renderWireCube(batch); - } - - void drawTerrain(gpu::Batch& batch) { - auto geometryCache = DependencyManager::get(); - static std::once_flag once; - static gpu::BufferPointer vertexBuffer { std::make_shared() }; - static gpu::BufferPointer indexBuffer { std::make_shared() }; - - static gpu::BufferView positionView; - static gpu::BufferView textureView; - static gpu::Stream::FormatPointer vertexFormat { std::make_shared() }; - - static gpu::TexturePointer texture; - static gpu::PipelinePointer pipeline; - std::call_once(once, [&] { - static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec4) * 2; // position, normals, textures - static const uint SHAPE_TEXTURES_OFFSET = sizeof(glm::vec4); - static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; - static const gpu::Element TEXTURE_ELEMENT { gpu::VEC2, gpu::FLOAT, gpu::UV }; - std::vector vertices; - const int MINX = -1000; - const int MAXX = 1000; - - // top - vertices.push_back(vec4(MAXX, 0, MAXX, 1)); - vertices.push_back(vec4(MAXX, MAXX, 0, 0)); - - vertices.push_back(vec4(MAXX, 0, MINX, 1)); - vertices.push_back(vec4(MAXX, 0, 0, 0)); - - vertices.push_back(vec4(MINX, 0, MINX, 1)); - vertices.push_back(vec4(0, 0, 0, 0)); - - vertices.push_back(vec4(MINX, 0, MAXX, 1)); - vertices.push_back(vec4(0, MAXX, 0, 0)); - - vertexBuffer->append(vertices); - indexBuffer->append(std::vector({ 0, 1, 2, 2, 3, 0 })); - - positionView = gpu::BufferView(vertexBuffer, 0, vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, POSITION_ELEMENT); - textureView = gpu::BufferView(vertexBuffer, SHAPE_TEXTURES_OFFSET, vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, TEXTURE_ELEMENT); - texture = DependencyManager::get()->getImageTexture("C:/Users/bdavis/Git/openvr/samples/bin/cube_texture.png"); - // texture = DependencyManager::get()->getImageTexture("H:/test.png"); - //texture = DependencyManager::get()->getImageTexture("H:/crate_blue.fbm/lambert8SG_Normal_OpenGL.png"); - - auto shader = makeShader(VERTEX_SHADER, FRAGMENT_SHADER, gpu::Shader::BindingSet {}); - auto state = std::make_shared(); - state->setMultisampleEnable(false); - state->setDepthTest(gpu::State::DepthTest { true }); - pipeline = gpu::Pipeline::create(shader, state); - vertexFormat->setAttribute(gpu::Stream::POSITION); - vertexFormat->setAttribute(gpu::Stream::TEXCOORD); - }); - - static auto start = usecTimestampNow(); - auto now = usecTimestampNow(); - if ((now - start) > USECS_PER_SECOND * 1) { - start = now; - texture->incremementMinMip(); - } - - batch.setPipeline(pipeline); - batch.setInputBuffer(gpu::Stream::POSITION, positionView); - batch.setInputBuffer(gpu::Stream::TEXCOORD, textureView); - batch.setIndexBuffer(gpu::UINT16, indexBuffer, 0); - batch.setInputFormat(vertexFormat); - - batch.setResourceTexture(0, texture); - batch.setModelTransform(glm::translate(glm::mat4(), vec3(0, -0.1, 0))); - batch.drawIndexed(gpu::TRIANGLES, 6, 0); - - batch.setResourceTexture(0, DependencyManager::get()->getBlueTexture()); - batch.setModelTransform(glm::translate(glm::mat4(), vec3(0, -0.2, 0))); - batch.drawIndexed(gpu::TRIANGLES, 6, 0); - } - - void draw() { - // Attempting to draw before we're visible and have a valid size will - // produce GL errors. - if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) { + if (!_currentTest && _testBuilders.empty()) { + qApp->quit(); return; } - updateCamera(); - makeCurrent(); - - gpu::Batch batch; - batch.resetStages(); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 0.0f, 0.1f, 0.2f, 1.0f }); - batch.clearDepthFramebuffer(1e4); - batch.setViewportTransform({ 0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio() }); - batch.setProjectionTransform(_projectionMatrix); - - batch.setViewTransform(_camera); - batch.setPipeline(_pipeline); - batch.setModelTransform(Transform()); - //drawFloorGrid(batch); - //drawSimpleShapes(batch); - //drawCenterShape(batch); - drawTerrain(batch); - - _context->render(batch); - _qGlContext.swapBuffers(this); - - fps.increment(); - if (fps.elapsed() >= 0.5f) { - qDebug() << "FPS: " << fps.rate(); - fps.reset(); + // Tests might need to wait for resources to download + if (!_currentTest->isReady()) { + return; } - } - - void makeCurrent() { - _qGlContext.makeCurrent(this); - } -protected: - void resizeEvent(QResizeEvent* ev) override { - resizeWindow(ev->size()); - - float fov_degrees = 60.0f; - float aspect_ratio = (float)_size.width() / _size.height(); - float near_clip = 0.1f; - float far_clip = 1000.0f; - _projectionMatrix = glm::perspective(glm::radians(fov_degrees), aspect_ratio, near_clip, far_clip); - } + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.setViewTransform(_camera); + _renderArgs->_batch = &batch; + _currentTest->renderTest(_currentTestId, _renderArgs); + _renderArgs->_batch = nullptr; + }); + +#ifdef INTERACTIVE + +#else + // TODO Capture the current rendered framebuffer and save + // Increment the test ID + ++_currentTestId; +#endif + } }; + int main(int argc, char** argv) { QGuiApplication app(argc, argv); - QTestWindow window; - auto timer = new QTimer(&app); - timer->setInterval(0); - app.connect(timer, &QTimer::timeout, &app, [&] { - window.draw(); - }); - timer->start(); + MyTestWindow window; app.exec(); return 0; } -#include "main.moc" - From 11de8a52b268b723e36ea9d88767e7f8dea2d755 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 22 May 2016 22:52:56 -0700 Subject: [PATCH 0190/1237] fix broken procedural skybox when leaving and re-entering --- libraries/procedural/src/procedural/Procedural.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index cedf76b37a..781b508bc7 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -100,10 +100,6 @@ bool Procedural::parseUrl(const QUrl& shaderUrl) { return false; } - if (_shaderUrl == shaderUrl) { - return true; - } - _shaderUrl = shaderUrl; _shaderDirty = true; From 1f2f9da01911001914953503bfae11eda6d9e3d7 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 22 May 2016 23:34:45 -0700 Subject: [PATCH 0191/1237] fix keys getting stuck --- libraries/ui/src/OffscreenUi.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 4fb25e3e3f..dfd9056703 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -590,6 +590,7 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { // let the parent class do it's work bool result = OffscreenQmlSurface::eventFilter(originalDestination, event); + // Check if this is a key press/release event that might need special attention auto type = event->type(); if (type != QEvent::KeyPress && type != QEvent::KeyRelease) { @@ -597,7 +598,8 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { } QKeyEvent* keyEvent = dynamic_cast(event); - bool& pressed = _pressedKeys[keyEvent->key()]; + auto key = keyEvent->key(); + bool& pressed = _pressedKeys[key]; // Keep track of which key press events the QML has accepted if (result && QEvent::KeyPress == type) { @@ -607,7 +609,7 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { // QML input elements absorb key press, but apparently not key release. // therefore we want to ensure that key release events for key presses that were // accepted by the QML layer are suppressed - if (!result && type == QEvent::KeyRelease && pressed) { + if (type == QEvent::KeyRelease && pressed) { pressed = false; return true; } From bd6f6d2eef4916646334b16878bf8cc565c3ce7d Mon Sep 17 00:00:00 2001 From: Geenz Date: Mon, 23 May 2016 09:53:22 -0400 Subject: [PATCH 0192/1237] Typo. Not quite sure how this got here. --- libraries/render-utils/src/spot_light.slf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 63e87e95c0..4191ba3f63 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -74,7 +74,7 @@ void main(void) { vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance4); + float radialAttenuation = evalLightAttenuation(light, fragLightDistance); float angularAttenuation = evalLightSpotAttenuation(light, cosSpotAngle); // Final Lighting color From 5fe01acaa84c30d284d0033b32b5496e6a3ad87e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 23 May 2016 10:03:13 -0700 Subject: [PATCH 0193/1237] Added more comments to AvatarDataPacket section. --- libraries/avatars/src/AvatarData.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 7a20f24da8..b74fc8de2e 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -61,12 +61,14 @@ namespace AvatarDataPacket { } PACKED_END; const size_t HEADER_SIZE = 49; + // only present if HAS_REFERENTIAL flag is set in header.flags PACKED_BEGIN struct ParentInfo { uint8_t parentUUID[16]; // rfc 4122 encoded uint16_t parentJointIndex; } PACKED_END; const size_t PARENT_INFO_SIZE = 18; + // only present if IS_FACESHIFT_CONNECTED flag is set in header.flags PACKED_BEGIN struct FaceTrackerInfo { float leftEyeBlink; float rightEyeBlink; @@ -76,6 +78,17 @@ namespace AvatarDataPacket { // float blendshapeCoefficients[numBlendshapeCoefficients]; } PACKED_END; const size_t FACE_TRACKER_INFO_SIZE = 17; + + // variable length structure follows + /* + struct JointData { + uint8_t numJoints; + uint8_t rotationValidityBits[ceil(numJoints / 8)]; // one bit per joint, if true then a compressed rotation follows. + SixByteQuat rotation[numValidRotations]; // encodeded and compressed by packOrientationQuatToSixBytes() + uint8_t translationValidityBits[ceil(numJoints / 8)]; // one bit per joint, if true then a compressed translation follows. + SixByteTrans translation[numValidTranslations]; // encodeded and compressed by packFloatVec3ToSignedTwoByteFixed() + }; + */ } #define ASSERT(COND) do { if (!(COND)) { int* bad = nullptr; *bad = 0xbad; } } while(0) From 9aad38b2c2582a73d72ad0ad22740d07fe0e7ab4 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 23 May 2016 10:39:39 -0700 Subject: [PATCH 0194/1237] merge fix --- libraries/avatars/src/AvatarData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 7d571a826f..61ee649273 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -296,7 +296,7 @@ public: QUrl skeletonModelURL; QVector attachmentData; QString displayName; - AvatarEntityMap avatarEntityMap; + AvatarEntityMap avatarEntityData; }; static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); From 4342a071086731f25aa1f936acb3831e2d7043fd Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 23 May 2016 11:11:37 -0700 Subject: [PATCH 0195/1237] Updated AvatarDataPacket section with sequence number info. --- libraries/avatars/src/AvatarData.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 1ca4b1beb1..cc61036915 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -49,6 +49,7 @@ const glm::vec3 DEFAULT_LOCAL_AABOX_SCALE(1.0f); const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData"; namespace AvatarDataPacket { + // NOTE: AvatarDataPackets start with a uint16_t sequence number that is not reflected in the Header structure. PACKED_BEGIN struct Header { float position[3]; // skeletal model's position From 266dfcd8e941cc68e134274c7e5cb65b047d021f Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 23 May 2016 11:45:55 -0700 Subject: [PATCH 0196/1237] new lightswitches and cleanup old code, also more blocky stuff --- .../DomainContent/Home/blocky/blocky.js | 19 ++ .../Home/switches/livingRoomFan.js | 28 ++- .../Home/switches/livingRoomLightDown.js | 197 ------------------ .../Home/switches/livingRoomLightUp.js | 195 ----------------- ...gRoomDiscLights.js => livingRoomLights.js} | 24 ++- 5 files changed, 57 insertions(+), 406 deletions(-) delete mode 100644 unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js delete mode 100644 unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js rename unpublishedScripts/DomainContent/Home/switches/{livingRoomDiscLights.js => livingRoomLights.js} (91%) diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js index 0c4b252a21..948ccd5e04 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/blocky.js +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -171,8 +171,27 @@ var TARGET_BLOCKS_POSITION = { } }, + findBlocks: function() { + var found = []; + var results = Entities.findEntities(this.position, 10); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + print('got result props') + if (properties.name.indexOf('blocky_block') > -1) { + found.push(result); + } + }); + return found; + }, + cleanup: function() { print('BLOCKY cleanup'); + var blocks = this.findBlocks(); + print('BLOCKY cleanup2') + blocks.forEach(function(block) { + Entities.deleteEntity(block); + }) + print('BLOCKY after find and delete') this.targetBlocks.forEach(function(block) { Entities.deleteEntity(block); }); diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js index fa8d0968a0..09d0fb97c8 100644 --- a/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js +++ b/unpublishedScripts/DomainContent/Home/switches/livingRoomFan.js @@ -9,9 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { - var ON_MODEL_URL="atp:/switches/fanswitch_on.fbx"; - var OFF_MODEL_URL="atp:/switches/fanswitch_off.fbx"; +(function() { var FAN_SOUND_ENTITY_NAME = "home_sfx_ceiling_fan" var SEARCH_RADIUS = 100; @@ -131,14 +129,21 @@ if (this._switch.state === 'off') { this.fanRotationOn(); this.fanSoundOn(); - + setEntityCustomData('home-switch', this.entityID, { state: 'on' }); Entities.editEntity(this.entityID, { - modelURL: ON_MODEL_URL - }); + "animation": { + "currentFrame": 1, + "firstFrame": 1, + "hold": 1, + "lastFrame": 2, + "url": "atp:/switches/fanswitch.fbx" + }, + }) + } else { this.fanRotationOff(); @@ -147,10 +152,17 @@ setEntityCustomData('home-switch', this.entityID, { state: 'off' }); - Entities.editEntity(this.entityID, { - modelURL: OFF_MODEL_URL + "animation": { + "currentFrame": 3, + "firstFrame": 3, + "hold": 1, + "lastFrame": 4, + "url": "atp:/switches/fanswitch.fbx" + }, }) + + } diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js deleted file mode 100644 index 86c9a70716..0000000000 --- a/unpublishedScripts/DomainContent/Home/switches/livingRoomLightDown.js +++ /dev/null @@ -1,197 +0,0 @@ -// -// -// Created by The Content Team 4/10/216 -// Copyright 2016 High Fidelity, Inc. -// -// this finds lights and toggles thier visibility, and flips the emissive texture of some light models -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function() { - - var SEARCH_RADIUS = 100; - - var _this; - var utilitiesScript = Script.resolvePath('../utils.js'); - Script.include(utilitiesScript); - Switch = function() { - _this = this; - this.switchSound = SoundCache.getSound("atp:/switches/lamp_switch_2.wav"); - }; - - Switch.prototype = { - prefix: 'hifi-home-living-room-disc-', - clickReleaseOnEntity: function(entityID, mouseEvent) { - if (!mouseEvent.isLeftButton) { - return; - } - this.toggleLights(); - }, - - startNearTrigger: function() { - this.toggleLights(); - }, - - modelEmitOn: function(glowDisc) { - var data = { - "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", - "Tex.CeilingLight.Emit": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-On-Diffuse.jpg", - "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" - } - - Entities.editEntity(glowDisc, { - textures: JSON.stringify(data) - }) - }, - - modelEmitOff: function(glowDisc) { - var data = { - "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", - "Tex.CeilingLight.Emit": "", - "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" - } - - Entities.editEntity(glowDisc, { - textures: JSON.stringify(data) - - }) - }, - - masterLightOn: function(masterLight) { - Entities.editEntity(masterLight, { - visible: true - }); - }, - - masterLightOff: function(masterLight) { - Entities.editEntity(masterLight, { - visible: false - }); - }, - - glowLightOn: function(glowLight) { - Entities.editEntity(glowLight, { - visible: true - }); - }, - - glowLightOff: function(glowLight) { - Entities.editEntity(glowLight, { - visible: false - }); - }, - - findGlowLights: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === _this.prefix + "light-glow") { - found.push(result); - } - }); - return found; - }, - - findMasterLights: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === _this.prefix + "light-master") { - found.push(result); - } - }); - return found; - }, - - findEmitModels: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === _this.prefix + "light-model") { - found.push(result); - } - }); - return found; - }, - - findSwitch: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === "hifi-home-living-room-light-switch-up") { - found.push(result); - } - }); - return found; - }, - - toggleLights: function() { - - var glowLights = this.findGlowLights(); - var masterLights = this.findMasterLights(); - var emitModels = this.findEmitModels(); - - glowLights.forEach(function(glowLight) { - // _this.glowLightOff(glowLight); - }); - - masterLights.forEach(function(masterLight) { - _this.masterLightOff(masterLight); - }); - - emitModels.forEach(function(emitModel) { - _this.modelEmitOff(emitModel); - }); - - - Audio.playSound(this.switchSound, { - volume: 0.5, - position: this.position - }); - - Entities.editEntity(this.entityID, { - position: { - x: 1103.9894, - y: 460.6867, - z: -75.5650 - } - }); - - - var otherSwitch = this.findSwitch(); - - print('other switch:: ' + otherSwitch) - - - var success = Entities.editEntity(otherSwitch.toString(), { - position: { - x: 1103.5823, - y: 460.6867, - z: -75.6313 - } - }) - print('edit success ' + success) - }, - - preload: function(entityID) { - this.entityID = entityID; - setEntityCustomData('grabbableKey', this.entityID, { - wantsTrigger: true - }); - - var properties = Entities.getEntityProperties(this.entityID); - - //The light switch is static, so just cache its position once - this.position = Entities.getEntityProperties(this.entityID, "position").position; - } - }; - - // entity scripts always need to return a newly constructed object of our type - return new Switch(); -}); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js deleted file mode 100644 index 1be08a6fec..0000000000 --- a/unpublishedScripts/DomainContent/Home/switches/livingRoomLightUp.js +++ /dev/null @@ -1,195 +0,0 @@ -// -// -// Created by The Content Team 4/10/216 -// Copyright 2016 High Fidelity, Inc. -// -// this finds lights and toggles thier visibility, and flips the emissive texture of some light models -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function() { - - var SEARCH_RADIUS = 100; - - var _this; - var utilitiesScript = Script.resolvePath('../utils.js'); - Script.include(utilitiesScript); - Switch = function() { - _this = this; - this.switchSound = SoundCache.getSound("atp:/switches/lamp_switch_2.wav"); - }; - - Switch.prototype = { - prefix: 'hifi-home-living-room-disc-', - clickReleaseOnEntity: function(entityID, mouseEvent) { - if (!mouseEvent.isLeftButton) { - return; - } - this.toggleLights(); - }, - - startNearTrigger: function() { - this.toggleLights(); - }, - - modelEmitOn: function(glowDisc) { - var data = { - "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", - "Tex.CeilingLight.Emit": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-On-Diffuse.jpg", - "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" - } - - Entities.editEntity(glowDisc, { - textures: JSON.stringify(data) - }) - }, - - modelEmitOff: function(glowDisc) { - var data = { - "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", - "Tex.CeilingLight.Emit": "", - "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" - } - - Entities.editEntity(glowDisc, { - textures: JSON.stringify(data) - - }) - }, - - masterLightOn: function(masterLight) { - Entities.editEntity(masterLight, { - visible: true - }); - }, - - masterLightOff: function(masterLight) { - Entities.editEntity(masterLight, { - visible: false - }); - }, - - glowLightOn: function(glowLight) { - Entities.editEntity(glowLight, { - visible: true - }); - }, - - glowLightOff: function(glowLight) { - Entities.editEntity(glowLight, { - visible: false - }); - }, - - findGlowLights: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === _this.prefix + "light-glow") { - found.push(result); - } - }); - return found; - }, - - findMasterLights: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === _this.prefix + "light-master") { - found.push(result); - } - }); - return found; - }, - - findEmitModels: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === _this.prefix + "light-model") { - found.push(result); - } - }); - return found; - }, - - findSwitch: function() { - var found = []; - var results = Entities.findEntities(this.position, SEARCH_RADIUS); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - if (properties.name === "hifi-home-living-room-light-switch-down") { - found.push(result); - } - }); - return found; - }, - - toggleLights: function() { - - var glowLights = this.findGlowLights(); - var masterLights = this.findMasterLights(); - var emitModels = this.findEmitModels(); - - glowLights.forEach(function(glowLight) { - // _this.glowLightOff(glowLight); - }); - - masterLights.forEach(function(masterLight) { - _this.masterLightOff(masterLight); - }); - - emitModels.forEach(function(emitModel) { - _this.modelEmitOff(emitModel); - }); - - - Audio.playSound(this.switchSound, { - volume: 0.5, - position: this.position - }); - - Entities.editEntity(this.entityID, { - position: { - x: 1103.9894, - y: 460.6867, - z: -75.5650 - } - }); - - var otherSwitch = this.findSwitch(); - - print('other switch:: ' + otherSwitch) - - var success = Entities.editEntity(otherSwitch.toString(), { - position: { - x: 1103.5823, - y: 460.6867, - z: -75.6313 - } - }) - print('edit success ' + success) - }, - - preload: function(entityID) { - this.entityID = entityID; - setEntityCustomData('grabbableKey', this.entityID, { - wantsTrigger: true - }); - - var properties = Entities.getEntityProperties(this.entityID); - - //The light switch is static, so just cache its position once - this.position = Entities.getEntityProperties(this.entityID, "position").position; - } - }; - - // entity scripts always need to return a newly constructed object of our type - return new Switch(); -}); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/switches/livingRoomDiscLights.js b/unpublishedScripts/DomainContent/Home/switches/livingRoomLights.js similarity index 91% rename from unpublishedScripts/DomainContent/Home/switches/livingRoomDiscLights.js rename to unpublishedScripts/DomainContent/Home/switches/livingRoomLights.js index 6fb2ea538a..2c6208481f 100644 --- a/unpublishedScripts/DomainContent/Home/switches/livingRoomDiscLights.js +++ b/unpublishedScripts/DomainContent/Home/switches/livingRoomLights.js @@ -10,8 +10,7 @@ // (function() { - var ON_MODEL_URL = "atp:/switches/lightswitch_on.fbx"; - var OFF_MODEL_URL = "atp:/switches/lightswitch_off.fbx"; + var SEARCH_RADIUS = 100; var _this; @@ -40,7 +39,7 @@ "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", "Tex.CeilingLight.Emit": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-On-Diffuse.jpg", "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" - } + }; Entities.editEntity(glowDisc, { textures: JSON.stringify(data) @@ -52,7 +51,7 @@ "Metal-brushed-light.jpg": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/Metal-brushed-light.jpg", "Tex.CeilingLight.Emit": "", "TexCeilingLight.Diffuse": "atp:/models/Lights-Living-Room-2.fbx/Lights-Living-Room-2.fbm/CielingLight-Base.jpg" - } + }; Entities.editEntity(glowDisc, { textures: JSON.stringify(data) @@ -143,8 +142,15 @@ setEntityCustomData('home-switch', _this.entityID, { state: 'on' }); + Entities.editEntity(this.entityID, { - modelURL: ON_MODEL_URL + "animation": { + "currentFrame": 1, + "firstFrame": 1, + "hold": 1, + "lastFrame": 2, + "url": "atp:/switches/lightswitch.fbx" + }, }); } else { @@ -162,7 +168,13 @@ }); Entities.editEntity(this.entityID, { - modelURL: OFF_MODEL_URL + "animation": { + "currentFrame": 3, + "firstFrame": 3, + "hold": 1, + "lastFrame": 4, + "url": "atp:/switches/lightswitch.fbx" + }, }); } From 099a675a186e4f91c88829d806ea38963bba5604 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 24 May 2016 08:03:29 +1200 Subject: [PATCH 0197/1237] Remove extra logging --- interface/resources/qml/dialogs/FileDialog.qml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index b00ee76b5e..5cd972a38f 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -130,8 +130,6 @@ ModalWindow { choices = [], i, length; - console.log("####### folder parts: " + JSON.stringify(folders)); - if (folders[folders.length - 1] === "") { folders.pop(); } @@ -160,9 +158,6 @@ ModalWindow { onLastValidFolderChanged: { var folder = d.capitalizeDrive(lastValidFolder); - - console.log("####### lastValidFolder: " + folder); - calculatePathChoices(folder); } From fdcd667d3c4f54c8958de1c89c9007adf8130582 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 23 May 2016 13:33:42 -0700 Subject: [PATCH 0198/1237] Fix hand controller pointer after input changes. --- interface/resources/controllers/hydra.json | 8 +- interface/resources/controllers/vive.json | 4 +- .../controllers/handControllerPointer.js | 112 ++++++------------ 3 files changed, 46 insertions(+), 78 deletions(-) diff --git a/interface/resources/controllers/hydra.json b/interface/resources/controllers/hydra.json index 42237033af..8233685763 100644 --- a/interface/resources/controllers/hydra.json +++ b/interface/resources/controllers/hydra.json @@ -16,8 +16,12 @@ { "from": "Hydra.L0", "to": "Standard.Back" }, { "from": "Hydra.R0", "to": "Standard.Start" }, - { "from": [ "Hydra.L1", "Hydra.L2", "Hydra.L3", "Hydra.L4" ], "to": "Standard.LeftPrimaryThumb" }, - { "from": [ "Hydra.R1", "Hydra.R2", "Hydra.R3", "Hydra.R4" ], "to": "Standard.RightPrimaryThumb" }, + { "from": [ "Hydra.L1", "Hydra.L2" ], "to": "Standard.LeftPrimaryThumb" }, + { "from": [ "Hydra.R1", "Hydra.R2" ], "to": "Standard.RightPrimaryThumb" }, + { "from": [ "Hydra.L3" ], "to": "Standard.L3" }, + { "from": [ "Hydra.R3" ], "to": "Standard.R3" }, + { "from": [ "Hydra.R4" ], "to": "Standard.RightSecondaryThumb" }, + { "from": [ "Hydra.L4" ], "to": "Standard.LeftSecondaryThumb" }, { "from": "Hydra.LeftHand", "to": "Standard.LeftHand" }, { "from": "Hydra.RightHand", "to": "Standard.RightHand" } diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 4085d71c27..fec93c9132 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -17,8 +17,8 @@ { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, - { "from": "Vive.LeftApplicationMenu", "to": "Standard.Back" }, - { "from": "Vive.RightApplicationMenu", "to": "Standard.Start" }, + { "from": "Vive.LeftApplicationMenu", "to": "Standard.LeftSecondaryThumb" }, + { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, { "from": "Vive.RightHand", "to": "Standard.RightHand" } diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index f4e4492a88..e7be3af5dd 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -22,10 +22,7 @@ // (For now, the thumb buttons on both controllers are always on.) // When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. // Otherwise, the active hand controller shows a red ball where a click will act. -// -// Bugs: -// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.) -// While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not. + // UTILITIES ------------- @@ -270,75 +267,43 @@ function toggleHand() { } // Create clickMappings as needed, on demand. -var clickMappings = {}, clickMapping, clickMapToggle; -var hardware; // undefined -function checkHardware() { - var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive' : null); // not undefined - if (hardware === newHardware) { - return; - } - print('Setting mapping for new controller hardware:', newHardware); - if (clickMapToggle) { - clickMapToggle.setState(false); - } - hardware = newHardware; - if (clickMappings[hardware]) { - clickMapping = clickMappings[hardware]; - } else { - clickMapping = Controller.newMapping(Script.resolvePath('') + '-click-' + hardware); - Script.scriptEnding.connect(clickMapping.disable); - function mapToAction(button, action) { - clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]); - } - function makeHandToggle(button, hand, optionalWhen) { - var whenThunk = optionalWhen || function () { - return true; - }; - function maybeToggle() { - if (activeHand !== Controller.Standard[hand]) { - toggleHand(); - } +var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); +Script.scriptEnding.connect(clickMapping.disable); - } - clickMapping.from(Controller.Hardware[hardware][button]).peek().when(whenThunk).to(maybeToggle); - } - function makeViveWhen(click, x, y) { - var viveClick = Controller.Hardware.Vive[click], - viveX = Controller.Standard[x], // Standard after filtering by mapping - viveY = Controller.Standard[y]; - return function () { - var clickValue = Controller.getValue(viveClick); - var xValue = Controller.getValue(viveX); - var yValue = Controller.getValue(viveY); - return clickValue && !xValue && !yValue; - }; - } - switch (hardware) { - case 'Hydra': - makeHandToggle('R3', 'RightHand'); - makeHandToggle('L3', 'LeftHand'); - - mapToAction('R3', 'ReticleClick'); - mapToAction('L3', 'ReticleClick'); - mapToAction('R4', 'ContextMenu'); - mapToAction('L4', 'ContextMenu'); - break; - case 'Vive': - // When touchpad click is NOT treated as movement, treat as left click - makeHandToggle('RS', 'RightHand', makeViveWhen('RS', 'RX', 'RY')); - makeHandToggle('LS', 'LeftHand', makeViveWhen('LS', 'LX', 'LY')); - clickMapping.from(Controller.Hardware.Vive.RS).when(makeViveWhen('RS', 'RX', 'RY')).to(Controller.Actions.ReticleClick); - clickMapping.from(Controller.Hardware.Vive.LS).when(makeViveWhen('LS', 'LX', 'LY')).to(Controller.Actions.ReticleClick); - mapToAction('RightApplicationMenu', 'ContextMenu'); - mapToAction('LeftApplicationMenu', 'ContextMenu'); - break; - } - clickMappings[hardware] = clickMapping; - } - clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable); - clickMapToggle.setState(true); +// Move these to vive.json +function makeCenterClickWhen(click, x, y) { + var clickKey = Controller.Standard[click], + xKey = Controller.Standard[x], // Standard after filtering by mapping + yKey = Controller.Standard[y]; + return function () { + var clickValue = Controller.getValue(clickKey); + var xValue = Controller.getValue(xKey); + var yValue = Controller.getValue(yKey); + var answer = clickValue && !xValue && !yValue; + return answer; + }; } -checkHardware(); +if (Controller.Hardware.Vive) { + clickMapping.from(Controller.Hardware.Vive.RS).when(makeCenterClickWhen('RS', 'RX', 'RY')).to(Controller.Standard.R3); + clickMapping.from(Controller.Hardware.Vive.LS).when(makeCenterClickWhen('LS', 'LX', 'LY')).to(Controller.Standard.L3); +} + + +clickMapping.from(Controller.Standard.R3).peek().to(Controller.Actions.ReticleClick); +clickMapping.from(Controller.Standard.L3).peek().to(Controller.Actions.ReticleClick); +clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); +clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); +clickMapping.from(Controller.Standard.R3).peek().to(function (on) { + if (on && (activeHand !== Controller.Standard.RightHand)) { + toggleHand(); + } +}); +clickMapping.from(Controller.Standard.L3).peek().to(function (on) { + if (on && (activeHand !== Controller.Standard.LeftHand)) { + toggleHand(); + } +}); +clickMapping.enable(); // VISUAL AID ----------- // Same properties as handControllerGrab search sphere @@ -415,8 +380,8 @@ function update() { return turnOffVisualization(); } var controllerPose = Controller.getPoseValue(activeHand); - // Vive is effectively invalid when not in HMD - if (!controllerPose.valid || ((hardware === 'Vive') && !HMD.active)) { + // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) + if (!controllerPose.valid) { return turnOffVisualization(); } // Controller is cradled. var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), @@ -458,7 +423,6 @@ Script.scriptEnding.connect(function () { var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds function checkSettings() { updateFieldOfView(); - checkHardware(); } checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); From 7cf56401f5ccd1a80792b0393943bdacd87d6624 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 23 May 2016 15:05:44 -0700 Subject: [PATCH 0199/1237] teleport sparkles, dressing room switch --- ...oomDiscLights.js => dressingRoomLights.js} | 34 ++++++---- .../Home/teleport/downsparkle.json | 65 +++++++++++++++++++ .../Home/teleport/downsparkler.json | 63 ++++++++++++++++++ .../Home/teleport/upsparkle.json | 63 ++++++++++++++++++ .../Home/teleport/upsparkler.json | 44 +++++++++++++ 5 files changed, 255 insertions(+), 14 deletions(-) rename unpublishedScripts/DomainContent/Home/switches/{dressingRoomDiscLights.js => dressingRoomLights.js} (89%) create mode 100644 unpublishedScripts/DomainContent/Home/teleport/downsparkle.json create mode 100644 unpublishedScripts/DomainContent/Home/teleport/downsparkler.json create mode 100644 unpublishedScripts/DomainContent/Home/teleport/upsparkle.json create mode 100644 unpublishedScripts/DomainContent/Home/teleport/upsparkler.json diff --git a/unpublishedScripts/DomainContent/Home/switches/dressingRoomDiscLights.js b/unpublishedScripts/DomainContent/Home/switches/dressingRoomLights.js similarity index 89% rename from unpublishedScripts/DomainContent/Home/switches/dressingRoomDiscLights.js rename to unpublishedScripts/DomainContent/Home/switches/dressingRoomLights.js index 6e2c4908b9..a44fa8f2f3 100644 --- a/unpublishedScripts/DomainContent/Home/switches/dressingRoomDiscLights.js +++ b/unpublishedScripts/DomainContent/Home/switches/dressingRoomLights.js @@ -147,6 +147,16 @@ setEntityCustomData('home-switch', _this.entityID, { state: 'on' }); + + Entities.editEntity(this.entityID, { + "animation": { + "currentFrame": 1, + "firstFrame": 1, + "hold": 1, + "lastFrame": 2, + "url": "atp:/switches/lightswitch.fbx" + }, + }); } else { glowLights.forEach(function(glowLight) { @@ -161,9 +171,18 @@ setEntityCustomData('home-switch', this.entityID, { state: 'off' }); + + Entities.editEntity(this.entityID, { + "animation": { + "currentFrame": 3, + "firstFrame": 3, + "hold": 1, + "lastFrame": 4, + "url": "atp:/switches/lightswitch.fbx" + }, + }); } - this.flipSwitch(); Audio.playSound(this.switchSound, { volume: 0.5, position: this.position @@ -171,20 +190,7 @@ }, - flipSwitch: function() { - var rotation = Entities.getEntityProperties(this.entityID, "rotation").rotation; - var axis = { - x: 0, - y: 1, - z: 0 - }; - var dQ = Quat.angleAxis(180, axis); - rotation = Quat.multiply(rotation, dQ); - Entities.editEntity(this.entityID, { - rotation: rotation - }); - }, preload: function(entityID) { this.entityID = entityID; diff --git a/unpublishedScripts/DomainContent/Home/teleport/downsparkle.json b/unpublishedScripts/DomainContent/Home/teleport/downsparkle.json new file mode 100644 index 0000000000..8682a4ed4c --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/teleport/downsparkle.json @@ -0,0 +1,65 @@ +down sparkle + +{ + "color": { + "red": "#", + "green": "c", + "blue": "f" + }, + "isEmitting": 1, + "maxParticles": 1000, + "lifespan": 2, + "emitRate": 10, + "emitSpeed": 0.1, + "speedSpread": 0.6, + "emitOrientation": { + "x": 0.8, + "y": 0, + "z": 0, + "w": 0.7071068286895752 + }, + "emitDimensions": { + "x": 0, + "y": 0, + "z": 0 + }, + "polarStart": 0, + "polarFinish": 0, + "azimuthStart": -3.1415927410125732, + "azimuthFinish": 3.1415927410125732, + "emitAcceleration": { + "x": 0, + "y": -1, + "z": 0 + }, + "accelerationSpread": { + "x": 0, + "y": 1, + "z": 0 + }, + "particleRadius": 0.02500000037252903, + "radiusSpread": 0, + "radiusStart": 0.079, + "radiusFinish": 0.053, + "colorSpread": { + "red": "#", + "green": "e", + "blue": "8" + }, + "colorStart": { + "red": 255, + "green": 255, + "blue": 255 + }, + "colorFinish": { + "red": "#", + "green": "d", + "blue": "4" + }, + "alpha": 1, + "alphaSpread": 0, + "alphaStart": 1, + "alphaFinish": 0, + "emitterShouldTrail": 1, + "textures": "atp:/teleport/sparkle1.png" +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/teleport/downsparkler.json b/unpublishedScripts/DomainContent/Home/teleport/downsparkler.json new file mode 100644 index 0000000000..09ec79910a --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/teleport/downsparkler.json @@ -0,0 +1,63 @@ +{ + "Entities": [ + { + "accelerationSpread": { + "x": 0, + "y": 1, + "z": 0 + }, + "color": { + "blue": 207, + "green": 207, + "red": 207 + }, + "colorFinish": { + "blue": 207, + "green": 207, + "red": 207 + }, + "colorSpread": { + "blue": 125, + "green": 125, + "red": 232 + }, + "colorStart": { + "blue": 207, + "green": 207, + "red": 207 + }, + "created": "2016-05-23T20:41:38Z", + "dimensions": { + "x": 2.6566545963287354, + "y": 2.6566545963287354, + "z": 2.6566545963287354 + }, + "emitAcceleration": { + "x": 0, + "y": -1, + "z": 0 + }, + "emitOrientation": { + "w": 0.66226339340209961, + "x": 0.74927115440368652, + "y": -1.5258940038620494e-05, + "z": -1.5258940038620494e-05 + }, + "emitRate": 10, + "emitSpeed": 0.10000000149011612, + "id": "{e700e0a1-026a-4ebd-8609-6068b32df14e}", + "lifespan": 2, + "name": "home_particle_teleport_sparkle_down", + "queryAACube": { + "scale": 4.6014609336853027, + "x": -2.3007304668426514, + "y": -2.3007304668426514, + "z": -2.3007304668426514 + }, + "speedSpread": 0.60000002384185791, + "textures": "atp:/teleport/sparkle1.png", + "type": "ParticleEffect" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/teleport/upsparkle.json b/unpublishedScripts/DomainContent/Home/teleport/upsparkle.json new file mode 100644 index 0000000000..508366d293 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/teleport/upsparkle.json @@ -0,0 +1,63 @@ +{ + "color": { + "red": 255, + "green": 255, + "blue": 255 + }, + "isEmitting": 1, + "maxParticles": 210, + "lifespan": 3.6, + "emitRate": 11, + "emitSpeed": 0.5, + "speedSpread": 0.8, + "emitOrientation": { + "x": -1, + "y": -0.0000152587890625, + "z": -0.0000152587890625, + "w": 1 + }, + "emitDimensions": { + "x": 0, + "y": 0, + "z": 0 + }, + "polarStart": 0, + "polarFinish": 0, + "azimuthStart": -3.1415927410125732, + "azimuthFinish": 3.1415927410125732, + "emitAcceleration": { + "x": 0, + "y": 0.2, + "z": 0 + }, + "accelerationSpread": { + "x": 0, + "y": 0.30000000000000004, + "z": 0 + }, + "particleRadius": 0.02500000037252903, + "radiusSpread": 0, + "radiusStart": 0.013, + "radiusFinish": 0.014, + "colorSpread": { + "red": 0, + "green": 0, + "blue": 0 + }, + "colorStart": { + "red": 255, + "green": 255, + "blue": 255 + }, + "colorFinish": { + "red": 255, + "green": 255, + "blue": 255 + }, + "alpha": 1, + "alphaSpread": 0, + "alphaStart": 1, + "alphaFinish": 0, + "emitterShouldTrail": 0, + "textures": "atp:/teleport/sparkle2.png" +} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/teleport/upsparkler.json b/unpublishedScripts/DomainContent/Home/teleport/upsparkler.json new file mode 100644 index 0000000000..a573b3deae --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/teleport/upsparkler.json @@ -0,0 +1,44 @@ +{ + "Entities": [ + { + "accelerationSpread": { + "x": 0, + "y": 0.30000001192092896, + "z": 0 + }, + "created": "2016-05-23T20:56:55Z", + "dimensions": { + "x": 15.000479698181152, + "y": 15.000479698181152, + "z": 15.000479698181152 + }, + "emitAcceleration": { + "x": 0, + "y": 0.20000000298023224, + "z": 0 + }, + "emitOrientation": { + "w": 0.7070763111114502, + "x": -0.70713728666305542, + "y": -1.5258539860951714e-05, + "z": -1.5258539860951714e-05 + }, + "emitRate": 11, + "emitSpeed": 0.5, + "id": "{57104b1c-01a9-4f2b-b7bf-9eb406e8d78b}", + "lifespan": 3.5999999046325684, + "maxParticles": 210, + "name": "home_teleport_sparkler_up", + "queryAACube": { + "scale": 25.981592178344727, + "x": -12.990796089172363, + "y": -12.990796089172363, + "z": -12.990796089172363 + }, + "speedSpread": 0.80000001192092896, + "textures": "atp:/teleport/sparkle2.png", + "type": "ParticleEffect" + } + ], + "Version": 57 +} From 097b16aadc5e0f135e2df0a93a6bf7a939abccbe Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 23 May 2016 15:40:29 -0700 Subject: [PATCH 0200/1237] Fix for sensor reset on oculus. --- plugins/oculus/src/OculusBaseDisplayPlugin.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index a92c5b5b22..3b6545ae96 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -13,6 +13,8 @@ void OculusBaseDisplayPlugin::resetSensors() { ovr_RecenterTrackingOrigin(_session); + + _currentRenderFrameInfo.renderPose = glm::mat4(); // identity } void OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { From f82c3ba4f2e39e106a634d637c9ebbcbc585cb4a Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 23 May 2016 16:30:05 -0700 Subject: [PATCH 0201/1237] Revert "OpenVRDispalyPlugin: fix one-frame lag in resetSensors." This reverts commit 8381e74fb3478750362de176aa0710b369469c0f. --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 1f6b11f862..38719fdca5 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -122,19 +122,7 @@ void OpenVrDisplayPlugin::customizeContext() { void OpenVrDisplayPlugin::resetSensors() { Lock lock(_poseMutex); glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); - - glm::mat4 oldSensorResetMat = _sensorResetMat; _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); - - glm::mat4 undoRedoMat = _sensorResetMat * glm::inverse(oldSensorResetMat); - - // update the device poses, by undoing the previous sensorResetMatrix then applying the new one. - for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { - _trackedDevicePoseMat4[i] = undoRedoMat * _trackedDevicePoseMat4[i]; - _trackedDeviceLinearVelocities[i] = transformVectorFast(undoRedoMat, _trackedDeviceLinearVelocities[i]); - _trackedDeviceAngularVelocities[i] = transformVectorFast(undoRedoMat, _trackedDeviceAngularVelocities[i]); - } - _currentRenderFrameInfo.renderPose = _trackedDevicePoseMat4[vr::k_unTrackedDeviceIndex_Hmd]; } From edfce0d5badcac66e0b857c24b47875c37d33b04 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 23 May 2016 16:34:19 -0700 Subject: [PATCH 0202/1237] MyAvatar: reset fix, for both oculus and vive --- interface/src/avatar/MyAvatar.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 2505e258ed..12d2764e74 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -263,8 +263,9 @@ void MyAvatar::reset(bool andRecenter, bool andReload) { setPosition(worldBodyPos); setOrientation(worldBodyRot); - // now sample the new hmd orientation AFTER sensor reset. - updateFromHMDSensorMatrix(qApp->getHMDSensorPose()); + // now sample the new hmd orientation AFTER sensor reset, which should be identity. + glm::mat4 identity; + updateFromHMDSensorMatrix(identity); // update the body in sensor space using the new hmd sensor sample _bodySensorMatrix = deriveBodyFromHMDSensor(); From fafea941969c5e767d93827859d362c24ed28665 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 23 May 2016 16:48:21 -0700 Subject: [PATCH 0203/1237] new blocky, delete old files --- .../Home/blocky/arrangement1.json | 103 ------ .../Home/blocky/arrangement1A.json | 104 ------ .../Home/blocky/arrangement2.json | 179 ---------- .../Home/blocky/arrangement2A.json | 174 ---------- .../Home/blocky/arrangement3.json | 311 ------------------ .../Home/blocky/arrangement3A.json | 229 ------------- .../Home/blocky/arrangement4.json | 232 ------------- .../Home/blocky/arrangement4A.json | 203 ------------ .../Home/blocky/arrangement5.json | 254 -------------- .../Home/blocky/arrangement5B.json | 203 ------------ .../Home/blocky/arrangement5a.json | 197 ----------- .../Home/blocky/arrangement6.json | 238 -------------- .../DomainContent/Home/blocky/blocky.js | 36 +- .../DomainContent/Home/blocky/newblocks1.json | 136 ++++++++ .../DomainContent/Home/blocky/newblocks2.json | 107 ++++++ .../DomainContent/Home/blocky/newblocks3.json | 277 ++++++++++++++++ .../DomainContent/Home/blocky/newblocks4.json | 277 ++++++++++++++++ .../DomainContent/Home/blocky/newblocks5.json | 175 ++++++++++ 18 files changed, 991 insertions(+), 2444 deletions(-) delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement1.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement2.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement3.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement4.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement5.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json delete mode 100644 unpublishedScripts/DomainContent/Home/blocky/arrangement6.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/newblocks1.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/newblocks2.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/newblocks3.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/newblocks4.json create mode 100644 unpublishedScripts/DomainContent/Home/blocky/newblocks5.json diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement1.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement1.json deleted file mode 100644 index 754db172b6..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement1.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "Entities": [{ - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{e4fd5fc4-10b9-4d84-b6ba-b732cc7de7b7}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0147705078125, - "y": 0.5, - "z": 0 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.12884356081485748, - "y": 0.35638594627380371, - "z": -0.14361406862735748 - }, - "rotation": { - "w": -0.31651788949966431, - "x": -0.31587702035903931, - "y": -0.63225758075714111, - "z": 0.63265430927276611 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{1e310b85-7823-414b-8448-4435e056c07a}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0, - "y": 0, - "z": 0.0084075927734375 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.14361406862735748, - "y": -0.14361406862735748, - "z": -0.13520647585391998 - }, - "rotation": { - "w": 0.34120702743530273, - "x": -0.34154266119003296, - "y": 0.61931788921356201, - "z": 0.61916530132293701 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:28:38Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{1f07cd0e-f9d1-46f3-b71a-ad7127d1a0b0}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0155029296875, - "y": 0.25, - "z": 0.0084686279296875 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.12811113893985748, - "y": 0.10638593137264252, - "z": -0.13514544069766998 - }, - "rotation": { - "w": 0.62478065490722656, - "x": -0.62526893615722656, - "y": -0.33089190721511841, - "z": -0.33040362596511841 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }], - "Version": 57 -} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json deleted file mode 100644 index 2d224c7d0a..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement1A.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Entities": [ - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:28:38Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "id": "{061cfb78-aece-414a-a56b-a83df3d3c188}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "parentID": "{adc9842c-db50-4bbf-8d90-8c12e5ad59ac}", - "position": { - "x": -0.0069605633616447449, - "y": -0.0044899135828018188, - "z": 0.24996849894523621 - }, - "queryAACube": { - "scale": 0.86168444156646729, - "x": 1098.883544921875, - "y": 460.20965576171875, - "z": -69.462593078613281 - }, - "rotation": { - "w": -6.8545341491699219e-05, - "x": -0.022976815700531006, - "y": 0.9996645450592041, - "z": 0.00011834502220153809 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "id": "{526b3076-78eb-438d-8566-ae0d0599029c}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "parentID": "{adc9842c-db50-4bbf-8d90-8c12e5ad59ac}", - "position": { - "x": 0.0026676803827285767, - "y": -0.016830384731292725, - "z": 0.4999808669090271 - }, - "queryAACube": { - "scale": 0.86168444156646729, - "x": 1098.8680419921875, - "y": 459.95965576171875, - "z": -69.462570190429688 - }, - "rotation": { - "w": 3.6507844924926758e-05, - "x": 0.99916994571685791, - "y": 0.040203869342803955, - "z": -0.0002717822790145874 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "angularDamping": 0, - "angularVelocity": { - "x": 0, - "y": 0.52359879016876221, - "z": 0 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "id": "{adc9842c-db50-4bbf-8d90-8c12e5ad59ac}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.14361406862735748, - "y": -0.14361406862735748, - "z": -0.14361406862735748 - }, - "rotation": { - "w": -0.69973748922348022, - "x": -0.69991332292556763, - "y": 0.10158070176839828, - "z": -0.10084760189056396 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - } - ], - "Version": 57 -} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement2.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement2.json deleted file mode 100644 index 2a47e2c5d4..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement2.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "Entities": [{ - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{e4fd5fc4-10b9-4d84-b6ba-b732cc7de7b7}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0531005859375, - "y": 0.17498779296875, - "z": 0.10013580322265625 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.090513482689857483, - "y": 0.031373724341392517, - "z": -0.043478265404701233 - }, - "rotation": { - "w": 0.70144569873809814, - "x": 0.089502327144145966, - "y": 0.089585684239864349, - "z": 0.70138269662857056 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:28:38Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{82c627f6-d296-4ee9-9206-56aa06846013}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0238037109375, - "y": 0.5, - "z": 0 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": -0.01949755847454071, - "y": 0.4566987156867981, - "z": -0.04330126941204071 - }, - "rotation": { - "w": 0.00020232143288012594, - "x": 0.89600801467895508, - "y": 0.00046253428445197642, - "z": 0.44403755664825439 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": -0.007318682037293911, - "y": -0.0010040029883384705, - "z": -9.1018431703560054e-05 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{c07b3366-9fb2-4192-a2aa-963bbfa46de1}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0, - "y": 0, - "z": 0.11705780029296875 - }, - "queryAACube": { - "scale": 0.27386128902435303, - "x": -0.13693064451217651, - "y": -0.13693064451217651, - "z": -0.019872844219207764 - }, - "rotation": { - "w": 0.52331775426864624, - "x": -0.52310466766357422, - "y": -0.47588011622428894, - "z": -0.47543454170227051 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{1fa9f95d-b040-4d52-8c50-6ceaaf794bba}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.024658203125, - "y": 0.3499755859375, - "z": 0.00504302978515625 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": -0.10369677841663361, - "y": 0.22162060439586639, - "z": -0.12331195175647736 - }, - "rotation": { - "w": 0.69705569744110107, - "x": 0.69662469625473022, - "y": 0.11998884379863739, - "z": -0.12012538313865662 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": -0.0049706185236573219, - "y": 0.0010576546192169189, - "z": 0.00093202776042744517 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{02fb5b0d-17cb-44b2-ad41-9dbe24bcd606}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_block", - "position": { - "x": 0.1104736328125, - "y": 0, - "z": 0.097320556640625 - }, - "queryAACube": { - "scale": 0.27386128902435303, - "x": -0.026457011699676514, - "y": -0.13693064451217651, - "z": -0.039610087871551514 - }, - "rotation": { - "w": 0.58954423666000366, - "x": 0.58955228328704834, - "y": -0.39041024446487427, - "z": 0.39044272899627686 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }], - "Version": 57 -} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json deleted file mode 100644 index 42ae9059ac..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement2A.json +++ /dev/null @@ -1,174 +0,0 @@ -{ - "Entities": [ - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{c170eefd-bd60-4d6b-a475-9c084f1c2de0}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_block", - "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", - "position": { - "x": 0.13519594073295593, - "y": 0.49943554401397705, - "z": 0.017574317753314972 - }, - "queryAACube": { - "scale": 0.82158386707305908, - "x": 1099.1766357421875, - "y": 460.05300903320312, - "z": -69.957748413085938 - }, - "rotation": { - "w": 0.7015533447265625, - "x": -0.70165455341339111, - "y": 0.08770480751991272, - "z": 0.088382631540298462 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{336bdac8-0229-460d-a9be-38efbc8b7e1f}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_blocky_block", - "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", - "position": { - "x": 0.083991743624210358, - "y": 0.49937903881072998, - "z": -0.082286134362220764 - }, - "queryAACube": { - "scale": 0.82158386707305908, - "x": 1099.06591796875, - "y": 460.05300903320312, - "z": -69.939018249511719 - }, - "rotation": { - "w": -0.6799309253692627, - "x": -0.68009138107299805, - "y": -0.19405338168144226, - "z": 0.19368152320384979 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "id": "{3ba19cc0-2572-4f42-9c21-1d9da982855c}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", - "position": { - "x": 0.10286395996809006, - "y": 0.32441270351409912, - "z": -0.029775351285934448 - }, - "queryAACube": { - "scale": 0.86168444156646729, - "x": 1099.0992431640625, - "y": 460.20794677734375, - "z": -69.975509643554688 - }, - "rotation": { - "w": 0.39181840419769287, - "x": -0.58902782201766968, - "y": 0.58839577436447144, - "z": -0.39155444502830505 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{7b4e39a0-2e4b-49da-a9c0-18bca0adb568}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "parentID": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", - "position": { - "x": 0.0086878137663006783, - "y": 0.14853793382644653, - "z": 0.00078312074765563011 - }, - "queryAACube": { - "scale": 0.77012991905212402, - "x": 1099.113037109375, - "y": 460.42950439453125, - "z": -70.023597717285156 - }, - "rotation": { - "w": 0.57103759050369263, - "x": -0.57109135389328003, - "y": -0.41725897789001465, - "z": -0.41673195362091064 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": -0.0010035448940470815, - "y": 0.00021353544434532523, - "z": 0.00018817203817889094 - } - }, - { - "angularDamping": 0, - "angularVelocity": { - "x": 0, - "y": 0.2617993950843811, - "z": 0 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:28:38Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{505fe8c9-4678-43ac-9eaf-c8327c08b02a}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "queryAACube": { - "scale": 0.086602538824081421, - "x": -0.04330126941204071, - "y": -0.04330126941204071, - "z": -0.04330126941204071 - }, - "rotation": { - "w": 0.00035001290962100029, - "x": 0.68673843145370483, - "y": 0.0003638292255345732, - "z": 0.72690451145172119 - }, - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":false},\"hifiHomeKey\":{\"reset\":true}}" - } - ], - "Version": 57 -} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement3.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement3.json deleted file mode 100644 index 990656eacd..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement3.json +++ /dev/null @@ -1,311 +0,0 @@ -{ - "Entities": [{ - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{099ee4ce-8cef-4885-b1a5-46c4efc9ec6d}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_block", - "position": { - "x": 0.0489501953125, - "y": 0.074981689453125, - "z": 0.1115264892578125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.0056489259004592896, - "y": 0.03168042004108429, - "z": 0.06822521984577179 - }, - "rotation": { - "w": 0.28857278823852539, - "x": 0.28886386752128601, - "y": -0.64528524875640869, - "z": 0.64567047357559204 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": -7.4302828579675406e-05, - "y": 0.0010566487908363342, - "z": -0.00053769041551277041 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:30:40Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{83e6f44b-7444-4377-a89e-439a034e6cd3}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.18896484375, - "y": 0.075103759765625, - "z": 0.31917572021484375 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.14566357433795929, - "y": 0.03180249035358429, - "z": 0.27587443590164185 - }, - "rotation": { - "w": 0.00027349172160029411, - "x": 0.89839118719100952, - "y": 0.0002214395790360868, - "z": -0.43919605016708374 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 0.00010077288607135415, - "y": 0.0014465302228927612, - "z": 2.9299229936441407e-05 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:35:02Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{44f920b7-a2ba-4e65-a30a-27bc127f4570}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.121337890625, - "y": 0.075042724609375, - "z": 0.225860595703125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.07803662121295929, - "y": 0.03174145519733429, - "z": 0.18255932629108429 - }, - "rotation": { - "w": 0.55899232625961304, - "x": -0.43351781368255615, - "y": 0.4330800473690033, - "z": -0.55859774351119995 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{f1b7be87-97d7-4c28-ba77-c431c3a248a4}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.2723388671875, - "y": 0.07513427734375, - "z": 0.44538116455078125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.22903759777545929, - "y": 0.03183300793170929, - "z": 0.40207988023757935 - }, - "rotation": { - "w": 0.098181776702404022, - "x": 0.70029288530349731, - "y": 0.70022231340408325, - "z": 0.098178565502166748 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:28:38Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{82c627f6-d296-4ee9-9206-56aa06846013}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.327392578125, - "y": 0.07513427734375, - "z": 0.52436065673828125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.2840912938117981, - "y": 0.03183300793170929, - "z": 0.48105937242507935 - }, - "rotation": { - "w": 0.68337905406951904, - "x": -0.18168261647224426, - "y": 0.18157178163528442, - "z": -0.68338578939437866 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{ae9bb770-d8a5-4a8f-9f07-512347c41fe7}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0238037109375, - "y": 0, - "z": 0.05812835693359375 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.11981035768985748, - "y": -0.14361406862735748, - "z": -0.085485711693763733 - }, - "rotation": { - "w": 7.7116979809943587e-05, - "x": 0.9558948278427124, - "y": 9.8852447990793735e-05, - "z": -0.29370918869972229 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{5dbfa65e-77f6-4646-864c-6ef99387bf21}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0, - "y": 0.074981689453125, - "z": 0 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": -0.04330126941204071, - "y": 0.03168042004108429, - "z": -0.04330126941204071 - }, - "rotation": { - "w": 0.70565086603164673, - "x": -0.70586901903152466, - "y": 0.043502748012542725, - "z": 0.043741695582866669 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:35:02Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{be46be67-9288-4eef-97be-1bc501bda8f0}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.1683349609375, - "y": 6.103515625e-05, - "z": 0.2762603759765625 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": 0.024720892310142517, - "y": -0.14355303347110748, - "z": 0.13264630734920502 - }, - "rotation": { - "w": 0.22011587023735046, - "x": 0.67208588123321533, - "y": -0.67190313339233398, - "z": -0.21999000012874603 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:32:51Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{eef1688c-5eba-4a57-8ece-39339fee5ffe}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.3221435546875, - "y": 0.000152587890625, - "z": 0.478668212890625 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": 0.17852948606014252, - "y": -0.14346148073673248, - "z": 0.33505415916442871 - }, - "rotation": { - "w": -0.00010969530558213592, - "x": 0.29818582534790039, - "y": 7.1413240220863372e-05, - "z": 0.95450782775878906 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }], - "Version": 57 -} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json deleted file mode 100644 index 4446c0f23a..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement3A.json +++ /dev/null @@ -1,229 +0,0 @@ -{ - "Entities": [ - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{68b93994-05ce-49e8-acfc-113439b0016c}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_block", - "position": { - "x": 0.1077880859375, - "y": 0.075286865234375, - "z": 0.0563507080078125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.06448681652545929, - "y": 0.03198559582233429, - "z": 0.01304943859577179 - }, - "rotation": { - "w": -0.48592910170555115, - "x": -0.48633205890655518, - "y": 0.51336151361465454, - "z": -0.51362788677215576 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:30:40Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{a55936ed-a7bb-4367-a08d-2c35610908ca}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.3470458984375, - "y": 0.075897216796875, - "z": 0.1311492919921875 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.3037446141242981, - "y": 0.03259594738483429, - "z": 0.08784802258014679 - }, - "rotation": { - "w": -0.00018426775932312012, - "x": -0.70232832431793213, - "y": -0.00030004978179931641, - "z": 0.71185272932052612 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:35:02Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{1f6d3b72-8e31-47d7-8306-61722554b8e5}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.23583984375, - "y": 0.075042724609375, - "z": 0.1005401611328125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.19253857433795929, - "y": 0.03174145519733429, - "z": 0.05723889172077179 - }, - "rotation": { - "w": -0.38409394025802612, - "x": 0.59400159120559692, - "y": -0.5937197208404541, - "z": 0.38357573747634888 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:28:38Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{4f17bcac-91f2-4a0d-ae1d-00c6628ef578}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.583251953125, - "y": 0.07513427734375, - "z": 0.204864501953125 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.5399506688117981, - "y": 0.03183300793170929, - "z": 0.16156323254108429 - }, - "rotation": { - "w": -0.58473116159439087, - "x": 0.39768025279045105, - "y": -0.39757427573204041, - "z": 0.58470022678375244 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{5dfd5f47-3040-4d58-be0d-ce96a2962913}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.490966796875, - "y": 0.07513427734375, - "z": 0.1775970458984375 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.4476655125617981, - "y": 0.03183300793170929, - "z": 0.13429577648639679 - }, - "rotation": { - "w": 0.13917262852191925, - "x": -0.69330555200576782, - "y": -0.69324028491973877, - "z": 0.13919799029827118 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "id": "{a7497e99-914a-4a51-9d96-70198f7bbb0e}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.054931640625, - "y": 0, - "z": 0.03050994873046875 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.088682428002357483, - "y": -0.14361406862735748, - "z": -0.11310411989688873 - }, - "rotation": { - "w": -3.9517879486083984e-05, - "x": -0.80475461483001709, - "y": -0.00011920928955078125, - "z": 0.59360742568969727 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{3167fe1f-5d3e-49a5-aee4-b4a657eb93ff}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0, - "y": 0.074981689453125, - "z": 0 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": -0.04330126941204071, - "y": 0.03168042004108429, - "z": -0.04330126941204071 - }, - "rotation": { - "w": -0.65145671367645264, - "x": 0.65158241987228394, - "y": -0.2746637761592865, - "z": -0.274961918592453 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - } - ], - "Version": 57 -} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement4.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement4.json deleted file mode 100644 index 6bb6c4067a..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement4.json +++ /dev/null @@ -1,232 +0,0 @@ -{ - "Entities": [{ - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:30:40Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{973dc65a-0200-4c27-b311-a16632abd336}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.060791015625, - "y": 0.439910888671875, - "z": 0.02478790283203125 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": -0.067563965916633606, - "y": 0.31155592203140259, - "z": -0.10356707870960236 - }, - "rotation": { - "w": -0.36391082406044006, - "x": -0.60559296607971191, - "y": 0.60754096508026123, - "z": 0.3629324734210968 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": -0.00066184467868879437, - "y": 0.00055500119924545288, - "z": -0.0027390613686293364 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "dynamic": 0, - "id": "{ae9bb770-d8a5-4a8f-9f07-512347c41fe7}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0587158203125, - "y": 0, - "z": 0.06317138671875 - }, - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.084898248314857483, - "y": -0.14361406862735748, - "z": -0.080442681908607483 - }, - "rotation": { - "w": 0.62027901411056519, - "x": 0.62011599540710449, - "y": 0.33941084146499634, - "z": -0.33986839652061462 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 0.0011035117786377668, - "y": 0.00049801170825958252, - "z": -1.9822968170046806e-05 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{fc5d6d27-52b0-4334-b60b-84c27f466f76}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "position": { - "x": 0, - "y": 0.2999267578125, - "z": 0 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": -0.12990380823612213, - "y": 0.17002294957637787, - "z": -0.12990380823612213 - }, - "rotation": { - "w": -0.18395118415355682, - "x": -0.18451963365077972, - "y": -0.6821141242980957, - "z": 0.68325310945510864 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 3.076787106692791e-05, - "y": 0.00042589753866195679, - "z": -0.0022651664912700653 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{fa73da33-a242-4384-9795-317caae92c29}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0673828125, - "y": 0.149932861328125, - "z": 0.0480499267578125 - }, - "queryAACube": { - "scale": 0.27386128902435303, - "x": -0.069547832012176514, - "y": 0.013002216815948486, - "z": -0.088880717754364014 - }, - "rotation": { - "w": 0.88347971439361572, - "x": -0.00053799827583134174, - "y": 0.46846789121627808, - "z": -0.0010445250663906336 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 0.0007340551819652319, - "y": -0.00011105835437774658, - "z": -0.0010081863729283214 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{d83f649b-1dfd-4fe3-8f25-b472665ec28a}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.112548828125, - "y": 0.29986572265625, - "z": 0.043243408203125 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": -0.017354980111122131, - "y": 0.16996191442012787, - "z": -0.086660400032997131 - }, - "rotation": { - "w": 0.67243033647537231, - "x": 0.67039400339126587, - "y": -0.22182278335094452, - "z": 0.22181977331638336 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 0.0043582655489444733, - "y": 0.0005900263786315918, - "z": -0.0029577072709798813 - } - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{f1b7be87-97d7-4c28-ba77-c431c3a248a4}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0489501953125, - "y": 0.4798583984375, - "z": 0.02193450927734375 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.0056489259004592896, - "y": 0.4365571141242981, - "z": -0.02136676013469696 - }, - "rotation": { - "w": 0.049316942691802979, - "x": -0.046750579029321671, - "y": 0.70699954032897949, - "z": 0.7039417028427124 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": -0.0029058775398880243, - "y": 0.00084279477596282959, - "z": -0.0020738139282912016 - } - }], - "Version": 57 -} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json deleted file mode 100644 index df74a6aa64..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement4A.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "Entities": [ - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{0aba0660-0991-47ab-b4d7-9c606f553e68}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", - "position": { - "x": 0.031455338001251221, - "y": -0.032536625862121582, - "z": -0.48027312755584717 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 1100.8177490234375, - "y": 461.0120849609375, - "z": -70.972358703613281 - }, - "rotation": { - "w": 0.0023152828216552734, - "x": -0.53879290819168091, - "y": 0.84243488311767578, - "z": -0.0008878018707036972 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{421d62c3-e5e5-4e51-9b14-46ce25a5cb11}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_blocky_block", - "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", - "position": { - "x": 0.017628543078899384, - "y": -0.0010632872581481934, - "z": -0.14994476735591888 - }, - "queryAACube": { - "scale": 0.82158386707305908, - "x": 1100.5284423828125, - "y": 460.40087890625, - "z": -71.226631164550781 - }, - "rotation": { - "w": 0.70702850818634033, - "x": -0.70705664157867432, - "y": -0.010112389922142029, - "z": 0.0089319320395588875 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{95792255-7fa6-4936-bb51-ce2f945dd5b9}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", - "position": { - "x": 0.023592084646224976, - "y": -0.084929108619689941, - "z": -0.29993364214897156 - }, - "queryAACube": { - "scale": 0.77942287921905518, - "x": 1100.482421875, - "y": 460.57196044921875, - "z": -71.256011962890625 - }, - "rotation": { - "w": -0.69225782155990601, - "x": -0.00045704841613769531, - "y": 0.00031775236129760742, - "z": 0.72165042161941528 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{4c3ba7ac-2122-4402-a05d-f70a32bfd9d8}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_blocky_block", - "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", - "position": { - "x": 0.05141855776309967, - "y": 0.034773021936416626, - "z": -0.29993501305580139 - }, - "queryAACube": { - "scale": 0.77942287921905518, - "x": 1100.59814453125, - "y": 460.57192993164062, - "z": -71.214653015136719 - }, - "rotation": { - "w": 0.68213790655136108, - "x": -0.0010509714484214783, - "y": -0.00042241811752319336, - "z": 0.73122292757034302 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:30:40Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{f04a15fe-4482-44f0-8b63-46ff8aa7538c}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "parentID": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", - "position": { - "x": 0.036200806498527527, - "y": -0.020915478467941284, - "z": -0.43993330001831055 - }, - "queryAACube": { - "scale": 0.77012991905212402, - "x": 1100.5477294921875, - "y": 460.71658325195312, - "z": -71.227401733398438 - }, - "rotation": { - "w": -0.51840746402740479, - "x": -0.47963690757751465, - "y": 0.51959860324859619, - "z": -0.48085314035415649 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "angularDamping": 0, - "angularVelocity": { - "x": 0, - "y": -0.2617993950843811, - "z": 0 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:33:00Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.10000000149011612, - "z": 0.25 - }, - "id": "{9aead5f1-edb1-4e75-a5f4-a121410f2dee}", - "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", - "name": "home_model_blocky_block", - "queryAACube": { - "scale": 0.28722813725471497, - "x": -0.14361406862735748, - "y": -0.14361406862735748, - "z": -0.14361406862735748 - }, - "rotation": { - "w": 0.2521953284740448, - "x": 0.2517753541469574, - "y": 0.66056090593338013, - "z": -0.66080713272094727 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - } - ], - "Version": 57 -} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement5.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement5.json deleted file mode 100644 index d04f78b106..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement5.json +++ /dev/null @@ -1,254 +0,0 @@ -{ - "Entities": [ - { - "angularVelocity": { - "x": -0.0058977864682674408, - "y": 0.012016458436846733, - "z": -0.019001182168722153 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{5dbfa65e-77f6-4646-864c-6ef99387bf21}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.118896484375, - "y": 0.261627197265625, - "z": 0.5547332763671875 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.07559521496295929, - "y": 0.21832592785358429, - "z": 0.5114319920539856 - }, - "rotation": { - "w": 0.25579428672790527, - "x": 0.659473717212677, - "y": 0.65893793106079102, - "z": 0.25586038827896118 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 0.0021444200538098812, - "y": 0.0011220350861549377, - "z": 0.0011650724336504936 - } - }, - { - "angularVelocity": { - "x": 0.0021077222190797329, - "y": 0.0096995644271373749, - "z": -0.00475650979205966 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{18d0fe48-f5af-4baa-bc76-fd02b2cf507b}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "position": { - "x": 0.1376953125, - "y": 0.11163330078125, - "z": 0.54648590087890625 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 0.0077915042638778687, - "y": -0.018270507454872131, - "z": 0.41658210754394531 - }, - "rotation": { - "w": -0.16635751724243164, - "x": -0.16560617089271545, - "y": 0.68745934963226318, - "z": -0.68724048137664795 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{f61afc15-4813-4306-8db2-6fb9dc3baf33}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.2005615234375, - "y": 0.111602783203125, - "z": 0.3127593994140625 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.072206541895866394, - "y": -0.016752198338508606, - "z": 0.18440441787242889 - }, - "rotation": { - "w": 0.5063401460647583, - "x": 0.5066148042678833, - "y": 0.4936140775680542, - "z": -0.4932173490524292 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "angularVelocity": { - "x": 0.0085022579878568649, - "y": 8.5791980382055044e-05, - "z": -0.0055606141686439514 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:59:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic": 0, - "id": "{050ba484-bd34-4819-a860-776f8d865a76}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0, - "y": 0.261627197265625, - "z": 0.6636199951171875 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": -0.04330126941204071, - "y": 0.21832592785358429, - "z": 0.6203187108039856 - }, - "rotation": { - "w": -0.11174160987138748, - "x": 0.6981397271156311, - "y": 0.69834953546524048, - "z": -0.11145715415477753 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 0.00021521201415453106, - "y": 0.0007597804069519043, - "z": 0.0016687994357198477 - } - }, - { - "angularVelocity": { - "x": 0.10312929749488831, - "y": 0.02031000517308712, - "z": 0.17793627083301544 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "dynamic":0, - "id": "{0a801214-14b7-4059-828a-61f18250a315}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.1876220703125, - "y": 0, - "z": 0 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.14432080090045929, - "y": -0.04330126941204071, - "z": -0.04330126941204071 - }, - "rotation": { - "w": 0.38613453507423401, - "x": 0.27518847584724426, - "y": 0.11528003960847855, - "z": 0.87285858392715454 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": -0.0028362239245325327, - "y": 0.0032131313346326351, - "z": 0.0032204082235693932 - } - }, - { - "angularVelocity": { - "x": 0.0060502123087644577, - "y": -0.0002331961877644062, - "z": -0.0040579927153885365 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:32:51Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{c0abdb26-3e55-422f-811a-306239ec4894}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0091552734375, - "y": 0.11163330078125, - "z": 0.663543701171875 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": -0.12074853479862213, - "y": -0.018270507454872131, - "z": 0.53363990783691406 - }, - "rotation": { - "w": 0.38818395137786865, - "x": -0.38840728998184204, - "y": 0.5908893346786499, - "z": 0.59101837873458862 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}", - "velocity": { - "x": 0.00010580161324469373, - "y": 0.00069457292556762695, - "z": 0.00075469817966222763 - } - } - ], - "Version": 57 -} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json deleted file mode 100644 index 634f1e6858..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement5B.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "Entities": [ - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{e9a7eb61-bdfe-484d-963a-fdae21bc12e7}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", - "position": { - "x": -0.16834764182567596, - "y": 0.17657718062400818, - "z": -0.14808396995067596 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 1102.9293212890625, - "y": 460.51165771484375, - "z": -70.490364074707031 - }, - "rotation": { - "w": -0.72276210784912109, - "x": -0.67909777164459229, - "y": 0.11672055721282959, - "z": 0.054192394018173218 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:59:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{242a2da8-26e9-439f-a6bd-a5174d68b791}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", - "position": { - "x": 0.17733007669448853, - "y": -0.041430100798606873, - "z": -0.14965415000915527 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 1102.8548583984375, - "y": 460.51361083984375, - "z": -70.082504272460938 - }, - "rotation": { - "w": 0.45803076028823853, - "x": -0.54004138708114624, - "y": 0.45782959461212158, - "z": 0.53765928745269775 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{8d10e070-80b5-41a1-a98d-2fb60a73f98c}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", - "position": { - "x": -0.16358290612697601, - "y": 0.17843163013458252, - "z": 7.3954463005065918e-05 - }, - "queryAACube": { - "scale": 0.77012991905212402, - "x": 1102.6942138671875, - "y": 460.10836791992188, - "z": -70.743125915527344 - }, - "rotation": { - "w": 0.50793719291687012, - "x": -0.0007769167423248291, - "y": -0.0016511008143424988, - "z": 0.86144548654556274 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:32:51Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{96880a75-218a-485b-91d3-be9741f320ed}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_blocky_block", - "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", - "position": { - "x": 0.16807788610458374, - "y": -0.044651858508586884, - "z": 0.00034449249505996704 - }, - "queryAACube": { - "scale": 0.77942287921905518, - "x": 1102.57421875, - "y": 460.10379028320312, - "z": -70.363555908203125 - }, - "rotation": { - "w": -0.00017070770263671875, - "x": -0.68149137496948242, - "y": -0.73185545206069946, - "z": -0.0021193623542785645 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{0fdd72df-a5f5-4f9c-b3ec-b79bc606b356}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "parentID": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", - "position": { - "x": 0.02161325141787529, - "y": 0.0003502964973449707, - "z": -0.14994175732135773 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 1102.9146728515625, - "y": 460.51358032226562, - "z": -70.236610412597656 - }, - "rotation": { - "w": 0.12661613523960114, - "x": -0.68729931116104126, - "y": 0.1313164234161377, - "z": 0.70317196846008301 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "angularDamping": 0, - "angularVelocity": { - "x": 0, - "y": -0.2617993950843811, - "z": 0 - }, - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{f9f90ed5-6a9e-45c1-99e2-db2ba28f12c2}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "queryAACube": { - "scale": 0.25980761647224426, - "x": -0.12990380823612213, - "y": -0.12990380823612213, - "z": -0.12990380823612213 - }, - "rotation": { - "w": 0.21236260235309601, - "x": 0.20996685326099396, - "y": -0.67540425062179565, - "z": 0.67427384853363037 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - } - ], - "Version": 57 -} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json deleted file mode 100644 index 3d12988bbc..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement5a.json +++ /dev/null @@ -1,197 +0,0 @@ -{ - "Entities": [ - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{e5dcde81-ec21-49cd-bfc1-47a04678d620}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.118896484375, - "y": 0.1500244140625, - "z": 0.24393463134765625 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.07559521496295929, - "y": 0.10672314465045929, - "z": 0.20063336193561554 - }, - "rotation": { - "w": 0.25630581378936768, - "x": 0.66857409477233887, - "y": 0.65368127822875977, - "z": 0.24492251873016357 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:32:51Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{ecbc77d5-b578-453c-b502-f4b7331c9088}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0091552734375, - "y": 3.0517578125e-05, - "z": 0.35125732421875 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": -0.12074853479862213, - "y": -0.12987329065799713, - "z": 0.22135351598262787 - }, - "rotation": { - "w": 0.38817429542541504, - "x": -0.38844889402389526, - "y": 0.59087514877319336, - "z": 0.59099721908569336 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{35d2e036-2cfa-4e62-8cb7-81beda65963d}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.2061767578125, - "y": 0.148101806640625, - "z": 0 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": 0.16287548840045929, - "y": 0.10480053722858429, - "z": -0.04330126941204071 - }, - "rotation": { - "w": -0.034055888652801514, - "x": 0.35255527496337891, - "y": -0.041519246995449066, - "z": 0.93420767784118652 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:59:13Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.05000000074505806 - }, - "id": "{e7edf689-89c9-4daf-b5e0-38c330f34c6e}", - "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0, - "y": 0.1500244140625, - "z": 0.3527984619140625 - }, - "queryAACube": { - "scale": 0.086602538824081421, - "x": -0.04330126941204071, - "y": 0.10672314465045929, - "z": 0.3094971776008606 - }, - "rotation": { - "w": -0.11314564943313599, - "x": 0.69872581958770752, - "y": 0.69771873950958252, - "z": -0.11012434959411621 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T21:46:06Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{764c37c5-892f-41b4-8a34-8e13561726f7}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.2005615234375, - "y": 0, - "z": 0.00042724609375 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.072206541895866394, - "y": -0.12835498154163361, - "z": -0.12792773544788361 - }, - "rotation": { - "w": 0.5063401460647583, - "x": 0.5066148042678833, - "y": 0.4936140775680542, - "z": -0.4932783842086792 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, - { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.05000000074505806, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{8b4d7620-0303-447c-815c-ed5a90ba33b3}", - "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", - "name": "home_model_block", - "position": { - "x": 0.1376953125, - "y": 3.0517578125e-05, - "z": 0.23415374755859375 - }, - "queryAACube": { - "scale": 0.25980761647224426, - "x": 0.0077915042638778687, - "y": -0.12987329065799713, - "z": 0.10424993932247162 - }, - "rotation": { - "w": -0.16896313428878784, - "x": -0.16664379835128784, - "y": 0.68758678436279297, - "z": -0.68624401092529297 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - } - ], - "Version": 57 -} diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement6.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement6.json deleted file mode 100644 index 17c88e8308..0000000000 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement6.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "Entities": [{ - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:32:51Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{845bc4f0-f59b-4826-9910-e92ab0a82eca}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.118896484375, - "y": 6.103515625e-05, - "z": 0.16530609130859375 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": -0.009458497166633606, - "y": -0.12829394638538361, - "z": 0.036951109766960144 - }, - "rotation": { - "w": 0.37096202373504639, - "x": -0.37190812826156616, - "y": 0.60173952579498291, - "z": 0.60161745548248291 - }, - "dynamic": 0, - - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T21:28:38Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{1af7f363-a1e4-4f2b-94af-fc8c592769e8}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.34423828125, - "y": 0.000152587890625, - "z": 0.4805755615234375 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.21588329970836639, - "y": -0.12820239365100861, - "z": 0.35222059488296509 - }, - "rotation": { - "w": 0.66173803806304932, - "x": 0.66201269626617432, - "y": -0.24910354614257812, - "z": 0.24846267700195312 - }, - "dynamic": 0, - - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:30:40Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{f05ff17b-a9ef-4b67-9f1e-a0cdbac1d73a}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.1954345703125, - "y": 9.1552734375e-05, - "z": 0.2958984375 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.067079588770866394, - "y": -0.12826342880725861, - "z": 0.16754345595836639 - }, - "rotation": { - "w": 0.61269545555114746, - "x": 0.61297011375427246, - "y": -0.35289537906646729, - "z": 0.35259020328521729 - }, - "dynamic": 0, - - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:37:13Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - - "id": "{d501c4f4-5ace-4337-a694-bf08230fd3ff}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.238037109375, - "y": 9.1552734375e-05, - "z": 0.35927581787109375 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.10968212783336639, - "y": -0.12826342880725861, - "z": 0.23092083632946014 - }, - "rotation": { - "w": 0.61916530132293701, - "x": -0.61983668804168701, - "y": -0.34154266119003296, - "z": -0.34023040533065796 - }, - "dynamic": 0, - - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:32:51Z", - "dimensions": { - "x": 0.10000000149011612, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{b18bd985-1db5-40cf-b53b-d8765fac9112}", - "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", - "name": "home_model_blocky_block", - "queryAACube": { - "scale": 0.27386128902435303, - "x": -0.13693064451217651, - "y": -0.13693064451217651, - "z": -0.13693064451217651 - }, - "rotation": { - "w": 0.67776000499725342, - "x": 0.67730224132537842, - "y": 0.20234990119934082, - "z": -0.20231938362121582 - }, - "dynamic": 0, - - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:30:40Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "id": "{0284ad28-9e2b-45f5-91b0-e00da9238b41}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_blocky_block", - "position": { - "x": 0.0765380859375, - "y": 3.0517578125e-05, - "z": 0.09934234619140625 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": -0.051816895604133606, - "y": -0.12832446396350861, - "z": -0.029012635350227356 - }, - "rotation": { - "w": 0.65304040908813477, - "x": -0.65294879674911499, - "y": -0.271351158618927, - "z": -0.271198570728302 - }, - "dynamic": 0, - - "shapeType": "box", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" - }, { - "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - "collisionsWillMove": 1, - "created": "2016-05-09T19:28:29Z", - "dimensions": { - "x": 0.029999999329447746, - "y": 0.05000000074505806, - "z": 0.25 - }, - "dynamic": 0, - "id": "{7090697d-bf3c-4edc-9090-0ef7e313151b}", - "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", - "name": "home_model_block", - "position": { - "x": 0.4200439453125, - "y": 0.00018310546875, - "z": 0.53116607666015625 - }, - "queryAACube": { - "scale": 0.25670996308326721, - "x": 0.29168897867202759, - "y": -0.12817187607288361, - "z": 0.40281111001968384 - }, - "rotation": { - "w": 0.66921496391296387, - "x": 0.66921496391296387, - "y": -0.22810709476470947, - "z": 0.22853434085845947 - }, - "shapeType": "box", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }], - "Version": 57 -} \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js index 948ccd5e04..92a11696da 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/blocky.js +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -48,23 +48,23 @@ var blocks = [ var arrangements = [{ name: 'tall', blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN], - target: "atp:/blocky/arrangement1A.json" + target: "atp:/blocky/newblocks1.json" }, { name: 'ostrich', blocks: [BLOCK_RED, BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_NATURAL], - target: "atp:/blocky/arrangement2A.json" + target: "atp:/blocky/newblocks5.json" }, { name: 'froglog', blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], - target: "atp:/blocky/arrangement3A.json" + target: "atp:/blocky/newblocks2.json" }, { name: 'allOneLeg', blocks: [BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_BLUE, BLOCK_BLUE, BLOCK_NATURAL], - target: "atp:/blocky/arrangement4A.json" + target: "atp:/blocky/newblocks3.json" }, { name: 'threeppl', blocks: [BLOCK_BLUE, BLOCK_YELLOW, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], - target: "atp:/blocky/arrangement5B.json" + target: "atp:/blocky/newblocks4.json" }, { name: 'dominoes', blocks: [BLOCK_RED, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW], @@ -82,9 +82,9 @@ var TARGET_BLOCKS_POSITION = { y: 460.5, z: -67.689 }; - +//#debug (function() { - //#debug + print('BLOCK ENTITY SCRIPT') var _this; @@ -93,7 +93,7 @@ var TARGET_BLOCKS_POSITION = { } Blocky.prototype = { - debug: false, + debug: true, playableBlocks: [], targetBlocks: [], preload: function(entityID) { @@ -122,7 +122,8 @@ var TARGET_BLOCKS_POSITION = { name: "home_model_blocky_block", type: 'Model', collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - dynamic: true, + dynamic: false, + collisionless: true, gravity: { x: 0, y: -9.8, @@ -173,7 +174,7 @@ var TARGET_BLOCKS_POSITION = { }, findBlocks: function() { var found = []; - var results = Entities.findEntities(this.position, 10); + var results = Entities.findEntities(MyAvatar.position, 10); results.forEach(function(result) { var properties = Entities.getEntityProperties(result); print('got result props') @@ -187,7 +188,7 @@ var TARGET_BLOCKS_POSITION = { cleanup: function() { print('BLOCKY cleanup'); var blocks = this.findBlocks(); - print('BLOCKY cleanup2') + print('BLOCKY cleanup2' + blocks.length) blocks.forEach(function(block) { Entities.deleteEntity(block); }) @@ -211,12 +212,13 @@ var TARGET_BLOCKS_POSITION = { name: "home_model_blocky_block", type: 'Model', collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - dynamic: true, - gravity: { - x: 0, - y: -9.8, - z: 0 - }, + dynamic: false, + collisionless: true, + // gravity: { + // x: 0, + // y: -9.8, + // z: 0 + // }, userData: JSON.stringify({ grabbableKey: { grabbable: true diff --git a/unpublishedScripts/DomainContent/Home/blocky/newblocks1.json b/unpublishedScripts/DomainContent/Home/blocky/newblocks1.json new file mode 100644 index 0000000000..2409c7c34c --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/newblocks1.json @@ -0,0 +1,136 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:35:13Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{aad3b1c3-6478-46dd-a985-800d0e38c8e7}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.069580078125, + "y": 0.153778076171875, + "z": 0.04181671142578125 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.058774903416633606, + "y": 0.025423094630241394, + "z": -0.086538270115852356 + }, + "rotation": { + "w": 0.95882409811019897, + "x": -1.896415778901428e-05, + "y": 0.28400072455406189, + "z": -1.0296697837475222e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:30:51Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{f5714678-d436-4353-b0c8-2d73599dda3a}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0181884765625, + "y": 0.153778076171875, + "z": 0.0773468017578125 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.11016650497913361, + "y": 0.025423094630241394, + "z": -0.051008179783821106 + }, + "rotation": { + "w": 0.95882409811019897, + "x": -1.896415778901428e-05, + "y": 0.28400072455406189, + "z": -1.0296697837475222e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:30:51Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{0cf93a58-18d2-4c69-9c1b-33d6d1e647b8}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.078857421875, + "y": 0.002899169921875, + "z": 0.11455535888671875 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.064756646752357483, + "y": -0.14071489870548248, + "z": -0.029058709740638733 + }, + "rotation": { + "w": 0.33741602301597595, + "x": -0.33740735054016113, + "y": 0.62139773368835449, + "z": 0.62142699956893921 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:33:02Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{7f8791fb-e236-4280-b9d4-4ee6d1663e2a}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.14361406862735748, + "y": -0.14361406862735748, + "z": -0.14361406862735748 + }, + "rotation": { + "w": 0.68138498067855835, + "x": -0.68140000104904175, + "y": 0.18894240260124207, + "z": 0.1889689564704895 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/newblocks2.json b/unpublishedScripts/DomainContent/Home/blocky/newblocks2.json new file mode 100644 index 0000000000..8487e709d4 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/newblocks2.json @@ -0,0 +1,107 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:37:24Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{2454f490-787c-491b-9b7e-c0e098af86e8}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.504150390625, + "z": 0 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.13693064451217651, + "y": 0.36721974611282349, + "z": -0.13693064451217651 + }, + "rotation": { + "w": 0.70711755752563477, + "x": 0.70709598064422607, + "y": 0, + "z": -2.1579186068265699e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:37:24Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{5bf66e29-a3b6-4171-8470-6e0462d51dd3}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0, + "z": 0.00112152099609375 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.13693064451217651, + "y": -0.13693064451217651, + "z": -0.13580912351608276 + }, + "rotation": { + "w": 0.70711755752563477, + "x": 0.70709598064422607, + "y": 0, + "z": -2.1579186068265699e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:39:35Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{2cf52537-9f88-41eb-86d6-14a5ff218fb6}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.253204345703125, + "z": 0 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.13693064451217651, + "y": 0.11627370119094849, + "z": -0.13693064451217651 + }, + "rotation": { + "w": 0.70711755752563477, + "x": 0.70709598064422607, + "y": 0, + "z": -2.1579186068265699e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/newblocks3.json b/unpublishedScripts/DomainContent/Home/blocky/newblocks3.json new file mode 100644 index 0000000000..846800a2eb --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/newblocks3.json @@ -0,0 +1,277 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:41:47Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{806e18fc-1ffd-4d7a-a405-52f1ad6cc164}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.1412353515625, + "y": 0.150634765625, + "z": 0.19216156005859375 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.09793408215045929, + "y": 0.10733349621295929, + "z": 0.14886029064655304 + }, + "rotation": { + "w": 0.96592974662780762, + "x": -1.8688122509047389e-05, + "y": 0.25880429148674011, + "z": -1.0789593034132849e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:41:47Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{3c99f577-fc38-4f5b-8b7b-2dc36b742ff9}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.078369140625, + "y": 0.150634765625, + "z": 0.10829925537109375 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.03506787121295929, + "y": 0.10733349621295929, + "z": 0.06499798595905304 + }, + "rotation": { + "w": 0.96592974662780762, + "x": -1.8688122509047389e-05, + "y": 0.25880429148674011, + "z": -1.0789593034132849e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:41:47Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{3aa37e21-48ff-4d41-be0b-d47a67e004cc}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.2056884765625, + "y": 0.150634765625, + "z": 0.2870941162109375 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.16238720715045929, + "y": 0.10733349621295929, + "z": 0.24379284679889679 + }, + "rotation": { + "w": 0.96592974662780762, + "x": -1.8688122509047389e-05, + "y": 0.25880429148674011, + "z": -1.0789593034132849e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:39:35Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{a34966c4-0f1c-4103-8857-f998559318e7}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.150634765625, + "z": 0 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": -0.04330126941204071, + "y": 0.10733349621295929, + "z": -0.04330126941204071 + }, + "rotation": { + "w": 0.96592974662780762, + "x": -1.8688122509047389e-05, + "y": 0.25880429148674011, + "z": -1.0789593034132849e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:41:47Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{96336167-827c-48ec-b641-ee462fb2abdc}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.2088623046875, + "y": 0, + "z": 0.28765869140625 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 0.078958496451377869, + "y": -0.12990380823612213, + "z": 0.15775488317012787 + }, + "rotation": { + "w": 0.68138498067855835, + "x": -0.68140000104904175, + "y": 0.18894240260124207, + "z": 0.1889689564704895 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:39:35Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{17b1be58-b3d6-4b27-856e-f74b8ba34309}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.001953125, + "y": 0, + "z": 0.00205230712890625 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": -0.12795068323612213, + "y": -0.12990380823612213, + "z": -0.12785150110721588 + }, + "rotation": { + "w": 0.68138498067855835, + "x": -0.68140000104904175, + "y": 0.18894240260124207, + "z": 0.1889689564704895 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:41:47Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{161eefc7-ba39-4301-913f-edb4f7d9236e}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0777587890625, + "y": 0, + "z": 0.10608673095703125 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": -0.052145019173622131, + "y": -0.12990380823612213, + "z": -0.023817077279090881 + }, + "rotation": { + "w": 0.68138498067855835, + "x": -0.68140000104904175, + "y": 0.18894240260124207, + "z": 0.1889689564704895 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:41:47Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{ea1eebe2-fef3-4587-9f87-f7812359ec8b}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_blue.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.152099609375, + "y": 0, + "z": 0.1891937255859375 + }, + "queryAACube": { + "scale": 0.25980761647224426, + "x": 0.022195801138877869, + "y": -0.12990380823612213, + "z": 0.059289917349815369 + }, + "rotation": { + "w": 0.68138498067855835, + "x": -0.68140000104904175, + "y": 0.18894240260124207, + "z": 0.1889689564704895 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/newblocks4.json b/unpublishedScripts/DomainContent/Home/blocky/newblocks4.json new file mode 100644 index 0000000000..5d5a7a8d9d --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/newblocks4.json @@ -0,0 +1,277 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:52:42Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{df9f2e81-c7c3-45b3-9459-0dc1647e3ac8}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.1153564453125, + "y": 0.3056640625, + "z": 0.19878387451171875 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.07205517590045929, + "y": 0.2623627781867981, + "z": 0.15548260509967804 + }, + "rotation": { + "w": 0.87880980968475342, + "x": -6.1288210417842492e-06, + "y": -0.4771721363067627, + "z": -2.0690549717983231e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:48:20Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{00f70709-d22b-4e50-98b6-c49783a1ca2d}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0565185546875, + "y": 0.3056640625, + "z": 0.0875244140625 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.01321728527545929, + "y": 0.2623627781867981, + "z": 0.04422314465045929 + }, + "rotation": { + "w": 0.87880980968475342, + "x": -6.1288210417842492e-06, + "y": -0.4771721363067627, + "z": -2.0690549717983231e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:48:20Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{925922c7-c8da-4862-8695-9e823cde5f43}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.165771484375, + "y": 0.1016845703125, + "z": 0.3075103759765625 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": 0.028840839862823486, + "y": -0.035246074199676514, + "z": 0.17057973146438599 + }, + "rotation": { + "w": 0.76051729917526245, + "x": 0.5797961950302124, + "y": 0.20168730616569519, + "z": -0.21159422397613525 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:48:20Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{9754d1bd-30f0-473c-87fa-159902f3ae54}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0.1016845703125, + "z": 0 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.13693064451217651, + "y": -0.035246074199676514, + "z": -0.13693064451217651 + }, + "rotation": { + "w": -0.24484673142433167, + "x": -0.2088056355714798, + "y": 0.70473498106002808, + "z": -0.63229680061340332 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:48:20Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{d2cd5cb1-cc1c-48b5-a190-b25279ca46aa}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0667724609375, + "y": 0.18121337890625, + "z": 0.155120849609375 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.076841607689857483, + "y": 0.037599310278892517, + "z": 0.011506780982017517 + }, + "rotation": { + "w": 0.9612659215927124, + "x": -1.8873581211664714e-05, + "y": 0.27562269568443298, + "z": -1.0461797501193359e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:48:20Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{168a3f35-29f3-4a5c-92e5-868ce8476052}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.08154296875, + "y": 0.256256103515625, + "z": 0.1437225341796875 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.046812012791633606, + "y": 0.12790112197399139, + "z": 0.015367552638053894 + }, + "rotation": { + "w": 0.96592974662780762, + "x": -1.8688122509047389e-05, + "y": 0.25880429148674011, + "z": -1.0789593034132849e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:48:20Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{8df113eb-303f-461a-914b-1ff86083af28}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0667724609375, + "y": 0.093353271484375, + "z": 0.155120849609375 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.076841607689857483, + "y": -0.050260797142982483, + "z": 0.011506780982017517 + }, + "rotation": { + "w": 0.9612659215927124, + "x": -1.8873581211664714e-05, + "y": 0.27562269568443298, + "z": -1.0461797501193359e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T22:48:20Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{6126fb1e-5702-4ea5-9840-fc93534a284c}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0667724609375, + "y": 0, + "z": 0.155120849609375 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.076841607689857483, + "y": -0.14361406862735748, + "z": 0.011506780982017517 + }, + "rotation": { + "w": 0.9612659215927124, + "x": -1.8873581211664714e-05, + "y": 0.27562269568443298, + "z": -1.0461797501193359e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} diff --git a/unpublishedScripts/DomainContent/Home/blocky/newblocks5.json b/unpublishedScripts/DomainContent/Home/blocky/newblocks5.json new file mode 100644 index 0000000000..88ccb23c44 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/blocky/newblocks5.json @@ -0,0 +1,175 @@ +{ + "Entities": [ + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T23:42:57Z", + "dimensions": { + "x": 0.029999999329447746, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{f89e9217-6048-4ad8-a9fa-80404931864c}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_yellow.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0689697265625, + "y": 0.336700439453125, + "z": 0.02086639404296875 + }, + "queryAACube": { + "scale": 0.25670996308326721, + "x": -0.059385254979133606, + "y": 0.20834545791149139, + "z": -0.10748858749866486 + }, + "rotation": { + "w": 0.66232722997665405, + "x": 0.66232722997665405, + "y": 0.24763399362564087, + "z": -0.24763399362564087 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T23:42:57Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.10000000149011612, + "z": 0.25 + }, + "id": "{e065e9c3-b722-4ad3-bbd4-3f12efb530f5}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_green.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0335693359375, + "y": 0.179962158203125, + "z": 0.0505828857421875 + }, + "queryAACube": { + "scale": 0.28722813725471497, + "x": -0.11004473268985748, + "y": 0.036348089575767517, + "z": -0.093031182885169983 + }, + "rotation": { + "w": 0.88294041156768799, + "x": -6.3091447373153642e-06, + "y": -0.46948501467704773, + "z": -2.0636278350139037e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T23:42:57Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{f1367c6c-744a-4ee2-9a6f-ffe89fe67e7e}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0, + "y": 0, + "z": 0.03073883056640625 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.13693064451217651, + "y": -0.13693064451217651, + "z": -0.10619181394577026 + }, + "rotation": { + "w": 0.67456501722335815, + "x": 0.67456501722335815, + "y": 0.21204251050949097, + "z": -0.21204251050949097 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T23:42:57Z", + "dimensions": { + "x": 0.10000000149011612, + "y": 0.05000000074505806, + "z": 0.25 + }, + "id": "{73c2bd96-e77d-4ba2-908b-1ee8ca7c814c}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_red.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.02685546875, + "y": 0, + "z": 0.0877532958984375 + }, + "queryAACube": { + "scale": 0.27386128902435303, + "x": -0.11007517576217651, + "y": -0.13693064451217651, + "z": -0.049177348613739014 + }, + "rotation": { + "w": 0.67456501722335815, + "x": 0.67456501722335815, + "y": 0.21204251050949097, + "z": -0.21204251050949097 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + }, + { + "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + "collisionless": 1, + "created": "2016-05-23T23:42:57Z", + "dimensions": { + "x": 0.05000000074505806, + "y": 0.05000000074505806, + "z": 0.05000000074505806 + }, + "id": "{28fd21e4-2138-4ca1-875b-1d35cbefa6b8}", + "ignoreForCollisions": 1, + "modelURL": "atp:/kineticObjects/blocks/planky_natural.fbx", + "name": "home_model_blocky_block", + "position": { + "x": 0.0506591796875, + "y": 0.485748291015625, + "z": 0 + }, + "queryAACube": { + "scale": 0.086602538824081421, + "x": 0.0073579102754592896, + "y": 0.4424470067024231, + "z": -0.04330126941204071 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shapeType": "box", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"hifiHomeKey\":{\"reset\":true}}" + } + ], + "Version": 57 +} From c7ae3396ebae20e1216afee4bdbcd90d975f9bfe Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 23 May 2016 17:29:16 -0700 Subject: [PATCH 0204/1237] end of day --- .../DomainContent/Home/blocky/blocky.js | 391 +++++++++--------- 1 file changed, 198 insertions(+), 193 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js index 92a11696da..56567cc971 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/blocky.js +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -46,208 +46,213 @@ var blocks = [ ]; var arrangements = [{ - name: 'tall', - blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN], - target: "atp:/blocky/newblocks1.json" -}, { - name: 'ostrich', - blocks: [BLOCK_RED, BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_NATURAL], - target: "atp:/blocky/newblocks5.json" -}, { - name: 'froglog', - blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], - target: "atp:/blocky/newblocks2.json" -}, { - name: 'allOneLeg', - blocks: [BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_BLUE, BLOCK_BLUE, BLOCK_NATURAL], - target: "atp:/blocky/newblocks3.json" -}, { - name: 'threeppl', - blocks: [BLOCK_BLUE, BLOCK_YELLOW, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], - target: "atp:/blocky/newblocks4.json" -}, { - name: 'dominoes', - blocks: [BLOCK_RED, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW], - target: "atp:/blocky/arrangement6B.json" -}] + name: 'greenhenge', + blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_YELLOW], + target: "atp:/blocky/newblocks1.json" + }, + { + name: 'tallstuff', + blocks: [BLOCK_RED, BLOCK_RED, BLOCK_RED], + target: "atp:/blocky/newblocks2.json" + }, + { + name: 'ostrich', + blocks: [BLOCK_RED, BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_NATURAL], + target: "atp:/blocky/newblocks5.json" + }, + { + name: 'fourppl', + blocks: [BLOCK_BLUE, BLOCK_BLUE, BLOCK_BLUE, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], + target: "atp:/blocky/newblocks3.json" + }, + { + name: 'frogguy', + blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_RED, BLOCK_RED, BLOCK_NATURAL, BLOCK_NATURAL], + target: "atp:/blocky/newblocks4.json" + }, + { + name: 'dominoes', + blocks: [BLOCK_RED, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW], + target: "atp:/blocky/arrangement6B.json" + }] -var PLAYABLE_BLOCKS_POSITION = { - x: 1097.6, - y: 460.5, - z: -66.22 -}; + var PLAYABLE_BLOCKS_POSITION = { + x: 1097.6, + y: 460.5, + z: -66.22 + }; -var TARGET_BLOCKS_POSITION = { - x: 1096.82, - y: 460.5, - z: -67.689 -}; -//#debug -(function() { + var TARGET_BLOCKS_POSITION = { + x: 1096.82, + y: 460.5, + z: -67.689 + }; + //#debug + (function() { - print('BLOCK ENTITY SCRIPT') - var _this; + print('BLOCK ENTITY SCRIPT') + var _this; - function Blocky() { - _this = this; - } - - Blocky.prototype = { - debug: true, - playableBlocks: [], - targetBlocks: [], - preload: function(entityID) { - print('BLOCKY preload') - this.entityID = entityID; - }, - createTargetBlocks: function(arrangement) { - var created = []; - print('BLOCKY create target blocks') - - var created = []; - var success = Clipboard.importEntities(arrangement.target); - if (success === true) { - created = Clipboard.pasteEntities(TARGET_BLOCKS_POSITION) - print('created ' + created); + function Blocky() { + _this = this; } - this.targetBlocks = created; - print('BLOCKY TARGET BLOCKS:: ' + this.targetBlocks); - }, - createPlayableBlocks: function(arrangement) { - print('BLOCKY creating playable blocks' + arrangement.blocks.length); - arrangement.blocks.forEach(function(block) { - print('BLOCKY in a block loop') - var blockProps = { - name: "home_model_blocky_block", - type: 'Model', - collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - dynamic: false, - collisionless: true, - gravity: { - x: 0, - y: -9.8, - z: 0 - }, - userData: JSON.stringify({ - grabbableKey: { - grabbable: true - }, - hifiHomeKey: { - reset: true + Blocky.prototype = { + debug: false, + playableBlocks: [], + targetBlocks: [], + preload: function(entityID) { + print('BLOCKY preload') + this.entityID = entityID; + }, + createTargetBlocks: function(arrangement) { + var created = []; + print('BLOCKY create target blocks') + + var created = []; + var success = Clipboard.importEntities(arrangement.target); + if (success === true) { + created = Clipboard.pasteEntities(TARGET_BLOCKS_POSITION) + print('created ' + created); + } + + this.targetBlocks = created; + print('BLOCKY TARGET BLOCKS:: ' + this.targetBlocks); + }, + createPlayableBlocks: function(arrangement) { + print('BLOCKY creating playable blocks' + arrangement.blocks.length); + arrangement.blocks.forEach(function(block) { + print('BLOCKY in a block loop') + var blockProps = { + name: "home_model_blocky_block", + type: 'Model', + collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + dynamic: true, + collisionless: false, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true + } + }), + dimensions: block.dimensions, + modelURL: block.url, + shapeType: 'box', + velocity: { + x: 0, + y: -0.01, + z: 0, + }, + position: PLAYABLE_BLOCKS_POSITION } - }), - dimensions: block.dimensions, - modelURL: block.url, - shapeType: 'box', - velocity: { - x: 0, - y: -0.01, - z: 0, - }, - position: PLAYABLE_BLOCKS_POSITION - } - var newBlock = Entities.addEntity(blockProps); - print('BLOCKY made a playable block' + newBlock) - _this.playableBlocks.push(newBlock); - print('BLOCKY pushing it into playable blocks'); - }) + var newBlock = Entities.addEntity(blockProps); + print('BLOCKY made a playable block' + newBlock) + _this.playableBlocks.push(newBlock); + print('BLOCKY pushing it into playable blocks'); + }) - print('BLOCKY after going through playable arrangement') - }, - startNearTrigger: function() { - print('BLOCKY got a near trigger'); - this.advanceRound(); - }, - advanceRound: function() { - print('BLOCKY advance round'); - this.cleanup(); - var arrangement = arrangements[Math.floor(Math.random() * arrangements.length)]; - this.createTargetBlocks(arrangement); + print('BLOCKY after going through playable arrangement') + }, + startNearTrigger: function() { + print('BLOCKY got a near trigger'); + this.advanceRound(); + }, + advanceRound: function() { + print('BLOCKY advance round'); + this.cleanup(); + var arrangement = arrangements[Math.floor(Math.random() * arrangements.length)]; + this.createTargetBlocks(arrangement); - if (this.debug === true) { - this.debugCreatePlayableBlocks(); - } else { - this.createPlayableBlocks(arrangement); + if (this.debug === true) { + this.debugCreatePlayableBlocks(); + } else { + this.createPlayableBlocks(arrangement); - } - }, - findBlocks: function() { - var found = []; - var results = Entities.findEntities(MyAvatar.position, 10); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - print('got result props') - if (properties.name.indexOf('blocky_block') > -1) { - found.push(result); - } - }); - return found; - }, - - cleanup: function() { - print('BLOCKY cleanup'); - var blocks = this.findBlocks(); - print('BLOCKY cleanup2' + blocks.length) - blocks.forEach(function(block) { - Entities.deleteEntity(block); - }) - print('BLOCKY after find and delete') - this.targetBlocks.forEach(function(block) { - Entities.deleteEntity(block); - }); - this.playableBlocks.forEach(function(block) { - Entities.deleteEntity(block); - }); - this.targetBlocks = []; - this.playableBlocks = []; - }, - debugCreatePlayableBlocks: function() { - print('BLOCKY debug create'); - var howMany = 10; - var i; - for (i = 0; i < howMany; i++) { - var block = blocks[Math.floor(Math.random() * blocks.length)]; - var blockProps = { - name: "home_model_blocky_block", - type: 'Model', - collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - dynamic: false, - collisionless: true, - // gravity: { - // x: 0, - // y: -9.8, - // z: 0 - // }, - userData: JSON.stringify({ - grabbableKey: { - grabbable: true - }, - hifiHomeKey: { - reset: true + } + }, + findBlocks: function() { + var found = []; + var results = Entities.findEntities(MyAvatar.position, 10); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + print('got result props') + if (properties.name.indexOf('blocky_block') > -1) { + found.push(result); } - }), - dimensions: block.dimensions, - modelURL: block.url, - shapeType: 'box', - velocity: { - x: 0, - y: -0.01, - z: 0, - }, - position: PLAYABLE_BLOCKS_POSITION - } - this.playableBlocks.push(Entities.addEntity(blockProps)); - } - }, - unload: function() { - this.cleanup(); - }, - clickReleaseOnEntity: function() { - print('BLOCKY click') - this.startNearTrigger(); - } - } + }); + return found; + }, - return new Blocky(); -}) \ No newline at end of file + cleanup: function() { + print('BLOCKY cleanup'); + var blocks = this.findBlocks(); + print('BLOCKY cleanup2' + blocks.length) + blocks.forEach(function(block) { + Entities.deleteEntity(block); + }) + print('BLOCKY after find and delete') + this.targetBlocks.forEach(function(block) { + Entities.deleteEntity(block); + }); + this.playableBlocks.forEach(function(block) { + Entities.deleteEntity(block); + }); + this.targetBlocks = []; + this.playableBlocks = []; + }, + debugCreatePlayableBlocks: function() { + print('BLOCKY debug create'); + var howMany = 10; + var i; + for (i = 0; i < howMany; i++) { + var block = blocks[Math.floor(Math.random() * blocks.length)]; + var blockProps = { + name: "home_model_blocky_block", + type: 'Model', + collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + dynamic: false, + collisionless: true, + // gravity: { + // x: 0, + // y: -9.8, + // z: 0 + // }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true + } + }), + dimensions: block.dimensions, + modelURL: block.url, + shapeType: 'box', + velocity: { + x: 0, + y: -0.01, + z: 0, + }, + position: PLAYABLE_BLOCKS_POSITION + } + this.playableBlocks.push(Entities.addEntity(blockProps)); + } + }, + unload: function() { + this.cleanup(); + }, + clickReleaseOnEntity: function() { + print('BLOCKY click') + this.startNearTrigger(); + } + } + + return new Blocky(); + }) \ No newline at end of file From 7dabce9cff02f74db3a9b737ca98b06708d57580 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 23 May 2016 19:04:35 -0700 Subject: [PATCH 0205/1237] Check throttle before idling/painting --- interface/src/Application.cpp | 45 +++++++++++++++++++---------------- interface/src/Application.h | 3 ++- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b0e4880011..7d1610c78e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -174,7 +174,6 @@ static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const int MSECS_PER_SEC = 1000; static const int MIRROR_VIEW_TOP_PADDING = 5; static const int MIRROR_VIEW_LEFT_PADDING = 10; static const int MIRROR_VIEW_WIDTH = 265; @@ -633,7 +632,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one - const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SEC; + const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; auto discoverabilityManager = DependencyManager::get(); connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); @@ -1828,9 +1827,9 @@ bool Application::event(QEvent* event) { // Presentation/painting logic // TODO: Decouple presentation and painting loops - static bool isPainting = false; + static bool isPaintingThrottled = false; if ((int)event->type() == (int)Present) { - if (isPainting) { + if (isPaintingThrottled) { // If painting (triggered by presentation) is hogging the main thread, // repost as low priority to avoid hanging the GUI. // This has the effect of allowing presentation to exceed the paint budget by X times and @@ -1838,14 +1837,17 @@ bool Application::event(QEvent* event) { // (e.g. at a 60FPS target, painting for 17us would fall to 58.82FPS instead of 30FPS). removePostedEvents(this, Present); postEvent(this, new QEvent(static_cast(Present)), Qt::LowEventPriority); - isPainting = false; + isPaintingThrottled = false; return true; } - idle(); - - postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); - isPainting = true; + float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); + if (shouldPaint(nsecsElapsed)) { + _lastTimeUpdated.start(); + idle(nsecsElapsed); + postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); + } + isPaintingThrottled = true; return true; } else if ((int)event->type() == (int)Paint) { @@ -1855,7 +1857,7 @@ bool Application::event(QEvent* event) { paintGL(); - isPainting = false; + isPaintingThrottled = false; return true; } @@ -2639,10 +2641,9 @@ bool Application::acceptSnapshot(const QString& urlString) { static uint32_t _renderedFrameIndex { INVALID_FRAME }; -void Application::idle() { - // idle is called on a queued connection, so make sure we should be here. - if (_inPaint || _aboutToQuit) { - return; +bool Application::shouldPaint(float nsecsElapsed) { + if (_aboutToQuit) { + return false; } auto displayPlugin = getActiveDisplayPlugin(); @@ -2661,16 +2662,21 @@ void Application::idle() { } #endif - float msecondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_USEC / USECS_PER_MSEC; + float msecondsSinceLastUpdate = nsecsElapsed / NSECS_PER_USEC / USECS_PER_MSEC; // Throttle if requested if (displayPlugin->isThrottled() && (msecondsSinceLastUpdate < THROTTLED_SIM_FRAME_PERIOD_MS)) { - return; + return false; } // Sync up the _renderedFrameIndex _renderedFrameIndex = displayPlugin->presentCount(); + return true; +} + +void Application::idle(float nsecsElapsed) { + // Update the deadlock watchdog updateHeartbeat(); @@ -2687,7 +2693,7 @@ void Application::idle() { PROFILE_RANGE(__FUNCTION__); - float secondsSinceLastUpdate = msecondsSinceLastUpdate / MSECS_PER_SECOND; + float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND; // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) { @@ -2697,9 +2703,6 @@ void Application::idle() { _keyboardDeviceHasFocus = true; } - // We're going to execute idle processing, so restart the last idle timer - _lastTimeUpdated.start(); - checkChangeCursor(); Stats::getInstance()->updateStats(); @@ -2926,7 +2929,7 @@ void Application::loadSettings() { } void Application::saveSettings() const { - sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SEC); + sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); diff --git a/interface/src/Application.h b/interface/src/Application.h index 28dbcead47..11a591776e 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -329,7 +329,8 @@ private: void cleanupBeforeQuit(); - void idle(); + bool shouldPaint(float nsecsElapsed); + void idle(float nsecsElapsed); void update(float deltaTime); // Various helper functions called during update() From 3391430f0a2f061c95a9bdf7f74bbfdbc0ab62ec Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 24 May 2016 20:28:49 +1200 Subject: [PATCH 0206/1237] Tidy zone flying and ghosting entities editor options Move options to top of section from under "skybox" subsection Fix capitalization of labels --- scripts/system/html/entityProperties.html | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index efe7e6cc65..67d168481b 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1717,6 +1717,14 @@
+
+ + +
+
+ + +
@@ -1801,15 +1809,6 @@
-
- - -
-
- - -
-
M From 1d9981e6242d7fda4e9e6db824948b5f6ad45a2a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 13:31:19 -0700 Subject: [PATCH 0207/1237] first cut at support for verifying all protocol version compatibility --- domain-server/src/DomainGatekeeper.cpp | 42 +++++++++++++---- domain-server/src/DomainGatekeeper.h | 5 +- domain-server/src/DomainServer.cpp | 39 +++++++++++++++ domain-server/src/DomainServer.h | 2 + domain-server/src/NodeConnectionData.cpp | 9 ++++ domain-server/src/NodeConnectionData.h | 2 + interface/src/Application.cpp | 23 ++++++++- interface/src/Application.h | 8 ++++ interface/src/Menu.cpp | 7 +++ interface/src/Menu.h | 1 + .../src/scripting/WindowScriptingInterface.h | 4 +- libraries/networking/src/DomainHandler.cpp | 47 +++++++++++++------ libraries/networking/src/DomainHandler.h | 12 ++++- libraries/networking/src/LimitedNodeList.cpp | 4 ++ libraries/networking/src/LimitedNodeList.h | 6 ++- libraries/networking/src/NLPacket.cpp | 9 ++-- libraries/networking/src/NLPacket.h | 4 +- libraries/networking/src/NodeList.cpp | 32 +++++++++++-- libraries/networking/src/NodeList.h | 15 ++++++ libraries/networking/src/PacketReceiver.cpp | 13 ++++- .../networking/src/udt/PacketHeaders.cpp | 43 ++++++++++++++++- libraries/networking/src/udt/PacketHeaders.h | 21 ++++++++- 22 files changed, 304 insertions(+), 44 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 61cc775e08..9d5ee75818 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -56,10 +56,24 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetVersion(); + + QDataStream packetStream(message->getMessage()); // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr()); + + QByteArray myProtocolVersion = protocolVersionsSignature(); + if (nodeConnection.protocolVersion != myProtocolVersion) { + QString protocolVersionError = "Protocol version mismatch - Domain version:" + QCoreApplication::applicationVersion(); + qDebug() << "Protocol Version mismatch - denying connection."; + sendConnectionDeniedPacket(protocolVersionError, message->getSenderSockAddr(), + DomainHandler::ConnectionRefusedReason::ProtocolMismatch); + return; + } + //qDebug() << __FUNCTION__ << "Protocol Version MATCH - continue with processing connection."; + if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) { qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection."; @@ -97,7 +111,9 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSenderSockAddr(); @@ -332,7 +351,7 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node bool DomainGatekeeper::verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr) { - + // it's possible this user can be allowed to connect, but we need to check their username signature QByteArray publicKeyArray = _userPublicKeys.value(username); @@ -370,7 +389,8 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, } else { if (!senderSockAddr.isNull()) { qDebug() << "Error decrypting username signature for " << username << "- denying connection."; - sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr); + sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr, + DomainHandler::ConnectionRefusedReason::LoginError); } // free up the public key, we don't need it anymore @@ -382,13 +402,15 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, // we can't let this user in since we couldn't convert their public key to an RSA key we could use if (!senderSockAddr.isNull()) { qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; - sendConnectionDeniedPacket("Couldn't convert data to RSA key.", senderSockAddr); + sendConnectionDeniedPacket("Couldn't convert data to RSA key.", senderSockAddr, + DomainHandler::ConnectionRefusedReason::LoginError); } } } else { if (!senderSockAddr.isNull()) { qDebug() << "Insufficient data to decrypt username signature - denying connection."; - sendConnectionDeniedPacket("Insufficient data", senderSockAddr); + sendConnectionDeniedPacket("Insufficient data", senderSockAddr, + DomainHandler::ConnectionRefusedReason::LoginError); } } @@ -402,7 +424,8 @@ bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByt if (username.isEmpty()) { qDebug() << "Connect request denied - no username provided."; - sendConnectionDeniedPacket("No username provided", senderSockAddr); + sendConnectionDeniedPacket("No username provided", senderSockAddr, + DomainHandler::ConnectionRefusedReason::LoginError); return false; } @@ -416,7 +439,8 @@ bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByt } } else { qDebug() << "Connect request denied for user" << username << "- not in allowed users list."; - sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr); + sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr, + DomainHandler::ConnectionRefusedReason::NotAuthorized); return false; } @@ -452,7 +476,8 @@ bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteA // deny connection from this user qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; - sendConnectionDeniedPacket("Too many connected users.", senderSockAddr); + sendConnectionDeniedPacket("Too many connected users.", senderSockAddr, + DomainHandler::ConnectionRefusedReason::TooManyUsers); return false; } @@ -516,7 +541,8 @@ void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) { } } -void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr) { +void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, + DomainHandler::ConnectionRefusedReason reasonCode) { // this is an agent and we've decided we won't let them connect - send them a packet to deny connection QByteArray utfString = reason.toUtf8(); quint16 payloadSize = utfString.size(); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index c4ac32fabf..09e3b04ed7 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -19,6 +19,8 @@ #include #include +#include + #include #include #include @@ -74,7 +76,8 @@ private: const HifiSockAddr& senderSockAddr); void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr); - void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr); + void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, + DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown); void pingPunchForConnectingPeer(const SharedNetworkPeer& peer); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index cfec72a24b..18ca7e2941 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -303,6 +303,36 @@ const QString FULL_AUTOMATIC_NETWORKING_VALUE = "full"; const QString IP_ONLY_AUTOMATIC_NETWORKING_VALUE = "ip"; const QString DISABLED_AUTOMATIC_NETWORKING_VALUE = "disabled"; + + +bool DomainServer::packetVersionMatch(const udt::Packet& packet) { + PacketType headerType = NLPacket::typeInHeader(packet); + PacketVersion headerVersion = NLPacket::versionInHeader(packet); + + //qDebug() << __FUNCTION__ << "type:" << headerType << "version:" << (int)headerVersion; + + auto nodeList = DependencyManager::get(); + + // This implements a special case that handles OLD clients which don't know how to negotiate matching + // protocol versions. We know these clients will sent DomainConnectRequest with older versions. We also + // know these clients will show a warning dialog if they get an EntityData with a protocol version they + // don't understand, so we can send them an empty EntityData with our latest version and they will + // warn the user that the protocol is not compatible + if (headerType == PacketType::DomainConnectRequest && + headerVersion < static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { + + //qDebug() << __FUNCTION__ << "OLD VERSION checkin sending an intentional bad packet -------------------------------"; + + auto packetWithBadVersion = NLPacket::create(PacketType::EntityData); + nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr()); + return false; + } + + // let the normal nodeList implementation handle all other packets. + return nodeList->isPacketVerified(packet); +} + + void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { const QString CUSTOM_LOCAL_PORT_OPTION = "metaverse.local_port"; @@ -376,6 +406,10 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); + + // set packetVersionMatch as the verify packet operator for the udt::Socket + //using std::placeholders::_1; + nodeList->setPacketFilterOperator(&DomainServer::packetVersionMatch); } const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; @@ -666,6 +700,8 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet message, SharedNodePointer sendingNode) { + + //qDebug() << __FUNCTION__ << "---------------"; QDataStream packetStream(message->getMessage()); NodeConnectionData nodeRequestData = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr(), false); @@ -746,6 +782,9 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) { } void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) { + + //qDebug() << __FUNCTION__ << "---------------"; + const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NUM_BYTES_RFC4122_UUID + 2; // setup the extended header for the domain list packets diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index fef3221b7d..c39e405380 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -99,6 +99,8 @@ private: void optionallyGetTemporaryName(const QStringList& arguments); + static bool packetVersionMatch(const udt::Packet& packet); + bool resetAccountManagerAccessToken(); void setupAutomaticNetworking(); diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 28f769298c..5ddcbf1792 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -19,6 +19,15 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c if (isConnectRequest) { dataStream >> newHeader.connectUUID; + + // Read out the protocol version signature from the connect message + char* rawBytes; + uint length; + + // FIXME -- do we need to delete the rawBytes after it's been copied into the QByteArray? + dataStream.readBytes(rawBytes, length); + newHeader.protocolVersion = QByteArray(rawBytes, length); + //qDebug() << __FUNCTION__ << "...got protocol version from node... version:" << newHeader.protocolVersion; } dataStream >> newHeader.nodeType diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h index 34119ffdab..9264db637e 100644 --- a/domain-server/src/NodeConnectionData.h +++ b/domain-server/src/NodeConnectionData.h @@ -28,6 +28,8 @@ public: HifiSockAddr senderSockAddr; QList interestList; QString placeName; + + QByteArray protocolVersion; }; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b0e4880011..821cd83c31 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -631,6 +631,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); + + connect(&domainHandler, &DomainHandler::resetting, nodeList.data(), &NodeList::resetDomainServerCheckInVersion); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SEC; @@ -653,7 +655,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); - connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); + + connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, this, &Application::limitOfSilentDomainCheckInsReached); + //connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); + + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); // connect to appropriate slots on AccountManager @@ -4569,6 +4575,21 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const { Physics::setSessionUUID(sessionUUID); } + +// If we're not getting anything back from the domain server checkin, it might be that the domain speaks an +// older version of the DomainConnectRequest protocal. We will attempt to send and older version of DomainConnectRequest. +// We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we +// need to be downgraded to talk to it). +void Application::limitOfSilentDomainCheckInsReached() { + //qDebug() << __FUNCTION__; + + auto nodeList = DependencyManager::get(); + + nodeList->downgradeDomainServerCheckInVersion(); // attempt to use an older domain checkin version + + nodeList->reset(); +} + bool Application::askToSetAvatarUrl(const QString& url) { QUrl realUrl(url); if (realUrl.isLocalFile()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 28dbcead47..edd1b6187d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -261,6 +261,12 @@ public slots: void resetSensors(bool andReload = false); void setActiveFaceTracker() const; +#if (PR_BUILD || DEV_BUILD) + void sendWrongProtocolVersionsSignature(bool checked) { + ::sendWrongProtocolVersionsSignature(checked); + } +#endif + #ifdef HAVE_IVIEWHMD void setActiveEyeTracker(); void calibrateEyeTracker1Point(); @@ -314,6 +320,8 @@ private slots: bool displayAvatarAttachmentConfirmationDialog(const QString& name) const; void setSessionUUID(const QUuid& sessionUUID) const; + void limitOfSilentDomainCheckInsReached(); + void domainChanged(const QString& domainHostname); void updateWindowTitle() const; void nodeAdded(SharedNodePointer node) const; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 538410a47d..a21aa71753 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -545,6 +545,13 @@ Menu::Menu() { addActionToQMenuAndActionHash(networkMenu, MenuOption::BandwidthDetails, 0, dialogsManager.data(), SLOT(bandwidthDetails())); + #if (PR_BUILD || DEV_BUILD) + addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false, + qApp, SLOT(sendWrongProtocolVersionsSignature(bool))); + #endif + + + // Developer > Timing >>> MenuWrapper* timingMenu = developerMenu->addMenu("Timing"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 36d285e2cf..fcaf8e6caa 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -167,6 +167,7 @@ namespace MenuOption { const QString RunTimingTests = "Run Timing Tests"; const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; + const QString SendWrongProtocolVersion = "Send wrong protocol version"; const QString SetHomeLocation = "Set Home Location"; const QString ShowDSConnectTable = "Show Domain Connection Timing"; const QString ShowBordersEntityNodes = "Show Entity Nodes"; diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 0f51a484c4..145d17faaf 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -16,6 +16,8 @@ #include #include +#include + class WebWindowClass; class WindowScriptingInterface : public QObject, public Dependency { @@ -45,7 +47,7 @@ public slots: signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); - void domainConnectionRefused(const QString& reason); + void domainConnectionRefused(const QString& reasonMessage, DomainHandler::ConnectionRefusedReason reason = DomainHandler::ConnectionRefusedReason::Unknown); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 44ce63e6c6..08810010a6 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -355,34 +355,53 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes } } +bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) { + switch (reasonCode) { + case ConnectionRefusedReason::LoginError: + case ConnectionRefusedReason::NotAuthorized: + return true; + + default: + case ConnectionRefusedReason::Unknown: + case ConnectionRefusedReason::ProtocolMismatch: + case ConnectionRefusedReason::TooManyUsers: + return false; + } + return false; +} + void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer message) { // Read deny reason from packet + ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown; quint16 reasonSize; message->readPrimitive(&reasonSize); - QString reason = QString::fromUtf8(message->readWithoutCopy(reasonSize)); + QString reasonMessage = QString::fromUtf8(message->readWithoutCopy(reasonSize)); // output to the log so the user knows they got a denied connection request // and check and signal for an access token so that we can make sure they are logged in - qCWarning(networking) << "The domain-server denied a connection request: " << reason; + qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage; qCWarning(networking) << "Make sure you are logged in."; - if (!_domainConnectionRefusals.contains(reason)) { - _domainConnectionRefusals.append(reason); - emit domainConnectionRefused(reason); + if (!_domainConnectionRefusals.contains(reasonMessage)) { + _domainConnectionRefusals.append(reasonMessage); + emit domainConnectionRefused(reasonMessage, reasonCode); } auto accountManager = DependencyManager::get(); - if (!_hasCheckedForAccessToken) { - accountManager->checkAndSignalForAccessToken(); - _hasCheckedForAccessToken = true; - } + // Some connection refusal reasons imply that a login is required. If so, suggest a new login + if (reasonSuggestsLogin(reasonCode)) { + if (!_hasCheckedForAccessToken) { + accountManager->checkAndSignalForAccessToken(); + _hasCheckedForAccessToken = true; + } - static const int CONNECTION_DENIALS_FOR_KEYPAIR_REGEN = 3; + static const int CONNECTION_DENIALS_FOR_KEYPAIR_REGEN = 3; - // force a re-generation of key-pair after CONNECTION_DENIALS_FOR_KEYPAIR_REGEN failed connection attempts - if (++_connectionDenialsSinceKeypairRegen >= CONNECTION_DENIALS_FOR_KEYPAIR_REGEN) { - accountManager->generateNewUserKeypair(); - _connectionDenialsSinceKeypairRegen = 0; + // force a re-generation of key-pair after CONNECTION_DENIALS_FOR_KEYPAIR_REGEN failed connection attempts + if (++_connectionDenialsSinceKeypairRegen >= CONNECTION_DENIALS_FOR_KEYPAIR_REGEN) { + accountManager->generateNewUserKeypair(); + _connectionDenialsSinceKeypairRegen = 0; + } } } diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 03141d8fef..4d8505e549 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -84,6 +84,15 @@ public: bool isSocketKnown() const { return !_sockAddr.getAddress().isNull(); } void softReset(); + + enum class ConnectionRefusedReason : uint8_t { + Unknown, + ProtocolMismatch, + LoginError, + NotAuthorized, + TooManyUsers + }; + public slots: void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); @@ -115,9 +124,10 @@ signals: void settingsReceived(const QJsonObject& domainSettingsObject); void settingsReceiveFail(); - void domainConnectionRefused(QString reason); + void domainConnectionRefused(QString reasonMessage, ConnectionRefusedReason reason = ConnectionRefusedReason::Unknown); private: + bool reasonSuggestsLogin(ConnectionRefusedReason reasonCode); void sendDisconnectPacket(); void hardReset(); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 2c10d0627e..714b69fd89 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -162,13 +162,17 @@ QUdpSocket& LimitedNodeList::getDTLSSocket() { } bool LimitedNodeList::isPacketVerified(const udt::Packet& packet) { + //qDebug() << __FUNCTION__; return packetVersionMatch(packet) && packetSourceAndHashMatch(packet); } bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { + PacketType headerType = NLPacket::typeInHeader(packet); PacketVersion headerVersion = NLPacket::versionInHeader(packet); + //qDebug() << __FUNCTION__ << "headerType:" << headerType << "version:" << (int)headerVersion; + if (headerVersion != versionForPacketType(headerType)) { static QMultiHash sourcedVersionDebugSuppressMap; diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 0cbe9668b3..3b648a138b 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -221,6 +221,10 @@ public: void setConnectionMaxBandwidth(int maxBandwidth) { _nodeSocket.setConnectionMaxBandwidth(maxBandwidth); } + void setPacketFilterOperator(udt::PacketFilterOperator filterOperator) { _nodeSocket.setPacketFilterOperator(filterOperator); } + bool packetVersionMatch(const udt::Packet& packet); + bool isPacketVerified(const udt::Packet& packet); + public slots: void reset(); void eraseAllNodes(); @@ -267,8 +271,6 @@ protected: void setLocalSocket(const HifiSockAddr& sockAddr); - bool isPacketVerified(const udt::Packet& packet); - bool packetVersionMatch(const udt::Packet& packet); bool packetSourceAndHashMatch(const udt::Packet& packet); void processSTUNResponse(std::unique_ptr packet); diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 575a2c7a9c..93f8659663 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -24,8 +24,8 @@ int NLPacket::maxPayloadSize(PacketType type, bool isPartOfMessage) { return Packet::maxPayloadSize(isPartOfMessage) - NLPacket::localHeaderSize(type); } -std::unique_ptr NLPacket::create(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage) { - auto packet = std::unique_ptr(new NLPacket(type, size, isReliable, isPartOfMessage)); +std::unique_ptr NLPacket::create(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage, PacketVersion version) { + auto packet = std::unique_ptr(new NLPacket(type, size, isReliable, isPartOfMessage, version)); packet->open(QIODevice::ReadWrite); @@ -61,13 +61,12 @@ std::unique_ptr NLPacket::createCopy(const NLPacket& other) { return std::unique_ptr(new NLPacket(other)); } -NLPacket::NLPacket(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage) : +NLPacket::NLPacket(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage, PacketVersion version) : Packet((size == -1) ? -1 : NLPacket::localHeaderSize(type) + size, isReliable, isPartOfMessage), _type(type), - _version(versionForPacketType(type)) + _version((version == 0) ? versionForPacketType(type) : version) { adjustPayloadStartAndCapacity(NLPacket::localHeaderSize(_type)); - writeTypeAndVersion(); } diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index 4527094322..f49f8498a5 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -38,7 +38,7 @@ public: sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID + NUM_BYTES_MD5_HASH; static std::unique_ptr create(PacketType type, qint64 size = -1, - bool isReliable = false, bool isPartOfMessage = false); + bool isReliable = false, bool isPartOfMessage = false, PacketVersion version = 0); static std::unique_ptr fromReceivedPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); @@ -73,7 +73,7 @@ public: protected: - NLPacket(PacketType type, qint64 size = -1, bool forceReliable = false, bool isPartOfMessage = false); + NLPacket(PacketType type, qint64 size = -1, bool forceReliable = false, bool isPartOfMessage = false, PacketVersion version = 0); NLPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); NLPacket(const NLPacket& other); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index c295ffc700..50677908a5 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -292,7 +292,9 @@ void NodeList::sendDomainServerCheckIn() { return; } - auto domainPacket = NLPacket::create(domainPacketType); + auto packetVersion = (domainPacketType == PacketType::DomainConnectRequest) ? _domainConnectRequestVersion : 0; + //qDebug() << __FUNCTION__ << " NLPacket::create() version:" << (int)packetVersion; + auto domainPacket = NLPacket::create(domainPacketType, -1, false, false, packetVersion); QDataStream packetStream(domainPacket.get()); @@ -312,12 +314,28 @@ void NodeList::sendDomainServerCheckIn() { // pack the connect UUID for this connect request packetStream << connectUUID; + + // include the protocol version signature in our connect request + if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { + QByteArray protocolVersionSig = protocolVersionsSignature(); + packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); + //qDebug() << __FUNCTION__ << " including protocol version --------------------------"; + } else { + //qDebug() << __FUNCTION__ << "_domainConnectRequestVersion less than HasProtocolVersions - not including protocol version"; + } + } else { + //qDebug() << __FUNCTION__ << "NOT a DomainConnnectRequest ----------- not including checkin details -------"; } // pack our data to send to the domain-server including // the hostname information (so the domain-server can see which place name we came in on) - packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList() - << DependencyManager::get()->getPlaceName(); + packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); + if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasHostname)) { + packetStream << DependencyManager::get()->getPlaceName(); + //qDebug() << __FUNCTION__ << " including host name --------------------------"; + } else { + //qDebug() << __FUNCTION__ << "_domainConnectRequestVersion less than HasHostname - not including host name"; + } if (!_domainHandler.isConnected()) { DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); @@ -345,6 +363,7 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; + //qDebug() << __FUNCTION__ << " _numNoReplyDomainCheckIns:" << _numNoReplyDomainCheckIns << " --------------------------"; } } @@ -504,15 +523,22 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { + //qDebug() << __FUNCTION__; + if (_domainHandler.getSockAddr().isNull()) { // refuse to process this packet if we aren't currently connected to the DS return; } + //qDebug() << __FUNCTION__ << "_numNoReplyDomainCheckIns:" << _numNoReplyDomainCheckIns; + // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; + //qDebug() << __FUNCTION__ << "RESET.... _numNoReplyDomainCheckIns:" << _numNoReplyDomainCheckIns; + // emit our signal so listeners know we just heard from the DS + //qDebug() << __FUNCTION__ << "about to emit receivedDomainServerList() -----------------------------------------------"; emit receivedDomainServerList(); DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 4b196d5f7b..3158262c87 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -68,6 +68,13 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } + /// downgrades the DomainConnnectRequest PacketVersion to attempt to probe for older domain servers + void downgradeDomainServerCheckInVersion() { + qDebug() << __FUNCTION__ << "----------------------------------------------------------"; + _domainConnectRequestVersion--; + + } + public slots: void reset(); void sendDomainServerCheckIn(); @@ -85,6 +92,12 @@ public slots: void processICEPingPacket(QSharedPointer message); + void resetDomainServerCheckInVersion() + { + qDebug() << __FUNCTION__ << "----------------------------------------------------------"; + _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); + } + signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); @@ -123,6 +136,8 @@ private: HifiSockAddr _assignmentServerSocket; bool _isShuttingDown { false }; QTimer _keepAlivePingTimer; + + PacketVersion _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); }; #endif // hifi_NodeList_h diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index 530efc5fb3..cb47625d6d 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -309,22 +309,29 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(SharedNodePointer, matchingNode)); - + + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; + } else if (metaMethod.parameterTypes().contains(QSHAREDPOINTER_NODE_NORMALIZED)) { success = metaMethod.invoke(listener.object, connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(QSharedPointer, matchingNode)); - + + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; + } else { success = metaMethod.invoke(listener.object, connectionType, Q_ARG(QSharedPointer, receivedMessage)); + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } } else { listenerIsDead = true; } } else { + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "Got verified unsourced packet list." << "packetType:" << packetType; + // qDebug() << "Got verified unsourced packet list: " << QString(nlPacketList->getMessage()); emit dataReceived(NodeType::Unassigned, receivedMessage->getSize()); @@ -332,6 +339,8 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei if (listener.object) { success = listener.method.invoke(listener.object, Q_ARG(QSharedPointer, receivedMessage)); + + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } else { listenerIsDead = true; } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index b04d582d6d..72875322f6 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -12,7 +12,9 @@ #include "PacketHeaders.h" #include +#include +#include #include #include @@ -58,9 +60,13 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AssetUpload: // Removal of extension from Asset requests return 18; + + case PacketType::DomainConnectionDenied: + return static_cast(DomainConnectionDeniedVersion::IncludesReasonCode); + case PacketType::DomainConnectRequest: - // addition of referring hostname information - return 18; + return static_cast(DomainConnectRequestVersion::HasProtocolVersions); + default: return 17; } @@ -80,3 +86,36 @@ QDebug operator<<(QDebug debug, const PacketType& type) { debug.nospace().noquote() << (uint8_t) type << " (" << typeName << ")"; return debug.space(); } + +#if (PR_BUILD || DEV_BUILD) +static bool sendWrongProtocolVersion = false; +void sendWrongProtocolVersionsSignature(bool sendWrongVersion) { + sendWrongProtocolVersion = sendWrongVersion; +} +#endif + +QByteArray protocolVersionsSignature() { + static QByteArray protocolVersionSignature; + static std::once_flag once; + std::call_once(once, [&] { + QByteArray buffer; + QDataStream stream(&buffer, QIODevice::WriteOnly); + uint8_t numberOfProtocols = static_cast(PacketType::LAST_PACKET_TYPE) + 1; + stream << numberOfProtocols; + for (uint8_t packetType = 0; packetType < numberOfProtocols; packetType++) { + uint8_t packetTypeVersion = static_cast(versionForPacketType(static_cast(packetType))); + stream << packetTypeVersion; + } + QCryptographicHash hash(QCryptographicHash::Md5); + hash.addData(buffer); + protocolVersionSignature = hash.result(); + }); + + #if (PR_BUILD || DEV_BUILD) + if (sendWrongProtocolVersion) { + return QByteArray("INCORRECTVERSION"); // only for debugging version checking + } + #endif + + return protocolVersionSignature; +} diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 030b4af8c9..b6237e74d6 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -61,7 +61,7 @@ public: AssignmentClientStatus, NoisyMute, AvatarIdentity, - AvatarBillboard, + TYPE_UNUSED_1, DomainConnectRequest, DomainServerRequireDTLS, NodeJsonStats, @@ -94,7 +94,8 @@ public: ICEServerHeartbeatDenied, AssetMappingOperation, AssetMappingOperationReply, - ICEServerHeartbeatACK + ICEServerHeartbeatACK, + LAST_PACKET_TYPE = ICEServerHeartbeatACK }; }; @@ -109,6 +110,11 @@ extern const QSet NON_SOURCED_PACKETS; extern const QSet RELIABLE_PACKETS; PacketVersion versionForPacketType(PacketType packetType); +QByteArray protocolVersionsSignature(); /// returns a unqiue signature for all the current protocols + +#if (PR_BUILD || DEV_BUILD) +void sendWrongProtocolVersionsSignature(bool sendWrongVersion); /// for debugging version negotiation +#endif uint qHash(const PacketType& key, uint seed); QDebug operator<<(QDebug debug, const PacketType& type); @@ -179,4 +185,15 @@ enum class AvatarMixerPacketVersion : PacketVersion { AvatarEntities }; +enum class DomainConnectRequestVersion : PacketVersion { + NoHostname = 17, + HasHostname, + HasProtocolVersions +}; + +enum class DomainConnectionDeniedVersion : PacketVersion { + ReasonMessageOnly = 17, + IncludesReasonCode +}; + #endif // hifi_PacketHeaders_h From 12a1857280f835db093cd12ece0cd1583d43520b Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 15:20:11 -0700 Subject: [PATCH 0208/1237] check point with protocol refusal working --- domain-server/src/DomainGatekeeper.cpp | 17 +++++++- interface/src/Application.cpp | 13 +++++- interface/src/Application.h | 1 + .../src/scripting/WindowScriptingInterface.h | 2 +- libraries/networking/src/DomainHandler.cpp | 41 ++++++++++++++++--- libraries/networking/src/DomainHandler.h | 2 +- libraries/networking/src/PacketReceiver.cpp | 10 ++--- 7 files changed, 72 insertions(+), 14 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 9d5ee75818..80137935ca 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -548,12 +548,27 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H quint16 payloadSize = utfString.size(); // setup the DomainConnectionDenied packet - auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, payloadSize + sizeof(payloadSize)); + auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied); // , payloadSize + sizeof(payloadSize) // pack in the reason the connection was denied (the client displays this) if (payloadSize > 0) { + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); + qDebug() << __FUNCTION__ << "about to write reasonCode:" << (int)reasonCode; + uint8_t reasonCodeWire = (uint8_t)reasonCode; + qDebug() << __FUNCTION__ << "about to write reasonCodeWire:" << (int)reasonCodeWire; + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); + connectionDeniedPacket->writePrimitive(reasonCodeWire); + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); + qDebug() << __FUNCTION__ << "about to write payloadSize:" << payloadSize; + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); connectionDeniedPacket->writePrimitive(payloadSize); + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); + qDebug() << __FUNCTION__ << "about to write utfString:" << utfString; + qDebug() << __FUNCTION__ << "about to write utfString.size():" << utfString.size(); + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); connectionDeniedPacket->write(utfString); + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); + } // send the packet off diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index eca3ecc679..58a273737c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -630,8 +630,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); - connect(&domainHandler, &DomainHandler::resetting, nodeList.data(), &NodeList::resetDomainServerCheckInVersion); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; @@ -1068,6 +1068,17 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : firstRun.set(false); } +void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) { + qDebug() << __FUNCTION__ << "message:" << reasonMessage << "code:" << reasonCode; + qDebug() << __FUNCTION__ << "DomainHandler::ConnectionRefusedReason::ProtocolMismatch:" << (int)DomainHandler::ConnectionRefusedReason::ProtocolMismatch; + + if (static_cast(reasonCode) == DomainHandler::ConnectionRefusedReason::ProtocolMismatch) { + qDebug() << __FUNCTION__ << " PROTOCOL MISMATCH!!!"; + notifyPacketVersionMismatch(); + } +} + + QString Application::getUserAgent() { if (QThread::currentThread() != thread()) { QString userAgent; diff --git a/interface/src/Application.h b/interface/src/Application.h index edb69eb2ad..69f48e9541 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -330,6 +330,7 @@ private slots: static void packetSent(quint64 length); void updateDisplayMode(); void updateInputModes(); + void domainConnectionRefused(const QString& reasonMessage, int reason); private: static void initDisplay(); diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 145d17faaf..72f4ccd866 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -47,7 +47,7 @@ public slots: signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); - void domainConnectionRefused(const QString& reasonMessage, DomainHandler::ConnectionRefusedReason reason = DomainHandler::ConnectionRefusedReason::Unknown); + void domainConnectionRefused(const QString& reasonMessage, int reason); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index d80d4e76fd..67a41c866a 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -103,7 +103,9 @@ void DomainHandler::hardReset() { _sockAddr.clear(); _hasCheckedForAccessToken = false; - _domainConnectionRefusals.clear(); + + //qDebug() << __FUNCTION__ << "about to call _domainConnectionRefusals.clear();"; + //_domainConnectionRefusals.clear(); // clear any pending path we may have wanted to ask the previous DS about _pendingPath.clear(); @@ -142,6 +144,10 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const // set the new hostname _hostname = hostname; + // FIXME - is this the right place??? + qDebug() << __FUNCTION__ << "about to call _domainConnectionRefusals.clear();"; + _domainConnectionRefusals.clear(); + qCDebug(networking) << "Updated domain hostname to" << _hostname; // re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname @@ -366,25 +372,50 @@ bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) { void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer message) { // Read deny reason from packet - ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown; + uint8_t reasonCodeWire; + + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "message->getPosition():" << message->getPosition(); + message->readPrimitive(&reasonCodeWire); + qDebug() << __FUNCTION__ << "reasonCodeWire:" << reasonCodeWire; + ConnectionRefusedReason reasonCode = static_cast(reasonCodeWire); + qDebug() << __FUNCTION__ << "reasonCode:" << (int)reasonCode; + + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "message->getPosition():" << message->getPosition(); + quint16 reasonSize; message->readPrimitive(&reasonSize); - QString reasonMessage = QString::fromUtf8(message->readWithoutCopy(reasonSize)); + qDebug() << __FUNCTION__ << "reasonSize:" << reasonSize; + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "message->getPosition():" << message->getPosition(); + auto reasonText = message->readWithoutCopy(reasonSize); + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "reasonText:" << reasonText; + QString reasonMessage = QString::fromUtf8(reasonText); + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "reasonMessage:" << reasonMessage; + + qDebug() << __FUNCTION__ << "line:" << __LINE__ << "message->getPosition():" << message->getPosition(); // output to the log so the user knows they got a denied connection request // and check and signal for an access token so that we can make sure they are logged in qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage; - qCWarning(networking) << "Make sure you are logged in."; + + qDebug(networking) << "_domainConnectionRefusals:" << _domainConnectionRefusals; if (!_domainConnectionRefusals.contains(reasonMessage)) { + qDebug(networking) << "about to call _domainConnectionRefusals.append(reasonMessage);"; _domainConnectionRefusals.append(reasonMessage); - emit domainConnectionRefused(reasonMessage, reasonCode); + qDebug(networking) << "_domainConnectionRefusals:" << _domainConnectionRefusals; + + + emit domainConnectionRefused(reasonMessage, (int)reasonCode); + } else { + qDebug(networking) << "ALREADY EMITTED domainConnectionRefused() ----------------------------"; } auto accountManager = DependencyManager::get(); // Some connection refusal reasons imply that a login is required. If so, suggest a new login if (reasonSuggestsLogin(reasonCode)) { + qCWarning(networking) << "Make sure you are logged in."; + if (!_hasCheckedForAccessToken) { accountManager->checkAndSignalForAccessToken(); _hasCheckedForAccessToken = true; diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 41a3e544f6..226186f1d0 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -124,7 +124,7 @@ signals: void settingsReceived(const QJsonObject& domainSettingsObject); void settingsReceiveFail(); - void domainConnectionRefused(QString reasonMessage, ConnectionRefusedReason reason = ConnectionRefusedReason::Unknown); + void domainConnectionRefused(QString reasonMessage, int reason); private: bool reasonSuggestsLogin(ConnectionRefusedReason reasonCode); diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index cb47625d6d..8df9a1038a 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -310,7 +310,7 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei Q_ARG(QSharedPointer, receivedMessage), Q_ARG(SharedNodePointer, matchingNode)); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; + //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } else if (metaMethod.parameterTypes().contains(QSHAREDPOINTER_NODE_NORMALIZED)) { success = metaMethod.invoke(listener.object, @@ -318,19 +318,19 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei Q_ARG(QSharedPointer, receivedMessage), Q_ARG(QSharedPointer, matchingNode)); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; + //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } else { success = metaMethod.invoke(listener.object, connectionType, Q_ARG(QSharedPointer, receivedMessage)); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; + //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } } else { listenerIsDead = true; } } else { - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "Got verified unsourced packet list." << "packetType:" << packetType; + //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "Got verified unsourced packet list." << "packetType:" << packetType; // qDebug() << "Got verified unsourced packet list: " << QString(nlPacketList->getMessage()); emit dataReceived(NodeType::Unassigned, receivedMessage->getSize()); @@ -340,7 +340,7 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei success = listener.method.invoke(listener.object, Q_ARG(QSharedPointer, receivedMessage)); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; + //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } else { listenerIsDead = true; } From f1e826b3050bc22e28be39f1e1f18a4bfcd49c38 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 24 May 2016 15:28:11 -0700 Subject: [PATCH 0209/1237] make blocky swipable --- .../Home/blocky/arrangement6B.json | 5 - .../DomainContent/Home/blocky/blocky.js | 426 ++++++++++-------- .../DomainContent/Home/blocky/wrapper.js | 11 +- .../DomainContent/Home/reset.js | 9 +- 4 files changed, 236 insertions(+), 215 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json b/unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json index edced193a8..2a0510e259 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json +++ b/unpublishedScripts/DomainContent/Home/blocky/arrangement6B.json @@ -200,11 +200,6 @@ }, { "angularDamping": 0, - "angularVelocity": { - "x": 0, - "y": -0.2617993950843811, - "z": 0 - }, "collisionSoundURL": "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", "created": "2016-05-09T19:30:40Z", "dimensions": { diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js index 56567cc971..c7e35d15e7 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/blocky.js +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -46,213 +46,239 @@ var blocks = [ ]; var arrangements = [{ - name: 'greenhenge', - blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_YELLOW], - target: "atp:/blocky/newblocks1.json" - }, - { - name: 'tallstuff', - blocks: [BLOCK_RED, BLOCK_RED, BLOCK_RED], - target: "atp:/blocky/newblocks2.json" - }, - { - name: 'ostrich', - blocks: [BLOCK_RED, BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_NATURAL], - target: "atp:/blocky/newblocks5.json" - }, - { - name: 'fourppl', - blocks: [BLOCK_BLUE, BLOCK_BLUE, BLOCK_BLUE, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], - target: "atp:/blocky/newblocks3.json" - }, - { - name: 'frogguy', - blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_RED, BLOCK_RED, BLOCK_NATURAL, BLOCK_NATURAL], - target: "atp:/blocky/newblocks4.json" - }, - { - name: 'dominoes', - blocks: [BLOCK_RED, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW], - target: "atp:/blocky/arrangement6B.json" - }] + name: 'greenhenge', + blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_YELLOW], + target: "atp:/blocky/newblocks1.json" +}, { + name: 'tallstuff', + blocks: [BLOCK_RED, BLOCK_RED, BLOCK_RED], + target: "atp:/blocky/newblocks2.json" +}, { + name: 'ostrich', + blocks: [BLOCK_RED, BLOCK_RED, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_NATURAL], + target: "atp:/blocky/newblocks5.json" +}, { + name: 'fourppl', + blocks: [BLOCK_BLUE, BLOCK_BLUE, BLOCK_BLUE, BLOCK_BLUE, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL, BLOCK_NATURAL], + target: "atp:/blocky/newblocks3.json" +}, { + name: 'frogguy', + blocks: [BLOCK_GREEN, BLOCK_GREEN, BLOCK_GREEN, BLOCK_YELLOW, BLOCK_RED, BLOCK_RED, BLOCK_NATURAL, BLOCK_NATURAL], + target: "atp:/blocky/newblocks4.json" +}, { + name: 'dominoes', + blocks: [BLOCK_RED, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW, BLOCK_YELLOW], + target: "atp:/blocky/arrangement6B.json" +}] - var PLAYABLE_BLOCKS_POSITION = { - x: 1097.6, - y: 460.5, - z: -66.22 - }; +var PLAYABLE_BLOCKS_POSITION = { + x: 1097.6, + y: 460.5, + z: -66.22 +}; - var TARGET_BLOCKS_POSITION = { - x: 1096.82, - y: 460.5, - z: -67.689 - }; - //#debug - (function() { +var TARGET_BLOCKS_POSITION = { + x: 1096.82, + y: 460.5, + z: -67.689 +}; +//#debug +(function() { - print('BLOCK ENTITY SCRIPT') - var _this; + print('BLOCK ENTITY SCRIPT') + var _this; - function Blocky() { - _this = this; + function Blocky() { + _this = this; + } + + Blocky.prototype = { + busy: false, + debug: false, + playableBlocks: [], + targetBlocks: [], + preload: function(entityID) { + print('BLOCKY preload') + this.entityID = entityID; + Script.update.connect(_this.update); + }, + + createTargetBlocks: function(arrangement) { + var created = []; + print('BLOCKY create target blocks') + + var created = []; + var success = Clipboard.importEntities(arrangement.target); + if (success === true) { + created = Clipboard.pasteEntities(TARGET_BLOCKS_POSITION) + print('created ' + created); } - Blocky.prototype = { - debug: false, - playableBlocks: [], - targetBlocks: [], - preload: function(entityID) { - print('BLOCKY preload') - this.entityID = entityID; - }, - createTargetBlocks: function(arrangement) { - var created = []; - print('BLOCKY create target blocks') + this.targetBlocks = created; + print('BLOCKY TARGET BLOCKS:: ' + this.targetBlocks); + }, - var created = []; - var success = Clipboard.importEntities(arrangement.target); - if (success === true) { - created = Clipboard.pasteEntities(TARGET_BLOCKS_POSITION) - print('created ' + created); - } - - this.targetBlocks = created; - print('BLOCKY TARGET BLOCKS:: ' + this.targetBlocks); - }, - createPlayableBlocks: function(arrangement) { - print('BLOCKY creating playable blocks' + arrangement.blocks.length); - arrangement.blocks.forEach(function(block) { - print('BLOCKY in a block loop') - var blockProps = { - name: "home_model_blocky_block", - type: 'Model', - collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - dynamic: true, - collisionless: false, - gravity: { - x: 0, - y: -9.8, - z: 0 - }, - userData: JSON.stringify({ - grabbableKey: { - grabbable: true - }, - hifiHomeKey: { - reset: true - } - }), - dimensions: block.dimensions, - modelURL: block.url, - shapeType: 'box', - velocity: { - x: 0, - y: -0.01, - z: 0, - }, - position: PLAYABLE_BLOCKS_POSITION + createPlayableBlocks: function(arrangement) { + print('BLOCKY creating playable blocks' + arrangement.blocks.length); + arrangement.blocks.forEach(function(block) { + print('BLOCKY in a block loop') + var blockProps = { + name: "home_model_blocky_block", + type: 'Model', + collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + dynamic: true, + collisionless: false, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true } - var newBlock = Entities.addEntity(blockProps); - print('BLOCKY made a playable block' + newBlock) - _this.playableBlocks.push(newBlock); - print('BLOCKY pushing it into playable blocks'); - }) - - print('BLOCKY after going through playable arrangement') - }, - startNearTrigger: function() { - print('BLOCKY got a near trigger'); - this.advanceRound(); - }, - advanceRound: function() { - print('BLOCKY advance round'); - this.cleanup(); - var arrangement = arrangements[Math.floor(Math.random() * arrangements.length)]; - this.createTargetBlocks(arrangement); - - if (this.debug === true) { - this.debugCreatePlayableBlocks(); - } else { - this.createPlayableBlocks(arrangement); - - } - }, - findBlocks: function() { - var found = []; - var results = Entities.findEntities(MyAvatar.position, 10); - results.forEach(function(result) { - var properties = Entities.getEntityProperties(result); - print('got result props') - if (properties.name.indexOf('blocky_block') > -1) { - found.push(result); - } - }); - return found; - }, - - cleanup: function() { - print('BLOCKY cleanup'); - var blocks = this.findBlocks(); - print('BLOCKY cleanup2' + blocks.length) - blocks.forEach(function(block) { - Entities.deleteEntity(block); - }) - print('BLOCKY after find and delete') - this.targetBlocks.forEach(function(block) { - Entities.deleteEntity(block); - }); - this.playableBlocks.forEach(function(block) { - Entities.deleteEntity(block); - }); - this.targetBlocks = []; - this.playableBlocks = []; - }, - debugCreatePlayableBlocks: function() { - print('BLOCKY debug create'); - var howMany = 10; - var i; - for (i = 0; i < howMany; i++) { - var block = blocks[Math.floor(Math.random() * blocks.length)]; - var blockProps = { - name: "home_model_blocky_block", - type: 'Model', - collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", - dynamic: false, - collisionless: true, - // gravity: { - // x: 0, - // y: -9.8, - // z: 0 - // }, - userData: JSON.stringify({ - grabbableKey: { - grabbable: true - }, - hifiHomeKey: { - reset: true - } - }), - dimensions: block.dimensions, - modelURL: block.url, - shapeType: 'box', - velocity: { - x: 0, - y: -0.01, - z: 0, - }, - position: PLAYABLE_BLOCKS_POSITION - } - this.playableBlocks.push(Entities.addEntity(blockProps)); - } - }, - unload: function() { - this.cleanup(); - }, - clickReleaseOnEntity: function() { - print('BLOCKY click') - this.startNearTrigger(); + }), + dimensions: block.dimensions, + modelURL: block.url, + shapeType: 'box', + velocity: { + x: 0, + y: -0.01, + z: 0, + }, + position: PLAYABLE_BLOCKS_POSITION } - } + var newBlock = Entities.addEntity(blockProps); + print('BLOCKY made a playable block' + newBlock) + _this.playableBlocks.push(newBlock); + print('BLOCKY pushing it into playable blocks'); + }) - return new Blocky(); - }) \ No newline at end of file + print('BLOCKY after going through playable arrangement') + }, + + startNearTrigger: function() { + print('BLOCKY got a near trigger'); + this.advanceRound(); + }, + + advanceRound: function() { + print('BLOCKY advance round'); + this.busy = true; + this.cleanup(); + var arrangement = arrangements[Math.floor(Math.random() * arrangements.length)]; + this.createTargetBlocks(arrangement); + + if (this.debug === true) { + this.debugCreatePlayableBlocks(); + } else { + this.createPlayableBlocks(arrangement); + + } + this.busy = false; + }, + + findBlocks: function() { + var found = []; + var results = Entities.findEntities(MyAvatar.position, 10); + results.forEach(function(result) { + var properties = Entities.getEntityProperties(result); + print('got result props') + if (properties.name.indexOf('blocky_block') > -1) { + found.push(result); + } + }); + return found; + }, + + cleanup: function() { + print('BLOCKY cleanup'); + var blocks = this.findBlocks(); + print('BLOCKY cleanup2' + blocks.length) + blocks.forEach(function(block) { + Entities.deleteEntity(block); + }) + print('BLOCKY after find and delete') + this.targetBlocks.forEach(function(block) { + Entities.deleteEntity(block); + }); + this.playableBlocks.forEach(function(block) { + Entities.deleteEntity(block); + }); + this.targetBlocks = []; + this.playableBlocks = []; + }, + + debugCreatePlayableBlocks: function() { + print('BLOCKY debug create'); + var howMany = 10; + var i; + for (i = 0; i < howMany; i++) { + var block = blocks[Math.floor(Math.random() * blocks.length)]; + var blockProps = { + name: "home_model_blocky_block", + type: 'Model', + collisionSoundURL: "atp:/kineticObjects/blocks/ToyWoodBlock.L.wav", + dynamic: false, + collisionless: true, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true + } + }), + dimensions: block.dimensions, + modelURL: block.url, + shapeType: 'box', + velocity: { + x: 0, + y: -0.01, + z: 0, + }, + position: PLAYABLE_BLOCKS_POSITION + } + this.playableBlocks.push(Entities.addEntity(blockProps)); + } + }, + + unload: function() { + this.cleanup(); + Script.update.disconnect(_this.update); + }, + + clickReleaseOnEntity: function() { + print('BLOCKY click') + this.startNearTrigger(); + }, + + update: function() { + if (this.busy === true) { + return; + } + var BEAM_TRIGGER_THRESHOLD = 0.075; + + var BEAM_POSITION = { + x: 1098.5159, + y: 460.0490, + z: -66.3012 + }; + + var leftHandPosition = MyAvatar.getLeftPalmPosition(); + var rightHandPosition = MyAvatar.getRightPalmPosition(); + + var rightDistance = Vec3.distance(leftHandPosition, BEAM_POSITION) + var leftDistance = Vec3.distance(rightHandPosition, BEAM_POSITION) + + if (rightDistance < BEAM_TRIGGER_THRESHOLD || leftDistance < BEAM_TRIGGER_THRESHOLD) { + _this.startNearTrigger(); + } + } + } + + return new Blocky(); +}) \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/blocky/wrapper.js b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js index 2b56b716ec..dfddbaff5d 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/wrapper.js +++ b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js @@ -11,18 +11,18 @@ var RESETTER_POSITION = { - x: 1098.27, - y: 460.43, - z: -66.15 + x: 1098.5159, + y: 460.0490, + z: -66.3012 }; BlockyGame = function(spawnPosition, spawnRotation) { - var scriptURL ="atp:/blocky/blocky.js"; + var scriptURL = "atp:/blocky/blocky.js"; var blockyProps = { type: 'Box', - name:'home_box_blocky_resetter', + name: 'home_box_blocky_resetter', color: { red: 0, green: 0, @@ -33,6 +33,7 @@ BlockyGame = function(spawnPosition, spawnRotation) { y: 0.25, z: 0.25 }, + rotation:Quat.fromPitchYawRollDegrees(-0.0029,32.9983,-0.0021), script: scriptURL, userData: JSON.stringify({ "grabbableKey": { diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index bdc97fcbc6..a15c0c4d87 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -330,7 +330,6 @@ y: 179.0293, z: 89.9698 }); - print('home before cuckooClock') var cuckooClock = new MyCuckooClock({ x: 1105.5237, @@ -341,11 +340,11 @@ y: -57.0089, z: -0.0013 }); - print('home after cuckooClock') + var blocky = new BlockyGame({ - x: 1098.27, - y: 460.43, - z: -66.15 + x: 1098.5159, + y: 460.0490, + z: -66.3012 }) print('HOME after creating scripted entities') From b9aa667c558be8c59dc58667d8656a696d5902f8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 25 May 2016 10:37:30 +1200 Subject: [PATCH 0210/1237] Fix QML test environment --- tests/ui/qml/main.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 1745658193..97f0c0a613 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -23,14 +23,17 @@ ApplicationWindow { Desktop { id: desktop anchors.fill: parent - rootMenu: StubMenu { id: rootMenu } + + //rootMenu: StubMenu { id: rootMenu } //Component.onCompleted: offscreenWindow = appWindow + /* MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: desktop.popupMenu(Qt.vector2d(mouseX, mouseY)); } + */ Row { id: testButtons From 2eef07e414854815b17ef8a155ea5dce9939dae9 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 15:38:08 -0700 Subject: [PATCH 0211/1237] cleanup and dead code removal --- domain-server/src/DomainGatekeeper.cpp | 26 +------------------ domain-server/src/DomainServer.cpp | 13 +--------- domain-server/src/NodeConnectionData.cpp | 5 ++-- interface/src/Application.cpp | 13 ---------- interface/src/Application.h | 4 +-- .../src/scripting/WindowScriptingInterface.h | 4 +-- libraries/networking/src/DomainHandler.cpp | 24 ----------------- libraries/networking/src/LimitedNodeList.cpp | 4 --- libraries/networking/src/NLPacket.cpp | 1 + libraries/networking/src/NodeList.cpp | 17 ------------ libraries/networking/src/NodeList.h | 13 +++------- libraries/networking/src/PacketReceiver.cpp | 11 -------- 12 files changed, 11 insertions(+), 124 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 80137935ca..bc89b99e8a 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -55,10 +55,6 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSize() == 0) { return; } - - //qDebug() << __FUNCTION__ << "packetVersion:" << message->getVersion(); - - QDataStream packetStream(message->getMessage()); // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it @@ -72,8 +68,6 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSenderSockAddr(); @@ -548,27 +537,14 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H quint16 payloadSize = utfString.size(); // setup the DomainConnectionDenied packet - auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied); // , payloadSize + sizeof(payloadSize) + auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied); // pack in the reason the connection was denied (the client displays this) if (payloadSize > 0) { - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); - qDebug() << __FUNCTION__ << "about to write reasonCode:" << (int)reasonCode; uint8_t reasonCodeWire = (uint8_t)reasonCode; - qDebug() << __FUNCTION__ << "about to write reasonCodeWire:" << (int)reasonCodeWire; - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); connectionDeniedPacket->writePrimitive(reasonCodeWire); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); - qDebug() << __FUNCTION__ << "about to write payloadSize:" << payloadSize; - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); connectionDeniedPacket->writePrimitive(payloadSize); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); - qDebug() << __FUNCTION__ << "about to write utfString:" << utfString; - qDebug() << __FUNCTION__ << "about to write utfString.size():" << utfString.size(); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); connectionDeniedPacket->write(utfString); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "connectionDeniedPacket->getDataSize():" << connectionDeniedPacket->getDataSize(); - } // send the packet off diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 18ca7e2941..f6fbb3f470 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -309,8 +309,6 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { PacketType headerType = NLPacket::typeInHeader(packet); PacketVersion headerVersion = NLPacket::versionInHeader(packet); - //qDebug() << __FUNCTION__ << "type:" << headerType << "version:" << (int)headerVersion; - auto nodeList = DependencyManager::get(); // This implements a special case that handles OLD clients which don't know how to negotiate matching @@ -320,9 +318,6 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { // warn the user that the protocol is not compatible if (headerType == PacketType::DomainConnectRequest && headerVersion < static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { - - //qDebug() << __FUNCTION__ << "OLD VERSION checkin sending an intentional bad packet -------------------------------"; - auto packetWithBadVersion = NLPacket::create(PacketType::EntityData); nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr()); return false; @@ -407,8 +402,7 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); - // set packetVersionMatch as the verify packet operator for the udt::Socket - //using std::placeholders::_1; + // set a custum packetVersionMatch as the verify packet operator for the udt::Socket nodeList->setPacketFilterOperator(&DomainServer::packetVersionMatch); } @@ -701,8 +695,6 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet message, SharedNodePointer sendingNode) { - //qDebug() << __FUNCTION__ << "---------------"; - QDataStream packetStream(message->getMessage()); NodeConnectionData nodeRequestData = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr(), false); @@ -782,9 +774,6 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) { } void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) { - - //qDebug() << __FUNCTION__ << "---------------"; - const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NUM_BYTES_RFC4122_UUID + 2; // setup the extended header for the domain list packets diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 5ddcbf1792..13bb9123d8 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -24,10 +24,11 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c char* rawBytes; uint length; - // FIXME -- do we need to delete the rawBytes after it's been copied into the QByteArray? dataStream.readBytes(rawBytes, length); newHeader.protocolVersion = QByteArray(rawBytes, length); - //qDebug() << __FUNCTION__ << "...got protocol version from node... version:" << newHeader.protocolVersion; + + // NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator. + delete[] rawBytes; } dataStream >> newHeader.nodeType diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 58a273737c..bf897015f8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -654,11 +654,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); - connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, this, &Application::limitOfSilentDomainCheckInsReached); - //connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); - - connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); // connect to appropriate slots on AccountManager @@ -1069,16 +1065,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) { - qDebug() << __FUNCTION__ << "message:" << reasonMessage << "code:" << reasonCode; - qDebug() << __FUNCTION__ << "DomainHandler::ConnectionRefusedReason::ProtocolMismatch:" << (int)DomainHandler::ConnectionRefusedReason::ProtocolMismatch; - if (static_cast(reasonCode) == DomainHandler::ConnectionRefusedReason::ProtocolMismatch) { - qDebug() << __FUNCTION__ << " PROTOCOL MISMATCH!!!"; notifyPacketVersionMismatch(); } } - QString Application::getUserAgent() { if (QThread::currentThread() != thread()) { QString userAgent; @@ -4595,12 +4586,8 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const { // We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we // need to be downgraded to talk to it). void Application::limitOfSilentDomainCheckInsReached() { - //qDebug() << __FUNCTION__; - auto nodeList = DependencyManager::get(); - nodeList->downgradeDomainServerCheckInVersion(); // attempt to use an older domain checkin version - nodeList->reset(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 69f48e9541..a17250a58e 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -262,9 +262,7 @@ public slots: void setActiveFaceTracker() const; #if (PR_BUILD || DEV_BUILD) - void sendWrongProtocolVersionsSignature(bool checked) { - ::sendWrongProtocolVersionsSignature(checked); - } + void sendWrongProtocolVersionsSignature(bool checked) { ::sendWrongProtocolVersionsSignature(checked); } #endif #ifdef HAVE_IVIEWHMD diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 72f4ccd866..dfe02a5064 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -16,8 +16,6 @@ #include #include -#include - class WebWindowClass; class WindowScriptingInterface : public QObject, public Dependency { @@ -47,7 +45,7 @@ public slots: signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); - void domainConnectionRefused(const QString& reasonMessage, int reason); + void domainConnectionRefused(const QString& reasonMessage, int reasonCode); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 67a41c866a..1efcfc7f27 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -104,9 +104,6 @@ void DomainHandler::hardReset() { _hasCheckedForAccessToken = false; - //qDebug() << __FUNCTION__ << "about to call _domainConnectionRefusals.clear();"; - //_domainConnectionRefusals.clear(); - // clear any pending path we may have wanted to ask the previous DS about _pendingPath.clear(); } @@ -145,7 +142,6 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const _hostname = hostname; // FIXME - is this the right place??? - qDebug() << __FUNCTION__ << "about to call _domainConnectionRefusals.clear();"; _domainConnectionRefusals.clear(); qCDebug(networking) << "Updated domain hostname to" << _hostname; @@ -374,40 +370,20 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointergetPosition():" << message->getPosition(); message->readPrimitive(&reasonCodeWire); - qDebug() << __FUNCTION__ << "reasonCodeWire:" << reasonCodeWire; ConnectionRefusedReason reasonCode = static_cast(reasonCodeWire); - qDebug() << __FUNCTION__ << "reasonCode:" << (int)reasonCode; - - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "message->getPosition():" << message->getPosition(); - quint16 reasonSize; message->readPrimitive(&reasonSize); - qDebug() << __FUNCTION__ << "reasonSize:" << reasonSize; - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "message->getPosition():" << message->getPosition(); auto reasonText = message->readWithoutCopy(reasonSize); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "reasonText:" << reasonText; QString reasonMessage = QString::fromUtf8(reasonText); - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "reasonMessage:" << reasonMessage; - - qDebug() << __FUNCTION__ << "line:" << __LINE__ << "message->getPosition():" << message->getPosition(); // output to the log so the user knows they got a denied connection request // and check and signal for an access token so that we can make sure they are logged in qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage; - qDebug(networking) << "_domainConnectionRefusals:" << _domainConnectionRefusals; - if (!_domainConnectionRefusals.contains(reasonMessage)) { - qDebug(networking) << "about to call _domainConnectionRefusals.append(reasonMessage);"; _domainConnectionRefusals.append(reasonMessage); - qDebug(networking) << "_domainConnectionRefusals:" << _domainConnectionRefusals; - - emit domainConnectionRefused(reasonMessage, (int)reasonCode); - } else { - qDebug(networking) << "ALREADY EMITTED domainConnectionRefused() ----------------------------"; } auto accountManager = DependencyManager::get(); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 714b69fd89..2c10d0627e 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -162,17 +162,13 @@ QUdpSocket& LimitedNodeList::getDTLSSocket() { } bool LimitedNodeList::isPacketVerified(const udt::Packet& packet) { - //qDebug() << __FUNCTION__; return packetVersionMatch(packet) && packetSourceAndHashMatch(packet); } bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { - PacketType headerType = NLPacket::typeInHeader(packet); PacketVersion headerVersion = NLPacket::versionInHeader(packet); - //qDebug() << __FUNCTION__ << "headerType:" << headerType << "version:" << (int)headerVersion; - if (headerVersion != versionForPacketType(headerType)) { static QMultiHash sourcedVersionDebugSuppressMap; diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 93f8659663..34a159ae6c 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -67,6 +67,7 @@ NLPacket::NLPacket(PacketType type, qint64 size, bool isReliable, bool isPartOfM _version((version == 0) ? versionForPacketType(type) : version) { adjustPayloadStartAndCapacity(NLPacket::localHeaderSize(_type)); + writeTypeAndVersion(); } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 5f3f34dafb..082200fccc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -293,7 +293,6 @@ void NodeList::sendDomainServerCheckIn() { } auto packetVersion = (domainPacketType == PacketType::DomainConnectRequest) ? _domainConnectRequestVersion : 0; - //qDebug() << __FUNCTION__ << " NLPacket::create() version:" << (int)packetVersion; auto domainPacket = NLPacket::create(domainPacketType, -1, false, false, packetVersion); QDataStream packetStream(domainPacket.get()); @@ -319,12 +318,7 @@ void NodeList::sendDomainServerCheckIn() { if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { QByteArray protocolVersionSig = protocolVersionsSignature(); packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); - //qDebug() << __FUNCTION__ << " including protocol version --------------------------"; - } else { - //qDebug() << __FUNCTION__ << "_domainConnectRequestVersion less than HasProtocolVersions - not including protocol version"; } - } else { - //qDebug() << __FUNCTION__ << "NOT a DomainConnnectRequest ----------- not including checkin details -------"; } // pack our data to send to the domain-server including @@ -332,9 +326,6 @@ void NodeList::sendDomainServerCheckIn() { packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasHostname)) { packetStream << DependencyManager::get()->getPlaceName(); - //qDebug() << __FUNCTION__ << " including host name --------------------------"; - } else { - //qDebug() << __FUNCTION__ << "_domainConnectRequestVersion less than HasHostname - not including host name"; } if (!_domainHandler.isConnected()) { @@ -363,7 +354,6 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; - //qDebug() << __FUNCTION__ << " _numNoReplyDomainCheckIns:" << _numNoReplyDomainCheckIns << " --------------------------"; } if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { @@ -531,22 +521,15 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { - //qDebug() << __FUNCTION__; - if (_domainHandler.getSockAddr().isNull()) { // refuse to process this packet if we aren't currently connected to the DS return; } - //qDebug() << __FUNCTION__ << "_numNoReplyDomainCheckIns:" << _numNoReplyDomainCheckIns; - // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; - //qDebug() << __FUNCTION__ << "RESET.... _numNoReplyDomainCheckIns:" << _numNoReplyDomainCheckIns; - // emit our signal so listeners know we just heard from the DS - //qDebug() << __FUNCTION__ << "about to emit receivedDomainServerList() -----------------------------------------------"; emit receivedDomainServerList(); DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 3158262c87..b269554e77 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -69,11 +69,7 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } /// downgrades the DomainConnnectRequest PacketVersion to attempt to probe for older domain servers - void downgradeDomainServerCheckInVersion() { - qDebug() << __FUNCTION__ << "----------------------------------------------------------"; - _domainConnectRequestVersion--; - - } + void downgradeDomainServerCheckInVersion() { _domainConnectRequestVersion--; } public slots: void reset(); @@ -92,11 +88,8 @@ public slots: void processICEPingPacket(QSharedPointer message); - void resetDomainServerCheckInVersion() - { - qDebug() << __FUNCTION__ << "----------------------------------------------------------"; - _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); - } + void resetDomainServerCheckInVersion() + { _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); } signals: void limitOfSilentDomainCheckInsReached(); diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index 8df9a1038a..9cbff8abbd 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -309,29 +309,20 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(SharedNodePointer, matchingNode)); - - //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; - } else if (metaMethod.parameterTypes().contains(QSHAREDPOINTER_NODE_NORMALIZED)) { success = metaMethod.invoke(listener.object, connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(QSharedPointer, matchingNode)); - - //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; - } else { success = metaMethod.invoke(listener.object, connectionType, Q_ARG(QSharedPointer, receivedMessage)); - //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } } else { listenerIsDead = true; } } else { - //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "Got verified unsourced packet list." << "packetType:" << packetType; - // qDebug() << "Got verified unsourced packet list: " << QString(nlPacketList->getMessage()); emit dataReceived(NodeType::Unassigned, receivedMessage->getSize()); @@ -339,8 +330,6 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei if (listener.object) { success = listener.method.invoke(listener.object, Q_ARG(QSharedPointer, receivedMessage)); - - //qDebug() << __FUNCTION__ << "line:" << __LINE__ << "success:" << success << "packetType:" << packetType; } else { listenerIsDead = true; } From 0553ff5f9fc0c7edcbd24e0e4e1c6f14b6a7b561 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 15:41:11 -0700 Subject: [PATCH 0212/1237] cleanup and dead code removal --- libraries/networking/src/PacketReceiver.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index 9cbff8abbd..423e753ae0 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -309,11 +309,13 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(SharedNodePointer, matchingNode)); + } else if (metaMethod.parameterTypes().contains(QSHAREDPOINTER_NODE_NORMALIZED)) { success = metaMethod.invoke(listener.object, connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(QSharedPointer, matchingNode)); + } else { success = metaMethod.invoke(listener.object, connectionType, From 3b0081fbbba9276029f5b37a8efc11ecfd03eaea Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 15:42:55 -0700 Subject: [PATCH 0213/1237] cleanup and dead code removal --- libraries/networking/src/PacketReceiver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index 423e753ae0..87c77967a5 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -315,7 +315,7 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(QSharedPointer, matchingNode)); - + } else { success = metaMethod.invoke(listener.object, connectionType, From 5668db9e4a6f54e4971fe34f9abce516f365b867 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 15:43:30 -0700 Subject: [PATCH 0214/1237] cleanup and dead code removal --- libraries/networking/src/PacketReceiver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index 87c77967a5..530efc5fb3 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -309,7 +309,7 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei connectionType, Q_ARG(QSharedPointer, receivedMessage), Q_ARG(SharedNodePointer, matchingNode)); - + } else if (metaMethod.parameterTypes().contains(QSHAREDPOINTER_NODE_NORMALIZED)) { success = metaMethod.invoke(listener.object, connectionType, From 183f38e4f06c28fa69bcd4dfba5bcc4b669c3d65 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 16:07:57 -0700 Subject: [PATCH 0215/1237] pop up warning if domain over capacity --- domain-server/src/DomainGatekeeper.cpp | 4 ++-- interface/src/Application.cpp | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index bc89b99e8a..9023510214 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -443,10 +443,10 @@ bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteA // find out what our maximum capacity is const QVariant* maximumUserCapacityVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; - + if (maximumUserCapacity > 0) { unsigned int connectedUsers = _server->countConnectedUsers(); - + if (connectedUsers >= maximumUserCapacity) { // too many users, deny the new connection unless this user is an allowed editor diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bf897015f8..f3322caffb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1065,8 +1065,20 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) { - if (static_cast(reasonCode) == DomainHandler::ConnectionRefusedReason::ProtocolMismatch) { - notifyPacketVersionMismatch(); + switch (static_cast(reasonCode)) { + case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: + notifyPacketVersionMismatch(); + break; + case DomainHandler::ConnectionRefusedReason::TooManyUsers: + case DomainHandler::ConnectionRefusedReason::Unknown: { + QString message = "Unable to connect to the location you are visiting.\n"; + message += reasonMessage; + OffscreenUi::warning("", message); + break; + } + default: + // nothing to do. + break; } } From a3f1ece978987b203b50e438628b417fdfa1823e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 24 May 2016 16:10:12 -0700 Subject: [PATCH 0216/1237] Do button mapping in C++ and simplify vive.json accordingly. Vive and Hydra now use PrimaryThumb and SecondaryThumb. Fix warnings for Neuron. --- interface/resources/controllers/hydra.json | 10 +++---- interface/resources/controllers/neuron.json | 4 +-- interface/resources/controllers/vive.json | 10 ++++--- .../src/controllers/StandardControls.h | 4 +++ plugins/openvr/src/ViveControllerManager.cpp | 22 ++++++++++++++- plugins/openvr/src/ViveControllerManager.h | 1 + .../controllers/handControllerPointer.js | 28 +++---------------- 7 files changed, 42 insertions(+), 37 deletions(-) diff --git a/interface/resources/controllers/hydra.json b/interface/resources/controllers/hydra.json index 8233685763..066676140c 100644 --- a/interface/resources/controllers/hydra.json +++ b/interface/resources/controllers/hydra.json @@ -16,12 +16,10 @@ { "from": "Hydra.L0", "to": "Standard.Back" }, { "from": "Hydra.R0", "to": "Standard.Start" }, - { "from": [ "Hydra.L1", "Hydra.L2" ], "to": "Standard.LeftPrimaryThumb" }, - { "from": [ "Hydra.R1", "Hydra.R2" ], "to": "Standard.RightPrimaryThumb" }, - { "from": [ "Hydra.L3" ], "to": "Standard.L3" }, - { "from": [ "Hydra.R3" ], "to": "Standard.R3" }, - { "from": [ "Hydra.R4" ], "to": "Standard.RightSecondaryThumb" }, - { "from": [ "Hydra.L4" ], "to": "Standard.LeftSecondaryThumb" }, + { "from": [ "Hydra.L1", "Hydra.L3" ], "to": "Standard.LeftPrimaryThumb" }, + { "from": [ "Hydra.R1", "Hydra.R3" ], "to": "Standard.RightPrimaryThumb" }, + { "from": [ "Hydra.R2", "Hydra.R4" ], "to": "Standard.RightSecondaryThumb" }, + { "from": [ "Hydra.L2", "Hydra.L4" ], "to": "Standard.LeftSecondaryThumb" }, { "from": "Hydra.LeftHand", "to": "Standard.LeftHand" }, { "from": "Hydra.RightHand", "to": "Standard.RightHand" } diff --git a/interface/resources/controllers/neuron.json b/interface/resources/controllers/neuron.json index 2d61f80c35..d0962c72db 100644 --- a/interface/resources/controllers/neuron.json +++ b/interface/resources/controllers/neuron.json @@ -1,7 +1,7 @@ { "name": "Neuron to Standard", "channels": [ - { "from": "Hydra.LeftHand", "to": "Standard.LeftHand" }, - { "from": "Hydra.RightHand", "to": "Standard.RightHand" } + { "from": "Neuron.LeftHand", "to": "Standard.LeftHand" }, + { "from": "Neuron.RightHand", "to": "Standard.RightHand" } ] } diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index fec93c9132..60a46ba3ce 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -1,23 +1,25 @@ { "name": "Vive to Standard", "channels": [ - { "from": "Vive.LY", "when": "Vive.LS", "filters": ["invert" ,{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LY" }, - { "from": "Vive.LX", "when": "Vive.LS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LX" }, + { "from": "Vive.LY", "when": "Vive.LSOuter", "filters": ["invert"], "to": "Standard.LY" }, + { "from": "Vive.LX", "when": "Vive.LSOuter", "to": "Standard.LX" }, { "from": "Vive.LT", "to": "Standard.LT" }, { "from": "Vive.LeftGrip", "to": "Standard.LB" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, - { "from": "Vive.RY", "when": "Vive.RS", "filters": ["invert", { "type": "deadZone", "min": 0.6 }], "to": "Standard.RY" }, - { "from": "Vive.RX", "when": "Vive.RS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.RX" }, + { "from": "Vive.RY", "when": "Vive.RSOuter", "filters": ["invert"], "to": "Standard.RY" }, + { "from": "Vive.RX", "when": "Vive.RSOuter", "to": "Standard.RX" }, { "from": "Vive.RT", "to": "Standard.RT" }, { "from": "Vive.RightGrip", "to": "Standard.RB" }, { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, + { "from": "Vive.LSCenter", "to": "Standard.LeftPrimaryThumb" }, { "from": "Vive.LeftApplicationMenu", "to": "Standard.LeftSecondaryThumb" }, + { "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" }, { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index f101ba6c51..79c23bc6ee 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -43,6 +43,8 @@ namespace controller { LEFT_SECONDARY_THUMB_TOUCH, LS_TOUCH, LEFT_THUMB_UP, + LS_CENTER, + LS_OUTER, RIGHT_PRIMARY_THUMB, RIGHT_SECONDARY_THUMB, @@ -50,6 +52,8 @@ namespace controller { RIGHT_SECONDARY_THUMB_TOUCH, RS_TOUCH, RIGHT_THUMB_UP, + RS_CENTER, + RS_OUTER, LEFT_PRIMARY_INDEX, LEFT_SECONDARY_INDEX, diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 12567b10d1..6e75454b5f 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -282,7 +282,22 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u for (uint32_t i = 0; i < vr::k_unControllerStateAxisCount; i++) { handleAxisEvent(deltaTime, i, controllerState.rAxis[i].x, controllerState.rAxis[i].y, isLeftHand); } - } + + // pseudo buttons the depend on both of the above for-loops + partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_OUTER); + partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_OUTER); + } + } +} + +void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int outerPseudoButton) { + // Populate the L/RS_CENTER/OUTER pseudo buttons, corresponding to a partition of the L/RS space based on the X/Y values. + const float CENTER_DEADBAND = 0.6f; + if (_buttonPressedMap.find(sButton) != _buttonPressedMap.end()) { + float absX = abs(_axisStateMap[xAxis]); + float absY = abs(_axisStateMap[yAxis]); + bool isCenter = (absX < CENTER_DEADBAND) && (absY < CENTER_DEADBAND); // square deadband + _buttonPressedMap.insert(isCenter ? centerPseudoButton : outerPseudoButton); } } @@ -443,6 +458,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI // touch pad press makePair(LS, "LS"), makePair(RS, "RS"), + // Differentiate where we are in the touch pad click + makePair(LS_CENTER, "LSCenter"), + makePair(LS_OUTER, "LSOuter"), + makePair(RS_CENTER, "RSCenter"), + makePair(RS_OUTER, "RSOuter"), // triggers makePair(LT, "LT"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 672ad59cfe..bd5d4a39f4 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -61,6 +61,7 @@ private: void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand); void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand); + void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int outerPseudoButton); class FilteredStick { public: diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index e7be3af5dd..ca3b5e8cf2 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -266,39 +266,19 @@ function toggleHand() { } } -// Create clickMappings as needed, on demand. var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); Script.scriptEnding.connect(clickMapping.disable); -// Move these to vive.json -function makeCenterClickWhen(click, x, y) { - var clickKey = Controller.Standard[click], - xKey = Controller.Standard[x], // Standard after filtering by mapping - yKey = Controller.Standard[y]; - return function () { - var clickValue = Controller.getValue(clickKey); - var xValue = Controller.getValue(xKey); - var yValue = Controller.getValue(yKey); - var answer = clickValue && !xValue && !yValue; - return answer; - }; -} -if (Controller.Hardware.Vive) { - clickMapping.from(Controller.Hardware.Vive.RS).when(makeCenterClickWhen('RS', 'RX', 'RY')).to(Controller.Standard.R3); - clickMapping.from(Controller.Hardware.Vive.LS).when(makeCenterClickWhen('LS', 'LX', 'LY')).to(Controller.Standard.L3); -} - - -clickMapping.from(Controller.Standard.R3).peek().to(Controller.Actions.ReticleClick); -clickMapping.from(Controller.Standard.L3).peek().to(Controller.Actions.ReticleClick); +clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick); +clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); -clickMapping.from(Controller.Standard.R3).peek().to(function (on) { +clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(function (on) { if (on && (activeHand !== Controller.Standard.RightHand)) { toggleHand(); } }); -clickMapping.from(Controller.Standard.L3).peek().to(function (on) { +clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(function (on) { if (on && (activeHand !== Controller.Standard.LeftHand)) { toggleHand(); } From 66a90cc3f8ad1e415ae38f29c025793ef6d478d5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 25 May 2016 11:31:39 +1200 Subject: [PATCH 0217/1237] Make file browser dialog resizable --- interface/resources/qml/dialogs/FileDialog.qml | 2 +- interface/resources/qml/windows-uit/ModalFrame.qml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 5cd972a38f..0b539f9667 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -25,7 +25,7 @@ import "fileDialog" //FIXME implement shortcuts for favorite location ModalWindow { id: root - //resizable: true + resizable: true implicitWidth: 640 implicitHeight: 480 diff --git a/interface/resources/qml/windows-uit/ModalFrame.qml b/interface/resources/qml/windows-uit/ModalFrame.qml index 77344829d5..415ae03284 100644 --- a/interface/resources/qml/windows-uit/ModalFrame.qml +++ b/interface/resources/qml/windows-uit/ModalFrame.qml @@ -18,13 +18,13 @@ Frame { HifiConstants { id: hifi } Rectangle { - id: modalFrame + id: frameContent readonly property bool hasTitle: window.title != "" anchors { fill: parent - topMargin: -hifi.dimensions.modalDialogMargin.y - (modalFrame.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) + topMargin: -hifi.dimensions.modalDialogMargin.y - (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) leftMargin: -hifi.dimensions.modalDialogMargin.x rightMargin: -hifi.dimensions.modalDialogMargin.x bottomMargin: -hifi.dimensions.modalDialogMargin.y @@ -38,7 +38,7 @@ Frame { color: hifi.colors.faintGray Item { - visible: modalFrame.hasTitle + visible: frameContent.hasTitle anchors.fill: parent anchors { topMargin: -parent.anchors.topMargin From fb9f6185db724c0fc3f30ec46ad70353e3a848de Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 17:19:53 -0700 Subject: [PATCH 0218/1237] CR feedback --- domain-server/src/DomainGatekeeper.cpp | 5 +++-- interface/src/Application.cpp | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 9023510214..b940d46849 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -535,9 +535,10 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H // this is an agent and we've decided we won't let them connect - send them a packet to deny connection QByteArray utfString = reason.toUtf8(); quint16 payloadSize = utfString.size(); - + // setup the DomainConnectionDenied packet - auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied); + auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, + payloadSize + sizeof(payloadSize) + sizeof(uint8_t)); // pack in the reason the connection was denied (the client displays this) if (payloadSize > 0) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f3322caffb..48b418b93c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4594,7 +4594,7 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const { // If we're not getting anything back from the domain server checkin, it might be that the domain speaks an -// older version of the DomainConnectRequest protocal. We will attempt to send and older version of DomainConnectRequest. +// older version of the DomainConnectRequest protocol. We will attempt to send and older version of DomainConnectRequest. // We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we // need to be downgraded to talk to it). void Application::limitOfSilentDomainCheckInsReached() { From ff45633c21ca3f9a496fa620e5fb7490968af874 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 24 May 2016 17:36:52 -0700 Subject: [PATCH 0219/1237] bump entities version/CR feedback --- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 9de149d32d..a960edbdc8 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_NO_FLY_ZONES; + return VERSION_ENTITIES_MORE_SHAPES; case PacketType::AvatarData: case PacketType::BulkAvatarData: return static_cast(AvatarMixerPacketVersion::AvatarEntities); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index b6237e74d6..a04a737c41 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -178,6 +178,7 @@ const PacketVersion VERSION_ENTITITES_HAVE_COLLISION_MASK = 55; const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; +const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, From 1c4eed640fcdd1747484e383b8d59b4f092dbaea Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 25 May 2016 13:10:26 +1200 Subject: [PATCH 0220/1237] Fix up file browser resize handle and outlining --- .../resources/qml/dialogs/FileDialog.qml | 2 ++ .../qml/styles-uit/HifiConstants.qml | 1 + .../qml/windows-uit/DefaultFrame.qml | 24 ++++++++++------ interface/resources/qml/windows-uit/Frame.qml | 28 +++++++++---------- .../resources/qml/windows-uit/ModalFrame.qml | 13 ++++++--- .../resources/qml/windows-uit/ModalWindow.qml | 2 ++ .../resources/qml/windows-uit/Window.qml | 1 + 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 0b539f9667..7921e2549d 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -29,6 +29,8 @@ ModalWindow { implicitWidth: 640 implicitHeight: 480 + minSize: Qt.vector2d(300, 240) + HifiConstants { id: hifi } Settings { diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 16f150b2d9..f2698da574 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -141,6 +141,7 @@ Item { readonly property real textPadding: 8 readonly property real sliderHandleSize: 18 readonly property real sliderGrooveHeight: 8 + readonly property real frameIconSize: 22 readonly property real spinnerSize: 50 readonly property real tablePadding: 12 readonly property real tableRowHeight: largeScreen ? 26 : 23 diff --git a/interface/resources/qml/windows-uit/DefaultFrame.qml b/interface/resources/qml/windows-uit/DefaultFrame.qml index 04905656ce..6334086e4e 100644 --- a/interface/resources/qml/windows-uit/DefaultFrame.qml +++ b/interface/resources/qml/windows-uit/DefaultFrame.qml @@ -20,6 +20,14 @@ Frame { Rectangle { // Dialog frame id: frameContent + + readonly property int iconSize: hifi.dimensions.frameIconSize + readonly property int frameMargin: 9 + readonly property int frameMarginLeft: frameMargin + readonly property int frameMarginRight: frameMargin + readonly property int frameMarginTop: 2 * frameMargin + iconSize + readonly property int frameMarginBottom: iconSize + 11 + anchors { topMargin: -frameMarginTop leftMargin: -frameMarginLeft @@ -45,17 +53,17 @@ Frame { anchors { right: parent.right; top: parent.top; - topMargin: frameMargin + 1 // Move down a little to visually align with the title - rightMargin: frameMarginRight; + topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title + rightMargin: frameContent.frameMarginRight; } - spacing: iconSize / 4 + spacing: frameContent.iconSize / 4 HiFiGlyphs { // "Pin" button visible: false text: (frame.pinned && !pinClickArea.containsMouse) || (!frame.pinned && pinClickArea.containsMouse) ? hifi.glyphs.pinInverted : hifi.glyphs.pin color: pinClickArea.containsMouse && !pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white - size: iconSize + size: frameContent.iconSize MouseArea { id: pinClickArea anchors.fill: parent @@ -70,7 +78,7 @@ Frame { visible: window ? window.closable : false text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white - size: iconSize + size: frameContent.iconSize MouseArea { id: closeClickArea anchors.fill: parent @@ -85,11 +93,11 @@ Frame { id: titleText anchors { left: parent.left - leftMargin: frameMarginLeft + hifi.dimensions.contentMargin.x + leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x right: controlsRow.left - rightMargin: iconSize + rightMargin: frameContent.iconSize top: parent.top - topMargin: frameMargin + topMargin: frameContent.frameMargin } text: window ? window.title : "" color: hifi.colors.white diff --git a/interface/resources/qml/windows-uit/Frame.qml b/interface/resources/qml/windows-uit/Frame.qml index f21097ea62..9519a44cf0 100644 --- a/interface/resources/qml/windows-uit/Frame.qml +++ b/interface/resources/qml/windows-uit/Frame.qml @@ -22,12 +22,10 @@ Item { property bool gradientsSupported: desktop.gradientsSupported - readonly property int iconSize: 22 - readonly property int frameMargin: 9 - readonly property int frameMarginLeft: frameMargin - readonly property int frameMarginRight: frameMargin - readonly property int frameMarginTop: 2 * frameMargin + iconSize - readonly property int frameMarginBottom: iconSize + 11 + readonly property int frameMarginLeft: frameContent.frameMarginLeft + readonly property int frameMarginRight: frameContent.frameMarginRight + readonly property int frameMarginTop: frameContent.frameMarginTop + readonly property int frameMarginBottom: frameContent.frameMarginBottom // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes @@ -76,8 +74,8 @@ Item { id: sizeOutline x: -frameMarginLeft y: -frameMarginTop - width: window ? window.width + frameMarginLeft + frameMarginRight : 0 - height: window ? window.height + frameMarginTop + frameMarginBottom : 0 + width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 + height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 color: hifi.colors.baseGrayHighlight15 border.width: 3 border.color: hifi.colors.white50 @@ -88,11 +86,11 @@ Item { MouseArea { // Resize handle id: sizeDrag - width: iconSize - height: iconSize + width: hifi.dimensions.frameIconSize + height: hifi.dimensions.frameIconSize enabled: window ? window.resizable : false hoverEnabled: true - x: window ? window.width + frameMarginRight - iconSize : 0 + x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 y: window ? window.height + 4 : 0 property vector2d pressOrigin property vector2d sizeOrigin @@ -124,10 +122,12 @@ Item { HiFiGlyphs { visible: sizeDrag.enabled x: -11 // Move a little to visually align - y: -4 // "" + y: window.modality == Qt.ApplicationModal ? -6 : -4 text: hifi.glyphs.resizeHandle - size: iconSize + 10 - color: sizeDrag.containsMouse || sizeDrag.pressed ? hifi.colors.white : hifi.colors.white50 + size: hifi.dimensions.frameIconSize + 10 + color: sizeDrag.containsMouse || sizeDrag.pressed + ? hifi.colors.white + : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) } } } diff --git a/interface/resources/qml/windows-uit/ModalFrame.qml b/interface/resources/qml/windows-uit/ModalFrame.qml index 415ae03284..13c560a519 100644 --- a/interface/resources/qml/windows-uit/ModalFrame.qml +++ b/interface/resources/qml/windows-uit/ModalFrame.qml @@ -22,12 +22,17 @@ Frame { readonly property bool hasTitle: window.title != "" + readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) + readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y + anchors { fill: parent - topMargin: -hifi.dimensions.modalDialogMargin.y - (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) - leftMargin: -hifi.dimensions.modalDialogMargin.x - rightMargin: -hifi.dimensions.modalDialogMargin.x - bottomMargin: -hifi.dimensions.modalDialogMargin.y + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom } border { diff --git a/interface/resources/qml/windows-uit/ModalWindow.qml b/interface/resources/qml/windows-uit/ModalWindow.qml index af099eb275..6b007160d5 100644 --- a/interface/resources/qml/windows-uit/ModalWindow.qml +++ b/interface/resources/qml/windows-uit/ModalWindow.qml @@ -19,4 +19,6 @@ Window { destroyOnCloseButton: true destroyOnInvisible: true frame: ModalFrame{} + + property int colorScheme: hifi.colorSchemes.light } diff --git a/interface/resources/qml/windows-uit/Window.qml b/interface/resources/qml/windows-uit/Window.qml index e9477f3c7e..d614b21ce2 100644 --- a/interface/resources/qml/windows-uit/Window.qml +++ b/interface/resources/qml/windows-uit/Window.qml @@ -52,6 +52,7 @@ Fadable { // property bool pinned: false property bool resizable: false property bool gradientsSupported: desktop.gradientsSupported + property int colorScheme: hifi.colorSchemes.dark property vector2d minSize: Qt.vector2d(100, 100) property vector2d maxSize: Qt.vector2d(1280, 800) From ea8ba7e2d19fe6dbb7a047048e291aa91609ae72 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 24 May 2016 18:12:40 -0700 Subject: [PATCH 0221/1237] Fixes based on code review feedback. * Use isNaN instead of glm::isnan * prefer abort() over writing to a nullptr, in release assert. * warn if packet size does not match expectations --- libraries/avatars/src/AvatarData.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index cc61036915..16e4bd5437 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -92,7 +92,7 @@ namespace AvatarDataPacket { */ } -#define ASSERT(COND) do { if (!(COND)) { int* bad = nullptr; *bad = 0xbad; } } while(0) +#define ASSERT(COND) do { if (!(COND)) { abort(); } } while(0) AvatarData::AvatarData() : SpatiallyNestable(NestableType::Avatar, QUuid()), @@ -442,7 +442,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { glm::vec3 position = glm::vec3(header->position[0], header->position[1], header->position[2]); _globalPosition = glm::vec3(header->globalPosition[0], header->globalPosition[1], header->globalPosition[2]); - if (glm::isnan(position.x) || glm::isnan(position.y) || glm::isnan(position.z)) { + if (isNaN(position)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: position NaN, uuid " << getSessionUUID(); } @@ -454,7 +454,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { unpackFloatAngleFromTwoByte(header->localOrientation + 0, &yaw); unpackFloatAngleFromTwoByte(header->localOrientation + 1, &pitch); unpackFloatAngleFromTwoByte(header->localOrientation + 2, &roll); - if (glm::isnan(yaw) || glm::isnan(pitch) || glm::isnan(roll)) { + if (isNaN(yaw) || isNaN(pitch) || isNaN(roll)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: localOriention is NaN, uuid " << getSessionUUID(); } @@ -471,7 +471,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { float scale; unpackFloatRatioFromTwoByte((uint8_t*)&header->scale, scale); - if (glm::isnan(scale)) { + if (isNaN(scale)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: scale NaN, uuid " << getSessionUUID(); } @@ -480,7 +480,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _targetScale = std::max(MIN_AVATAR_SCALE, std::min(MAX_AVATAR_SCALE, scale)); glm::vec3 lookAt = glm::vec3(header->lookAtPosition[0], header->lookAtPosition[1], header->lookAtPosition[2]); - if (glm::isnan(lookAt.x) || glm::isnan(lookAt.y) || glm::isnan(lookAt.z)) { + if (isNaN(lookAt)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: lookAtPosition is NaN, uuid " << getSessionUUID(); } @@ -489,7 +489,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _headData->_lookAtPosition = lookAt; float audioLoudness = header->audioLoudness; - if (glm::isnan(audioLoudness)) { + if (isNaN(audioLoudness)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: audioLoudness is NaN, uuid " << getSessionUUID(); } @@ -522,8 +522,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto parentInfo = reinterpret_cast(sourceBuffer); sourceBuffer += sizeof(AvatarDataPacket::ParentInfo); - const size_t RFC_4122_SIZE = 16; - QByteArray byteArray((const char*)parentInfo->parentUUID, RFC_4122_SIZE); + QByteArray byteArray((const char*)parentInfo->parentUUID, NUM_BYTES_RFC4122_UUID); _parentID = QUuid::fromRfc4122(byteArray); _parentJointIndex = parentInfo->parentJointIndex; } else { @@ -634,8 +633,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { int numBytesRead = sourceBuffer - startPosition; - // AJT: Maybe make this a warning. - ASSERT(numBytesRead == buffer.size()); + if (numBytesRead != buffer.size()) { + if (shouldLogError(now)) { + qCWarning(avatars) << "AvatarData packet size mismatch: expected " << numBytesRead << " received " << buffer.size(); + } + } _averageBytesReceived.updateAverage(numBytesRead); return numBytesRead; From 9ec6b774608026ef53d9d14e050b8b85e4997003 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 25 May 2016 13:49:01 +1200 Subject: [PATCH 0222/1237] Make file browser dialog movable --- interface/resources/qml/dialogs/FileDialog.qml | 1 + interface/resources/qml/windows-uit/DefaultFrame.qml | 2 +- interface/resources/qml/windows-uit/ModalFrame.qml | 7 +++++++ interface/resources/qml/windows-uit/ModalWindow.qml | 6 ++++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 7921e2549d..00a66c01cc 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -30,6 +30,7 @@ ModalWindow { implicitHeight: 480 minSize: Qt.vector2d(300, 240) + draggable: true HifiConstants { id: hifi } diff --git a/interface/resources/qml/windows-uit/DefaultFrame.qml b/interface/resources/qml/windows-uit/DefaultFrame.qml index 6334086e4e..84f435480b 100644 --- a/interface/resources/qml/windows-uit/DefaultFrame.qml +++ b/interface/resources/qml/windows-uit/DefaultFrame.qml @@ -42,7 +42,7 @@ Frame { } radius: hifi.dimensions.borderRadius - // Allow dragging of the window + // Enable dragging of the window MouseArea { anchors.fill: parent drag.target: window diff --git a/interface/resources/qml/windows-uit/ModalFrame.qml b/interface/resources/qml/windows-uit/ModalFrame.qml index 13c560a519..44c0b6a456 100644 --- a/interface/resources/qml/windows-uit/ModalFrame.qml +++ b/interface/resources/qml/windows-uit/ModalFrame.qml @@ -42,6 +42,13 @@ Frame { radius: hifi.dimensions.borderRadius color: hifi.colors.faintGray + // Enable dragging of the window + MouseArea { + anchors.fill: parent + drag.target: window + enabled: window.draggable + } + Item { visible: frameContent.hasTitle anchors.fill: parent diff --git a/interface/resources/qml/windows-uit/ModalWindow.qml b/interface/resources/qml/windows-uit/ModalWindow.qml index 6b007160d5..f429e98ac3 100644 --- a/interface/resources/qml/windows-uit/ModalWindow.qml +++ b/interface/resources/qml/windows-uit/ModalWindow.qml @@ -14,11 +14,13 @@ import "." Window { id: window - anchors.centerIn: parent modality: Qt.ApplicationModal destroyOnCloseButton: true destroyOnInvisible: true - frame: ModalFrame{} + frame: ModalFrame { } property int colorScheme: hifi.colorSchemes.light + property bool draggable: false + + anchors.centerIn: draggable ? undefined : parent } From 9f6e9718399f1c38e5db103a578f39be6dfc04e6 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 25 May 2016 07:24:34 -0700 Subject: [PATCH 0223/1237] Default to darker reverb settings, by increasing the high-frequency rolloff and damping. --- libraries/audio/src/AudioEffectOptions.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio/src/AudioEffectOptions.cpp b/libraries/audio/src/AudioEffectOptions.cpp index 9d3ce9299b..a9e4cbae6c 100644 --- a/libraries/audio/src/AudioEffectOptions.cpp +++ b/libraries/audio/src/AudioEffectOptions.cpp @@ -32,7 +32,7 @@ static const QString LATE_MIX_LEFT_HANDLE = "lateMixLeft"; static const QString LATE_MIX_RIGHT_HANDLE = "lateMixRight"; static const QString WET_DRY_MIX_HANDLE = "wetDryMix"; -static const float BANDWIDTH_DEFAULT = 10000.0f; +static const float BANDWIDTH_DEFAULT = 7000.0f; static const float PRE_DELAY_DEFAULT = 20.0f; static const float LATE_DELAY_DEFAULT = 0.0f; static const float REVERB_TIME_DEFAULT = 2.0f; @@ -42,7 +42,7 @@ static const float ROOM_SIZE_DEFAULT = 50.0f; static const float DENSITY_DEFAULT = 100.0f; static const float BASS_MULT_DEFAULT = 1.5f; static const float BASS_FREQ_DEFAULT = 250.0f; -static const float HIGH_GAIN_DEFAULT = -6.0f; +static const float HIGH_GAIN_DEFAULT = -12.0f; static const float HIGH_FREQ_DEFAULT = 3000.0f; static const float MOD_RATE_DEFAULT = 2.3f; static const float MOD_DEPTH_DEFAULT = 50.0f; From ba77aaf7efdb0607001d9f2f6a16b8d03460f6e9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 25 May 2016 11:06:08 -0700 Subject: [PATCH 0224/1237] remove collision of isPlaying property and slot --- libraries/script-engine/src/ScriptAudioInjector.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptAudioInjector.h b/libraries/script-engine/src/ScriptAudioInjector.h index 0d16b26fdf..4de12af62c 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.h +++ b/libraries/script-engine/src/ScriptAudioInjector.h @@ -19,7 +19,7 @@ class ScriptAudioInjector : public QObject { Q_OBJECT - Q_PROPERTY(bool isPlaying READ isPlaying) + Q_PROPERTY(bool playing READ isPlaying) Q_PROPERTY(float loudness READ getLoudness) Q_PROPERTY(AudioInjectorOptions options WRITE setOptions READ getOptions) public: From 64720444cf4527f10eebb360018269af366cb72c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 25 May 2016 11:11:06 -0700 Subject: [PATCH 0225/1237] correct existing uses of the isPlaying property --- script-archive/FlockOfbirds.js | 84 +++++++++---------- .../ACAudioSearchAndInject.js | 6 +- script-archive/avatarSelector.js | 4 +- script-archive/baseball/baseballCrowd.js | 4 +- script-archive/controllers/hydra/airGuitar.js | 17 ++-- script-archive/drylake/ratCreator.js | 2 +- script-archive/entityScripts/movable.js | 58 ++++++------- script-archive/example/audio/birdSongs.js | 40 ++++----- script-archive/lobby.js | 72 ++++++++-------- script-archive/playTestSound.js | 25 +++--- 10 files changed, 155 insertions(+), 157 deletions(-) diff --git a/script-archive/FlockOfbirds.js b/script-archive/FlockOfbirds.js index f466fa2909..c2fb54f0a6 100644 --- a/script-archive/FlockOfbirds.js +++ b/script-archive/FlockOfbirds.js @@ -3,8 +3,8 @@ // examples // // Copyright 2014 High Fidelity, Inc. -// Creates a flock of birds that fly around and chirp, staying inside the corners of the box defined -// at the start of the script. +// Creates a flock of birds that fly around and chirp, staying inside the corners of the box defined +// at the start of the script. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -13,12 +13,12 @@ // The rectangular area in the domain where the flock will fly var lowerCorner = { x: 0, y: 0, z: 0 }; var upperCorner = { x: 30, y: 10, z: 30 }; -var STARTING_FRACTION = 0.25; +var STARTING_FRACTION = 0.25; var NUM_BIRDS = 50; var UPDATE_INTERVAL = 0.016; -var playSounds = true; -var SOUND_PROBABILITY = 0.001; +var playSounds = true; +var SOUND_PROBABILITY = 0.001; var STARTING_LIFETIME = (1.0 / SOUND_PROBABILITY) * UPDATE_INTERVAL * 10; var numPlaying = 0; var BIRD_SIZE = 0.08; @@ -36,17 +36,17 @@ var ALIGNMENT_FORCE = 1.5; var COHESION_FORCE = 1.0; var MAX_COHESION_VELOCITY = 0.5; -var followBirds = false; +var followBirds = false; var AVATAR_FOLLOW_RATE = 0.001; var AVATAR_FOLLOW_VELOCITY_TIMESCALE = 2.0; var AVATAR_FOLLOW_ORIENTATION_RATE = 0.005; -var floor = false; +var floor = false; var MAKE_FLOOR = false; var averageVelocity = { x: 0, y: 0, z: 0 }; var averagePosition = { x: 0, y: 0, z: 0 }; -var birdsLoaded = false; +var birdsLoaded = false; var oldAvatarOrientation; var oldAvatarPosition; @@ -79,10 +79,10 @@ function updateBirds(deltaTime) { birds[i].entityId = false; return; } - // Sum up average position and velocity + // Sum up average position and velocity if (Vec3.length(properties.velocity) > MIN_ALIGNMENT_VELOCITY) { sumVelocity = Vec3.sum(sumVelocity, properties.velocity); - birdVelocitiesCounted += 1; + birdVelocitiesCounted += 1; } sumPosition = Vec3.sum(sumPosition, properties.position); birdPositionsCounted += 1; @@ -93,10 +93,10 @@ function updateBirds(deltaTime) { var randomVelocity = randomVector(RANDOM_FLAP_VELOCITY); randomVelocity.y = FLAP_UP + Math.random() * FLAP_UP; - // Alignment Velocity - var alignmentVelocityMagnitude = Math.min(MAX_ALIGNMENT_VELOCITY, Vec3.length(Vec3.multiply(ALIGNMENT_FORCE, averageVelocity))); + // Alignment Velocity + var alignmentVelocityMagnitude = Math.min(MAX_ALIGNMENT_VELOCITY, Vec3.length(Vec3.multiply(ALIGNMENT_FORCE, averageVelocity))); var alignmentVelocity = Vec3.multiply(alignmentVelocityMagnitude, Vec3.normalize(averageVelocity)); - alignmentVelocity.y *= VERTICAL_ALIGNMENT_COUPLING; + alignmentVelocity.y *= VERTICAL_ALIGNMENT_COUPLING; // Cohesion var distanceFromCenter = Vec3.length(Vec3.subtract(averagePosition, properties.position)); @@ -107,10 +107,10 @@ function updateBirds(deltaTime) { Entities.editEntity(birds[i].entityId, { velocity: Vec3.sum(properties.velocity, newVelocity) }); - } + } // Check whether to play a chirp - if (playSounds && (!birds[i].audioId || !birds[i].audioId.isPlaying) && (Math.random() < ((numPlaying > 0) ? SOUND_PROBABILITY / numPlaying : SOUND_PROBABILITY))) { + if (playSounds && (!birds[i].audioId || !birds[i].audioId.playing) && (Math.random() < ((numPlaying > 0) ? SOUND_PROBABILITY / numPlaying : SOUND_PROBABILITY))) { var options = { position: properties.position, volume: BIRD_MASTER_VOLUME @@ -126,43 +126,43 @@ function updateBirds(deltaTime) { // Change size, and update lifetime to keep bird alive Entities.editEntity(birds[i].entityId, { dimensions: Vec3.multiply(1.5, properties.dimensions), lifetime: properties.ageInSeconds + STARTING_LIFETIME}); - + } else if (birds[i].audioId) { - // If bird is playing a chirp - if (!birds[i].audioId.isPlaying) { + // If bird is playing a chirp + if (!birds[i].audioId.playing) { Entities.editEntity(birds[i].entityId, { dimensions: { x: BIRD_SIZE, y: BIRD_SIZE, z: BIRD_SIZE }}); numPlaying--; - } + } } // Keep birds in their 'cage' var bounce = false; - var newVelocity = properties.velocity; - var newPosition = properties.position; + var newVelocity = properties.velocity; + var newPosition = properties.position; if (properties.position.x < lowerCorner.x) { - newPosition.x = lowerCorner.x; + newPosition.x = lowerCorner.x; newVelocity.x *= -1.0; bounce = true; } else if (properties.position.x > upperCorner.x) { - newPosition.x = upperCorner.x; + newPosition.x = upperCorner.x; newVelocity.x *= -1.0; bounce = true; } if (properties.position.y < lowerCorner.y) { - newPosition.y = lowerCorner.y; + newPosition.y = lowerCorner.y; newVelocity.y *= -1.0; bounce = true; } else if (properties.position.y > upperCorner.y) { - newPosition.y = upperCorner.y; + newPosition.y = upperCorner.y; newVelocity.y *= -1.0; bounce = true; - } + } if (properties.position.z < lowerCorner.z) { - newPosition.z = lowerCorner.z; + newPosition.z = lowerCorner.z; newVelocity.z *= -1.0; bounce = true; } else if (properties.position.z > upperCorner.z) { - newPosition.z = upperCorner.z; + newPosition.z = upperCorner.z; newVelocity.z *= -1.0; bounce = true; } @@ -171,7 +171,7 @@ function updateBirds(deltaTime) { } } } - // Update average velocity and position of flock + // Update average velocity and position of flock if (birdVelocitiesCounted > 0) { averageVelocity = Vec3.multiply(1.0 / birdVelocitiesCounted, sumVelocity); //print(Vec3.length(averageVelocity)); @@ -184,10 +184,10 @@ function updateBirds(deltaTime) { MyAvatar.orientation = Quat.mix(MyAvatar.orientation, birdDirection, AVATAR_FOLLOW_ORIENTATION_RATE); } } - } + } if (birdPositionsCounted > 0) { averagePosition = Vec3.multiply(1.0 / birdPositionsCounted, sumPosition); - // If Following birds, update position + // If Following birds, update position if (followBirds) { MyAvatar.position = Vec3.sum(Vec3.multiply(AVATAR_FOLLOW_RATE, MyAvatar.position), Vec3.multiply(1.0 - AVATAR_FOLLOW_RATE, averagePosition)); } @@ -211,12 +211,12 @@ Script.scriptEnding.connect(function() { }); function loadBirds(howMany) { - oldAvatarOrientation = MyAvatar.orientation; + oldAvatarOrientation = MyAvatar.orientation; oldAvatarPosition = MyAvatar.position; var sound_filenames = ["bushtit_1.raw", "bushtit_2.raw", "bushtit_3.raw"]; /* Here are more sounds/species you can use - , "mexicanWhipoorwill.raw", + , "mexicanWhipoorwill.raw", "rosyfacedlovebird.raw", "saysphoebe.raw", "westernscreechowl.raw", "bandtailedpigeon.wav", "bridledtitmouse.wav", "browncrestedflycatcher.wav", "commonnighthawk.wav", "commonpoorwill.wav", "doublecrestedcormorant.wav", "gambelsquail.wav", "goldcrownedkinglet.wav", "greaterroadrunner.wav","groovebilledani.wav","hairywoodpecker.wav", @@ -252,19 +252,19 @@ function loadBirds(howMany) { { red: 216, green: 153, blue: 99 }, { red: 242, green: 226, blue: 64 } ]; - + var SOUND_BASE_URL = "http://public.highfidelity.io/sounds/Animals/"; - + for (var i = 0; i < howMany; i++) { var whichBird = Math.floor(Math.random() * sound_filenames.length); - var position = { - x: lowerCorner.x + (upperCorner.x - lowerCorner.x) / 2.0 + (Math.random() - 0.5) * (upperCorner.x - lowerCorner.x) * STARTING_FRACTION, - y: lowerCorner.y + (upperCorner.y - lowerCorner.y) / 2.0 + (Math.random() - 0.5) * (upperCorner.y - lowerCorner.y) * STARTING_FRACTION, + var position = { + x: lowerCorner.x + (upperCorner.x - lowerCorner.x) / 2.0 + (Math.random() - 0.5) * (upperCorner.x - lowerCorner.x) * STARTING_FRACTION, + y: lowerCorner.y + (upperCorner.y - lowerCorner.y) / 2.0 + (Math.random() - 0.5) * (upperCorner.y - lowerCorner.y) * STARTING_FRACTION, z: lowerCorner.z + (upperCorner.z - lowerCorner.x) / 2.0 + (Math.random() - 0.5) * (upperCorner.z - lowerCorner.z) * STARTING_FRACTION - }; + }; birds.push({ - sound: SoundCache.getSound(SOUND_BASE_URL + sound_filenames[whichBird]), + sound: SoundCache.getSound(SOUND_BASE_URL + sound_filenames[whichBird]), entityId: Entities.addEntity({ type: "Sphere", position: position, @@ -282,8 +282,8 @@ function loadBirds(howMany) { } if (MAKE_FLOOR) { var FLOOR_THICKNESS = 0.05; - floor = Entities.addEntity({ type: "Box", position: { x: lowerCorner.x + (upperCorner.x - lowerCorner.x) / 2.0, - y: lowerCorner.y, + floor = Entities.addEntity({ type: "Box", position: { x: lowerCorner.x + (upperCorner.x - lowerCorner.x) / 2.0, + y: lowerCorner.y, z: lowerCorner.z + (upperCorner.z - lowerCorner.z) / 2.0 }, dimensions: { x: (upperCorner.x - lowerCorner.x), y: FLOOR_THICKNESS, z: (upperCorner.z - lowerCorner.z)}, color: {red: 100, green: 100, blue: 100} diff --git a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index 381a7ee902..30567b4fc7 100644 --- a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -49,7 +49,7 @@ function debug() { // Display the arguments not just [Object object]. EntityViewer.setCenterRadius(QUERY_RADIUS); // ENTITY DATA CACHE -// +// var entityCache = {}; // A dictionary of unexpired EntityData objects. var examinationCount = 0; function EntityDatum(entityIdentifier) { // Just the data of an entity that we need to know about. @@ -146,7 +146,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n return; } that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC - if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap. + if (!that.injector.playing) { // Subtle: a looping sound will not check playbackGap. if (repeat()) { // WAITING => PLAYING // Setup next play just once, now. Changes won't be looked at while we wait. that.playAfter = randomizedNextPlay(); @@ -208,7 +208,7 @@ function updateAllEntityData() { // A fast update of all entities we know about. stats.entities++; if (datum.url) { stats.sounds++; - if (datum.injector && datum.injector.isPlaying) { + if (datum.injector && datum.injector.playing) { stats.playing++; } } diff --git a/script-archive/avatarSelector.js b/script-archive/avatarSelector.js index dc2916a1a8..47740ef0b3 100644 --- a/script-archive/avatarSelector.js +++ b/script-archive/avatarSelector.js @@ -283,7 +283,7 @@ function actionStartEvent(event) { if (avatarIndex < avatars.length) { var actionPlace = avatars[avatarIndex]; - print("Changing avatar to " + actionPlace.name + print("Changing avatar to " + actionPlace.name + " after click on panel " + panelIndex + " with avatar index " + avatarIndex); MyAvatar.useFullAvatarURL(actionPlace.content_url); @@ -395,7 +395,7 @@ function update(deltaTime) { Overlays.editOverlay(descriptionText, { position: textOverlayPosition() }); // if the reticle is up then we may need to play the next muzak - if (currentMuzakInjector && !currentMuzakInjector.isPlaying) { + if (currentMuzakInjector && !currentMuzakInjector.playing) { playNextMuzak(); } } diff --git a/script-archive/baseball/baseballCrowd.js b/script-archive/baseball/baseballCrowd.js index de9b53f9ec..1459ce6e67 100644 --- a/script-archive/baseball/baseballCrowd.js +++ b/script-archive/baseball/baseballCrowd.js @@ -21,7 +21,7 @@ var CHATTER_VOLUME = 0.20 var EXTRA_VOLUME = 0.25 function playChatter() { - if (chatter.downloaded && !chatter.isPlaying) { + if (chatter.downloaded && !chatter.playing) { Audio.playSound(chatter, { loop: true, volume: CHATTER_VOLUME }); } } @@ -31,7 +31,7 @@ chatter.ready.connect(playChatter); var currentInjector = null; function playRandomExtras() { - if ((!currentInjector || !currentInjector.isPlaying) && (Math.random() < (1.0 / 1800.0))) { + if ((!currentInjector || !currentInjector.playing) && (Math.random() < (1.0 / 1800.0))) { // play a random extra sound about every 30s currentInjector = Audio.playSound( extras[Math.floor(Math.random() * extras.length)], diff --git a/script-archive/controllers/hydra/airGuitar.js b/script-archive/controllers/hydra/airGuitar.js index f8606808c1..73c7099eed 100644 --- a/script-archive/controllers/hydra/airGuitar.js +++ b/script-archive/controllers/hydra/airGuitar.js @@ -22,12 +22,12 @@ function printVector(v) { return; } -function vMinus(a, b) { +function vMinus(a, b) { var rval = { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }; return rval; } -// The model file to be used for the guitar +// The model file to be used for the guitar var guitarModel = HIFI_PUBLIC_BUCKET + "models/attachments/guitar.fst"; // Load sounds that will be played @@ -47,7 +47,7 @@ chords[6] = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Guitars/Guitar+-+Me chords[7] = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Guitars/Guitar+-+Metal+E+short.raw"); chords[8] = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Guitars/Guitar+-+Metal+G+short.raw"); -// Steel Guitar +// Steel Guitar chords[9] = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Guitars/Guitar+-+Steel+A.raw"); chords[10] = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Guitars/Guitar+-+Steel+B.raw"); chords[11] = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Guitars/Guitar+-+Steel+E.raw"); @@ -83,8 +83,8 @@ if (leftHanded) { } var lastPosition = { x: 0.0, - y: 0.0, - z: 0.0 }; + y: 0.0, + z: 0.0 }; var audioInjector = null; var selectorPressed = false; @@ -106,7 +106,7 @@ function checkHands(deltaTime) { var chord = Controller.getValue(chordTrigger); if (volume > 1.0) volume = 1.0; - if ((chord > 0.1) && audioInjector && audioInjector.isPlaying) { + if ((chord > 0.1) && audioInjector && audioInjector.playing) { // If chord finger trigger pulled, stop current chord print("stopping chord because cord trigger pulled"); audioInjector.stop(); @@ -119,7 +119,7 @@ function checkHands(deltaTime) { guitarSelector += NUM_CHORDS; if (guitarSelector >= NUM_CHORDS * NUM_GUITARS) { guitarSelector = 0; - } + } print("new guitarBase: " + guitarSelector); stopAudio(true); selectorPressed = true; @@ -160,7 +160,7 @@ function checkHands(deltaTime) { } function stopAudio(killInjector) { - if (audioInjector && audioInjector.isPlaying) { + if (audioInjector && audioInjector.playing) { print("stopped sound"); audioInjector.stop(); } @@ -212,4 +212,3 @@ function scriptEnding() { Script.update.connect(checkHands); Script.scriptEnding.connect(scriptEnding); Controller.keyPressEvent.connect(keyPressEvent); - diff --git a/script-archive/drylake/ratCreator.js b/script-archive/drylake/ratCreator.js index 60ccf1a1a3..6f6b322f84 100644 --- a/script-archive/drylake/ratCreator.js +++ b/script-archive/drylake/ratCreator.js @@ -340,7 +340,7 @@ function moveRats() { var metaRat = getMetaRatByRat(rat); if (metaRat !== undefined) { if (metaRat.injector !== undefined) { - if (metaRat.injector.isPlaying === true) { + if (metaRat.injector.playing === true) { metaRat.injector.options = { loop: true, position: ratPosition diff --git a/script-archive/entityScripts/movable.js b/script-archive/entityScripts/movable.js index b7ecfbbc8e..06b30ce15e 100644 --- a/script-archive/entityScripts/movable.js +++ b/script-archive/entityScripts/movable.js @@ -8,7 +8,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function(){ +(function(){ this.entityID = null; this.properties = null; @@ -30,13 +30,13 @@ "http://public.highfidelity.io/sounds/MovingFurniture/FurnitureMove2.wav", "http://public.highfidelity.io/sounds/MovingFurniture/FurnitureMove3.wav" ]; - + this.turnSoundURLS = [ "http://public.highfidelity.io/sounds/MovingFurniture/FurnitureMove1.wav", "http://public.highfidelity.io/sounds/MovingFurniture/FurnitureMove2.wav", "http://public.highfidelity.io/sounds/MovingFurniture/FurnitureMove3.wav" - + // TODO: determine if these or other turn sounds work better than move sounds. //"http://public.highfidelity.io/sounds/MovingFurniture/FurnitureTurn1.wav", //"http://public.highfidelity.io/sounds/MovingFurniture/FurnitureTurn2.wav", @@ -50,7 +50,7 @@ this.turnSound = null; this.moveInjector = null; this.turnInjector = null; - + var debug = false; var displayRotateTargets = true; // change to false if you don't want the rotate targets var rotateOverlayTargetSize = 10000; // really big target @@ -61,12 +61,12 @@ var yawZero; var rotationNormal; var yawNormal; - var stopSoundDelay = 100; // number of msecs of not moving to have sound stop - + var stopSoundDelay = 100; // number of msecs of not moving to have sound stop + this.getRandomInt = function(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; - } - + } + this.downloadSounds = function() { for (var i = 0; i < this.moveSoundURLS.length; i++) { this.moveSounds[i] = SoundCache.getSound(this.moveSoundURLS[i]); @@ -95,7 +95,7 @@ if (debug) { print("playMoveSound() --- calling this.moveInjector = Audio.playSound(this.moveSound...)"); } - + if (!this.moveInjector) { this.moveInjector = Audio.playSound(this.moveSound, { position: this.properties.position, loop: true, volume: 0.1 }); } else { @@ -148,7 +148,7 @@ var upVector = { x: 0, y: 1, z: 0 }; var intersection = this.rayPlaneIntersection(pickRay.origin, pickRay.direction, this.properties.position, upVector); - + var newPosition = Vec3.sum(intersection, this.graboffset); Entities.editEntity(this.entityID, { position: newPosition }); }; @@ -158,7 +158,7 @@ var pickRay = Camera.computePickRay(mouseEvent.x, mouseEvent.y) var upVector = { x: 0, y: 1, z: 0 }; var intersection = this.rayPlaneIntersection(pickRay.origin, pickRay.direction, - this.properties.position, upVector); + this.properties.position, upVector); this.graboffset = Vec3.subtract(this.properties.position, intersection); }; @@ -183,18 +183,18 @@ this.lastMovedPosition.y = mouseEvent.y; } } - + this.move = function(mouseEvent) { this.updatePosition(mouseEvent); - if (this.moveInjector === null || !this.moveInjector.isPlaying) { + if (this.moveInjector === null || !this.moveInjector.playing) { this.playMoveSound(); } }; - + this.release = function(mouseEvent) { this.updatePosition(mouseEvent); }; - + this.rotate = function(mouseEvent) { var pickRay = Camera.computePickRay(mouseEvent.x, mouseEvent.y) var result = Overlays.findRayIntersection(pickRay); @@ -205,7 +205,7 @@ var centerToZero = Vec3.subtract(center, zero); var centerToIntersect = Vec3.subtract(center, result.intersection); var angleFromZero = Vec3.orientedAngle(centerToZero, centerToIntersect, rotationNormal); - + var distanceFromCenter = Vec3.distance(center, result.intersection); var snapToInner = false; // var innerRadius = (Vec3.length(selectionManager.worldDimensions) / 2) * 1.1; @@ -213,10 +213,10 @@ angleFromZero = Math.floor(angleFromZero/innerSnapAngle) * innerSnapAngle; snapToInner = true; } - + var yawChange = Quat.fromVec3Degrees({ x: 0, y: angleFromZero, z: 0 }); Entities.editEntity(this.entityID, { rotation: Quat.multiply(yawChange, this.originalRotation) }); - + // update the rotation display accordingly... var startAtCurrent = 360-angleFromZero; @@ -245,7 +245,7 @@ } } - if (this.turnInjector === null || !this.turnInjector.isPlaying) { + if (this.turnInjector === null || !this.turnInjector.playing) { this.playTurnSound(); } }; @@ -267,7 +267,7 @@ this.rotateOverlayOuter = null; this.rotateOverlayCurrent = null; } - + this.displayRotateOverlay = function(mouseEvent) { var yawOverlayAngles = { x: 90, y: 0, z: 0 }; var yawOverlayRotation = Quat.fromVec3Degrees(yawOverlayAngles); @@ -356,14 +356,14 @@ var pickRay = Camera.computePickRay(mouseEvent.x, mouseEvent.y) var result = Overlays.findRayIntersection(pickRay); yawZero = result.intersection; - + }; - + this.preload = function(entityID) { this.updateProperties(entityID); // All callbacks start by updating the properties this.downloadSounds(); }; - + this.clickDownOnEntity = function(entityID, mouseEvent) { this.updateProperties(entityID); // All callbacks start by updating the properties this.grab(mouseEvent); @@ -372,13 +372,13 @@ var nowMSecs = nowDate.getTime(); this.clickedAt = nowMSecs; this.firstHolding = true; - + this.clicked.x = mouseEvent.x; this.clicked.y = mouseEvent.y; this.lastMovedPosition.x = mouseEvent.x; this.lastMovedPosition.y = mouseEvent.y; this.lastMovedMSecs = nowMSecs; - + this.pickRandomSounds(); }; @@ -391,7 +391,7 @@ if (this.clicked.x == mouseEvent.x && this.clicked.y == mouseEvent.y) { var d = new Date(); var now = d.getTime(); - + if (now - this.clickedAt > 500) { this.displayRotateOverlay(mouseEvent); this.firstHolding = false; @@ -402,13 +402,13 @@ this.firstHolding = false; } } - + if (this.rotateMode) { this.rotate(mouseEvent); } else { this.move(mouseEvent); } - + this.stopSoundIfNotMoving(mouseEvent); }; this.clickReleaseOnEntity = function(entityID, mouseEvent) { @@ -418,7 +418,7 @@ } else { this.release(mouseEvent); } - + if (this.rotateOverlayTarget != null) { this.cleanupRotateOverlay(); this.rotateMode = false; diff --git a/script-archive/example/audio/birdSongs.js b/script-archive/example/audio/birdSongs.js index 557fc81f5b..9e949a19ed 100644 --- a/script-archive/example/audio/birdSongs.js +++ b/script-archive/example/audio/birdSongs.js @@ -22,7 +22,7 @@ var BIRD_VELOCITY = 2.0; var LIGHT_RADIUS = 10.0; var BIRD_MASTER_VOLUME = 0.5; -var useLights = true; +var useLights = true; function randomVector(scale) { return { x: Math.random() * scale - scale / 2.0, y: Math.random() * scale - scale / 2.0, z: Math.random() * scale - scale / 2.0 }; @@ -33,11 +33,11 @@ function maybePlaySound(deltaTime) { // Set the location and other info for the sound to play var whichBird = Math.floor(Math.random() * birds.length); //print("playing sound # " + whichBird); - var position = { - x: lowerCorner.x + Math.random() * (upperCorner.x - lowerCorner.x), - y: lowerCorner.y + Math.random() * (upperCorner.y - lowerCorner.y), - z: lowerCorner.z + Math.random() * (upperCorner.z - lowerCorner.z) - }; + var position = { + x: lowerCorner.x + Math.random() * (upperCorner.x - lowerCorner.x), + y: lowerCorner.y + Math.random() * (upperCorner.y - lowerCorner.y), + z: lowerCorner.z + Math.random() * (upperCorner.z - lowerCorner.z) + }; var options = { position: position, volume: BIRD_MASTER_VOLUME @@ -63,31 +63,31 @@ function maybePlaySound(deltaTime) { constantAttenuation: 0, linearAttenuation: 4.0, - quadraticAttenuation: 2.0, + quadraticAttenuation: 2.0, lifetime: 10 }); } - + playing.push({ audioId: Audio.playSound(birds[whichBird].sound, options), entityId: entityId, lightId: lightId, color: birds[whichBird].color }); } if (playing.length != numPlaying) { numPlaying = playing.length; //print("number playing = " + numPlaying); - } + } for (var i = 0; i < playing.length; i++) { - if (!playing[i].audioId.isPlaying) { + if (!playing[i].audioId.playing) { Entities.deleteEntity(playing[i].entityId); if (useLights) { Entities.deleteEntity(playing[i].lightId); - } + } playing.splice(i, 1); } else { var loudness = playing[i].audioId.loudness; var newColor = { red: playing[i].color.red, green: playing[i].color.green, blue: playing[i].color.blue }; if (loudness > 0.05) { - newColor.red *= (1.0 - loudness); - newColor.green *= (1.0 - loudness); - newColor.blue *= (1.0 - loudness); + newColor.red *= (1.0 - loudness); + newColor.green *= (1.0 - loudness); + newColor.blue *= (1.0 - loudness); } var properties = Entities.getEntityProperties(playing[i].entityId); var newPosition = Vec3.sum(properties.position, randomVector(BIRD_VELOCITY * deltaTime)); @@ -120,7 +120,7 @@ Script.scriptEnding.connect(function() { }); function loadBirds() { - var sound_filenames = ["bushtit_1.raw", "bushtit_2.raw", "bushtit_3.raw", "mexicanWhipoorwill.raw", + var sound_filenames = ["bushtit_1.raw", "bushtit_2.raw", "bushtit_3.raw", "mexicanWhipoorwill.raw", "rosyfacedlovebird.raw", "saysphoebe.raw", "westernscreechowl.raw", "bandtailedpigeon.wav", "bridledtitmouse.wav", "browncrestedflycatcher.wav", "commonnighthawk.wav", "commonpoorwill.wav", "doublecrestedcormorant.wav", "gambelsquail.wav", "goldcrownedkinglet.wav", "greaterroadrunner.wav","groovebilledani.wav","hairywoodpecker.wav", @@ -155,13 +155,13 @@ function loadBirds() { { red: 216, green: 153, blue: 99 }, { red: 242, green: 226, blue: 64 } ]; - + var SOUND_BASE_URL = "http://public.highfidelity.io/sounds/Animals/"; - + for (var i = 0; i < sound_filenames.length; i++) { birds.push({ - sound: SoundCache.getSound(SOUND_BASE_URL + sound_filenames[i]), - color: colors[i] + sound: SoundCache.getSound(SOUND_BASE_URL + sound_filenames[i]), + color: colors[i] }); } -} \ No newline at end of file +} diff --git a/script-archive/lobby.js b/script-archive/lobby.js index 3095740c93..6fa4a42cb6 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -88,19 +88,19 @@ var DRONE_VOLUME = 0.3; function drawLobby() { if (!panelWall) { print("Adding overlays for the lobby panel wall and orb shell."); - + var cameraEuler = Quat.safeEulerAngles(Camera.orientation); var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { x: 0, y: 1, z: 0}); - + var orbPosition = Vec3.sum(Camera.position, Vec3.multiplyQbyV(towardsMe, ORB_SHIFT)); - + var panelWallProps = { url: LOBBY_PANEL_WALL_URL, position: Vec3.sum(orbPosition, Vec3.multiplyQbyV(towardsMe, panelsCenterShift)), rotation: towardsMe, dimensions: panelsDimensions }; - + var orbShellProps = { url: LOBBY_SHELL_URL, position: orbPosition, @@ -128,13 +128,13 @@ function drawLobby() { visible: false, isFacingAvatar: true }; - + avatarStickPosition = MyAvatar.position; - panelWall = Overlays.addOverlay("model", panelWallProps); + panelWall = Overlays.addOverlay("model", panelWallProps); orbShell = Overlays.addOverlay("model", orbShellProps); descriptionText = Overlays.addOverlay("text3d", descriptionTextProps); - + if (droneSound.downloaded) { // start the drone sound if (!currentDrone) { @@ -143,7 +143,7 @@ function drawLobby() { currentDrone.restart(); } } - + // start one of our muzak sounds playRandomMuzak(); } @@ -157,31 +157,31 @@ function changeLobbyTextures() { req.send(); places = JSON.parse(req.responseText).data.places; - + var NUM_PANELS = places.length; - var textureProp = { + var textureProp = { textures: {} }; - + for (var j = 0; j < NUM_PANELS; j++) { var panelIndex = placeIndexToPanelIndex(j); textureProp["textures"]["file" + panelIndex] = places[j].previews.lobby; }; - + Overlays.editOverlay(panelWall, textureProp); } var MUZAK_VOLUME = 0.1; -function playCurrentSound(secondOffset) { +function playCurrentSound(secondOffset) { if (currentSound == latinSound) { if (!latinInjector) { latinInjector = Audio.playSound(latinSound, { localOnly: true, secondOffset: secondOffset, volume: MUZAK_VOLUME }); } else { latinInjector.restart(); } - + currentMuzakInjector = latinInjector; } else if (currentSound == elevatorSound) { if (!elevatorInjector) { @@ -189,7 +189,7 @@ function playCurrentSound(secondOffset) { } else { elevatorInjector.restart(); } - + currentMuzakInjector = elevatorInjector; } } @@ -205,14 +205,14 @@ function playNextMuzak() { currentSound = latinSound; } } - + playCurrentSound(0); } } function playRandomMuzak() { currentSound = null; - + if (latinSound.downloaded && elevatorSound.downloaded) { currentSound = Math.random() < 0.5 ? latinSound : elevatorSound; } else if (latinSound.downloaded) { @@ -220,11 +220,11 @@ function playRandomMuzak() { } else if (elevatorSound.downloaded) { currentSound = elevatorSound; } - + if (currentSound) { // pick a random number of seconds from 0-10 to offset the muzak var secondOffset = Math.random() * 10; - + playCurrentSound(secondOffset); } else { currentMuzakInjector = null; @@ -233,36 +233,36 @@ function playRandomMuzak() { function cleanupLobby() { toggleEnvironmentRendering(true); - + // for each of the 21 placeholder textures, set them back to default so the cached model doesn't have changed textures var panelTexturesReset = {}; panelTexturesReset["textures"] = {}; - - for (var j = 0; j < MAX_NUM_PANELS; j++) { + + for (var j = 0; j < MAX_NUM_PANELS; j++) { panelTexturesReset["textures"]["file" + (j + 1)] = LOBBY_BLANK_PANEL_TEXTURE_URL; }; - + Overlays.editOverlay(panelWall, panelTexturesReset); - + Overlays.deleteOverlay(panelWall); Overlays.deleteOverlay(orbShell); Overlays.deleteOverlay(descriptionText); - + panelWall = false; orbShell = false; - + if (currentDrone) { currentDrone.stop(); currentDrone = null } - + if (currentMuzakInjector) { currentMuzakInjector.stop(); currentMuzakInjector = null; } - + places = {}; - + } function actionStartEvent(event) { @@ -271,19 +271,19 @@ function actionStartEvent(event) { // check if we hit a panel and if we should jump there var result = Overlays.findRayIntersection(event.actionRay); if (result.intersects && result.overlayID == panelWall) { - + var panelName = result.extraInfo; - + var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { var panelIndex = parseInt(panelName.slice(5)); var placeIndex = panelIndexToPlaceIndex(panelIndex); if (placeIndex < places.length) { var actionPlace = places[placeIndex]; - - print("Jumping to " + actionPlace.name + " at " + actionPlace.address + + print("Jumping to " + actionPlace.name + " at " + actionPlace.address + " after click on panel " + panelIndex + " with place index " + placeIndex); - + Window.location = actionPlace.address; maybeCleanupLobby(); } @@ -328,7 +328,7 @@ function handleLookAt(pickRay) { var placeIndex = panelIndexToPlaceIndex(panelIndex); if (placeIndex < places.length) { var actionPlace = places[placeIndex]; - + if (actionPlace.description == "") { Overlays.editOverlay(descriptionText, { text: actionPlace.name, visible: showText }); } else { @@ -378,7 +378,7 @@ function update(deltaTime) { Overlays.editOverlay(descriptionText, { position: textOverlayPosition() }); // if the reticle is up then we may need to play the next muzak - if (currentMuzakInjector && !currentMuzakInjector.isPlaying) { + if (currentMuzakInjector && !currentMuzakInjector.playing) { playNextMuzak(); } } diff --git a/script-archive/playTestSound.js b/script-archive/playTestSound.js index 318df6a257..573c8879c4 100644 --- a/script-archive/playTestSound.js +++ b/script-archive/playTestSound.js @@ -2,12 +2,12 @@ // playTestSound.js // examples // -// Created by Philip Rosedale +// Created by Philip Rosedale // Copyright 2014 High Fidelity, Inc. // -// Creates an object in front of you that changes color and plays a light -// at the start of a drum clip that loops. As you move away it will tell you in the -// log how many meters you are from the source. +// Creates an object in front of you that changes color and plays a light +// at the start of a drum clip that loops. As you move away it will tell you in the +// log how many meters you are from the source. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -17,7 +17,7 @@ var sound = SoundCache.getSound("https://s3.amazonaws.com/hifi-public/sounds/Dru var position = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 }), Quat.getFront(MyAvatar.orientation)); -var time; +var time; var soundPlaying = null; var baseColor = { red: 100, green: 100, blue: 100 }; @@ -38,8 +38,8 @@ var box = Entities.addEntity({ function checkSound(deltaTime) { var started = false; - if (!sound.downloaded) { - return; + if (!sound.downloaded) { + return; } if (soundPlaying == null) { soundPlaying = Audio.playSound(sound, { @@ -47,9 +47,9 @@ function checkSound(deltaTime) { volume: 1.0, loop: false } ); started = true; - } else if (!soundPlaying.isPlaying) { + } else if (!soundPlaying.playing) { soundPlaying.restart(); - started = true; + started = true; } if (started) { Entities.editEntity(box, { color: litColor }); @@ -67,19 +67,19 @@ function checkSound(deltaTime) { lifetime: lightTime / 1000 }); Script.setTimeout(resetColor, lightTime); - } + } var currentDistance = Vec3.distance(MyAvatar.position, position); if (Math.abs(currentDistance - distance) > 1.0) { print("Distance from source: " + currentDistance); distance = currentDistance; - } + } } function resetColor() { Entities.editEntity(box, { color: baseColor }); } - + function scriptEnding() { Entities.deleteEntity(box); if (soundPlaying) { @@ -93,4 +93,3 @@ function scriptEnding() { // Connect a call back that happens every frame Script.scriptEnding.connect(scriptEnding); Script.update.connect(checkSound); - From 8f3918b5baa01bf350253d5a0c4ccc6ced6edfaa Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 25 May 2016 11:21:05 -0700 Subject: [PATCH 0226/1237] Fix primitive shape collisions --- libraries/entities/src/ShapeEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 2527dedab2..c39a477a35 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -161,7 +161,7 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit // This value specifes how the shape should be treated by physics calculations. // For now, all polys will act as spheres ShapeType ShapeEntityItem::getShapeType() const { - return SHAPE_TYPE_ELLIPSOID; + return (_shape == entity::Shape::Cube) ? SHAPE_TYPE_BOX : SHAPE_TYPE_SPHERE; } void ShapeEntityItem::setColor(const rgbColor& value) { From 8198a1b4d110bd334fe7f272961148fa4caffa5f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 25 May 2016 11:30:45 -0700 Subject: [PATCH 0227/1237] Add preference to control behavior. --- interface/src/avatar/MyAvatar.cpp | 2 + interface/src/avatar/MyAvatar.h | 3 ++ interface/src/ui/OverlayConductor.cpp | 62 +++++++++++++------------- interface/src/ui/PreferencesDialog.cpp | 5 +++ 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 94b213420f..e1159cf962 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -733,6 +733,7 @@ void MyAvatar::saveData() { settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); settings.setValue("useSnapTurn", _useSnapTurn); + settings.setValue("clearOverlayWhenDriving", _clearOverlayWhenDriving); settings.endGroup(); } @@ -855,6 +856,7 @@ void MyAvatar::loadData() { setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); + setSnapTurn(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool()); settings.endGroup(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 92f006feb8..135e9f6417 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -160,6 +160,8 @@ public: Q_INVOKABLE bool getSnapTurn() const { return _useSnapTurn; } Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } + Q_INVOKABLE bool getClearOverlayWhenDriving() const { return _clearOverlayWhenDriving; } + Q_INVOKABLE void setClearOverlayWhenDriving(bool on) { _clearOverlayWhenDriving = on; } // get/set avatar data void saveData(); @@ -396,6 +398,7 @@ private: QString _fullAvatarModelName; QUrl _animGraphUrl {""}; bool _useSnapTurn { true }; + bool _clearOverlayWhenDriving { true }; // cache of the current HMD sensor position and orientation // in sensor space. diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index fa74989f4f..cb211fd918 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -68,37 +68,39 @@ void OverlayConductor::update(float dt) { void OverlayConductor::updateMode() { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - float speed = glm::length(myAvatar->getVelocity()); - const float MIN_DRIVING = 0.2f; - const float MAX_NOT_DRIVING = 0.01f; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; - bool nowDriving = _driving; // Assume current _driving mode unless... - if (speed > MIN_DRIVING) { // ... we're definitely moving... - nowDriving = true; - } else if (speed < MAX_NOT_DRIVING) { // ... or definitely not. - nowDriving = false; + if (myAvatar->getClearOverlayWhenDriving()) { + float speed = glm::length(myAvatar->getVelocity()); + const float MIN_DRIVING = 0.2f; + const float MAX_NOT_DRIVING = 0.01f; + const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; + const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; + bool nowDriving = _driving; // Assume current _driving mode unless... + if (speed > MIN_DRIVING) { // ... we're definitely moving... + nowDriving = true; + } else if (speed < MAX_NOT_DRIVING) { // ... or definitely not. + nowDriving = false; + } + // Check that we're in this new mode for long enough to really trigger a transition. + if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. + _timeInPotentialMode = 0; + } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. + _timeInPotentialMode = usecTimestampNow(); + } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { + _timeInPotentialMode = 0; // a real transition + if (nowDriving) { + _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); + } else { // reset when coming out of driving + _mode = FLAT; // Seems appropriate to let things reset, below, after the following. + // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. + qApp->getActiveDisplayPlugin()->resetSensors(); + myAvatar->reset(true, false); + } + if (_wantsOverlays) { + setEnabled(!nowDriving, false); + } + _driving = nowDriving; + } // Else haven't accumulated enough time in new mode, but keep timing. } - // Check that we're in this new mode for long enough to really trigger a transition. - if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. - _timeInPotentialMode = 0; - } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. - _timeInPotentialMode = usecTimestampNow(); - } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { - _timeInPotentialMode = 0; // a real transition - if (nowDriving) { - _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); - } else { // reset when coming out of driving - _mode = FLAT; // Seems appropriate to let things reset, below, after the following. - // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. - qApp->getActiveDisplayPlugin()->resetSensors(); - myAvatar->reset(true, false); - } - if (_wantsOverlays) { - setEnabled(!nowDriving, false); - } - _driving = nowDriving; - } // Else haven't accumulated enough time in new mode, but keep timing. Mode newMode; if (qApp->isHMDMode()) { diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 9b1146340e..8f60844cc3 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -61,6 +61,11 @@ void setupPreferences() { auto setter = [=](bool value) { myAvatar->setSnapTurn(value); }; preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Snap turn when in HMD", getter, setter)); } + { + auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenDriving(); }; + auto setter = [=](bool value) { myAvatar->setClearOverlayWhenDriving(value); }; + preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when driving", getter, setter)); + } { auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; auto setter = [](const QString& value) { Snapshot::snapshotsLocation.set(value); }; From 3137cd64e1c9cb499e85dd579fdbf120bdbddf46 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 25 May 2016 11:40:17 -0700 Subject: [PATCH 0228/1237] clear AddressManager previous lookup on 404 --- libraries/networking/src/AddressManager.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 619a1d7903..80989acd2c 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -383,8 +383,12 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) { qCDebug(networking) << "AddressManager API error -" << errorReply.error() << "-" << errorReply.errorString(); if (errorReply.error() == QNetworkReply::ContentNotFoundError) { + // if this is a lookup that has no result, don't keep re-trying it + _previousLookup.clear(); + emit lookupResultIsNotFound(); } + emit lookupResultsFinished(); } From b0ce65ec0197e2c7154ca7f21573337638235063 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 25 May 2016 11:42:06 -0700 Subject: [PATCH 0229/1237] trying to mimic previous behavior more closely --- assignment-client/src/Agent.cpp | 2 +- .../src/EntityTreeRenderer.cpp | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 12 ++++--- libraries/script-engine/src/ScriptEngine.h | 2 +- libraries/script-engine/src/ScriptEngines.cpp | 31 +++++++++++-------- libraries/script-engine/src/ScriptEngines.h | 1 + 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 65e193dec6..327f6de695 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -476,7 +476,7 @@ void Agent::aboutToFinish() { setIsAvatar(false);// will stop timers for sending identity packets if (_scriptEngine) { - _scriptEngine->stop(); + _scriptEngine->stop(false); } // our entity tree is going to go away so tell that to the EntityScriptingInterface diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 56f6438e70..bc3c6b2441 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -114,7 +114,7 @@ void EntityTreeRenderer::clear() { // Unload and stop the engine here (instead of in its deleter) to // avoid marshalling unload signals back to this thread _entitiesScriptEngine->unloadAllEntityScripts(); - _entitiesScriptEngine->stop(); + _entitiesScriptEngine->stop(false); } if (_wantScripts && !_shuttingDown) { diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index e7938ac5a0..61ab8c0c63 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -290,7 +290,7 @@ void ScriptEngine::waitTillDoneRunning() { assert(workerThread != QThread::currentThread()); // Engine should be stopped already, but be defensive - stop(); + stop(false); auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { @@ -941,13 +941,15 @@ void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { } -void ScriptEngine::stop() { +void ScriptEngine::stop(bool marshal) { _isStopping = true; // this can be done on any thread // marshal us over to the correct thread - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "stop"); - return; + if (marshal) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stop"); + return; + } } if (!_isFinished) { _isFinished = true; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index ef7e075021..4d39365626 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -84,7 +84,7 @@ public: //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts - Q_INVOKABLE void stop(); // this can be called from any thread + Q_INVOKABLE void stop(bool marshal); // this can be called from any thread // Stop any evaluating scripts and wait for the scripting thread to finish. void waitTillDoneRunning(); diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 5e5b8bfde9..6575a12c99 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -160,7 +160,7 @@ void ScriptEngines::shutdownScripting() { scriptEngine->disconnect(this); // Gracefully stop the engine's scripting thread - scriptEngine->stop(); + scriptEngine->stop(false); // We need to wait for the engine to be done running before we proceed, because we don't // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing @@ -359,7 +359,10 @@ QStringList ScriptEngines::getRunningScripts() { QList urls = _scriptEnginesHash.keys(); QStringList result; for (auto url : urls) { - result.append(url.toString()); + ScriptEngine* engine = getScriptEngineInternal(url); + if (engine) { + result.append(url.toString()); + } } return result; } @@ -388,7 +391,7 @@ void ScriptEngines::stopAllScripts(bool restart) { reloadScript(scriptName); }); } - it.value()->stop(); + it.value()->stop(true); qCDebug(scriptengine) << "stopping script..." << it.key(); } } @@ -411,7 +414,7 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { reloadScript(scriptName); }); } - scriptEngine->stop(); + scriptEngine->stop(false); stoppedScript = true; qCDebug(scriptengine) << "stopping script..." << scriptURL; } @@ -459,7 +462,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL } auto scriptEngine = getScriptEngine(scriptUrl); - if (scriptEngine) { + if (scriptEngine && !scriptEngine->isStopping()) { return scriptEngine; } @@ -484,19 +487,21 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL return scriptEngine; } -ScriptEngine* ScriptEngines::getScriptEngine(const QUrl& rawScriptURL) { +ScriptEngine* ScriptEngines::getScriptEngineInternal(const QUrl& rawScriptURL) { ScriptEngine* result = nullptr; - { - QReadLocker lock(&_scriptEnginesHashLock); - const QUrl scriptURL = normalizeScriptURL(rawScriptURL); - auto it = _scriptEnginesHash.find(scriptURL); - if (it != _scriptEnginesHash.end() && !it.value()->isStopping()) { - result = it.value(); - } + const QUrl scriptURL = normalizeScriptURL(rawScriptURL); + auto it = _scriptEnginesHash.find(scriptURL); + if (it != _scriptEnginesHash.end()) { + result = it.value(); } return result; } +ScriptEngine* ScriptEngines::getScriptEngine(const QUrl& rawScriptURL) { + QReadLocker lock(&_scriptEnginesHashLock); + return getScriptEngineInternal(rawScriptURL); +} + // FIXME - change to new version of ScriptCache loading notification void ScriptEngines::onScriptEngineLoaded(const QString& rawScriptURL) { UserActivityLogger::getInstance().loadedScript(rawScriptURL); diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index a9c273b2a7..6d887ce95f 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -87,6 +87,7 @@ protected: void onScriptEngineLoaded(const QString& scriptFilename); void onScriptEngineError(const QString& scriptFilename); void launchScriptEngine(ScriptEngine* engine); + ScriptEngine* getScriptEngineInternal(const QUrl& rawScriptURL); QReadWriteLock _scriptEnginesHashLock; QHash _scriptEnginesHash; From 66bdbf910d2e6cd27aba1e23e5bb8ebf00a3177c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 25 May 2016 11:55:04 -0700 Subject: [PATCH 0230/1237] optionally include the metaverse session ID as an http header --- interface/src/DiscoverabilityManager.cpp | 4 ++++ libraries/networking/src/AccountManager.cpp | 13 +++++++++++-- libraries/networking/src/AccountManager.h | 4 ++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 83f87f82ba..24256fdf39 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -127,6 +127,10 @@ void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply if (!dataObject.isEmpty()) { _sessionID = dataObject[SESSION_ID_KEY].toString(); + + // give that session ID to the account manager + auto accountManager = DependencyManager::get(); + accountManager->setSessionID(_sessionID); } } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 9080e3cc53..46e72170e5 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AccountManager.h" + #include #include @@ -26,13 +28,13 @@ #include +#include "NetworkLogging.h" #include "NodeList.h" #include "udt/PacketHeaders.h" #include "RSAKeypairGenerator.h" #include "SharedUtil.h" +#include "UserActivityLogger.h" -#include "AccountManager.h" -#include "NetworkLogging.h" const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; @@ -216,6 +218,13 @@ void AccountManager::sendRequest(const QString& path, networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + // if we're allowed to send usage data, include whatever the current session ID is with this request + auto& activityLogger = UserActivityLogger::getInstance(); + if (activityLogger.isEnabled()) { + static const QString METAVERSE_SESSION_ID_HEADER = "HFM-SessionID"; + networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER.toLocal8Bit(), _sessionID.toString().toLocal8Bit()); + } + QUrl requestURL = _authURL; if (path.startsWith("/")) { diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 89a2240bbb..4803d2625f 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -86,6 +86,8 @@ public: static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); + void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } + public slots: void requestAccessToken(const QString& login, const QString& password); @@ -136,6 +138,8 @@ private: bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; + + QUuid _sessionID; }; #endif // hifi_AccountManager_h From dc7d6d470ddc6ba5866131dc04a32c1cb7de5bee Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 25 May 2016 12:21:50 -0700 Subject: [PATCH 0231/1237] Don't reset head when we come back. --- interface/src/avatar/MyAvatar.cpp | 6 ++++-- interface/src/avatar/MyAvatar.h | 2 +- interface/src/ui/OverlayConductor.cpp | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 04224d6fa9..358b3d72ba 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -234,7 +234,7 @@ QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) { return AvatarData::toByteArray(cullSmallChanges, sendAll); } -void MyAvatar::reset(bool andRecenter, bool andReload) { +void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "reset", Q_ARG(bool, andRecenter)); @@ -247,7 +247,9 @@ void MyAvatar::reset(bool andRecenter, bool andReload) { if (andReload) { _skeletonModel->reset(); } - getHead()->reset(); + if (andHead) { // which drives camera in desktop + getHead()->reset(); + } setThrust(glm::vec3(0.0f)); if (andRecenter) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 135e9f6417..f709f79542 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -94,7 +94,7 @@ public: AudioListenerMode getAudioListenerModeCamera() const { return FROM_CAMERA; } AudioListenerMode getAudioListenerModeCustom() const { return CUSTOM; } - Q_INVOKABLE void reset(bool andRecenter = false, bool andReload = true); + Q_INVOKABLE void reset(bool andRecenter = false, bool andReload = true, bool andHead = true); void update(float deltaTime); virtual void postUpdate(float deltaTime) override; void preDisplaySide(RenderArgs* renderArgs); diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index cb211fd918..83d729779c 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -93,7 +93,7 @@ void OverlayConductor::updateMode() { _mode = FLAT; // Seems appropriate to let things reset, below, after the following. // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. qApp->getActiveDisplayPlugin()->resetSensors(); - myAvatar->reset(true, false); + myAvatar->reset(true, false, false); } if (_wantsOverlays) { setEnabled(!nowDriving, false); From 49769f7d2969626c515ff554ef9aa1ed716081e6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 25 May 2016 13:39:13 -0700 Subject: [PATCH 0232/1237] trying again -- frantic clicking on reload no longer appears to wedge things --- assignment-client/src/Agent.cpp | 2 +- .../src/EntityTreeRenderer.cpp | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 9 ++---- libraries/script-engine/src/ScriptEngine.h | 2 +- libraries/script-engine/src/ScriptEngines.cpp | 29 ++++++++----------- libraries/script-engine/src/ScriptEngines.h | 1 - 6 files changed, 18 insertions(+), 27 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 327f6de695..65e193dec6 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -476,7 +476,7 @@ void Agent::aboutToFinish() { setIsAvatar(false);// will stop timers for sending identity packets if (_scriptEngine) { - _scriptEngine->stop(false); + _scriptEngine->stop(); } // our entity tree is going to go away so tell that to the EntityScriptingInterface diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index bc3c6b2441..56f6438e70 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -114,7 +114,7 @@ void EntityTreeRenderer::clear() { // Unload and stop the engine here (instead of in its deleter) to // avoid marshalling unload signals back to this thread _entitiesScriptEngine->unloadAllEntityScripts(); - _entitiesScriptEngine->stop(false); + _entitiesScriptEngine->stop(); } if (_wantScripts && !_shuttingDown) { diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 61ab8c0c63..a5e3be8a43 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -290,7 +290,7 @@ void ScriptEngine::waitTillDoneRunning() { assert(workerThread != QThread::currentThread()); // Engine should be stopped already, but be defensive - stop(false); + stop(); auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { @@ -944,12 +944,9 @@ void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { void ScriptEngine::stop(bool marshal) { _isStopping = true; // this can be done on any thread - // marshal us over to the correct thread if (marshal) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "stop"); - return; - } + QMetaObject::invokeMethod(this, "stop"); + return; } if (!_isFinished) { _isFinished = true; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 4d39365626..1077dce686 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -84,7 +84,7 @@ public: //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts - Q_INVOKABLE void stop(bool marshal); // this can be called from any thread + Q_INVOKABLE void stop(bool marshal = false); // Stop any evaluating scripts and wait for the scripting thread to finish. void waitTillDoneRunning(); diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 6575a12c99..29c223f4b3 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -160,7 +160,7 @@ void ScriptEngines::shutdownScripting() { scriptEngine->disconnect(this); // Gracefully stop the engine's scripting thread - scriptEngine->stop(false); + scriptEngine->stop(); // We need to wait for the engine to be done running before we proceed, because we don't // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing @@ -359,10 +359,7 @@ QStringList ScriptEngines::getRunningScripts() { QList urls = _scriptEnginesHash.keys(); QStringList result; for (auto url : urls) { - ScriptEngine* engine = getScriptEngineInternal(url); - if (engine) { - result.append(url.toString()); - } + result.append(url.toString()); } return result; } @@ -383,7 +380,7 @@ void ScriptEngines::stopAllScripts(bool restart) { // Stop and possibly restart all currently running scripts for (QHash::const_iterator it = _scriptEnginesHash.constBegin(); it != _scriptEnginesHash.constEnd(); it++) { - if (it.value()->isFinished()) { + if (it.value()->isFinished() || it.value()->isStopping()) { continue; } if (restart && it.value()->isUserLoaded()) { @@ -414,7 +411,7 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { reloadScript(scriptName); }); } - scriptEngine->stop(false); + scriptEngine->stop(); stoppedScript = true; qCDebug(scriptengine) << "stopping script..." << scriptURL; } @@ -487,21 +484,19 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL return scriptEngine; } -ScriptEngine* ScriptEngines::getScriptEngineInternal(const QUrl& rawScriptURL) { +ScriptEngine* ScriptEngines::getScriptEngine(const QUrl& rawScriptURL) { ScriptEngine* result = nullptr; - const QUrl scriptURL = normalizeScriptURL(rawScriptURL); - auto it = _scriptEnginesHash.find(scriptURL); - if (it != _scriptEnginesHash.end()) { - result = it.value(); + { + QReadLocker lock(&_scriptEnginesHashLock); + const QUrl scriptURL = normalizeScriptURL(rawScriptURL); + auto it = _scriptEnginesHash.find(scriptURL); + if (it != _scriptEnginesHash.end()) { + result = it.value(); + } } return result; } -ScriptEngine* ScriptEngines::getScriptEngine(const QUrl& rawScriptURL) { - QReadLocker lock(&_scriptEnginesHashLock); - return getScriptEngineInternal(rawScriptURL); -} - // FIXME - change to new version of ScriptCache loading notification void ScriptEngines::onScriptEngineLoaded(const QString& rawScriptURL) { UserActivityLogger::getInstance().loadedScript(rawScriptURL); diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 6d887ce95f..a9c273b2a7 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -87,7 +87,6 @@ protected: void onScriptEngineLoaded(const QString& scriptFilename); void onScriptEngineError(const QString& scriptFilename); void launchScriptEngine(ScriptEngine* engine); - ScriptEngine* getScriptEngineInternal(const QUrl& rawScriptURL); QReadWriteLock _scriptEnginesHashLock; QHash _scriptEnginesHash; From b16e701ea3d6a6287ba175240596d6cbbc5c50eb Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 25 May 2016 13:53:28 -0700 Subject: [PATCH 0233/1237] Fix broken protocol for shapes --- libraries/entities/src/ShapeEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index c39a477a35..84208cc6f1 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -155,7 +155,7 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_SHAPE, entity::stringFromShape(getShape())); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); - APPEND_ENTITY_PROPERTY(PROP_COLOR, getAlpha()); + APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); } // This value specifes how the shape should be treated by physics calculations. From 2f3fa80b96eab8a8130fcb6be580932c38e74f00 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 26 May 2016 09:52:22 +1200 Subject: [PATCH 0234/1237] Add Clipboard.getContentsDimensions() JavaScript method --- interface/src/scripting/ClipboardScriptingInterface.cpp | 4 ++++ interface/src/scripting/ClipboardScriptingInterface.h | 1 + libraries/entities/src/EntityTree.cpp | 7 +++++++ libraries/entities/src/EntityTree.h | 1 + 4 files changed, 13 insertions(+) diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index b0ef6c760d..b803080538 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -14,6 +14,10 @@ ClipboardScriptingInterface::ClipboardScriptingInterface() { } +glm::vec3 ClipboardScriptingInterface::getContentsDimensions() { + return qApp->getEntityClipboard()->getContentsDimensions(); +} + float ClipboardScriptingInterface::getClipboardContentsLargestDimension() { return qApp->getEntityClipboard()->getContentsLargestDimension(); } diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index 73bebd4836..4737a194df 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -22,6 +22,7 @@ signals: void readyToImport(); public slots: + glm::vec3 getContentsDimensions(); /// returns the overall dimensions of everything on the blipboard float getClipboardContentsLargestDimension(); /// returns the largest dimension of everything on the clipboard bool importEntities(const QString& filename); bool exportEntities(const QString& filename, const QVector& entityIDs); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index b85ee1dcf9..581e0a9568 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1284,6 +1284,7 @@ class ContentsDimensionOperator : public RecurseOctreeOperator { public: virtual bool preRecursion(OctreeElementPointer element); virtual bool postRecursion(OctreeElementPointer element) { return true; } + glm::vec3 getDimensions() const { return _contentExtents.size(); } float getLargestDimension() const { return _contentExtents.largestDimension(); } private: Extents _contentExtents; @@ -1295,6 +1296,12 @@ bool ContentsDimensionOperator::preRecursion(OctreeElementPointer element) { return true; } +glm::vec3 EntityTree::getContentsDimensions() { + ContentsDimensionOperator theOperator; + recurseTreeWithOperator(&theOperator); + return theOperator.getDimensions(); +} + float EntityTree::getContentsLargestDimension() { ContentsDimensionOperator theOperator; recurseTreeWithOperator(&theOperator); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 03b8a9b55a..a85624c9ae 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -207,6 +207,7 @@ public: bool skipThoseWithBadParents) override; virtual bool readFromMap(QVariantMap& entityDescription) override; + glm::vec3 getContentsDimensions(); float getContentsLargestDimension(); virtual void resetEditStats() override { From f8e2cf80646dec3244ef788457c64d12e7567b7c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 20 May 2016 18:47:06 -0700 Subject: [PATCH 0235/1237] cleanup vhacd-util logging and variable names --- tools/vhacd-util/src/VHACDUtil.cpp | 89 +++++++++++++-------------- tools/vhacd-util/src/VHACDUtil.h | 2 +- tools/vhacd-util/src/VHACDUtilApp.cpp | 35 +++++------ 3 files changed, 60 insertions(+), 66 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 57ad185f45..b21c2ee01e 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -33,7 +33,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { if (!fbx.open(QIODevice::ReadOnly)) { return false; } - std::cout << "Reading FBX.....\n"; + qDebug() << "reading FBX file =" << filename << "..."; try { QByteArray fbxContents = fbx.readAll(); FBXGeometry* geom; @@ -182,31 +182,34 @@ AABox getAABoxForMeshPart(const FBXMesh& mesh, const FBXMeshPart &meshPart) { bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, VHACD::IVHACD::Parameters params, FBXGeometry& result, - int startMeshIndex, - int endMeshIndex, + int startPartIndex, + int endPartIndex, float minimumMeshSize, float maximumMeshSize) { + qDebug() << "num meshes =" << geometry.meshes.size(); + // count the mesh-parts - int meshCount = 0; + int numParts = 0; foreach (const FBXMesh& mesh, geometry.meshes) { - meshCount += mesh.parts.size(); + numParts += mesh.parts.size(); } VHACD::IVHACD * interfaceVHACD = VHACD::CreateVHACD(); - if (startMeshIndex < 0) { - startMeshIndex = 0; + if (startPartIndex < 0) { + startPartIndex = 0; } - if (endMeshIndex < 0) { - endMeshIndex = meshCount; + if (endPartIndex < 0) { + endPartIndex = numParts; } - - std::cout << "Performing V-HACD computation on " << endMeshIndex - startMeshIndex << " meshes ..... " << std::endl; + qDebug() << "num parts of interest =" << (endPartIndex - startPartIndex); result.meshExtents.reset(); result.meshes.append(FBXMesh()); FBXMesh &resultMesh = result.meshes.last(); - int count = 0; + int meshIndex = 0; + int partIndex = 0; + int validPartsFound = 0; foreach (const FBXMesh& mesh, geometry.meshes) { // each mesh has its own transform to move it to model-space @@ -214,51 +217,55 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, foreach (glm::vec3 vertex, mesh.vertices) { vertices.push_back(glm::vec3(mesh.modelTransform * glm::vec4(vertex, 1.0f))); } + auto numVertices = vertices.size(); + + qDebug() << "mesh" << meshIndex << ": " + << " parts =" << mesh.parts.size() << " clusters =" << mesh.clusters.size() + << " vertices =" << numVertices; + ++meshIndex; foreach (const FBXMeshPart &meshPart, mesh.parts) { - - if (count < startMeshIndex || count >= endMeshIndex) { - count ++; + if (partIndex < startPartIndex || partIndex >= endPartIndex) { + ++partIndex; continue; } - qDebug() << "--------------------"; - std::vector triangles; unsigned int triangleCount = getTrianglesInMeshPart(meshPart, triangles); // only process meshes with triangles if (triangles.size() <= 0) { - qDebug() << " Skipping (no triangles)..."; - count++; + qDebug() << " part" << partIndex << ":"; + qDebug() << " skip (zero triangles)"; + ++partIndex; continue; } - auto nPoints = vertices.size(); AABox aaBox = getAABoxForMeshPart(mesh, meshPart); const float largestDimension = aaBox.getLargestDimension(); - qDebug() << "Mesh " << count << " -- " << nPoints << " points, " << triangleCount << " triangles, " - << "size =" << largestDimension; + qDebug() << " part" << partIndex << ": " + << " triangles =" << triangleCount + << " largestDimension =" << largestDimension; if (largestDimension < minimumMeshSize) { - qDebug() << " Skipping (too small)..."; - count++; + qDebug() << " skip (too small)"; + ++partIndex; continue; } if (maximumMeshSize > 0.0f && largestDimension > maximumMeshSize) { - qDebug() << " Skipping (too large)..."; - count++; + qDebug() << " skip (too large)"; + ++partIndex; continue; } // compute approximate convex decomposition - bool res = interfaceVHACD->Compute(&vertices[0].x, 3, (uint)nPoints, &triangles[0], 3, triangleCount, params); - if (!res){ - qDebug() << "V-HACD computation failed for Mesh : " << count; - count++; + bool success = interfaceVHACD->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); + if (!success){ + qDebug() << " failed to convexify"; + ++partIndex; continue; } @@ -290,8 +297,8 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, resultMeshPart.triangleIndices.append(index2); } } - - count++; + ++partIndex; + ++validPartsFound; } } @@ -299,12 +306,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, interfaceVHACD->Clean(); interfaceVHACD->Release(); - if (count > 0){ - return true; - } - else{ - return false; - } + return validPartsFound > 0; } vhacd::VHACDUtil:: ~VHACDUtil(){ @@ -319,16 +321,9 @@ void vhacd::ProgressCallback::Update(const double overallProgress, const char* const operation) { int progress = (int)(overallProgress + 0.5); - if (progress < 10){ - std::cout << "\b\b"; - } - else{ - std::cout << "\b\b\b"; - } - + std::cout << "\b\b\b"; std::cout << progress << "%"; - - if (progress >= 100){ + if (progress >= 100) { std::cout << std::endl; } } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 34c2c22e0e..f394301839 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -34,7 +34,7 @@ namespace vhacd { bool computeVHACD(FBXGeometry& geometry, VHACD::IVHACD::Parameters params, FBXGeometry& result, - int startMeshIndex, int endMeshIndex, + int startPartIndex, int endPartIndex, float minimumMeshSize, float maximumMeshSize); ~VHACDUtil(); }; diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index 2af6bca337..81eb6b399d 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -301,17 +301,17 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : Q_UNREACHABLE(); } - - // load the mesh - + // load the mesh FBXGeometry fbx; auto begin = std::chrono::high_resolution_clock::now(); if (!vUtil.loadFBX(inputFilename, fbx)){ - cout << "Error in opening FBX file...."; + qDebug() << "error reading input file" << inputFilename; + return; } auto end = std::chrono::high_resolution_clock::now(); auto loadDuration = std::chrono::duration_cast(end - begin).count(); + qDebug() << "load time =" << (double)loadDuration / 1000000000.00 << "seconds"; if (splitModel) { QVector infileExtensions = {"fbx", "obj"}; @@ -352,38 +352,37 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : params.m_oclAcceleration = true; // true //perform vhacd computation + qDebug() << "running V-HACD algorithm ..."; begin = std::chrono::high_resolution_clock::now(); FBXGeometry result; - if (!vUtil.computeVHACD(fbx, params, result, startMeshIndex, endMeshIndex, - minimumMeshSize, maximumMeshSize)) { - cout << "Compute Failed..."; - } + bool success = vUtil.computeVHACD(fbx, params, result, startMeshIndex, endMeshIndex, minimumMeshSize, maximumMeshSize); + end = std::chrono::high_resolution_clock::now(); auto computeDuration = std::chrono::duration_cast(end - begin).count(); + qDebug() << "run time =" << (double)computeDuration / 1000000000.00 << " seconds"; + + if (!success) { + qDebug() << "failed to convexify model"; + return; + } int totalVertices = 0; int totalTriangles = 0; - int totalMeshParts = 0; foreach (const FBXMesh& mesh, result.meshes) { totalVertices += mesh.vertices.size(); foreach (const FBXMeshPart &meshPart, mesh.parts) { totalTriangles += meshPart.triangleIndices.size() / 3; // each quad was made into two triangles totalTriangles += 2 * meshPart.quadIndices.size() / 4; - totalMeshParts++; } } int totalHulls = result.meshes[0].parts.size(); - cout << endl << "Summary of V-HACD Computation..................." << endl; - cout << "File Path : " << inputFilename.toStdString() << endl; - cout << "Number Of Meshes : " << totalMeshParts << endl; - cout << "Total vertices : " << totalVertices << endl; - cout << "Total Triangles : " << totalTriangles << endl; - cout << "Total Convex Hulls : " << totalHulls << endl; - cout << "Total FBX load time: " << (double)loadDuration / 1000000000.00 << " seconds" << endl; - cout << "V-HACD Compute time: " << (double)computeDuration / 1000000000.00 << " seconds" << endl; + qDebug() << "output file =" << outputFilename; + qDebug() << "vertices =" << totalVertices; + qDebug() << "triangles =" << totalTriangles; + qDebug() << "hulls =" << totalHulls; writeOBJ(outputFilename, result, outputCentimeters); } From 0647f5b2d096f9b8bd5d3373cb4ddbc0af87c12a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 23 May 2016 11:31:58 -0700 Subject: [PATCH 0236/1237] attempt to close open mesh parts --- tools/vhacd-util/src/VHACDUtil.cpp | 244 ++++++++++++++++++-------- tools/vhacd-util/src/VHACDUtil.h | 1 - tools/vhacd-util/src/VHACDUtilApp.cpp | 2 +- 3 files changed, 174 insertions(+), 73 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index b21c2ee01e..43d820b6dd 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include "VHACDUtil.h" @@ -49,7 +50,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { reSortFBXGeometryMeshes(result); } catch (const QString& error) { - qDebug() << "Error reading " << filename << ": " << error; + qDebug() << "Error reading" << filename << ":" << error; return false; } @@ -57,13 +58,12 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } -unsigned int getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangles) { +void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangles) { // append all the triangles (and converted quads) from this mesh-part to triangles std::vector meshPartTriangles = meshPart.triangleIndices.toStdVector(); triangles.insert(triangles.end(), meshPartTriangles.begin(), meshPartTriangles.end()); // convert quads to triangles - unsigned int triangleCount = meshPart.triangleIndices.size() / 3; unsigned int quadCount = meshPart.quadIndices.size() / 4; for (unsigned int i = 0; i < quadCount; i++) { unsigned int p0Index = meshPart.quadIndices[i * 4]; @@ -77,10 +77,7 @@ unsigned int getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector + struct hash { + std::size_t operator()(const TriangleEdge& edge) const { + return (hash()(edge.indexA) ^ (hash()(edge.indexB) << 1)); + } + }; +} + +// returns false if any edge has only one adjacent triangle +bool isClosedManifold(const std::vector& triangles) { + using EdgeList = std::unordered_map; + EdgeList edges; + + // count the triangles for each edge + for (size_t i = 0; i < triangles.size(); i += 3) { + TriangleEdge edge; + for (int j = 0; j < 3; ++j) { + edge.indexA = triangles[(int)i + j]; + edge.indexB = triangles[i + ((j + 1) % 3)]; + edge.sortIndices(); + + EdgeList::iterator edgeEntry = edges.find(edge); + if (edgeEntry == edges.end()) { + edges.insert(std::pair(edge, 1)); + } else { + edgeEntry->second += 1; + } + } + } + // scan for outside edge + for (auto& edgeEntry : edges) { + if (edgeEntry.second == 1) { + return false; + } + } + return true; +} + +void getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) { + // Number of hulls for this input meshPart + unsigned int numHulls = convexifier->GetNConvexHulls(); + qDebug() << " hulls =" << numHulls; + + // create an output meshPart for each convex hull + for (unsigned int j = 0; j < numHulls; j++) { + VHACD::IVHACD::ConvexHull hull; + convexifier->GetConvexHull(j, hull); + + resultMesh.parts.append(FBXMeshPart()); + FBXMeshPart& resultMeshPart = resultMesh.parts.last(); + + int hullIndexStart = resultMesh.vertices.size(); + for (unsigned int i = 0; i < hull.m_nPoints; i++) { + float x = hull.m_points[i * 3]; + float y = hull.m_points[i * 3 + 1]; + float z = hull.m_points[i * 3 + 2]; + resultMesh.vertices.append(glm::vec3(x, y, z)); + } + + for (unsigned int i = 0; i < hull.m_nTriangles; i++) { + int index0 = hull.m_triangles[i * 3] + hullIndexStart; + int index1 = hull.m_triangles[i * 3 + 1] + hullIndexStart; + int index2 = hull.m_triangles[i * 3 + 2] + hullIndexStart; + resultMeshPart.triangleIndices.append(index0); + resultMeshPart.triangleIndices.append(index1); + resultMeshPart.triangleIndices.append(index2); + } + qDebug() << " hull" << j << " vertices =" << hull.m_nPoints + << " triangles =" << hull.m_nTriangles + << " FBXMeshVertices =" << resultMesh.vertices.size(); + } +} + +float computeDt(uint64_t start) { + return (float)(usecTimestampNow() - start) / 1.0e6f; +} bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, VHACD::IVHACD::Parameters params, FBXGeometry& result, - int startPartIndex, - int endPartIndex, float minimumMeshSize, float maximumMeshSize) { - qDebug() << "num meshes =" << geometry.meshes.size(); + qDebug() << "meshes =" << geometry.meshes.size(); // count the mesh-parts int numParts = 0; foreach (const FBXMesh& mesh, geometry.meshes) { numParts += mesh.parts.size(); } + qDebug() << "total parts =" << numParts; - VHACD::IVHACD * interfaceVHACD = VHACD::CreateVHACD(); - - if (startPartIndex < 0) { - startPartIndex = 0; - } - if (endPartIndex < 0) { - endPartIndex = numParts; - } - qDebug() << "num parts of interest =" << (endPartIndex - startPartIndex); + VHACD::IVHACD * convexifier = VHACD::CreateVHACD(); result.meshExtents.reset(); result.meshes.append(FBXMesh()); FBXMesh &resultMesh = result.meshes.last(); int meshIndex = 0; - int partIndex = 0; int validPartsFound = 0; foreach (const FBXMesh& mesh, geometry.meshes) { + // find duplicate points + int numDupes = 0; + std::vector dupeIndexMap; + dupeIndexMap.reserve(mesh.vertices.size()); + for (int i = 0; i < mesh.vertices.size(); ++i) { + dupeIndexMap.push_back(i); + for (int j = 0; j < i; ++j) { + float distance = glm::distance(mesh.vertices[i], mesh.vertices[j]); + if (distance < 0.0001f) { + dupeIndexMap[i] = j; + ++numDupes; + break; + } + } + } + // each mesh has its own transform to move it to model-space std::vector vertices; foreach (glm::vec3 vertex, mesh.vertices) { @@ -224,87 +320,93 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, << " vertices =" << numVertices; ++meshIndex; + std::vector openParts; + + int partIndex = 0; foreach (const FBXMeshPart &meshPart, mesh.parts) { - if (partIndex < startPartIndex || partIndex >= endPartIndex) { + std::vector triangles; + getTrianglesInMeshPart(meshPart, triangles); + + // only process meshes with triangles + if (triangles.size() <= 0) { + qDebug() << " skip part" << partIndex << "(zero triangles)"; ++partIndex; continue; } - std::vector triangles; - unsigned int triangleCount = getTrianglesInMeshPart(meshPart, triangles); - - // only process meshes with triangles - if (triangles.size() <= 0) { - qDebug() << " part" << partIndex << ":"; - qDebug() << " skip (zero triangles)"; - ++partIndex; - continue; + // collapse dupe indices + for (auto& i : triangles) { + i = dupeIndexMap[i]; } AABox aaBox = getAABoxForMeshPart(mesh, meshPart); const float largestDimension = aaBox.getLargestDimension(); - qDebug() << " part" << partIndex << ": " - << " triangles =" << triangleCount - << " largestDimension =" << largestDimension; - if (largestDimension < minimumMeshSize) { - qDebug() << " skip (too small)"; + qDebug() << " skip part" << partIndex << ": dimension =" << largestDimension << "(too small)"; ++partIndex; continue; } if (maximumMeshSize > 0.0f && largestDimension > maximumMeshSize) { - qDebug() << " skip (too large)"; + qDebug() << " skip part" << partIndex << ": dimension =" << largestDimension << "(too large)"; ++partIndex; continue; } + // figure out if the mesh is a closed manifold or not + bool closed = isClosedManifold(triangles); + if (closed) { + unsigned int triangleCount = triangles.size() / 3; + qDebug() << " process closed part" << partIndex << ": " + << " triangles =" << triangleCount; - // compute approximate convex decomposition - bool success = interfaceVHACD->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); - if (!success){ - qDebug() << " failed to convexify"; - ++partIndex; - continue; - } - - // Number of hulls for this input meshPart - unsigned int nConvexHulls = interfaceVHACD->GetNConvexHulls(); - - // create an output meshPart for each convex hull - for (unsigned int j = 0; j < nConvexHulls; j++) { - VHACD::IVHACD::ConvexHull hull; - interfaceVHACD->GetConvexHull(j, hull); - - resultMesh.parts.append(FBXMeshPart()); - FBXMeshPart &resultMeshPart = resultMesh.parts.last(); - - int hullIndexStart = resultMesh.vertices.size(); - for (unsigned int i = 0; i < hull.m_nPoints; i++) { - float x = hull.m_points[i * 3]; - float y = hull.m_points[i * 3 + 1]; - float z = hull.m_points[i * 3 + 2]; - resultMesh.vertices.append(glm::vec3(x, y, z)); - } - - for (unsigned int i = 0; i < hull.m_nTriangles; i++) { - int index0 = hull.m_triangles[i * 3] + hullIndexStart; - int index1 = hull.m_triangles[i * 3 + 1] + hullIndexStart; - int index2 = hull.m_triangles[i * 3 + 2] + hullIndexStart; - resultMeshPart.triangleIndices.append(index0); - resultMeshPart.triangleIndices.append(index1); - resultMeshPart.triangleIndices.append(index2); + // compute approximate convex decomposition + bool success = convexifier->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); + if (success) { + getConvexResults(convexifier, resultMesh); + } else { + qDebug() << " failed to convexify"; } + } else { + qDebug() << " postpone open part" << partIndex; + openParts.push_back(partIndex); } ++partIndex; ++validPartsFound; } + if (! openParts.empty()) { + // combine open meshes in an attempt to produce a closed mesh + + std::vector triangles; + for (auto index : openParts) { + const FBXMeshPart &meshPart = mesh.parts[index]; + getTrianglesInMeshPart(meshPart, triangles); + } + + // collapse dupe indices + for (auto& i : triangles) { + i = dupeIndexMap[i]; + } + + // this time we don't care if the parts are close or not + unsigned int triangleCount = triangles.size() / 3; + qDebug() << " process remaining open parts =" << openParts.size() << ": " + << " triangles =" << triangleCount; + + // compute approximate convex decomposition + bool success = convexifier->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); + if (success) { + getConvexResults(convexifier, resultMesh); + } else { + qDebug() << " failed to convexify"; + } + } } //release memory - interfaceVHACD->Clean(); - interfaceVHACD->Release(); + convexifier->Clean(); + convexifier->Release(); return validPartsFound > 0; } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index f394301839..62d1779946 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -34,7 +34,6 @@ namespace vhacd { bool computeVHACD(FBXGeometry& geometry, VHACD::IVHACD::Parameters params, FBXGeometry& result, - int startPartIndex, int endPartIndex, float minimumMeshSize, float maximumMeshSize); ~VHACDUtil(); }; diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index 81eb6b399d..b04bf8e43e 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -356,7 +356,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : begin = std::chrono::high_resolution_clock::now(); FBXGeometry result; - bool success = vUtil.computeVHACD(fbx, params, result, startMeshIndex, endMeshIndex, minimumMeshSize, maximumMeshSize); + bool success = vUtil.computeVHACD(fbx, params, result, minimumMeshSize, maximumMeshSize); end = std::chrono::high_resolution_clock::now(); auto computeDuration = std::chrono::duration_cast(end - begin).count(); From 7d7c9914470bf510f5d8fa7a5a659877293a1c78 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 23 May 2016 11:36:40 -0700 Subject: [PATCH 0237/1237] connect progress callback --- tools/vhacd-util/src/VHACDUtil.cpp | 2 +- tools/vhacd-util/src/VHACDUtilApp.cpp | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 43d820b6dd..c1fbc1c2a6 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -424,7 +424,7 @@ void vhacd::ProgressCallback::Update(const double overallProgress, int progress = (int)(overallProgress + 0.5); std::cout << "\b\b\b"; - std::cout << progress << "%"; + std::cout << progress << "%" << std::flush; if (progress >= 100) { std::cout << std::endl; } diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index b04bf8e43e..64d4fb3588 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -329,10 +329,10 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : if (generateHulls) { VHACD::IVHACD::Parameters params; - vhacd::ProgressCallback pCallBack; + vhacd::ProgressCallback progressCallback; //set parameters for V-HACD - params.m_callback = &pCallBack; //progress callback + params.m_callback = &progressCallback; //progress callback params.m_resolution = vHacdResolution; params.m_depth = vHacdDepth; params.m_concavity = vHacdConcavity; @@ -346,7 +346,6 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : params.m_mode = 0; // 0: voxel-based (recommended), 1: tetrahedron-based params.m_maxNumVerticesPerCH = vHacdMaxVerticesPerCH; params.m_minVolumePerCH = 0.0001; // 0.0001 - params.m_callback = 0; // 0 params.m_logger = 0; // 0 params.m_convexhullApproximation = true; // true params.m_oclAcceleration = true; // true From 78357057b6f129a5bca9b3014205c36fc1d2cb51 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 23 May 2016 14:20:25 -0700 Subject: [PATCH 0238/1237] return non-zero on error, add verbose option --- tools/vhacd-util/src/VHACDUtil.cpp | 70 ++++++++++++++++++--------- tools/vhacd-util/src/VHACDUtil.h | 10 +++- tools/vhacd-util/src/VHACDUtilApp.cpp | 56 +++++++++++++++------ tools/vhacd-util/src/VHACDUtilApp.h | 11 ++++- tools/vhacd-util/src/main.cpp | 2 +- 5 files changed, 107 insertions(+), 42 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index c1fbc1c2a6..dcd8bdd5b6 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -28,13 +28,16 @@ void reSortFBXGeometryMeshes(FBXGeometry& geometry) { // Read all the meshes from provided FBX file bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { + if (_verbose) { + qDebug() << "reading FBX file =" << filename << "..."; + } // open the fbx file QFile fbx(filename); if (!fbx.open(QIODevice::ReadOnly)) { + qWarning() << "unable to open FBX file =" << filename; return false; } - qDebug() << "reading FBX file =" << filename << "..."; try { QByteArray fbxContents = fbx.readAll(); FBXGeometry* geom; @@ -43,14 +46,14 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } else if (filename.toLower().endsWith(".fbx")) { geom = readFBX(fbxContents, QVariantHash(), filename); } else { - qDebug() << "unknown file extension"; + qWarning() << "unknown file extension"; return false; } result = *geom; reSortFBXGeometryMeshes(result); } catch (const QString& error) { - qDebug() << "Error reading" << filename << ":" << error; + qWarning() << "error reading" << filename << ":" << error; return false; } @@ -230,10 +233,12 @@ bool isClosedManifold(const std::vector& triangles) { return true; } -void getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) { +void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const { // Number of hulls for this input meshPart unsigned int numHulls = convexifier->GetNConvexHulls(); - qDebug() << " hulls =" << numHulls; + if (_verbose) { + qDebug() << " hulls =" << numHulls; + } // create an output meshPart for each convex hull for (unsigned int j = 0; j < numHulls; j++) { @@ -259,9 +264,11 @@ void getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) { resultMeshPart.triangleIndices.append(index1); resultMeshPart.triangleIndices.append(index2); } - qDebug() << " hull" << j << " vertices =" << hull.m_nPoints - << " triangles =" << hull.m_nTriangles - << " FBXMeshVertices =" << resultMesh.vertices.size(); + if (_verbose) { + qDebug() << " hull" << j << " vertices =" << hull.m_nPoints + << " triangles =" << hull.m_nTriangles + << " FBXMeshVertices =" << resultMesh.vertices.size(); + } } } @@ -273,14 +280,18 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, VHACD::IVHACD::Parameters params, FBXGeometry& result, float minimumMeshSize, float maximumMeshSize) { - qDebug() << "meshes =" << geometry.meshes.size(); + if (_verbose) { + qDebug() << "meshes =" << geometry.meshes.size(); + } // count the mesh-parts int numParts = 0; foreach (const FBXMesh& mesh, geometry.meshes) { numParts += mesh.parts.size(); } - qDebug() << "total parts =" << numParts; + if (_verbose) { + qDebug() << "total parts =" << numParts; + } VHACD::IVHACD * convexifier = VHACD::CreateVHACD(); @@ -315,9 +326,11 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, } auto numVertices = vertices.size(); - qDebug() << "mesh" << meshIndex << ": " - << " parts =" << mesh.parts.size() << " clusters =" << mesh.clusters.size() - << " vertices =" << numVertices; + if (_verbose) { + qDebug() << "mesh" << meshIndex << ": " + << " parts =" << mesh.parts.size() << " clusters =" << mesh.clusters.size() + << " vertices =" << numVertices; + } ++meshIndex; std::vector openParts; @@ -329,7 +342,9 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, // only process meshes with triangles if (triangles.size() <= 0) { - qDebug() << " skip part" << partIndex << "(zero triangles)"; + if (_verbose) { + qDebug() << " skip part" << partIndex << "(zero triangles)"; + } ++partIndex; continue; } @@ -343,13 +358,17 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, const float largestDimension = aaBox.getLargestDimension(); if (largestDimension < minimumMeshSize) { - qDebug() << " skip part" << partIndex << ": dimension =" << largestDimension << "(too small)"; + if (_verbose) { + qDebug() << " skip part" << partIndex << ": dimension =" << largestDimension << "(too small)"; + } ++partIndex; continue; } if (maximumMeshSize > 0.0f && largestDimension > maximumMeshSize) { - qDebug() << " skip part" << partIndex << ": dimension =" << largestDimension << "(too large)"; + if (_verbose) { + qDebug() << " skip part" << partIndex << ": dimension =" << largestDimension << "(too large)"; + } ++partIndex; continue; } @@ -358,18 +377,21 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, bool closed = isClosedManifold(triangles); if (closed) { unsigned int triangleCount = triangles.size() / 3; - qDebug() << " process closed part" << partIndex << ": " - << " triangles =" << triangleCount; + if (_verbose) { + qDebug() << " process closed part" << partIndex << ": " << " triangles =" << triangleCount; + } // compute approximate convex decomposition bool success = convexifier->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); if (success) { getConvexResults(convexifier, resultMesh); - } else { + } else if (_verbose) { qDebug() << " failed to convexify"; } } else { - qDebug() << " postpone open part" << partIndex; + if (_verbose) { + qDebug() << " postpone open part" << partIndex; + } openParts.push_back(partIndex); } ++partIndex; @@ -391,14 +413,16 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, // this time we don't care if the parts are close or not unsigned int triangleCount = triangles.size() / 3; - qDebug() << " process remaining open parts =" << openParts.size() << ": " - << " triangles =" << triangleCount; + if (_verbose) { + qDebug() << " process remaining open parts =" << openParts.size() << ": " + << " triangles =" << triangleCount; + } // compute approximate convex decomposition bool success = convexifier->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); if (success) { getConvexResults(convexifier, resultMesh); - } else { + } else if (_verbose) { qDebug() << " failed to convexify"; } } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 62d1779946..04e8dab92d 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -25,6 +25,8 @@ namespace vhacd { class VHACDUtil { public: + void setVerbose(bool verbose) { _verbose = verbose; } + bool loadFBX(const QString filename, FBXGeometry& result); void fattenMeshes(const FBXMesh& mesh, FBXMesh& result, @@ -35,7 +37,13 @@ namespace vhacd { VHACD::IVHACD::Parameters params, FBXGeometry& result, float minimumMeshSize, float maximumMeshSize); + + void getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const; + ~VHACDUtil(); + + private: + bool _verbose { false }; }; class ProgressCallback : public VHACD::IVHACD::IUserCallback { @@ -44,7 +52,7 @@ namespace vhacd { ~ProgressCallback(); // Couldn't follow coding guideline here due to virtual function declared in IUserCallback - void Update(const double overallProgress, const double stageProgress, const double operationProgress, + void Update(const double overallProgress, const double stageProgress, const double operationProgress, const char * const stage, const char * const operation); }; } diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index 64d4fb3588..97b483290a 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -18,6 +18,9 @@ using namespace std; using namespace VHACD; +const int VHACD_RETURN_CODE_FAILURE_TO_READ = 1; +const int VHACD_RETURN_CODE_FAILURE_TO_WRITE = 2; +const int VHACD_RETURN_CODE_FAILURE_TO_CONVEXIFY = 3; QString formatFloat(double n) { @@ -33,14 +36,15 @@ QString formatFloat(double n) { } -bool writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1) { +bool VHACDUtilApp::writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart) { QFile file(outFileName); if (!file.open(QIODevice::WriteOnly)) { - qDebug() << "Unable to write to " << outFileName; + qWarning() << "unable to write to" << outFileName; + _returnCode = VHACD_RETURN_CODE_FAILURE_TO_WRITE; return false; } - QTextStream out(&file); + QTextStream out(&file); if (outputCentimeters) { out << "# This file uses centimeters as units\n\n"; } @@ -105,6 +109,9 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : const QCommandLineOption helpOption = parser.addHelpOption(); + const QCommandLineOption verboseOutput("v", "verbose output"); + parser.addOption(verboseOutput); + const QCommandLineOption splitOption("split", "split input-file into one mesh per output-file"); parser.addOption(splitOption); @@ -195,8 +202,10 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : Q_UNREACHABLE(); } - bool outputCentimeters = parser.isSet(outputCentimetersOption); + bool verbose = parser.isSet(verboseOutput); + vUtil.setVerbose(verbose); + bool outputCentimeters = parser.isSet(outputCentimetersOption); bool fattenFaces = parser.isSet(fattenFacesOption); bool generateHulls = parser.isSet(generateHullsOption); bool splitModel = parser.isSet(splitOption); @@ -305,13 +314,15 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : FBXGeometry fbx; auto begin = std::chrono::high_resolution_clock::now(); if (!vUtil.loadFBX(inputFilename, fbx)){ - qDebug() << "error reading input file" << inputFilename; + _returnCode = VHACD_RETURN_CODE_FAILURE_TO_READ; return; } auto end = std::chrono::high_resolution_clock::now(); auto loadDuration = std::chrono::duration_cast(end - begin).count(); - qDebug() << "load time =" << (double)loadDuration / 1000000000.00 << "seconds"; + if (verbose) { + qDebug() << "load time =" << (double)loadDuration / 1000000000.00 << "seconds"; + } if (splitModel) { QVector infileExtensions = {"fbx", "obj"}; @@ -332,7 +343,11 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : vhacd::ProgressCallback progressCallback; //set parameters for V-HACD - params.m_callback = &progressCallback; //progress callback + if (verbose) { + params.m_callback = &progressCallback; //progress callback + } else { + params.m_callback = nullptr; + } params.m_resolution = vHacdResolution; params.m_depth = vHacdDepth; params.m_concavity = vHacdConcavity; @@ -346,12 +361,14 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : params.m_mode = 0; // 0: voxel-based (recommended), 1: tetrahedron-based params.m_maxNumVerticesPerCH = vHacdMaxVerticesPerCH; params.m_minVolumePerCH = 0.0001; // 0.0001 - params.m_logger = 0; // 0 + params.m_logger = nullptr; params.m_convexhullApproximation = true; // true params.m_oclAcceleration = true; // true //perform vhacd computation - qDebug() << "running V-HACD algorithm ..."; + if (verbose) { + qDebug() << "running V-HACD algorithm ..."; + } begin = std::chrono::high_resolution_clock::now(); FBXGeometry result; @@ -359,10 +376,15 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : end = std::chrono::high_resolution_clock::now(); auto computeDuration = std::chrono::duration_cast(end - begin).count(); - qDebug() << "run time =" << (double)computeDuration / 1000000000.00 << " seconds"; + if (verbose) { + qDebug() << "run time =" << (double)computeDuration / 1000000000.00 << " seconds"; + } if (!success) { - qDebug() << "failed to convexify model"; + if (verbose) { + qDebug() << "failed to convexify model"; + } + _returnCode = VHACD_RETURN_CODE_FAILURE_TO_CONVEXIFY; return; } @@ -377,11 +399,13 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } } - int totalHulls = result.meshes[0].parts.size(); - qDebug() << "output file =" << outputFilename; - qDebug() << "vertices =" << totalVertices; - qDebug() << "triangles =" << totalTriangles; - qDebug() << "hulls =" << totalHulls; + if (verbose) { + int totalHulls = result.meshes[0].parts.size(); + qDebug() << "output file =" << outputFilename; + qDebug() << "vertices =" << totalVertices; + qDebug() << "triangles =" << totalTriangles; + qDebug() << "hulls =" << totalHulls; + } writeOBJ(outputFilename, result, outputCentimeters); } diff --git a/tools/vhacd-util/src/VHACDUtilApp.h b/tools/vhacd-util/src/VHACDUtilApp.h index 016b7b7b2f..9405126c65 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.h +++ b/tools/vhacd-util/src/VHACDUtilApp.h @@ -15,12 +15,21 @@ #include +#include + class VHACDUtilApp : public QCoreApplication { Q_OBJECT - public: +public: VHACDUtilApp(int argc, char* argv[]); ~VHACDUtilApp(); + + bool writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1); + + int getReturnCode() const { return _returnCode; } + +private: + int _returnCode { 0 }; }; diff --git a/tools/vhacd-util/src/main.cpp b/tools/vhacd-util/src/main.cpp index 0e8d72abd3..42c9db9513 100644 --- a/tools/vhacd-util/src/main.cpp +++ b/tools/vhacd-util/src/main.cpp @@ -23,5 +23,5 @@ using namespace VHACD; int main(int argc, char * argv[]) { VHACDUtilApp app(argc, argv); - return 0; + return app.getReturnCode(); } From 7ee4dea4cacee95d8929e1e2550879bc4b43ce58 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 23 May 2016 15:30:19 -0700 Subject: [PATCH 0239/1237] remove magic number --- tools/vhacd-util/src/VHACDUtil.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index dcd8bdd5b6..7ae370fc87 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -310,8 +310,9 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, for (int i = 0; i < mesh.vertices.size(); ++i) { dupeIndexMap.push_back(i); for (int j = 0; j < i; ++j) { - float distance = glm::distance(mesh.vertices[i], mesh.vertices[j]); - if (distance < 0.0001f) { + float distance = glm::distance2(mesh.vertices[i], mesh.vertices[j]); + const float MAX_DUPE_DISTANCE_SQUARED = 0.000001f; + if (distance < MAX_DUPE_DISTANCE_SQUARED) { dupeIndexMap[i] = j; ++numDupes; break; From 21ef30d410116753e44207493091056c5440f1ac Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 23 May 2016 16:50:02 -0700 Subject: [PATCH 0240/1237] fix warning on windows build --- tools/vhacd-util/src/VHACDUtil.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 7ae370fc87..21d42a13d5 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -377,7 +377,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, // figure out if the mesh is a closed manifold or not bool closed = isClosedManifold(triangles); if (closed) { - unsigned int triangleCount = triangles.size() / 3; + unsigned int triangleCount = (unsigned int)(triangles.size()) / 3; if (_verbose) { qDebug() << " process closed part" << partIndex << ": " << " triangles =" << triangleCount; } @@ -413,7 +413,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, } // this time we don't care if the parts are close or not - unsigned int triangleCount = triangles.size() / 3; + unsigned int triangleCount = (unsigned int)(triangles.size()) / 3; if (_verbose) { qDebug() << " process remaining open parts =" << openParts.size() << ": " << " triangles =" << triangleCount; From e8e059c63786cc0453eef222d1f0bb0f3303bea7 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 24 May 2016 12:57:32 -0700 Subject: [PATCH 0241/1237] better variable names and fewer magic numbers --- tools/vhacd-util/src/VHACDUtil.cpp | 154 ++++++++++++++------------ tools/vhacd-util/src/VHACDUtilApp.cpp | 5 +- 2 files changed, 84 insertions(+), 75 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 21d42a13d5..a59250f794 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -9,11 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include -#include #include "VHACDUtil.h" -const float COLLISION_TETRAHEDRON_SCALE = 0.25f; +#include +#include + +#include // FBXReader jumbles the order of the meshes by reading them back out of a hashtable. This will put @@ -61,46 +62,46 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } -void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangles) { +void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangleIndices) { // append all the triangles (and converted quads) from this mesh-part to triangles std::vector meshPartTriangles = meshPart.triangleIndices.toStdVector(); - triangles.insert(triangles.end(), meshPartTriangles.begin(), meshPartTriangles.end()); + triangleIndices.insert(triangleIndices.end(), meshPartTriangles.begin(), meshPartTriangles.end()); // convert quads to triangles - unsigned int quadCount = meshPart.quadIndices.size() / 4; - for (unsigned int i = 0; i < quadCount; i++) { - unsigned int p0Index = meshPart.quadIndices[i * 4]; - unsigned int p1Index = meshPart.quadIndices[i * 4 + 1]; - unsigned int p2Index = meshPart.quadIndices[i * 4 + 2]; - unsigned int p3Index = meshPart.quadIndices[i * 4 + 3]; + const uint32_t QUAD_STRIDE = 4; + uint32_t quadCount = meshPart.quadIndices.size() / QUAD_STRIDE; + for (uint32_t i = 0; i < quadCount; i++) { + uint32_t p0Index = meshPart.quadIndices[i * QUAD_STRIDE]; + uint32_t p1Index = meshPart.quadIndices[i * QUAD_STRIDE + 1]; + uint32_t p2Index = meshPart.quadIndices[i * QUAD_STRIDE + 2]; + uint32_t p3Index = meshPart.quadIndices[i * QUAD_STRIDE + 3]; // split each quad into two triangles - triangles.push_back(p0Index); - triangles.push_back(p1Index); - triangles.push_back(p2Index); - triangles.push_back(p0Index); - triangles.push_back(p2Index); - triangles.push_back(p3Index); + triangleIndices.push_back(p0Index); + triangleIndices.push_back(p1Index); + triangleIndices.push_back(p2Index); + triangleIndices.push_back(p0Index); + triangleIndices.push_back(p2Index); + triangleIndices.push_back(p3Index); } } void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, - unsigned int& meshPartCount, - unsigned int startMeshIndex, unsigned int endMeshIndex) const { + uint32_t& meshPartCount, + uint32_t startMeshIndex, uint32_t endMeshIndex) const { // this is used to make meshes generated from a highfield collidable. each triangle // is converted into a tetrahedron and made into its own mesh-part. - std::vector triangles; + std::vector triangleIndices; foreach (const FBXMeshPart &meshPart, mesh.parts) { if (meshPartCount < startMeshIndex || meshPartCount >= endMeshIndex) { meshPartCount++; continue; } - getTrianglesInMeshPart(meshPart, triangles); + getTrianglesInMeshPart(meshPart, triangleIndices); } - auto triangleCount = triangles.size() / 3; - if (triangleCount == 0) { + if (triangleIndices.size() == 0) { return; } @@ -115,10 +116,12 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, // turn each triangle into a tetrahedron - for (unsigned int i = 0; i < triangleCount; i++) { - int index0 = triangles[i * 3] + indexStartOffset; - int index1 = triangles[i * 3 + 1] + indexStartOffset; - int index2 = triangles[i * 3 + 2] + indexStartOffset; + const uint32_t TRIANGLE_STRIDE = 3; + const float COLLISION_TETRAHEDRON_SCALE = 0.25f; + for (uint32_t i = 0; i < triangleIndices.size(); i += TRIANGLE_STRIDE) { + int index0 = triangleIndices[i] + indexStartOffset; + int index1 = triangleIndices[i + 1] + indexStartOffset; + int index2 = triangleIndices[i + 2] + indexStartOffset; // TODO: skip triangles with a normal that points more negative-y than positive-y @@ -155,23 +158,21 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, } } - - AABox getAABoxForMeshPart(const FBXMesh& mesh, const FBXMeshPart &meshPart) { AABox aaBox; - unsigned int triangleCount = meshPart.triangleIndices.size() / 3; - for (unsigned int i = 0; i < triangleCount; ++i) { - aaBox += mesh.vertices[meshPart.triangleIndices[i * 3]]; - aaBox += mesh.vertices[meshPart.triangleIndices[i * 3 + 1]]; - aaBox += mesh.vertices[meshPart.triangleIndices[i * 3 + 2]]; + const int TRIANGLE_STRIDE = 3; + for (int i = 0; i < meshPart.triangleIndices.size(); i += TRIANGLE_STRIDE) { + aaBox += mesh.vertices[meshPart.triangleIndices[i]]; + aaBox += mesh.vertices[meshPart.triangleIndices[i + 1]]; + aaBox += mesh.vertices[meshPart.triangleIndices[i + 2]]; } - unsigned int quadCount = meshPart.quadIndices.size() / 4; - for (unsigned int i = 0; i < quadCount; ++i) { - aaBox += mesh.vertices[meshPart.quadIndices[i * 4]]; - aaBox += mesh.vertices[meshPart.quadIndices[i * 4 + 1]]; - aaBox += mesh.vertices[meshPart.quadIndices[i * 4 + 2]]; - aaBox += mesh.vertices[meshPart.quadIndices[i * 4 + 3]]; + const int QUAD_STRIDE = 4; + for (int i = 0; i < meshPart.quadIndices.size(); i += QUAD_STRIDE) { + aaBox += mesh.vertices[meshPart.quadIndices[i]]; + aaBox += mesh.vertices[meshPart.quadIndices[i + 1]]; + aaBox += mesh.vertices[meshPart.quadIndices[i + 2]]; + aaBox += mesh.vertices[meshPart.quadIndices[i + 3]]; } return aaBox; @@ -187,9 +188,7 @@ struct TriangleEdge { } void sortIndices() { if (indexB < indexA) { - int t = indexA; - indexA = indexB; - indexB = t; + std::swap(indexA, indexB); } } }; @@ -204,16 +203,18 @@ namespace std { } // returns false if any edge has only one adjacent triangle -bool isClosedManifold(const std::vector& triangles) { +bool isClosedManifold(const std::vector& triangleIndices) { using EdgeList = std::unordered_map; EdgeList edges; // count the triangles for each edge - for (size_t i = 0; i < triangles.size(); i += 3) { + const uint32_t TRIANGLE_STRIDE = 3; + for (uint32_t i = 0; i < triangleIndices.size(); i += TRIANGLE_STRIDE) { TriangleEdge edge; + // the triangles indices are stored in sequential order for (int j = 0; j < 3; ++j) { - edge.indexA = triangles[(int)i + j]; - edge.indexB = triangles[i + ((j + 1) % 3)]; + edge.indexA = triangleIndices[(int)i + j]; + edge.indexB = triangleIndices[i + ((j + 1) % 3)]; edge.sortIndices(); EdgeList::iterator edgeEntry = edges.find(edge); @@ -235,13 +236,14 @@ bool isClosedManifold(const std::vector& triangles) { void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const { // Number of hulls for this input meshPart - unsigned int numHulls = convexifier->GetNConvexHulls(); + uint32_t numHulls = convexifier->GetNConvexHulls(); if (_verbose) { qDebug() << " hulls =" << numHulls; } // create an output meshPart for each convex hull - for (unsigned int j = 0; j < numHulls; j++) { + const uint32_t TRIANGLE_STRIDE = 3; + for (uint32_t j = 0; j < numHulls; j++) { VHACD::IVHACD::ConvexHull hull; convexifier->GetConvexHull(j, hull); @@ -249,17 +251,17 @@ void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& res FBXMeshPart& resultMeshPart = resultMesh.parts.last(); int hullIndexStart = resultMesh.vertices.size(); - for (unsigned int i = 0; i < hull.m_nPoints; i++) { - float x = hull.m_points[i * 3]; - float y = hull.m_points[i * 3 + 1]; - float z = hull.m_points[i * 3 + 2]; + for (uint32_t i = 0; i < hull.m_nPoints; i++) { + float x = hull.m_points[i * TRIANGLE_STRIDE]; + float y = hull.m_points[i * TRIANGLE_STRIDE + 1]; + float z = hull.m_points[i * TRIANGLE_STRIDE + 2]; resultMesh.vertices.append(glm::vec3(x, y, z)); } - for (unsigned int i = 0; i < hull.m_nTriangles; i++) { - int index0 = hull.m_triangles[i * 3] + hullIndexStart; - int index1 = hull.m_triangles[i * 3 + 1] + hullIndexStart; - int index2 = hull.m_triangles[i * 3 + 2] + hullIndexStart; + for (uint32_t i = 0; i < hull.m_nTriangles; i++) { + int index0 = hull.m_triangles[i * TRIANGLE_STRIDE] + hullIndexStart; + int index1 = hull.m_triangles[i * TRIANGLE_STRIDE + 1] + hullIndexStart; + int index2 = hull.m_triangles[i * TRIANGLE_STRIDE + 2] + hullIndexStart; resultMeshPart.triangleIndices.append(index0); resultMeshPart.triangleIndices.append(index1); resultMeshPart.triangleIndices.append(index2); @@ -273,7 +275,7 @@ void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& res } float computeDt(uint64_t start) { - return (float)(usecTimestampNow() - start) / 1.0e6f; + return (float)(usecTimestampNow() - start) / (float)USECS_PER_SECOND; } bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, @@ -299,6 +301,9 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, result.meshes.append(FBXMesh()); FBXMesh &resultMesh = result.meshes.last(); + const uint32_t POINT_STRIDE = 3; + const uint32_t TRIANGLE_STRIDE = 3; + int meshIndex = 0; int validPartsFound = 0; foreach (const FBXMesh& mesh, geometry.meshes) { @@ -325,7 +330,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, foreach (glm::vec3 vertex, mesh.vertices) { vertices.push_back(glm::vec3(mesh.modelTransform * glm::vec4(vertex, 1.0f))); } - auto numVertices = vertices.size(); + uint32_t numVertices = vertices.size(); if (_verbose) { qDebug() << "mesh" << meshIndex << ": " @@ -337,12 +342,13 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, std::vector openParts; int partIndex = 0; + std::vector triangleIndices; foreach (const FBXMeshPart &meshPart, mesh.parts) { - std::vector triangles; - getTrianglesInMeshPart(meshPart, triangles); + triangleIndices.clear(); + getTrianglesInMeshPart(meshPart, triangleIndices); // only process meshes with triangles - if (triangles.size() <= 0) { + if (triangleIndices.size() <= 0) { if (_verbose) { qDebug() << " skip part" << partIndex << "(zero triangles)"; } @@ -351,8 +357,8 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, } // collapse dupe indices - for (auto& i : triangles) { - i = dupeIndexMap[i]; + for (auto& index : triangleIndices) { + index = dupeIndexMap[index]; } AABox aaBox = getAABoxForMeshPart(mesh, meshPart); @@ -375,15 +381,16 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, } // figure out if the mesh is a closed manifold or not - bool closed = isClosedManifold(triangles); + bool closed = isClosedManifold(triangleIndices); if (closed) { - unsigned int triangleCount = (unsigned int)(triangles.size()) / 3; + uint32_t triangleCount = (uint32_t)(triangleIndices.size()) / TRIANGLE_STRIDE; if (_verbose) { qDebug() << " process closed part" << partIndex << ": " << " triangles =" << triangleCount; } // compute approximate convex decomposition - bool success = convexifier->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); + bool success = convexifier->Compute(&vertices[0].x, POINT_STRIDE, numVertices, + &triangleIndices[0], TRIANGLE_STRIDE, triangleCount, params); if (success) { getConvexResults(convexifier, resultMesh); } else if (_verbose) { @@ -401,26 +408,27 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, if (! openParts.empty()) { // combine open meshes in an attempt to produce a closed mesh - std::vector triangles; + triangleIndices.clear(); for (auto index : openParts) { const FBXMeshPart &meshPart = mesh.parts[index]; - getTrianglesInMeshPart(meshPart, triangles); + getTrianglesInMeshPart(meshPart, triangleIndices); } // collapse dupe indices - for (auto& i : triangles) { - i = dupeIndexMap[i]; + for (auto& index : triangleIndices) { + index = dupeIndexMap[index]; } // this time we don't care if the parts are close or not - unsigned int triangleCount = (unsigned int)(triangles.size()) / 3; + uint32_t triangleCount = (uint32_t)(triangleIndices.size()) / TRIANGLE_STRIDE; if (_verbose) { qDebug() << " process remaining open parts =" << openParts.size() << ": " << " triangles =" << triangleCount; } // compute approximate convex decomposition - bool success = convexifier->Compute(&vertices[0].x, 3, (uint)numVertices, &triangles[0], 3, triangleCount, params); + bool success = convexifier->Compute(&vertices[0].x, POINT_STRIDE, numVertices, + &triangleIndices[0], TRIANGLE_STRIDE, triangleCount, params); if (success) { getConvexResults(convexifier, resultMesh); } else if (_verbose) { diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index 97b483290a..f67a02aa21 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -318,10 +318,11 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : return; } auto end = std::chrono::high_resolution_clock::now(); - auto loadDuration = std::chrono::duration_cast(end - begin).count(); if (verbose) { - qDebug() << "load time =" << (double)loadDuration / 1000000000.00 << "seconds"; + auto loadDuration = std::chrono::duration_cast(end - begin).count(); + const double NANOSECS_PER_SECOND = 1.0e9; + qDebug() << "load time =" << (double)loadDuration / NANOSECS_PER_SECOND << "seconds"; } if (splitModel) { From 2efec2a878b0a7f71f508754bc3c7434d4e0adca Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 24 May 2016 15:01:26 -0700 Subject: [PATCH 0242/1237] fix warning on windows about 64 to 32 bit cast --- tools/vhacd-util/src/VHACDUtil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index a59250f794..b6d77d62d7 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -330,7 +330,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, foreach (glm::vec3 vertex, mesh.vertices) { vertices.push_back(glm::vec3(mesh.modelTransform * glm::vec4(vertex, 1.0f))); } - uint32_t numVertices = vertices.size(); + uint32_t numVertices = (uint32_t)vertices.size(); if (_verbose) { qDebug() << "mesh" << meshIndex << ": " From 402b7f22822b80d2c72f25e7bdb7d708d6ea28d5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 25 May 2016 20:53:43 -0700 Subject: [PATCH 0243/1237] address review feedback --- tools/vhacd-util/src/VHACDUtil.cpp | 92 +++++++++++++++------------ tools/vhacd-util/src/VHACDUtilApp.cpp | 4 -- tools/vhacd-util/src/VHACDUtilApp.h | 4 ++ 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index b6d77d62d7..7acb26d81e 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -47,7 +47,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } else if (filename.toLower().endsWith(".fbx")) { geom = readFBX(fbxContents, QVariantHash(), filename); } else { - qWarning() << "unknown file extension"; + qWarning() << "file has unknown extension" << filename; return false; } result = *geom; @@ -63,18 +63,20 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangleIndices) { - // append all the triangles (and converted quads) from this mesh-part to triangles - std::vector meshPartTriangles = meshPart.triangleIndices.toStdVector(); - triangleIndices.insert(triangleIndices.end(), meshPartTriangles.begin(), meshPartTriangles.end()); + // append triangle indices + triangleIndices.reserve(triangleIndices.size() + (size_t)meshPart.triangleIndices.size()); + for (auto index : meshPart.triangleIndices) { + triangleIndices.push_back(index); + } // convert quads to triangles const uint32_t QUAD_STRIDE = 4; - uint32_t quadCount = meshPart.quadIndices.size() / QUAD_STRIDE; - for (uint32_t i = 0; i < quadCount; i++) { - uint32_t p0Index = meshPart.quadIndices[i * QUAD_STRIDE]; - uint32_t p1Index = meshPart.quadIndices[i * QUAD_STRIDE + 1]; - uint32_t p2Index = meshPart.quadIndices[i * QUAD_STRIDE + 2]; - uint32_t p3Index = meshPart.quadIndices[i * QUAD_STRIDE + 3]; + uint32_t numIndices = (uint32_t)meshPart.quadIndices.size(); + for (uint32_t i = 0; i < numIndices; i += QUAD_STRIDE) { + uint32_t p0Index = meshPart.quadIndices[i]; + uint32_t p1Index = meshPart.quadIndices[i + 1]; + uint32_t p2Index = meshPart.quadIndices[i + 2]; + uint32_t p3Index = meshPart.quadIndices[i + 3]; // split each quad into two triangles triangleIndices.push_back(p0Index); triangleIndices.push_back(p1Index); @@ -105,8 +107,6 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, return; } - int indexStartOffset = result.vertices.size(); - // new mesh gets the transformed points from the original for (int i = 0; i < mesh.vertices.size(); i++) { // apply the source mesh's transform to the points @@ -118,6 +118,7 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, const uint32_t TRIANGLE_STRIDE = 3; const float COLLISION_TETRAHEDRON_SCALE = 0.25f; + int indexStartOffset = result.vertices.size(); for (uint32_t i = 0; i < triangleIndices.size(); i += TRIANGLE_STRIDE) { int index0 = triangleIndices[i] + indexStartOffset; int index1 = triangleIndices[i + 1] + indexStartOffset; @@ -178,26 +179,39 @@ AABox getAABoxForMeshPart(const FBXMesh& mesh, const FBXMeshPart &meshPart) { return aaBox; } -struct TriangleEdge { - int indexA { -1 }; - int indexB { -1 }; +class TriangleEdge { +public: TriangleEdge() {} - TriangleEdge(int A, int B) : indexA(A), indexB(B) {} - bool operator==(const TriangleEdge& other) const { - return indexA == other.indexA && indexB == other.indexB; + TriangleEdge(uint32_t A, uint32_t B) { + setIndices(A, B); } - void sortIndices() { - if (indexB < indexA) { - std::swap(indexA, indexB); + void setIndices(uint32_t A, uint32_t B) { + if (A < B) { + _indexA = A; + _indexB = B; + } else { + _indexA = B; + _indexB = A; } } + bool operator==(const TriangleEdge& other) const { + return _indexA == other._indexA && _indexB == other._indexB; + } + + uint32_t getIndexA() const { return _indexA; } + uint32_t getIndexB() const { return _indexB; } +private: + uint32_t _indexA { (uint32_t)(-1) }; + uint32_t _indexB { (uint32_t)(-1) }; }; namespace std { template <> struct hash { std::size_t operator()(const TriangleEdge& edge) const { - return (hash()(edge.indexA) ^ (hash()(edge.indexB) << 1)); + // use Cantor's pairing function to generate a hash of ZxZ --> Z + uint32_t ab = edge.getIndexA() + edge.getIndexB(); + return hash()((ab * (ab + 1)) / 2 + edge.getIndexB()); } }; } @@ -212,14 +226,12 @@ bool isClosedManifold(const std::vector& triangleIndices) { for (uint32_t i = 0; i < triangleIndices.size(); i += TRIANGLE_STRIDE) { TriangleEdge edge; // the triangles indices are stored in sequential order - for (int j = 0; j < 3; ++j) { - edge.indexA = triangleIndices[(int)i + j]; - edge.indexB = triangleIndices[i + ((j + 1) % 3)]; - edge.sortIndices(); + for (uint32_t j = 0; j < 3; ++j) { + edge.setIndices(triangleIndices[i + j], triangleIndices[i + ((j + 1) % 3)]); EdgeList::iterator edgeEntry = edges.find(edge); if (edgeEntry == edges.end()) { - edges.insert(std::pair(edge, 1)); + edges.insert(std::pair(edge, 1)); } else { edgeEntry->second += 1; } @@ -243,6 +255,7 @@ void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& res // create an output meshPart for each convex hull const uint32_t TRIANGLE_STRIDE = 3; + const uint32_t POINT_STRIDE = 3; for (uint32_t j = 0; j < numHulls; j++) { VHACD::IVHACD::ConvexHull hull; convexifier->GetConvexHull(j, hull); @@ -251,20 +264,21 @@ void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& res FBXMeshPart& resultMeshPart = resultMesh.parts.last(); int hullIndexStart = resultMesh.vertices.size(); - for (uint32_t i = 0; i < hull.m_nPoints; i++) { - float x = hull.m_points[i * TRIANGLE_STRIDE]; - float y = hull.m_points[i * TRIANGLE_STRIDE + 1]; - float z = hull.m_points[i * TRIANGLE_STRIDE + 2]; + resultMesh.vertices.reserve(hullIndexStart + hull.m_nPoints); + uint32_t numIndices = hull.m_nPoints * POINT_STRIDE; + for (uint32_t i = 0; i < numIndices; i += POINT_STRIDE) { + float x = hull.m_points[i]; + float y = hull.m_points[i + 1]; + float z = hull.m_points[i + 2]; resultMesh.vertices.append(glm::vec3(x, y, z)); } - for (uint32_t i = 0; i < hull.m_nTriangles; i++) { - int index0 = hull.m_triangles[i * TRIANGLE_STRIDE] + hullIndexStart; - int index1 = hull.m_triangles[i * TRIANGLE_STRIDE + 1] + hullIndexStart; - int index2 = hull.m_triangles[i * TRIANGLE_STRIDE + 2] + hullIndexStart; - resultMeshPart.triangleIndices.append(index0); - resultMeshPart.triangleIndices.append(index1); - resultMeshPart.triangleIndices.append(index2); + numIndices = hull.m_nTriangles * TRIANGLE_STRIDE; + resultMeshPart.triangleIndices.reserve(resultMeshPart.triangleIndices.size() + numIndices); + for (uint32_t i = 0; i < numIndices; i += TRIANGLE_STRIDE) { + resultMeshPart.triangleIndices.append(hull.m_triangles[i] + hullIndexStart); + resultMeshPart.triangleIndices.append(hull.m_triangles[i + 1] + hullIndexStart); + resultMeshPart.triangleIndices.append(hull.m_triangles[i + 2] + hullIndexStart); } if (_verbose) { qDebug() << " hull" << j << " vertices =" << hull.m_nPoints @@ -419,7 +433,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, index = dupeIndexMap[index]; } - // this time we don't care if the parts are close or not + // this time we don't care if the parts are closed or not uint32_t triangleCount = (uint32_t)(triangleIndices.size()) / TRIANGLE_STRIDE; if (_verbose) { qDebug() << " process remaining open parts =" << openParts.size() << ": " diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index f67a02aa21..c7257bd5c2 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -18,10 +18,6 @@ using namespace std; using namespace VHACD; -const int VHACD_RETURN_CODE_FAILURE_TO_READ = 1; -const int VHACD_RETURN_CODE_FAILURE_TO_WRITE = 2; -const int VHACD_RETURN_CODE_FAILURE_TO_CONVEXIFY = 3; - QString formatFloat(double n) { // limit precision to 6, but don't output trailing zeros. diff --git a/tools/vhacd-util/src/VHACDUtilApp.h b/tools/vhacd-util/src/VHACDUtilApp.h index 9405126c65..ebb8164634 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.h +++ b/tools/vhacd-util/src/VHACDUtilApp.h @@ -17,6 +17,10 @@ #include +const int VHACD_RETURN_CODE_FAILURE_TO_READ = 1; +const int VHACD_RETURN_CODE_FAILURE_TO_WRITE = 2; +const int VHACD_RETURN_CODE_FAILURE_TO_CONVEXIFY = 3; + class VHACDUtilApp : public QCoreApplication { Q_OBJECT From 991da9e248338898b7ac582a5acaa81fd9d9e83e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 26 May 2016 09:51:55 -0700 Subject: [PATCH 0244/1237] fix cut-paste error in settings. --- interface/src/avatar/MyAvatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 358b3d72ba..babbcfadbe 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -859,7 +859,7 @@ void MyAvatar::loadData() { setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); - setSnapTurn(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool()); + setClearOverlayWhenDriving(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool()); settings.endGroup(); From 2c02c963d4f06b009fb54a456d6fff67e9803a69 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 26 May 2016 12:22:39 -0700 Subject: [PATCH 0245/1237] Moving shape definition to a shared location --- libraries/render-utils/src/GeometryCache.cpp | 240 ++----------------- libraries/render-utils/src/GeometryCache.h | 9 +- libraries/shared/src/shared/Shapes.cpp | 186 ++++++++++++++ libraries/shared/src/shared/Shapes.h | 79 ++++++ 4 files changed, 284 insertions(+), 230 deletions(-) create mode 100644 libraries/shared/src/shared/Shapes.cpp create mode 100644 libraries/shared/src/shared/Shapes.h diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 02aca4216e..1154f27ee0 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "TextureCache.h" #include "RenderUtilsLogging.h" @@ -54,7 +55,7 @@ static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3); static const gpu::Type SHAPE_INDEX_TYPE = gpu::UINT32; static const uint SHAPE_INDEX_SIZE = sizeof(gpu::uint32); -void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices) { +void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const geometry::VertexVector& vertices) { vertexBuffer->append(vertices); _positionView = gpu::BufferView(vertexBuffer, 0, @@ -63,7 +64,7 @@ void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, c vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, NORMAL_ELEMENT); } -void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const IndexVector& indices, const IndexVector& wireIndices) { +void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const geometry::IndexVector& indices, const geometry::IndexVector& wireIndices) { _indices = indexBuffer; if (!indices.empty()) { _indexOffset = indexBuffer->getSize() / SHAPE_INDEX_SIZE; @@ -126,90 +127,19 @@ size_t GeometryCache::getCubeTriangleCount() { return getShapeTriangleCount(Cube); } -using Index = uint32_t; using IndexPair = uint64_t; using IndexPairs = std::unordered_set; -template -using Face = std::array; - -template -using FaceVector = std::vector>; - -template -struct Solid { - VertexVector vertices; - FaceVector faces; - - Solid& fitDimension(float newMaxDimension) { - float maxDimension = 0; - for (const auto& vertex : vertices) { - maxDimension = std::max(maxDimension, std::max(std::max(vertex.x, vertex.y), vertex.z)); - } - float multiplier = newMaxDimension / maxDimension; - for (auto& vertex : vertices) { - vertex *= multiplier; - } - return *this; - } - - vec3 getFaceNormal(size_t faceIndex) const { - vec3 result; - const auto& face = faces[faceIndex]; - for (size_t i = 0; i < N; ++i) { - result += vertices[face[i]]; - } - result /= N; - return glm::normalize(result); - } -}; - -template -static size_t triangulatedFaceTriangleCount() { - return N - 2; -} - -template -static size_t triangulatedFaceIndexCount() { - return triangulatedFaceTriangleCount() * VERTICES_PER_TRIANGLE; -} - -static IndexPair indexToken(Index a, Index b) { +static IndexPair indexToken(geometry::Index a, geometry::Index b) { if (a > b) { std::swap(a, b); } return (((IndexPair)a) << 32) | ((IndexPair)b); } -static Solid<3> tesselate(Solid<3> solid, int count) { - float length = glm::length(solid.vertices[0]); - for (int i = 0; i < count; ++i) { - Solid<3> result { solid.vertices, {} }; - result.vertices.reserve(solid.vertices.size() + solid.faces.size() * 3); - for (size_t f = 0; f < solid.faces.size(); ++f) { - Index baseVertex = (Index)result.vertices.size(); - const Face<3>& oldFace = solid.faces[f]; - const vec3& a = solid.vertices[oldFace[0]]; - const vec3& b = solid.vertices[oldFace[1]]; - const vec3& c = solid.vertices[oldFace[2]]; - vec3 ab = glm::normalize(a + b) * length; - vec3 bc = glm::normalize(b + c) * length; - vec3 ca = glm::normalize(c + a) * length; - result.vertices.push_back(ab); - result.vertices.push_back(bc); - result.vertices.push_back(ca); - result.faces.push_back(Face<3>{ { oldFace[0], baseVertex, baseVertex + 2 } }); - result.faces.push_back(Face<3>{ { baseVertex, oldFace[1], baseVertex + 1 } }); - result.faces.push_back(Face<3>{ { baseVertex + 1, oldFace[2], baseVertex + 2 } }); - result.faces.push_back(Face<3>{ { baseVertex, baseVertex + 1, baseVertex + 2 } }); - } - solid = result; - } - return solid; -} - template -void setupFlatShape(GeometryCache::ShapeData& shapeData, const Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { +void setupFlatShape(GeometryCache::ShapeData& shapeData, const geometry::Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + using namespace geometry; Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); VertexVector vertices; IndexVector solidIndices, wireIndices; @@ -259,7 +189,8 @@ void setupFlatShape(GeometryCache::ShapeData& shapeData, const Solid& shape, } template -void setupSmoothShape(GeometryCache::ShapeData& shapeData, const Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { +void setupSmoothShape(GeometryCache::ShapeData& shapeData, const geometry::Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + using namespace geometry; Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); VertexVector vertices; @@ -303,147 +234,6 @@ void setupSmoothShape(GeometryCache::ShapeData& shapeData, const Solid& shape shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); } -// The golden ratio -static const float PHI = 1.61803398874f; - -static const Solid<3>& tetrahedron() { - static const auto A = vec3(1, 1, 1); - static const auto B = vec3(1, -1, -1); - static const auto C = vec3(-1, 1, -1); - static const auto D = vec3(-1, -1, 1); - static const Solid<3> TETRAHEDRON = Solid<3>{ - { A, B, C, D }, - FaceVector<3>{ - Face<3> { { 0, 1, 2 } }, - Face<3> { { 3, 1, 0 } }, - Face<3> { { 2, 3, 0 } }, - Face<3> { { 2, 1, 3 } }, - } - }.fitDimension(0.5f); - return TETRAHEDRON; -} - -static const Solid<4>& cube() { - static const auto A = vec3(1, 1, 1); - static const auto B = vec3(-1, 1, 1); - static const auto C = vec3(-1, 1, -1); - static const auto D = vec3(1, 1, -1); - static const Solid<4> CUBE = Solid<4>{ - { A, B, C, D, -A, -B, -C, -D }, - FaceVector<4>{ - Face<4> { { 3, 2, 1, 0 } }, - Face<4> { { 0, 1, 7, 6 } }, - Face<4> { { 1, 2, 4, 7 } }, - Face<4> { { 2, 3, 5, 4 } }, - Face<4> { { 3, 0, 6, 5 } }, - Face<4> { { 4, 5, 6, 7 } }, - } - }.fitDimension(0.5f); - return CUBE; -} - -static const Solid<3>& octahedron() { - static const auto A = vec3(0, 1, 0); - static const auto B = vec3(0, -1, 0); - static const auto C = vec3(0, 0, 1); - static const auto D = vec3(0, 0, -1); - static const auto E = vec3(1, 0, 0); - static const auto F = vec3(-1, 0, 0); - static const Solid<3> OCTAHEDRON = Solid<3>{ - { A, B, C, D, E, F}, - FaceVector<3> { - Face<3> { { 0, 2, 4, } }, - Face<3> { { 0, 4, 3, } }, - Face<3> { { 0, 3, 5, } }, - Face<3> { { 0, 5, 2, } }, - Face<3> { { 1, 4, 2, } }, - Face<3> { { 1, 3, 4, } }, - Face<3> { { 1, 5, 3, } }, - Face<3> { { 1, 2, 5, } }, - } - }.fitDimension(0.5f); - return OCTAHEDRON; -} - -static const Solid<5>& dodecahedron() { - static const float P = PHI; - static const float IP = 1.0f / PHI; - static const vec3 A = vec3(IP, P, 0); - static const vec3 B = vec3(-IP, P, 0); - static const vec3 C = vec3(-1, 1, 1); - static const vec3 D = vec3(0, IP, P); - static const vec3 E = vec3(1, 1, 1); - static const vec3 F = vec3(1, 1, -1); - static const vec3 G = vec3(-1, 1, -1); - static const vec3 H = vec3(-P, 0, IP); - static const vec3 I = vec3(0, -IP, P); - static const vec3 J = vec3(P, 0, IP); - - static const Solid<5> DODECAHEDRON = Solid<5>{ - { - A, B, C, D, E, F, G, H, I, J, - -A, -B, -C, -D, -E, -F, -G, -H, -I, -J, - }, - FaceVector<5> { - Face<5> { { 0, 1, 2, 3, 4 } }, - Face<5> { { 0, 5, 18, 6, 1 } }, - Face<5> { { 1, 6, 19, 7, 2 } }, - Face<5> { { 2, 7, 15, 8, 3 } }, - Face<5> { { 3, 8, 16, 9, 4 } }, - Face<5> { { 4, 9, 17, 5, 0 } }, - Face<5> { { 14, 13, 12, 11, 10 } }, - Face<5> { { 11, 16, 8, 15, 10 } }, - Face<5> { { 12, 17, 9, 16, 11 } }, - Face<5> { { 13, 18, 5, 17, 12 } }, - Face<5> { { 14, 19, 6, 18, 13 } }, - Face<5> { { 10, 15, 7, 19, 14 } }, - } - }.fitDimension(0.5f); - return DODECAHEDRON; -} - -static const Solid<3>& icosahedron() { - static const float N = 1.0f / PHI; - static const float P = 1.0f; - static const auto A = vec3(N, P, 0); - static const auto B = vec3(-N, P, 0); - static const auto C = vec3(0, N, P); - static const auto D = vec3(P, 0, N); - static const auto E = vec3(P, 0, -N); - static const auto F = vec3(0, N, -P); - - static const Solid<3> ICOSAHEDRON = Solid<3> { - { - A, B, C, D, E, F, - -A, -B, -C, -D, -E, -F, - }, - FaceVector<3> { - Face<3> { { 1, 2, 0 } }, - Face<3> { { 2, 3, 0 } }, - Face<3> { { 3, 4, 0 } }, - Face<3> { { 4, 5, 0 } }, - Face<3> { { 5, 1, 0 } }, - - Face<3> { { 1, 10, 2 } }, - Face<3> { { 11, 2, 10 } }, - Face<3> { { 2, 11, 3 } }, - Face<3> { { 7, 3, 11 } }, - Face<3> { { 3, 7, 4 } }, - Face<3> { { 8, 4, 7 } }, - Face<3> { { 4, 8, 5 } }, - Face<3> { { 9, 5, 8 } }, - Face<3> { { 5, 9, 1 } }, - Face<3> { { 10, 1, 9 } }, - - Face<3> { { 8, 7, 6 } }, - Face<3> { { 9, 8, 6 } }, - Face<3> { { 10, 9, 6 } }, - Face<3> { { 11, 10, 6 } }, - Face<3> { { 7, 11, 6 } }, - } - }.fitDimension(0.5f); - return ICOSAHEDRON; -} // FIXME solids need per-face vertices, but smooth shaded // components do not. Find a way to support using draw elements @@ -451,24 +241,24 @@ static const Solid<3>& icosahedron() { // Maybe special case cone and cylinder since they combine flat // and smooth shading void GeometryCache::buildShapes() { + using namespace geometry; auto vertexBuffer = std::make_shared(); auto indexBuffer = std::make_shared(); // Cube - setupFlatShape(_shapes[Cube], cube(), _shapeVertices, _shapeIndices); + setupFlatShape(_shapes[Cube], geometry::cube(), _shapeVertices, _shapeIndices); // Tetrahedron - setupFlatShape(_shapes[Tetrahedron], tetrahedron(), _shapeVertices, _shapeIndices); + setupFlatShape(_shapes[Tetrahedron], geometry::tetrahedron(), _shapeVertices, _shapeIndices); // Icosahedron - setupFlatShape(_shapes[Icosahedron], icosahedron(), _shapeVertices, _shapeIndices); + setupFlatShape(_shapes[Icosahedron], geometry::icosahedron(), _shapeVertices, _shapeIndices); // Octahedron - setupFlatShape(_shapes[Octahedron], octahedron(), _shapeVertices, _shapeIndices); + setupFlatShape(_shapes[Octahedron], geometry::octahedron(), _shapeVertices, _shapeIndices); // Dodecahedron - setupFlatShape(_shapes[Dodecahedron], dodecahedron(), _shapeVertices, _shapeIndices); + setupFlatShape(_shapes[Dodecahedron], geometry::dodecahedron(), _shapeVertices, _shapeIndices); // Sphere // FIXME this uses way more vertices than required. Should find a way to calculate the indices // using shared vertices for better vertex caching - Solid<3> sphere = icosahedron(); - sphere = tesselate(sphere, ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT); + auto sphere = geometry::tesselate(geometry::icosahedron(), ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT); sphere.fitDimension(1.0f); setupSmoothShape(_shapes[Sphere], sphere, _shapeVertices, _shapeIndices); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index a2f79de029..c46a9bb084 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -21,6 +21,8 @@ #include +#include + #include #include @@ -121,9 +123,6 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) { seed); } -using IndexVector = std::vector; -using VertexVector = std::vector; - /// Stores cached geometry. class GeometryCache : public Dependency { SINGLETON_DEPENDENCY @@ -297,8 +296,8 @@ public: gpu::BufferView _normalView; gpu::BufferPointer _indices; - void setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices); - void setupIndices(gpu::BufferPointer& indexBuffer, const IndexVector& indices, const IndexVector& wireIndices); + void setupVertices(gpu::BufferPointer& vertexBuffer, const geometry::VertexVector& vertices); + void setupIndices(gpu::BufferPointer& indexBuffer, const geometry::IndexVector& indices, const geometry::IndexVector& wireIndices); void setupBatch(gpu::Batch& batch) const; void draw(gpu::Batch& batch) const; void drawWire(gpu::Batch& batch) const; diff --git a/libraries/shared/src/shared/Shapes.cpp b/libraries/shared/src/shared/Shapes.cpp new file mode 100644 index 0000000000..dabdf21202 --- /dev/null +++ b/libraries/shared/src/shared/Shapes.cpp @@ -0,0 +1,186 @@ +// +// Created by Bradley Austin Davis on 2016/05/26 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Shapes.h" + +namespace geometry { + +using glm::vec3; + +// The golden ratio +static const float PHI = 1.61803398874f; + +Solid<3> tesselate(const Solid<3>& solid_, int count) { + Solid<3> solid = solid_; + float length = glm::length(solid.vertices[0]); + for (int i = 0; i < count; ++i) { + Solid<3> result { solid.vertices, {} }; + result.vertices.reserve(solid.vertices.size() + solid.faces.size() * 3); + for (size_t f = 0; f < solid.faces.size(); ++f) { + Index baseVertex = (Index)result.vertices.size(); + const Face<3>& oldFace = solid.faces[f]; + const vec3& a = solid.vertices[oldFace[0]]; + const vec3& b = solid.vertices[oldFace[1]]; + const vec3& c = solid.vertices[oldFace[2]]; + vec3 ab = glm::normalize(a + b) * length; + vec3 bc = glm::normalize(b + c) * length; + vec3 ca = glm::normalize(c + a) * length; + result.vertices.push_back(ab); + result.vertices.push_back(bc); + result.vertices.push_back(ca); + result.faces.push_back(Face<3>{ { oldFace[0], baseVertex, baseVertex + 2 } }); + result.faces.push_back(Face<3>{ { baseVertex, oldFace[1], baseVertex + 1 } }); + result.faces.push_back(Face<3>{ { baseVertex + 1, oldFace[2], baseVertex + 2 } }); + result.faces.push_back(Face<3>{ { baseVertex, baseVertex + 1, baseVertex + 2 } }); + } + solid = result; + } + return solid; +} + +const Solid<3>& tetrahedron() { + static const auto A = vec3(1, 1, 1); + static const auto B = vec3(1, -1, -1); + static const auto C = vec3(-1, 1, -1); + static const auto D = vec3(-1, -1, 1); + static const Solid<3> TETRAHEDRON = Solid<3>{ + { A, B, C, D }, + FaceVector<3>{ + Face<3> { { 0, 1, 2 } }, + Face<3> { { 3, 1, 0 } }, + Face<3> { { 2, 3, 0 } }, + Face<3> { { 2, 1, 3 } }, + } + }.fitDimension(0.5f); + return TETRAHEDRON; +} + +const Solid<4>& cube() { + static const auto A = vec3(1, 1, 1); + static const auto B = vec3(-1, 1, 1); + static const auto C = vec3(-1, 1, -1); + static const auto D = vec3(1, 1, -1); + static const Solid<4> CUBE = Solid<4>{ + { A, B, C, D, -A, -B, -C, -D }, + FaceVector<4>{ + Face<4> { { 3, 2, 1, 0 } }, + Face<4> { { 0, 1, 7, 6 } }, + Face<4> { { 1, 2, 4, 7 } }, + Face<4> { { 2, 3, 5, 4 } }, + Face<4> { { 3, 0, 6, 5 } }, + Face<4> { { 4, 5, 6, 7 } }, + } + }.fitDimension(0.5f); + return CUBE; +} + +const Solid<3>& octahedron() { + static const auto A = vec3(0, 1, 0); + static const auto B = vec3(0, -1, 0); + static const auto C = vec3(0, 0, 1); + static const auto D = vec3(0, 0, -1); + static const auto E = vec3(1, 0, 0); + static const auto F = vec3(-1, 0, 0); + static const Solid<3> OCTAHEDRON = Solid<3>{ + { A, B, C, D, E, F}, + FaceVector<3> { + Face<3> { { 0, 2, 4, } }, + Face<3> { { 0, 4, 3, } }, + Face<3> { { 0, 3, 5, } }, + Face<3> { { 0, 5, 2, } }, + Face<3> { { 1, 4, 2, } }, + Face<3> { { 1, 3, 4, } }, + Face<3> { { 1, 5, 3, } }, + Face<3> { { 1, 2, 5, } }, + } + }.fitDimension(0.5f); + return OCTAHEDRON; +} + +const Solid<5>& dodecahedron() { + static const float P = PHI; + static const float IP = 1.0f / PHI; + static const vec3 A = vec3(IP, P, 0); + static const vec3 B = vec3(-IP, P, 0); + static const vec3 C = vec3(-1, 1, 1); + static const vec3 D = vec3(0, IP, P); + static const vec3 E = vec3(1, 1, 1); + static const vec3 F = vec3(1, 1, -1); + static const vec3 G = vec3(-1, 1, -1); + static const vec3 H = vec3(-P, 0, IP); + static const vec3 I = vec3(0, -IP, P); + static const vec3 J = vec3(P, 0, IP); + + static const Solid<5> DODECAHEDRON = Solid<5>{ + { + A, B, C, D, E, F, G, H, I, J, + -A, -B, -C, -D, -E, -F, -G, -H, -I, -J, + }, + FaceVector<5> { + Face<5> { { 0, 1, 2, 3, 4 } }, + Face<5> { { 0, 5, 18, 6, 1 } }, + Face<5> { { 1, 6, 19, 7, 2 } }, + Face<5> { { 2, 7, 15, 8, 3 } }, + Face<5> { { 3, 8, 16, 9, 4 } }, + Face<5> { { 4, 9, 17, 5, 0 } }, + Face<5> { { 14, 13, 12, 11, 10 } }, + Face<5> { { 11, 16, 8, 15, 10 } }, + Face<5> { { 12, 17, 9, 16, 11 } }, + Face<5> { { 13, 18, 5, 17, 12 } }, + Face<5> { { 14, 19, 6, 18, 13 } }, + Face<5> { { 10, 15, 7, 19, 14 } }, + } + }.fitDimension(0.5f); + return DODECAHEDRON; +} + +const Solid<3>& icosahedron() { + static const float N = 1.0f / PHI; + static const float P = 1.0f; + static const auto A = vec3(N, P, 0); + static const auto B = vec3(-N, P, 0); + static const auto C = vec3(0, N, P); + static const auto D = vec3(P, 0, N); + static const auto E = vec3(P, 0, -N); + static const auto F = vec3(0, N, -P); + + static const Solid<3> ICOSAHEDRON = Solid<3> { + { + A, B, C, D, E, F, + -A, -B, -C, -D, -E, -F, + }, + FaceVector<3> { + Face<3> { { 1, 2, 0 } }, + Face<3> { { 2, 3, 0 } }, + Face<3> { { 3, 4, 0 } }, + Face<3> { { 4, 5, 0 } }, + Face<3> { { 5, 1, 0 } }, + + Face<3> { { 1, 10, 2 } }, + Face<3> { { 11, 2, 10 } }, + Face<3> { { 2, 11, 3 } }, + Face<3> { { 7, 3, 11 } }, + Face<3> { { 3, 7, 4 } }, + Face<3> { { 8, 4, 7 } }, + Face<3> { { 4, 8, 5 } }, + Face<3> { { 9, 5, 8 } }, + Face<3> { { 5, 9, 1 } }, + Face<3> { { 10, 1, 9 } }, + + Face<3> { { 8, 7, 6 } }, + Face<3> { { 9, 8, 6 } }, + Face<3> { { 10, 9, 6 } }, + Face<3> { { 11, 10, 6 } }, + Face<3> { { 7, 11, 6 } }, + } + }.fitDimension(0.5f); + return ICOSAHEDRON; +} + +} + diff --git a/libraries/shared/src/shared/Shapes.h b/libraries/shared/src/shared/Shapes.h new file mode 100644 index 0000000000..2d7827d046 --- /dev/null +++ b/libraries/shared/src/shared/Shapes.h @@ -0,0 +1,79 @@ +// +// Created by Bradley Austin Davis on 2016/05/26 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_shared_shapes +#define hifi_shared_shapes + +#include + +#include +#include + +#include + +namespace geometry { + + using Index = uint32_t; + using Vec = glm::vec3; + using VertexVector = std::vector; + using IndexVector = std::vector; + + template + using Face = std::array; + + template + using FaceVector = std::vector>; + + template + struct Solid { + VertexVector vertices; + FaceVector faces; + + Solid& fitDimension(float newMaxDimension) { + float maxDimension = 0; + for (const auto& vertex : vertices) { + maxDimension = std::max(maxDimension, std::max(std::max(vertex.x, vertex.y), vertex.z)); + } + float multiplier = newMaxDimension / maxDimension; + for (auto& vertex : vertices) { + vertex *= multiplier; + } + return *this; + } + + Vec getFaceNormal(size_t faceIndex) const { + vec3 result; + const auto& face = faces[faceIndex]; + for (size_t i = 0; i < N; ++i) { + result += vertices[face[i]]; + } + result /= N; + return glm::normalize(result); + } + }; + + template + size_t triangulatedFaceTriangleCount() { + return N - 2; + } + + template + size_t triangulatedFaceIndexCount() { + return triangulatedFaceTriangleCount() * VERTICES_PER_TRIANGLE; + } + + Solid<3> tesselate(const Solid<3>& solid, int count); + const Solid<3>& tetrahedron(); + const Solid<4>& cube(); + const Solid<3>& octahedron(); + const Solid<5>& dodecahedron(); + const Solid<3>& icosahedron(); +} + +#endif From b4d14f06d8dfe73ba2064366a074e89a832d74e5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 26 May 2016 14:35:17 -0700 Subject: [PATCH 0246/1237] Bugfix for avatar LOD If an avatar was LOD'ed out, it would never become visible again, because the bounds of the skeleton model were never again. This ensures that the model bound is updated, even when the avatar is LOD'ed away. --- interface/src/avatar/Avatar.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ccda77a44d..3e22448386 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -303,6 +303,9 @@ void Avatar::simulate(float deltaTime) { head->setScale(getUniformScale()); head->simulate(deltaTime, false, !_shouldAnimate); } + } else { + // a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated. + _skeletonModel->simulate(deltaTime, false); } // update animation for display name fade in/out From 1ec421350d68101e6b93f01aa9ef33d5035eedd9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 26 May 2016 13:16:20 -0700 Subject: [PATCH 0247/1237] send the UUID session ID without curly braces --- libraries/networking/src/AccountManager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 46e72170e5..bac031885f 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -222,7 +222,8 @@ void AccountManager::sendRequest(const QString& path, auto& activityLogger = UserActivityLogger::getInstance(); if (activityLogger.isEnabled()) { static const QString METAVERSE_SESSION_ID_HEADER = "HFM-SessionID"; - networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER.toLocal8Bit(), _sessionID.toString().toLocal8Bit()); + networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER.toLocal8Bit(), + uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); } QUrl requestURL = _authURL; From 6a962d7aab95b8742b7bac005cae9dbd716bc859 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 26 May 2016 12:42:35 -0700 Subject: [PATCH 0248/1237] Unix build fix --- libraries/shared/src/shared/Shapes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/shared/src/shared/Shapes.h b/libraries/shared/src/shared/Shapes.h index 2d7827d046..3486a0a663 100644 --- a/libraries/shared/src/shared/Shapes.h +++ b/libraries/shared/src/shared/Shapes.h @@ -48,7 +48,7 @@ namespace geometry { } Vec getFaceNormal(size_t faceIndex) const { - vec3 result; + Vec result; const auto& face = faces[faceIndex]; for (size_t i = 0; i < N; ++i) { result += vertices[face[i]]; @@ -65,7 +65,7 @@ namespace geometry { template size_t triangulatedFaceIndexCount() { - return triangulatedFaceTriangleCount() * VERTICES_PER_TRIANGLE; + return triangulatedFaceTriangleCount() * 3; } Solid<3> tesselate(const Solid<3>& solid, int count); From 5b6660e7ea23e0561fc709821cf93fef7e457c43 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 27 May 2016 10:35:46 -0700 Subject: [PATCH 0249/1237] Default preference to false. --- interface/src/avatar/MyAvatar.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index f709f79542..d3da32e0ed 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -398,7 +398,7 @@ private: QString _fullAvatarModelName; QUrl _animGraphUrl {""}; bool _useSnapTurn { true }; - bool _clearOverlayWhenDriving { true }; + bool _clearOverlayWhenDriving { false }; // cache of the current HMD sensor position and orientation // in sensor space. From ac5912df01493075122b7d9680adedf5c0a5ed56 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 27 May 2016 13:39:30 -0700 Subject: [PATCH 0250/1237] Fix crash on shutdown of SteamVR --- interface/src/Application.cpp | 20 ++++++++- libraries/plugins/src/plugins/DisplayPlugin.h | 6 ++- .../plugins/src/plugins/PluginManager.cpp | 13 +++++- libraries/plugins/src/plugins/PluginManager.h | 1 + .../oculus/src/OculusBaseDisplayPlugin.cpp | 3 +- plugins/oculus/src/OculusBaseDisplayPlugin.h | 6 +-- .../src/OculusLegacyDisplayPlugin.cpp | 3 +- .../src/OculusLegacyDisplayPlugin.h | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 8 +++- plugins/openvr/src/OpenVrDisplayPlugin.h | 12 +++--- plugins/openvr/src/OpenVrHelpers.cpp | 43 ++++++++++++++++--- plugins/openvr/src/OpenVrHelpers.h | 2 + plugins/openvr/src/ViveControllerManager.cpp | 5 +++ 13 files changed, 101 insertions(+), 23 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 48b418b93c..f2d4f9edf1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1503,7 +1503,13 @@ void Application::paintGL() { // FIXME not needed anymore? _offscreenContext->makeCurrent(); - displayPlugin->beginFrameRender(_frameCount); + // If a display plugin loses it's underlying support, it + // needs to be able to signal us to not use it + if (!displayPlugin->beginFrameRender(_frameCount)) { + _inPaint = false; + updateDisplayMode(); + return; + } // update the avatar with a fresh HMD pose getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); @@ -5098,9 +5104,17 @@ void Application::updateDisplayMode() { foreach(auto displayPlugin, standard) { addDisplayPluginToMenu(displayPlugin, first); + auto displayPluginName = displayPlugin->getName(); QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, [this](const QSize & size) { resizeGL(); }); + QObject::connect(displayPlugin.get(), &DisplayPlugin::outputDeviceLost, [this, displayPluginName] { + PluginManager::getInstance()->disableDisplayPlugin(displayPluginName); + auto menu = Menu::getInstance(); + if (menu->menuItemExists(MenuOption::OutputMenu, displayPluginName)) { + menu->removeMenuItem(MenuOption::OutputMenu, displayPluginName); + } + }); first = false; } @@ -5116,6 +5130,10 @@ void Application::updateDisplayMode() { foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { QString name = displayPlugin->getName(); QAction* action = menu->getActionForOption(name); + // Menu might have been removed if the display plugin lost + if (!action) { + continue; + } if (action->isChecked()) { newDisplayPlugin = displayPlugin; break; diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 41f380aa86..1f6a16cd46 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -137,7 +137,7 @@ public: } // will query the underlying hmd api to compute the most recent head pose - virtual void beginFrameRender(uint32_t frameIndex) {} + virtual bool beginFrameRender(uint32_t frameIndex) { return true; } // returns a copy of the most recent head pose, computed via updateHeadPose virtual glm::mat4 getHeadPose() const { @@ -170,6 +170,10 @@ public: signals: void recommendedFramebufferSizeChanged(const QSize & size); + // Indicates that this display plugin is no longer valid for use. + // For instance if a user exits Oculus Home or Steam VR while + // using the corresponding plugin, that plugin should be disabled. + void outputDeviceLost(); protected: void incrementPresentCount(); diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 936cb8a7ee..eb6465aab2 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -62,11 +62,10 @@ PluginManager::PluginManager() { extern DisplayPluginList getDisplayPlugins(); extern InputPluginList getInputPlugins(); extern void saveInputPluginSettings(const InputPluginList& plugins); +static DisplayPluginList displayPlugins; const DisplayPluginList& PluginManager::getDisplayPlugins() { - static DisplayPluginList displayPlugins; static std::once_flag once; - std::call_once(once, [&] { // Grab the built in plugins displayPlugins = ::getDisplayPlugins(); @@ -90,6 +89,16 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { return displayPlugins; } +void PluginManager::disableDisplayPlugin(const QString& name) { + for (size_t i = 0; i < displayPlugins.size(); ++i) { + if (displayPlugins[i]->getName() == name) { + displayPlugins.erase(displayPlugins.begin() + i); + break; + } + } +} + + const InputPluginList& PluginManager::getInputPlugins() { static InputPluginList inputPlugins; static std::once_flag once; diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 2e056414ec..cf0b8efe64 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -17,6 +17,7 @@ public: PluginManager(); const DisplayPluginList& getDisplayPlugins(); + void disableDisplayPlugin(const QString& name); const InputPluginList& getInputPlugins(); void saveSettings(); }; diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 3b6545ae96..e9f8545cff 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -17,7 +17,7 @@ void OculusBaseDisplayPlugin::resetSensors() { _currentRenderFrameInfo.renderPose = glm::mat4(); // identity } -void OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { +bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo = FrameInfo(); _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds();; _currentRenderFrameInfo.predictedDisplayTime = ovr_GetPredictedDisplayTime(_session, frameIndex); @@ -26,6 +26,7 @@ void OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; Lock lock(_mutex); _frameInfos[frameIndex] = _currentRenderFrameInfo; + return true; } bool OculusBaseDisplayPlugin::isSupported() const { diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index ed6f1a36b3..3e2c223908 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -16,11 +16,11 @@ class OculusBaseDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; public: - virtual bool isSupported() const override; + bool isSupported() const override; // Stereo specific methods - virtual void resetSensors() override final; - virtual void beginFrameRender(uint32_t frameIndex) override; + void resetSensors() override final; + bool beginFrameRender(uint32_t frameIndex) override; float getTargetFrameRate() const override { return _hmdDesc.DisplayRefreshRate; } diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 753ff923dd..f89e71b829 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -40,13 +40,14 @@ void OculusLegacyDisplayPlugin::resetSensors() { ovrHmd_RecenterPose(_hmd); } -void OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { +bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo = FrameInfo(); _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _trackingState = ovrHmd_GetTrackingState(_hmd, _currentRenderFrameInfo.predictedDisplayTime); _currentRenderFrameInfo.rawRenderPose = _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); Lock lock(_mutex); _frameInfos[frameIndex] = _currentRenderFrameInfo; + return true; } bool OculusLegacyDisplayPlugin::isSupported() const { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 5900ad4c58..453a6f9168 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -27,7 +27,7 @@ public: // Stereo specific methods void resetSensors() override; - void beginFrameRender(uint32_t frameIndex) override; + bool beginFrameRender(uint32_t frameIndex) override; float getTargetFrameRate() const override; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index dba8fca208..2e0bebeadd 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -121,7 +121,12 @@ void OpenVrDisplayPlugin::resetSensors() { } -void OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { +bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + handleOpenVrEvents(); + if (openVrQuitRequested()) { + emit outputDeviceLost(); + return false; + } double displayFrequency = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float); double frameDuration = 1.f / displayFrequency; double vsyncToPhotons = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float); @@ -148,6 +153,7 @@ void OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { Lock lock(_mutex); _frameInfos[frameIndex] = _currentRenderFrameInfo; + return true; } void OpenVrDisplayPlugin::hmdPresent() { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 75193c5c98..fda5e37c2a 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -18,16 +18,16 @@ const float TARGET_RATE_OpenVr = 90.0f; // FIXME: get from sdk tracked device p class OpenVrDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; public: - virtual bool isSupported() const override; - virtual const QString& getName() const override { return NAME; } + bool isSupported() const override; + const QString& getName() const override { return NAME; } - virtual float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } + float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } - virtual void customizeContext() override; + void customizeContext() override; // Stereo specific methods - virtual void resetSensors() override; - virtual void beginFrameRender(uint32_t frameIndex) override; + void resetSensors() override; + bool beginFrameRender(uint32_t frameIndex) override; void cycleDebugOutput() override { _lockCurrentTexture = !_lockCurrentTexture; } protected: diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 8536ffd5d9..155bc9f079 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -26,6 +26,11 @@ using Lock = std::unique_lock; static int refCount { 0 }; static Mutex mutex; static vr::IVRSystem* activeHmd { nullptr }; +static bool _openVrQuitRequested { false }; + +bool openVrQuitRequested() { + return _openVrQuitRequested; +} static const uint32_t RELEASE_OPENVR_HMD_DELAY_MS = 5000; @@ -56,17 +61,17 @@ vr::IVRSystem* acquireOpenVrSystem() { if (hmdPresent) { Lock lock(mutex); if (!activeHmd) { - qCDebug(displayplugins) << "openvr: No vr::IVRSystem instance active, building"; + qCDebug(displayplugins) << "OpenVR: No vr::IVRSystem instance active, building"; vr::EVRInitError eError = vr::VRInitError_None; activeHmd = vr::VR_Init(&eError, vr::VRApplication_Scene); - qCDebug(displayplugins) << "openvr display: HMD is " << activeHmd << " error is " << eError; + qCDebug(displayplugins) << "OpenVR display: HMD is " << activeHmd << " error is " << eError; } if (activeHmd) { - qCDebug(displayplugins) << "openvr: incrementing refcount"; + qCDebug(displayplugins) << "OpenVR: incrementing refcount"; ++refCount; } } else { - qCDebug(displayplugins) << "openvr: no hmd present"; + qCDebug(displayplugins) << "OpenVR: no hmd present"; } return activeHmd; } @@ -74,12 +79,38 @@ vr::IVRSystem* acquireOpenVrSystem() { void releaseOpenVrSystem() { if (activeHmd) { Lock lock(mutex); - qCDebug(displayplugins) << "openvr: decrementing refcount"; + qCDebug(displayplugins) << "OpenVR: decrementing refcount"; --refCount; if (0 == refCount) { - qCDebug(displayplugins) << "openvr: zero refcount, deallocate VR system"; + qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; vr::VR_Shutdown(); + _openVrQuitRequested = false; activeHmd = nullptr; } } } + +void handleOpenVrEvents() { + if (!activeHmd) { + return; + } + Lock lock(mutex); + if (!activeHmd) { + return; + } + + vr::VREvent_t event; + while (activeHmd->PollNextEvent(&event, sizeof(event))) { + switch (event.eventType) { + case vr::VREvent_Quit: + _openVrQuitRequested = true; + activeHmd->AcknowledgeQuit_Exiting(); + break; + + default: + break; + } + qDebug() << "OpenVR: Event " << event.eventType; + } + +} diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 4b06ca0813..1e5914844c 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -16,6 +16,8 @@ bool openVrSupported(); vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); +void handleOpenVrEvents(); +bool openVrQuitRequested(); template void openvr_for_each_eye(F f) { diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 6e75454b5f..953501ccec 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -214,6 +214,11 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); + handleOpenVrEvents(); + if (openVrQuitRequested()) { + deactivate(); + return; + } // because update mutates the internal state we need to lock userInputMapper->withLock([&, this]() { From b280eebd03026c807f40a4d3c157879ec9993307 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 27 May 2016 11:44:23 -0700 Subject: [PATCH 0251/1237] Only count users against max capacity --- domain-server/src/DomainServer.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index f6fbb3f470..052a7c0fec 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -715,9 +715,13 @@ void DomainServer::processListRequestPacket(QSharedPointer mess unsigned int DomainServer::countConnectedUsers() { unsigned int result = 0; auto nodeList = DependencyManager::get(); - nodeList->eachNode([&](const SharedNodePointer& otherNode){ - if (otherNode->getType() == NodeType::Agent) { - result++; + nodeList->eachNode([&](const SharedNodePointer& node){ + // only count unassigned agents (i.e., users) + if (node->getType() == NodeType::Agent) { + auto nodeData = static_cast(node->getLinkedData()); + if (nodeData && !nodeData->wasAssigned()) { + result++; + } } }); return result; From 0ec7eae58ca7191531d7464ceebca40ddf5895d4 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 27 May 2016 16:59:42 -0700 Subject: [PATCH 0252/1237] fix shapes to property polymorph and persist --- .../entities/src/EntityItemProperties.cpp | 19 ++++++-- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + scripts/system/html/entityProperties.html | 48 ++++++------------- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index f273507d0d..ba39727ff9 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -314,6 +314,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly); CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); + CHECK_PROPERTY_CHANGE(PROP_SHAPE, shape); + changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); changedProperties += _skybox.getChangedProperties(); @@ -435,6 +437,9 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool if (_type == EntityTypes::Sphere) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, QString("Sphere")); } + if (_type == EntityTypes::Box || _type == EntityTypes::Sphere || _type == EntityTypes::Shape) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SHAPE, shape); + } // FIXME - it seems like ParticleEffect should also support this if (_type == EntityTypes::Model || _type == EntityTypes::Zone) { @@ -1144,7 +1149,11 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_STROKE_WIDTHS, properties.getStrokeWidths()); APPEND_ENTITY_PROPERTY(PROP_TEXTURES, properties.getTextures()); } - if (properties.getType() == EntityTypes::Shape) { + // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE + // when encoding/decoding edits because otherwise they can't polymorph to other shape types + if (properties.getType() == EntityTypes::Shape || + properties.getType() == EntityTypes::Box || + properties.getType() == EntityTypes::Sphere) { APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); @@ -1211,7 +1220,6 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem } else { packetData->discardSubTree(); } - return success; } @@ -1434,7 +1442,12 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_WIDTHS, QVector, setStrokeWidths); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_TEXTURES, QString, setTextures); } - if (properties.getType() == EntityTypes::Shape) { + + // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE + // when encoding/decoding edits because otherwise they can't polymorph to other shape types + if (properties.getType() == EntityTypes::Shape || + properties.getType() == EntityTypes::Box || + properties.getType() == EntityTypes::Sphere) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 92e5b52f75..db743f81e4 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_MORE_SHAPES; + return VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 97398c6744..320635379d 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -179,6 +179,7 @@ const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; +const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 5c87f753d9..2a82d8fa74 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -557,7 +557,6 @@ } properties = data.selections[0].properties; - elID.innerHTML = properties.id; elType.innerHTML = properties.type; @@ -675,6 +674,9 @@ for (var i = 0; i < elShapeSections.length; i++) { elShapeSections[i].style.display = 'table'; } + elShape.value = properties.shape; + setDropdownText(elShape); + } else { for (var i = 0; i < elShapeSections.length; i++) { console.log("Hiding shape section " + elShapeSections[i]) @@ -1349,6 +1351,17 @@

+
@@ -1362,39 +1375,6 @@
-
- M -
- -
- - -
-
- - -
- - From 3bee4b1f9f07e933c4a5e5a8d46e4503b96315ba Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 27 May 2016 16:59:42 -0700 Subject: [PATCH 0253/1237] fix shapes to property polymorph and persist --- .../entities/src/EntityItemProperties.cpp | 19 ++++++-- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + scripts/system/html/entityProperties.html | 48 ++++++------------- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index f273507d0d..ba39727ff9 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -314,6 +314,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly); CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); + CHECK_PROPERTY_CHANGE(PROP_SHAPE, shape); + changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); changedProperties += _skybox.getChangedProperties(); @@ -435,6 +437,9 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool if (_type == EntityTypes::Sphere) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, QString("Sphere")); } + if (_type == EntityTypes::Box || _type == EntityTypes::Sphere || _type == EntityTypes::Shape) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SHAPE, shape); + } // FIXME - it seems like ParticleEffect should also support this if (_type == EntityTypes::Model || _type == EntityTypes::Zone) { @@ -1144,7 +1149,11 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_STROKE_WIDTHS, properties.getStrokeWidths()); APPEND_ENTITY_PROPERTY(PROP_TEXTURES, properties.getTextures()); } - if (properties.getType() == EntityTypes::Shape) { + // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE + // when encoding/decoding edits because otherwise they can't polymorph to other shape types + if (properties.getType() == EntityTypes::Shape || + properties.getType() == EntityTypes::Box || + properties.getType() == EntityTypes::Sphere) { APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); @@ -1211,7 +1220,6 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem } else { packetData->discardSubTree(); } - return success; } @@ -1434,7 +1442,12 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_WIDTHS, QVector, setStrokeWidths); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_TEXTURES, QString, setTextures); } - if (properties.getType() == EntityTypes::Shape) { + + // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE + // when encoding/decoding edits because otherwise they can't polymorph to other shape types + if (properties.getType() == EntityTypes::Shape || + properties.getType() == EntityTypes::Box || + properties.getType() == EntityTypes::Sphere) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 92e5b52f75..db743f81e4 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_MORE_SHAPES; + return VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 97398c6744..320635379d 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -179,6 +179,7 @@ const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; +const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 5c87f753d9..2a82d8fa74 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -557,7 +557,6 @@ } properties = data.selections[0].properties; - elID.innerHTML = properties.id; elType.innerHTML = properties.type; @@ -675,6 +674,9 @@ for (var i = 0; i < elShapeSections.length; i++) { elShapeSections[i].style.display = 'table'; } + elShape.value = properties.shape; + setDropdownText(elShape); + } else { for (var i = 0; i < elShapeSections.length; i++) { console.log("Hiding shape section " + elShapeSections[i]) @@ -1349,6 +1351,17 @@
+
@@ -1362,39 +1375,6 @@
-
- M -
- -
- - -
-
- - -
- - From 3fe9b67e45e43963a91d547a08dbdcb6c4c50261 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 27 May 2016 19:24:56 -0700 Subject: [PATCH 0254/1237] FIxing the reflection jittering by moving the normal into world space --- libraries/gpu/src/gpu/Transform.slh | 9 +++++++++ libraries/render-utils/src/DeferredGlobalLight.slh | 3 ++- libraries/render-utils/src/model.slv | 2 +- libraries/render-utils/src/model_lightmap.slv | 2 +- libraries/render-utils/src/model_lightmap_normal_map.slv | 4 ++-- libraries/render-utils/src/model_normal_map.slv | 4 ++-- libraries/render-utils/src/overlay3D.slv | 2 +- libraries/render-utils/src/point_light.slf | 2 +- libraries/render-utils/src/sdf_text3D.slv | 2 +- libraries/render-utils/src/simple.slv | 2 +- libraries/render-utils/src/skin_model.slv | 3 +-- libraries/render-utils/src/skin_model_normal_map.slv | 4 ++-- libraries/render-utils/src/spot_light.slf | 2 +- libraries/render-utils/src/standardTransformPNTC.slv | 2 +- 14 files changed, 26 insertions(+), 17 deletions(-) diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index 078b19d494..246167e186 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -119,6 +119,15 @@ TransformObject getTransformObject() { } <@endfunc@> +<@func transformModelToWorldDir(cameraTransform, objectTransform, modelDir, worldDir)@> + { // transformModelToEyeDir + vec3 mr0 = <$objectTransform$>._modelInverse[0].xyz; + vec3 mr1 = <$objectTransform$>._modelInverse[1].xyz; + vec3 mr2 = <$objectTransform$>._modelInverse[2].xyz; + + <$worldDir$> = vec3(dot(mr0, <$modelDir$>), dot(mr1, <$modelDir$>), dot(mr2, <$modelDir$>)); + } +<@endfunc@> <@func transformModelToEyeDir(cameraTransform, objectTransform, modelDir, eyeDir)@> { // transformModelToEyeDir diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 7608c8ae08..d29c8bea87 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -66,7 +66,8 @@ vec3 evalGlobalSpecularIrradiance(Light light, vec3 fragEyeDir, vec3 fragNormal, // prepareGlobalLight // Transform directions to worldspace - vec3 fragNormal = vec3(invViewMat * vec4(normal, 0.0)); + // vec3 fragNormal = vec3(invViewMat * vec4(normal, 0.0)); + vec3 fragNormal = vec3((normal)); vec3 fragEyeVector = vec3(invViewMat * vec4(-position, 0.0)); vec3 fragEyeDir = normalize(fragEyeVector); diff --git a/libraries/render-utils/src/model.slv b/libraries/render-utils/src/model.slv index f1989dcf76..825cb66a48 100755 --- a/libraries/render-utils/src/model.slv +++ b/libraries/render-utils/src/model.slv @@ -36,5 +36,5 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> } diff --git a/libraries/render-utils/src/model_lightmap.slv b/libraries/render-utils/src/model_lightmap.slv index f73b6a02a7..161ceed14c 100755 --- a/libraries/render-utils/src/model_lightmap.slv +++ b/libraries/render-utils/src/model_lightmap.slv @@ -39,6 +39,6 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> } diff --git a/libraries/render-utils/src/model_lightmap_normal_map.slv b/libraries/render-utils/src/model_lightmap_normal_map.slv index cb333f50e3..5fb60d9227 100755 --- a/libraries/render-utils/src/model_lightmap_normal_map.slv +++ b/libraries/render-utils/src/model_lightmap_normal_map.slv @@ -39,6 +39,6 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> - <$transformModelToEyeDir(cam, obj, inTangent.xyz, _tangent)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> + <$transformModelToWorldDir(cam, obj, inTangent.xyz, _tangent)$> } diff --git a/libraries/render-utils/src/model_normal_map.slv b/libraries/render-utils/src/model_normal_map.slv index ded37923c2..425dc204d9 100755 --- a/libraries/render-utils/src/model_normal_map.slv +++ b/libraries/render-utils/src/model_normal_map.slv @@ -39,6 +39,6 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> - <$transformModelToEyeDir(cam, obj, inTangent.xyz, _tangent)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> + <$transformModelToWorldDir(cam, obj, inTangent.xyz, _tangent)$> } diff --git a/libraries/render-utils/src/overlay3D.slv b/libraries/render-utils/src/overlay3D.slv index d39e5a2f01..ee28367413 100644 --- a/libraries/render-utils/src/overlay3D.slv +++ b/libraries/render-utils/src/overlay3D.slv @@ -32,5 +32,5 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> } diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 8c9ff2c8ad..96cf7152d9 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -61,7 +61,7 @@ void main(void) { vec3 fragLightDir = fragLightVec / fragLightDistance; // Eval shading - vec3 fragNormal = vec3(invViewMat * vec4(frag.normal, 0.0)); + vec3 fragNormal = vec3(frag.normal); vec4 fragEyeVector = invViewMat * vec4(-frag.position.xyz, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); diff --git a/libraries/render-utils/src/sdf_text3D.slv b/libraries/render-utils/src/sdf_text3D.slv index d8b7587789..29bc1a9e85 100644 --- a/libraries/render-utils/src/sdf_text3D.slv +++ b/libraries/render-utils/src/sdf_text3D.slv @@ -27,5 +27,5 @@ void main() { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal.xyz)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal.xyz)$> } \ No newline at end of file diff --git a/libraries/render-utils/src/simple.slv b/libraries/render-utils/src/simple.slv index d56d1cc8e2..64d3e24192 100644 --- a/libraries/render-utils/src/simple.slv +++ b/libraries/render-utils/src/simple.slv @@ -34,5 +34,5 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> } \ No newline at end of file diff --git a/libraries/render-utils/src/skin_model.slv b/libraries/render-utils/src/skin_model.slv index c8501b8ddf..c538c8321e 100755 --- a/libraries/render-utils/src/skin_model.slv +++ b/libraries/render-utils/src/skin_model.slv @@ -45,6 +45,5 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, position, _position, gl_Position)$> - <$transformModelToEyeDir(cam, obj, interpolatedNormal.xyz, interpolatedNormal.xyz)$> - _normal = interpolatedNormal.xyz; + <$transformModelToWorldDir(cam, obj, interpolatedNormal.xyz, _normal.xyz)$> } diff --git a/libraries/render-utils/src/skin_model_normal_map.slv b/libraries/render-utils/src/skin_model_normal_map.slv index db4b206405..b57a52f2e6 100755 --- a/libraries/render-utils/src/skin_model_normal_map.slv +++ b/libraries/render-utils/src/skin_model_normal_map.slv @@ -50,8 +50,8 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, position, _position, gl_Position)$> - <$transformModelToEyeDir(cam, obj, interpolatedNormal.xyz, interpolatedNormal.xyz)$> - <$transformModelToEyeDir(cam, obj, interpolatedTangent.xyz, interpolatedTangent.xyz)$> + <$transformModelToWorldDir(cam, obj, interpolatedNormal.xyz, interpolatedNormal.xyz)$> + <$transformModelToWorldDir(cam, obj, interpolatedTangent.xyz, interpolatedTangent.xyz)$> _normal = interpolatedNormal.xyz; _tangent = interpolatedTangent.xyz; diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 4191ba3f63..e5bdac1325 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -68,7 +68,7 @@ void main(void) { } // Eval shading - vec3 fragNormal = vec3(invViewMat * vec4(frag.normal, 0.0)); + vec3 fragNormal = vec3(frag.normal); vec4 fragEyeVector = invViewMat * vec4(-frag.position.xyz, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); diff --git a/libraries/render-utils/src/standardTransformPNTC.slv b/libraries/render-utils/src/standardTransformPNTC.slv index f7e72b6997..0ced5ba6e2 100644 --- a/libraries/render-utils/src/standardTransformPNTC.slv +++ b/libraries/render-utils/src/standardTransformPNTC.slv @@ -30,6 +30,6 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, varNormal)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, varNormal)$> varPosition = inPosition.xyz; } \ No newline at end of file From 1e7cd06d0e5ed13497977cc30e89519a08393e51 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sat, 28 May 2016 20:34:27 -0700 Subject: [PATCH 0255/1237] add audio peak limiter to audio mixer --- assignment-client/src/audio/AudioMixer.cpp | 17 +++++++---------- .../src/audio/AudioMixerClientData.cpp | 9 ++++++++- .../src/audio/AudioMixerClientData.h | 6 ++++++ libraries/audio/src/AudioConstants.h | 3 +++ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 0f51bd00b1..1832c29d1e 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -339,21 +339,18 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) { } }); - int nonZeroSamples = 0; + // use the per listner AudioLimiter to render the mixed data... + listenerNodeData->clampAudioSamples(_mixedSamples, _clampedSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - // enumerate the mixed samples and clamp any samples outside the min/max - // also check if we ended up with a silent frame + // check for silent audio after the peak limitor has converted the samples + bool hasAudio = false; for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) { - - _clampedSamples[i] = int16_t(glm::clamp(int(_mixedSamples[i] * AudioConstants::MAX_SAMPLE_VALUE), - AudioConstants::MIN_SAMPLE_VALUE, - AudioConstants::MAX_SAMPLE_VALUE)); if (_clampedSamples[i] != 0.0f) { - ++nonZeroSamples; + hasAudio = true; + break; } } - - return (nonZeroSamples > 0); + return hasAudio; } void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) { diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 93a51b1df2..6a25027310 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -17,6 +17,8 @@ #include #include +#include ; + #include "InjectedAudioStream.h" #include "AudioMixer.h" @@ -26,7 +28,8 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : NodeData(nodeID), _outgoingMixedAudioSequenceNumber(0), - _downstreamAudioStreamStats() + _downstreamAudioStreamStats(), + _audioLimiter(new AudioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO)) { // of the ~94 blocks in a second of audio sent from the AudioMixer, pick a random one to send out a stats packet on // this ensures we send out stats to this client around every second @@ -38,6 +41,10 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : _frameToSendStats = distribution(numberGenerator); } +void AudioMixerClientData::clampAudioSamples(float* input, int16_t* output, int numFrames) { + _audioLimiter->render(input, output, numFrames); +} + AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { QReadLocker readLocker { &_streamsLock }; diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index ff4143cf08..e543c51b0c 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -21,6 +21,8 @@ #include "PositionalAudioStream.h" #include "AvatarAudioStream.h" +class AudioLimiter; + class AudioMixerClientData : public NodeData { Q_OBJECT public: @@ -61,6 +63,8 @@ public: // uses randomization to have the AudioMixer send a stats packet to this node around every second bool shouldSendStats(int frameNumber); + void clampAudioSamples(float* input, int16_t* output, int numFrames); + signals: void injectorStreamFinished(const QUuid& streamIdentifier); @@ -77,6 +81,8 @@ private: AudioStreamStats _downstreamAudioStreamStats; int _frameToSendStats { 0 }; + + std::unique_ptr _audioLimiter; }; #endif // hifi_AudioMixerClientData_h diff --git a/libraries/audio/src/AudioConstants.h b/libraries/audio/src/AudioConstants.h index 38fead87f1..dbbe434915 100644 --- a/libraries/audio/src/AudioConstants.h +++ b/libraries/audio/src/AudioConstants.h @@ -18,6 +18,9 @@ namespace AudioConstants { const int SAMPLE_RATE = 24000; + const int MONO = 1; + const int STEREO = 2; + typedef int16_t AudioSample; From 7fd531541c3436fc75fdf1c009fc8cc88a664025 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 29 May 2016 12:08:10 -0700 Subject: [PATCH 0256/1237] simplify --- assignment-client/src/audio/AudioMixer.cpp | 4 ++-- assignment-client/src/audio/AudioMixerClientData.cpp | 8 +------- assignment-client/src/audio/AudioMixerClientData.h | 6 ++---- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 1832c29d1e..03bb32cd53 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -340,12 +340,12 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) { }); // use the per listner AudioLimiter to render the mixed data... - listenerNodeData->clampAudioSamples(_mixedSamples, _clampedSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + listenerNodeData->audioLimiter.render(_mixedSamples, _clampedSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); // check for silent audio after the peak limitor has converted the samples bool hasAudio = false; for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) { - if (_clampedSamples[i] != 0.0f) { + if (_clampedSamples[i] != 0) { hasAudio = true; break; } diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 6a25027310..d9fa7efe64 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -17,8 +17,6 @@ #include #include -#include ; - #include "InjectedAudioStream.h" #include "AudioMixer.h" @@ -29,7 +27,7 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : NodeData(nodeID), _outgoingMixedAudioSequenceNumber(0), _downstreamAudioStreamStats(), - _audioLimiter(new AudioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO)) + audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO) { // of the ~94 blocks in a second of audio sent from the AudioMixer, pick a random one to send out a stats packet on // this ensures we send out stats to this client around every second @@ -41,10 +39,6 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : _frameToSendStats = distribution(numberGenerator); } -void AudioMixerClientData::clampAudioSamples(float* input, int16_t* output, int numFrames) { - _audioLimiter->render(input, output, numFrames); -} - AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { QReadLocker readLocker { &_streamsLock }; diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index e543c51b0c..17274a1519 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -16,12 +16,12 @@ #include #include +#include #include #include "PositionalAudioStream.h" #include "AvatarAudioStream.h" -class AudioLimiter; class AudioMixerClientData : public NodeData { Q_OBJECT @@ -63,7 +63,7 @@ public: // uses randomization to have the AudioMixer send a stats packet to this node around every second bool shouldSendStats(int frameNumber); - void clampAudioSamples(float* input, int16_t* output, int numFrames); + AudioLimiter audioLimiter; signals: void injectorStreamFinished(const QUuid& streamIdentifier); @@ -81,8 +81,6 @@ private: AudioStreamStats _downstreamAudioStreamStats; int _frameToSendStats { 0 }; - - std::unique_ptr _audioLimiter; }; #endif // hifi_AudioMixerClientData_h From f65f99a3e0bff366e422c7b6ab7a959cce779714 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 29 May 2016 12:34:57 -0700 Subject: [PATCH 0257/1237] fix warning --- assignment-client/src/audio/AudioMixerClientData.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index d9fa7efe64..20003ba10d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -25,9 +25,9 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : NodeData(nodeID), + audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), _outgoingMixedAudioSequenceNumber(0), - _downstreamAudioStreamStats(), - audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO) + _downstreamAudioStreamStats() { // of the ~94 blocks in a second of audio sent from the AudioMixer, pick a random one to send out a stats packet on // this ensures we send out stats to this client around every second From 1fb8f659a7c6908282e82e83e39561a4b47a4d87 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 29 May 2016 13:33:24 -0700 Subject: [PATCH 0258/1237] add test content --- script-archive/tests/audio/1760sine.wav | Bin 0 -> 2880044 bytes script-archive/tests/audio/testPeakLimiter.js | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 script-archive/tests/audio/1760sine.wav create mode 100644 script-archive/tests/audio/testPeakLimiter.js diff --git a/script-archive/tests/audio/1760sine.wav b/script-archive/tests/audio/1760sine.wav new file mode 100644 index 0000000000000000000000000000000000000000..fd2701ffef5d68f771e81f1dde5ecbb5d875257a GIT binary patch literal 2880044 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 miPNkoYWQ(@?+hL?zE_TYTe$74 z3KFj+=I!9Ov)5AbuZpSfZ0=2PzFqZkc#qSR1pgrSCb;jc=2zKE-izEwew-#cSKt)= zy3Wz$$6+6R8}h`BCts@B3-D9V9ZoYVd~{uFV2g?kHh>co-+)lTp#kH zo4Sm*4Y*Kx*l*|jtcI1}T3@7jdzs|hkr!P{-X&u{XymWJLk9N)+*)<6kiSB{-N-Yf zY9CQvbp5CYJ^bZdvENyFmja0UG0B$QWhr?rzb8M=t=Q;!zXpu(TsLrf#~ZaR&($5A zv28l>WIojA3}#;{?m^DC*9cEW@kQ-($oL;*?-IUOZD(IyI9hYkYGXEr6`Hte@J+l+ zeP{k3jFVpKX+19rJ_Gli4G%APeF?P336OK8=I!kV1$!0Jyxpc1EGdfIOnsdCPMgW+ z<*Vo0Us}FAGg;p4t=bpT-kIlD*gK~Qw-($F_IW)a-z(($J}aN4=lV7gU-a(%AiS1p ze#PD;?s+ve4HF*n4Vqt#iG57&LH33#A3c0tt;lQHNc{Hc9oG(g$@8N853)BrfqL|l zY>!^3I6P(N0`d>4Jx;%UGfq|$p89hR^GnxZA|7Rk^-eeSXC~udd{q*n42k8Lyh%#I5b=??HX% zeR2O#Ui2#UQh6@QdC~5c4-!wwyWP_?7sVc@L3n*OnqP6Ap|g0G)cXpYB5*$@49%Og zM$e1FkArzTax&cW0{<$;;%jwU`OCV19TUZ40-u-SGi)S}337et4>G5SJ-p1VReaHV zBqsyz2mFILSJ*p)`w?7ep?N#_?JbE@r1%VO|JtJmw^s4`F6V!jzCW?6eh*?UitiQn z&d9f8-i~vH{$SSuA5bq9|AX)@f!~fEy_#P!uMb?cdVTMV`4xD5>`M*LawQ*qjPA8$ z|KNI)kDmDq?1_VK0`n^$yPg-$)il?4dhZ;zd2-KL4l#vt55n(^d$3mCSLk`YX!+LK zuyR4xl%0F^cY7jnAldI6HSgDe7isTYSKDybg}7=h?Q+OwZY{VU%x|v_&7OF5$n)Ku z9X`9*?MQG*`|P&Tcm5&$m;755KcARzZ4b>E*68mm&Wmy%XG_)FG8Z+x;hN5Y#QVzN zAshdLdDT~lC$lW`W%63$9`qxgjPjV|39pa2wfw$PUdzkGXW;zRYVn5iJ;?L+1KJSr z(JQ|*d=tv&h5cYR`d$@PwQ+0cniXew+a5P53YMmHi;z?eIHqpWK!B?J3&Fi{h#83~nv^ow3J> zTsGSBh1GS{T-rM;ZY|Fl@IT1!cICBHb5Z2_cI$Hn!)u8i{TSH~-V~pgg9GQ=TM?&7 zy|2(8H20m=evseoHr?mdgZK=tmah{AN?vqE<@-*jB+sBY+vnzt^E*3wL;v>ebE>OO zzr8=LC?WF|%JnHegW}c(>0Zk`y*JU&sfcoY$RXE>H=N(?;1scMVv^tKIcYR+w~;q| zynL?|zg_j{yW|~~oQy-?rr~+CcgB8@dC1X1Ihe$&LMk+tngkkcvF|x=^pge_s;lUl@~sudzY{u zgm-B*`3F^=VXA8jJ0Cr`wZXYrYO6>n4ph?{1x`jwm$AO zXZWw=8Qg_iYb}jG8d6yz=L-BQ_;FTIp5Y|D+rg8G*uSi3W#&t|CvKYXuWXcOP`*^= zi}L&m=jyFPK0AI}*O26Ioo{)Gyi4FS6jZltzq$X3(QoLUIMv7bvFCgAZnx50^k_mW z@|bkZ`>*T=ZR8Cv2&;*4(7+eHU;H@b?h~(_KT%t8KL5w`5B2ZWOqz>2+wBL<{Hr~b zLq?CD??Lnj1J?%XJQ?J#Ds^rxa(#*PKRDUe@50{0e!_tqNxjt4!YB6movgp^+orq6 z8sRe}(fq0>d3fLHJ>Avu>dj+WWg+4PFy^AJ#QoSxz6obNFADAl`{I0*zas`)l^Ofygv2&DH!o?>3YKgkULW39MKotXFBP03_D$e@_50~uo!@?T&PwWefm_RY27X`h zJ*d1(@TJC*1?+h8zEmuJs)>dYm5yi zt{S`mb+xv$&UCKq-&g3Rs{LU9h%v+${Y$uN;9n{Kpz@{unEngpMe)93Un)3|p29=s z`4#5v*yD7~OHXT+=xjMbTs5sU{^$(Z8$kiW|5e|Gd(zhCAgEqpExKk+^EScYZmc2lY}f3TM0AJ)QPA?M*!| zz6V=NFBNl9o?i_}*+t*0Q1MN$FO~TW;A|r=%KWS5dmQEz;asU+YMyXEFc;a) z7CtyTY08@;SM>Xsyy4)tv(Iaka3DD^3O)npkU8JZJ$mjt8+p;@KCdYqvj)CI=jwKB z9q~oShb;4sqxTi^qI?g6s}{ZN5$ZcL2eP9__NWM7P3G;&V}d>ozuQ%w0lids0l>eS z>N>agHr+>$cRTjZYe#jZoJ`QkNA^_|_R5G|Hr(c}NKVG7ubasWp#BHJDT+Nht8$yu*`8yE7Mr}5=jV9({Y0L)jiz@y_RjT5SJu6? zfzSlkjbh$j=G^V`wKxlQ{)@Q@W}n|ogDUFt?0NO%F@wdA}g?m^D=jUlcY-tBl_ z!9S>SGWcGB*N1zsu6A0BX)ys6ATcdGPpI44tWm+Mn~97}kF-lNAoxVP}3j3vv4TE4T^uUtT00Q3JK z=Iw@eN%c}4rH|vJ`*F$_u1`C+ z!m2RwJFD*%dzY}sLEo7@CeKjMi_aDKSLjV}j~@Sn;K_ii_9pqA(W4LX$`9HmJ}>4W ztM?W6alqM*w!E}FDRV{99O^s6YiZvbgzsCFPJUwc@G&ncA>;~npX7%1RtU$cs z>DtGOV#62st{K&pax%>8dpKi(?%_pmqQCBmdp+dasAnYKeo^LEx8!{lO!r^~&D+C^ z_a2ycW{mFPRe1*FkPY7ia((XE_b0!U(2Me-$_s$^75MGwr6MoN`Kv~nx5LBxS$U7V zG}=3Z&j6l`;xml5%k?p*2;2{F)!rdqALgRGcYbTfn}^n&&#!ZoyeQv;o0e=7uVoX> zMJGv42Hz{*JI~U&YKmLSJ$mNWD!;Q}Posn-h-tD#51KV__|3T!CS8E&N-C`af{uT53l#gEZ z2jQc?PUnjEgWZIy#ysSAM|Jg>-sU#V+s*SU^isU!x)*yXn=0GxE zlsQHCUj0Sf56s)k#S741_JcQM-afRuBju21QvOPDiqLoF{h;y>URda*H40Y^y;SsZ zRKDGnJSI=3lS{aPPd=|=$}`|x zac_b-kj%e=$7H_c<>d*P=_cTO-Y{^2k3p zK|8wC!ZQo?=2lL2Sj)-+6VGB=|4#a+=xQy-_bp1;Dm!knSP(r9Td z^DC7XeNpGC4T}3V>Wz>%@5I5`rgP=6>TAi#fK$ZpcJ6s;;U6tZ6E6V!QhooG*SE2( z`k2GjAlI3_cS_GIKR#LebkV}_MZT*?^`Liqz{$tNH(_uf!TnJ4D}z%6Un)3|_#gbY zdvMQzxo5`Llyvxy@ELgTd_(7cG&CK(*zw5QC2h0sNgkWf+42$jy!c#Ue&sbXUC)cw z#cVY7okOLMqjG)dc^O^+-s7-$34R=KYn3-VopQ)?EFso<$|1uGP)P49?41n{ufhG` zxhT)ujrkSw?dW;^FnWUDk8{!%@27k_eDv54?xK58`Mhusa{elm^6lL7+C=?9qV2 z+p%}Ht^CG1Lf-A*U-2F%d(>QCD|wgTo9HfFHRcql`4v2I247ThYvJK7(D|Y~7iIp{ z9zBN)J_F~FC)={^JSNJQ%K0mUhiv8)DPAAu?dUsS(e@E%dxQSIdcf36g%{vg;>jpZ z5%^c&6e-^XJaO)BTdKm2e^TBnFHN{=Pmy;ij`ldnU%?CDvfxST7f=own6wNPo~Ky$OrFuawUV9uv$(pN(&rpE0vg_JcF2 zH&G(~LAS*8v~J`D;9e>?+XkNjTs3&Z_qb2Ec7ZtCr^sUh?^5gRS(3kcw)f6HYy{e$&M<~w#ZtYig{Pt7CLsq@i6w0^%AoHu=PKOh> zcBH(ou85Bw-tZ2zAKXU$L3m8e_aJ*M!BvZ-yeRilu^-HrJ`Q-u&(gfTh~^C7$?%-v z@sw|*N6+VK8_lm8hHv)C4y%i4iXWq0S=df`^zepXJn^@lXFyH{Ts7Q-;MO9~0KYSQ z^y*yol)NbJ!9UjYCVsn1Qs~+M{eKWVWcCl&q=dbX+;y#Ybi=scsbB6tkmV__xeV6j>H+4_kVmsf2$A!tZ ztS-w3$9cbQm%kb=-z)eg{K*sND1PUN;@t5UCB3rIm29?0|`%@ zKfT*k4jFj{@Y|mu9EvD7Jh|7b#s$UkE3`w47E-<)KKfdjw;=E zK12IK%-hZLt0>EK;?};kV+!@1F>mKwU*QrP`6lv$LcQ9X`Z&z%i?F=9+?ttM^bYyF z%sgb|WOy!$zB7B5I4=tC5=(0XXN^L-rh3F#k!lk;pTG%PSIl#uM!9HpXh%= z*H8R|^6i*k%__M^{LU5PwLE{~^=o@%&H!%h7|lWB{FT#)2Fl6II{umXyjoh0lP50L zbgq=o>!8m4KraV|+amLpG!hRPK6>!446mibN?&V??`A2<->2 z4Ov5d9Goka7v*_7_zV%n*VlAg@qwku!g&VsT$JDK^^_L{zkMnBahTVK9{tz!z3L-5 zWW3v%v%Oey$n3QQS8W<`AhXK?C5Oy@XL!RMull)$_ue*ayNR>SdmMAm3;RKMOdN^( zfjtg;mn`0sLcWQ5F0O|5gW#&==zC{)m%x(&XZz1Jk;Qus%sJz2`X6LoAI}-kA8cw; zduR5B^IY^F>ZKyz&i+AjUbK;T$VOfid3A|KR(>-tHaX z>PT}@b*|ts2_a7$pDXyCIoFp=9$xjnx^g0uylTqyUh25mpS44ASECz3S5Le?O8};khVy$oxN;LLT0OdcK|SLGVS{mx}q7;?}+^97y%PY9t;q_RidQ z<{a{+bq*`1Sr?f2?VBWr3{PB=a3GC21N_dhrF-n&?W#B7*tcnTzR$+6I=TmOu2P5t zspbsE-dTBgKPNuJA2e_09{p(Ai0ZL&iOb zd3y@^Ca@nwUR3d~?sU3O{5S=~Z{Hr7D}9`Tx^?F#ADU|Nc`eIKDSCVVpQkre-`jpe z|DQ&C`JJ1SBwkDKMaNSwmHDEj#I0>@i`VSDknPl*dHvahR*dJukyY|6KP! zO?d|7McE60oXngv6$j=N|3&AD{mv@i?j!jt_??-n=1udfO~NSx51IEk^OyC@D3jg< z_Rh$+D~|~{MMaa}lReH2@}>Hv`Ym;_1eo^D{B8%I!6~LNewFs=qDAy>@9YswJQ?f< zAI+FgygsK@bFzYVzJGXt@cP(~!|yBbWVn~gy$N^$FlSIX8TJo?-;Vtt=2w;4fw(`S z>q0Xo{yk)HcPHJ$tL{PMui%?NA4kpGe_1#|yAyAV$qp+Z53kCLB7cSNmFi7sM`u*- za5~#_%+TUVsWThrKS%HOA89{`xoAOkrT)D_t`EKmoGZ>>@!q*OvS3NQR^J9PW&Y8 z+`>+?Q+3}2zuPev1y{}R@HRHRDE>j@8Pq-4VN90+6~t95)$^i;f3WFnUF{nk^9K(0 z{3GCoV=yy+vCCyP`({=(JkbOgVz#s2KzovuJEty zdtUfn!RG~TE$7=y$!j^xa>ClMa(>n{yS(W5kW+dN8UKTri`LY-oNG83y3K#Bdy=F0 zCa`w~|BAf;>`VPsa>(m---P09ze0T+^d_)(_NF|8m&vthcu-tEd`!t?f-`!k9(;q}!MpJ57l;+i@!Pp0|19p0ri=ij``6KDP(JA<OrdC>pA2g+T%P-ejMbl zB6MCK@(fn*iPWPvda2B3XgcdmeH_lW^ZRO#?lA#RhI4%%Q(kmEackjAeeg(VNxSTJ z$)gf_+V!0)$VZHb=#M?UXg_F^zB9Z4?1@7khdpt; zALN{j4|#a&J9Qv_dr@S8sXurxpqZ-%{~-Pcu^$Xr8@#RY zpwqcp;;KETb0FbeVlM#Z4E-}}m^sr)#>OQ)pz z=jN4Nv-9IP#pIKRSEHU+Cy&53w`hJ zv*-P$d&6C1-i};fxuw^Nsx|YA-#@V6%xij{0p|)lWb_BKrSHtX)YkHDhi`)KL3o!^ zbZx!rnPj@}lU`vvGY}Jwi)!j zDidBG-dE_+cOTGBda2+P!RLkf)$`q(sF!L#zf$)gbGDPGbejE($w!a>^% z*xJ;`;U2x>Gr%{oa`D+Y6Z}q&F6i&pzEFIriJ7C9&$R?w8_4GckIA;WI?169%{iH;Qoc-RZE;S_q@EY}qOGXs zl`nh-Pwn!;cC*(^89OqQIFP?ePKJG{ydUH}4$c+)&bSBB$9bG`eVAXNM-OhT;iEV2 zaa4~Ue&@k)-|P7+a3I0`01w%|H*wRH>*M|)@15~(htG?BsXjCp4PE+XYLIy1%;&0r zxN1vu&h}05hW8^L@>eEK5zpJ>yx$OBpPGxpyJXDUUB%~xe7iX>3LY}{g9nJ$w{~I` zdE&mf*qMCv?c{yMoFe38z`q(Ry;S9QR{X2?sshOyzAnu%(Ou_@YLsvPLB3Z_^Pi3X zSWBh-;E$tobY9=8qGg$*$d`IMscPM1;a}M(hrB6hYh?bC2CZ<(7E=z{-V?_;nHl^4 zJe^bBR`Lu62NHW6#gkEdhOgy+koP$3U2-0g<5L)RGsa09OLI|`7ghZBAJZ%79&Df7 zI{TfH4oAMaXp_EkP3ZOL1LQGb{*}sKsXdPJT7u8uc&?__x8wSO!#yjhmwHosF7BJC zN&5TB$hRN0zGPV_+>g+c)$%_$pZtSK+Lm~SX1P9t*Jqz+nAQ7z>N}fz6P$0ayY6@L z33|7~Kghlb_;J7~%I^{a|+hM7f#m$v!x_5^A0tnkec!o& z_Jho4Q15oto4~sr{C0!S0N(_2)%H;ing0j>xgJX1aI?=#@kN>6KA!UJ@H;aPS>@Zg zk7MK+=F#4nz2WNJj_=i9(Kq#+4Cd`&(su@5^i2BC`85?kpLqS+Zud-melsP*$c3Qczxj3x?7wQ*QPy? zXLy&4c{_XJGU*=NL;b;Onz!>kc=x|Lz}D|AXjFfCKr0^l=oo7Tk}$((~FyK6>Sw;Cm3giJPHW z`nz5EabDKX70<7jhnzO0Hu_M0&S3c4|*KnXS~D$R=(r_XpV<&i=t5;(nOtS89)gx#-jJ zj=JA@r_+qecsu`~%3pC#rha&lPd52d6`#TIm?%z>d49$3cI5h)1Np0-L+*JOPlo5B zoRhg(JFVmTfg?PB4Tz$5`!jLJqb7$e@m@Z7eHS0~brO~UaCKcxv1jx!3&Tj=jwvyN&kcQh#x2VOv!<0dAB=TTP6jQ z-`U|DdZ}6DcgFuc z@Y{`hQ2D$tXMleYy$R+(-Xi~CNukGG-X-uEmQqee<=f}XtCXCK(H}&fq14i6#cyk3 zir+sl|IC=0vJMZ~_2~J3(CE>>UDAqt^t~(<)F1r2;*!kURc`{hKF%S71F3Q{_vfv@ z>)pPS_Bh;k9z^>=^l^;;!SMabMN8#>5dA^)ywIcPTpxN~o0c?cg-bR^7Si_$erI@i z@xDU-3VUbnrBq)C-|M4vwZRA+Eei^3s%nDHtDsYXM4Wq6k2IH8dqAH z^CaDa=Kn!>Oz!5lH}5+e9LQOcZ|8YCdJ~3s=^j&_VP5f{InJh6}J|-zD}~o!MVzhcaG^s^LF;7UOSdw=9lZ68c1_d#TR9-<#y_&I?Mbj zE_^=a`oO;mKIuWei74{$s=4T6bgqoKDDz}41q>rj(Vg0+vyS8qXaAt#;YDwvo9>Cb ztow1`F+r{m^LE9p{ZRNX;hkrBRdUEz zh5PZ{=-2(u$eh7(L8{CdO2reW&Xtp$7XaK3o?kV@d*9_tRXuv{rK0DB{1tO+*G{Y< zJ_9_wPek;fcl&Pf@G9;H-dC!RgIr(kz!yyU_7mizUrJs8^}WKm8fBTk>|x4Zh0vUV zeO|G&cZM&Od-R->Q8{GH+YRmqa(%OUmkcYU9{m=&2jTOAHynE$K3D(S@7z3xY~&e` z7j;Q$Y29V%O`bTu2lsU7HNbsLAOD9V#>HNuxhVLef9Tu~`#uhM$cj@0zjH_G58C2y z$K=tw-Jx$wxA&@or8j|Is^Yg}@60?I_LvNg|5SWlKaS3${oobaJG0LVc?R}*y?ef} zu2J6)qDRlZi5Xt)2i@tky)OOy6zV&3ABR10v-ek?E+D@%JSNC9d`9!DRLLPDFKYCi zja(mk6Rq_9AokAQ+Rt*2XwBGL!M1@hK8nL^1k{j`jfa{wDGYI zNBqbC9(uQfQ>5k$?1^K}Hu$2r2ifO^oDA=AH0pVQCsRTFLH3y7-46a0_JgXIx+whM zq7>nNsC$t2IEpVCxYSqfLFTu=KdeZ4UhJE|J%}EC-Sz1w`|sOB^Y#}k-&$=e7i3MP zKF&n)rE1=jq(8|2gV+x$4{yM>V9GO;3IB@y&bvGOXF$s_5Bv9x@QVFa`&j%qs+S7B zD7*kKQ(hE(=j#=h^&B$#gO})kki7uPKWOgb92f3~ajuxpfd4_|qvvx~NnEwW%u&lD z$s5kTRCNyqQXdEBO8pNaCu6@K@n%NqRiqVIb_}sg3mDA z^HM;BoU89Be-%%@)BzWEQQw(!G7FYHoKZ$TFZgjd*S92m|Dtp)H@>CxozWiz|H|mm z8-5&cYcaoK-$Wt#QkT#@i2mT6>(fs@L3swV&#UzCfD7YodBpvg6mmT3S)Eh#qRiW! z$u|+YwndVo^#kdpRvZXFGoj{v>3Q|1cl(EN)y?vv%tIcP(8E%m_*44%{9EK7#C~uB z^>LV6Ym3PWEAp`ocj(*Nt*C02?ss-2uVp#)y#B36k3P;Rnzt8J|3 z0(Jjj9&w5Sy*dcL-SAp2XqFev(|Z%2XwSquEm$>k>m<*ir%fJS^d{WIkAwXn=A!(* z;-1%d?U#l3H}g&Wod2VIuP|qTFZB!ZnEWwht@I|4LpF0iIEVZfd3e#s!Mh!P9QFbX z)9*p{4=N7iW0dRTcRRQr%x4IZxhV4)4$!%35!1!Qtpx`%DEBSm*23=`AbaPj#8oSn zo>!{y`qq!?O#8t=@((_ev0&M7%W-SN%9yMvly84SxV3z)ZfUmIaLUQp@10vv9|t@c z#o6XP4)fd5o4~uB=M3CSogsdlUY4@NbLnUE?^K*WF@g3t6Go=eJqUh#J@p4w9|!lK z;$Pu=rM%&szuH259R6P6TyY-<9^Np@^}V!wzQx~av-@5le^nH;ney%VOSVLA4hrzP zf6#5+=XH_z3{jR>XfFEp{y$D{r2gQK#FH_;SN1+H><7VXUv>2%P7jvQWQ0dEDTZP@cr(!*0cA1vHAXo z=lASquXW$o<+xzr9?CN~&>rXLiiCwrwfPnI=rI=^LvsdjimFZwzW93E4)5c|9Rv{4||*A)br|2a|ZUrVa~ukuMB#( zKTPi{^l@_L7tZLQ9P%ETxA!Ohm7V1a@$ll^&gTj|8F<5)hde2$c6{~_SJ&*up9~yG z%tiI@;JrSs!y|li!``L6v(DK@4q4yhfPaPj)u!TkhZ|~kG`hNG5BXMmUvZwHMEryN zzCx}K{C1T?=Jyr6mhi;gj_)82By+amOXXfF{DU8+xEu2A=sUmYadl+2{10A|IRkhy z$n`C*t(3iUxut88L+aeE;idPEI-YN8p5c}^Y^2YPkohqk2~Fnb6VKA#8NF2A4+a^$ z0GPMy{XxvHxbIw`<*$&F;rtai+jF&dyYfve8dx&=-RXVEKL|d9^?nc@6Xe?!SB>Xa z+;>j)n-YE|c4T6cxs|vdqlkZXm;BCruDCbxQ&w!rfg>@LzrwldAN)-8YYCUk#}aSE z-O>0A=nrzw3;w}s={sLO>(fx}yn*IdZo+4{L-{M!qmLwRE&FjkOMXW4T9(m%@c-{S zCvW*X<@#*agWonjG})qV+3}n}?Yk zmTidKO*!NWI#(;p4-&uKNqERS7lkJd=Suei;N7lzUUjm^VQ#H)kMlR;6rq>;apEQO z1cT2D9uw}RvM<$%`0dya-f4bayq1e&?k3#(FZnC@CSJBIC9azBe~`~ry17t%sryGq z4V>-R#W25u$7FW;?A$`~(FZt2(|zR@)h#~9w1s?Lu^I<5{M?h(6~x(oiTLfvU%`*V ze&^LPXTW!Gam*E(i?T1ZiKr=O@RB+zVvz15p&wgTFvK$o)$P` zDaYTwu*;C^Dr*{1?47}r;a)29MZ3G^5TBuz`pzTn>*F9V3Z4veYti$< zyq(WgA^E)8OcTYs#60AQ4VTaUR&|*?y!|ZWlY4CJmc6j7$H~)m_h>H4-$8gSnJ0t& zU}ewOnwHhRcVubF4LVm{lI&CGZ4IWL*X`z+!joZdIC`n@nDGDLGZ)KGgb-(&^9<+@ z>V0SMMOSHioB`4w5-s@8L# zmKPn8b~f2T>!tGh3Y=~9aqh+6Gx?b>N^gR_mY2^qR$VOkW>cjVPv&2e7X@egr?_ME zZdV>&@Y@&H9y+q9vc{2UA@1SvyqwXv0arF09jh1I{AZ^3Za~VWl?5ls9}7`Mi*C2M-y09QN=cf5qGn&h_c{)m-rcBrH6(V!gT8WTSZj z(3|*`{s(8uyq)t5zi7S*%&!z@+eGs#_)?kA08iX<`3^26Ztas7r?%y4e9_@n`F42X zI`c(uYWyqaA>)08a|NEvEtxZPkdL18qTp=5Pwy+{^}X-Baj^f`bdCFgd3!GL`nnNk z`vm)CFBM9ALRX@;;NxH5pCc=KIr{u@Kc(P zzA0j>e*^VWPtf}c97uS0c|X`F?{?c{)%}rD&NjrwbiGB>krzU^_(&) ze`=<@uXOJcdo9tUPulVdaUeN=HS7513nf;ZZQZ-ngXW^!E=(iuk~P1beW|%azA^Mt zZI-1+?w;yH=PJD3a>`U*PWkq()SGxje5nsQwGFZvgFX&?UK7k$mv>)~72reuL3nue z{UCVAI9F~b|0&*ZorgRzVQ}<7@jFjyT2xzkB*ws#;qPGkcXw#sK8CzYd|$!Ci#$WN z`}8)iiw91GkRJ!{E9TY?sSY{UOg=Bn8Q_~x9LVD7`=pPAoA4Og-TP7Tt|?t;E?Oac(F1m0c>0ejoSH#CdhpxNFPcnzQRYB`CxgAS;?}AjeXIB;INuK6 z1pB;N$ZLr_1NWW5XGqzSUi`-48#Q^vt^Hcg74lc`(U-`%ilE*EIFO$uJ6Rkx{~+d~ zcwh0mo!?i;i?Wa2yW!f|hN^n%d8xfK@(kcWs+GyVs8KM0--d*VtY zFPceS0NuM3tnG0WPX_OH<>6iJ=S%&;7V|ygY>y`X)h*iNU_a=TYD0UR#;gUh$0;-L zuj(i-+GOZEbFR;IO*1L;AzK0a4?UsZF~5S(>m{v^v;V|17yZePqx{Zy#cMgzr(oDrw|mX)l0)ud)tiXz#3|xA z1N?*dAGG%4@OO}NedtZVmzqiWb|15ixhcMAe$EX0G3|q#q&Lwwe@jM}6i3S$>JOSE zFIq!fwRGY$aL-FWSJ*qRFJ7nlalmiqzVki0uZo2GQMhWC4qlRZ`>V+h zNlu2}?cl22iMSE>N#ggEX8@<@v7raGIm1s`o|=z-ZfV<5$McQNbKKqbKWnU`uMI$s%oZ=yOr(EAQ>Um*5h+H51&S}CaV$L@H2hpQ{&SXzKWW~Qy zp19AG?XB*su1kBuaSSS zyOxuI#{{{)a*I=vP3ruuvr4-hYn3?zyx}S@3jP(|SJ)3Cf5m?1|7iRxvU^ib=KYkf zH+@fe2JX?rm+DG>9F;@1rTo=9(#OHM0tZt0og=i~L|UZ%vS&^IG^ecmVUZ8z+d0=4 zbvUCqY0InBAH0`Zzb%kFyf|07FBN_qea@ivgFF|--Z^Q@=HiIMKg-^^cKk&0S|Wd? zyy4)g!E1?kJ9_lseq>tt2kqoLsB?-YkIfMOU{lri#OvdHyL0xN^lrDMJ`VEj#`hJx z0POQpzSIhfW0C{?587!wWaJqXr|6vY2Xn=jI%{97f%}2^72a3yJ7 z`wDXg@EMw_ejvUmxF4GWdM>D=`4#4(g~D%V?-KY7j`Y5ID(z6tO5#AWZ=zsorg#B1 zkbe*!6a8EvCj&2l%8O=Q*wb>C_@V`2?+{-!cU8%fU17n#Zo}{N*+ZNn%ti4(sQ6cy zUn#yQyq4gLjwEia;%uvT`?JDl;5Bfe zzJvO?!vEl)U?=ZEL(6(jYKpe<(Srl2a>xO0JBR(%D$fAlgyQvK-i|qg&Q(Ltt4Zcp zae*skeg!`c{Lb?=?g#eH$TPr4kMH2GryZ})a$H0?WREDD_+rxr^U)P67A_CmGOj;y zYr!cxR5Xw{MS5@I8OmR&c{}&$IVY2&<@$n4=WgvrdmIlsSLi!~0}0;*dZ`nL`(f;P zA&1QS!B>;rEgz8rg?j`><8JqgdB2r+B>5^sB(SE!>i`)yvO2i|bZMfJTib3fGlDkH#OxN4Wr-V%?Ar)6BSE6qjwNdD@_kvsewBO2qX6YI_1 zG;eqD_K^REUXCQnztSM*fok~QNjr?kGam*m^AcV-`b zd&2yf8zF!7*-1PZ`}1u_!)ZUr_Z7SV;C{e2!E*-HOZ_$Ow}>47t0Q0XXw&@8cwZ@B z>Mg7JRqq8CiO+D4-d7zjwG#&tc?RTUFc!D+YOJeSe0aU# ze-IqV>6#x0oNd0ZR89u(EA$6v9iP7U#VR*+#xSR6AGJdC{}NLk3T#Gp|qYr5ba{@bIEX&wSA!M@PzwhG;$d85$3nd-Ns3 zRZBB}tmUslHDBrzT7M9{KK5GXQC@UP_}Tx5f7K<`A*n+94kFJ0P7%D8Pmtdk^Q-2F zxBPz@Il{xy=99X<)Jx^PGrZxKsgEW)`zfvAvoGW;j zls6n6le9=X>JKVT5ziS^UX;B{oNouWcDLMD;EQ6;fcF)6eaJHuwCuRxFMNiuq{Wt3 zrSE*Trm)dTe5o~y(&;;h`>JrNpT=h}(Y#&dulnYntei`82HVHmUfMNd$NaYV7Skl! zJLA6M{op0(J0D8%pqxzi?0MREP|u5AHovOP8Q|fK6aE#?8M>y1(f^>s`QMvo893YO ze^7Y=bf4F0;$IEStSb7m=A&2M@C?ac6;FR}bd>O9@+$&!gNdsaEZ;%=4+c5Tryl(P z;mKg{j9edc)xxFkyqtRU9kRz^ZY_HBeuj5D_Xm}4Vs2^h)(28MELD~P>weCPD|zoo zeC_zAD)E@`yq)KwTP4>A4y4gP_~oYhf}2&BH4pFRfF2935DyuixLq^uz2x|~gVVkn za}9eOzOUH3RA%=L^_{_i)bs7^ALKj(-tFVb3(%Tce_u|fXi4#^R_)!c=i6uPy}GR> zH9TpNv+lI+Q_%H95V6@p7cMc`*D7foQyhG_LOIUFIDHa zgZr_@&opAE`?R(vs5b$=DEv6Q$3cIv`DXBad{KRVgTO`h58g z#s_8*4_Wzf{w(`J^d_)(#(przJz07aBNFduediqVd3EMM{z7vG_y^gKqrBnpd0{_z zW8~k+YuV=yIFNN`Co~*(P8~dL>_*D_Rbvh${s(((bjhAq=6>=Ybsgl# z0Z)cGMLv@2`_s_*3y!9kF2PDrKzgtfT0g~ z5476jY^Cp@gU#`}zTypM?~*=eI7I)0@J)aN$vGKtKkOfObb9wj1aUu@FZ%D~r_&CS z7a+un*T=rp?-uz7eKCI5kgl$Ujn^fI%=Z;~sj5GSxhOomym#*GqlY&fejN1Z@xI!i z^>Oma=XGBE&g~kv7ITJV^5YCiJ0rdc=4`9~LEYz7Cms{dGr$Y*zXjiII!Nz!<`k*k z1pLn6GklQvqj{Y69Xuo)$QbbgI3(GpzOglk_RdZ8ZhukcS1r`@nnK)KK34~2k8?^q zCKW|b(YXq8>F@Ox`RJ=xY%%AUoZ|aN?V-Ih{5Zdy&aVi{ot+*`|AWS!m-2aG-p=`} zxWMh>dV58>+`Jii?$6bSq(=`<5qw_8{FUkt-lZHeeDr~Cx0)T#w;c_m9=(0iAxr;t zH?v|&%8$g=df&%q;P(~w&JH&JFx*#|Gkmk@`%Zj@UNmQ5Ph39z4?ZaWgYJ?;K1F^U zv%IgsLsne1g_yAE$1c z&H5mXFB(OAXJa^t#^}JY?(M#9iUZ=yOr*ossLyclW3J z3f`sm^{11@N?w$C$jTF!NORGA>O14U!u$$7ul$NR+W#PUee8Eua|Yy)F&EY649H)> z?_5dVCC-a3mAPo=oDAPr&->&Ld)=+M+3tM1oGX5}>z+8|WRP!vHhNsbHS(C;BA(1> zk2cE5%s1qaztVcC;J1S>iuslL4k9PRygqP>)c>H`<0$@>&Tr@51n-@lUGp1n)XY1a zS!|+QA9Fu~$mdn4^-}kchj%CO`Z#~Ji+uEf#8nHCyeRVR^)&_Lcg9>4y;OLY+LmsK z%qLC}_npU9Zn@TrzMsrRM4ka&OY~Cfr8m*i-2R=t6|e8dtYyMy08a*coF}7wh=0X-2KByTzG!#i zUwx2LL;gYD<6thz-$6ffE9GP`7lqFY{Pts&p84s-{rGqCv%+r=t*@Y*OabMu(8obu zbP91GySNrNUanbucpdErms#crza1W4mFrV`XXF{+o9HRMRMn%$|KMwD&08{y=Na-0 zc(<#*Gwv(jQQvs(Bj1E^Ke#!=J^x?yzT$T~`@C$;-sa28doS1+Fm>$4!Buo$HCA0N z_;S?y zR}-{619NM^f$U8l-X6s3Qy!D=4gJC1)OS`M6I=4|a?h(@{ubdO<6I?`o1{lSc~rsF zEs^(@{*`!rUoV;x^ws!0gC~x?mUEJ9)?cFgYAbO+&>t)kPEpRPca{`Ne{fGr)`jVs zFLeoV)zXUJq&^P&=-~xeYw)FFKlmQ`os(!T3JxUm8JJtk97vUunJhhewZ~DMZPmx& zIm18e+B;tIsHZ#wxF3qM?dmXesaX2TV{||z* z4PM{d{=fg%9J1OwcQy1T@NUm0zw?{a9~>b)dhBtSTN}7<>GqEGpC$d>61&DLvy6PH z=y@6Mt9+Wbt33|#qTshHAN{QKVDhEfU7usf^{Mw2<_ze0HQ(pqjoBV7ew=LKA%7YB zTw<%nfn;7^JDn@$K*B%Bc?Nh)%6mR0|AXG*o48H;!BHOV9iP_qI=Qf{E6qhcEmia# zL@(99VZZa%!4t-22J|8iZ<}G>&iQu5fmB`q_$IK&K~6?LSNIMpZ+Kzj72yohiQI;?zoe=UmMv^mHdqkyrG@go}UZ1pue$@5zr^4tC-*_!CzR{z0C%=Znt^ zoFe8SJCKi_`#8!Q?kDH!f5Zy_{uTQsm?xt+MeV8e;+rULY477A`$3#5HE-v+sLp}Z zdtUGl<`P$}M%z2%-JaQL-md2vkZ=D7?FTuBY@Ca-FEw*;1aJ9|tN{|a79_IV+P%-$vT0-#5)I7NE? zigSI)GcaFtD)n(NzvBHMpR2$6*;*jfmm~t|`HohWx(aUsSO%m>h-kY%Q~D1UK?)-OtM>l$;uN4?^5xSqE)Tt!d1JM6o!RMA4lC+?b4%P zE!^4y;uNWQJNqU!M;1)=r+Iq_d6z14o=W>X+0oL4{5W{G@0A=f&Xw-R*%UBwY}VlY z&eIw$(7Dpz?d-=vzWuM-{0d$Gc$fMI_aL6kt8`x}&bFQK8ElWWf7e1jdiI9jNob)y z4*2aB;WJcPacdu!{$SJ1ko)HC{h~^VFKXn;@ZOoZAHSY<(Ds8oXGjz72fy2!i2HHC z(3@bNm)FH<W9i+lbBcngH^J}rpTrCBYSSTlw=2%J;)`A<4kY}8 ziYMb?mA~S-sLIKx`IUNK;lApp<&dA&yy2=xuX4!$jRT4IRh#ru;RR@?JVS2SUgC@H zTvfWH;1BW)oRd*}QE)%F~LvoCo}^j<3WosC{g_VCup{HiZA}BW~ea>oo@h;&zi1*cet31O; z1~0&GH|L!jOg*m^ z?fr8G_F8h^**Uuh<=fFqMZW!#c;bA=W&}LE;Cq@g;J(6Kv||wF4E%1#ebrUGONr&l zr$Xv0wB7{vIPk<7Igq+{N%8uOUQ6`ou^((P?^;zX_f>13-SQo*-`0^jJ1O2WX6;(? zJ7ez*zw^2A{?w!Ygx>9Gop^mJhup==H(~8Dv7WapK7;Zu;XBxyxF3qErn~?{=sSqM z^Zj{-P%VcX&8Y~c zdAqG;?&{am!*jDMekDK7;(_ms-aUOllui6Dy04ViGT7zL&ERuSSMM(xEIb*Ut6BRh zPYk)}*H++ek{mMl?ROHQW3JNtYKGe#;(mmdhEb2cir!c7E-CH@INP0l^q7mDs_R8_ z2K2meUvZw{3gwVFf7O%nqTHLvm-`CyE99?`lW`FK75vU2_2qUo)OSuNFTfG$c`*la zXX8)8XLx08oMm>BEzKFG(fo?OEUUtMe7vbi|w@HNV} z=MJeI?`!2TDVpjx>YUa~MbGQs$?V5L&kMeZ2b@|5*^k*t9^Q7z$z%rjj@=~u_U5Yp zE%+ZC@5 zzEtj|o=JL9?yG02gU>bJ3?;rOdh{Q!*kCRu4=;P-W;u2ht{Ue>(H~TPoLR!H)$^kI zx#Isp{tjZ^-myNG`h$uu%K0n!2k#`rh{t5MTVu1s`HrKZ^c^&M!#i_7>dhY}-lYG* z2I|qP`wF~1#b?&ogMM7FlV?K_i^Gy z^TdQFqdkLNz1@d?&~tpJUaCWCB=u6^o0#R6Gwg3Z*FzSN#{}>8>ER~rf3WgIz`pqH z_tt-&G=}(B#vVO#eddMLD>BV_!mUMraJKe8sB(QXwfU9NV}iNpQQ^1u@d|foyg85d zgMZ3gzGkfDQ;l0YMCMoA9~_a`Oz-ykkQd2g@_Vy`@~Y}7X-WOj;4wj76#1*%RUKCEt6iGcl6~~(O>8WlFTGUdT|ytnx<{{k zsZUw)8Q71b@>i{t7gfI0G@7^nK)uwjg{y|V=o^N;GtSj-anxqxP4(#GtoF{UDStJBczxh(vp4(>&D;OT zM?b-^$C+T@e%u&2+M~7OR9)|rQDrW|RjZbBh5L#8DAS&=PE|0?e*_IY)qbM?mIjN;YARpa;7?7goa-y*$< zxmNS`!mu#kp44~dJx<-W4w^Hte-QjDQNt?IpIb`N+^SphVe{;mo#J^%M0O#8=XHed7c$aj( z=Fcr~qUfc<6Q}oa_&W&hhvIA-^9=AVfzN=xGxHgkFS>;|Mey+M&#{Q#8T~=z z+p)*t|3Ty#3MkKj`%2G?f&R&`Sx8?{b+s#zcc0x-1Fl9LA={{-{|htGRR>}F3qphyPfmxcwb#Pd$X#(;0xM2 zzn1K>@gd3~|C9R8>@oS3zJsq)UKAWi_y?Czo&i1jLt37p1X>n;K<_K`CO9X9dApt$Rr7Y{^@9GEk%m)AU(M)EE(uWuan2bTy}ZA#lt_oNXPKmYJAWB+2sRSR@$mOakAtq!SO zlJ;BvMEonv8GM?`d;V!?H}8ScOI2Jom1ls*guMX$XwGn%di0aX!)x7lE}Xw}hW(hf zLC%yH<@psjMQ0@6uKYOQUol^lJ-px)A%B(Mc#ZfB$y>&+jTTOkk@OdTqS;UvhduMwCx0dq^ zx^LpJe=f#&2wzaBJI4cZt`>9+P({FA5JYdS1x&m1(_+uRVRlWAYc%C*-w6kKUN;)Az^44v;Qo?mee`6zMKI?qLo=c+&Lor`Zg=+sX6_ML{l zGxF`52Uj@zH++A#iE@3q&r5N(;kD#B!;!=~+T$Q6^Vra`o_YY!ScCdiAvi9fAx}O*_tOa-z$2iI)wUz+;;}AFGKpyrPJRL zj|q6l*gG?~7XO1e6}L`zTnlrYH*o*xT{OQcBEBemsoR8qHCH&>Mt=JN`X9u(Qavwl zAn_e!AN_6Oi*jDnx{srJUe@#W9n|w`?f6vln5g|AJaHG#`Zm1poHlrhn>@S)EjE2!hDZ1o z$le)#9Om`4XdFmu4{y}rpK5Z%yTo}>wZ|!(I$8S;t|~uBdz>!97v($y-dE^NsB;Bg zpYl6{1Bo8}SF|5gUH~`B^?|FV`0ellU@qF-`_ZB08V?ygulwKaJBR&E_Jemiacgy7 zD&|*sx5K+MSL+WlrwF`02kK2QXS=K1SNgj>EH|g(x6=+<{>q*D&U|0Rksl|FzJpIz zmlr*i8NFt_rBl~OIxKt6a9`;>8F1CO=LJt(5Y0t*38x6$4`VO2=kUAKcYgi&?7f#L zhYT;k+AZsg7sy(qQVtpW!D(%OzF2)?#=hm-Th@P(G|G~&W>Dq<;a>&2^z+K1Jj2JD$3*Q1yI%kK zbb*#b4iz80+7I&kO6SSM2EH|}zgLjUotvQsp3Hx&`ZzdO%>4ilS$Ry*qvtsT`p)cK z;vPNn?XNafk$1^Mc*wfn8T@v==VjeX#hihA^sWo)HLqnZy|3Vji=w^rX5urPqW{6< z@>QoI>-Tly$*fy>VUbVJx8rvXd63>$${W7aGKaWoGxyHae5tsvxbJM7Gq5*&_TJ0e z+EPQ4VzgWz_vj0UeEEmorTrSO5BzrUklB~|=9!`!E>3OaqtBb)Omor8Ry-L!*XLmw zm)t|@fo5Gd{M`wE}Cz`_&>Zec~kX-Aqf>>a&l`XfEp0B zJheD=%S&sQTLP18)?Xq2Am;7hGtBkP3ww9Tj#VAzoK<_46p_c|UY}y|dFk&fk&@O(LX#>r#I48qAnJ0+Puwu;^;UUl2S8?Jg${|}u zB#{?D`Ef2w4jDXT=4?NZ+HQGY`Zx!UERp`;L*4_3&rnBsh9=4(b52Ha)m};VAkOxs zg5Rp@AIB**T>b~qn?Ro7ipJUgJa%fh$uDKZ4&tgE(tKXvKyr`%kKR}C z#Nj)*m^>!j#|fkyvhu`%`+;66=lbAx-emr8g=JxMVD`A)UJ){9P&s7w(OdIHk#8R+ z?<<_Ew};JkYa$LLzJt1lSNG%eAKJsaAN6srQVzLUcrvzfUpZyFZG72~7ri9!cJ_v& z@2uzB;Y-E)>R6>m{wCr;a*y6zZlZTPyZ|LrH$*xtd)9P_`9ab=8O07uaw6G{PqXve~>-A*ER0PRrC0Sr=vaTe-Q8XXyFure+7Pf zg!EFmN3Z?|;fXWy8IWfP67SOAEpcm}AiuNXs_h`|2YM6iALRc*qsIjKE9Hp;r>Jw^ z`D5Y9^z#ZKJ_GJ6><8ha2UiXK!6D?MUo~Qla3J-)^NwKw!b1k9sGH2&(RV&{WKr#; zrv1c$`#~G?9h!?OKTgD% ze^vI;=I!hS;JoNpp1#6wA5MPfZ;AT>Pn_E0VBWsW5}0JWzHVFK-WlZK?dbEs@F?G% zVWpan9(|l%#Oq_84EE04$4T0fR=j|?AI_9#K+kKv#{JkebrNy5F&AA`UO~PI@Q^bz zJo1lLzIkRB%^6g_y`6ks%EJpkPMv`#gM2&tyiU~F+l=t|b>wFM)`;tI#}mIdPfYNV z9zDEEOK5&|Gs|OLrNQsaK6=&1VgKM4+WQJzHNLNGH+-7(ie<@~r!w~yJzE`g?#|5+ zm%d)x$Hf>tyu8O*tmTlwDdJooxN1BXg(ptu$*6hz+v6V5yy4(LvUf?%+p%{x`smp= zQ6KUGaf+DN7g`!BIT`R7;K#vtu!8o^_#b>N;fhsng6CJr^>O|Re9_AVjh#3}7ne^q z-!-+y@1z{^y+PfbcHNkN<`e4Uz(;?Y{5Z(Tgx2q;?;y`bRgZph(3j(P4e8>#tMN+B zqQj}w^U{4@(~fVykl$k4_krP&zWMTQ&t0{5$xgbjI>?WM`-<}n%44GD?VM-89tV48 z=GIOa^(}dL!Ry0*5PfIN+xzAJ!{GBW@?@|dWRHoOGoUxopYrVwWk-}fa`GRPXW;(e zuW^Sd*Y^~0YdO!bjCx+oXMl${g7$;RGX#>?Qq8Zxf%K=jDEOxCUy0AKxOO6)EA&#I);QbfrT#ebZyt6w$Lso? z3@>{~{5a@&U8I~0@(kcWBF_N-Am*Yw#bd&`KJ+G(Z-RUD$yPZT&h;@*2He_il0)V> z1H1q&sn@s7p#Q<*`#dI?w==&T?{@UOxbM8w@^|ui!5f}ucwd2kW!&Rb*oBZcoVm4* z7N?X9ja$okQS`i8i2H$D-zM<__%>WV`)k#ef-g3Gl+wrIlkB{)M|K2piX3d7_h=Zo zmEP^}T6!luCGRWc;bk7O>P^5uXeWIfysu)&kApb_=aBQO#C>( z=k5^qV=MW*kdw(ZIm#aAP2sA+KL|d9ttE7IKzdm24)Vl>(0x@*b5Z!{kF8i?=;QF5 zfqSWnFIsuxiHpn4m|`y$SFcLW$RhJcE%Z!}%-D$)u5g5WUoh zdef<-^7nF{qn?*t%GQjB^FI~t2XnTW->$f|lgDlve2{t*w-XSDd8?KJJuC^gqabXZ|0=caZ%!OT`-VcI*1+V4Ars&#AIalvn^_}lFzb^c%&YYrr@h+i1$ownxCd!J2 zR?m^|;9KM316M9QLY_GA`jA6rUn>00p_J(G;@^|p(nw-Y&t~<#S_syb=l^>e_W@=r!Ida!jpHbg>?zM}oPoy~mIFJuo zI#$1((Z|rEZ+~eQ`EgoJ6U|rX-9BM#*5CuqQyMOuy)K_0g}Erd zuZ9>ryvWJKmi$EXE9QQLNgoG3diW+{Xpb{YxF4Knc-r8J1OJNo?aGg{eOz>4(!y%W zU)jgIYjXzlopUrFJ)f&w@l7n~w8w#W3Aw%yy073LRPQVM4U35T@$5xk>P;Xo`XzB| z?5JXDiLG@EyeeV7FAeq=VF>kCE@-A?gn*&i03ijd8z5?DVe}zUaf0>+8Pp zwPbfIU+M?WX`TFodE_x+?nl&_qm`cd8QR|YfbiR~A7oBZ$I|~HFTnXlzCpF*iQ7s1 z_6_16WDhU)&Tmtm;UUS%U_S_NxY1)W^Y~0HFRF5V*J|=8FN)p-`0ea5NtZdpe!K8` z^C@$AdCt>@yy(%&D9MY0Q-r;9tCo}T9lLpOCFS~lqq!*io%wym9uxST-}b*9anta= zVt%{oJ2wdT!zp#H@Q{CRp6&MbuotazeTrK#AkHk~SX<4=S%E^RMojpHKXE?Car)eyc}h)0_eG zt0mj*Hhd~RdiW-ke^Bv7)67MbZ{JPtEA$81H!*`eaeQBKkDhbL@Ogpz0nRqKA1a5e z_Jf>nM<0i|YKkX=`)aswimrr=mi$%QcaD;0P`p0QA$to?M(02_#x)ZE>IE$?iv1wo z?Z~%ZB7XZM%E_QN(NmkZtNtK+;^3p-Og*p9lb;r^CA_LslLWmF? zpK@v?^_|Zq+grM&Y!dzzda3r0x4u+FzSMidli3(B#lWroUf%70*L+^+dA;aSKXRx4 zZxO%6eMEa_rB3|E9v>yat6dcH<22Rmj zm)>4EEAN;`LzV5fI#fCWp=2!4dU@m$y?sqy@qlhocc~S0p9g@BC4`(Mg z9CY4H?<+UyrE(6rU3{rJpP{SeO!8A{l{pDAzvBKN^N``kQ9b(m^P;-uGeUMlmDkwb2wzVkKdr8YO)p0_y`M*Md4ym&5({MD;X z@AvFaduK1=svS-InS5USKdAbH`n;WeUYtYz|K9C-4*B!sr_v5m9|!p>?43*GzQVbJ z7l3=Id5za<78txs@H_K&5Z>^Igx|iC_JjN#gnzJDzJol!iluxz{5Usia>*OcJ^Hkj zADf4p?3ZnbEFoX2F(-pO1LtILu69drBEOS29N$6sQhCl$OZ`E-+nLY6ygtmYev4=% zJ_GXY-1EYHg&Z>Xad2OCCH@tAE%Cm>yL}1e+tpmu*qgxnO6A-2oJ>FAi^9X3Yw99< zXPz_sMtKI^!yA;FU-8Rn2kFu8n(i6Zv8=?j&V0iMjQ+uAt$Y)D&+A2+U;W-} zf4<{rSZSE#kioxNV&IFicgfpo&hUcx=(&&cYVsqNk5bMOXS@FFgobM8jfTB5_XpWW zZ{!s5oB@8EM615D&Tp?>G&Sf8`XAKygYXY35AXEj+bGuuKMuIH{BAEKP6gg@=uSoI_+RD2fUQ3>f@|*$hEAX%2cLryhIgoFbJxuwl zksb{r-x9y`4`yG=$#nA`H1xfmlbaUQzJFvP<@)$LIG4C;?cd#No=!e`&tOOMcFeDoFBLo)_oy!Mxu(rl{XzY{ zTBe;V#VLZ%3v-4qwC|w4AH;nXQW{Qt2KEA=M{k_BV?PKF?=6GR>ttOoZO-r*af*;b z{z2YXoM*T!KKeZ2sy*PehdA4qGg$Wr`My$p9NkBcJ`VOcS2eFC{5bHXBGVzJA&EnfpFC@%Y7=ZQI>f%YIOK!_jx%K5VwzEkobgHmQ>GqA?{0D2I%^=tJI~ zw8znX^xX6MC*`k@L*_X{jd+(PH&l{`7d#o{8EnnB4f%HTQn^29Pw%Uz(+Aj=3MqZx8m}9acn~BFwLlzcS{K(I1?Xv{dGz z;9n{K;H01ni`J|>VSdhJvkaV~$--w){PzFQeTDs?dSA5<>Y(>kt2P(iM018in%5HE zaGk5BINSIi1ZVq0;vwUI5P4DXkQdcf(S0?*%IAeW4$rTybn@A=XtBAr_UAs74!PwA4IMX`@xk9m(sgkOvmgi3yhs?{<6#ajv4u9-w!- zM<>5C_Jio-d{2GnyL7JX$F$OYg?GC<@kP5?K9d}B;;HcZa=R})r;I8P4kUVB--$OI z?{@Ipjecj$+xdSG9$vlYrRMGW|KLn{x5rw>u3c-zZ%2QS^9;->+AVz1@cN`v3FU`! zyoi6*)v(98pD*gRAR}PX*v+yZgx3;thW_L+;U4|Ty0(rNiCYUk1J6ahwEsbHKfpr< zukV1CXRtZOd*>a*DOwoQme6eeAIZrmU#iN2m&8((DLU-hALW%PL=hs^vd&WoC5E~@+J|LT*g@nrBFWUd-| zUffIdp*#c5l^1a!jlK!hOMOtwGkm-0e+!zcz9&x{yx}_k3cU&Jo!M&{e&%15k5DhQ zi{(EuZ#NUS792?2S8e2@N1j3L2MZhPYZe_|SDZ?|ROXAmzIWE~Oyc!*H+U^^u8?N{ zUli~58OHVez>{JBpyIbXQ*Q!u z2INI2nCpq(4!^VFiz3%oozh?PhU>gOz30XJcKGP=9gNlb&Q%s$$ssG>1n&pI{kSbT z8H;c~FmJ~mM{#S>$Fbf!n-(5fvC&*?YG3wPRN?e8={x^&I=3P;_x1G1)i##M)vu?A z5@#Fx!9@f2jNUuF5BVmzj}sfXbzDEMIpi^UihL7sYhJPZE6G9f?OxJLRosu;=C*`| zF;}&BJI~waZtbe^WERuBozE3GMMESfqw?*4rMxK5MXfp8yvG5j2!0&wot1aVmiX=9 zU+Fm+^qtwer1Sc~XBbI6dY#XJa|NEvYVu8}dHb~EZ%N-7b5Zt&WABU}eGkgFZ@n=6 zc);H425v35AL@Ojd|rQ?XFwn4Gs^X4X`CWwd0*uTSMAbrw*_gMcS+^?*t?{Bsqi~< zzMXsY*bhD(-9OmXyFcan;%fKPT=X&GemIhU(C%0}^#`*l-yWmA+c_u0z6qVz_mX9W z75C#2udU;j2CmXPCd^e^pyfrCCr;(t*<-?c9PFLhH=+82A@XkDFMLroZ$~e6676xg zM?W;N-Q2A4+h>RuU@q-(^f?3a3_cnUd1&-2l4tlm;%)z*MvnAwu=%9U{p7r|2W5T* z{?(;|->ZH&>)%k}oH=;n*v-^2j?W+Rpm>+qM~`>Anlo&>&%?`oXWUoqll5NObx0b zZ}>=4dnZp^Lis^MuJ4ijla--o_TA{>WcPSGaUj{dR40C%^ug6u+*gABwnA-{M=AnsSNnRB1cKC5-kr&`Ul)qwrdkFFR z(DTCH89gufaRx*cP2WE{a$u-q$F-)@Z&yU*PNzLi*y=! z6Y4d-DEc_|HplB8IT=~zlI^QR`OTOzXE65Uh?e`XG&?0V@o}H_D!gKJLXrIGpIa+ z_4^7r8T7pFMEn$YH1U#oLc&v4yuM265B4X1yK`#r)^Oo7C{7W-uZEL{m-!58KM3E% zzmvui54lXZwP7v~du5MX9B83=JN7vJ|4Yxy*dGjZTs*LZd=vN|Rc^d z^D_Mpg4cKGgcs$Ijrl9?503HKHY~`kk^DGwWq!q8fasD#M;6vjYC6z!faI@UO}K16 zlGs4K3GR9QgU%KB49x3;Zvyuf=dUJNd6#m8huqz%=*E2N4`yU^OR=|np8O1PKbR*2 z?g#U)!2MXi@;l*vs62ypPA1?!AHCj7<@Xi*&Ujy8euZ2g^RM9ZDr)TNTBP*{kDC8S zeDuh-^IX(aejw-Rv@^+$^lmqLEpcDz{XyhKk-vK1*_Y;`KNQq#s!nkyA3giLz;7Sr zacyLd%thIc<4Imi_QY|I{!z`B3Qm#AUybs)9LfxMRN^Lle&5OLLZO1>RFdc51QA5?v3{13wK%=4=yfp3rN zBOViLo(%p6(RT*7R`mz}l6c45N;%}e`s7JY274UNiypFgYToe9`$6^sZ1Mkf73(xcxbxjy(N(0AUS^Gw?3$#xcJnll{N^6fZR`SX8|cOdW5Lkltk zCXUUdJi{gGJJ)Erz6Y}-%N`;A6>`Ynw_}e}XMT;|S3SHvh8`rZW!aJVl51Im)|Fcv zh~FMUIT_v$g5M6F%sumS;$1Rp{z3MJtNkE&eaye=>$OexIJ|e}yeQ5U@}lZo*;v9? z&(!`01BnC4Ib`rfkE}>oxQu%AAuhLXhMaq*x=gqq@H@xRxq9N_G~#RnsC+pL1Lr$2hkry4taiA*X)P2{1yA?eH*Hs*AJdb z{C3Xu@qUoMgW!uI&!Bo<$hWU7Uvp|s{XyZ@8uOy?@a7J2q4(AN!yAg%Z5czHZJjTw z_Bi?SZqHp+x+E`bp73P08n|jI*Z1b(8;1PVANS}@r_ALCgsbM1l1aYQurqtfkJBL@ z6Xax=&oGJjqW%pR&Nf$FFyuvXu9P?Yr;*!;FN(f1zJngYJ-qu9w-&iR@MPdIX}5Gw z4cr=8+Dg7u&LP8Na!-7zQ;09R!+l2EpT%owx8aoJudv7Ad^`3y$cys($|3%dsCTCC z5w05Nko7!+@_FH0?HygB`Mf?}v1(x~-B;X8MgEHW&Yw~KDs3LD+^8s6>9{n%&swaghX7X`N#^Q&5|M?YWQ?Qxc%q>j`p+XA#4a*yHR zT93Yf`0en9xArM&dHcfD<3ZG;XPylF&U|0#JujX!*b-01*z@9dJAVg_Ts7u?+>N&} zdz%^8rtsu%9ZL-Id}o)`9m zc(-HDfczCW+v>hjy;SURCW|MIdlT?^vDXrL2Fw{0uW!e=#gyyYL>^weufUVRef6tw zwu6O#6_%S_(WvD`(RarC3ip-Dw};ZZ{b}msuph^ML-h8beU*mqAm{pSo7)rKjQKHS zlvN)G`$31K1D1j7Ze+!lR2*4cJE^JM;G?h8`h!1ebA~s{9;A0W_nnm=M{yt@rthGw z`Br?<{GBuGC@)$fy;Ss_yHU>zxjv&OE}i_&*gLED6+Cg^Un#B{JaKC8%z1`z-@LHB zl)r*^DWCXP{BD2!_{_c6x7`(QcnbAWuTs99dC1(Ox6UEYt+!~oKICNJ^ZKWF0k9v0 z7vN5O2l;W_C?}Jqy{|43PsUy2s&W1bJQ>_qM$Wd8FN!^m&XWQE3Uh{;)T6%?5*Kqj zp-u9ltHqa!UMlA8?dv~Jdcl&gW?<$a;;J>?jG&whpDXbC>}f8Fb2Z1&j(ihAxpTzt zj6M#|mBaM_$C!b;NADG`8qO8_omGEuNc90je-Q60crAI}p5!-~d=tpYERMOR@%peI z3?;86dJ~VW`zdQlNriS_J?QNz_Z4`^+@l{QKCcC3U9;Ub{yn+BrJDHdjnqr!bA|jB z?+4*`E+Sr^6M4h`Azn+ItD$KJh+7L^D*gw-X8@-N_Z9f$ap8FVuWq@J+x!sODGfT>@7P=St;d98$70Z+JK1 zA?MTl%13g@6D7}}xF6uRBhP?$dtc!*=>9=)KRDMnQ1kF|t`FSWE~yd1LvALorSeU{ z3&318BcB2JD}J|oWxl8V55i-DUh3f$X$H=={%%LU9eGjo=+Vddo${jd2fj<(5A2;c zi8uT$Z7z!b;NZ-q)W^Yng}pP+ugoJXFWpU8_EhtCipRzw!FmKX{Dt zqUd?~2xr@JDzW@vj#t_jbYFp6+c}4<@(l3f;N8wWucoT+3u-r2k~h4k^yraq$6WMn zy04Jyn@D?S<(p7^(SFpMfPXN&wC$+F`CF8e8RJtY{Xy09f`8Dr?b(ZECqiW2KE|>_ zeDpd8lJ_{6x3kysUGl`?zS7_An2YLu9ORIZlYtjtqxlHUMb+LJ{Xw;NR(qTSC!Q9+ zGw0i}$7xGg5OXc$1)pr{<8*vye{4=^g!m=~uKO`-X~}-_4;~Uu(bLhdQm#+uU$u37 zR_A(haak9u`|3As-VU!N_fpwwS)h5tl`pkxN*B!&XF3&G|E}hVvsv~R(?85>R(`+8 zC#ZIOuJ{MR7X=S_3C-J8Z(_IjopE1*vz<%ak5J!U!|zhh3v&kU(MOZla&vLy;p;U; zja^)K4f%pNMJk7k`4xM^k#Dz@znAluw3EpXTI^D`Wc15FPPsnJ+dF8^0B$Y3mc16F zi+|8hINR7eV~^8wqjUEBvWJNS`B#shsh5iP74sR8zw#Jb)pLB)vfBNU>#Lys;9M(j zIDGV~kAu9ZMe9u*pqz~I(LYY!CA{0wqv!uYoqq-21i!CtlHZvqDLa{40K6O+WtDh2l>BLHrNGkK;r+b4O$Tx92zQg28{z2p!HVLNcg0kM)9*1)>)rr^4uaJ)(?{?0MB7gNO&D(7y zFAD!4axy!JTZ{YZZYK_;&Xf5}^U-sj!N{#mw|ch+iPv&YZjij&XG>lbJQ>UxFc*av zAYI<=Z;f*oF97G;(VO7!Am{r0+6vthMy&Jm39k{4Np#FL$+v@BiySiNMb*6B`hE43 z*2jS_)#x#KfZkWkfdsd9eo&jC}ecoP1 z9+N`ydHvON*qpJlhCIAH7X|<7kLK;@5B3@!>RS|6V(^B~^L3GPHBG$XVM)=JSE-MK zyeN99yvN~pyY>HIc)dwDMaW+{QEvj@LH4EcoPqZ^-!Ff7L3)56&9D3#E}p$5oT5i8 zuZh>vI^X_F+~LHl=2sGiM)xBRZw2*ID>`|@!%JI_I-bAP97yL1{XxY;2EW}hB3FE= zn74BdIi5UmPgF;oYrGlm@-X=a!L3a<=bO63_lbIU`tH#&17|vRx&9mZojHdbx_Wl{ z?A!v%AshQR$jR{kpnk5rsw;{HkoMF892RYx)`$2GP(eqN=4|v0E>Aw17--MnQO^dW&Hq3OK?kncED=z@& zuQ0#jKF*BenHNf1?n?ejc}(8XzJt5uzG5!`da0|oq)DE^g?g#}L0>Q0w6dCdsmg1~ z{44Ya!GUB>5%xIP55gM`P7&uBCYirq-ecCyF8`$~Cu!Bt~^yK#Q?4$VbzUx8Bu?~>lf@uHjz{s&JHw|3UP_sKWG zxjyE8G*eCnbB3M7zv4Lq=AtSu3JxSZyvmQG`@C*Zp25iL>$!0(an-)2`4##&DlZBT zub=sz=^k;4@Ev4c-zSv=i2K3scI0H3TZ=ryo~aum?UoHEF93fB!Ts3Tc%vrja2oki z**^#`0Qgsm1IcrSc5@-+MOD5X^Y&SWd^>wBl_#!q{;F8>hV#3981-?$>+73;vNG(< zyEnQz**tDDCT~W*d~3gF%jt z#On*nolWNoJQ>B=4t0!@IRkw3#ykV(MR8v}F8#r_^~<&g?5jHAMO-!YZs$G@=NWd8 z&#TM%dq*Q^&d?#AIP@lB4g0|-|Iot=uO)NUxR-jV;1==v;Pc}BAm>G``3&&H^~=x7 zuuFNs@+tA#&Em)Tiu}&-E-_zJKUcX{-$CZd;C=PBaJJdI1b#bmGMJ01TpxTBSGKj% zcW}JiSMcz5cg>-9`+0+R30yUBAocGc_i@lm#eD@Ha>pP$dbjKEE9Pu};Ot|?*?uLt zC%xOj+2)+g4#|twQBG!1u#@+n4D%~^;tp8sY0fZLa(#Nw3wcrQO|Zvga@!D@i*|^A zaFB4e;W07tWb#a18hPFEnj5A0=L27UdZ@ z-(FcXFf(DzXyRY7hxbX1Q-nQ^wQpiJ^>Ny1e)T|Vhvksv(RDYnmWqe>P)`r-fA9+B zMbS%DdmQF7^xpU~@fp6}bV>dP!ISaTa((Pe&6{CIxjyW1j#m!IPs?~9#m;h8>!r@E zFSk1a81LEB{+#Qpf%bD!Nj+B-9+s2lwcV%{E3IT_3u3TI@`Z;fvyo=gwfJ7bS?k$hg9 zZwCjGc`_=0g>%&qcP#OHvoCq#c#rcM`JLlRex@G1O;R`M<8*v?r+FrM;%&?X`TP@c&n|!J41z`RaeDvTmC=ajd zP2k<0V92+_H^I5Smo2B0Y&OJgpS7>*#Ndn5$-`^pe!#l~pVut6re-_w59+>D^qu2s zUnA}Z_q@=@fyadXILM2#hnL^&=sP3V2Os_9h6?8`gC}a-j}8-h6GmU^iORnDTQh7a zf5km7&NF=J={IWE)bvPO`3~07-dTA}m@kT+7w50^oQx~&2lc!t=aAd!e-QgY?oF^C z2k$HOZrAyuBTa3RXJGyn_vnYEeMX#ZbziCXRql+!`M=Zriv5G&Ga!GZ=I!WBaISAb zSuZ+Qf0OyuFL6h-xhUt5tz+h=oG*y`(U!1?yq4gL^1FR@=^I<^QoB&k3;A}{^YXOv@S^X`9uwO?%wIJc z{DbhseZPDX@fmW-yJY*g;`JGO6W|mnzNjDl54MueYs1R_T{I!+{P^r4F4~*{UI4}G zdwp-<@l6-@wA}0Skkxl^k9-H=;RXMS-&ZPsl~(-5;h$=D8oqA!W>l=`Nv~t0j!W-S4S_j$DyB+*?A9J1X+tW2p5$D^LCoYOSyx_^OcM1DJeqS+H zjdL>l<-St=!4T?AFrNYE3LX>o(N7f)B>1A}J1^Vby8d%|w=)M4c~RZN`;p8U+@s!^ zUNSm*;7rGE*MBBI4tp&XxArF8R}1M}aUTbq?d1!XYkY=K+T*A`PKS7Sk#C2`1f1>9 zW1pvd`+}Gsr8j}Tv+AYt-Wl_D_~`%toXiR7;~>{JKzj7Q)BB3O0LWkcAb#iE`2_|) zj&&c0&(&P=hJ%NkCim5bmFE{t4ElC__K=5&C&M|Jm$kWQ(B7%iOGS?!`F8eNs=R2c z=HUgWNS`x+`;jN!CCsn5=Vkm3M$}tQttzj~@iLsNPbwqN?4dpm-tBqwTZqqaVR@eg zY0{&gBzcB&n?6i&7mvxCWxY;*N_(82M`rook^ez>0Unb*&TCCeYAcT{ENRSgU-yBf z!-BbJXsNAumv#<&!RLzforfiU73-(nSE`rFdBMSne=OFb`e)xiD0ckt$ENAjhUtOm674EC+)bl!7=SDrR9+X4=fV@larA}<9B3|DXnv43- zeFaVtdzVVJo)@^a@GiZldE&m&^6l`3uU~nd=Iy-4`BA)<@OiyU6L#LE96CWucgkv!hPl9nmeRs{N$k8MQJOKn4dSbQ{P$dPc}8Pa$DO?>ofj{|Qw_JgB4akjy4$9IsqAF7v%oJ_aWu&u$R z4#!%*Yi*w9mMimiez%{Cog{Mx#evlOIOwGsbA3w#S1kNw#d`BDlO5ex%xAE>eoOeG z!K)oCbI6yPN9XE~`#A6}A=d|A>H*n1W6rQ-`|N#{C!Ui1U^3lT%EJpz5%*H(Lh&@}l4r!RLkim6~61t`GUEP1H+`I#YbZMf(nNZ{pJOUWUCh z=AzuA_tZGs%)iq6ILNnqYkUUfn~2r^2l2iNDYd6_HQm7L!yZTZ2Xow~w+-%;>w7%2 zvgirns-cgAzVk;bGPGXmyOL+%9zFZKls6nanE=N{1NV}TzGGQ2af%@Qo+o30i7Ty^cNcSE&v8tt8XFStZG@zCwR+cS|eXSM2j@H5aYgy(B*@()WSkHhp)||Dfuns{4xhSKV9- zhkP-9YEUh4iiTVH2brq|9`YIS@OsEx6y7DplW8+eBoA*^fcMz92A4Ze(L8a?Rm0vH z-$C$XMtj`o)SKx1zWRygSBm>#_g}t)?6u6%INOSU6}!fZ^6jDLZr==*o)_mu3&b}8 zz9{%t@Wg>96SCTl^6hhS^Mxm)-dCzO!TBq9uV9zkH|KWpn1EYbq0JdAepABF#tu(x zG2cm8LOEo-+rd@CcM$iLebPbc(W`g+0GTuRkcSsMWPZ2XNKS_R&i&~-sODGj#DRz0 zd%+LOC!1SLt<+1!yuF*`kd1Ru^ysm7E)$OldS1+LM=$lu@kK+NgfGfHFO_FdeP{4w zkQe3uL6sK;SB<}ec(=2E5PfGoFAA=j^}HSL_AfjSkQX44dK3R4o(%Ivd48olCimi7 zO#bEzR(qTu&fc!NP*5v;2A7T9v)?G|BixUlCD->6@!P$kAEi74_fl2gxx!*kz0@%B zhToyNsLm-GE zFF?L&lX!TyQ*VO*2UQLkbB0)%x4+)D-92T5*>6ht7ux)a`3yK$I;W_E_@dyqgHr_m zAovU(T7U4DxH}PBY45DO0Nt{CZyei+v;DsFM%v@>|Df)*m^lq2t}hlaNj_hahuIeRZ| zvt2(sX&G@pHi+N38+myBiL1u^cJO5UMx7%LBy($rr~ND0#gLOx`$5dF+TvSGe&&nI zdoI{Qe9;QZA>%s;AHABl^SQFM%8OnxdsB~I`MkjWSXgo^>ydTUmUfG?c;f6k`MmCG z9uw>br)d4b4&h%tk-49IskpCr&R`agiO#%N@GgD2&wnGgh+*^r5{Q{Gb2fu88hdYIdgnw zHsjccFk^^BRNvd{)44gLpA>j zJ^Gh~vmIQ$O?+PPJ1d?{a}V!AJ-@=e{oRAvk9aNDE!dzvyyo)?+z;?%FlPY&3j0Cy z2aUXFy6kbbl82YQmWtQcRpz2?#OI~*q8Fs+wY=9J7cc4$o+~<;`B}s$vwR^ONH^ka zPd(9lPg(H`D`T|Z8TWSd2MgU21}q}JsPYf;Ty&CaZC#5q5BG<@lW9)5v$l3Te3W>7mvn!S&nx!u!Z!h~S~Ph~kVEDkz2bf-9&){Sczr__5>E!b z3GNRXTs6gK2-f%Zz2Y%pPEkaU_lPgb{C4a#}WvJaO;hYR| zeF5Z&W3MH0eVB_D1aGGQV3FQCgVzU-34HYE5AwY|y<~>TNAE+uiG!9u%lwKxyn(_O z?L+q~?41?Aojttpm~c+!m$^?$Z=%v=4BfAoFKYCi;UC0)Q03eACRA%5J##<6XHXo- z7D+A08;<-{bG|5iUW$ioaMcXHDCP{{)&`N+5DBQha=Lk3K4@DsZURM<$O6`h(1^y)JuaFP(3H zQNIW8hGv^QCg35rOY=bycu z20WP(y0`Zi4&-R+d2xSmI`P{_ERK>s&U>3D9$XS;~x%wd+CajVDAo2_LWM z)$v=E7mL0lF97F|r)#ez=I!jYG`s-reRp;mSv%*{r~78xyaxkIo9KR}IFKrTbu4Cl z$O4~519HiiiX3t@?QuBIu%q~eJ;5jHZcJ&{t@oUo-+)w{+oPW*yFIzi}Rw`4kK;_-5BDWEvLcDA#`pI2!oR}(>U!+m(2sn7CBO66Hl8?eYk!eE!!FA8 zseC)iFb+TSLi## zH?e;F*1_-h2p}HvT0L(^k3O&>Up6*$X~x{AxJx)NuB--PcxgYF_ zQ}-**A*=Twdi2OM^di0}`<=PxHJ0{+$v!?I-^aX?&}6w29~*T(aHv;~^qqM>=$z_E zygv3?&LU0`crxw*FGv0({<39H!mZdlq1nFIB+u~p;qa}_xsRs}x8cdakCR6HcJo|R z<=gQdA#j&N%YkI8W2KsKaa)cp1bouBAG#d|~W zdh(^N6>hEKUnvhS^V`u&{eW_P@Gku@!dtvc{fYa*xjz1`;>*e@FA5&=3gW8GIJW+3 zd*=r|9fuZ7D4J0l=U{oua%padnJb9O-AQ1_1O+J3Kl{Teva)SFmDdC{>AFOknHl6({J z@b+4{P5OhFGh}P;5^~5-P2c!P&x^fF=nv)+_v1aMNc|p!fADkZc{S&L4AY)C%o*Ud zH1D02Zz6~OgDuW95dR9_6?&;xvtqR$$6Yu@Z<%;9zYQEjzEpfy=%vELtN88b$is{C zial}Ox<_v(Ts7qSaKCCH`$6oT**C%7aMhatza4!X=8JMKHF?#@#j~talOBk7N%e8S zLsold?&ILQ;yeT2+x=Ib`Jef~%8gKlrlbkkLzJzcc$L zRQ?L{cKipy>r?!zD(cZQ|4MyVj@mznJcIHN@_rC=hOIh(h28{usYY*NFy$FkZ(?kS z#V2Jzj@yKWUN#)a+2qH0QSuD%rAEsh$DHd^`77{6`MY{IJ*42H^1JjOWM8T~-LIAj zpCO0dgZK~PJ@_Z_r7HfFnqL{bzDDW~4)*$}pFeSH8}|ok?uUn*SK+6|)^6?8qiw`GMYp@H?v>y~@eJyOck{QS#JkOwdUdW5?7q2CsSNN_L5?Ad-$uk(f31?FdxlwX`hL8T@@tc(wioVzR zcIVvit&bi4M)Qzwht|aIPPl9t5#LAeap0pbx9Q`6&j4OuJ#n_tAB+%A(GM|RA&Y$y z$VboKCH8p@vCfwseF)`6(VK{t`4zkXUG;fof2D?csq9N-uA0%CF#A&R9)uU5TW|k% z_ih9crwBQjH?7}DPKNncJZJC@IT`axLWAXQd`whT;1KaU+xF<0->!W0-TSun>=Do_ za!mX$bid*r{a?j5F^&8<=sUw>QdM-P^5^5e*LP43S@m)3Ep^oMGS5ZL`KwR!l4w7; zP0!mM=JZQEWLY%-Y{VEjuZn9foSa2GjuZAA`s=~c6J)?C>OY2b^Un=t%I4^oYINR`f zajx$J@lBZLSKzllpuFhX;E$#6%$%YQ{U3DRSikD*1XC}SJ#pAOt9-k95AvL$SbHs# z#2aq-CJyL41MXLAH3yQpYUcmoiOjv?;q8N5c@&J>*Kw13*yO4 zkz61A&OrtFI)`k`MY)fIJi}AswN#!s&LJ~r8}}>YJvboYo~7QzzpAfmIPGZTwFGAy zduL;Q#e4?ELuS7-czx)lf&-~~6Sv4Wp*(R<%seMPFZM1q_G~LYuWp5_O!+H5ufT6t zK6>suE00NAo4#`_c>ykxCvMHzLYgy#kS{fldJ`q&qwhrATGe-MkY1{Vdh}|KbBewz za6j-}@qQ4zKE7X}=cT?Ycj*tdac&^5CFifqzEtq~IEVZ_@sQt4ej?2|cji`C>d~_& zPVr<$#J?2j5zyB2nZDcUJ*f81+pR5=98<$q`l<#3mOR5$>fg`(ct zP2)TV{phjHA+UPUk@;oic`r$xf%EMl$3D60q3_QGvKFQQe#d(Ff zDCX^N{6R03&nxyWfvcvxOPIG$DVUrdu+Y&Op!w~b7d7(j*bnmF*~s;A&ub{<8Mu$5 z-h=#I{l(;o1NQ^_LA(c*AIC%H?Xl!ta_ig4vunW1k#C8A5Pj!s@*jMHIFQ^Q1P}Q_ z(T&P#^3fyDu$*$poNtH6#BM0&q94;92k$}ld|NDIRnlsP-TI{}r zi985VG z?$N`KqdX?)5Au119P&xxY^!@a=2vx^1BtvS_Jh}{ml{D_wP!T{O7V~}Z->|NU*t>W zzH?XWZsOJ&J$kiw2KNI!`gIc?4sm$SnR3W#Kj_rV3-CSlyfVX@W~)68`@Gr;|7uxD z3h`uAP6mBv^d`*Sr2@*gqetJU--CQ!MOjBIPL}PDo6@7dAbn?b zZ!Z#`7yN^3buU%zoz;FYdDZfg5B6R@xvs`pa>$ss@0UK#I#Vw-N&8Yc-~OfC+Z%c| z4Ou@SXGSCCuaIwt7a*foh2}GW1IhU-bzULgep&ZYFA%rZUA`-L;&u>UlzDwEQ$vW? zhyLIM*Svm%y)Kg9`GMutgztpko=@i$czxh(e@i_tvv1;$yq2F1p59}MQwzz9@|;1< z+u;S6Oy^bU_-@nf;x;6vTE3h&FMJN&+f}|@@fqMBoUQ#hcn_+1`$Nlp6R+=nUBl^? z2mDMuuNzt68!Puk(ESQ|2JqY2!~0w8efbYI%6`!Bn4Ho%8F($h**3hE1=JsG)SfuZ z8JvjwfxWY(Eb+*+>Maf@<-9`Qnf*A($tdqq_}*p2li~f~`Rpm;cV_=!VQ{JT4;BXp zdOuD)WW&SDd{Nx5%y|Z!SIob%o0Crb_8*Cd48Jq>&hT0~kss$P;URNgRNdQ=Z_gxd zt*w8M|AX-3aK0U00Oeh(wv32>IkI~|TjKSNu8pCd7jnq#1qhNp4xd*%Z@2AD6v%hw zZ}P;!8;<`V_zZ5nR}YV&{$QqME#;6mOV6tn`RI*34!9rd%C8-DzB17%vd6~3o5y#d z9=-BAqc;&G{=pr^e`?0-dx_qIR{E~YeP{L$-lZHe_*X3@hkPw7R{G8rw8!Zh&`;)~ zwjLACU%_jM`PFxs7s>DJMP2~huXvARr+r>cJ;7&Su9}f&!2N0=@%p|aAHC|)qvyrm za5ZOOZ#d5xxQ~-{w&V}ImdcOAb5S*KpJN@mc*&}?lIhyRi+elfqToQ<$^D9Zsfl!6 z{dxHj>ZPh2GJ65+bY66frAqVLk&{vHL7Z13$d`)m3ZA&XyI&fZ>sv!!OU&E(Kgb>v za|Yf!6@y@h0maT6TcMQt^8T@ z+j)NFWwEo=iGL8^73W3OT=XdA8NjWbR=wHbCl4>;i?Wa2_B{x{^B?p2>N|H5pO?`e z3?Wb4M(L$q%C0AF?VIHDVn0svo>$ZCbQ=yN&qe=}>}YjKNzdpm{Xy(;9EKKX?g#eH z>Eg#3b^W4vEfoi{ZSJ(KZsY|}d*>VEAN10kZFm8q^_*b``JID=Q&dwo#dUSRp~Be) z|LQy9eke~I^ZLNqe#1I<(X+&@#k?K83Gn*3H^JW(?pL<_cAhg#kerO+1?VOolh5WQ zg-7`14}XHbs}McE!hX<^@>kmqy?oBMA>S=|K%&q1kblR#o=|If7#}J8_KA{jhtG>S zMe02``_zcqU7fo2?da(q@Dl9@KNk)p&qco`uVw4pPSi_94jH*VGykd+@!OftpnCLt zUUk*;t9~Xw4(|ss7eyau$^0`BV`Y!SxxTcL#jA!co+G?Ih9 zrTFdWJFEK@=lXcgpmH+iJOiIs#pA}1FExbtqPBAeq-T z`HSS+hm$uveCy+f|5asg_qzLU13&V;6M9oTye~&~C4T!(@>*^eZY{nm@I{&Xq595d zZ#Z&&$>PV^L3{?xMOAMC`$3#nZsdtWkDhbL%8wIBe9^q}VBxoO-x+-zaMjSqL7oBg z_Du4I^B!jk^>Nf(w0VzS?Qu@TuqO`rE9K#Bai(#9FnO1Fesv>j7V+ER^HTjm@Z0y0 zk3Prus@~(UA7^;7hqZ$8S2dN@$46e@=dz^NJHyh*H{mV4RP|jo_X2=h%e+3`;}nfs zq4T0Y(wsr<2S3nUHSlBfCe-QjD#TSL&nYkaxi(-$19=+npj5hhan1{@MXK+6hXZu#{ZrbCZm)eHr zqN8i0+GfHh5S{8$u|LSxEpaG!L7Y)sZ6Miy&1ZOd|t}C1fSQikE^7n2);W%=fxR>HWL}UyPO{Hf$|2Y5 zIYT~q;?N&NUKIX8zF)D27k(Uf@h**!^9sEQo?jiR>Ox)s@EO2wKaqKXdi1!roBMWSt6}*2$qO+0`gs#)`wimt89Ze5AKaTzC3(?yp4}*a6?{DZh(p^x+E9w$b7mu}w((flj3k6!gsZ&IFtd#Q@6R&&(hia*`k*<*tI)n4Ku z=Tctu){W2;&lCS@!J@wj{|fi^zH(kIq&*ID$PeOYNe&sDBKYXxF@bMF<*%5lX7q8; zciu-m`Zb!*!1H$IGpP6A1=&0EoB`aA`oxB~T=B%gKgb>v<`f-CeuZ+#$wz`I*T+0$ zV}9i=bB5xYi!^6YbJ3>Ms%$^v^%!`80 zfIbest6RcvH#kMOU**%B0X*b;@*hkRzGxrW5AyvAoFcpjnN!4G0BiW}d8;h>i7n&Y zC?_+M`p!3x=9Nz^7@s~(b3d5R@Sfxux=WA#3p%eknGG>~$AwteUFPS5mp@q9L*SLA5&nA{Xj(cjz~nvPUG ze)t2;zf$w|3zS1Pe5qcRCh~c$onSw-si(8^=BrbW{j0)__BhO~g>M4i75fL*lXq!g z;^&s-#J~E1IFNZYznz>e_jcs3?3P~8ejLs-sJ=7)gX|5j7oN<-&DGgWsoYED`_*Oo zuE4Dwp`xWyUy3=>X|3Ss;Gd#SW z@h_4m4s(XLYGY|X$XqpWKX{L0M>*sI;>kos-HU%{c}3^1)>1DO{3~#4AHBDCmH8F( zWSD;ie!HJjOB)Vk@WP3bXMl%yX0tp4`0eQSN!UIB&Q*{n){ZrwUhR*rl|yel71o>ygmvEe=0=jMj4o z+^;adLT{pd$Gna|FRFU#?N9rMS2Ad4-1b$G&Ll-WlE{?43K3&r9+8?oi*kxo^VG4!ph(eecoU znRCe44|eNYN&G9tZ{JDtt6;fbu^$IHWcX6K@63MZeEP1~?+jn6k!QgC3jFpp!z2By z;d|z-Xm)RJb>-U8b>)+VTgyBda3FDBVeedO)1#lQ{Ww7$@QIx z>6h@(a*y&?L!>uhcSiXpoM=CY9zFJh-nH90J=?dnXODp1(s!=wXf?S{K(ewRk&5ZRE9u9GQuBGmIWn{PzU$l?xaYCs-$h~VN53VyrcwcJb`$d*@b zP~Ulya3In1$`fua{)6yZ!tbp349b_vyuLrJoV)1na$a$N@a={?w`Af#V$N{i@*qAU z>auv^z!$Y6o(%WAx>HWZ_^w<$p9$z4=_$Sm^qo21Zsrtqv+kli19-?|bT75z%<8$G zGH(Z;p+V>R@E(M3f_?PtOU3#DaF&(h}=zXu;mo?#R1 z2Xhu|jm`~z-@85KMYBx4)aHF>^l{W4=T&+SmQwx-d(ZYAnsScSPlrMNb&mc9t2MYJY=4WqVEhZ0OnUKs7DW8AAG5Wy6^n{v9CDokzJt5aqARi7yKOp!%-VyuD~b{)~F^rJmP5FWj%-;l=%GH@yd+ zNNbfFzO~cgBUMdJue)C#nB`j+dQ&_mFX{i_Nb-3_XbvQJeawM0@15bt;XH$t`Z$=2 z^1a>Y(Hr|g^qrNDp7%H<<>FcT|sYl;I=a8$4ZdO(w_q_g@_R&|#evth*%qe1iyPAul@62Ay zPaJ;o7&T}E`3GO4^9o)|=3lL=xlFxOa6h7~K{OWyr|8488|v%HV}hO+=2zUKZ=SzW z-f(yUuy^KMpTU7tK6-fK;2%V;58stKuRIFBsQBR6dgAr97@9j_{fygmURBTSNWF;_ zy((PBUjLbLeLrUIO!2S|Cocec6NhZ}ILwoIA@Z4kw&a`eCf|gu*An@5c$YA5A0mC{ zdzQaY4tbGJ{D5M&F%2)-%o&(}rS>>@4@Q%JFx^s|XeT*jb#FIww%H3{+}lfNj{|S` zXY&%m=lHD}-mUkPcK77F!hcZp=qGR4Nq%RQlle~eINTt<%ivxg#wtEH!cK zXXH+BBAyJ-ueuhl%xFoRB4dxk`F6}1uyaNRAJg}Cm6PHB zAp7VQ5Bcq3%X)oAz0{h@bL1anpBMUr;Htr6V$4OAkDlLyUgUSiccnbMe7~|M-vn}f zmjfeA^LF^WinJGi--FC&C@+4I@}dE9zXAtxxn-U3uQm;yrFk;8ygtmYT3ngp^nQpjl@o-=@dWf_nnJulqbqfI?82l4_0nR=!2fYdK zkg<0*JiN=bAIIC$l!%@exF4MB!+VhTIIW3WyOp@L_z!|xyK!6^c}y^80I%@o4i;aknln614K#6z6!+uAh`0TJj95PZ z3*s{%&oG+y&fqg7mu;c_po`T}^JFj=ZIpTY1DcC+{tEYY=E>ZvJY94qbBF1?ay$GL zan;nE;d6Qq_Ktjp_Jd`eMpK>verNQ&m;(v!hXeV%a>(bUyy5IiW$zOAaTK=}IT_~G zV&1O$gS^LaitDQLqUhtmYx!P!5bbd$X`T$;gY27dqMjG}gQ|~X?s=g<$a&Fn@>=3Q zh&>MP2bqVAxhQ-S4rlBSOevj4{=w~%XV|_kM!W#b7sb7u`#5UejvVrI?J;5h;LXa5 z$46h^>9Vrds9_(@)A zG};?zQ>6Jsm?2Smki&;dAeWWyIM>A zL7rc6j~?$q@cO_PHGC5#;E8l$m@0A zd1Lgt;9&1gG;cThgX-Q64&-I=#9{Bu=hXn>)*>gvdmQwg)qc<^B{QQt^#?Z+zn$NM z@B+X`kNXw+IJ|ehUHOZRhZlSX+^^8{%BJ_=X9<@rZ^rkFbPIUQ)15fm+{dwqA~NUp%}sgny;@?abN!gtT-%zh?;xmBXjvO*`)i^K8 zp1873PfNZX+>g)6N00f{YwnJAhpL`D`~mS~{-OPY@X`C2yh|77j?#HiaMjv77hnD0 z*cTN~7p7-8>pVjO!@1=$$MbTVzSxGqUaa?H5pyr~S>svq0n{vp^+2%e@E6pjw z{7T)gkZ(uN>mtqDdGG9f{o?T(l^12s03W@|U$Mu8`B%7Kz4b^R2j7*4_~_X;(Rlhn z-8k2xegnO((7hes6+FD)$>h6@r#XZFmdKnI@*jMDWtn_eX3ln+WqqQ($v=o3GV}Vt z*@oZQ=y{pvqUkihiZbn;pWm~i_&MT!fK!D2Ap4!yxh2RR2VMYp!>`Dk;lBI_kr!2* zZOq%b@7$wrYtNnmy&}EhFA}GyHgxqP{Xyhp;G4K0`Sx8dnY~62TdDJ+_o(m8dha_kmv7Ir+T5=U4kYuCIe&H8bY7`F4tRYZ*Wc^hseg!fLGYI7oCQsmH48q8 zF3^3PEXv8KUaInWUDF<3AOF)4%jWMRK0{Vm;kdEX^9mIHRo~^`B)75Jr+i5FtM_Tn zfWGru`L2)`ePU*Y+^^sbudO^Mdu2U5*Ne?MA4_p6YF zcGeJmzj7cRGPoa@w<9M5pO?yu!ejEHjdv+J$8Sre&748;MZ@X4LjDSU9F=d!e^BMy zqsZ@!IfIAz2lo@NPx+mB-fnQVPw3tRbJbM79eDWUnjS*I>>j$|H0;-IOa2OFIDv>_Q>8DeViu`e@)!l zD+53BtqZ+I_jXS@uP|r89)~%Q%tOX|(AeW3C&T^0M%oV^r(Wv90c+ge6;4sCa3Ixt z5S}>WJ^1;&G#d`&d$J$Ie-OUZ=H8`Jy0<$D4|zd&jBp_DXzmC1ChV6*<^)Q53&+m9r@DZD=HahO}1H9XEQp6*xR$#5?fy$PN( z1WCRf9LRuze9HArc8ch+Y4FG6-KIMdzx@mG58~d=-xcx<^A`Qx`ft;B1&;~)Qjx#9 zPaH^aYqR@JalKjRe5Rg!sqpZE0|~w;`zDYVWnSM1Q{VZ@z`wgU5NA7VYlqx+X>TPz zW&MouqL?#)-@bzWgO5>fg6CI?*Z1+&5SlZ5nBkD(V*MuhmE~pPoA}h>6!qwrhuO_x zU+O8-{0e;>)uW%9bU*di>~}X$GEcV}^U7Mb&9oon_n^wR!@G1(zXzFr#q%rlCVnK}1adMRazh{C$$T!J zIQM{#((?lUDstlu8y~$r&D)U|g>M4%V9FXhwPDAf=<EH;H$HdqVD!;SxhBIedaUee$?%I1=yPG${PW*}d zIP=Iy&+{wAZ&x`Pb-(&GaG+OVzwxg3h<}AX4sytq)~-u#iEjdY(QeYm85w_(`Z$Ki zgmcJkav!(xrGirgt{U_DF35fm`SwQboA|P#Tj9!#7V;i6`v6eypBFrFQN$?%XPdp2`F(yG z;YE1{{tu>wZ5ZcGbB18qJHN90sBj>=5uX8GOY~CNH*r7Fmwfcf6Nmeik&|IQ1Lp0D z`;kL=2GyIu{E9i-JZJb#dK1pn$7!N@yW)OCN8OEYw7epoIMv5NPG+|DhO^I0y$5rK zM`_=Llg_upkMlcmYkAJVoNaK5oG354*(T3``xV}U!R>BS4!Qh)@cMY({(a1=!b849 z|G`PFcgdF;Nbft3}#**yi4mQa+I zFAAQ_8s8f6O&~9d{8e|EGjRT@tM~^o7d1GLfA_jT_jddT*%QZHHRT^fe-Lv9d{@}x zz&GLAJD2iTpUumVxhVMUAM^-tvJ*}bax&~OnW8w4q`Z)K=YiTDwFZglbT>?*r@9oOF1b(~n@Nyr=%ojz^OL)OW_50lkU9f_&oEn*D=oO}xIYy#r~FWA@Qwe&r`~QN>mJHfEsY zMS0FJ-t|G$k~$mhZ@mryf0g6Jw=6_?N^lsh7&%74xsAQ{S22gUGl0 zQJz8d2jQDQe-L?wqTs3WAB2C9^P+seTC{3u$*jFqCrfHtbzIly#0X#iQxR$N_gnfW zI>>*Jee^i5%7n9x-h^$>Ypv!Xt9g4S`JFEuze!$zJ=(hzCi8aeaWHScE`Dd@yBb+L zTfVEOX?}(NVDnxoczq3)R}xOfc#GGP@9lnDVsh;1-X1IWD|mPV+I1x_!2IxiG#BOl zpyF)bI9gclFL_ae`{8gU!0G)STWKx|--NnfWe@Kn`F5Ta2Wc`aW!H?CnV`Ek5M zzK{8H!ULPVv*HvX&(MNBJ|iM z;q`Hk9-Jb~MUCFXh2yt$f6(AxfzR+a_umG7Nbf;Dubz+WK|U{@x3e!*@nrBGWZ#6J z>lzzR+=&=(>JJvW`OJINYOW?gw%*?4t*#2)zlOw}Y$3=N0yYx3rIb zgUqjt|Dbvgs`F|n^(Mf9#D5UpC2+RKg?ti9G3ffm6i%cI=(GkE8m|e7`E8@9NKq zUs)CtuMc}?)k{sY**j0x=at#-3|=4ZSC2^_M}1e1dQ8>|w-#Oi%-fAVj^VX5yZ{Gf z@67kBycrMU9ta20);9tF;7(KD`SHWwkk=BtzB_-wtsPYxdur>xIUBEJJ-cK(`3D16 zPb;n8-}20D`mXRFM4kcrLG%ZceUijC0d8$v&V!}@Ng85}GkJKY>O6ztcQ*2(;Hu%g zGI)LPJHwX>o(#_!inTBG!{J?fPic4mMu6n{;ua0Dew);&z2W01-){Bsl^pWJ_z$9f z3mo88)bAbXU^mhYQ&YE13cPCfg!mYx^-gYeO>@x3%~sC#|W z(W*{|XUhBv=hgR;lkvH}lX_m@Kyt2+{W#|TAafuMt{VI3*<*tI)c~4{{uD9B|CGra z4n6~W!<$ksWKR}vID0LTXMlf@dlTk-yWi%E*$;)YowRCsN%-CiCrfHtb}a65V#HYL zrE+hA=M308^LvmvMcCt{XFOfFzhcI*lB+J_o5+=3s%;+!9LS%Ge#$IQ>0uq3{6v~d z?)zJxI($&~2k(U55Z?sy?TUvSeX49%_-vdAik?|hvOb^4=U20ILISb zIFR@czDu0#d!1eT2Y9bH^_|WA!HL2{24{P?_Lvk`Nws3&O$CvgvxL-vI zXM3Xd#Hk*=`mR)-!Ns#jzzdXTFnBVryE_OExxM6&*=xxh$V%$tfX@K0WrOSo?S?kl z_?HdyN7qoOVnPiC@fO`X%3hW)|X=hbb=4e~DW-dXWQ zIp2PZax%XSe9gUu-NCA-4!=+RLGZ7zcV@q{>f@j{Q9bu9;fpGsOzG9B#|~F?DO@Ff zoId1BwNiiZdylb$3dd!IJ*1pWD)AYb=lYuSkoi6MXs*wgi^A`0n-{%8-&G;y+kc8! zM(;u9Ga%p29uv;D|0lVH)j4HFhFjskDyHiGAbJzZKWKR3e6F9P-UM=e?v!U}*USq5 zz9{DH;MU^1;=OaksS4phg4f4=XM4&sz>o7+uS>*LGxt*0y7`bF2RvlOlVPqJdh}Ci zex>H32KOVJda3NAS2>x9=~D~Vl;1vTe`SjB8M@LQhy6Iw#AgU=cjHFbiRVo@nWm-S zs+m1;V?w^y{XzIrl_w6IBAi!h@67)}-VeePhn`opWjFa!*Z5wg^9nf`_VE6Lczyg` zG5;!q{DbIujne!p=4@}i8g%SPMd!k-3@19T(8sYna=+p^1GpbQP|r(wEf>-KDj=zW z{5Xc+d79=x=8NCCJ?#g;;%Ed{N99aK8di#{3`LLisD6i-P;%q~{FirP@7b zPw&Bd#QnH1x5La2CwUIb(tc;o^`SSR`p$>sKe*cWp571QJ&2wc?+3BRxlZSmW0F&9 zC~>y!sXv%UduQI`;C?kOBvH?=lrQz)Nkiq{Zt!Hdmx{UQx7T>|8(#Sy&p7k z$i{btzB7B`;G<8o9GEvZe7@fb`47VHZ02n9dl0?}{09?##)TY@c{QQIawmRvR8`<$ z@=d5cPUvdKROh6v)}BkQWX;~VjsAl>G+)%%JF7VZ`0dQUf)_yLMa|sWPL#h=du}5ts`%Ef6W)=dpqB+(4&8IakMo^d|vMyTumHEo{N^!oS|qz ziEux_XW+c3tuGZmuOga@!n=g;%It|lt`A%_=Jowsdi0$O|EY6*YA)K4=uO{M)+DcC zD|%JBc*$J!$IOZp59^zHE~@&2*}k=*H*NYj9@0z2fADwRo8UPE_i@fmnO=$O*R`gQWF%Ri;wL~!+XhZE#W<@pu% zIPA5|>(fDhSL_8a`p)1ppyw6nT@qXzyM5(H7)O#d+mMygu%Ev43!CyBgUKKCivuQRE*4w-y{o%o+YK=GBA; z#J|FSkbM)4ryYqe3Sa6jYu6<=iPtylly_~J`-XEPc|GGk>P})&01l8MzZ$4z2Im)_K#_kYk4{o-EAHuupNZ z{yVwP@`}9qWhun1OLe|odBe@~cJxwrI83QtcqCE2EBFV^J$l9c zK)&7B52p3{%*9jlMJtJ03m^Rq${~Z-hkQH#2Yg>${x&?&kLnt~k4i-h)Bj z>x0(|pJ79Ek>vW;KhhsuM7@bCC-ZAMbX?QtH2HCm>w7iPZcb*{x^ZKL18Lslw359u z?+48sNaT>4->;0lGjhnhAH^6ek>8|qaR7#(#_ z_RfZP>7{eq4h3zA$Z0|ELAA$G{44Zvz!%N16q-D7{!UI;t{+`j?x+3E-1CATXR^}= zJ+^3HD)*f+zXE3)JeimFY%P9aWnU?J&xnh{0W6K?n{sUu|MRhb+e94 zZkN_EH-vJ?=z0BSx?eH>s=aB>5KBHU@I_lnzTNK3-8w(l?0&<%&IQIq-Payo=Bk1F zv7P!jiu<9sYIn((x_WpVdBfABm&%^FK*=)%5@*}I$8nVW6*!Qb>)S_MHSqdE+ugn~ zmGTVi;r%AbVOeBOfV>9_+^hqVeBKWEkNCVIDKCn?^PReuXX^J)D-BrPntZA3AA~1P zacjZZMxFul_S>PkzLy66-Mvw~OSbu|Ta_1%kJjFBV}8XwdOOO=@ZK4H=T_v2+me@D zmUv{U_J(H=pW!RZqWM2Zc=`Wi>f_rEWkZS-;A zn}{L~wlA9Q?Uh!$BxgFW?CCxDp1!v~h_55RGv0%ki(-BS9x`|`hm-yx z97x6MV-N5A@cr^#v3F@^k16CI%qGMZv$~d;5PQmd^h|JSOZPRQdKTd9Ml&d1Xeo!h^)wb`jr%d482a_x4Ki#IZMA z-LK&D>T>vCRSUbUhA;IU*TR0UdtKIiQFAZ#1>zwa zy$R0s!4rpk`$wAJjvjrN>6^!I8XVDMvUmZoAH-bL=%q4W6!UiQ+wooPqrNltgT}m_ zzbkdWs-QeWelrdvdZ}hFz~s{RS345_3S2eLA%k1%(YK9fkAUYRz2YxeK1-;Hy%n10 zdwJmBn*9fj9zFL`vwDphmNlvU%uB>6LT`e3GHr#=pu7OzB=?nGY9PG_nTO2xcE#)C zy))-U*+-8)4(COg*T;Qlo--hSW$+n{{FUmZHs{G8&!FZE;K`iuD0i4bp12)(FH0Xs z{Rd00Iy*Z&_i)Ji3AvPS2mcCvoH4^z^xEO#d;Kis+p%}XoZ+#wwz*-#{lNWdwQqgs zud!by{9^HTc`^nMS>+kP{QwUc`73aWz-M4yALa}V z#200LJMs+Zr7{N+`S#yw-j4H%Jtpi0sFJ*BHl0_Pw}Xd_{h;#jo-evX-&KX)<1lCY zKILREzruIL-lg;mmlVflygub!dX!Uy95VM(|FJm6Iwi@D@}ihu!4t=QXXT?u9|xT6 zrp`t6A4&cS^Y%yiSMczj)%SMx#2LK_cma;;`xSV|8?Lr>e%Q0c(7XxjX4J;nTSi$f z5dUhd<})ZS0Osu-g#(GbGy5huFRDCo*yDg(8+ocib3ZU|pDI4Ddv%jsbNUVSIv*Hg zb8k-?kniRze&<7`-2ilKa&}$stcI4K(#8W^LRdd-{l;#^PiJ0pK(%td*Ag&guX`q)9|!X*=GG!7Q%n893$n-Qh~Ifld5G|kLp1jT zbI}<+wl?F*%=Y_;=AyMX0#CfW=hNceD`SNFf%9s|p*}PhO(d@+&lwCJ@?`SFJ=j03 zbjs?s^d7|gDstnFeUWl+Z%6kl@Y_El?^3q!uLB3WH#B`+<$C!2t?g-kNj z`#2L{ROOIkty47j1AXVl&K>&)d2a|_r{|)`i#}%Z@S>L*AonZuyx@0k*D=4Dhj&HT zx^X_#AH=+!eO^b%3xHgo+7CKB*D$1bLf(venzth-gI+4~qGPU~JAPYoefX~0r*+7k zroSupdEJzpOmFF>?&#!6Ipiqe6geb0k>45j_S(AlsE>0gaFz`R66clTKt@p?hxa%* zued+hg>o|B6sbHz$@otPzfT^Mn@87{`xp49Pg&T)I%VPH^eLLNJ*&qi;)`;g0rxBO zT(sAooyE^ePKNzBMotEMXXHiei35q=gqn*w90)26T-`SHq4hKCQ%h>H!Z+@e?+V-x z58_{45D)K1+G7I$V4K`dX>Ss@R(Sz(4HpCn$(@#!J7cShfNdXKGxiz&~b-h;FKvQ6BN9hAR^1Us6oKf-5%e)==EA%FCUUj26 z19KqZqgUKoBi9Gc_W$ZlFi!?OFXWIvq&?35dCM(DiZ} zb!C#%tRAKE9xSGO`~2`Yzg5HC$-BhdTKI9m7quf#Tsh4d*0?PkkmTb-+>d*fhwHd`kHhmT>~Y2nTREwt?45Zo%G}xw z!hvL7-!W58#`q7S=cW3CoI_q$Q+;wK-LL+!I8yx1@P=nee{iavi{gHz_Bg8Vj6Dwe zIR99jqPZW6e+7>T=a4g~m&)EHi}X@Chm1ZB=aA7KR9=7v;UTY>G@5)9-nuvOS&Ez9 z4}w!@x5Vew$HZ@kZz78NIGjUPJ$m$=b2Mk0`B(6mfPcmOcK*F~C{)29^$9ZW_na*GFdr)y8 z)qW5?FYsj0qc?o??1=;Sqx+JZSrIlqFZk#U4=>(>ue;lmhnL@j$TPqj4le+EOjb~T z5Z@KJwfE!liO-<=ga0H>5xh%BlV2gOnlTqe&&%9*1`nBgsWa%j8bO>Q?mJHsUS9|5 zd71ePA(C%Lo`LxcK}nG`7tJX7U~l!wV(sDO@9Nb=2g)JCH?hrOs?8pU_k#y3-lz8< z`*D~j13!-I%(O{wQ-9E#_zdPfjtlv5_}+ebV6N|-(BHLxQ1RQ*$H83m2K5IUtuCp- zs{>0NOdj6qzzCW%4A6WAqwkFG>R8g>Z2Zp5{XqWe^Lfkkz5V0CvwBQ)a(;y0Zg93E z#Fq+hxY|3f9_~gxFV4yQiM-*D`kj+Z-lZt*AG}xBM81h&;cO!>3QrvN&fDdFg`OAw zgV^JMCxf}@S2Ab7{i?fgwviV_-#N3FH{Gw!6TkgoT;7Z#>JP$?gE<4ZAFXUSMet3a zN55&@(lDnvuOxnDlNVL_EA~6X8;+irIICw{HTUqcA4mDT%$y>40l-5>zWtQ;TB`kEQ`6t% zJ*ardFO%O{^-{~JKd5+p(^gL|ZE>KH<_v3W&MTf@p_hs|!?r`gTW038Ty|LUqGk@H zc`nN5mGWA$H~d}V$ta)KP1)msCxdyr!EZ;;%ka_nq#ixySIReG&NJ|N1s*c*ow@JK zTs6fhijqCfj!y1|kny85Qh?fZvNo&jFV z`~{x~pP@FdUC8$8`6*B7|Z(KKroi4Bd#|&hsli zuO`rYusOfo;MS%~Zvt~sKCgQ3*-n0E_N5{(s^;x#?`+GH@pru~o;YyT9FlfgyDhmz z_p2!KLRSE{?K>B-N!}&7QFNT{^FtbBffx{ekxI zUTpRsyd-nc??$}s{}cHKk&{^wRwOwY@Q{ENIo;dg1yJ7b7ir$Eczt|c71Dd~W7-dbTg!6>#Y0vcNW2Fl z$ir*?4=Nt=9h2V~o;Y{`{I~3A=9^IWcJ591x!%$KL9@rCO!m%--;SINxV3lX-i|&F z?{UDtLT}<^=6UgXVJ_-J{Xz7d@m-;p3O)n!qG8gbcM^{Y^6iQ*ikuAQSID<7p8s>i zXyRYB(f2FPi{gI8c?RT=!P!P{ViV;V3KwjWJr1~P@J*mMfw|~xI=WtZsNTdFIj@ix#ooD!e5ogd zs|N0e!IMF*uP*dAy&r_ni+!os-W*{F1-V#=T)KSq7HOkVctHL_*ckZfiDU_4!i(=BaaDtc$qH>o(y{dw(g4+Un=qp zFVel;$cuu1#q;)dxgF9*CUh!AuJWSDw==(eUHwOA#}ohRKKbYkZ#eSp+;_%) z(3mqQZfzp<=;4V|{XuZH&3sY#yzm};V&-ys4}yPnPWFS$lQD9AO-*JF^ zAx@Fn4@PdhmeoUgslmi&a3C)LczwSHeh}3ZUrXQBJCu{jbsHx=dLxId_@Z}c@4RMs zlwTtCo!2Ke&3StIy7613H(}ey;qwajtI4z8<_xx;IPN0zF+Zw@Y8*BHvXE`Lvnr0DQY5q zdoFQ`qND2K9}=gC^P>4~?=(DruJX`(ngglMEBKv*b#KDpY=bX4lkV-d-<9IYAlEme z#}>-Bw~Wgt&bC#!weVU7OP&F}RLn)WkJIbiyA8QCziOWA%kTHD>pjUKqesv4_TRE% zH*O{V)y_`c`?hW7^Wr^@gWbU@x5LxOH^F_JA0;p9d%fIcMK7OW%O^cK^B3_=FkjT+ zxt7i!U!5j?XXW$Srnw*BUooHIRq{LIJ*d2vziE$2lr@n4gOfH-Jh<}gCi<>;E{a?q z@>j*elfk^5`3&sG;hvZBJFA|Tnu}&?etR(OanN^0Z$kO#rwg~%-sBC(dBuC2-P)H5 z{*}9R1nmdG>-%R_W7FT=FAdD~y-m5ki#mt=Oy4ritwk@jT<1j<_XB+#o?n^y448}d zq4!{<-VY+*-rJOCi1k|#{sqlN9ppaFKMdnP=oNeayfdhHZQg51H)z#UbX(SJ?OKOvqdlQj#UU}A*6Q{_0Ue$!=NUrZI z*$?u)ojFD3oJ=P5QrQz%NPCi`dr9kg1jj53|ng3bN`)uUhIkE9zA=PjNSzPgDsO< zq=smo4Bmr7B;Q_d`Afooq(2Bg1Nfrg$uyeg?Y;FL2R(ZB53Wmei0eLmz4*MqXFzX) z_k+_GzLOp*UI5OEZlL_tdg6Y}r~M%2SK#&a5{e(-6UxBp_K%Bg4SBTqF4h@5VV;Mp`b-?J#rYBp><@j=Elb{AT5aqVF>+#k=G}-f;LP2K|pb zLwTpswX;s`BpKMHT%^XO4SKxlkTlDP8isI)fCxbl> z=A!5ig8QNNIJmbvT)BNTw>(I^mf+U-I#i!*PhDaXI9QP z`MhlB498*yCfv0=h>x`KhNJJSczx`5#(vPNcDp{Wyy?4A??LWOaGrs4$jptHC!>7yoEJ6o`rr*eVdAQp@9p3rv(JnBIJjS3qIzmezN?es;XObe6YK}E$6+5m^6mJp(DP!C$@|AP zU2RP~nRUXg9c8Ja{~&UG>@lg)IhhgUi3{8MHhb)SRJ5-))^n%YG2^ zs~z+o4AI^t#jTww{lWi}`4#32FP+;?+z;#r@m=*Kj|uKqoEMGtn;(8)-crl@L`PFz z6nh-yn*e8dfJnZ-k6KB z7ho6N+xfiWxo9Wa54xIoGR$W{zP*p`rNWnr9{tU%u1j`Vo2;$r-p>9(?xkK0oDtO& ze^>m2Db!0vu8(_Oj?2DD8Y<^ia}O_jOj^Ztp1yf}Dc##0i6;Zjb|~fB-%Ix|SXchr z(biWc>O6yETw!9S_F5W!=k2r~WbaZmy$6{us`k$4rQVB=i8>!RM7-hl+KJP$?)9G-;R_ENu(ncmfW!;-{Lho_#9~?U@UHZ=8K&~};!!d7n z)B8d7U77D!oI_5Pd%Nm8hf#kJy$O}SQXXD)zcRin@UPIDSS>tc#r;TLm01$D_v*>D zH7*^C$P@QNM8^ERmVt?lvomOq1Ft1=eTw_R^DFjwA%BIO41DzHr7Hj6Sm`^%H-SA4 z@>l;L&UVPwu7|&*9I`RL`q|=5a|ZU~nEQk1-tPUYRPHo*J;WLDbcZuii+;{GC zZik82$3A-Wao~5x{A!Nh{O~Wd-`VU-%`XoYetYmjKkdg+`F41h*3x%{|6tZ|_ul^P z>gYYVUHZ;POdLpqTdTa`ku+ym+wUFM2jVfo-kH5i>l!wUP86Fe#ov$BlDLwiPr33I-k--DsT7d3J+=y|=lIED6uSLwSdAaD2|sgFU>+i>jQAv3JIP@Z4Ni znP2U48FT%Y<9CGv$$cE;8Q@E0PaOE|=sRDsjFkOg_r5!YTMO@!yUz6)Ihg|CGqB&; z+@ptw7yNd94`P00z8IDpf zb;_0gC9_aO7zGcvjt zektb_ya2p+Zc4;|kmsW5y>^n{8Q!In;X@2z*EyCqTnO8HWm*9WiV{ko>p4s>3z-&y65qfU*c_h4JkrzzjAd=t%cGM!AG zxLd-%f^P!73FdxaKUl2&ILKeI7Xbc2m6K8bLHOv=OZ|epOYrcnr91_mimL>Y#aBh%d*EY@}fmL&v1r(sRI)q%+3m1J8s;dx0LHdlTll=;xGgM{mOH4aa^E z_jdH1RbCW1nLJZ}5V^jwAq#yH2Nb!DY3O@y>!F}6vvM4W-;VcSx%j;B9|TVZoNe?b zFlSKncHG9 z@v(Go&+RvXeDsb70!pW@Zk=i;ewVvjR@tBcNG z{h0Yn(Vfck$46h^O?{k4=M4NFM30`oE9FbIlqDQ_zxrc`pFF$_Ync6d;$BPo z{8JG=+8h2Woma_{7psww{MqRAG}M>n%|Bba(2I=UKcbE+2Gc`Lw#qp$9cna zZwIf>*6$4d6?_vahm3psyYwCmTzgXQRQ9DtQVyBVEABh<{OVflT@zOgygv4rxYK{I zs_3@(yf9~g7ockHXj2Z^j`CMw$G)QZ75G<2s6Pl^AJ0Wc589ylSLi!qj{|OPLCqDt zca9;xDE@=vHNSnTKCi$-h9?gBE94o*3!ec!uYW8~T9rw2hJ5OIu_w;xJHumw_h4{! z3i&2p5?&wsCPI&GzS`2+;kicbT{83AdESou)sLCGQ{1d?CAX(}yV;Ke{uTa%BjrEn z(YLJAnA(U_JNM1fUQ71GA%6v5D)ZZUe#L%goLB5i-L7+e!>n^@e#O01_?@|rqx#P9 z4}LOudXE6&6u}$L^LF;|uF-yHgImkr758yM+TFS_jppt6u5ezNdtQaK$Kmhl0nM+f zG{2qs4B$X=UX z(o4l$)Yg~!5&0%KFZu$VSD1^AshxFd2l>3v^TM1Vuo<_un)ssVd3_h-8)ETE8Ss(Y z#D+het0dn<(^7CC=L%085A<=i6+ch;cEyu{H(cGXd@0Yso;b{}m{Y|0cIJ!j&^(z)Ij@u#fIYn2cOFcf zZSwE?zEtIj!~E(X-P_0de>Y-% zpH>}<$ZHAy75X^fs$I#RK=Z3j#Mz$Wy&<@SIFPy2^TNCxc~SN|R}rTu&N_7Q65*=h zKZtz0um6w4Rf8WVgZ9q+A2jmq?BR7MpV!A%olV@2hm;rXG;^iguhg7@@9oI-wbk6( z*WG`md%JBf6+Zeu_75V5T&w-gkx}=FfAxLL81m5!v%RzUPkX{n+`KWZ zoonx$;W2vetn%&1i#kv*Rq>F`UQ71Gf!7BgJ?`xbB-a<%u1<2u;1sFzivNS~O)MC& z#K$Y-WXzu_f2IC|;6U1+vD03FGV9Yzey84q%8Pc;??LuEf8_hC_~@C}*FpR^$o1XU zTs7Ze8T1}heH_f&4UfrFg{vsncP#lu;q?VoZjH%OjAy$8V;%@S`oI7QKvze>}*KFk@IFA7c(xN6|ZfX|?Ospw58&h|fr zhiv=@k-s`O_sN-Qlg15OMtKH!OfD3i$lR6UVSOXnHLX?dOq#c&H-Wyh>f;#sE97L* zAB+)C9KQ#Rd^_&#IIrr7e}!Bh^RIjx`pA3Gj<~h(rIwjIyv$YmY+j~#!?{0b<}Unux-|dnvdmP-aniB8E75zb;0eMl( zMR|{7%-b_%E(*S=kN@}7^D3)b`LX_?+PlR6LG--L zbB0FZGZ^`Ho{RoK`Kwgg5B@*C&c?sTvi;+GHe26O`+oO;u_xQYZ1!l@TFlI>kVr~$-%=9gR*bQfu*}%w@AWy3^E|KXJMQ2AaK5hV zJdfk^dB5SuDRgrk`-85Riu{%I(Ts zCLR;?=xWAZu8+h=(Hq??PH&fEDv zSlZl5??LcIi-}t+y#Qu?Bu_^2MZx{x`&FTO5B?mQyyA1(52l9{y3HJWa?oxI-@5o? zhRPkeFQpuf?`E{>#M$OO4tRYo#QjKBJQ>q@yUdGnkNyvui%K4{^zd?CboYU9`d(ET zE#jEpuJv(Var&M1INVE}LI1%ml#{7C;!8Xk&bMPOiu~2zHv);zz`fK|@|X-DULQEy zydMN-TlR6Z95Uut=y_S_JiIb5id-M|I1#JfpgF@5;vqkOVOrbPe$k^>yEv;HGXDoP z4y3Q}rB+j(0lB`VB7fC=${^q2!o&MlM3eBu!SDQ-m@}Y1DEm0f*+xzVUVuv?CzD4! znYO38i9QbYgUE}17eAah+tQDtdEySK{os~i-6?-He}7tuG4t)!i;Vu%ADpdtGR!F| zp`I7~&fpZW&r5Q)B?mG=a6j^e#{`_B)hoWF-URoZ;fZ6933xJ`XOQo~kFIqg?#EV- z?D_3c&9T!BwRCUioDAj+xVQh9zAv$_?mfso`fFk?D!FRNw+|e#yT?@F;k`-ucG*j1 zULW!d<7h6*dmP-axQ`=qec%*f?+kA3&f@3y`Y3KK@>l#Gl$>qO^7Md8GMHM_jv)BQn@cN`T9R9&Z%8OPP zKamkFax&5r=hl|jFLv}QmuYlw2lrzk@%lE8oFzDr;9uE^{~&UG=;N>_j_2**e#oAe z?4_FC+rfdnQ*-|0Ov)ior+oWGF~7>2U+B>;di2~&g%?227Y!mGy^Ht{;@-Yi+^>p; zb#d6*P$%;3Z_v5I-g(x2e9^*{?Sj`Qy_R?ng8L!;&gi8sP+kCdc+Z5UDjz-0)z5>< zEkZO+^n77043{PBhtc&5ovPT7D?hRy=z2o7#5KcZSyz za|WENCw#k2dBWs92w!TKI7{LbAt%G!kFiemy8oc5kDmR5?3;kk3;#iIKY9<{JZX_v z!or&68HNJ#(PKXdUuu{7KaOv$^33xQ{z2LEvZb61=2y&bM@|NNoNVC@*XCDm7{89Q zSr?k^v%Om9n|LSayCtJynhp2Jk0UvdoqP1iU$KuKc?NKb&>y^DIH+=ce81{EZ|6CK zJRdMP~NY!bEWq#F;@-yL9Iv6y$R+N!IvuMqV^{Dt2V)B zh@<}?bBea8y)%4XZA&vl3I+d)^P&}$ztT8GAty?ym)artS1oj|;K#xDihEx0TJoHM z--8P)dMZv4d=r`{ZlJHjl>Q@Xdc57Zgz^l3Wew8h+s##PVw}?z>JMu7D}$Pg;@+tu)P9h;YSKrK{vhWWaK8dq%|7|<_w=9cPmiupy|d^`MoQ zx3kx>ndVn=kCR8e)ZN7EYg>DQ`h(A8>?j_w*O$EE$RRJGbCoQ76LRltO}$i~yxFNf z3D!m*^2EXCWn1rI9X5E|xR2d?&+8JEPn@C{U7jK6)HA9#aXhY_?pL24`Nsu!@`m$% zaAZuI+7BWx%6taoMUlTk-x=vkF7xe}xAWe)RB=DR>yvZQK*6nTIBqVyOCP%Tnb##MPw-@x3LY}< zSNt9fU-g#pt2nE5VcD~(H-Y|OJn^r7T=IhO#MM#$O7^^b%jRsbOtPW8DE!Wnf5m)J zaMdmu_6twk@4}CR@0HB;!NYq+a3FbpCFcz7vCYE6i*r?eEs*Z*vX}aNN>%Pk)uVSA zTj-V<(z5iG*v|~9E6&iIVGDUJrQdn7?tA5NIP?66&G&lup#7lqrNYBIN6g#d4fig; zxY2B_kJt~cFPXFd50Sq*JK4$m*U;7En?SCw*v*BwYRoC(9I}n_n8^R23E zr-_+Es_a~^|7yam#higXCT$(BIbNmtmFc{lJ#mZARvic{y`D8_O|{WnmuFxOq;|g= zO??~#^(JJ05WUoI<0cxHuKI_#x5Jm(Pv^%0{|Y%7aMfm6Ti4$@o>LVla((DcU@ppi zXUXd`7d&JG^#_q}N3QP~(Z>O=&um>p_AGI(I_G5IAN-e7p2&-uAM!5?++ag?}DS%K_@=E z*4?({DJ$a1+=((bOfg(0KEn*rOO^BXy@~yeZ^rjX?wYee-P?KIj-D6#gUtOHIHJ18 zl*T1Go(%E~0UP|4A18-$GRR*^FMxqOac(C4LF5^prT?J!sg@gF_5+7zOo~!mH4BfKF%||KltNu+jR3Q@cObQ^`o8_zE|vt17{n&KJXcyyWrZE zr^{d6BVHeR^zaX&M~``Xh{|6f*N6EPbGDJ|dv5XDe0Xl499{@Ik8++~%kh~ExRT)x|ekh@F&6}#7vzTzk9 z4;CmsgXVYUJQ5|2Dee}3rvDXrOhQ}VjLw-AYIQb?9jHssh6?>PS zr27@;89EM{3-1#6?F+-&qi-8ti8--krrP5SzfgI^cY9cN$J*~Ghpg{;>GKTi$ANe0 zee%(xmx^2;d=vOyfm?f1>~W;etC04C(F-HJJ|Lb9`h)OV4xxGbaUH*%|ARbl=bl$s zSgqd#6Tfp1c`cdW4jwZ4gOXDOt{VP>n74DTk9jieiF;Fcm(CY9QvT`_o!1iltJ_gq z=jV7>O=y1#|G|J0-&FT5*pOx}ax(BuB+$9~k#aKfex>d6$su!<@m2I7Qqav^4rEzq3{SEa9W?JI{*d z?YtiZXPbLo&l0C7Qs*DU_X>NQxY0%zH|h`GHnbD>V=VRPkr%~((4Xd4n73cciY(nh z->cm{9;Y7tvx}!Fj|q5v8V9nwu0O~gUgQ~4hPYBrhUZt<<46y0>9x7U*=Db0XTB(U z6YvkhYl+?j_Bir>6}&&QBsTNy)r%>ACHsS=!sk^iaxyY6s`b3!iDMr<@}kU>k$Hw0 z!o$mX2F~?KuO)M9!TpdP6Z8j>LoV{j5$Ec+Wjz*TdQMfoRO|=&d&Qih_vt?f{uT4v z<$v%w;%xJOFkE-PGK=e$$>4s)-X-R2BY&0XGCk-A z;vqBl1N%XE0W8n89Sl^xRCr9lfds!@^7=+DeuH>@HzJyXKOhdImDv}y{f`D$bkBJ- z`StiGjJp%hQeIT^rK0DBUTUl26lpn`ezYIN-Wfb(%o(J|1p7hUudv7YZ|F4dv&st~ z^X=>fxV&)|-P^b6I7Rs@I|R2@yI(P1l>LL4GbESH-~YSV8d|yM?Ei`t5%Vdc~to5WgmxoUeaU2oFaU${v7jxYm@Rj<9p>WWx$9%J*LpP z;{D(*`d$TZ@F}xU{C3>i7lgH``xU%P;EPJ`hZ*e$m#unImy_u^bn~QeF~8Ds$oO7a zQ;#0~LF}DnPUf%UHZ*T9S3P=gYh#Gl*XzDsDtI#RE>#^FexEND^LEK80{^O=@}hQ0 z?Z!`q*Ao9hK35LpAH(k$_?$3*IFI78NOU~}3`PGxeDMD|;ugsi!^d5>Q15X_CqAo$J zTvm=Q>gU`x!lakVdC{SJ{ZBPfzWoEiflL=%HGU7`T>W*tpbB09?*uEOZ-Q5-!F4=>-ZBo7&T=kAJs#ksyqC#PQjRPh-k zR}FlI2l21|6F*$d8JOR`Rq#dO^MZ%>C&d>9uMfS6NTUz+=slFz@)7dH@qVyC<*%?G zgm-C{+T%$6RsHFF@g9`-_J0yrtwhbQz}c=K{uT4v;hP8$Ts3&Z3+X-hw&ChB`vo6( zP9@H^3+-{Rcm6bSfN_%W0`OdvxwV`_h9_<~^__Ws1vA6aGQw6u~#4^-|5E1}W}`#%BmH*$-OQ&$bR7 zTsE%Ez0W)w(RYrf_aOVcG;g@%et2%Lp*{}IRieltgR6%875BWrL$)~Nr_0IUKZv=g zcCOfuvoGcr@kQC^b)>fM(cp@Qa~?{b`~Yt_?pORD#2$xveVzY<%-P z;`>3aVt(~%>^}0P&hS1nIcJ!ILr%jV_whxMZ-*yN>*KI*g7fWsZ%5yG7QF|3<07a( zsNJs$hMk)1=KW)6y4nw7es%hP^qs#_J$mMg-ivC9bv2w{)_XxR^(OY(O}&2UfAUNCi=S82R zIYSiP+dI}S&Gy{Bhx|AN{S2eyU1q4Bm&VyfUbI8(oz1B~_2K6>;A!50Nj26=|$Av0CJy@fd2?1|HJijcqhxA0BGAM>xPwD@t*boyR( zEPYx02VE$CW$&^B$)?sdl4Jx$EI!Pmb3(5y+3 zUMq>KRvc?aoT44V8-DZnmMVYIOAShJPo0yuvFgt8j;nr@zuM|PkT~0^#AgWe%ANG+ z&^h+EZUiY`s^;N^#{}~$&11s(b~$Ib-DE{xOU}3RoI!H7!M{SDLH3>D^U7Af3En#+ z-_E^M&Wkz=pBMM&rx+{@jZpy9o3wX^cj+UVGw}RMdY3dF@;`(p zu3g7(=l5C?8gDWopUnqr7~Adey=KXW7Qsq{m#f=ePLL$;=km_LC78UfTSM&y|+Hnn&+J@ELv-a|X`0^BxBt6UjqnUY|Ab zWUzPMdhKD`dr#R+$nhwee@AgYc<)?eH%)kWf9!N`*XR1+n}Bx-c~RzU!#}8bOyGCc zb0ERLy5tuYc8~ZB$n}9IBRNI*5AwNUPaL>v%>CeAD)Sl4qa5dLb1&Du2YvIrQvF1p z!7neb>c(-K`uD8E2A7We$i3e@OXanMH=MlyL4s4Xg81#o_2GW?%!PL-*O%xrjqdFo zhTG9g6;B5Ic5}gJu%!OrpIJ*vcM5JTdi3CaOeM}X_*dMUXz%z&^>M&M-kbPC`em`l z!G93F32;BaRfE@3{~pxNmF6)4UliXf=`lHANTK-^ax!=i)}NlYKdmHA@MPMOE^YKK zpGkY1*4{lv`8yW}l&U!cda1JS>>+xoew1fO%uFqrOPnHm>f>m025^et1z?^G-`gdp zNbiZ`@0H%?g>wb|mF6+Qdk{V^+nmsfM~;45+j`&p3VY{=Y0fZ}?(K^x-;Vtta>#*{ zZ|DCY-`m-jdUDB3>UrVb{*3T>$vgvlcrh2neh@qvO0Rms*v&w(TcOpxjwna`47z*yvUE^PJB^#On#ugb5nFU@vo5UyWM1cuJvGW zna_r9)OQ|4d{I7E>|K)oLEPK_4&LlqFZRy8j=nE?sW?}s(rXKE*PK7;c72y!#t`R; z>7G5QKM2lto;X+bwk6l*5%+`VqUaA+<{F6mA$fhNAr_G@#vV4TReoolw_|?Ac?P@( zuWo#|-0SdqnqNIUYL0V(;Hnj@EF-QO@(d-!7k#h%>c-}zK=OITo9uBkUn+azO353( zllY>NvyHxU8gU@8cZP4mVnTsOF3m+}7=EMs750OCuHbk6iF&D1;=3jH$O)|IfAlc% z`Xs05P|O8&zv7-3<_z#VGq=_{DVY9)@DHNz{JPUczcAVlz8v$PCGJ5nE(zpI4rJ&5AoA@|#__9@M1PRy49GM57Mi%?h~Y(*XOKQG-s7;(OZp~Cu64I< zr(7TQI4!ZxlyA@UbefnkWH;TfJ`KGpw?(N85@w}b+qR5M~Cl2T8kF3Yn z>^7PU{#D08a3DQ;6%|G|6WJ&3(CeDwHU;eLgl*LXE&@K^J8a3Hmuj47W1->V?y z4d?Hb#@V(K-z#~)LJk@K!D`_jWbOy@3`40u7(;W0?}gtPb5Xg+F{A!q@CK_So488i zesIr=`Bx838A$nd_R;hA3f}N9Yx^G!sOXW?o%YV3B%V&MExcKCnS2w>7wx&=*JZAT zX7Wv7er5i27s|it4# zkAuE5_Be5)bNji|{EEGn|DkimIb__g_&o^j2e@jOGk{a{v99OU`9H}0LFtKWkK87D z6Yx#+85(GR`$oX2=l1R_el}z2s@KW81nx)WkrysZYb)v(Gde-^otcNs??HHY`8~)U zlN#aS1qX7f;330DKP~!#;d9~h`a^LbgDV^;&%k|Wa6e=p2lw_~3oa$!m$dGxDPA&wte1)Vn9$ueg_r zTwgx%8Q^zjemnaI@g7XbOcn1zoU5M%_v4`255jASoJ<`32S*TR+nVlI@B)-QfU73? zSKzm!M}I-^`rx%>pO^HdZX}P%zL?A8wH)l*o%mPCA#2_x&6f&44)gkc_gkRjUvaJv zJY@JL7H3o2KCFPKL zKL}qcda0O;b|HTIzU48(3jiLn<6!pd4+mj@GikekN;o|@njlnE|4dV`Jz4P zTyZZIUI2|R+P@$(%`(x>_$~RJP4|PE&+C`a8KRe(r{lLTHU`EuD;_fTgOUS@?^S`= z<7j(l$rlYbPF`(PJQ>Uxupg9jhK{B1<8Ut(xjxL>P5JHc4}t@U9P*a=tx@-4T@Alc zZ^D`K?fkux{=tdyJ(IiU%&+Ku^h@IPp+DFV+z_!Z<`Q{$pAb1@Ezj`8nkwq022$Ty zazBtm4hy>*-D(&ab4q-#q}Oux_9fY7!n=g;)pL7&Pu;uWW8a_lII)yx$fKUuNA9KL z!UlU-+t%MWoe0gs;8Fhj#=A*Daf^*_>b_T|=V}z)uaN6IMO-y)KZv~O`ZULa zL)G(@7a-rGK$q)VulzXdAN)Cecj6Pq3GqFX?Syx!GiMuqXLwB5=Y{W8c8NgLDy-aYgZhK__i-#mu21&pxtID@ zV-4*G`F>@(AC%su?m3UBUMlWa>`ToMo;b`IG|o1BUgg(n0)a^ovk z9t)`}7yK*sh95GlAr9o($$7+Ymz?c4bUc}N$h*`+`@zBo@MKCwFZJ@qjwEmLF0IL2 zCwMYlX)X$n$yy!%iu3L8O-LVoSK@3BAfFd=Kjgff?^p1px=ma=WS`yC>z9d#toJU3 zRP?3q)ys~5QXdC>=URFXK2E(<@Y~^CVoni!UYK9;-uaI3#DUkR%^8f8L!K$TmJ743 zC?~_*+TnZs=-$q`KFr(G4SBJ)QN8AEcP}NsGw%nvM{jQQP4G#bl~-K#=W&~QZ;?YD zIL{)gFgD$=k9zc+lfm9u?r}7~bLElei7%@EUa@atyi@i_&t7+%+7$;9ew+boZe)d* z?l`dQ?3<0B^cW!a&ZhG#_FCdS*p1$U@bIEP2!6ZE^=i9}A=AY7YP#s-;C^LB_jc}0 zz?a&^_-*`)Dcf`7Dvihd>vmb35_=r>O~_s<_i?TYz9{$%xVOXS<=*LB!DE74ALrYV z>)UjGCjAH7ll+PM!M)V_(}fMT4#mTMm^{P#zoDz>J;z1US?0Xj^F07d43Cd!;u$F^PDj;dB|S7Y08(nNAw46bvb148Sozjw^r_*W&Ub6 z@%jeb=cAWAWbm)hOJyFi=69aGJ%aYm?BUh;4B*LNKlmB(knz3RM!gB}8JO4i9?co> zA4Jbf`wwd0E8MU6-VRO??(LNi;K?w*UFNT_cMhe!GdPgViia%u4A*sYQAb@)M$3!7 zLLOf9ak}R8ByTw0gBO*DmwWU*ME;6ByvQ?14{!g1EZXCI6aQjL1##8bKZyR|`j9r_ zAv5`t`_j*4%%FnqZU~700oh$HU{GA^i)hzPuUgfnL&DVNS z9|v<$>~V0e(DRzulReV z&D-t8d$2=zEgMxYb(rowh+N-d>JRP~`F82G)b=5k^_{tw%3c8EuZF0; z^M*-FybKHX>E@!$twqmE>(K`%c+k9^_s;JR{z&9x@?z5kr-*y>O*ec`J-@et_*eK| znXg-%?MwG|XS%l|&j3G8ldkW~oT3i8Uty1f_uwtcGlUBtJ-kcshHD;^`*Sjqvkgws zG|G$4oS5wSnBa?|m&)HOTiW9sQT!`-Eq9AP&Q7s+Mh+Q!oFwYeGf#%^?eLgj?+i~I z_*XbrrSu1kbPF$FZV(dtfKd zw)BR>kAptW7Vr_$hU7CIh%T^9n>G> z?-ln4wf7*;ue2UL@}jSrq5Y|@G-vpx`muryX%@n} zv_s5Ak-uUNB+ixW(KAm*dU(Namz-^na*x9s&KEa#i2dM4)OXHZSx$Rrd-6NOYYFcX z{(~~l08box6P#zj_X<22zry#5^H=P5_Emfa_Ia6$JcHyE<>+!U@R(?O9OSRG9{tLNkzSdT z9EbYQxf-^&l6W$cj7Q_F)-BHVCLZ!TZ8`lCMjI&K{&GyCp(FbJuzJ6-PB|mJ)ck7B z28*OFVlIllGdM+ZkAprAb3g6|XSx10X0)TV*%!5akA~2_9p|b#@n_M;(fjCYDTj>j z70*SjZFgMrr``lSyqI61KNww^NL)3|yM&xf>WZI2UB!QJ3-LvfXLy6YSJ)5o{c5Y= zY_or`ojBV?0YT0Wjq2zPPLVtD8CsKk<05I^o>UULzwUGq-P_>}*LwkEe~>+KRk_0y zp8;L~ZGMICm3}VD+z-z6Vb1VL;sE2DlxM)b9bSM}O#063G2#6ndS2kF{YgH0^t_N~ zK#v~hO5-8lHM}Ie0CA&pI?dZ(Ge)m^B4bbSGsJg9emVBu{51OKUsmL?H3xIq(I7QmtnfdLPOy&&Wi*5@qTG?tSSy>of8sO{v=%|+7 zTbnnYcO#w*?{V(uK;m2_uQ)(F8Cq1neLc;u z_SiX#Ji`y^`=~!C=b~Q|uTSeu)DgFKfUm<8N9uX8hqocC@0w4^k0UwTJQqc-FO+)p zBV&GC;zGTNT*Yt4eh~btDB{*Wx7Uw+^!+H;7qjs5o7h70c3+Xd8Z^%=s+fHAaxN-)GT>}4T{X%04f&nH*?#`Q^tQr&u{0MwDZJs_ zOJ%N_i?)4^MIQ9lrQ2nloVUoUQuKa_o&efc{k1fs)nl^T;+xn1J>N}^b_?dEj=;LrsCYioh z%qgnd=%wZi;MQW!AagSEJ-AhQ0l@3i_Jdc5s|LQPelE&hOTM?`eg(fX^RM7t;(L2H z+g9qO;(NupzDE~is6I~Z$(uEGg(uTLOYCcWOZf-oxq{yrJQ?hr!EZND>n z(Ra3~zi~XbDoC8GU^-XQYq^m4SMYfisXPO6GB{V@s)2u{&x<05jQ60%Z*MW&6C6nP zO_-fS&rAAo1`?-;|AWZ&At!?zGQ0pcXfBF-J9vFE*EdOUwvp?Df3TYP44h|}VQ3cb zLG)7Xh={G_rzaNYo+&~ z^gDwuD*J<`d@>PxWq3N1HynOv$1@#siL-59|F`glBY&lN;^5)s z95VRr$o1{-0nWxxbsddu`Hrc+pD* z|0>R8j|0v&djT+S4?5LwBiOzddBZVhfY(xc528m8uccp}SE?`dQklJZn7sh*E6s9ok{=!m_i^QD5<_jPq2;%s9-h#WHR?S6HY z^d8iF6ZjAEe$dvTsG$F(M1-Z;B_=HZ<4`ORj19v&5-_Ba{`5_@Oq zOKqWC-|NKLUbw$b{Rfd}K;K#Bkl9C%`4#76;F~~y@PB)FF>jwaF?q;7ySJ}jJ$b$6 zO5yi(zk+YVp1xOK2|tc$PG%6@+u_HF5PN6v+vWF)dtUGM${YEr(^ZjY;9hDx~SKLj~-k#=C?- z5P4DL`u?u@?c|K>)ih^dp3GE(nV~7FaDJ|b74-*8uZ5iWf;@5T^LinrGIwQV;xXU4 zT^6SYIgS0$ZEc8EVUPbG(a@`0di~ z%w7O+ws+}r$mkDp-$z4I0mo(#TMnUuc*_oJHTqVVvdkHb9Vf5v`cSiR!cP$%!R zTiTpU2i^3Z|Ilf1-EpZ@^s_zy0I0wXk5IJN=;fd4oqTn+GZU|63 zWS%pC`_X84Ip#asJ0pL^-f-rN9w%Ux{L6{ zd8Ya$SP7mCxF5`|{fOpQmV$>YJ#oSI4LVMdUqV9xhV1s*yG6F#DjgQ znr~vD@AHe_jJ|5vD|+;Qjd_{w?VWpGdOpJp<>A$OUf?0)d&T#5y^lUtM_CI89jP!F3NdPnd{U3 zgWU6ikDl}G;1uz_ow*-o#8nH*+f;Suc>7g<>v@AerhGg4gOV@0N%SW09>kmh`73aW zz}enS9uwJjmc9v`E98*Dt+f!o32-2*#e496Zms0W$T`DTbgnqh@VK#B`MfX}m7ML% z%cdGSbleZj+p%|sZ^9bf_ zic{2MLAvNoOcR`A zN8c+C!ISaR-P_T}VO}5S`glM1tKp#d4}#YRJ_GmYIp2;v!v)HVYJME>uUu%(z&-j0 zaUj`iiM})E8Me8t3$eP-YsvX`%&)+Kgm0qrdk|iLqLpoiqLtfpeH_j+T;AA{6ciU~ ze0y~qc>(afdWgK?&cyuyU(`Z*m!v0dXYTXD6PKRWk9-p!U9+=od&+`(^o_J1yt3?J znlq%6cd1s*83q{NBA?g1ik?T0iF`Y9$k!td#{5Pelfl0BQwEIqq{o|$%g*jNu!MZ{ zdlat^J}>b4HtYI0@LIkT6zh^8{)5Pi@?2DU!@()Kt@C+lc?Qjw%KWQYss4(8RX|)d z_FCe8WlsJ+;E|*Y~QP7w>W4wFJMN{W$0kYIBAWDLd(V z6-<2`y(cbt#ZRH{cz;ja4|q(_A6%6=Vf8ZOtT>Cc7ir#}McyUm)^ZLRIhjHe&bG`m zL>k}Fc>%!dJ45-aHG%_a>hm&G?#vyb_JjW)R}CJM^+R^k-uX818DuXN9LUGSd+^UO z8(bTMZ$|8ksWZGyobAqiXYSE+{)&64$jR`$U2=-_ob5fzKM0=}=NZ5k<+-TzE^Vd0 zbBXGuf&&>qIT>)(WRKoj_`JZ`W^XvWOA&&9^|#=Q!n^dk$^44BYTDkpmChA<6THVk zo&o%;4&x`3zgjFf+j5VCzH=?jui*1yPuwxxoS~m_f8r0+^ZM=Nbiu!3AHC%Dp_gj$ zv<>C@;G;)gvF=%m&vx!8aoI#auB=rnxA56OseDSov|ZT%Y7$)lr@SJQ>sZ zRh5Y!=jn`Ho}0;O|ZvAKNsyi z)c^n456%?aTIMrw{tA7ZoCn;mcrFTG>U14nlsS-^AIIG6E1EONo)`XuxVKCG6?4_l zn~?l=3!TTLZ$Va?C3)h86JHebt6hrM$GJZCTFQAlINR%pQ-pat_i@;l3hqac+T&zA%^BxC4||kf-Pn@kFTCOK0@R%@Zm?B7`gJP~8b-xh5T^+5LG};gd-X!fvH0#r zvqZcHzp4&7vF%!S+cw47HtpklYB%Njm6JC`o`Lty-1CBW=~en(@tgsCQSQ;pTpx35 z9~b;9&WmF2%zo#~e&JzF(XECTsgJ{R2G_RdE>!D!UMs}j8Q&{8ztVbM{e*we)4EIj z_2UJ~?+hQke_n3YE%Hq)BF=UnohL5RD@*hTd5?pf%(zbctNea(oxDrz^FlAxN^sR| zl5FBCj8Ctr&x#QIcI*d{lfizF{m%R!ynm0wxjyMjO&{VsF~iegLEW;qiO*0tKVR)} zf_3-yf5*Q-xxP5!eqer8>XsVPuIBCf{-D-NWv`{T;xjN0xs3LMMJtQLw*~k*_ZoGV z_*d^zP6m8Y_N8*q3;ZkeCiJ{M^t`mWXsmhr%0QN z!bd+=%-e58>?i)!(8Ys&?WPPKQK@psH-y)cd40?m_3V{Ba)Q%Eza@08M#h|8;uN%6 z_)_s6WG?{xIPCM197xG;haU%gQTB$14Sv_!uKv&C`Bi?RKghn+Y~o*WAE%V|gT=Ay z42PC4Ul`@}0rkAJK91x-Mr3<#|K!L&E_k$M_ggi3rHgyei6x_B?iubzhf^;#hr9rm zl)u8f9l1V7u^&X9p_+WDymy8#m3tH5K*EnB=M2~n%KH`egPgwt4_Wq7^XC_Nv{7%O z?Alz>ceYk}2D}G@>$WSeCH{lb=fypGcmX8$gZZM&XJG%J!>AzV{D6x9i!%4adF+paD#d@WO6;AxsJ=7i4DC-L z-(E1kN#`HLy!}(*U4kcWAI+~G(S5JRQcmW!@H?A&;=ug~Fa48xsgj4xIT_5`$ErEQ zKTUk}!-=Z~Z#X#H5v!ifs3;yzy;S+V;=MEPakjcwjGH&u$J&zk?chNAC798<3e4MF z_4jd$|FIvGa|ZCQ*t>)|1Lp0rkF%wpadd*q^q?P?jErff`PEgwSDo@kzAJq6ruX(R z+B<`PHE=|C>JK^zPSG9ed5zKSogbw<1N*%6zEnMDTjL?iIRo-n;9qHd9C$6Kj{T7C zSKJ?jcj<@8#pGSOD*QNlZY}1b;B13a#NH*&U%@w_`3K)CuNOXgK3DU|W5V28o?jg> zjEQYsnh{du=0aWo_`EQ`8kTZ0-qL8BxIV3)$ct+8tET(7AK)|GuKBg_XW9>Pe-L@m zuWQZB#!^lO|3P?{-j05rc*y-meA45s#-(R>QvT|3!BxY5knipM9@Kcq{}EoxEwsnc z`p)oq$vgx2?S0Ab%zK>9UVsMOz1?*0j6Du~sW#Lf+`HVU`0WkEL*8BdG0Od@hRN}Ljh`i+Outfi zqo($x%k?U|tRYh;W_ms%yq4JGZ1J#|&@Mc2oI|!0{Hsdgn?N6DD*XpLmh$(Cb253u zx;x}I{4Vx`n2YkgojoSp9}IFX4%kZm!9u#X2dVxba(#8;KggV-xx_;TPe$hY4jD$1 z*RsfMy0~9q-d>UWV#=}juBz`mM{sL7e}z5HG}W7sdN}&4!#*#`zq(+U z7Cm&aqi>HXeMao-F{v@??2ZFVORrIX(2DpBK4q4N+J9+QJ+H-r-;R4b&qduuPKM_U z{Jmm61Gu&NK2E;k6v27@HBF`YbOH&%Fi6`@f@3Vrl4PM`7mA{hSaQp}Hy}DR< zOZ4bdDbLWG@(lN4+oCqlFY#y@Z}qgbZ8`b891Bv@>=G@EKg18Ed^`A86&61U&bB;P zXG2}pe();Y+c_^9AbP2@4rl1j70<8uT+LDQcIl(%e7od+=)K|h@0}&5Xk^N_@m-CU ziCMb+Aam97>3anqJ?D_YlQF$tO^WZK`p(R)McOXT{HZ)bix z`Z&IMTgYSLNBu$M8LX7wS-ZCfcJfVVo;dgiwcZ5J8Q}BcxhQiWxi{fR^DFqgrZw*D z@#F~GDNpM)ne~#otUM|w>Lj!Jt5blXnv#c(PQ3@`IYQV zT%`LIyp|6qch8w$@hHtjUvs=NCP($1r$#@k?(NQvi_TV4A7|j2>Q477_g?uT&j7C_ z`{=npDEEU)vfJ05hy_M){q=>+4uLD)z8p^@{%rZ+JI{eBoU}A18v|gO?O%o9|c1xAT4wyuJ|U?xW25 z6p7vh{)6C)zP>ssGpS?&acjYWlszwbzbbT_VZxJvf6&rsM_x;CYs-lP32rUF2id!n z;yKgA8*Z9I<{tgaj(?5Ga=jP)N5tpC=LP>D_BiO#YjXzlaUPU!$6WL!(VH;P{R(+e z&F>sV9^PkZ-tK4LPk8}k{;Gt06Z{{{rTri{+jtKKivFO#b;#ff;y|L8s^$863tk`Y z?Z_d+M{hy7KF%|2=@(DAzVC@sbT4{w*k!-h9=IRG{0ei?C3J5eFv4NVK;d2bj6A$q zt~bVvcC<7*Qu~<5$spH<-o$z0Y`a{qq5M^{XRigd%cdI44R?rBWImyda>#Q}9Ht&U zyy4(q!S9TGyZl}yhge0v5PO8qm9zI*!L5aNDP89c$KDyfRO|;Ird;2L6yW_k+mE$etJWIQgBpYI5Fw z{^T9CcLrC@PWV#c^ZG;l2lr84RL?_Z4&-0rdxdiaKMwe!vwCeBIl<|Y-=eU#=tjdB z!M_5(-Ay-VfENILXZ#1b=Y<}9l-Kfw2bZTB@~Q9qv3nWqo%ucJlQ)ab)trQPQv>pH zi315v5x8pj4;IFz7(N$%9Ohs7Qy)iqOxTYDz9`Q{kr!nSr1V-M*C%@u%onW|KKh*p zmg+ol!xq0S?(LkD(ek49qDTK};t%O{!soTmZrzZX6H`5VEVxX3(c4kQ^K(3`C$v1( zo&3&6?(+}E(cT$-9Q0DzyTo%*=?%vo2mE&Ial90tVZ-^Y&8?I}E+)?QgWiLDZ}*K` zO1%m6=)u`$ZtV>3Uj)D1JTg6`i2BZz7D08f$Kt4$iu)CN;=WYe59T4GkK-)(qVSmP zAzv!;45uG3zj{^lakP0mdJ_YL*OL1<{2l~X&6oVn%vGDFU``RdmID=M8~1kpULk)ay#U}qdRi~0xoF>c7GggbyKph} z2f6Red4{E{-Y|Zn>vVU+SL`P4{s> zkiWvc{fy!iv3Ch`QREqr>(hHoWKIU(E98*D7d0LWB(B;F!L5}%WXu`14zqID)^Pds z0=i#KTpdZ=5Bvw;J^TT2Ytfr161|B+v3JItL2_%gy|XbhUGxW$>-$0Uow<*bP977^ z^=W$?_;Ef+>r3YfT(yp;I>r}zJFX{Ww!Zo&j7n@Y}lx4kUAmq&K`p>~UtQdprLJ7ZINUeH`Y=_?>DZ4=;N3=;N$2 zl*D#K4pRF;%lf~MZ>|c=^C0gMpDXE!1E&ZaNSmm<*bKwo<*~#m>NnJv`h&yw?kpZc zIT>&uv3KTPYA*G>Tvc8aoNe$K;I%Xt-f-mlm?slaTB&+o%ol|x?m*0+5qEVy`kqG@ zR&>efnf!*}Kwd7qQBy}=fE3Yp{%x7Fp(D1*N^kgkIoqDMS;#}!@eUhstIgtO`kApo<3V8v*zw)a~ zJhq~;I`_Gh@8YeEU5Kmph47_fkCX3_KfgJuId-Puzsq_rND)39{vdmoW{Ww4U!EuZ2f>r^6+SQYyqHrYdlTR@phvIIi!LKCK-=1- z;@*zEa~%0nwYjLwU)`ZTj-U8mRjE7!JSLLg4*u0e!{^HH{Hl(Bg`5n3uYMIClWM!P zA+8gXJ$oqpwbU+&Gcu40CmT9MgRJeh{5mA?Y754k=^ac`e3 zJSOm^`l%j0c*w|$!be}G^LfF;o2~BcL+L#TUf+M{TyehLy=`;91a-fA^7b$SD(e)intZL#kFqCc*mBGZ)+btI={kR_?`C=5BWyT<&)0J zKL{SO_8-K(y*v4Fz7o7X?oA*kW27ANDf$nlg;+$s5c{nD@Og26aJ}#@eMC9r_VL)` zAb&N*P`AuM_)?iK3ZED6anK(`k6!u*(Z^{B{yXB6n0mvT;@&Rv43#v$day5*eG~Vh zTMaM7oL(|r+^<~QhF{pFdk^aUIQqVGnjt^7OVnfYJ{G+R%lZbUvre=& zo{S$V^6ku%0l%I3SJ)5AJ`Q`Acz*RpCr%OW?br`;k6z~bFmG=YoFZTHE-_CAKCk!H ze(=m>7w@y7sVnvyUb%0NgLCyl%D3@$M$5#^wEhK$tLL65z1Efb&IKNYy7|?5&uN{w zwcr#nXL~_KPs+)#&+A_BwTMG87pX@-$hX^+!6SC|aBd7gTX7(&^lH|VYj&wQ1NJy~ zL~lah$8iz-cFr@%x#$Gdn~-zSY~clva|Y%VY4djUysV5q3A0mu^72gj&g`2=F&q#) zWO$dP-O$;h~5PJ&e9VH&bA-TukaqcPTnQ(MR!x*d4TT=izh~3 zGkg-$5OG`N`pn5=GFSB`ct6-6JiN#==zSA~^9zKJ{$uhxYdPfMDZ7ZXJ*TeR;+H{F z#(wCQ9Ad8agWU6yJ}>0@;PV2n4|!3y!yDCm5chWY=(nsa3ojIVoTBDU%433@%tSF4 zy>@zw@>>2(IT`R|m;(v^l|Okck&`)2+z*_qFDZY8a|N$u?)C+dJdl&hJ4B9rxoZ@vrU}UXD4T zcrvn=iu)D&2me973HDm{9=dT-B+VJXtu^gCcg~CYDV_{-Yr$13h+VIEecJpAJY@8p z`CQGWxu|PfLBExw*SJgzIwd%e+{Xc56nRlI<#z_3;m@ov>ZS6zdWgILoWJ58J^VOk zW}HKoeH`}0F<+GDqB7UVeViiUo8Y}O{DWN*t&At*pA&l=c$b)~hQ9MZ6|e8-$yPKJH- zxL?Wdl||wQX?+Tgs=25c-P`X`kN!8EFBM!h?SA!oe6M6%;)`-l27CtD$C*O;tM01r zyo>nldcG*;SCX@R$uB(YZgeyCyj;l(0M0hQ2jQE*{R;m<@I}FA@Fnk(^q3U4 zgjvLa{Oh=N{cP*^2X7ZSWbj2Xzrr48z)&9@Uv$~3*N9sinLTU!ZsLCA)AtJd!I9LX zUmSMD?^U{A-6PI6yy4d5qaU>9M%Kd8PY#5V$HbBTgP1eO|De7%fxh#@bZ_T;JG@IW z-_AT_&Wq;GFBa#jRJ;c>i2Lzx;faes7Ff4K*PD1T_J|>I#c!cb-e)Ek46}9EN!;o*fB zfbUoEn4mYoK6?043#sRYy)$!)*gwd99Ohqf{^}u`Gki&N(Q9;XzZtPt%tZ%@b2Y6o zT+FWquBkD07`r9;5nr_9m*ysSm6O4H5I%b8cjoVvFU=W_#!aI6RVC#`(W8fNVkzbN zkQc>!Q2GaPzk-jRy_NwwP7&s!>t1{P89|s;@_QahIO{4D>-h=F;uM~b~_y>=x9P$z24aeR&pYB)8Rl|SK zWnvojyna2|RCB)YZ2F!=$4^eADj`C z;1WB!px+GPo6xu)|H#-$`K#NMZ{I-kcK9YZ&+uWV95T<_kr&0B;jiOe>Sxh=kn`=_ zOI@k+m`DyJdS2`?LC@Nv+X%D2B?(T#Y>*bk!bj6B0M(eq08e00G@^}Q-G$&2DYh@Kbx&YW)#P~WSR5c9~F zVh$(PL;k8d_t}&m;w_9_6$cV|(RP|Mu-9^mF3+Ix+qpl8dpqV= z$X|5`uVo{7mynYI5BVXQw#%0y$&HdgH zJ+CF%9fAY-RL1V&XZHr0^l{+FDWZA1)|&ta(vSK$X6iq9oz7J#`Mls=0-r(a<1nY_ z8ReV6_i8b{2hmIA{M9t-4Hh&NlkaZ?86Hrj^Vm zUSC&-LgDjTyW$JOOM6m7H5~_Py0bWSGhXBGxsKtZwF6i zsEK!J8TBUM4TsN*`3zdVoqZF?Us;jgS<4}NP`0 zy*8<7)xSx{coBFRS0MX`5A{tEXi{r3v__TTAy z^-XQBqjM`BB46qN(VM{c3Oz6I+wr}+uxy6mUTkaBmihS}Ruh_^ddRkf_Rg5MGpFbU z(eq-T7xp;pn>a)~WcFGj*9U$(^ZLL;Moz|)=Iz`|W$#j<+7H6#C3_R^mDiCsJVLw& zx$pdtL(Z_DsXw^Z#5ckBcEho_%I&!?>bPoO(467^d^>Zt*A4m9&Y8{??+5#f`IYpB zzv}qAdJn?yEd7If$TxvJ!=G7Ch&cneYTCX1Uz9`Uc{}q(6V%?BeW}PHV=l_QRP+aX z3m!7^?fJ1bQN8DFcP|+irt8srkQYGqCYUdZoXj@&0mNrmZ}@b1+`=&8Y-5kZob9JF zmaiITJQ>%rHcEJx@E*jR!G(PE;K|@WIF9zt_h^2FUMl+s2d?>3@Y|;}R`qydMAs=# z`#!zcCHlPKid}K+t9dQi@9aX{TKosEoSwfwmF`#U$3gze^YHrfA2v4&F97F7 z+2=Lax!Wl7J_R%vovl2)JZF&SDtFjfI#Z{$tu-?^ z>GGod9|Wi9G;tu&ONAc?-zzJ#(T;ynfAB`cXE7HI&e21Z$Aojp@Oklj5L`9MXYiz6 zDtLY1w~rP*FXQNZnqTcU@lEi5kmn4K4b7euA^Oh6;ynmYoSa`_k0brgxL@5kZcRRV zc*Faec$YRz>M!1d!}nHFj~+bajO3ZTpBK+X|1_Cj@ti?&YwrbT zxn3XhvdGD>C+_w5#|5wN@0v@(H^F=cPL;HshTT)eVX^qs*MZ6|N|JBPigH{mOI$mn@pA--rndE$Nz zUA5v%%Jr=a`Ot0Z*fW%qk>9K1@isP>uE9W1;6ym!W&L7!)+5nQ#4g{RYN$mgZy zWROGF{)6yZf?LbJ3HGHTFFHup&T;i4B(4mekF4oI@^d-f-UIu&3e_y}3G0+^_H+MBjP3;>olv z%}{=vfI1^_w!!POCjQm^{PxzUmRJ|v{0iqv`kle8MbC?UUhGRn{z~f)!fVO*EBI33 zo6z5{@V&zQ3g4^oP8a0B~z@zv3LS##NJZQSkc8$8Dn?J@}%`>(lpf!2O6^HIee7-1E}- z4Dx;@ee_$XkHd3@AmU%`p}sTwon_Ap9+S7I=fz&jtF*_FT(w7uTg&q+(>@NkYR-mM z!L0=c65p#}-MNxE8Th=w>zh8d#4RPnGIC_>VMCJeJ97@1^9;ye!H?4+zE|jZ!DFKD zJHw9?D00Z~FT9rA^SYijaLsO`b(|IX=q(O``@y+Bd)i0^`6#? z2A7We*!{72wo!RBZ#OPn?6p~UZ-*~6a@8B;$61u^Lvsf3MUg}1`xScuuy@`v(o=YN zeahx+uu8I4Jmd#`ue6-ZSb7g~Zvs9qN1C_)MqU6Xy0;_W{wVF8EewB06;mIF^9<}U z;W-1JD{b$r_wX{O=#=o7TrtVFwUC74pPM{uSn;2IZso zq50Ku$}=D*qn#`EJ0Dn{D)J0lg^%7^?Qw9w(tCI{o{UXYL2QcQ;PT}Qmw9DP>MMHm z!}eAb56xJz>P_QUan{PaG^4GkU-alTE>2?3&@8wg=%sT03iB)Or4Cs0SJsl!iUW~n z-=aAK^N^XVhFo7ZeXm+OzM;Ldo#+pOhkQYuE6wlBoNb=B!(*~dczDy&Y!j`GC&aw{ z|I3Twd&RxfwI!>BZ=yxK2l+qfFe=zNKcFnUU}c+_x8q#3iTf3D$Q?;O!atZ+60-k~ z)42@~JLHQ0U^4Nqm|N>I_6PD0CLCL-;}kKk@BaIh#uv>TvdeDf_48sb$~hUHU-dlt z4e>=KXIt_`;SJ|J12{$Cx5JkT4kUZS&6J0?R`k4>tETr_nsOky=hZ}eoQ>p5MXqma ztcBpJEutR%U&r&*ylCLR-fuk_rH+16IVyTp4O_75_@-A8ag zkdpyd4LlimmwFEkBA(21dn<(B`QIYf$NNF}2Qgo%t{U&1CHDip3G4^Y(S8sf6XcNL$Jt>KSQme6 z1@T3{i|;~n1}!JUdmQGs!|&{{-~-RMCuULp3Y_g<3(pG9wnK8aocF2c)lS@x4_xmC zUnf7#M$zMxiFxNax%#E!3*%P@bJq1 zAo%T)1IcrS0_EZ5T%SBw%vEEa%yH36?OX7b*bl-tfqeVjsE$}C!Ec{7as7~LI~O`v zm|ubWfnKWQ$!Iwl%&#t!Z^BXJ+qL%~?(LSM=Vd{DXWlzwe#QJNc;e#eel??QIC+;A z(|Zto9B^yV$Kg2x=iA{8M{mNB@>k##!RM7*6`1Fq8bZDa?&IjaOYj0PXFFQ#2fn~r`pB~v+==@U>*7ZLL7rb>&X7HFcCUsen{%xP0|gHm@4=f{i-i|}`_8gQ zFFkSW1^BYI4{_C`FBLud>or$TPQ6|u@>h>8_)X^pFel%{wre5OqsO@dp8@msSmAd* zt-e<;$9_(pIOLGwo5&^p75luF(D&-XM!)hIhc}-uYHk%dXNU__Du|Eu|vhb}-I2Kd6Gxx=mqw(F17KvH(y+WQr z+YdGozx~%`y%(&bdprJv*gOA9erM$R(09hXo%e&tUr8VRTPA!4J!jjT_@X>#FwMzu zUeq}#(ZwKm$l&#HFLj)81?6PGfzwVr(RU)AMcZggcJiId32QPrt zgglSD`7Kc`vD1ZrkaIHAiL-r4@fl>UPjWx9UH=ZgMc#0D0hote-D7HF*xB8p@4Sch zgMNZ93eGm~2j34fTlu%)74i?pDb6;|73UdVGp<82RXzkk89{U1+xd_6p$*UpacU z%QWIMv=FC=@9iGF?$UqIzbuI6qE#X%Q%yM;xirB*o zPaN~x?|%=X@BDZ{N?KQX4-QMI%8eyI&UTBRbewH?;?9Xaj^uved&N0q><8t!YNP+4 zmKTL5j{DB+^MZd6ew<~-@#Ffc4~``6hjy;ODdJu# zbBg#sxZcx6^}HHu&KLfizK8gt_Q?)8b1ELAK8`*wddV;~dMJ4T`V&tkLgm}_UQ3H} zO~Mm*#cz?AU+MkMoNw386>@z;_XgW;@R;~f z&&!(ngE&|H#eWcc=SUN;<&NT^8B5h3N8@aBk6z35)hQk_dh~&1z8ktHwG#&t->Yik zY$GSro&JO9d6~}Jzp3p>eP`USn1_6Ua>($8rwacd{LbtR=N`SC*yAu?l=D}+a$_pv zi35pwyX3c<`UkOh#(oevnZ?Ec;hW(1p!5%7&LDfKfr2NaZReHnUQ@&L0J4^o{ya4R;>Z5qb%qij?J$$LAo;c*&*&BYT zlP4~4gAIA&u*Wg&;|wz4_3@mcqeJ7Wp+{encv|oo_*@~+@QBVEUf|I&-r{L%+wIo^ zP8=mK0Pa`F_3?f%NaU}!(H;k!?X|jld!g_@@}(jt0}dqogZv&$Et$Lj_tOOpT^)*teLvaF`zPVGls)=0gSJ})3lHy$Dc{6* zGg^_4p8JE~Kx#dDZ7#Y_=LL}dLF8oSiarjwYWyGE8&jt{SIigH`p&qw>p76{m}vXK zAC^oPy$R-Q%m1L<4>D&P+*-c3e@GerNVgfX{%uDE5Q6x3hPN&s8CL z;$j8&BiR0L%Js2-Q2KGOAAFAT43d8}io5`N9x{Kg^zXq%rPbm+$UZN4Ey3&KJr43$ z=;PpiC3!NR(){YC>ZQ8q{Db$zdyqNX*yCt<26#;1U6Ow1BDai?HZfX5x32^LE_Z<$I9*&d6U$ z?^1Oq?g!7?{m3_g`xWQ1 zAC&Jw&hq4{rwpSh*e!-P`^l^&M zz`hB10pLruKG%9MsLX$ZEpgQ_7sb3iO8Mx~qv!u1I7KL6=n2_GpG5L zJI&i$g&*f5HD}N`MRGrgxhQ(6-1E9}dTWEV!?t1HO?LDCnfMII^?@(Sz6o$YxIZX! zeSHeP5`0ng=+Pg%Lp^%<=-CUvd4@YR|1JE5`p&OEu$PMcAo5rE4|czgCsSp#jI&J& zP`N(ronNEgL{oH|VWf^*JG1TC3pGbPw?}5Tt^JSkm<-?RbE@Trm;Jz@8IzV#A19ga zSB{F`Zml@m_+AAjcnIzX_zbwWBPW9%J$$L$qwhO3KzIRmtDcwHI_X{F_aL7ucmc4- z0k;UNXmIkN}k+mRQ=oI&<+F4LT$F?fT>w`=dgx8nP&{-E|AL>~uvQQWWC!>fI- z`WK|ryj{*k4b=1cdC)Xdk6v;=FlW$n)i^H-?g#Rsmb#pbH`77oWwTb^=D)pT)zhaNc z8On=d&d^=>yx2$2{Hwh&SCq#@-mfYTEH1rCerN1)0>~R~b?&zEm>_=z?g!7W;KzBV z?b!>}!bksa>JR=SW4E|pF$Xf-%dl|2*bhpNiI?CZ`-mPr_BePCUZ*||`p%MD%ls?N z=Y?FK1kaPEmhAe;h1mTZj0{+%@|wk zmL@pcaxPjx{441NK!4CXt|RHPI9K36>T}406ud&T&pX9w^PFc%A>vOl8X7$P$Iab$8J-Nh< zd=p#x&1idG%tg^3WS-0};$OiF(1-la(vKtg4DjQye-QWfAhCD$6TOM|tQQR~8~35| zr7~A-^Q6ay1`r2w*xp^mPpiJO9jMBRG)Ys(~-c zbB3ix@3{7)D;qt^XHj1C9&xs%hgZvscF^AWeh%dOir;>Q<_xP>99DBt=GMxd*YmU= zv=P0DVDfqK{R$jN@EPzQOe4-V@(l2pV2^`74!q$!Z-*BEd4^4{w~70KTp#8PGT*-B z?9KyAOMj>RprznIwjZ>hT;Gb zg^}NRm`M)#rI;4s4QEf>t)?#LS`G%41&H_Ho*4)&)*C;>bwJf0gRe*DkQTJ%?{2t}o<@YL)dZ`-!YNq#3)Jq*h`$6fW@62aN zSKN=TVn3KK@}d{%e&swdLvbKEhdj_YQRGGY9z9fRWA;y74!PDaP2`Z_^TJ%T`ar1o z4_Yd2E%t*R!oxd1%xq<&VRX#*%Dd#+Hcapt_BjiSnY%ze2v9y#UhZC3{}~5q=!>aX2rE zK2DqR0!VJHoHH;7l6w>H3BNP=qI$0-dh`L#kJ7oyIPY)%WOc(Y*cB#DUaz?n3^-Z))3! zQ^fPD&jbfj=8)eOd4}s*j%#X!Cysj)oELq~>9XG zoDBAZlCv%MIDZ{4teTzYo$8xlPICs|yn?Dfk9V#2uwFE{d|Zk9pm`mUTZzwrJj4Ib zDZ-oqTs4`K!F~|`!Os3c?oCJzB>SD87-2VMu*k`vKiEY4_Lm*4%)Y4Ye>AwlKBtG^ zx1UbGSa@6bQYE*R@9oV|Tjm$2`xSe5@gGEQg7<@%i^6M(JOiJr1BP{!7iEu0QNz{K z^Y*9dIFQ=CUGhcc{A!!vw>S6xuzCG?ufy);^}@po?-Kh`3mfbdXFFMO)i7_Dd4?F` zY=1-h!A!+Nh8KX()xD@z$|3h$kgod9(i_g)5A2=UyQJ}uZ>f9x6Xcr!p8*OyOwXYgdg|3`mt)1+|W$I<+Q3kT1#wyFQ?cma6< z*zb%zPJ!SQ$^D?2>Um+_jy%J?8{Vg0*t=cK+rcS9kDj@;(ntTpl2O9%d_{3TEQD{u zHpxu*CL&6Ah+H4`IG8hZ?xnWtd|vnuaxay0$a{p39`|-<;$PuE2tN+)?LlfT%Dlc^ zxhsjYjU2KI-P_Sim2=V4CjUWr!xtNa;_fD0+vr~Ic{uYv{*~-IgV)D?9IfYtdHemn ziPd6{0}iCzJHs1Zl{-SmllhYRI5=1E#N|_-0rxBBY$MOWKKhH~AM9tsDf&OY-p8-! zd;kCMCyC6lIV{p&8B-KeE~-bxaM-o+Vs0wI7@>dC}9-cP?`GZuwKqhsOeUMQyTM zds6ZY;HsfNXylMF7e)RG`@!wvO9l61R>4f0Jq~jq&F^;RGc57X8rw&BeS@fv!+eIe zHF#fPF3SId7N0jl{x4>DLZjtgd`#4jL8FP+2d^c3UbqKU-#J>mmV6IBM)`K-1yH?I z_QZA0eOUT9;I}iML3u5Cer3Gd;Y;04oT8qE?_|K|g&gvSc}c`WHovdHRa>|0+X%ma z)8n=d>e#JVdR~|_L|bQ54jH^Y&WnNrsd6%yx5Gyducgs_ZXajvqadymN=|KO$5H~0NWxjyvh;k9HAq=W9! z!#~J<=f^15hjWGfU`*+C;q^JDhGx&ExoCrpCk}juQ!!IRmYe#{#=L#0X^+DmlUV-* z@#8pBZ{q!t^C{QI?{?nffcwFmZKrGg&Jm`b7xR!~{B!8teuwz&zrV);_hY4xSID<9 zgA-aTcd0*!cl%88JKIT*-hnvV2B*mIDNpZHhmwKV7f$qUQu9-3yHGK5U zlj-QP^ZFe5UUi^cANI~X7yXcOGU!b>EPf&JbIa;w-xH^(a8MVSi)NN2k&ix5xN6Mn zYwG#%(An~CXFm?k74|q=o8CR|do*ZgO-@^CP*SAsJ2MZtnE0Y+iBp7mJM$T;B`?bU z!T(F{AiTaKRr5}5z3%4H{G`L^Lh(CuA17_P@0fI&i(-C-cRTx?k!P4!{s{TJMi5_A zc}&=s3hxqgig=I1`$6u}vma+#k6e0RMcQybZ1e4HYfec{W_R&(`vZk1gB&uSD>I(~ z9LRc`{UA7y;EQq&ndj~BJFl~R+Kzt(&NlZZzDROh8@VY!do7uNg}i99fvXc1iUsG>Fh-+7Ao zoq6xfb5Zzly6C>M@(=z>^DFdm`VtRW`3GZ4f7T;_~l0)iT;*0YCU>@-qu*YE@ zGCU?J!`^j&tL3Shk0jsD{Hv$3s*3yXpGzFb9^@ayTom^p=2xXt-=~}m{Lb%Hh7?RS zd3Xzm`!U@)f_h$k!`l{bOI%|)Oy2O^u@4UnbNlT^X!Sta4|1*#d8$+6Q?*uoEHWE3g@bf<_xW)w$Qn1 zON7Uyo9qX<=XFPTGMsPc@70Og=C+X@KMl|EZIpBM8gU@uqgUt3@R;Cz6;S4Mq?K~W z%tMY4e!Ia{Lk_uu-d7H5zepNObB6NbLHmP+Cu9Cz8NLbg_bOQUqWE5!-&dG3Y?huE zdi1@t7l6It*bh3cUAQS|*KYFTsCPT(`i%V`{DZv5nJXS%%o$>c137{6?a1{F(DQb~ zkApo9dwA8m{bJEg;$LwN8Q&{-E%Cil`F6aoFu(GkTwh1)zsY05+z-qd%=4?Y%f648 z6!6WsLgMvZBmW@pac~dTQocP~`h&akh_<*OL2#dDDvK-6gJC&60=bub=KUW@G;J&HLJqkoF;n}z z!0SUFN4?vbFB<(j{44Bnn&an3{UrUt_C9(K$zOqgh4)og**o)G^iJqT-`|E0_2^)C zq_+3*kn(QCt$oybFy+h4OLDG0a!u>+tNVlI@0BsXTA$G|#l`wX@?X++#?@0T>Lh?(mlHy;Xj{{%o zAe)|7Ir+TM$HCrtc6LZvv+Qv&XE5^_n15B|?j!uG82TUNa|KQj{s(ccxaY-v9DcW> z?>x)dLEcx)XE6FWj`VKFe$XS_j=TV$E?q9(aCmqzZ%0l>c`el*$2`CKOTs&$INN73FVa0Yv98K>L;ne5-kIKY z{^cd($zuXfoTF*Zfd4_Ec`;|3ee}lniv5Fa zmd(p|MCS*Gc|Sz?tBus7zak#qF{@T@NG*vtcv;Uyy#xM7c~S61F=y~0?uX%V=kay{FP*l`i;i|DW9P_I|+Lwww4m>8{s$q{4?ABvo-q?ly)&&Qs zN8d57XZX&k+ea=S&h`z;x2ydi@>jSA6$f(b)W>Kps`yu%Xg}B>`KzahC-dK=mNl`O zQ{hsZ}iHvT8eM+rA6&rmmfIGrnX4>F&DdC1^E z;vPhQP~C&@4>C^%J+FV9`n1ZkFoV8Vn2VMZPX@l!Z;98Z`Z%e}{zshcqCp+IZLPm_ zX2HRAnkNI!Hr`ho&X+bd>32Ig+nbkfkKRIioTi@J$d41SvnI!ZIFLLSZO^}g-}x^x zXW+iG@`gW`_MhZVR)>`AjGokY-g&(v^>OgNQvSj2lo!qDzt7dXu2yqv;de&Q3v*H1 z{MF%v3l{JAXCfa9>`Lz|=6+z#&~J4G<*(*u&nmM&;&ASE?Dm2JMA`3(2t^5$)s)+RX_p11S&is$X^-&Z^r1)sr3&qeXRLaq<@ zpxF~w+H}9?L&EFBx#ImG_*Xo?Vs7pI)L@xktt(k@@Y0!L?RUnz9lcb;!wcU8`p%V< zlTp1?yxWmyK+mg~xN3Y4f~)2`W}VK9VlK*lXPhhMx7!hCJKMK0^cR|6Veia2)37oBB9*YyX|}cWcbb0j9aAyW|-Tk;lX? zu5b9ZsXInS(7YYrt3cxQ*^7Trc}(W@dtZA@%=s(u`oMuS@}l@3RfRj2XoPd*72(>;>Q68l5@z+fi%8X-*|rD5OUda!a_ZI^qrmPe~|mm zMqZRTMVO0jnC>-ZJ>7%&UKzerH#{_u>@UNKPj{H^g;ST5O z8)x>|q~BL}X+MY@GCXm%9us)O*~5$PmEm``^;&{c#C!(DzXG2jxFA<^)jk+mJoT~g z*2UWr*V6n7eP{3)_*@}>W%k7FIW|DNODkyaY~-)hxdKlHT(yqk;Z^^GI9J?D<@wd2 zgu2+fp&NaFCH|FdfAB!c*TQF*Sog7On((i9k8>|BpL$+xPr6Fat2e#dna{9Wyi3ZL z3ce_EGRQMv@4R_X7x9>^Em=+ZcFtd!??L9tus57@eaydlhkEq2Id*FTl451f5Jnze zw{H1^z9p{OI?7))FIq4A!E&AJgWuV~>Y9?C(YNrZe6M~lhwMEj({!%*d-ZDaLo#pw zytd7>cQ)SbinGmL%esw^udc8<$-BLUyi5M%iDRD^^RLWa0OqRMQ~oM;bmtSK~ zuh8>qvb>bZKisW;(t?v~CoL<$d?y-S?y zdzSoz3pY8eJt_HigD+}3XK;#hf1u|D53hqYPOmirnnT==F2t?noQ#oYXx4er%Q;P{AxR6ZU)J8GJE2B6Tcn*gM1J2|DdD{yOh@60}W%&+jiQaqXGBRy!3W9*%| zKWLYvya4=e=lRuwsMh$~mcJ(aC+5wN1fLbd^5}cT`S#W||Iz=01GUE_n{s`MC(}O9 zV0gp1=XI0z&YY70w^sF?;iG?4_vo4Xp?G~WcI_pVL4W^&^@y+}d~H^pv^}U9d&oEp2Qj3=tM{gz0 zHvR`U3a6+!HJIM*i5s$r&wz84JLufF@c|boC-WEDI~V&+qWM*@_FA4I4rE4#2l*zz zt+mttgUs1R9|z~k%>DR?a>&ft?s@#5wRUzddt4o!=lfgeE$R=x-tOJbd*{B?qwky= zm>ojzE5p0AkbGW)6TXS@4q4%or1_#VwHLt1Gnn)32bX49@)A47xrc9?`oYNX2RM*H zE2gKN%QV!Xt zp}c!P$&0GJ^KJSc1ixMFox2jB;ggjAW?msr9Qaq5Uu97K3VhM~lIos%YaUW;#+44g%#DO$?^mf95xX!SFalTkcmcRO<+jrSG$gUpj*KTaF< zap0SPhqo`~kk8YcVM^U@*DUdQsh-zn>JOrigZ|)A;5Nf;j_! zuhg93!FfB+uM}4;i}<4KqeqWEMfg|P<7^gAk&zcQbJdKTjC0CH{l0Q`aeAtGR56_^ z!SE3@NS2H5c@$N%_(wP`-OOTZG98WzXG?`neM?dnP1hNEUFBUxhVMU=sVve z{uR98=sP1P15aFnK3BJIgphCIiL9mCW8yD48MSv-c~SVhMti+GWJZt1#x^=v%&o=u z3cb`1P4o7?4|tbemLC1w@`uE0xj*G=%JmtXZOq#*Xs#Oa?cAHdT(oClPKJGotMz2^ z^D<|^_v(z!$-uj0>^ar!g zPdz$)=e3->siE|~LVqy);I%Vb>N|8R8FYHw8v$n`(w7~w3`?|Mye_QNZzB1f!9!** zzzOmXzLU|j@LyGPPi>>Uv!mvV+VW)Zy}Cu5ZQO&*zry^gJx>PbYGCB!;ve*BSa5dl zp@pTtlHVCUdRs35drYv$d4cln?8ot;x#&#c^}RxSXLYXT5KktO_BiY@QN7gZ&Mwz} zK3QDpUoa~@K)jZ-G`}71D|HX1i_a@t@(jVmlfnE7Ts2#Md&003pUF0U9C!iV?9tTN z^1aiMS!HvwJEgW+KeoEBzPWKB`6ga(*w?-H;BGV*eJ%d7sgE(PP;bZCJ{78G8 z4DkY}zO$NNb&|fb`5uHX6+C2k0hou(duQe$^ISB){+BbM2eV2nCSKpHoqpt_Zxuen zw&*S7wdB00;avg`xh*vyY0(3FoHOHm`! zU#VQ5I#!P2a2Dltb>68k`*@K6-zei^4zHV$(}y4&?4* zvv(~JA3ggfz*SRwXMC?dvEiyIkBPCznX}@}^w5HLD{r54xHgmgIAv4&n0oY>i!%2E z-z(($IEUOy`$3*x!H<*TGl_CCs*m%E@Z0UrwNd^Gy@`FqRa;0tFL;;K|Dcg?&++|b z_;3$LyQ8(e=-tjfdd%A|6y4V6%7@-p$_r3PxxUsXJJY-!-zy`B48ADl?Z_cBPo{Oz zFzGu_3^*(A_RD7$99&znO1K})*`7-0ihC2-JA;Re{a_L8aawzBpjMvYcJ*!XhQmjXxu}`X08SC^LG*EEt#GmiteBNP)6|)W}72a3uAGFP1edFmhqLlsz*&B}hl`&^9 zJiMIi^MBx6@%##20DiY`)#nQR!D#Dit1KJVmxLYs=}eJ$mwbpX%HE}P;(nZ!d^>wB zl`qwu_JgIwld&ING%a^tOI%Z;kL5?&<4hpFsPY1Qo4Jp?0K9j`J!r3e6SjF#@UO<{ zJcGyTD#TxwRh>uMhu&?4#dv?75mrEnD1| z|L#3Fjr=%vM}o_OiThD$?YH`;jf-_32l*?{z(JAY;;&iuCESj^8Ty{&kiV$yemtVQ zL+&5fjV6x?dK2(2fl~zUlFEy=O3#bmSID>jtEw06ojY4k*yQ@Y(Rl{N>+=fuZd@LD zOny2uPkT&MZ{kh)UNH~Z;6UPiWy_N>c*yWOGhY;Z2A_ccMXXwO%rZE!!{W7JCBoUB zL;FG8gUmzD(!4%!AnhoJ%y|ZUud-y`&b&VG+xMk->RxIudADC5{+{ox(BGs#h#WF_ z$RBlowIS|-|3T%On5Oydiq8O#NfPY`2h{8pkBORJ8U8`#$N7|cshEr6T+Qnj>eB9^tNWxrrNyTN@=ldfpFySUiCE?Z;)_UZFj4%tPk=;DV@Y zK`(g~kZ+>(dxs;l>0H?-?WJ7bg3^75B7|EDemm!o4<+1)y+z*eQN(YD$Assih9~aA z>B)7Kt{aGltok^ZGw}DyMa~uc&d$`wd5-*poWJ_cbHa#i)OQ}5c+|3L*$)w30cXb* zP~VyJqUd==S%Z`ArT&yNO?s*94M(m|jI1yJ3_n_)KE6z6d&iEh1 ze(>q6MdX`6kDliYZ?xpvm} zGH2-1?td`7zc<~37ngX;eh@hs-aCh%I#Sh_@>k&X4Hi#aD7~+64{~0Vd3}=u&Jd?4 zufFcgToVTp{C3=f@0{P(bg$ZH8JJV_k*inTrPK8^Z{J6A(eCS9 zawE#S9Y0yy-1dscRr+3G&T!T8D$U#Z-9E7)l00#B^gjrGdq{S0S<7MPbM=h@J>DJi zir1B(`B820w=FMHk3PjGfqbdpK(Zg_-$|pa%U1Tw+FLw8{LbzJbEuc9JaN5=;J)8{e$pYf`2uc_Bh}aou=LdzpvDOkiARr zm^dX>QQsNwcJ=~z4DL3*PvG;BubJ{!iq8NZGWhLd>3j9H%-~jKQL+|w5(Uf_9hUWDxvW{MrYT6H~`4w_~_#edk3jB8P`VvoA_5Yw_%0}8d zb8mt@am;7n9(|VPY+pEiM|jA6Wj|>62jO?VEgVR5z8&WZ+*+Pr89Zc%a}B~N;vRjY ziL1tYoJ#SSDF5Id>f_XFo=k%9kR9Xth8IsQ9~s_nma`-A`T}*X&&Z3SKM4Mnd$<$r zoewOvEQqGL=p46OH$tih(mfcv^6%tbVo#jyyPb2$QBjwIUiQi-uO<5?z*STC;KI_~ zG`}+D3~Juq5SrurGx_L`*FJJQyu7Q-uP|q@eYdZl{>c0bOMEQNiFax5>_C1T^ZN=p zWW&RYxu}=;CjOjw*pjyFn+U&v>T#Qit9D)YCa`zLevtixWli_UyHrGThE~lfGWb_- z?Yx$AFLe&h+u@sle-M3~BJuF9UG|yfg+!;tYr@L>#*g^U^L>ZV%U12jLH=qZ@!La* zTl*mY3jM(}nzu9miu;4FC%dhCC^t+vkRv^=4$t+y8Cp*~WN_6mzf$*L#i3~NnDG1x zUI6&$-y{y?)u4q@cjH_19tXZuzw`nfMIq zd*v+mAbTz0$Js-C2F}SCKKko)uKq>b59Ipzdu2Ok!1s#J6>>7U^NOdnjIw{q)n(iD zw@-au^=M&shFwY*dAF;a40tl|O@RCHsb$Tw^AQsQz8{xIJufwHH~3e^-r0@vqP%zB zEIu!_#{pjyy$SF|;RQ(3+*-wN2UiU_nY6GHzeyvi$&Zsn@2eriXHb0{<;SrbT`+CS zyawU*DGzTI`MhrHyr{t``l7bEZM4Uw;RU`oLT|+$B(LSukx%G74)>k0cLoo+6LD)> zh^sb(czu_smx?|P^RH5f1NjWS+ha`mE8G7;^iny0Ot?R|MEh}g-VXlN@?k4|e281izEp5O;9X)5FZ|Bz;f0Sr%H;F1 z^)B5FeaH6~I#;Ua#q%rlQq}j0d-TlNo?x-JG{kKopI7UXom_Sbx0bmdCzAi1wkt2G z!lM1o%x_2jigSJNd12mu{Y)73opBFx&kOfp_W7;!ZUx8HEc~o0>{Qux7Z>}dT4ax7 z^as7jyX04Q?)1HV=Ze0QIRo+xbIIq0y)*X*e~$eu;Ro`iJ`w0T{&AX%M$`L>-|g(1 zm@OXOH-(3cy))0-;U8Q+Y>WG(mgn?-ka>MZZvuJIcgDu~FIn)}(zP^aKu(7DgYXZ6 zf2Df#)5Y(MxhQyjoReV=q`C*u^9nZQWR#D-lK5Agzk(l!^H-6=*=D|I%i&<+x3eGT zhUV6qd40&Y--x{v`i|s9;W5GgpuxZ5a|JH|zpuK_zqrK9(nh@r zL2mAP?`-y%%p*TeJ#lM|K92J6&XB&d@_FHV73aTkth?!d5ci;w7e(Lsjh6S^R}M?? zc_ZY%`h9g#_*eKJ#JoKyyOVGrna_YePF45E2D_4PVglu_ZVI=S^9+w1pHF>f_;Jj0 zQT4ro*AnLnew;1ST1GiO)xl-Q^|_}$t9q<(T}I~=2kV!~10~lNa`}CS?>xtk*e-bn z<9oG*dZ~(2r1+wIu9z?S5b>{eP!73x`PS%Snzv)lfE+UTS9}j*&X7j`gIlCGF^Tx? zcwafveh{1@@Y|8=gV&PxgNjpxo)>e9iilf_yeRt4=%rS;PO1Ch^li$wA4qxBIzGAc zy6(Ai%H5BDUh6=-KJ+HkT$JBe_+E{tJ&yA5GOv$wGMJ0P=Y@OF@OixbMt+=YCns zSB|irrk+=TX?~^lILsHlO7p9!J?=L)fA4%GK>Jd`zhdvwMDhYKuMhL~1H@;*9_O#r zqleeB6Zuj-t$T&v&OZ9J{r$$Qlb#p8SDR^%0}n6y&fsik5w8#aL6vXk{UGi^yTyM_ z{M@o~*@cJ+dXIztAm{opzq0k1JTx@K#DP?tZEzsLXLxJpm7Her0;FtMOPp<$XYdL5 zUi>%?i`Ryg`Mo~koaYXQxiT01Q`*02?`-x0fcvpo{|_<;a;*8q;cpSI5ASyVUVSCK)I!QLR34kTD|S;W}&cT1h5RQbHr z-Wfixy_y4QpX4gJKID)CdgRFWiu3K@^?|FV^6lUhfzQC)T6n{4`_Aa&lq9w-_6#qP zzO((c+b7?voKx_o?oBYaw!<}l$@Sq}alRd10OXM8688gpXYM=yTlc)4tMO?ml>H#? zL2$Ms7gyUV*EodK2*DD31yFS3%^*nNE8gqvwTrJ2*wQ zeH`vhnDY#V#{@ZK_78I38M(eQp5;1+3~nuZcpvnbG{xOdoNW1FNe|-nWm1p+;^|wG z>wDb#x8#S{bKxh+Djz+2cw6G{ zkS|qvc)9P4b7e1l26#-A7rz zDAxymJMy9-ZuK;8$6RzNeXs1+Mr{h*_2IEU)p)mTaZebwLUJ;9ElrYd&z1e4!;!$U z5aHG`XM4|~$g|!JA9jDjSh_m!RX!Hkh zuJFFXydC)~z6aUo1^yK{kUpmGRjnz{&^0$qa>ye*ej2_>&qdi2*Ui+&i4yKdo7I)P z0BxEB`7&{~@x4MX6`Z1^VFka-*~XlKJ#pxH#nSwWIYoWKOZ5B-d*@m5z6x3KrfGil z51BK7`(evf0}uJx{d?qn_1~n{HO5>t-#uwqlFwx6(PNJTZ#edYO^vPJJ4&9xIcc|W zYxf*lsNYwLfAwj?&#_IRS-!swALijm{lPHG^^GHM_;;D-iyAZ!S^U zlEH6>FLe&Rub6)Y9x^!F%tMB6qJa4A>82h%^BLGT~YFGhc9)v z{vYHy!_oyu$nVTvOXk+%eTDDUG~&0z3&5NrXY#}`zg>AumM@65>Em#30zP{9CT8xc zlsp5zSH32%rTN|NB=?|gZ$kMdRF9syAIxu8y$O7;z$rSA@>S*~%C~!K9`YY_{z~x~ z%p6F(uh5(LATOn2*@?F=?ez$X;A>Wjf;e0#Z?f4&5 zIb`nRbnCWh(D(Aa0;dSxaB#N2^4vpSOL)VdPy0`@3(XmN7k(zZz81`dJSTk|_;Di7RuQ+hkMNKUj|uV& z!@aHt{UfS1zR~h$>JP5;NtE{$_fp#^&%nM^!)u8?&awrcw&Nj#e+BQ7vBv?Qfpf?_ zzd}w1zEtFpk&}UkH^8mwMnLu8{pFIAL0*)5Ue5@h!K(c@>K=TLeDn?`k4csIQq7$0 z#{-|CUMlhoxChm{9Uk7!G{17U9!&WvvsUs~{BB=2z1RHzE%CK93-`l*bem0HROJ~e zgEkqz7l3{A@TD@Jp-u7(^`W<7KPI2ofXIGL5Y?t}k8IplG~Z&&?6=6>`#9#-B(yq4_qs@MHN_AZ&ZAFZPtp6cN8{`EQ1OLY`p zpYp^prwHc?dz?ekOZ|3S-k`4C^7VdD`3Jc_XntR*{8g#&MZuF%a|Y&qj9X>3@$e$g z@IUgU{xz{_(K}(A{d`A!B{^i|WQL^u+vImv`F5w#`Q#19yd8b#ESigcL>`k{^1cH9 z>fv=Rx%10=9RH{GO*D{qsg}H!w*EnNt~k#yyR7xF)42wkGvM96AnLv;*SDPdIM3?u z75X@sGa!EzRNY8@9Q2*l9!GH?jkzekS3$&YH}e_b4c|`QrOk;MG-p`szj17zfivBj zZ_KQIet!jViZEv|=SBH_<*WBN@Gfzlf%#V<&) z`f<0}$&e@150oCIQ2f21DA`WD#@EMR7{gLwRIp?=F-6xL;yi0jBZ^!!zJ^B{%<4hGV zfZ1z#?MxAIKdN=2w_AgqAfQb~<;5?!lK#^Q*7P@0>WS(A`(>oxzjg z{8d%))BD4!Z{7%z`ITkC0sU?-)jcn8YZdn+Snfe1f3ZLj^{u6O)e~6eU9LVDO z%V+*^Fuf#c!|SUetaE5CI$iTcyABQYE($J%Qf zeQp_&=(sp7Y%9I5+UMJuTZ=u;cFKz?-voU0?1|$&4)_eeiQn17I!5MKe-!VM@>=43 zH9meo% zE9C{a6IvJhDeVWDe+56zcsW;f^1VVICzyN_59Zsyj`0ae(%vQB58BHfr$3!5#Z}uh zHcEKN=%sQFS@GMElkunbRlthC^ce+P$h$N{&efyg&T)m3Z;v5fALp+)FA5Iif0A0) zEYdxCbq{iHqBVYg)RmxD^t)a4=({wFSf}LjEdEJiN@=hBtiE`NAgqUR{QUnmCZ)^>LoT$n_zI48ADuajsBrqHxeR z)bmogz6?{}*>WP0_*d%P&O90RrNR@JJFh9ORs7D#^})L|rS5;H@9z6w(RZ0uDW2pX zR2~yMnP1_3#q;*(Bl`%SVLas-IM=88&gOhOzE=yPT7HL9RCR2|uEm?0*Zi0G?U;-9 z-ycdn`ks@3CiUrcw?W|ASxA_v&7JWYm?Qm%Q?aOz+Xq=y@EqcPaITMYGKc+;E#e1@Ug=cUdS{DX?u7bNeiDM#O>IRoxNgHwe5pyDBCpPxqgcK#n!-tcQ@ z3hP}d*EdD)aqzujpO@l(d}y;DgvSIq8M~+KMi+`F4s!1 z{D^uJ%4^B~!Jmic5D(e#4|bNE47h5srN7wl85Fk`{=sXecRTvdh93txWT&;q>3@*> zII2Hr_;J8*htCVVzUj^#uiZGAM|&Lj2ZJ8?A1s;b5$+&;XLy&gP23OMgFJ8Ncf0cA z;C(fj-tD{}{3&R-*XAK__GlE3Nw9Ehx#xwxv%yt^-&yri!To?20Pm|X;uPU~wKwHk zI#GbtE2RtF&FLN;`mex%|+Qih)iO@_?Y#$o+<3kE z!mlOY-D=|nFua!dUbRXuRe3GJ7v+4rC%vx*M0&;lDD$gL^gjshM~~xg)BDQo;r;Qn zPhAzAE9BdelWC>BGxDMi#FGg=^+nYmO+9+duegt+xV6Z)9}!Bt=-&dG3fU~VUCNpV|gB-HK*;YIm<}O2Gb&dBw_ zW6~HOOVBHH8{As&G3(_X zY$XmP`*Avv$K&wXSEIeEleyUV^ zEsN&ej&ra~uv}cyo#vw8$?%+^rs(U;11Y_%6Oy~E>zezv>3ap=L|yD5J-;%%;kysT z2(Qn9yy19Xp*Nv&$lz=@S%%X8pv5Oi&qdh_fc#b5%BQm`v>yk#K6owRA1u~h0QS*y z&ua$pkl_uVQ}CW}ikMrg<_w>R9|xYeyEg)>pEJ#`?5L0P;jzIrKANkh`Z(P4%BTJy z_@aSjq1jHtLpJ6Nd%Jt+oD6(kn71=e#^5vj!E_JS3-<&6gFI*OwzSbbm^aN)cru}S zF4~FC)dzXW!KHh134++Tj~!MNsm5-@(i<*>}-7WU5AEw=Lhek{ot16 zTWQ|@Fx`V$lyAS5(t^d{yXysCM9ll1?fz3g$2Z)YCzZj&btIhlE(=42nW*a zqrcVYOz-xfY}Zt~q`g*8>d_nTE1okL?;jM`_Ax&<6I%nfVn8RweaJB*JtoW zm5<(0_fp|qdaK8Kzr!hFJ_FCMkVD2?6kdP|;q?Uz2a@OQ>fQc6`6iGTc*y*{s?vD|eqUjJrMv*(A**|k^P-QMa(&GGfEVDb z=lBt2erv*<=^jj3RueHk;M}Y)vzB#e-J(|6seyi7TnMbw0r;6T28YMY+77s~&j$}_CDnP0(2 z53eQiSH0+a^;eHyB;S4@;i_e9{GTFw)AtI!3Dw7G&sA&Gdd~X;HDlKz zn;bHHsm%RAABS@??3*Z?T1NBs4%cp+%&!bCm`43U#cu~+l=BRwHaTQ?c+sPWALqIK z<;4Stf5n{bS-W;qUKE@n^asI1zHhlJejMMf~x_b z%&&56=Iw5#T%XD_?6ct^wL4ByVZ{v6Nr@UzBiZ|1P3-ZMi7uj$7$nCxP`dX}l+Isy z%KU17!mZffLi2qu4}V4G49qFwUaGn8jQJJj3^kM&Ro-yqWZ-u;=2y%YO^`hfxV7k| zf`4^{ax&=AZ>D@Z^V{L0hj)p2$Yx(^vF2ZuMQ^5a)xPHiemnX&$RRUl`y0t29~C|W zdZ}NFFBSQAe6NtdLcX29S6y6Ms5enS_n@sW75{_id3{6P@G&;|t6?5DhHun;oC)#I zP=Ap7&M{{z4@Jt}ng0jT9|Q*yxjy7Y;dj16d{IBz59Yf2>32KM72a3a<1kkZ+z&Mu zRh%N_T|$3Qc}UGW(fNS*+cowud*oD2mTdu$OY3JgJQG5ox zufS*M6WD#c``~@u$CHma zn(jfBL*~BoDj#p*i^3aT6ZAK)cZc}PJvh4zy$Rmq{3_n?*BkbB?_=UXs(d^4gAR88 ztnG1petC!7p6f;@dkSY;<*&e1qe-KWu z^ZTmr;N9IPHAE0!RPkiM7lk+6@qu|e-d8>}Z&#i;?oAlqE94o#DKh3)>Exq_hnKxe z=+T2yguSyPi+wrdvKCZtbSAJ;jg1UQ2j*@jr-j#d{p}Zs)lu zyx~`ahI`D2KaxxcA`_%1q%@7Xc#U&G{mkM91BYBtbKdAByn2W;0tMUxho<1YC_+^E)==qgT zz}Mrpkbe-p)bx^+4P#a{-IUGTv9{;|xnUtvzw*!X6^RJ$* z@g*O<>ZO7&%DxHA+c9To(Ov+)2O}ldx2Jnw(_EDESKu@7oZ->qb7|hr9$w_zk&_u; z_pxh+@UP4q$ktJgPdU5nx<32Vah>Zso&0QCWnN;1<#+sp=y^>hUn=+v*gK;)@#?Bb znls?t&c2DZo^C_udT*9oU+(gq^uFpQdmQAi;9UZr;qR-GH)NEAAG|7Dwd!_$9F@Nc zlw2S3?f4%Aza66a)SE!hYqTjZ3T`d-II;eT z3-&MFV985#iR&4@!-fNidoXB)gEeTyjPxMwT~a(5>~Yd8pDaxzPLbi8z}|V;%9o{& z6QFrA@xxa8Obq!t<^}2xB8P0;gUHDQW_M0)vmz&RYvcUVy@w*tdeON;-&ygmFmKQG zy-J*I=Bl~o{z0EBc18H!!@owKnKKc?mSBl?mbF`5!E$Tpv8Vh4b#j*~xwo zd{Op0t33|p?eGG0(tYQbJboGe4!y6i9~@5}6F0dB%{_Yfab{(QmbD&Mz0{W^Cj(B= zm((9LdzT_MIjudR&z12%X!bjUTf2iekp9k&*XmEcR~aJbYW9km=`#z8Dt|rcbZwS% zc)t%QC!=~^@x-k)di0gryM!F_taf~cuVaQJ+^2gGoFaHF?@@mczKM>M>+4PYcIK*8 z={y7S?br{Z@BEW^0g#ii^#a`5S4;lEb^U$EWK8cq|C0EfRldEW%g*Z|r;h2pGkD1S zy+XbneH=5d5BDH*AoJ^gmYfXxQt!(>sCpB|ynXZXZIr)yWavHe@cPkQ6h8VG>f@}V zcRS~=s!e+wcmcNidCB`K>4ZhUubgOph5g_*;frpamZv?uDlhsmdBbl}P6oNYvD%Mg zZtp3Q_YVAVjs^zf5xkj5l&OOUt68;x14c~;y$>1Km zdt+Ah;QhOb2WG|UcYAcp3-{5bF~nfdJv`+hj>TUV()CW_w<{uO#&E-vq150?2Aa>(F|;@#e!TdU@x zI9F|rl?`xh0~L4yh{eZoqH1{*TYX8t#U8inBkbx zN&D!NPs|~{DEL?KhJ%O9KCf@6k5j1USG*s@oPl#P${UWoGx%5L{UE#mhTl2T`Wp4T zz;8GE2hpQ9I7RUAf`7H5;{CiAbWWyk;StKoaK3#raf<9LUZ%bC$F5%VZvV09jPPVQ z&+zE+W6~eQxw7SKWA8k%VIgrpZkYUo@X=%MJdJu2YA*U>!YTP)!8b9ers7!8u7#T% z*PhUOXXTqf-xr?M`o--I6NaYRBbzh;q;YJ^a=c4GPV$RT!=2v~?T$yuy1NFJO6Phi)3AJ~I*Rrp5 zU&^;KXP8{K$8~*w?=e}^yUo8ubA~4JhU0w&?~tZ|n0~OuP1k1=aw5@{>bqY zw8!CGAH3nrRpUHERrd)EQJU9h-s70(?dT8Qw+tcgQZn6xzLXc8wJTcpod;x<7Z2JW zOumUn2If+40=`uCd2J(}Od$DEe~%~QtaE*wLss)E_IWkjn62{+f7ia$O4{S(xm(Ha zd?w}v&A;M#`>jT&b8Ux%B!9)e33zywZ^Dhd0PM%PN$;zx!$*pD3B6SFJ;)xD_W5?@ zqest+=b|2k>vc}%L-NrF(|*ur#8$ubFnjHrU=J_nMUm@E);#2kIWu;?MSbU{p4})f zxWEK(++3cH8=W3+a z<)8?eUwug&NN~2Nv^-O@Pw$(L_j@|@veFmQaerNW1!AEcK z+xt+Dp7(>;J0s7~a3iF;|NgzjPh>5jo|nVgX#GEE_IV-C08gCai-On3`$2ekAETU1 z<)LWV4|dUh=T8!T)49GjJ!jxP4)X2FkE8fkf1Lk=ZxlGm=l2!vLFUQe z9?Tzfmb~GLC&S!Y><4kK610!LV@hU*2i=3)u6J>1rT;<9Mb-Zxdi3C5O%xx!@`l4} z2@d4ZS_jiy6rMQr2hpQ{Y;Yy{ox#~wUI6FRx!JRHP6qwK%fhV%p8+0|gkc5lZ&03r z`-8`mUbV((Z}@HPqvxCqa>(l4?k}7o)k_8UBcPzD@|Tk?*QPs1^xL8H4B*MI-OBs66IS|Op+C6Y zZ*7<(^}N{U1x}Gmx2^RT&qN$tLwSa1%E@rPUCpoN=)QCA^7o_jg2TPL3~lLI+O(dy zwQAlzMmR-3+i=y=mVIJ*De>N-?63kqzY$-0R*=U897yygT&x{r&M^1X4%y=rOe>mq zm;8g^6s_0%_L|eTC5No;LEHXdL+EeX@9Y`ab-agocokn1Jmik4A=-~)o{OqH1Gpas z2a@@sn2SE0RY~u5vv&zOWW3u|UUa7TCIaN$9!xppT*=8Wr^qwho;XGDO)U1`FxF#W zfZM$rv#Ou}f97PC(|!=#S~CaoP4eT|Yj3!Ew}XE*k@h&~4-RO@7v;PtxV5gikE|P= z+|Rm4`p)c$GkmENEq261&YR{WUI6A4tG9IbT}Z}{!dU#XX>ax$2U!V6G!D3bO# zmDUbPj^r_MpnH(}IG2K=#XrdWD`S2YxGPfgMZsryet%$f6DXUzGy+^ue2Xr(C>X4?g#gA9vv7=UI6Z;^1GcqahQw3W5VzDFHO0=5nj1N zW)PnNeH?QRc@cTTC&+#fb5VX@vCoU=SI8mHGjY|v%Dkxe&YALVzpVKTn`!S1UZ3J$ zbxi3fy@{lXMDaU=f5jZgPl!{5UMl7c;4`o<_3xCEX_Y*~MjPJ*_*Z$$cSaWk&-Ly) zw1xc6;ERG=tN0Ay*5ZBD-batUbBUj~1QE$R`%-a43 zr0>jJHRcp`A`dUPAJ`9?`B%6HUB~wuT%qR-ymw|Vz?`yHnKR^>yq4gq!Ml`9e&_x* z`{*9zJl%6vh~CTrcW=}TKV(Ij>3Vo<$f&k&l%fmV36Cr z8#Aki?B7MXKI{h_*Dl-?Xqq#yH{9?|82KwZ>P>V=b)fwq<_uMbB5eH5?1?jYecYP} zD|geq3GStG&kK8J)k`({IN-@(F3KE8cz91FzmWEU=^j*k2BYt+_Rh?$<@^=q?S_vY z`@!~oXMC?(dTwdjbbiXwSvzZT9MJssgCsMA@;C_6Z;!gjAU32GZA3e@hLug&>;e-p8 ziInR@FI90r;G00M5BtFo**hcOo=@D52;ued{0e+g_$Jt6f*i8q$?*Rme5vn_UF5$^ zIFRVkt38gGao}tt&j9bzvzk-HTs3eY zqlmM;iSk$6A4D(JfqeA8kcZdSyM#Q$Ckb`2cSAS&{yKcPjeijND|i7oFUtIO%o)H{ zGrZxI1mdb;j|0v&_Je1}6%OjuZEJn)nT5iWS!|t| zWVZ(YgVV%E&-=k`;vZzMCHjNroQ&a{u=Os119^nJmYI4k%KwANA#-m6d4?~lW}kYW z_RhEm^X4^^cjL zt=(t!(0*sW2Ms=h>Umw)c?QfG*b4x^^M#_ueg8Y{C*E-7F){M($cyf{KIhbN@uhNq zQ0;Nh$5C@p%-i7w;CwrH$XCzIKe(nOb;GDtagvjHOL#KOLq=W{JY>v83xns-xhj>O z*Oi>s)R{?3tgozEvte~f#KFs&vyI-wnq@~Qhivxn@;wM{ZAaoE!xQ)RsgmnmUG6{W zKs_(;`tZK`%xR9K}^j7`DpCE99G)KPB9!o)_;2d5_a} zIJj(1wli_IA0vKy6}{UlyL-y~ioHv}2@iRM@MO&1aJ6@yC|-ap;$Lz8iuvuFLk>Ij zSykV{^o&j>P7yr3<&x`DIb`Hy&>uvPKCk}gGj9|319^rZ*$=*X^quo1#J`#?+*sof!u@WrDp0LJ^lx=$2pkN%Q`-}>$)zQ*N2`L`0e%LqaUc>S7v@Y z`Z%2Hvpb@CsUziF@!pyHIEvqnT%X-qUT0zLzB zedy88a9-H2bmUIWli{4qdt)D=yl80k0L>TuBFSlO#HOHKyLHbiS@;b6KWOy4985VG z_B*Tm75vU>E^6+jg3qA*gZ#ZZkn&~bW#WtSxiWfQ2KR&K4Db(n(44`^G;dcqSgDdYUiqf>YOlyfgN zBq_rB`l_T2>Euf_@(kR^c`>nN(R%4kR5%0+zx_Gl$$(p{IFKDBf5rKB^t`YiWPZDG z5AI2M%sR%zDQXj+7xvBvO}ReiY{#D6P5nW5!_9n#JB@GYcY9-ei)9G$+wpGCr+ItD zvA|tX^lrzw!khs)8TL)QL*Fa*JF9cW+>biqU-?Vlc{+Ja_F_?^Kif-e<4FC*8-z0|(?9yGWg&qcl(f6eky!mZew zv>zNvy;Q?%312FFUf2&#uB&p*=s$T(+Vr0D&o7y5X-aI4%cmSN=A!UZLA|dgiI3i!_zc)P*UP-UkM<8f zdi=9mXS-n@b;EOrhx|!Gt;H+8f8>*auGE`&oz9ioJF|aqPFc%gN8Ov?xoDH+1$wu` zyHxD%E&V}w0eC;yU%UW~H)heieUX2{f`dylXnxf@ylm?Bk&*r0a_&fZQE)$I>%8cD zmAB*`d|x~!Mdb5h-$btmyq4?*K;L;t&BT@>cgwIOpD7_<#=Kz4Gr&IxFM!$)IwnrQTN-Jr`B|!50$`S<;q$8{un{XJD>c4E5-1asqcwAr53q&#sggEsHJ`Zmqfp zOZ2=QJY>%GAt%E*WO(8x2YefmzU-joukyWuC$6$xUeq~dV@5B@w{xDMKs+Y+UUjGM z74i(;^1i~nUG-AItz{l^-SBMf1wj7Fn2V}Djwf-pJEYE$eEaQ2{~qrR8R7Ml^ar^| zZ@jPYKd3m{xCb%6npJ)8#w<5?$%|q?xH+){^(M+jE}&fB&65R^XTZB%@fqxhf5rYm z&Wj#gYF)6Jec;2qO07fqr zejFo*3{DaDgNn}p{~&r^53l>X_?>^GbLB1XD|o|^zjClNQ2q-1D_7$6p^wAo>OaKm zOQ=XP?FZ9n-u{_oE!~6Fly5Jtza;Z^%&*`DKp*D~I#+nN&-UI*|AV$XWahV{M~}Vp zLhI|RtiqFlAE!Wj0r-2x^DEA`V?U_AS8CoKMqW$yE~#EBbBfptz;g!VMXORCw~kJJ zWSwj7+calTdfc z-QXdE&oK1=civuMeL{Tni^%8ImwH~x8~$r-1MwMNru`s$mmXR7x8%pIl_}q4))f6F zIpp;I6US_v-gW-fC9ey&mU~{Er9TKij#Ek}%JuR5ioF2fGk|{uA3fiL@DFw-PLXx((}SxlsVfysYeg~75EI`eq7QX zUi2osbdMhScDp3-uh^H`DE&eF53-M5dr-}gE7Ct-r3+l z@|*#A2E4DpXTZBXE3C+GvT)V#KL}6U7s*}3muh&!9f`AjPwxjaP4BDQ)brX)e&=o` zz9@Jy$TQRv5Bb&jKSjDzfAFL3uQeh8#j|qF1*ki))E9PIJKUigbZ1wexi%LH{6mxch z^d^kHGxOWAcec0tCwT!n(SGo8^2A*%`gPyc)34VZbX_YPNbY$V+z;fC=TKghbA8-* zMsFhc@`ny*#KZf0oFe8kAb+LiqIkD&ATPkfLxboZ#C}k5YmvXQ(Koioh5BjX02Rq=&DtvxZTRK9Ba!oEL>}!ja}z@Wip#(%^nP zleL(*YVa=M-45>(-tFLiVDEg_(h@&E>L>E!OzqL!c>jB6@&dr`yoY!)(WMoKV$O~i z{uTS^nFD!C_i^yPnqS_LI7Obs7rjt)XW#kLzLe`@4kY*J?^1uz!Q}H|J_9@^&kIk6 z=U3pjuMcacIm0@dUm-8rsauiGiz;5F?bs9vhd$rL88x9lemuhAa_Po}^A zUa2{Q>JP&2{Fj7AON-{JO(VW&%i++nK${HRX^oZ%6*B*YSV0n_tyhyvZ95j|t~R zIoH>B^&T7VQUm=DzU*}^XkL`v@&?PWgfGcs!ajP%ze10m`B&h#BQH9Kd|olM$62S} z?eII#l5>T8yP7l1mOKOegP1e8hj)m}PuyVHPo6k%YiGMP$h`fDtfiFeb6UG#Q_!x; zV*`o%kuuCmJ}>qHa4(hjgHGpK4+oXa&2~)fnDn9baoG>T!@F1b?E@kw#@ARrrS}!{ zWbEvaX8;em+q&1N=XIvtoWbCJFrNY6D3fu$DmT5{i6%|-D)Xn4b)7hWId8HQ?3(JPdb z;dgr=`BLGD!?|LRiDS2fk?Ez~>^zk%#w2Yy;hc?6vGc z->dHH#w0&Re1;3e7d7@coI}2!ct0+GUeUC+QFbN|?>iX|l6&;(9z<{AFT_>* zF2a}Q4BVricW`aV>J6h;Ew;`|YNq!U@(jwuTP%HN_NA&^Uk>%WF6Fe)|KQ(Ot=zDt zWZ}V!XNtue?jw7gVTpE&*OA}(JI@bvZvx)%PSQ(l&u?cAWG&4_!N20~73S?<$$rpH zb8Fj(`|*zS=r7U#Aam6)XNV?VpTWNh%?>DY5*~8FkeA3~g8xD8(c5~%jl3v(mja0| z+I!$enYXjg%OiZN_`IA~V0e{u2X6?U0effU`W$KAzG=uS)T4(l^#$<{ zqBjA4J9=Ibo1DoD5VLXsz1wG3H{1xO_Z82tkVEF)#IBKV_X~Dzz1nc{y~^N%wdT>yQ9SY;5`odgNlDOvF>B) zJ9EC>$Fv_b-dE^Nz{87qJNTlB`dq1=7v`epA}0M#zTLbZ+^BnAoAo_t?&EN8;v#tg zz;939kS_do7kamYf5m);)t{Qq?=^2j|eoEf(hg_PTbQ)bC z9$xHm;NivI8GO+zr|b4zDf%+=KuT}g55kwq@Ag+bE)6g6y-9m#c;eI^N4?w8n^1ck z&bQlB-#K6Vof~D30}n6mLGZ8GW5T^u<#%QdB)DqJ^_-!P_~_AhzI8IUa(2NC6Q>B= zT5yUM_A8_L74xrn-mbW{!Nlu(Dr*UyD`U<89y0cWesr$xSXwB5#e1B4jV<3h9hqgy zU-7xZ{0f{R?{?;kf?NBj^t=q;L_Pfvavz7imQyTu6I=B63cNmWwtE$3 zXV|B7ww@5K8qU@G4re_l3a?M?2iX$`J_B<o#72fo`L@d(VMV)iu;4? zOI6+_@Y^RzPKG_aM$ZfXgD*?|>U!*Fk{4wUFZXe(4lO9H-{`*jV{5Cmb81j_aGCv) z*6;6W9x~qT@B+Z+#rwf2+CQjrGEa+#mwR5EXE;RpD~CAu@SRgjM#l7e)7eElyl+vy zojttymGvj>B`?bF_RTbBur7#_IYYyZ;Oc?;vr}K z4p+^AczxU-^ejxva7}Tto=SdBd=tv=48AD%SBG`}ioHueotbwqO>-dIQfrAP1HW^t zjUOj}`OfJ4;J3Xy54G=A)b#H8H-xKZP@uY58_;* z=XJDduAYl-rku?EL?1n8!1rpR=0Ga08u;x!#cK&}?VZryV)rLpvAh;Ps2!g{?FV@- z8dzq3#6j-CQC_t{F;RCYCxhMu`0WLfli~f~7vjgk{0e>?#Z@!=olB>7@H>7Sp+UrpB+Oz&&x;|Hs(-`1f4z|NpZYGnS=9 zW0dNUl$0YKM%2Q#^6-0*oV05qe(t&;6M^aW|tki^s&tX)Af+*hk-<=Aw9C zEz{m5=6)FOcKi>v*W6lo0oY@r_*d|lVDDTgeH`ou^K5v1%)c^t$a4=|6dyhE3_NfD zyY@|>M-LA#{LcKoYA5F^^kgydka<7I97y&LA}7OnhFY47a-QK{;?^>+Z@Bxf!e?mB z+2&jypDW~$os$AK1XB(f`$6#AtEG>F?-idbczBZyb{PAJf4^`K?H|na{*`)OpOYVldB{B^ ze|4kcN9oanTg!7%^d?&OQhS&<+a=i+;;ON4B7bs5Xd`)eIVbat_NA&`D(`WsH~Q-T zLD#`Sy62U%YRjJU@p&ok2l~#6FN!?F&ziGs^t|3G>5z3_@@VqWBQN?_MfIr(m-jel z^z|5(;d8J24>riTYS#I7^ZW{P2J8o!Qw07Myq1dlab`@m?41pMJM*u6C@;E2^6dqI zTWQ{&BmF^RE~>s)3#~8d`BiRhN7r2Pd7Y)cGx(ym{XzH#kweBF2lwD7<)Jjcg4a@U zKiH3xrgJh?;tgkBAA2p~^Fm$}=jtkPAkBG(@-DB^-kCjd+?xp4Fq62o{Jz5fAo2|L z!hvLdJKpVkj?SR|;BgbDC_whkcwg~;a0}%{;SCS%Gt<$I-tFptkhy9&SBlTDWy(Xd zn->+uB~x!A!gtf?2l~%+slVcX`l zhUB8Tt zv>)6^a|Y&YziH~@n0phdmkOQ?{5bjA!^<8M<9&s9du*xocreXHCusj5`ZzyO&x`q2 z%#&GR;xmBPXZ8Zv&fAf{+CpFBe5+g^&#&GiZ#X!R zoa+NmhJ9Y_;RRn5{C2$CU)KIX?xkXnQy=mN@kPhR3?QCN56a1a`!TQh>iQn5DkO)j zd|tjXzcTVy_#Z@0Wh{1|KGipR;1qj<>Ri=vmBqP+k+htHur1Kw9N zbEXpa!|(za->crU?V=0yTogQHg9FL_LF8m2#5ck7cJ|}kJ=e!vHRWB3lJ^xnCfu9g zJ`VVzoAmn%eP`w2-II7W?VNsJ;T}YP@cd#AOG{imd6&T1Mqczy>f@vm_e14HttM_Q zcrwgyKTGE-NAI0eDBsTfcE!I!o}tU&0MDYpE#$RSc?RamV16}i$1k)WL{26?V_ng# z1D7iEWbcgmRnpQUmO*iji;_aOO&&+R)R3Ck&fXILAn$Q*Q@$OX z?f4*^pg3igj6>%xxd5>)QUfGk69yyu4t-Rs9$KiVrerM!lej(q4*+1y6`Bzn^ zCS2az$~S>`JN7upA>)6LdlT&w9cj)`O8P>)K`}VowhFjh4lb%<>vfGxTWrdMNfitP^tavh*i~7m?Dzzw-xF2di$a7KEcUHa$ z?s=K#?P`zHrWL2ie6Ffb)r!ZYm(Gjw`-B*`+M}2oL5N<8^ydJ0T)l3&R;S?F3xUJNi@NsN=vGzn(Sy0Z@ zwSkmFp0U=q6<-wR3iGQEm&Aod`EE3E)piv=o*uR0CGsvs3J=+<53fd_cKAb*|9!0tXVDBIc@rQ^fNtaMg@n>Ycs`x4d+EUx5P& zZf%0R+s6&tYU0+S?+iYJ!TpHOctxKp-yKtkC(}%vZN({qcPTe8l;&4?4V%tQCJyB7 zq`>${>q{$FW~?UvU|wxUn|%8adSAhhgFa4hO)PnMu^+_y3g0Wviz**I`@B5$esEo@ zy)(Sw_sYCo@vk`7$K2WhroA)woqu2dkbJMeZ@1TZh8H}})0_cb%TvVb<6NKOUp1}% zf^x|0i9-$<9^TB+k-nC&{Y%m-`Z(Ml z8}1qNGx4vOTgy4*?)1KbCr+I!=8NujUQhSneRF?W>}9zf*C_LL_`J57yq3;3do zB8QB;DEc^OG`BWK`zBslInNpre>3SK@!K~@FBSJ-5%uVKE;`$@<6yh)1r2YX^EvYR zj*H|U#JoK*Bb9uqn`=8!Uew#~d&)DwW0I^;bnr{;9N&61!Vp)m}?@;*{T+IYnb*`h`F0?=!N8*0o;yQo-2< zrwDuoe6L=q+tolxYCuDNr}jA5J7bSiC;2P9+mUa_`wHH0v)2;+LFMx*@A69BqBA827nr_RLHgaUJiOTB zn0d(fAAExDLBkWrz0{!7H?%L6c{0qcWey~KseBKCQr-R7?2-;yT`7MB{uSSY%&qkvmF)At+^WUn$zzf$oFdNkfwP@PoTC3EJT1IF z-Vfp)1Xm63cJy(Kdl32dWb#cI-lf-$WS+}wuqFRJDYMsI@iSL}&npO>@b zMZYHwB;HrdXE@b+k9`1fw&8c?d+_bl2le~P{;@{lY%2~V`h(+%1IfKqTTania<2G$ zb%XrQ{J!e#-$}S1oRdM%3%-dCG;eP@)Ry)*oNs6T)w6UD#(J$BlIb=@INR_poroV{ zT_ST)%&+{VKM3vz_zZ2R=e2`+UfiP(lyhaw+Z|1LQOD?9nlpgcw{dhg>f?l*?zg{8 z`%;_9=Y{^@o%=z>{b2r8)1e@plR=&#vUnHy2ay+bp&T;vknwJB-5)e_Ag|fvWbi+T zTwh~!=A0t(55mJ6dh%1vLq3u46n(Gwef2Hn+m&x(weI8GL%s=cKUNB#0eMkyYnji$ zJQ@C8DZg_U69I6eke?L3D0@soh+EtEXxovS zg@-)OazS!3oM)&xb*18hSY!061X3RhnM@#!Q{sQ4;fxd>~U0oQ00)XnEW`n2Lm)`d%ona;9UX- z()@169;Z}#^xSv$-&rc(D|mP(27eRvbZnF5MobjtudpAyQExANQQU*f7X|+c9uww% zDBlGB2d_)6Pw}tr$+{b$2g-u^&W# z5dVX}kr#k-$mV%F_RgJUkAt3<+2@71=xax&@2I9c1NwujGLnks9k`%#ecX5c$TBpp zndYLN!@ldiQ_dCU?eOC`>U)s)gM1HSkAr)#>)bUyuZ>#W_ki-r3z9{o#)SO|s z`$he3$M@=q@Q3}K$38M}SC{dva(zxoGpR@K_(N^|^sbw1{DY^6`+?qs*%JpJJ^lyb z1qjf*z9`=~;UU8lw^j4ojdO*(=rj|b0sQvuQ@YJ|iryTzPWYnec`;9>zsWaYJHNub z9eo`5CX~knoNe}{DzByTrK0EMQMdb!`4xJp*bg?g{JXl#vAMLz8ATr6@912adlTGu zZbRPiJe#~|x4aFM7ggs<<&eQc#=CuJ+((wR)T8J9Aak~jJOg-r;J2$>AG}M-8;<`$ zTV5Z}uQ=BSULSg?*yC_-f^*0Yi`MA8Xkg9q<1xZjV-6(uyoxV%a=QJfiK9})H(~6Z(erZ2db8w#W5=pnTK?hw`~ToyjjLNAK6>@N3fvI5&F*l^*@pVp zy5>% z%4X$EUK_gH&Kk1(jkN*No0zV>;cDK_+*;;9;(xGQ=a8FMFU<5MZ@5=ezFXXo1j@;9 zzC9wM#?+&Cp!^m1?aaU8d^_gtJqO-PJuf4NjCuP%#0$VZdgPGHi7$GcJSO{`*Y=$t z{=xBK$kIPiIK zP6nPh=4`9p1bBUgl0&YcIRo#VQ;Xg_@SDkNskk554t%NjA2iQJ4bJw~DQ>2@D8H|QTxxCp2ai)PRr#Hf>q{VB-*d5bmX?^g5x?ql z1@1>6`BLrT_gQlYQ+We zd8u<%MVxKSulQVXu8)1G@J;+X;c4R5#!-Jzy{}rx!^?S5=8M7`j=nR$uiypvMYy#^ zl)nnrK6><0*+*|j`77j*k#9%-iu3JZv>)ss{y~)&Rr|p^xgYSv^&k%~bJZNAm#X~E z89w7jrOLVD9zEvms+S5My@TCQ_dkdybCup#%>CeZJACw|Hvfac;+x=k`!Jh)JLmec z+@>`3)B8d8J3l5oWX>UHYA--O@sK&+ZgAC%bA|U6_XicHs9khkT#Dt;k|kl$!d1Jb zzgO_3D!=m{;WNM+?iqYqdS3G*YRKmWt{V7PL6qw=crv92BhQSlE0w$`_XiDLAAhgz zKQ>o-Ufrz+6Hif|;pbBmFPAxIP_7U6Ah>D`(VOPHJM}hs!?)5Nr)OSzYTHC->*op2 zP);WPctA~={nx!G4lA6zHq?I6GjX4iZ(=;<89KXW)&5aA>%h7qYsM=p!>u#po0F$3u-_-A`+^M;elQHiH$C}>l;1q%1&iQukd4bPx)h5^1YhXo}F*0XBZvtF3 z_FBSYf^)^a3FdxaF3R((9Ll$sQIEcv=AzLn9wTpf5aqAB_0Jp~xu}{mC~ht03_WIVpHe(LqR&)Er~k=S z<6deJ`BM3R5bvvZ?(n5@o&h--czEF-Ts|b0d|rQ#ZJ?aYg@6}K|ATxFvfud%&D-an z@v3{j%VPtb#`ckO#W|U5+7GICySbNYo{O?44*o&jJLi~s^q&z=2LFS64h{QN;fr#vuesZnhPSmZ6?sv7uab(w z4^&rfk=_L6qRGS;Wq!NIu+7=S-r2&OYKU2XZBiRKd8J* z=;I(K)BV_y>b7>zY5o=VIOAgaQ@*{I^ishof^UL($e1%A-`-F^rEBh>=gB{)crtts zZgQIyER|L54Xmw=###;urKlY0=0MPWAgAazukCWfm5V>sqn;sTigDkk7GD- zAiK?OADu)0gUjSxg}D56C6wmv;ESq!yUJf}cC*s^>J<6B8e$^Jm%4G#^sd*1ha9k> zV^XvA18a{}SJp=qmkJNr*5~yny{|6P|KM@SGu)f}vd&+@8}4;^kL26$oBOkztK2yS zQyWLLeXOn1`{Lonyq!5k@J--*#r;9$1volTcN-eIbr~E; zxjyCdVt%_iS3Wy_k^SJPm8&vV70o|TL%j)b)tFmro?o37--LzcqW>n&Hs+!@S6k^^ zanI{^bVHoC|5zIYszhF|W@#>n-9TH_@D7lW>aQcSin-xwRg3;o?hmCr%N3 z^t^YzPTUXfd8z)OvByEKkMmdT4gZ_wUt#ZzTwiS1hw?w@Mso)A=-CSpAon2q2RSdw zUdwl>M~`!5-aCVT^#t)nnI{8}3D2+i9%SyvGvc-6-UQDXupi|9AkP_ikAuB4_*W14 z_X{5%^Rwlkcr7mu{U^<@;G19`vYYio%3u9easHI&<-O#iH*zw!qO<30nRet915);6$CNHVK9lx?;9sSgd|o(LYVQm$KqCDQDh?#}&P5Fk#FH^{GVI6cO!pw? zWZ(r*Udt4^2jK^bb~-h1q4)Fe{=Y5@7@!TkUa86IA~2bG5x z|AWXgfUCxyxT}=E+Lzc%a>(GS8Gan_uaIw7c?RQq6{Y#@n74;)yM3sgo?rbEFkkO+ zUJH)(N*t0!UdwXIGoX*7yy5V~;oS}|z(U_8VV^8nV=0JpjP5>r+m!dHKiKkX?TMT+ zzZ^f}euON4ZEZ+SW?9_{yNfd%-|DkNcrt1(8tJ=fbl3jDF4wLE+03t$A7=pVaggit zlDR1M&gi8Yy$ST4+mm+*J$mr3IET#NE4j=yZ~lDjyc!Y(W&^-EZPr(C*wdq`tt0hr7MolsM&4eUk#DGC^*};JQ>BURdZ4F z=sVN=YKOe9wngR#272B%_*S>Af_)ZY^%ND z+b^}3{1tp&Epcj(!`xb&t7~_>ufXebwEMgJpF=lzU#B@kjj88_{-Em7KO}rn)l0o8 z|AXuWK)xMwQFz1AcYcDtR|_+nh~LipLFBKvk7M>b+ei1By=BV#!{_vw?%3|)A14aS zrsepr^`jiJANif(1>kr4yHk3aIFQWi3le_2;`Q0i8Ng>iUKH;u&R?Am7(w}V^yuyB zTsi6d75dKTrQ(0EliuTSe-K{F=ZUKZJ_GkA+^iqToB_QF&h@4G+%Nfd)yFaNqMUCB zw-))UIMes)-N`9B&+zTM$$qC>^>Hv4DoFX-6fG3Xc!HwiG`C0p&@owim z4(Az=7hSdVsAXWBhKUk~h4K^d`WQVLk)e2sNSwR1T-;VM0`!D%YhuCl+kwZ=> z-I@K2_`K4|M-NXNzE^hSiQ_#E{s-5}9>?SI1$tlM9_0Cz@&Yv5><8ha=W_*KANZo+ zUn#CyhoqSsg10pua{S?1{WQvp{v!9_Gk5fz`Q84x_yyzddld5AAWdml{TX9Pk;?OZ|AsGJ0P-ADnOE zA+yIMWP@W;+xR`g7sdAqdmQ8$dg~mr;>oC7U#RSzRUZfcgE&_yeJ71d_UTIhgU#e0 zES%ajqRnH-x2t=QbI4C=Ph6?}SG^|<%O`Fv_Bd>7{G1(Sb5IEEG{=xO#wl!p&n|j1Yd|u2K#eVQt{XeL9GJlV=H|+jR(RY3+$a zUKAb^&h_O@-7Nou=ZI6J_Jhdv@!lEST5#1u<=xKnE5#|oyB&FkUeq64c&1eNSE`Q# zzq8#Bb@kJ`ZXEO?aUgF|{))M^@tQ9R53lOcGfzhCoeiIttv6hG;?Sd4-ta8qxBE$d zuqW{u)>sZNSt>ka#Z|*SxRBmgZOP}wz6qQw_D#SW&Ul(;kCrMvX{Q| z5%HzMNB@-OU+uB~viIa+d6UBH=d00(W@LX z^N->g|iy$R$QoUCo7H}NKU;^6bbyB&RJPo3*id*^G!zv3P}dZ~&h z^St}-LpOTYgh2NCHOy4&&Sr;a6i}!fVrrX>~Vg#dAD;8 zc|y#Omcy~XFSx0BeYU-cecI;*e!JmI^|Ca^-Hgtc{-EOZ;U09liysGk(NyW<{QvKH znZ4nhZ=dDaS#v+8@A!FRo7K~0F1m*HIGh*#jy$}p$&b?{J}>ThS!Iud95TF?$TNfp zUsQ2F_}#90Ui*amQI-Eq+J}j5)-lwh4=d?%?6_%ug*^^DyvAHqdBfR1*fzdxQs{>1 z+Z+!!Q7;w!LB-krd+e7{lY`@E&cMA?qd&NaxF5<#&;CK?Y_rdcb27GEHRfO8eWmye zm@_;$yIpi{Tq@<;7x-q5?%F@Z+~l=Hj~+hy8x=p% z9w$|L^qlKceH?JrcC_+MAkV-a-re?J_4XcCK=Uj3yx0q1^yrzh&F^-*)m0m3=sphS zqVRb!XB+b?!{_w|c}(n9`-;zt-|g@M@SMTeJ7dm(?-kD(ETyG)c$bjBY9bzT{+t_> zzXG2jRlG}Hmw);n-X-4S@b}8#x1*QJeP{OYVn5iHc*x*>z#DGl8TcOL{UGQ1kdpyt zyLt7O@&B~O5C^i~9sh$W-@cVNkZR5VUn)Ft!ON$v4a~_ZyMChO*FeWveRd5mr2e4t z4}yo>qyG$-dg63KWmSMcLJUiEs@J8mn7Ec2QY{6*Bz*g8vdOjtyX_F953itm*( z<&Ym-RZBT!&h?=`$h^K^?&6zxtHdemem!US*|Z;=I4YIq?eGsaMrY00LU~aqr)~0X zM-G{LUMk7}-ikDmVrZ|Qk^>m0Jm^_}0?Omk7@s;T{8F3sEV zZa-pq#^g&?obB@LCsIx&I9Qz$(^DVJ`>Z_p9AMB;)3{l0uukXETueHV6DQU)rkZp~UL*`sxSVWVFf5qM< z{0}k@`9CxleN^_&%)jEi=;0-6C@%_MDtO48XV_fkZ{i{2TwOiU_M$K4MYmB-27PDd zi=vkbULW}Fe6Bdp08Ww0A*);;{LcKo0t7gi#!((C}Ut#UBs&;)?@t!;Q4AsP~ zy`l40|ElhOETqIW%O&}hgdXGtI4|?|a_5Y`V@JL1<2tumdjXgOiJT04sXfR?Z}3IY zOMTD&``%-vH^KRKaMd^`lUw^+<(miA6~$$YT{+Jh9N$3R@YjyKbFM(=Mfu&1{1te8 z%4<25JSMkw-+5h8$bmm9vuiuMW)1j$jHloC^HP=`q#nJQvuzH}Y*VOU?@fm8#H-SA4_a+R#^L*l}VLyof zLFD@Iy+VKRA08LQmkRF^JiOdXWgar;+u;r8JOgsb!PHAd9|yh(@h)LM z$b8Wmno|@;dC>>ye=tQnaqoL2;j2RxLd{Z<62FWAX-cB9F=J1Ia}zWRC;B z=+qrPM>5Da0UmOJ=^lh9uJhpLZUw}@@}>M$Kztuj_t_r>?h|c@DASudjX6+4(3qWvKE z=!ctfGWQZ+^n=8&$v;?gYTV@yDA$+n<2v`kVvjrg&hVvn7j7;5&e%I!#JdC@@;c(G z!NYr2e5o$fAAIvbD&<8NTBpa`ul|YNSKJ>&on( zd47d+l@eM!dEBt?$VU%8!ydT@J0zyj{L1hy@tomy+(h#5qDPNjDsw-6%CD4MpR3%1 z|EhMd8{+=w(6?#M@NsOFWxRNJACkVa@-FQqP7%-BF&9-i8SZ%vZsp-seP_%?qv_q= zU%#)wzry#5xgVIfm&iS+d|v*`9jr5mv&|k}<(qJu?HHXGmumTl@(k#C&D6eB%o#>m zPe>of;PsikmdN$-yS;%pkkhw?ZfGn0L7p?eMaA_d$L@UUlJZN z_zXXt^18fN-tG6#{fYd8iih0hvG$Tf#=ITg@P8)^l5?eaec;L1EgDQcum4i64|^Qu z*7Em?`J(1M4*G*zq?d|ZALi|@gB!_5|GMr?ETH#Qa#7fU3zb`H+q>rLx#$t(=`v-;1s!=?m=*h)V#fQj~<-u2=Vap zoWbaM)d^P(Ib`;Eu{YdG{PuoTdyo3cJ;+{5e6Ki%e1HG!(c!+DKY?%-b`hm4#I za>&Rt_}_&C$>%CIEQ-EY%>C#``F8L{6~7(caOJhcy!~0?Uxi2f67Ua?+(EB(y)Ih8w*J#gjoF2YH6kD-*>x0lsJs@vq=ZJ!lykcYERb(7egs!@icBjJ32R`)Tq` zC~obXlkZ+~CQeZy&9781HAQ@0<7nPqP49N(kUL9ng3lGaOIJg#E!Y?PtK}8xZY}1b zX+_qIS69xr2F2gjJejQx_1(G-_V;`@a0|^D3YHZ`76tly-aq(Ow|5&hoSS@P#*Uvi z+O77Ze0yTX+M?OIKgjbd_`I0UfZhc9&NFKgG!MBwaUgr;9WI}(=c4)2n}9bQxjxTP z>3z$bCtW^w>W1{4ds|;jxIei=)*Rw&a}N0(nTsm!2hSM{-voU0cKY4U{lV-(!^w|x z+teRaJY>aHL(dEMpzVGT-X(J{b^Gx7eS93-UA%H4NB#%xC4U9JDE5Qc&wzr3)IcrBY(D^DEqq6xx* z%5L%ksJ=7uqAG{{z3E(mhm3m=J}oy%J72ZOe?9KyMJqwA(YBR#4D7Dn8Nxovrt z=Ix1IakO_vkKT4J%6{i-Y-Q*VNOUdm%)@EIx+zoQ=g&%|%%@74WttL5F!J$m-hvln1(s#9V+ z-J{35eK++c@NRFm>3L-jxW{#K?WM}u!e`fcOuJKmriGJAM)0(a=VXs+yW zkVD3t!RQYrWTX*SZFB8CuA9Y|x@zgCG`~WwZ@kRgm5=^pLR)Kx#8l!T6pnA7IL7upolIz2{;yDB6SIk3(Cl22$_??a3gt5nY zS9%j`EgwmL5WGIj8Q@EWCyw7&=ns}3eVjOuMotEE2K8=79|wEq0GeO%|Df6rdQvYH zdmMbPs>$cY^Q)omZS6j(evtSK_a~1Mp3IN=b+mUzUesgMdLLKes@)X+m72GIUj9Jd zhExaQehj30(4u=2h9?d@nLGIm$Y0GA4&>{S>*F5%yTpOKvqx{{Kq@~D_@eL+PWG!5 z&UTwc$;8?ImV6V|Ekp~&kOT*JL=K9%DWw0HRO6}o-qV!pE0_6InYCTKebSp7e789sZaI6S-nZ+_sz6tcakZ)%nJv?zLe}!JE;vwVRu5vQJ zR2J5@cP$w3%@{B3OGS?!dmP+@!LlC&w^n%p)VXSw{h;D(tGQ@7aX&cMS3&QquE|bW zZ;?0Lo_eX7rPn`d9d+SWe$^1#ZiFti? z=v<$f&)_fbc7t2nW=(jezbVfE-vstJH!Qa)FKXnFIp2<4A9`Nw1z>J1`zDO{6~0&S z58_;L4%sF7Rmr!rH{9Iw;(2>RbncwZQyoX%dbG3CmP>O^9xCrm_h5UOGvM709x~1q z`0XtVpNsp*lDhO;@lEht6z}!~`d-ZtpBJAi3apP8oW!9ra8mo;$1=?2j?na=dPnqnYgu_>*Md$FycVg)jRyqatQYzdi49O zZmX`YpI=<2xwX7^#yyB$D*L=_^H=Z>ewg@8+Rx%Ksc=r~JAPEUPZ#2BE1#F0_;J92 zNl7zkU$dxPPTp`zocl$(BkDfV2JZFfbT;H=P$F$%1 zU&NC^&x_~n4bjbU9+nF>_u%lX-aHZd0zW z2YF1KlL9yRZ)R=wV|$t__>g4YD$ zU)`kM#BjWR3rE)SSDc8qdfcNbybsvX&smy_d#{_;H-aEe}K6-Gr@xEf;1oynKcUI?$ z_k(W}PX>Eup0_7tq!4GjpthrH_JHrlOrT!shn8X5=f!gd&LKZRoFe7N2|l^wQhO)+ z$C_l1(@6O%czDsrfgcC|g9iVK^H<%81Ih0z=8OJp86Wew?hhg-!+BAht0t>U(hTxV zVBT)z85(HLU~oUYDSs8PbAF}+@!O+T^iO|Zyy1#}h51z)aX;K=2vRYFpc~; z$cy^h%o(;#={~!C^yav9%ZI|hx*JbMh5TFxZ3SlXh}!$T7s(vP7yqD`-xMOLb*P0Aa7Hy zuX#iZ%^7B&JY4QBbA}T#zp|S4IBAsYJ3?GFV}6CbGv=Z%6IZP{>HJ3D9j_hPcy5d2 zMd7tH@(kRYKn|JnqTsh9Lz=4{0ei?sLYntC#9FlJQ-V$$r|cAn|l*~p2#82cJOjLI#&j-ZywFB z_A5EqqM!qMYm7EB}LWLlV8bf=@*~C-1B3fETHc15Oe9=oMcS z`F8e(mvwnqdJ|(}E?V|U9|!L%J3Dy8!ISBd{4(Vk;2%WK3!XT<+tW0k;bwF$?VXJ| z1J0G=GwdR6t?CaV*JsSHxaZZ8dh}rj))Xzz7_&0m8XSL<<_tbZ*6VYHy|dv32&5eH z_J-7RzDM5Jae?wz(XIFlIXc&OnEE)pA4Jb9MDh$qUbH*qWZ?5^d9)4nCXjFExhV4O zYy0k_|3S>JnBUI)E96DN{ouUloqZhm=<&TWd*Z->gnzJ(IFRh&y%lrYGAQ<&s0qQ# z$?psfWZ6-F`Cg6Exjy*lF=u#3dK0OZJmO!O{Wu0+6#s+p4}vEHKEwONw@m3Jb5ZtM zW{&Qz{e$2@Do-5eukgKscPUXg+who}y-O`;9fY%On{P)S2Rvl-yqHs@yq3?)x#B*K z;$ImYNR{hTIhkykiz*JJ`X7YP3%&`=8E_AJ4l5#lJA4zXO*v%6Rl~d;+z;mU!3$8_ z(4e^=e6F@d=E;5#dz{Hf{InNf^vV^K7d5;~+@n84z0?%VDGIK!m~wsX5;KU;fP1i= zQ`^THMifoWnsb}-S3GZ@Kz$tM)}GI=Oxv6Iu=Rz6?#Yf>^C&M0emnOE|61@-?9V2? z=$lRQShdGdHer3xk3fFySyxZ9~fpay3d=v0_;T|k09H717n2Q?RTDRHTr)(WQ zzt1$sw%YF;AoF&A;eJeae5=p{Qkk{Gh^!ZcIi#?tN!FMQJy&74<4<4KyrPUUwt56 zOYYHU^c^>9y-(-4HN=ytC*MSyku8rRFB)|6GwM4#YL5weUhJC~Z^Lg_@2flW40fw8 zZ1maj#*y@M#nc~!-J+=Y@H@@>+6#5brB+ijb4(t-n`1zhZ9fb(>zQ>sa@J zWnISCh0FX3dY-pR*F22;{4If*#r^`bFoo#$xhHnD=cK%+yl+e?4c^al<9AiPVqJ}-Q) zLOkyo+}y3OVZ*uCh0idP`Z(}AgMXDTUd!>qDf+wSiz+{km3$N6K*AHp`$6Wa@w}b= z&fp=Nee|}y)DFZ|`<%X4zv=%$?xn&TuKGCOAuGQ#I7KRl>_hqXnt+Ii`j{rm)3M)> zZ-V`U1Bfq*T;IRrUoySh>#qd5JkWm=%^A@1+7f4{`-6Ut_H?e0L!P=Q+dT6)#v%#y72Ap!4kow0CCT#4Ymh;(L{4!>z^p>TW$R~tH#Q^%>hINC2Y!+HRVDF7Q)usu z9zA-g@bCuIEVuCwrjmyjdC^XkZ)dM%W1JV=gKJH_RPg%F60eW<&gMPNCi-4|Bs>|+ z8Tx3xD00Z?J3Gqv%0YNCm@~k;)Esl&@_g(UQLhDCsON?ML3rX+&ubz1oz)%(c?P}* z4Q{R44_-NuFL?$BYk=m9w!ip>W4PWAVt)0}k~r~sq3=BN^ppE{74}V!)_WY}kg>BFqsm&wo9_t_;6U-UfC$=a4Rf%}w(3_YvEO+wy(58id zkNd>3YUzI|hs?fI><1NJG=Oq4%ojzEzBQ+ad40{4zrtMfyl~av1%NmF_sZ;A7xJaP z=2tl{MfT2V#Bb+5j_v!3dC0-^Zg1@|xlg~_J?}DSm~F~4*mBj7Z%1!}^P*A3m)7@L zwcF|-`77kx`5t7S7xL}T>U%Ip`Z(sisM$vko($*u(DQ;fyjXHFs_*PWJ}>0@X3+bp z9dU}bN?w%bS23DXRC^`(^y9*l8DTvce|z=f%&9v+puA|7TY~rp{~lX!X{4OYaF0yw z;SJgrB>#iGOmhaKM?aSG?SCw&r+GWh)zRwi)Jt_q9+&Wd^n2a!Xbb|mfG_J;ay_lqA#z1wpF z-}LM>xJA4G=Do9#zXG=weP`|uvTtGwUNv&rN&=-aCWeUNm*noJN^n z!D~5r)N1is;vW1aZ9jR#yU0DLdR~UdM9teD)_Ktg^3faLEA&#~n*djh=M3=CpSJNX zRUDnRb3vvZaf%jcUSHi6f0v&9Ge$4=T_XG|&LM9f9@b~Nqr=786PwC{bEd4FMfc#e zwSGATWxt>3aPbY<hPzx#aU|os%(o zsp!#vWBMON-00aV{v`6kvB%{g#M za>%#`!71W=JG_?Q)`HiE{FUNt8#x)guh>7BEnWb;uhMLK6X@f7kmzpWs=;Hza|T;a z+`VJn23B+#TNinzjP`@dkHdUXJJVd0{|5)ho{sViUf~r-oNexY24w>_!;4|#$(wlO9m^0vgb#1|4A)CExhK_J=YWckSzGHJsoU*zm zznIY5T0wb+zoa*T{1x{H*<+G3r(kN+2#3eoIc>i*>*S}jA5`ylqyGnS z4j7Ycrxq_pWUZ~dZ}&X-LClU%1001gnG9x$eg}& z&(Z!>uQhFQOB@pC^;+=Bs3F8>fQJ|TLFBLC^Wr%Jd=q_E{kA^3_`QSCXI_vbuSgZ)W0e&oEk5@z6_dQ;oZ*ptL72*)SFz_Z8>c6;Gz6WtjUVnlqp`!8{r4anwDC{ooayLk`>! zKs;pRMcF^dz0@JGC!=1Y9zFP?+?#-ZP~~LG3ZD?}hw(p{BKdaS5Blm{pXv{S&yZbK zOZj&62UT8lwdE6&H@sGO$d9EjS}{uegUaWH{1y5*$TPt292s$e{s*V&dvGS@+gtP7 zdkyS7*4_Wf@K@#Cev^9i=uL3GUF~sH&JPmze=NbJKk63bH(={`h!6>IT@9|;_nrF^y7rHeL8I)`RKvF z;yFVbyP?FbtrI_v>ZPjrmEy^8k3MiiknC}Mx^5aYLc9R*rG69TN#84Q)wu7B{UFYj z;;IGOa6dkhy)%3hcwhNBI>=n~4cZT?T;J6b4w@%}{UG{-n76}g2_HS~LBo#&zq8^Y zPio4gJ&tE^C2@+d$1(Wrc0br3_Lsb9nf2jSzpsxf-Ys4L^aoYG9XuJ%Gw_@N|AQT~ z?n{0#;bGc4E3c*U(eu0=oNe@R97i@h>f%&p}QuhZj7flb%n>=aQS2VwhKW>%$ReNju#I)2NdH<3oVCQ+t{^@G zINOSUb+2%1nXATqoLcb$F!uw#ROa>Je-L>FaJG>{ZlV1k`%;-N+93J%<;0Vj+|;jX z-_bXAMrSszJ{doPdJ|=Z{r1l~{nwRHmoEJ`kDl*q)pOC_vkP^95Ih;=knz4U?+4jq z!rYIXxOB^Y$zOFRP7%Ma`lT-*Unk4#!+6B`#3)>o?vOx9uxNP;(gUa`v;lVhy7rQ z{Wrb6hUHJr2yG-k4)Ryv_2m-(>Ua4c zy&-c3%-i9cz`3faERb`>erN2RTj+Z=o;XFJHF3u+I?rJ22eEg?{~&r^syESn?izYu z8Q-g~i6;ZTDBkV-z8dEK`_Q+&Z-)H7;FH)NE#q79kdZ^iJ;i^=ZA%~1S z1K)$-s&QV_=y@4Edh(k(ewK8)b)z${IAmvBtB?;MfaUK->y6+ ztvN;Jo)^xQ(_Op(=6CxF`XA(b5dVYNZ0_#j64Ip;fl||J}>+a!ei22{||mE`$6=b*Y@?M z{MC;pKaTy#Ch?^nEAN)Kp5~(PJL4WS`Z!xACx<$iJSN|b$s6G0x~294af()Dj9eLQ zolZS3pB+9&)}7nhD$iheOnC2%e0z=ToyV+JRjXd?I(NO#IPzLD51D-vYJLUZ#OLyE-{5_d`Z(x$;eW6baX&ok z=AGHAdlQcF4oRWncdo1V?Ye2uaF1UC!o&;kwdONS)*jxL)hFU#q;s{a@X7uDlJ)qVVDL zm=!Nr|8oat+v?>V{8iLo@l9}#{tfaE;(ri48T4@;qWsk^>JNf{rQYp0SAQ(H6|&y@ zvd+oOk^P`M^(K&SuM$6w2XVG9nEnS_bRXxKcub7mMBMRNAkF^DBP0Bj3(*QF!8%-x=K6UE+5JpW#c% z_4Uj?ju6ka^|KO%U zFGw%-261a!^N<&i9|!z)o?p#8ee=q6mtOrdMlbSRrnw)OGr*V1bB4UK89CEvkK<20 z`W*URq3_K3_BPQ)ajP{4QgOEX@86?6amdNweKnwJqV%1;G*5duR3!V&2Z&k99s>sF!NogYbr{ z9CBJ}JIc2|LtHg=4=OJJ{}28^dz_qB@2e=?$AK51u%U%~^zaYjeWiGP_Y7{LoQ&$@ z%p^~oHDg`TEaHCLUk-CJNt34cUJRu_B&s% zsM5X(*W`P$LQ33@eOhft=L)_F_Ibf;`4IV1@owjwjCx;z-;P`#_vka}d(}EGn&al( z^kh}3?s<)}E?ChoeYc*A8o55zA2f46IM)ZR8gpy;eHGvsN?f&GxZ>cvGvn*_ z(Dw>FWbRFX`>~096D=*Dk#7QdhB4He_&TjRzpmoksY#c2i_eSCmGa{_jBF$?Kq&18 z@xE$jJ(ciyN=5dP(nQINBG0fcv~l4-;y$#jB_I8_()0SYa_)gO#3_m*Uuw-pUpiNu zlfk=vM`Tv3JcG*hF`t3w?TS;xeVj>tmGjb;en>gwb)g01;l=lg_s#?9T!AmDJSNB? zD{nYBkj%dtKPsbdsqr-=81=%r?t1=_sZ zbIUHDaJuM2Ib`?;&3@+%lD}deGVcfB;l+NC-&gPgEccoi{FUa`V$Oi?72a1(hk~~S zY;aDpi{DH6c5rLM&y1H|>LdPrwa0`#y#FBXNB3iImN;kKpZp?uEzx&oA3gXC@bEIf zy^-?m1yh?xv>{$!=*drrC&PTvr&3C3&VYCOH@zng%b%Pc+GO$qd`o+0aMjRvR=hsL zmwM-ZaC>BKV6gNiGUR`-DQQOhBJ0SND~bE@i*UA$IRoBT=;KTr_I2;wv>#kXUI6w@ zq*33QJtpYmu*ak&&OY_2F*oPUesl*yY|E_EWV`o zgCQIIwzWOnEOSxi^WuCvyq3scO=^0)s^X~6&PAC`tH015lgG)I%HAcj-`QQhSN=MG z#eE!j!`bibcyXGE*Jt>=jQmx9-FN2vmCE&Hxh>c4tDB}ggKyXC#DT=So%8MBGb}28 z|KP$i!@HgDL2$P5Zof`F`h6x&5#|ic7sb52ozr_Zd{N#z zFDp$vKBJ~wc*w@N`mgwLnEUajoGWm)F&BMf#}v)$3)b^?_QcKd>@?V}d!h8`e4|W>coA~X@yVP^lK5MhJQJUUn46h&pWp+3$6oxf5&FaBO}f6#sQyVOhd zB_DmR^qr9x^;_$w`#5;FBiCn797tRK73QLf1Bw1%g!V3(_s*Q_gD*9k<_v9qXb~@f zJ?)**qX(}K`77kx;SI;$d4KFx{k=jjm3e*ehWFHcXZR+{o!9AkJ99sB=HyLnGqUN? zPEJLaX3O`=;J3$@#vGqrv)%rCQ(p9AOA7g&D`k(v+z))OIM;Wcd=uPvR{1OVaawzL z!IP=lXusN*dK1i(;XK3U0pAh7eT~ky-`RI&KEvA7-o%qZu21EV!58(i{J8i5lYbC7 zWX{RBSzk5noz45fkB9@=FT4-s`bsH>jNZgw>w9Sbpg-mMZqa^ln8(F{w`A{pD$47Q z{a_p2cP=BY8goD1(L5Qfhr4+aVU3fzysTFu*$zdC5*U#WRJ&qdj5$vGMD zuM&pD5x4d^%D0D;$3)FVTf~=&zO(9iMQNW`FPbx8?~J@C-dDqE&d~MPo8$#xz9{GV z!2Ljv9(!lo_m%y~=0`g?Z6{6<^RL)PUn1V6QsI7}=LN1B=lVE*h2DhXKq7y|dmPo9 z=t6s(qR4_}jg~FT3L}dGXX?Bt=2y)9U=F1E9|XT0oFeAdKCACR{0|1##8JK-edl)8 z4v8tL59J*aP7(49o2TZ_xf$I=`S$yWFPcVth6|^zR9ujp48B*&mugRY9Ps-7lyhb9 zMU6SbL#uXM9pfE|*9RUlyZ{@-3(#P?2Q5SL+`O8esw$=VmBX6P;>TE{R`gBZQ`mQZ z$m#3kiCaH?%` zah}1Rdi13<7sdAqJehB!o{DX@+=z*axS;1(jz2Wgx$2N)PkWq8>lYT6kbkhEOV5Fw z>0BY#caZ#p;4`ovr`xeNOI)%#C%-H?WX0>l_e%AhJ;@u6o>z;!udpBNp0^>jO=2hO zv4m&!|KR)LwM-7R7j7-@2eSrrcFnH+o$kTql#>ahbM-oLiqOa5bHzQcyk%QW`F7RE zX-o==pKpC-WkN=B(cA;SR_4~a(ElL!Qn7b#qIWyqSI8l&{1x~N$jM+osPdvsBeqZ< zr%Alw?8gBIQhApuXwHEDL6wskPCS{qkjo1W#a3B7G`}4^uc$M-4@MMU77uTGJs0Kg zmHB-I&bGyC`H*bsdD+fIcj-P3=iB4MK3KBOl1F*bcc<(eK3m>biq~h(UtvFpIRoe0 zx5On=zI~y*+c6jA-URl}$KxHyHvvwOx8|ySNu2GbnEAAKW`29~S?mYlo8a%&U+crg zW3s2q!vi~vebm1n@!M4&=Qr_Ma;^_^26y5Vf#3e)sfm}%sqdU7UQ6XM;obzkSD%%4 z%iEA@pXh8oLGO0%d0{_@{1x`jcY1h#t(-%gqL(Qr18+F;R~yb1G}v|TGnv1e0E|>g+-;9~$_bug+;U7#9?^3D$9|ZTKz5X8zJ^3!>+wDfe zH*q_<$<(6@?;G>_H*>=rwnKNKN2wq=+i+lg{(b2v!vUkQ@bjOrp>UkM`XXZe{yOdw{ z$BFhAr#Xi8*{O3f$n_y7gLnH(mwFo>^1tKl)_}7OZtd$$S>zuydS2)ca*zI2eal(y zd9@c#(PiqDbDs3Bd_o36zuRFTGUzHJG<6-voN8iiaE*wtq>wB`3}?x+nb)&K9nk zk?S+|&WhK^a|Xq&#rNt#-AjdkkU5aes~3@XsXz4xmCg8UtfAt^f<19NKTChIE=U82^LV0nSb?7+WGve#1{qk!;9uu{JpwK{XzJ=R3C@)3?1kmEF%xE;uLXz z5Ph7LOMjf_;dh4q2eHSQP5JhhiPv{qxF2szA16rr=<}Cti7X~g(M|1%t0unaLhDP! zDVlqrhWJ;71HLA%TI$k|$P184^LCso;(X~wzTlE^d&2XTTjNftd7i_v2*v)C#qhf`IX9x-m=_`iHfKW81Au|=IwU+ zf6#%rYPbh67X|kNy@`kY`-OW@f3Q|Oyf4V!c}_{Etj;oL;Q3Y6DKDG%mGT0hH(}d% zW{W$%ohSKj3G&}Q1>aQ^C>F}d=+0>3>$zE?O`>U#zL74lci{ZMlT@MKh8 z^uop_@&c%Q`)~BVQu+23OFxzW!9x08vBxBaINM(&bg(+;dHXiX$uyJCi+l7xlOM-x zRBGS7&J$&R#eE$3CU}1JX>}WYu4)&25c`W|tnjZM9=NB=t91*dN3XcG=DaApOXv@> zkDh&A+;;~5ir?+ZW1_ep@P-@b3cU%;uN1cy^LF%7k-y?zYL4b#VegFp!9wy)a1Pnv zK*B$`bj3gEd$l0bk8&~-nhM;O5f2%AXZD!X5D%I6&d4)>Q`A=H`mlE%OW!N-uO1Kg zjQPoO&}NT=zBB(1GXLsp`d*!vIl~0<0-!(GLUU2hi{c&(xr>K4@pwSZF8lAukHg$r z)tfj&{C33`U2gJ*Pv7yHcmeLExhS72@Q}C2oWa=RfK#OQIPeeR9xNR2%@|L=ALk`5 zJxb>a-z!h*53<+tec@~?P7!=w@LHnpjC&CCt199YO&FEhcb~JT@Y}x=o=j(*L&p4y zbI9<-Dc^((<@(0g%|EmE;5@w_WPW?&p>{u9t@rEt&Y%$3w%fyr6`IX``fY+zI;qXm- zKPF3h^mD22JW6xbe58+qIRo>ruyAFALQNyczvzCOWEvAs&T&knu*`un>@UG zrRNn{{QLSot4e8p6(W5c<>7_jxq)~x;4`e0J`VWp>|Nq_J9?>4?VoY_))haOp8eNT zABQ>Hc9Iumzw_l2`Q$M%xV7LRqwhRle5v3w;2vBSw#YYw{DU{I%ryDvl`mCsir~jl z{Pt$qJ7=4E^vJiv!^_@q_59z}YtD z?eAWC^W-Pxy{PZ(WIdknY|4At3DWc8T%Yny;C(fo@>lI$x75~DzI9-2QGBcaL9>Tf zaX+?d9&!=&2j9@|cJ>0`eWg5c;4`cyAAO7V#O>65oTs#Zum_zhc;eK&{nr1dHyplH zV}A9ksYmY-)6a&pz58IK<}=`bkU5Y>UX*iviifOtGW@=3mi-|3?RV?ZZ;}4sHqDd4 z-Wk4Bz6as+LT_S*<6D{oxg~D3<#Y3)vs2sLsitKUfqP;MqlTeazX$-g%*Q6#3}i5)Usp+n#=B#Ww-3rNNUy zo?#bxc%7{d;-i}3nz4>C_i<*&?hQFu%YZY}nMy@~sQcRSCo zz!!xdr#MU3xA&y)6?zjfVbQ*C)4aX*%B<6UDTi$Kn3Pd3l|8)3^{HMe`zDZ+Q9gQ= zZ@)tSgP1cQhs^ijtMos}ygtQe=tk!X^LCz#{*-^M;{2&`mp^n~)7P8!IOi98SXzj) zjrSG&IN%f=E$=BFlYb{XO+I?XznU;?%jC78_KTj0JJM>80}dp-0Hane6u#&i!f(gB z9dickahh%JLFV=0d)1H>6dytSEAV8P`;o7`03TbP)825-^`V!#D|?{uWY|9#bn?AR z?dW}#N4->otHwTh#lQN7`Z(ZzIAzTxULW_oHhSL(xw7EH*k3F!#|#X2Cmyo$wQM?MPkm?1ui&F+?-Fvz30@ZRO>i%Der8+g(La`6Uf6g4 ztkYMo1k)ZTlJf1~^)a7;c``ZOkG$kcoW|7}6?y@GEdym$|tt5WHCajx%x?45t3bM-0pyj-$elV7Gh1A9!+o51|a zQ~n2Ewb)y3(7EFMAn%>s@-paLeIXtbaJF~Ze?#A^w9ux7&&M60TpzpuoEN=B`$2e2 zxHoY@^F{f4l^?i0GJ9EzC41S{$il#xt?t3bqyXZJGG|+H)y#Wm?mNR{veSN6P3-ab z(vs|fDPJTwTAj!f_bL4ks&l3KI6smf2cEcc`d(cnz9{yCFKI8pzbM!D$IuOuzXE65 z@H;DBANW`BrFKjTpqxze*@pTlZTMv0rI#IP&e~nkU2g_TA(Kz`24K zK=qyf@9!1Queg`WIb@ux!nkD1$HbF?-#OUj8ufAZ7Cw|7yJDF2OPXJCuFqTggPcQ# ze~^0sgXYfUlLx$H9 zIpp%hhfVk3=j06sSM5f~Wz&9;^H==7QhZVLCg7WBcewFvL;dS_{a#szY_{=+!#9CF z4stTg7gc>{lFJ`Gfk-e+(V!?qK&x zb&q2qC7rYGk#mLb6?n+wFIR}i1ijR;mNxXgLY_hG2hn#<>77^U4gD9+Z<=5_U-EWK<3roT99;(uR{~-2*Dksx)0;^Q$V_<9LrsvDpt+P+nB|QqdoThj#;UiVnyAMEv&N{;p$t z4BXpgTwS=$-WeRo8JgF(dC+i=^W>YjX&DlGDr#cza?KZIZ#epcFOqL!Z(-m4!PHAt zoFbz~&)iyHeXf}M5hT8e%O~1tUuw_UcG1OgtAx+6QG3G`r)Y%rM7;f)`I!N<$FcQ0 zW8MyKt>UV|M~`_s&J}tS;9tSRi#!A7R}cC33-^rqk^FV_%k5{>BpzQ@ zTB_ge;1uz@o#&#tQ}gH4MYpkVZ-Tu5;B23lo>zI|L)MoPT$5e0LQ5VzcBIho*D}AppZ_g)gZA-TT@-E?j5Oaob>&wDt;2u5C8NgLLL_K=U+f{EO zUfx&7iPy)RZMDb2e$e<{aepwm?}y~afzRtZdAHx2+=)0vpOJr%=b|`Q?4ySt2fRMz ziBlX%^t|AS!@K?UuGxcL@Td-mil~pdZ5bT3=Xv=dU)oEgxd>njHM4^quEL{3<;!_L#8GOL@biig)R}sPf||ZY}tt$_wz8=3nhi z{8oFHjK1?vloz$-A)}AO^LFq>`Q4tNd40wnXSMj9a|SpQS1s(oYT`gH6t5*X+wjpV zuO-jhk(1$EAA2oT4w-p&80q$vunpX0#{9W;_766H74ds;USmH9*5uUd-PnC z^H%e6Q?Hz0|fB zeI4ibDIUIk%7e2V>3zj>2E4C~c{}n~#+(8CD}!76CHW@sKZtu!@vnH^-k$EkKC3RR zUm$yD&NJ}d`A5qqv6qBX#ODfq94ER5d(*k9mRz6erQ&^M^d^w&bD}u|-tEk1K##tZ z-d6!Nd+bm3o-{09=i8H(o}K6I_tlu90gkR)YJaJmO&(sA>qDO5HR|Iui6@T#2QhEQ z-Wl_D=E=Oij%W$HhDm-izK>@R)Fa@a}n0#TRXgsiVI0B+dO$UVt{@^MWUi_c(zr z-TS{yJ+H&$F|ng_#a>Ik2jLC(U*49^75h>-FUmP&d(Bl--f-q2mlXCRt{V5~(VM`z z%63~mB+hGc@G0uigZp9rUh$m4k+^EiDdIeXI#)cu`k1)28z|S;hTiS7z?1Rk@)SZNB`2wMDlrEtjwvs$2Duf*)bk|=jN>$`Vuy3YjdG2uQA_@Zil)lIxhm&p_N$AW{DXJDV#L*$!K z`F3!&+sgkSd=uc-f+tfGFrWN5=y_r9ta@JP4=$jd7rs}Ri}Ji(FH| zd-jUbn7iafla~HKoNdh8^JzZ_&i4N!>|FeMzWe{*K_rXT<`l`yE}Mnf%v>|$GGi@C z8kVGkl4GI>sn5HF=%lPrQj)|RlALD7Wv-dI=CZ}uT(*|uY|TU>9e%gR^Z9zc-tSLb z-{1f6y1m}7_w)I9-0$yEo&ovxi938Khs++6e36sE{EFXK@B*MW;lJaTEUQ(M<06&U z@}}aq>t0LrCXkc4K<5g)z7myhN3QSdvcR*S3O`Qn#C$PlFwU>WQofz}S6zrx)HXX* z`3I}WyJY^p0>9m(X5OW}inFbsE33=*YbUqQQhgl!5B`VlEAHd;sVJqnD7b3U!`qv_ zgSfBwJ9v~hMV!9^pTU>zE2B5uN9>(3zk)Yh=8)4&hbbq6yl9~P!#h*T`yJj{&^vtr z`3IS+hCIW#hCHXG16O!@2yQLskTGvZ-Nbni{ z81y#nou4NjGI|qTDBpfP|IUG{f?F%^EB1LIC&TlrZDyXhZz+ciZf)@OJ>;WD-&xP~ zfl~zE1ov^^qaU?mt`;2khD``B-tY!f!P0`Kg$vFEDdzal@6wc=B`5n;cqre*dE)hP{z`Idv3F)(A98(suCT{JUesB50oXtIXXFF=4&r_F zFSU1;y;SgIDn+gj9$xqdy-bf{>xFkozOTM2>qPG>Tg54g6&y(TQrT;X@8G2{FOe6` zt^WN&$dU9yEo0=02+dFUQYR{JxIVvP4kY%2^@95`dB?BR$3dO}-$D2$3aVQ=Y;pa5 zq!;}UV(-lPcD&o=9%m1oEA+fFQak1SOXb@eiK_++>t{uaIYOJsenGL%!4vBj)*< z=6t?5N&OG*7&4nU+trk308fU!;ru_?!i-adb9Hp_QsptJQS)}@Y&V*D!;!zj9_P)N z2d2j8S@V7){~+e=;MN8e2V}NMY|{2C&i4FE?lpTId-SuToDB2Z??&DgJmmVO)2iq7 zUc8G|mhfZR72yqMU#fBMEPeFwE(Hlr5&AfMU%_JnJ_GWi+q_qYS_z*QbJcQ$cL`og zIlqE06?q2uy!gJt-nn_MuSj?;d43g6J+C!|A;eW{NBzOkX78)5-lGR!aM?@uRcy&F zgBJk*gPd>YKF%iL$H8}y`R(x0SBO21>`k;$JY?OwWS$r0oDAo$nEN4nsoa~8obATw zTGN{`-x2>x8@P!)yx_?gxoYSS29!U#Guhs`cZTSBVegEd7xP7@C_V$vMVWua+*;#( zRS>({^v}gnbC&p~k8mPycu4unhf523DGnrhUi-!TN}em^Wa`K_0iPH9=y9%Ot`EKm za6iH>dDiT9?Ag!Ot*8Ir!pDgJ!ABvVG<&zVOZp(blXisOSMZpCs|F7*pDTBoi*8ih z5A2=g|6ptFG;xZMZ|9zu=itrWYiK_RZY^_);=#{+sKO=xwX8x zv%zUXLoWmOW0)a-Rcq!;#di>S2FyjndwguuislS~g8LCfzKQ(ODntItnf!yyDOyN< z=ODpvN3Jizz!&AYDEv6_qK{K=;6U>GiunwfGn7zHW*+SaJ!(p5F8ZQbZz7vGMbcw} zzB9gq@B)DQQF+dt@>gSrukmR&`v!5>JLubaWktv(LXLy8@^&i z#_GaZM{bGU#1A9K(jMoi>F>hB3vW2Q0CD7-=$HIWyq#v1kV$l55v(F126Xg1s&(IY8K-^c7 zC)2xPALWpFkF$_CkbMQOuakIR;eT+mq3=A!#>S9CmcG>N(tE_&mRz;wbA}-M8p^jb z_XB-rOX4B(yPb1=$RWQKQ)6n1o;mNgfVasLhrB3%2bnM0P57PhzS6w_(vJgPANqsX zJ9j-bb+-d?KQL#2H~b#?CJx#qkteRh>|Yj*7X86o>f?aVp!)}rzrx; ziNjpfQuOF?U!5PBFY;INT+LHn%Zb$UdMt8%Mf4qHULQPhO&#;=)?NANgzt`Pg4d_- zoi|n6(Y&4W3}2WA5(g4>ipTi!p*LaC3_M0^J187xh1 zrt88Rj@|_JICx)`6JPX$_-B$FL=L&J>0Rd=gR;Huhg3x#kNMf;5&a772c7yIavVwT zE9Bc>T)9hY9cPs|Ei-_4$oFZFgZI^3vBv>toB5(%#I5x;yxXPM^4^^g`{xZldU%(* z81~NWAH@4g?w#ei0>6D5`RHX%2HtSV>jO`wXZn)mL$q(>EXYUCdZR&=QH^rQ$TRSL)g*lMJ;@7z9J0TC7vabGoW6tL$$(QdtH)&Wc`;Xwc`}ny0&_Q% z)||1pHAVDNyUwr_d9L(F1zhbT$_fnaw22Y&i zUoATwaDA`vE+wmX`;TE`{eGaH7ka69xAXfdPCU-}<}-x>2O^qslqC4KbLHzDVuJZFI4nYka> zJIiy0T%X>fM;`|q$R)(B1^)`YR9DI~u;2MS^}HUD#{}~$?s+u|o{Y|KXCFPh;oz$E zqW<79%3txj-NFL>LCo9nzCymed0v$HqS809X`=P8$35HHZn-_fkV7`Uul88|Ah;iv z22K%pebNhnbH(|q`owEllXr|ek$q)Tokb^m!Tr$rq9NolfzN9~Gmi;+^vGW^2hz*$ zQdshmqiW889({>b!1aXF(Iurh{gO}rA0FPi*s&@vn$~N-$X~HH9P=yi8NzluoH|ok z-!$Y2Un<`1+;_%(HK(Y0t&{KqfZuLOJ}>6C>pW!S8Kf5g?mc%JS-x;1b z?$OV^^mqqMMh5?^6euH?{+yCWiP$vKM0--_i>OzZZ{yR`@_K(U3RNELx0N2py#FYkehlU z&w%`uwP~d3)*}1aYkfu&R}DR{`^1ykCvwQGvuEz^KzRn{Y)fyr&S$_}bOHID@jqCq zS&6=L<1tI>O~Ave`<;;&WuMnL@jp1vkY~W&S@yi(qhDf{7sXuE*QV92KhA6}olH4o zOX7YQ`R(9rvp1Z%AM+{S9wPErB?T{w{1tdIACoWjBjqvqPsE$VlZlvD5is06tN$eO zTDCauNBk>uemman>|N>`{=T@cBxk#Y#R=lp%KVj)Q^fs2nZMGxwf~DRiarkJSNt7J zr9RFvQ{s{G z?cA%bo%`!RW&RIo`xClr!{Xah&+9qzhJ$~_a|Y>&d(pqO+l#ax45vQMAGCLtxjyWj z9}o|DnES5*5yV68A9J2^eJ0Aczf!TE_Bfa`3{&2v!17w!JG0l4-|g`5^1GeqqWa!h z?s4u?PA06!c5$wBZtc1e9eM|w<@(;!PQ|rcJxAq5;axJ$+h@U`1XW><*(75dKbc`>KxWLf9DPf{%t z?6t4r2PE&z(MmL$U!jk~-lajYpPQ1&!;5!&TjDb?_e1(|(3{ZrIMb+?3SX+^ezY_A z=rI@di(4q>qQ?2v7~(*(Z-Tv+GS6V_53V7vWyslL%8ObK+a%`gyvIqUKF)r-(N)*Z z{dwSK{<*ZzgohVAndhnJ^|tdLBG2%J@Wio?exIW|^}HmnkMFCH;<{tDm+#d2x6dM< z*Zg@8q8m;9Vt$AiO>>5P;y~^Z^Y$~ecYY~-A92;H#eQ&}uQuoK;x(pC#AhfRzf1A@ z;9Z)W;!ksik5htjbLd=whg>|Qi1G}Dv8$=?94XEf_Jc2rUg|LN_}7f1Xn-aFe0ZtZl@qvvzQ@2iSM z9cCvHr|6K~=&H(d)#`mUf;@3EcXy=TM04&3ax!`!hxa(}@LDM^fc52?TA%hCiHAJf z!0U_ij3@pT&qcAvVNcwP%9k2s-=TN*h`GdpOs8`N--Pth|7GB+@w>n z^_@4xt}%VKIF`PHxUcS!&#U>oUH45a_Z&lc2Ig!df3>myL<6r6_Z9QoIVS@@j^sdM zKZy4g^RJkz)j{VNKuR_m$Db5wV;eA9O z2i%Vfw8u#&pBHk-m%=>#E~+_$k>Ac7NbCpU;g#H4_)@ty0e(BUYHNts2X1X(+&r^f zpY%PemqpDWBoKTGJWeGuO+sZI8*-OrIX{2k)S z+&AQ8*pE{xJiHNN-j1A1FwL*5gpVG39Q0BjoBA92I9|$^igWd@7QMVjdTBu);(qv3 z&kH$Z&h;TDQ)tNb>HmY%mVJ~GlDoe2p6GdPA5uKtN%iRAA6z%0BYg)s&+s?;AC&$< za3DE73W1YI#(B6+y@sCw{}46G0~ea zeg{)%?;I&|$oLMX)BhlR^qYgWiSHo%I8#NQf%`b@o4_7tIe7sj2U7RZ4~T7Cur@T; z+k5aimr|=h;Y;OryWHbUJzGq^3FbgPy^jNa`*}K7ZZvNX-reQY3E>T2N9PJXdSeb* z@~_ZKWnLe9Osp?IP`*@nOdb;V134MoSL{ngPDZ}3UZg(G)FuG)`k-oAJg&uuw&@}Q|f+r*WgWz9r zk3LlN2R9R+;m5F5%8$c2N~@a z!`zRy>Re$z$h5eVDAjBTBF XVtV!Ee8M?ym#a#Qf_0cn8H*lYZy>A$LVDwNJQ`TSD z(z&(#zOtY_j`4kkc{{#?;1u;xew@D|t3z_Ut`BI-`i98yiBKK z40%!balmIV{~v^R=^A-VWZxP072a2e1y`+&;EOgXPLVM$ivA#aUN~3qO)w7`y;Qk( zo=m(xag{JJNKOITQRm_qoFs!y;Sy?Or)L{ z&Q%cQWaNI3xoXHWv{mn`&!|6`Mfvu|rmty_(?-liKTG(Ia>(qX$Gcs6;?PUY5pz-W z2YD`#mGD;kV;zRs+4;&CeD5gUGk*-$7&F8FPldltZ?qJi~G2n~**)$$?zsGg^EH zabH~}P7(Y#d|y3J?{@YA;NAXN%yrYKXxH%W{%yp5ko`DSqBo)EWWd>Go(%E~^1cG6 z2=jK=ihU=2cP?1pDDv%?GxVmpXn=iZ;(ja@-$C8OE4j6PxxUSMsV#3!vI*<4ZOC?d zx93y-DtgXB-?cPv2UpFN_@c;*f?F&7&g=y+=Rji4klTNJ`#RwTn3ids*h+AU(087D zX^h~>z>gzyGPNP=$Yavfr1$8tcSimSd*@%zd5ZtRHN@*fZ$ffEn1>9mnv=>Q_etJI zJmf&?(X-DB`F40rbg$(m>P_%mG=jJvSLr**`76xZ^F@D~zN$325m(Kg&J}yZ*=vb8Lu=X(T2Nk8-{Uk=4jFrAjd*?D z#DT>7O7i+%QrwRq`;L^qikWkia>&eq+%;qt^#}hXj|qEtF&9PO8Si#q>P-}n?@T;o zaMhNY`6j?;;CwqckdZ3Sp!X&QD4q=bgA?1=iadkv4X>j26>`WWjxY9WO}RcfXLvy8 z>RsphrmxL-eTNdxr&X$)ObX@Or6;aVe#QCrDaBTTvyJ{B_BiklqDSxf1fRiz<_yeLWByf$ef!?) zsh5iH;AZ7r;+#yC*yEr_FXs&R&sY)%a{G|&inERRmE?ZhS91o*lVQ#__JiPG;eCa^ z^F#U{lpbC_SIj8_pP`5H#9@!a9LQIQs|Nm6bx5|?%|S!xeFbi9>+I*qW1@Qj*f-%p z_f-XX;%cKd&&-)KIyfr7keCVKk}W%H1t#RcIgf0K8~^P3{FvgtQC2~_Y9d%z6qJ@lf6`b zgO46_QJF(NO5Z`w^~pR#&-BIg9jsp!Mf<^*Dke6pcS;zjd5)$3!AGY0=-Kmrqg)@~ zS4O^QkCnd*P7(7(;fce!`qlJBOcmW%zmb3N>&oX&%^`32@OWo&UvZv+-|gt7^85;O zQRWn(?+ng1-t8@#^>N^}^c-B^ol5_M;6S23h&;nHB7c=sxI%ctEml=#`BMIBo5(ZB z`PIgy+vn#7P4{Rsz_L?b-MT9in%!5@j{}d%4EGW;!Z7yiUCX4LM}yi?VNmbI7?)n%EC= zZvuOqcicCK{FMduyukgyydB&R+4GXV3AuO19!L7T+KV3jKjK~JzPd_#oDZsw2)^jE zf`3&*+*(UBo=h6e8IVKXm$Rf~`ROUw_gQ`KGG=h0$n{|^dQosc_*~68vZ`<;af;xH z<2(cUIPAxf_Z9n{Lp|&Tr)YxuAB@mO5U-E9wfG-IPUeTO6v|&UEl3S5P##{*@IQ#2 zS7*`3;qPFs^5ax2a-5w`?{=9NmA#2k@eWCL*|W$;5AV`%gR;G<$v1IT&9Cr3IGS=Y zVMTwfbry5cz|3IcKq4=i+y8Chn=pEpHd0=c=T~^Q>-j6}ou`m@X_4=eIY$Ktva8v= zeIt2z!55Xj)VsvLn$cs|kS*i8&uCbkaRAV{f7)$5aw~I`sN>tDjsx6#4eoV^5kAmi$7TZO&ia zyfBCO?ciUre-NIyPl$iTxjyy+{%$C_HhpmwGtCNgGA^E9{+_*9WfJ{g6K+kJ9@J ze1>+^A9N!>&RjZI`-s;UnmL7Xedy7%&ug|Jf3?grW?+HS=!SmeAH=-oo0MGzHrFA9+Rk-ioNqB;%vi@gL(Ts@xJ0Y1AG%a7nPnk?mO=)ct!M` zEh*p5a|Zdol6;1F^R5QGtMXSoXOKMPD)KHxkZ+<(KS#GN{(Zwo2|o_=uikNP5dA?p zXTblUKEF!sHHOX=&qW_b<;~1BcrCd{ZzJ}DrBBS;!Bsm-`KvL0KhT_kd-V7pe6mNc z`@EQ|#`*SV23QiGVLaVe7Q&ahJR@1W+n*uMw%#AKScv<|yzeaMqR2CRDRO;}={v~p zcH|ix#kszMz|6SS_3GFra_-B&Z8RqS`1!tQ%MMl18#HAAPZZFd;;@TwoXHG3{ zI)?qAFU_y61)W9JNQ@9=fyna89f4?$o2W9 zOwP?y9uwrRx*Ga8(i8XKPGGs~;XMVtL@yP+iNKw^Pxh_wYRGX)7#QO@KKS1ee~+mV zd{KCK+2^Hum-dUCjP3<+aO+Cv>MHRW*b~P*V=l`1E6hdb`)YFzkteQYloQQG;a#%+AN+QnUt#a8dza9g z$R6>$$o2I)yuaYZ^u^1EYX7GBmGsfe-o*D211QhH97wsxk$xN->JRd}9X+r8j$Qga zOXq52^mWsbm_H*QhOG7abI?F%8;gGuXB*!^@Q^WQ;2ynu)nW3OB>A*A_y=>J@;?X< zFZk{70t8*(WpzpPovn%AF1a6gx916-3^*U*yBk4 z)yFE&FlvPF;(Mt#v{Q^}LXiu@nD;Jio%c-NB3}gPhEF zX`d%_(%jGiO@J?|{}1wgC3(oq z{eTxh@{mL9I|#q?G2#?iMs=OBZG7>NFq*f6e}!J^G@3I^ObN){R9bb$_SVNX;XSsA zJOlWmc(*U|O&{Uhdy4You-_Sb=Xt~_dZl7~L$*`Am@}Y1$oY1@uWE?j9#ZU=X`fiH zeWp3Byt8(`;HvE>4kUXmabHRQAh@;6>$A&#j`*VR<8Xgaa<$KKZyT9^d|7W+7c9?`p)#<*&R?v44?<>iHOeTK2 z^wEP`%iqCv1EzWu2){GtqJ=^J9?uV`Bkl)yeVi9vpp94&Py0b|KWr(_fW0&C2icDU zJ_F{W^1fmpJ?8BvRQ^i$4?ejcL~nw5$ol+BzT4M%)rM5jyWLH2Ke`fEZNBQ`9Mml0 z?1G5xzhcEDddSmZvy*4o?l5`-xe{yf{&i>EAFL!LOdCBzjJ+5U96{ix5G!T`*9?% z&+du)O8yVx-7fosGT#m_K=X68fpW;<^Bzann%;`}PlT6&*T+3C{thmpUaGvWupgZ7 z8$0L7;#AY-Sj(txGq#N1HDo632mhixgUrcbkAwc8?wfF$Vbd&s6=;9|&eZbW)JtWa z4EBR~U%hJJA(snJoYC(LZ+HOpQsE7U&nu#6_t7ZIA)CLi;4#@?;4{GQjB|y#XgT?v zncto!dR{zl$N%8Qi7ixrQ1((=QeG6?TIrkMxu^y82j%;UxgYQ@NuCV60N1l7?(jX4 zdS$zqi^7ki=S8s}tfzVV$0vMtTq93hg!cXlEhD{f+L7C0kE8pYnX}C~8ObSPUSD$` zy~Rrwl)riyRUbQ6%-i8hJ#65M^1K~;oawteQXhx&?T-6Mh_UAm#ty2rXiH@AO>-{SNz=KTvZ9mK{CHsSXu3Rg|HssO#O7a=x`zpJ? zZ~N+68^PI@`F8L{OK3lc`4xMYJ~W%R4>s#f@Vg!7%9w8ludkB$?eLgLFMu&G%090> zIU4b=;G_3Yy@{iy)gsRzdtTty8hu`qcX*#ze^&JL7$|QRMpC(p(h0KEEB6 z)bolk>ksmG5PKYW0g%7K-q~&NPcGZ7reBXC4kYJATO_PW?Ur|}YzDpCn|kuOs*SQR zaJHEzQ*rLWfh&f+Gwv&J)nu-(M&%j$(7e5Czx~9&f`|9EdS8KujQ>I8+u=*a|Dg1x zj-~qw9$xA5!rqyAedwh!x0c`SdY*y#40yNOJmFp9dAqqMj^|g4$>$~CSD1_PyB*vQ zaEjoY!23$}CI$*$>a~D(4BQX&=%p6`zKOj@=L_x!&J{c+H%y0O?nnL=lH+xA&^yGF z=}dXiXOcdMccL6JzuP&_@Zs=Jd>m$1Qs4P;)JE}c|6kuh6Y;MuP#*_*(Sc@s(Ptbs zDSkV86G3rx#OwQ5aMh4!*sS>N;ES5)`mP&#sgftt#v#wO+~9WxR}Fbl=Bk+#2ePGj zw=<`x4e|P9zI_b&Qh6@Qew@D!T+jcGxF48{@_mIl1AG(NUUx(8MIMT|X?j1pFZ~a8 z6P#_yfh^Hl#}m_CNZA1tKTI z9uv$NjJZDMi*k=1`77kx<-1+>=;2F+$AtTX=%re%p1*#w;iHRvwb2pUU7u*m24&q!fuTS!jnFG1EpjY}r z@|ZNNTDaavan<;~Vt)H$ajtTn+Q*Uo!O_IO0 zuI-e^1ak)PWZ;{S-f-?a>wM8Iin9%Gxc7-EJ1U3+sh_Lc#QoUhdO_uo2hhA7+}evS zpIS|$yl7d@E6Lx*x6oP{_@bC!alRdS1`qO>r1*FZU)}37JGZJE=k8Dr`EY`>Haxz4 zQp@bQyPs9(ihC32rLs4?jTsLaJ^BNhMVxKo)XV^pXJ9@9bBZ1jSB-Nre6FOA9^6`< zUrBB)=dajnxk>ODipZCW|G`bf*@iC_ygvWj{L(*&vke|H`p)n>OAcfveFu&A75G>3 zzIsLQ+v7aP2A2!ID7*l2-p>Dn^1cGEkN3{VA*1iyP57PJ6UY8RcuYE;nnClc;lyvh zNZzGu=iI73vr7_QOYCuUuO&F!MYm_1JyzB|FFCb!LJRFbqUQy#CH@E96kRq4a9^ENz6t3K*ZHE%li|KI=S7Wq26zGRZvVvVA#t{^nMOtT4e#pTj_xZr zg8RXo?Ro3{cb1&&Q{hFtzBuxwV&0DX>RQ0N#6xDjD8H|)9#e?}gOxnUX+{hB&gqiPmj+H-V%(VuI(()IJli41}M(hXmyr|^KFkduR><1-h zTlR4l&sjviR6qMi#M##O&fLc-JNb(6c_Dv=`>Mh8AUa}RWx!DP4Z;)Gcr4h^^MZE? zJ$m?3WxgGIXZUfjA8ekJ8BRX0^J!OwZ{ncZJInc%^bevx2wy6^OZxnZd40uJmtEWj z7kQ^p&kOrO_78&B2QR>!BdZ0U0rM;V4&r?!J-kJt?_4~8Gx_K-7v=uowXB9jf6B?= zzCuohxgVUrf;YT*UKDfDJ?eju-&fpAwGe)sMo!8mDr|c(>0N^LD)3OQ}bHk377e z2oEpj3@xHM&)80LQM|A2P!8Egd3dMf<`G{MJQ;Adoyk9l95Qo?Iti{C_a>g^G2yxB zcfvQpc?P_%m;=f8l}+4U&1L1CwR4IN9u2!RntEOiZe9Fe3xA*XgFL^wIp|&IhNiD8 zpFcHocN@B|x+(q@-dD1h+Hv+3;%p-)vt?rAP>YxBL>~t|FFsd&#hf9S_ReDkx0dq^ zKZbeveJ}E&w*I;a&Pj@nrP9^KTb6Qoj91 z$}=oq@}+4&Y}0}@q50lp&GMq)ey}&3xwXjkf#1G~_RjV6Klr@hA#+Zq=D;ucKah8c z-&bL~pF8yx@kM_hw2t=9oRcxmMduQyNbgMq)0_d^59DOv;g!9K|Ii*s_9kS1@IR{O zrSoLi8;(2!_zdQL=S{+EiTRcJT$DW~mQe-5!wa5F2hqm?zunk(Hs|$8P7(K=(VLhc z&ehc7fXvp^AMC#J_S%I-JC8~UriR}Js>gMzCj-|fg> zIf^+$9dSRHf3@}Y^e5(`+)I_5?W9o4g>NbjWckQEwZ{PuS?>={68U!7^J1Tu^i3d# zEdK{@WYr}G#mzVKhPQFZB@gdq)yI+lgS)Ik#CH&W=hj*am2cmEyOpigOAW+te;oBV z)|2K8;I}hhlzU$1(+-i>l09)9h0hE8EA9_oH@w@W&x?B#n78BI9!UR#_mmd^IT_|5 zdj`jgJr3qqzG9E_o))?MZ|VEUyHuliGBGr7&x^H*a+QoOJD z-5yTgLHOupFO|7!+)ITQpp(eSF#oEY_@b4-5V}Bkj%dVw^s76WFJT7ke^YWxJhDu#XWlI;ce%T z?fT<&rq#=oeQ zioEEr=RB$oQXeOUcrs&6kLkYJG|_5UgX%l$-f-9CGLb{ZoI&Q0-=Oyu`*C=W1Md>^ zMdxZEaSs!J%bGxQQJF*DEPV8wLzW)iPp*7?V&aZl!smq^JvflaGf3`-yswb!lRRW_ zw&BO=r|v81F_AeL?$JxmHvbQT`*Dx_&hVvzFWN3UMDRtyzk)9nd*`ckt}wsSd#UhE zu-6jY5B?v#Pdu5o-ERiWCGN-TG2cgc32rU-yl`I`b22<<2())1{#DGJ&xlimzBBt$ zeQa8Zo|pc<0tXUvQRxNX_f?wd$l|5cAMDzDiv7bo{^flR?;-EfF!Fife{j#qeifdE zdHY+$t;M-|*L_`ozxMZPtuI>~pHe(M(<-r5TnTYM=F?pC1JQSW;fdbFU}p>AT|!v*KbDpam<@%&2 zZl2~(UQ0jPJ2$_tz`x>oJMvcDQ`!H=0^#|cg1y`+;$X|iau!ZthJQtnPEHA2a zYtbJJBR)gN8MaY5u^FbrqUR<3I4>XGU(h3c@$zBK@(e~#++*eQf`9N~Ge1uEmA|iz zEZR-{tAmbCh8*&dC-|bXcefUNQFwUaAH06}@&i0Cbg?Bsl z&i#^i(|(ZW3}Xi8duIsm68i_wo9&(B$-5M!c*x9?0bg{p*bhn{z4YTCCj)*vxF7FR zUUVk?54NV>1bE1a!n*`spOLe@M|gPIKX^)bOt2q>ZvyYDG0Km_-f+qNkUg)~*|T>$ zp8BS;LG|cyu5Os#kM0-lta_>JiNk$WsP7KYQTx@f^;ipHnA5)$IzSIZN4W|Bv z{vh+)WiRzzZL#X(aL+4qM3nEMIsaU|TKo?dkKbcZ zo|o=1k-iCY9~N^X=d> z;C;p1TKo?(|LVze#os~Qk27O;d&*zI3y|Y=H>5i9i0Y-brQQU5^x!kxq4`yl)>?6j z;01uk1l$kguU-?pzSx1ePTo)C`g)RoFu45QovHR6)qTaj3H%T48Zx~{piR@w`;=#x zMEya(W&SCXayOS&ow2z!#b$bs-9rk-cPIZ~lIe@ZOP{*O`G@vz>UmAxS$eWh#aQZ1 z#CduO&NlnJFc$@9+v>9A@hQcj;yd_)+B=(jmn!LfbzkIUz>@)24RZ#WL)QDw$-TT3 zudiPG4>k?8Y{sp%Azq*CO)y^+z6tD|k0}rDMpt`>jn#i#2syGw`JJ0Y-#Oz-aa}F( zukyuy5a-I=KZra-6V0!5uVtS4AC!IPCgQ4jKE;oNxhUuQp8OxgyIt-F-KwsiyDK=` z%x6H~*_>1Kr|J))kApk|`Z&_(bwF#<+K61A#pQ?MzLK7}0cw7gL;R~U1818(ye}UP zE&q%9&KpGjD#f%VwrQcujKc9d40#6dWY`-%Atg9>gYcN}|6n%*XItM7A}1qz^iT7p zeiZy|#Gsf5rh0?l`H|r}hFyS-~a2R9e-zB)?tEB${^&fDLM@1`9}_&yE( zL2y6Vm&!RA{to8P+(`L$JKOCBpH~a*oZ`s@Uf)Z-3HWj97Yq`891p)siq|)t_*b~E zg5n-0-pKM1d{J;eLOgPVb`ZBVTjXRM20ZFmT$ir+SAlU6+CNq#W~?lnd*s@M&9uk4 zMDwdeF&9mxJx;mHUaLTQU+p#XJO4}ZWRMrlBR<1u+7BlCcu{YHd#M%q7sY*rUMhU_ z;9tE>c?R?c(Z`W|h9~=j_#b3mA9HJ^cL_Pd{|RJulu5a!v;GE9MlrCZCJ9(%K{>r*>cI~bH#j7^l^}HN6+hzwcW&h<)0Z`+;|Lq9QeG3 zx>p9w7kq~E5u<}wdM+7Q;54S8PsP5IlXfm#U%%>H+&kJ5gU5uqYTWYzx0dHucwbGn zvA$J(Cc89AFMg>GkHw9X(Pm3w3G0qqL+G)_@X?&(sRgRmku7C zU3Ayb^FpqVzk|s2Vcrf7B))^5#OnhO86I9g!Ecv2nO6@7msj7JYTv1M=7@#9OU(WU z;m46Z`k>qiDIp@yz&shAGkmJ@qN!@{+^Kh~X@1_8$$u+J`VT{lB+gKa3DL0p4aU`Sz>d`-reN21jMw(x7 zFSQGCwp(du;$KVNofB6Qb9x%_kUfac0A3&Wykt%WduMRf;xk4Nx7K3S)hr*uRcjDE zdVPzLfF`If5@Z;#5qA_$|;X7#D^KlD>&3I#=9F^(PKwq~Ly_H{n7)`kB1X ze}#8D=AvJk-VpP4&Wm#Xis$XNT1(M)4nDg>@I`T7JrW#9?xphl>eqAk1plfl`EkIl z?PSPb)rQNalpUgo)_lroNv!1p3L5p{yW13zn$lzn77M0 zLxk{ofm0+okkaRc{FUx^mib{CtKZ!Wo;~O?Q#S*Wt zTyfQI1-wl?ddx+wk54Y1o@t$E6Sq%uUilY&2a&%r=YD*q`0axXxjx6F_ldLpUD`F` zs&U_W-0&3YO@QAHZf*WVi(w5t+u9b~o=HBhHR4<$f5jf&J>;9%;+;x;9ORIHr27iK ziCoGvq>8<>zv?@qKiJgqnE}&1HU@o4|AWnQ$m7Z9)kHaDea^6n?yHLQEJ$kvvK@Rzl=%xOg|6^KNLO1RG_;z%zoKF3-vW3MU z=i7reH2WWHOZj%@$;?&!D_iPKfX{$;drtqi-G2+1W#)H|r`|+w%E?5mZ&-CM?rkk* zc@KjZK=S&&SiIU)NOK1Cyk_*6V$H-ZPZY}s%oNsUZ$(nkpm^18C-f;GL$zJM*VlIkYpE;+fo!AdvO#5&CqXYjv zH@514@bH4GhCBoM&irm~X}k0GRGPP^rnXM7q5UBCIPiw^eZ~AM?mL4oD*evbJHway zB%h(qEH5hkI1ZGP$rA5&_$K&$wTisq1=Xz_3S7&D*YZo#>&g@Nol7b02c>TUdC_$7 zZs+`!ysz*-_%r1hAv>v^Fu3qrBb9=GhZbwh@S{mo=Q#1XGt;HTEoARRI zY}c6v5D(ep88l`JLyfa|I8t>`mB+zBBuAkV6gu;qjM!ayy(%tr!83ia{8WvR}Tl3KOi3R z)A;R}GaxStj|tD)ajwwEVgA)@;>mOtIppJu7l|A)@(kc1%lVb`nBd*+P5E|nz9{nT zGKZ|sub9_|c{}sl`G1gm^v7dvM?MP4R5@h+4&q$Fm&$yGU(R_}?YB#LVvoZ)8T7n@ z&wf?bS@7H8UE)0s=I!OG@BF6X^`Ym5{MB{JAur1qxnds8ufUT*kA4fy+tDA~s`yv< z4oZGI=NTR)hQx(wAFR+aQUy=O)}g@ld+Md?zSKf*_raH3c3K6K7hqqGD{;2L{pf1O zZ|8iwZRho@>cIF_V7Ob zpPZt-+6#(PpW!e`W_Ka20kyz*=DYq{%#Lf z`$1>1$6?O)9l>V+583E3;l6Wa{=EagoEuwJW|!K_YxruP&a*Es8fSVGTTkBbrlEMZ zZ>ReTK6)$4i}s^@d(7!j+T)BKTqyFQ>|OFwIT<-;zd}L%)|PsyJE)f` z-&f4P$`Jd(2H`Pb@6wG6o5`2Ty$R0Az+(b#E%t-RUtxZQc{}(F8Fa4TF#*3__n5ew zt}b$%ol5<|BX%Fq-noK$^xd?P@%Bj$*&%8!`WyWZR!1HdIb`Ycawonh{s+51#T(A~ zc6gV#HzDVuMs6*0)wu5*WXQ>|&#QpmSIocSJ&w6|Npe3XlP7M=c$XO#Du+DZcf*M1 zdrz~!dncs)6~X-&O8j=6&ww61=2zzgr%301JfwMh5cwv!=XGc8+@dn_0_=8tv0oeN z(SM+LeOX?A40_AihJ0Qj#OoUw-<9(1yvOmNy|bP}=3F1XgY}fZnnoPRwW%%0V}hIv z^6k8L_M|z3^e!PUiv6JE6tQn&zM+qUJr3vF(RW7A%Q$D)T*n?>^l^~C8nI$I<@&DE zyM2r6cO$+0exh>)?g#sE_&dnl+P;(*wI-g-SH!>KUTU7mUwP6Vhx6^oA#=~`hqQw< zzk+W9+z%t4;g0B~at^sW-B%HpN)5a|ysv_kFLho2VeZ!g!sazZKQO&%@P?05d%#()$X&32Tk3LTP5AuBlz9_hAoRg9FRfFi`z?aIs3B22-kKVW+lye5@U4q9%_tA6y z>d&<>reKQG5r-k$+J3ymTIN5c#~|qmL3iWb~ckU4q}4 zd-VF=xw(h8SosIRlYt*+!-x*OgX|wDj|uiTr{ipdhqrgdgobraD+Vt0{3!Tb#2Yd7 z)E~SWFofo!;HnvYUYNIs(|u*mZ?80cE^;!r$(Q<-;eXJ`f#e?jnBnO@?PlLt%A64jF!DAL8}Ny>oKmEaIxQa@g#8p8Pm?Uoodh{|+uY9i#e#@_iL_c8BuB;e93l z2a!Vt|7x`0x1Xo?)dy-n$oCbvwbLyun1^?@gY z{UGn19|=#K1Ke!nuTxs0Ry)#C+k#~v}IEV)D+$c>6GD!t)>%16(8 zXUs+OgNkY1oe z_Qc_RRUR=Wc)4dZ^(V|dJ|{JKL}o5Z@RC7$Ulg8`{#?(3^^I*i%LI^Fh>UrUPh5jIOKhPh9$K>C_kHdLUaBJhqH^DvnxvGzY`%355 zmTE2r4y5GuG53S>?dap^@Ah`t&m_6Uch(Mzx#&;lKC0Slm(k08cslh5vB#0;ig_~n zoPm22yPM71Qv^@O$W=q00p4)-d7+njBkN<@I~UbGrgJ6v4AR4keESwN{uT1=;6Tb8 za-Qq?kzFj3f$TZANS$uy~^yyR8^k4d!Ul$ zqR!fTG;e1wKtt2J&NmgmopXK2i+1a`Pt6%Hzk=7&Ix!?Ou((n9Qo(OG_Ho7q$9OIu zxWUQW?7os-OXfi8d*>sI(}XW|>-e2CZ)e{GcrroDET8fnWKSG;GRXCzKj_rkpZM*4 z5ARm|_NG+}*8A){cv5wY5cuUIlxhPLuQ_n`~rPf6?#=47~jMwla+7IeIdgPF?9}E?|zQL4}0iWS$ z%&!LD1m+BQw@W{cb(~FNXr^y*%j5Mw)zwZ^oFdLMoD&>Kxp&6+S@U z_ddL@;KlUl?}NSGAAR?mCh-` z-uYsfH}##Fe}!Bh^N``2_$_Pvj!E<#L=KrfarhsECl0wj$=ROpKljd?4ENQWw0DM& z9=X1t>&s3rDJje8Lwr&6ozbJ8cD9J-?Z{u{%zSLvJ9`qZkG%l!m>f#zru`$nLy}E) z*zV^~{Y&*y(I5PR&ehA|J(_Wf?g)>GpAwS0uC(Ti#VvoES;8A`t$eA{HvwNN&XwNBVP7iq zMVZflyeQvSw*m&cXB*Cy`F$n1AG{xw{=wfP>q0WT{-iz5Ny;IE-~L{FcdabpQra)% zA7sw9oHLB3yy)hcxilATEw~@ffD3{44C8zc8gNxfnKUnX# z9sNO>>tj!xbyU|GTgCq%INQ0jcMcHVCH8sA{8jTj!#cq!0R#iAuk`6W`{zX-rl!~jQTgOCfhP|4mGnE?i2fjQeLNTS7`&N!6W9-~Qhd=iv>#-C zJLU}74_+7VtF7b>4l<+y)*VWC(5Q9_RgEs`|8=*X|#7fWcOj!wR6?fo6xL-|G_GnGr&K{y$NgTd9A1Wisx4Y)prm*dd|s!1Nn{Oi}pP{Me*C4d&BV^ z#GK(Tajw9v4Or$&edoL6^D^H%e@=OZO!2;wedh(s{~>zxQ+Ae|>|5d4kSBT*JQu}x zkoV3L+CQqbp#2~`ygV2EQ}mtPYW51R7XO1Y&mJr5rtYiPlRwQ#C|PoP26>m<25%GG5B7O^`+YaEz_qo?^%=Pz zx_^+rgEA*0zk_uhi|AbW?WoAIQhprCDQfMo+4cL8<0vnB)by6%s-c&<$7^}G%^ zj;e{cwC`wGQT5u+D-UW`G{5o}+}cT$Z@(EZYo5i@`=+<(Txp)m24<^s#hfDZ`IX$` z#LoGQ{s&t{b(pbh{1)L|suuJ1z-3mN|FX$+U!g~jJOg}Q=+W!EzAWnFFlSqGYtfs) zcd+DSzlt#pd6Yv&FZFfGUxka_1fMJR0@x}(19*MlejwM!bI}i@ubB?V+>5+RbJ2H{ zH@t1OeUe*zC+$c=dD^cAemnDIFlT_@nSJ!~ef6d2rSg3R9x`&s;34xK2YgZHs=+^q z{$OtPZ}c66cPT;(c!C2N;*qC(6X;E_*RrrK^lfv-zAzeNWU}Y z?H+^AQ?5_;=)a9`CH#Z>xoR4kH*pjBCPtfnS=51gUMaoyi{1pdAKZ859)0^$rz#tp z;7i@;^;gLK$b&IAOryl!xl6x;jw5RpUfM-G6_?H{C4bd*q2%>`h$OW&+GqD`?}hupDe|@UGERF-+7PY3&gEu?#CA~f2f=cJSOa2 zvQPRjzLWM@!g=xnfcwF@z6_ss!aoSk_NIvz!z7;pbI~-B>+7GqOMM4_pgs<~;kw_M z^H<0-%%=arQ7gi<;JCWP8&B{>w+XJ=_W7HFf<2xcP}i}zF7?V};y^y4x#)Yu>jU>g z&fBFQ2V6DjAKXoS=h%|H^lrD)tP(O)JLdhXY#MQEoAYEk%uW*gc6SptL$03+bjN6CUGE-5m&8O_>2B++`9EUK<8@r(MXyz?A2`IS|m;r9^MDFlSF@z z=k5Jt&J(9d`f=DtU%%>X+)%;mLrw->%jSJ&ITu9^8FSIVTpz)2N4`C~^zIoOnqR?> z(`ANbRAKBY)A7YiRFD4dozU`^4j(A!k-lX4yR;u%uznKt=*@F7e6GO#Sl@q=;y?z` zoB=(r>b0|}kHhyB{s()9yG386`Be>ZYjIz}mkK@u_q_Oj@GsFzO%r)h^yrx{%Kw9{ zDK8427xL{bwX>o}&t3rdor}Czhgwr!6kba&dbhWA$f^GQ!gR48jMV&ukKX4*+Li5f zk2*RGn5J^b(m#k?-xl?5M~|N0?f4GLxhV4O{}sGG^F0poqR1hC9p6T?PDrPm4E_h< z^MZ$$?<=`?9y>g#*JpMgDqpJP6!DzltI9^|(Pxu?@QauV^5ZbCZ=d7nny^dzkA@fh zA>QrMM~|G$boPji!R7 z1@nu7{5(1gsHMI$@>e&A1G#|y2f=R#udkiM2G^fPju!KF=4=;vyAS@sWsgL&D6&MR}DRS^qu)T$oVVmao}BYAfAl;4&GG0)a#}X zgpVHHr4Oi=3cjdu-i~}be5v>jvNxRh4DP`zh|hq%v-}_YhWPE$NB@B4qFEw;#l6(d zGm6LWpj;pG+XF>^aH`^}vBv~`XYi0W3$7aXodchki!xu7=U2JvT)j>?8S{C2^Zg)l zee!+Pmbf46n*jd`=Sunqx$lg;sN6gMLi~1cis18tf3QCG5qV7D4M!gbT(u5)S*aEY z&uFJ<&Hzsw=lYnd#@?mnOMVV>_xq9NqBvKWw{y>nIglUk@HvrjWh>|t@Y)GV^hU3oI;F>l9xWqz&->$0wlJ27R) zuUSorfpK$HUKHF~qe+TW!mnwT+ zMqes>0pMNQ<2Xj}MVWsEPLX_H)lv?bdB~3p{C3?N?!R;X`bOf5E?NE(&94H=YwiTw zKi@lNM3`^9@LHlrUrhc%^PU&yWP)OcBkF}_BARevxvBA>|H|N z*|Q6Ui9C} zV{)=A`0S^*ZEUSyY8aX?xF2$lgPzwx>ZP(DXFv7m9g|v%{UCVAd|&+)d6@nOdGG8Z z_Bh}(RFM||JQ?sAkZ-S1?{@T3;RWDcD)Q~)8hVo#0KN(E+rfd9z6pL`p+5+pSH75w za*tl$;{+_T(gLYBkxlt_IcMlL!zwC2HkEQR^Jp$Q#lG&&lycX@rD~7Uuqx8PtwsLI zEBHUe>#HXpJ-h(mU-8~~D*5QkXz$FNZO$P(xH*d)GW!QJz5W<9n7Fl$r{)k>4Llj- z+mY)7zkR>pY%^Dl_s+Z@Y-Rf?@nkyXWl(Rze2;V9Wh{Ak8x{-{yguZwnAeBCGkjh~ zZtcVqYp-l=*2l>eJ+J48-@aD(2W2mnIgs#}aNij@WcCkQEKCY5_Woe-kA^+YtI7X~ zZz<*s=uK>;9zF65%vCdTYti%4xoTYvJ^FtM9y0$A@*d}v@b1)ihIfhggOYz`XU1>O z?mw8kOO4SF4Ec8SarEBA8O7_9edjK{*Ai#DxzEer#`0FR_#ZUp`l{8uy>NWD8J1C- zV^fJQ8XqB@Zw5ILuYU9_M~-%gc4grWH@gv`e&#+h=yJ zIL`p@66ZxRXZXacO7v1YoSL=UHoJY&KZqv-e*0a;tp%?S{z3NRpqC0hLo3_uw?k-t zgVW0KfBj+7H5ygYRJSk{@Y)#XfrGY$vFBJNh`_Y$Gq)ygvy3mHmJw!9$k* zLFx0FL;r(vkMrNKl}rAq@>fRgM=5dDT9Dt_iE@3$zBBV=eqHq3>{Qwhg5Q2kaUi>> zbH%;Xx19eNl;!n+_Jb9~XL!-yj<~fRHDQ+y9GycxdiD>(3jm+j{aU~Fnf-^kUk{iw zuQB?*>F+V$Mtl?;>zOccgP4oL69@0oaBYFwJ9EB$E#>--Elx5O#9Br<&)7=uD|jud z&g2WPrQ{(u_l8>&pW!p|TCN+>iT($9kMok^^})kicJk$l2@M;Z;s?ePXWRI0=e@J_ z<1nwUZPF-14!M&0&hVI|__Uw>Gxg}3h~M5o z-$Bl|cNBS1?xkWs7(_j<4+j@}Cx*6M_*(3jrj^7)W=@gpDa(OYQ&%O)vbYJl3~fK$Z0KK9XL&Y+(wV4Ch~pthVZ4rHzDU&=05r=wa3x(SDceUZz5ZG;y8!Qc?K74zk%O=$SzssWZU&xb+c+_QX07r&bG}{w z4sy?neG^aeuRfzW!z<#x+DG3(_R+I%0(ntzYq^gjIYsRAf``|9kAuDQzvG5z(bRW- zh0axo{d2uHig!Ea3@vEyjQ7=Kv3EA-i*7W_$#k00Dr#eFhUwtq_&H&|8%I2E=+VED z9;gD*z(jcGE~L`TfK7BJX-lVRSD_Z9li$o1*(E9`N)Q?3u5 zxWB|)l;7>0gn#hE_>S7=g3qAy+jXuQ_nj>$*H?5qRP3FZTMMol?yIT9L&klzI@EIE zz}TavlqElejT4?Y@ML)JJVNlVxIg&Gm93QP(|f7RRf8YrQO9j{8O?Gs*gL~xg1s|x zGVod^8FI*yTZ`TVczy5!Acw4Twt3!;y|bn11JiZI{V?C-;J!j$6np2>m5s#N&LZE0 zo|Ab-%&+A8%6VlO@nk|W{Y1WfeEW@pFUmgp3lScIe}(&sdtR3G9b^vVD+a#ke9B*? z3SJ*L+hI@4+XDrk;ZN$(&sCfv@Y_FI978-A=4_*vx<}-Y*+1yFbD!{e;l3J2duPre z-w1fyJ+Hra`^UA7KUtE;q*b&1phry!c>&mKsh=zOaoFbtP7(7(rElWbWB)-f;BjFVXu7c~O~@L4T0Fmh1(PJumL#{++n?yZ&_7Bb>&Nh0f;9p_RAo-&AiO;ZCc}&1V2Col2 z`Wz>Xyq3&ocr50kcwe#4i*tQ8aeK5Em4}!6IQDMci39no$cyUUC2(u?UMhTEGA9GB z8s6;{i#$z_sXvJCAm@mAlA`GWX*rv3JII@aBb?w8vrpAou9e zOWj;&(dilDK(g1edA@zz2|voWgWvw%iWS6X0RIa2)rFB>BF`Y_qPVYiSxvjX^z^cl zPjlW#J{{j$vq?xxby4{%+*iDJwlKMwu943RdU{BmB;m)JugYPWRC} z`#i-| z5P4DNw|Ax*%XwHiN^rT1$NI(F^X&Mb#q z8}fl{Xx@zJ|n`Src|lI+1>*P~2Dfv|_$F{)jW+#Cdz`T< zFA5Iik7@f7T(l8_&(P`Amz6Cn20B*_%JRA&QXP3P=B8s0QRL~ z@5~%XcuX*FA7|#9(DR~vuE4FWZMMhR7;CHE?Q-u7uG+L^7Fx)%kI7?FCBB2`(RWsP zQRZxe1IauY+*iCGlzSZZJHx~4NppsWg5QpFHKBc7ZNpD3j{BQ&KO%@zG)i~@y863{ zJr48weiI(v&Wc;xP4!ZLX~yf5{lVOc*2+hJqO40^Mrw-$TjJL4&50FWOU}uphBhvE zOZc5RC)3tpquCy33URjeIRp3T*+>Jzu@mQzq%CWsq$C1s5hbi55hlaoL?0LZJA%V^pR=n(#`X?2KjqD zKj0zlo#C6noFOP~o;G4d0_8>J9;aONagGW86>`WIU3L-=8GO+`$^W4~4s&a#iuo1$ z=pRrHnLQ>R!fVMqneWN(%yS0mqkr4^H}ZMiQJy&TQu)4`dugBG$si|#yeNEL@X`CW zU)TR1?iB$M^BxkXNOBNL&UZL^m6{Xf3W#4pP-`~NJWxR7R4)lIF&u zs0hykqM~f#7J@7yq8T9~VrHf}PUcvSrIy(^A&Q!1B<{%adwtG1*LB|y`u+V6*XzEn z`<&0`{g(g1!0J+m(~@U!oc}6uKk#nHyuEFQoZ8<`hwjTLw5}dT|AQ*efP6dlgUI!5 zlQ{!8Mbo?<5pOv1?eLgzzI`e2kkz~$ygub!Qu8a7>%)HV6`Ef;C9XXLtd?7sa{a95VViBPD;ud4_p}6y2&guQ}Vyfwb*;Deed6qK=8q)^8JfrR>U%6<(j( z58@u&Z%L+iJ3Mi1$wxn3@>lR$HYQylA3Zol4Z4@Qad8vP8Nk14*Vq2Z+`0|=-Ohd- z-VgFUnAhv%uyKATXRlhc&+=FD(HD&!L3{@A+d0?wADXwb$K-wDA;XuNFZZCQ<-BmV z!ISa6cAj|1r-&RtOg&jwoBZq9}i%up^QC`_p;$Lz8YO3~m zsXh+p8IW%e_stsmxa1jn>i@xSbpGlE;i_TpjPKQL^2FgD%rfl>pKbUliQKc>qwQI z-5d12x^3D!GoJzH3jc%LAKXR%gUV}(9zEuw%z?yykU85bFIq0X3GO>TFWisYHG!Ic z#d~K5y~mj>o;aK<{14)L1-|GX$68&P=rot+S5H!ZFvId0dE(X%eX3`$%k`RRC!gQD zljfptk&m8pGVtT%xyBN=mieOarE*RN|AR)~xdrtH&2v#B&v41|Y3vQ{4Tl%t@guWJ zT4g;>JmkHJr^JuLo;cnQR_VSob0Aw0uMhu&YtkGN+ggt%yppn0`%?LP1^$)t@IL%~ z#rZ4tm|!mYDD~*goNfFM!eg>Zd*b-rzC`=zc|XYftAm!;;v6++yF_{uJLP}SF>zhm z6M09=12w0J??L__d|dW}%#*oMQKfrc?UUPPeL%j6hNgk!OT8r=NcNb3Tg$yv^t_Oh zDYdpFPh9i&RaE$$nERGjY2FUMbC&CS4LvF=4*SwQsP@jxfmA&&m1h8FyPM>%)P8VE z&J^MyvyVQTax#8SGi*4JUywIEW2l?<50(`4%!ph%ROX`Wo8Wmn{s)yW6}}1E@0H3k z7@Te8^MW5|WK5-ac)6E~95UYR>~{vg{fG2Q>JKWfB|LGJrd%I9aqXP9UJa!^4twIh zk@uDLXh?Oj!%yz-4)}=X?dVOQ=LIhSb3e@QcJ9$n*gok{hVPTrsb2LTXS>cG;ggFN*$P3z@frTMN#%zstQEKk`kykTH+G zS86W$BE7E`_gm^cD&+gf*JE#6nqp>z|4O+&_;JA5o+`dn#eoE0bcFN=aSwu93!V(# zSJ(Q#?$*lgKvmZxAtmi;?~J_YY3fZd51HTXYA%ZXVBQ1=n>j;9S_`=c4L$>Vc+H&c z(SE0f<@a*#u$k_`6ymox=Rop32oB_?#r2lWiwhzOg90QcvsS#}%-J47y$SI8a%vyx zkR>_fB=VTx-44E};aw`reo1;>>1o}hN8iTT?m71G-lhE@{s*}~xVO!4>JOeLKMr`v z*gH?B`4zlN_#d?WUa9`zoHM&0;A}ImPxU6?;k`%iE5%jgoXnKsNb%7t-voQ&p0MfT zv=F~D`h!k7&!D&;fih?CbDHJx(ZHhdU8y&*(sDreCcym&CH|G_O|Zv=dC2fhFrVQ; zUZ3I=Im>?V_ch@~yY`36yS<(GQY$S7V}Faf8@i4-Ma;hnD|tkpE8K(ZT|)k9H02r4 zqi4=Gax$1R6kVNu{9t)E;WNOOs`@y^bgmSqi1&k>7j4yHGxc%!T*2p6pH!{yL2%WK z9P-A++aopxy-$8;_LxlBetx|@^(KbUoB_RwO|`8%@j&*e~{LdXMrpqp$sP;-;QoVL#ZGIFMgeH8uTR--GB)cs;-=0=L$-?`;3` zojVhWhkQ}@aejy#8M1`#!SM|*XkY44@`k^VQA#`+&dIDDI*)u4DVBmbyXdEF3K z-dFHV@ZMQ@m(ZJV5FRpn0eIdXXXB%{eYZzSt`GSuK3CrpXB#=>O92CEeg)qIax%f< z^8)w7{6EO~cI0IC(_GYew;TB@c;fJGua*6v?YY97LFKOuj|n`y-=VT1NXqNU`-hZG!X!LQo=Vj&;DIRjF?47sT>~R8e zCZ+~2v9ku+^aqtU9KFZZ~NFo>xrw@ zo^r_iKgjcT?$NV>j zUE;YYbBe$hMNY=dtz{0Rnztk0j_(!jLG+!$XJD>c8xx;_z2Un}UQ2V&Yi{`cm^+jg z9c$vZ<6JTK1AK;OZRVnxlxI*JNLS*?AkV;_xZs=#no|URJNC{2PVal{7`SP?yUax~ zXW%|gfb>#J3!ckZwDj-R6Y=#c=4JX8?>hV<^#{>Q#k`%p0PM#BXZsG#MFaG^-F^A* zw8x1!Gm?59g52nle3V8-_Adwe6PTX4lUg6#D>};<& zkl+;IT&ZJ-VJL*+%`OGSV1_hWfw{?f+@ zl>Q+5oo9M%Cmu5V&MB5J7A~4K-*=s!i`w3U%4<2Yp}=)X4DgsZ60a|4Tf>1C;&0p7BH9p8fT45f!(svJW;`dHzM-nZP12@kIh7(l($yY#(M z+*-^wm<< z8!zW-IOY139|t)ZHD`c-Q1L~+v5Aj7Y7d@R` zq0bfX2Nk!L{|B+hd7e1i=;L7TTq>Tp-B$bfRx)pAPaJqM;6UDqX|TK^xxP4^L-rL8 zB=>Pj3SOo>1LmT*2hqm?2eM`Klf)Ng4&*h{_X_<%=0F-gdiFbmFNz*LcrxhYOgZ_= z-V(hZM9+)!SGM_fW^d?~=W6Kj8CX?#IU3-%f{--}&uj zku-0wTJO94y+f@SIa#J{#En+;QQjEA4vN_#Y2W~ z!d><_BW^{}eo%RMRUfD6Y~7u)oi_F9=Xo_?R(KuVgZN&l{UGL7$jPAR^$%T{J!->Yuo^HTdk<%wfI4)+Ja>tjzGczx-X&lfHhJ_GiHoM(8Bd|uziHwgdg zFLbV!^s}0Jsk6uvmqR?6hOyrA>YpT;Fp#p z;y~hi)lPi$*gNA~;oYwKIPg2Se~8cPQSwbF9&){YU#%qX66Or~#Os4^;-IB3aX*m1 zLNAqj6Wn*^{FTZfw|2IFu3^yT2^(hAQ{NeVob*XvLspsiqVPMzKlq65r7AuH-tEY@ z_1E+fgIN4pfaV%T=ae`?s3ZECgS2I069$5GgPEm+UEuAYf zuaEPh;2|^r3VmmAw(k)C3VUZi^2Dh=j(z-Y?W2#NxhQyj-TiycefNPJvf|0KPkuY0 zo3%XgZ2HgncPc9N-Z_Qd?cSDp;cPbya(vFodApvsqvwTl1x^wBycG9?`3&${g4c&R z!zSrF!xJ~(IwjtozE|U^=hZ;x3Vj@K)$l*KImq9uQ{TIti^Mm?2|f9|_~-{(|D$`Uy(&iw4|z<; zw~>E~y^f_FdNQ%df7PS5&e;O6Kj440rQ?S$KVaM%9L9`&9QI z>{j25TWjob;0;eEzjKF~mlnJ$erLtm#vTVbnYC&5v>)t~Qkor8nn;}OlbSCIPaN|Z z&d|Ba(_TyV@Y=7aT<^c#=TIhjOn5F@6p_EU!BVhzbHrxJ^%c_hYV!8#_4kv4b&r1f zzAHM1>`gpm+=HBxvGosv&!F<6%D)$_oSUg|y?VOWA zAIHp-*)Q+*=Jz1xqL^Rt-kJM@zBF%FJY@FKGylq8@>k$9fCG6nzQxMfnSsS6hkKiN zGMF>m)xA{Ykna!=InXp0<#+pB$}@ED-A;V;mn?fJC!^l&|B^YwW65tNxLeB;e@Z`> ze^dDF={-D$tk(SYJH&y6C$2T+uh8>aMY%rsymn?=w2vM=dhBtStM=3E(KNqOoT3!! z(VKf-s+Wp+JM;RO`w^mh^b^PvS5JHAca|m4e(>_?+*+3o*}cvT8>RcsP4m*iHjf=8 z`Kx>5^Aoy_N?5O7UbyS*pmp1fSO~nYaIv|D*IK_cRPCY$TJ{+ zHG_QgH;!#63(0veHJEaJeyLM*u20Pw?4vgezuoLHG5k32E`?|MYwm}6k5l)+{EBmZ zP2x+1cM10(`v<{i;J&lUA^)BH&QBhhR?>unXa|X^| zfvbjGAM>x2-`PKYo^|lDq}9s{=h$#-e-`f&zE_3P^9pX}F#%8JEA3rs8k9@?_NF*5 z;uNK7F97>-ek4yEc*rfYW|wq4^0jyY;G_RN>HzVtUJwo>czsbcX8>OmbB5MQ{_Cb~ zyMLgC_Dv{XDtz?n-R{%yQe`RmojHdLZ+Lk@kG<1Q*46~m-Z|XYI&1I3G|MLPJHxv) zOM6V=KfqO+EIlvo4`!I=?VJ}y&+B>mUMb&1#o->6;~TPEmr&0Od+M#kyeRWU*++kR*l2mTBPX+x{s+&xZ+Dnh9e30!@Aj6~)`=-;?liyJdbKs} zo$Z)g4$yx~)~IUa00dyn>m z1I0IS-_j@cbmYj8rPT9Mdbw~H^FmJ_~_B|0$1&Co>u~9)12Xz*dHTDge>=t?U$?1Rb*!4if?Fs)ia}{ zpr@RxCwhKJdmNP)#XVT8xoTNuAvxovkAs|y;>pbL_*my;GAy4iv`9`SP`I_szZy*U zU?iO@^arzC6KIbE-^5+&55kw4)n_94=#g&+2hu_3kW0x6u#0@D-H20k(d01!XB$0w z+RHsSj`}$8d4U6ox#*02mrrl1ZPj6O zukWchk+kTLl`EJ`VdPcz(rR0PZ`td*EEb zk8^?e?HK z^wQ_VN6+te=BgQU(T~P&8#vR$-^uPu?Xev4O{koVzvg~meg&^3-d8IvUr^5reH_D& z(<38_a((vVOYKqlUPGqqvVJk%?}dCz_u&1Qx#1Ux&oHUey*mwO9d*7PeVpHg&wxIT zx(DBhxkx$Wo1q(gF86=atwH9ZZ8c}x$X`{oNu`_&I7METdht!z4Q_b0o%5Ef@9Uly zIFK);lxHt0O(b6`&ef)|tHPS*y+*x>|B8PQ9$wX>AF^z|)ld7K*$aTZb577lnghvw zoO^oCz~3w685FM%duN_soi*k9jCXs2=Blar6?2NXH-UNkUGn27uO&Pt2ConNEBJAc zzrwqn=k53(gcpGQI8Xbx9qu7qHKUKyf_QyF!u^;?zSL^U$$$gNJ$g&O9M`c8&sXj_ zJfV0&X2S~jycGW`;N)GIGer7YW_?EQEBk2o>6^yy82G-2zmwyY8tOayk{4j=5E-5)#<`&-n#(A7RQ{rl5xx*oF8VVE!rpdAUr0V7gg`8ZxdQrTT_1!dmMO7>YsHOtlsSg zw>FjbIC!^n9|wEq=J()-KDWqgS#22>^YYxT{vC$9ndS`UyeRjb?@5nd`Mh3@Jw@Jd zoGbVTOAiMW&(3V|5Ka+%6F66#XJ9`L`v;jPqd42>c}>m9EBpOe>nra$MR*hu582Fb zhu0GCEAV7?6?kOCkk_(#t`ELc&R>Do$N4M8RSVh{VCp*?+z+?jj}GsmIgsFffUEX5 zx2C4ArRVi{@(?{|Kp%&Fsoq0UCp|Iqe+#@N*Oy14RJrJB-i&3 z-J{QMws-Cq*EBCZY}44$#OqUe2KaH9`@wvMtykMR*FW20aQ1|J-FL>koqP17u2s^x zVox0MqMYkf->VO0kHg&Br~O@qyUDv9ygv5PJH)ppUn=(p!9yP8SxtG-yX1*O&nupK z^x*Yz-x=R4Gp7g~$gH6eld^mw;32h!ja!DHgr>E<2#pB;z;iTxmZmyj0)w{~Yocj~38 zy|eN=zv*_r=~z{VBeP3dXFZxcEWuTJ6P)WSZ<9gus}970w6ola&ei7%z0^ZAXJDV# zE9A!kw^q#=R)sZ)#{^zW<>3Vfaz)|t)&E!)rF&ju4}B|13fPX%nWac zxohbg`@KF_<7nQ_`$70|7A@_bv8UiKdqYp&s0q^Er7sqy$=(_7t2rJMB`+Ez?{@5+ z@xH?MYNFE|5BBgj=OG7Cj~+Q0_;Jw3F?{skGsO2xAU*@;qV>|7K#%_ZoqF02f-lPZ zLAL_aHcsb>iXW{a}Xl2jN}fo)>er;U9#@gy-#;Ux8CJLe3SqAMet; zeO}SkHBT=uH@(}HcL{lhImBmBKKgj_#Pt$R5qshmEPXCxCvnyAZpU0S+&6w!#lp3g z?6{V+A5?tNKaS;7ZvyZ3X-nQq4a~`*Jp zc*EHX(2C}wn71qL2RM-IqkmoU?c@Bu8fxztkTbL)==GXW+T$5PG*Wug{+N3~PMuh5oL4^s1N2IT_r89?N%7FO}!*YR&); zFM1OyFA5H1rt1Xic}*s6?a}z5)+q9&zOXm+WNl3d^(Jr+syz;KAkoJ$^ZM9pi8%xN z2f-=gJq|p)2NuRqPKLbz@Wk;RXQ=f^e2bNHiO-<;qE_Nx^`)H5T;a(m&Ng^5?UEX; zd#rBDe_s4!()&QMI8Pe6IHI@7BWZ%PLplehf`;vwoU*O8hvg=cT;i zJa5mL;2{5lVRWv*{lL4O&lTpPB@RE(TvYi|(M$F5`+itKFXs;VdO!G<_IW8@ALgRq zAv0$?e{mu02OsU*@DLw8^BJ%oY@^SWt%nzL26&g?;q8-hlzLv^sxb!=eH?f#vu4ys z-;Wz*Ij=d}<>JSwr1uqj0h;ff@x9tFdz^0m?dX4yc``Ux$n}9!gt;i@SLjXf{0jFV zpQ|w;@!l5VGrUam_HgkJ&R_aM#wP_YoA~X>Uwv-M^%=ZA@UQSc2o9v-$5Hck_?_7c z5Hl-Ua(#E2{SP{sY^d`7JsODGfn_ypR6Y)iDd3~H`!21gGD|q5QkG-b9SDe3M zP7&T$=3Jlc`>KgJ+suLF{oup-SL%OI?Qz%}ep>hF@jsYglNVJSNb|gXqwY=AcP^+~ zFY~L$B!9^vb4~{DcJQy%yd60iezzO^_8lg^=u>$|^d85^$#8#A<*&f4Wgq?SM0e`Z zbAJ%`AoE4p=LN2s;%pmzXY6qlPbOfS!@;JrcPP&=$nyf_+c_uWBVGVw-tM^a>-Zri z{?+Z8VB)vW^^Kdge_=9t0pMNQK5&-w=yS6DW(da1$HI~-1lkG?_kWUzN;J_C9aD+`wq zUsU}Mjz6?c&l%La-RzrSuO;#f$X_AXw|sSy@Q}?pWccWD5282mv-?L5VbyWOXJC&B z-dD)=nK?x&FRHj7oWD|D0OUpgkRMbV=-@}h2(Z&!Y2@Q|5*WxfX;g@>&8qN?wV{UA8o+;>JF2j8mz;>j>qO?l#W zcI-}m9C*XIM_&_~>vNHO6JHTuw7s6UbAJ$CfIV%}dU%mH9J#)G#8ulwygtksz}a@o zOHFH&=xF_q+=Ki-XxsBzNnU_c#FOFPL`vb()&E!)ZS|%674|q8k{31lgX&yyZ(=QZ zOpw3Axf-%;`Re6`v-ee>-b8s(^}PZQnSEZ$H-S6@?m_UcS|+Ya>tgav;2zvCqrr3! zjvA6G{z34skn2M)Re9pR(tHN?@G@tcJ-qzALf@J9&de!7u21y`*<)hl`mEjwdd|T4 z_T%wy(z|_E!SnLI>eO?C z#{VF{uP_${S1r#qu3v(;kMKo#eg!YUVb2` z`F70PXOui~;3W_1<}z_u_t= zT)lK3M{(7_{Xo9`w$91ezT0sRDqde;&Sc_?1}vFGy@{L0?4|F_@Aka7RdTNIy)rmO z$RYnjyq5lQ59W%0koV5G2f?jHuJ1nWakxhh9&)$kHEZTbZvuN9a3C?i!n++FUi9ey zDtQKQYda*rlkk*vU*h-V1;F`6%%nrB&TXr^+xhXn0s4PX@sRPoDyaLAczqXj{)&AQS5I%O zb)oqcd|up3b@)HHAN)VaUVx9TwsLmRJ+FqiQIwN0a(&9XWZvU+rvBhSx6A!E_}n5d z0Q36Xh5G@&v+~ii7a)N6S9UaS&*}3w&69yI)mwPT-sD~KFP@v}AUtI50i^@6UFrT5p zvPu32AM1OUINK8sO(MQ1--Ao(-G1?OVXae#f?lVGjr99v_R>X%=-r+gwq>m6fHUqN zJ4~bRRdM#KDgRDzZgvlvbI9OqBhP@jD0=jjC$3ji=buRbJn<>(Fzv?yr^wcigFOy% z$k^lX{AzyDuWPzf{>oW=Uf{PYKEoW!Gr(&(DrAYbwO_XDc;U$yduPQ%#+;!u@sJla zn~QQE2R-^g$sx0U5P4CYt2rJ;13%I}ug@38iWlI){FUNc`s{>ttkdh||7 zPSkh)je4n=Um@40{5a^*!xP6ndiYX1ADLd#ChKwXn7|8wJOj9D@B)k;k}f%92g@z- zhBxZ}LGDe!W6~$3ID1Ly;-k}OkE7;S%tOYU;pDLVUae)$pn6_G@%NH`T|a)i@1YD+ zPNs0DvnK5SY1>NK50#$42kJSK-MuM(#STs7|Fs2;uQ zJF7VZyq27kVa_&tc$r&^9P&-^@S-=N{Da^#@b~Jb=E?LV&USaoGen))A-+^_)f|$- zXwJ}d_O9fws>!>gdJ|UfcIO+El=2yxaj`vmY zl8MCqV4oNAqPPdyH-Y&TJaJF-3~{+p6D;p5e6Qel#`g;RcFfz6zf!%4+cA;ko8Z24 z(^<85R{Sf@^|5z}`J(VmV19+3SM&eDA*Nm`^V`$NKM1axr+AlgC@<>bTzK_;>ZN9A zJ_F~Fr&VuvIP3nd^ao$3{1xwU@`d~H!M@bOc+G(X_hX`buR4g|89AAJ={vXY+t@ju zczsj0SFf*23Z`DF;a$R<0lietU;QnvZr+-(+_7T@d{2F6%&$JlepNWz$cz4~JcRs% z4w93Be{htgQutTtltYHs68tNJtETb{$o0VsP(%Jf^l{Kj?JC}-QaV>gFID*_Fu%fF z6yGbOmx}!$`Z(Ye8T|Ghbgq~ugYVUR--WXdE?i+L6h98n8Cp?)kmpyJi}L&m-z$}G zch&E9YQ}{MR+}i$2*u|I9nW ztp&eb^_}s(`mO%}x2C4AsygX8gS&WmuUA|=G5p$R(wi`Nec6(~!ac~n3B?zEC8Z=g zp7K{EZLRh)XF#5zt#bo;c=L%{`>y4Z=3l`R_rDW& zE6(QsH+?7dCV0;9_>r%x8mW)NJ}=Bg;RWEFOsUKn_VS1#mT0#|L~_RH(*CEvc>cKA?^py85FnHZsj-H8{TVg;K`fhqsN>fcGl-KXK*49 z??<{v&+~S0)k4S{zJa^|K~B>=N(L5=?@D{;WVr`741HYZ8Q?JiukWaSUl~4n%o+N{ z-hH4q!FdM9q*n1et?tstVg8lT$FZHa+u0o!A3f(qdEQ=AQEi%Ebs!(T$}`y8DggMEW?~AH=*pTytyDo8Vj@`p)1$I<7oo z^S;7d6z{9~zKdoZT)0wtsfyR9xN4KB=VkbvTU_xYJ_FAgnEQe6Rp!vgdxp9ESrbZq z9D}nxKXY<%xp)DTHyoZg^IVj9eSVZ@K#!i!Rk?6$ajxK9VxQL^QFlVu_|)`&-Ob7F zpEAD!w-$Sxlj-O4Z&jQ-;d8CLO=^!ZLy{+Tnt7hSSID;`*JmfbRLmJxr8$uopm(!- zFn?@@=C`wda3l35Z08IO(su?AS^W>9=haC6g9Sk$)W=c%L2$OgDZ;#+IYsEv=lA-4 z*jSlgVUM#aEPt%efbXR5tln4rKggWz%~v0BzW=P_;G7A0rgwX??46Mp7nF$rBfN z@=ncE;vq*#{%TX4V>1sgxF6idnYLs?YGBSrooD!H;1=OCz!PWQI|oRvZ}HNJcDiD8Toef=%1fEGUmMHbJ;sz=|7OTwd^0n_X<2@ z#jPFrf8-gsKZyC&m*wtxscFuMt*rk}cs1pdYzy&_cRHMOA2pzGY>J78j6DwfQmaqT z-j`IEw0f}YoiDDRvVF{<%yap5P1JXWCk}pR+=Hs;Wt^)9`X78}nPv5=!mxe6o!&^k zRNRBeAtTSA`0b}@?;KA&8E}f6tj^jC@bT4_G{4$3AzR+<=NCLWbM>SVL)P{vZR4YP zeV^(ceQ1g6k$+a%*$s5N)PJK-P3WztJ+YT;I7K6g13A0s&oy0_S5V(MQ07H7qu7QvguqYZ#Z~;7xQZ>F4Fuem3YX=w=0i{nz!TK&N&&(uZ;W^@(e29&V8H| z@=fskDxWxz;HovxU%?At-aF?~4mnWg8O9x&Z1P%~`#5UOz2{LYsxZ^!hW+lBrI%Z1-QyXd<1 z4+hfr%2D1|%#*>q{Wa;O8gmBlMOFSv_2_Ta1h}{o2NK>T_B$JM(H64DVV{?6zMZ|{ zuBKccb8F$XG(2%b^qk>Az9@Jy%vIw)PUG1YbPsY~ln#gkFJ ziM^(G`ySn!V4oLrYt_45d6#ex!tczvzQF38`hA6S^(DQpMiNg3ew<%Ve;|3$dF01I z{tA2N&E(^d|uzB?@M$gz9{!`nAdlu{|1_0?UQ`FoBt!kDS{`iM0>-rcSawF z^9%;Ryn1EA+{1ti=$RQ&yiu{$acOF9=$fe#RLrzEbiM?;R7ZV;{ z6)@QI!#-0w-6Vc{5P6r-cXnHTZB3-)ugW`i>)md+hj1Xjpqvai+kbPjv-^tfLGDek zFO@xUUgQNBNqL6q1tTntardJ)%qW~tKgeF@4A?uTrL|6UBtH&&m!?&J?C_)R(f76K zO|>uR0sLG2jTsp{lEZ$+|T?-k zr-*&2>(V;q9W4*i-tdA6+2jrPmL5HL$l%Fv9|ye&SL?e8k0iU0e~|ais+X$#&SrkQ znv3qTI+*r2$n|CS8Q^)DI7Rm?ed!+LJj1w#=PN4?PZqA4;$OjIa<9hUv>#MC8T=1| zvkm_s_BhPzQ@#oAd4W@eo)`9mX_kGOFB<4_r^f%}OS(6K_f=HpWa&HSif`i71AI~T zP2gO$mLB~dYa)tvNS>jaDc8s6is$WW&HxVN+vEj6UKBlg>~Y`?2d4=4Ah>Gq4>k>g z$E1k*gUGkD*V6D0mN@(fE=23L*W zSICP#L3^A5G{3q*a|Z6?boXyF+@p7CM^6)1ZMW4S-YF?;o!>SG;?_zjC4epm7g!o?&H=u|rl)dUEDD>Up6zVVgsS$HXzQHTehOcjg>2=lake1fOA< ziCe39$cd}pUN+Yn9AB4oN#|tHcMc_9AMU|C{k=j?27J*e+pE^wt(X!YZ5>QrOYr*4 zy;Ss_`^2@FzcQ?d_@eORSc!jy?-k|@@Gc!HpLTqk@UM7&)nw`q;(ev&4ESETS%)Na z(%cWsMX&VF^0^*b8}%9WofY>3y$R$Q9`2hk_BhO~RX(qcu4Bc+JD+$m24~ysqsLq{ zmG}(IlbP-@Ro?B`~(nmI+tGjtSA(HZUY;_nsa?dUrj zUI6>&S~`DB?{@Hz*=vdURf66-dk@$|^Q-=GUszTMuMcwu=3k}SOZ_YZ&bZ!N20U=uecB;d`)Q-m0(y@zH}P6Gu7ZuM^r? z?Gx9fJ(+h{&qdj5c~^2WPs~iAy|eNHaK7D5`@Ehya;VC|Zh+}rU9=33=}Gf;_ue}@ zdYSx#;K?{A1*{9%)_B04_zYQn27CSzFhBfOOuhUMs$3s?Ey3BwyWN;y!Mg+xFXmT9 z4jDWd_J-T;ohPIQ6KC80$`tw^+%A6SG@3Ka_gz2qiJqY@*L8nz!O}N%A4mNUS~S1? z9(e&S6SwxgPWNc<9H99O+{eK^h;xPc6?@{47p;xD5t`|9m3pa)FN$*oU#jv=aGrtl zSG;$=9ldGB#tHT`7yXFxqL_<<*T*?z=6>vuoJ_jRMVa4z%CyJ9yd8XoAmY}dmumaI z!uKkB@y8JxgTlOAB+tNn2ISk>!;877;V~IOIhlXj@EO4Uuu5JO{3|Eoejwkz{c3CH z#%CP{Z=A46czw#3x}wMDZQd1+$tm)s4ohfH+z-qd?Co$5!aw+X)WO(Fi)YLWlxN@` z{p>Rp`)3vXseSb9cji0;&#&g#Z*Btt1KJiT17uaUV!Pu z7p)}@By-i!9}Fb!hr>hW?Te&0;ZJi0@MJJ=A8b7qZ%_Oy@Q``$jK1@U$k)k7A5MIR z4Z{6Eo&mm8`}hiLm*v;j%zkLD?{mt@;CuCVw}z&pRgWF{prj4;Qk5Tv`J%hqq{{p1 z{DKiWCxbad8|NbF55niw&Uz%_wUlDwY%6{{_zWu7XZ9|szVonU;nqO%m?+EexT>IL~|f*eG|;B-9mX$o?of`U`uOD;kV-+RJlHQc){yKzMcEd z;34CEg&cB)4W9uyWaO{FzXG43$}&Ru?aV{wTpx3anEL^r7rs~UTK4z+mHIfhE&XGU zi;q6f)kpW8F=v1mz$2rgpa*dv{asvpt{)oVYn}D!!VL0;yH4LS{*!_4djvW)UA}oN zOM7^g&x`q2syE>nT|oQ6g|niC&u~lTqR-JisQ6dRDdKti7~x;RM}LvHAHFnaus=9; zTi80sBq!l)hil)2NAK3dpYea*lxN_35OW63GdxB;`rV0V($D4JuBfCQJ-8p~lOCOU zalvrnK<3QYq~Gn+j{i&UK{KBrlsvp%0}80`+@JCcKhGZN_tUVAz1r#=@)Gg_ux|qM zEA&$NeU&HkcJxx2CsW^fi|)~be>IPC$SH+0Dc9Gs!zOvRCokG>`KyW7$KNaVT7H}0 zY;|nbOLe60)otRcshmuj@Y`>SH@y1;97y~Ra{j8y;z{?Qi#}K2w^f%|c8wx{3OVH=23gdY7$@|Z+f{j?v4c`_;|gTAx*d$onQwG*~q zq25HK^)1Sa&fHfmp14!Q7flxa)vB;Uofkzf6}<`MMd1bLmUnC73CW*HU@I!N21FLAA%h`wIVqw}mgN{LY4V37$CgQn^2P$?{ojP1J4T ze!SskXU9D++=J{(J(YeTzoz2SiQ(5il|Bym44#%o>JQ?3)kMBj^isjCbs$gNYbhU_ z@}lNE!}3Kx3!eeK3EmHa&wzP*1My_wT|y4|vA%&1>3P-3yM2Q2kR!+&&i+B}?|Y1j|1+9;RWD4gYmvPROM**hRmvB>2FbjgxoXH?MTi&RRHQfYkh5LK(z~7W?R*cykMrW*K=FCu-H!a# zO3L*ie}z7dpU$^W(ww5HIU8lp@PV8wJK|p*T)1G?Lf_RxU3Je3dTQ$1;ygo{LqPSCql?9tYUauCoB{bO@EMR7 z#rF#DE8K%4AMnw)*8M@e+u>cRrJRiF;~0MDIh1EmK6>U9@mv&p=ed-V=}Yq~tG8A9 zgC&Q9if3gytvnL{mUTYm+tKp^_v6W)>tv6!!m=fl zPX=xlA3gem=uLosrFeac`@tR)?s@UNJGEJuWRU7fqo4o+=@Gd#(U%kuW zJ9i(+iyCtV_D$qck3Ln-)l~6@k3F>ETyCBHlkNIW(>&xH{k~#u?Kt8yuz!$webe_{ zJ)K?KzC*TfKT_mCt1(Fpus*aEctG-N-iq9y0oa+;;}2NcASJ9djnXa}muMT19V+TP5>$e^uB6o>Uov#pDX!x^d?>sUf&<& ziBmZl_Ly`eKEod3K-SV+6uk)_6A$@bbPoBQ9cX?Pbo?tiS7siv;?|BFuzBpNhvctv zv>%6k6SxN_Zl7>y?YV+FyC>Tbx0d+~#$1&5IPeb|ygvL7zOTPmr-qI3`%&&e^t_73 zdJg!}{S)CK!{?=Z^w>M&f6&?4?m6@(RDV$Y52k6qv-%$d|EhJ?%#w~r{$15Tc~Q(6 zt_e@(MV;$2dR|Z4^d^|!j=U&%GRU`MkHfxH><3?>IYUHd6Xj&!F)0%sGPob$KyuFu zedjLZiBsMsK34%t?5#mdCZ|pnZ}?>5etb0kDe;(KkApk|`p($nyuA05f)^y;-bfyk zJ%^tszcV~>oEJ6v&fv)yIT`G6Dy&_JTg$n=oz(L}u210hYt%zI}eCj&o@T{Pc=c(=E@^82w(WxhFnG;hb=S@9XB(EF;8 z?!o-HRhCZ|#?PATyJ2W&I#*Lq_TKx6c*D_moP)=8`nY@1 zxst!aelSe(qN+d0o;dIs%=xPWl5b}}4!&3K?@OcpAbhFzE6%THPVaW* zwNySYa6dB7`5cU+UB)Ev!?Q z_@)NrWK+H!UdvAgZW-TYdW+~h@|a+cvtE2&ekWhrTUPK)MvUY|=gHogb29Ly!W*vm z?Qc@g%e;4nAIFXM&ZXoH-`UZ_ZMlDoB@4jPudS+ z?`-?M0{@D+AAGJj*yi?buNWoYA2m5%&)+2 z|EfHMe5vR=Gp`Tc@Lv{mnwc`mXGp4v19>uiZ(>*Ce!%biKk0 zgClOu6+VNz{14i4)zG7#6Mm0)$e1%Iz9{yC;MU@QaENfWU)UQ&`F8d$#m)L`VX9?w zT+{rgrx%R>WZ(xLzD_Oa-LCx3cwhO`T(pJud3B>)U#jJ^g>mG^L4OeQ_MUr7wQoZ8 zosn+`2XcwGSI7^MFUK}o?wayf*bm~}9xVJTgUSKp9^lk_L3c0@KeViHllF2s_VGW3Hl)UKLb6fNt zXDjUo`F#~4z0}Ne;|}>!{%T760_&h<%T}ipzAyQ9=3kv5Un+Zen{#WCZ)ZLO{||!C zU{AchJn9b)CocfFAL!A0U;E|6?TT~xKc(-YdAq?WV!t!L+aK;rMV&2^Ow!4Z1Md=lubikykKV+zC3ZAt z0G~nSuZ+1Uczy8jt{wVR&mfl@+7rh)8P4^U9_~f+c5rLQh=&*RcIIre7l8MJElqp| z==@c%^iuPQ-;Q&Y zqxbGz0}B}Qu2oH==f}J=i$$2ZY|~v=sUxYb6EGhFuy{+{X+hA?F}D2Bze*k zGb>FVUi=TXb>5=AOWd2_zBA87eRe$i>P_#fG(B(UTp#jRU99gUwAc5bJ@p5%cgFjQ=k4%KaITN{ z&VR1yLi6@El#{8~erKF3{$Bkx_CLhyL#~f`eaJI#FLlvUkBoBRs&Q{(zHiK|&&k7! zbG1dhmi9Jz26JANxgQ^oe`30GbPnZY(DULs!)@Y=!fT1|72a3oyr`LfbuT6&{6fIr z$?uGNuu10a_WFMiJY>&idz^jLA6)BmjrKU$JA+%>k@n8;@K(|L3jB8VO&~AoMSM|% zv(0(YF2ZLxPCl>2r3puat3Pr0k^0We7ge01Q^c)RKKewOGfbg*yRY<88-*{*?yB+?)oelxj2}doZpJcz5awMUx)lvAOyvJeg2e`Ej zl4r<}xu|!uzVqYB&RH|5KZyAi=Nb51;d}Lrf9v5M;-lw1PB-E}GEWA3=R4%bQG5pE zkeRd1=Zbw^iZ9B&3AM*zP7%BS8J1j`i|!a0<`F>WYNO^rg8RXK=ayF{InDLhGVo){ zU**T8SPn>@!MF#@$Tu-W_q>q5!rqxZamovToDBRp6Xbmrw9TKmwfJ6LTN6>Vlg<@; zm)MU}O+Bw#$&13f^u&=5N}RKvNPaWn8EaYMPwD^5zbSs_)E=XTWK3#5^U{Lh#DUy6 zqhLbAAp7Uq(D&+Kc{k00d^u%T_QKMo;%<5`-$I~^H=2s&+D8F--C(+$vv+b7=;PV2%9sNPYznbcDrzViNA4ZQJ-X(Ye)SSUf_i^TgUnZ^^a(#_w9kmx=r?va? z-}Qcwy#UIW`kC~+FmG?rzEtG;jCXtG32))nW{7tQJY@6t3Ukped8=tZ2yZy%SMV-9 z$gO1_^5$MGI~3GjqW?kuUirq`t+=Rt^!3`igkGwd-;Vv@MBST+u)e)4fp~q`54M%Q z^N*%?JMyB+3t*wVDD#k4Kj2-0-}!#rXv_a}UKCyc#TP|?@U4W$lUrrYE_qbu?aB)P zuO+y(Zq&z7UVsSld7($&f;dHX2V4AHduK|g4Sfc9UJ00`IosxZ`!3TxxRCbFJ@*F5 z-kIkN+#h7GW%0ln9#j7hPSK5HO_#xE*fFqB=2z*KeZ;>4PsX3-4D5-6cZoTWQ^-FE z?^2@X6v2gFUcpz|F2))~JuE4+Axj#bt2fOS9@nK$}?2yyr|*F!9B>|E9?gq z2NLN!wWtGb3fqW#oig;E5A-3K9u{B>pHsOWjR;&E5C_<)4FKsGvtW_pCLf+ zoxjlhc6fNsY3Ec;R=Rm>Qh(VcDHy4VQzQ-uEabPLV@&VO)~s%Y_SOE%04S zd{KCpdQmS`an<-9e35z+%{kl1i+`^rQ(0EwanZ9Tr;bvV*l(jUZ(wE^S-k> zSK!vFd(fQg^BR&qsng8=Ef{U7i@Q&IXEkR4U$k3ZYFg_=C)4~2_n^_oc}>sTnX}FB zcIL_O{~&mMlPG`HKs_(yMd712=lU>j$GHNhh**YQu6W)K&UQ-J7U|=J zR$Fvl)Zih5tA-qM!=O##F#%5o@2fGAZ?8UaopOErKgj*TCx}zTygu~2xIf6eK9z6Z zEj@bh8SpXq1f%RSUbP=|B0tW{l9LId|3Oa^{|fK+QGREJWlN7<&D${-1y9CTdJ|3J$5H&N ztq}!50bY;wy)Qj4U*fk160c8jAQe|l^-{svX20_(`5%mxd3!5stHiW4_q+q;(`i45 z-UN6uoNqtB;BkEqa{lUk{tuFG?cQA_BcCDb5U^BxaY+=8T9D6mwNr! zMw+*S-wqFN5czS`yS+%}SNuPyxF3A3xJM7~5_}Vri}xJvQ8`Zj2Yo`0>%B8_GFg2l z38x4=WcDucyj|sF&`V{HiNXCaIFPspJ0!oG(Al~_@kDy{1Kf|w1*0tW+KLT?fGK7ch?^P)8tGvZqA_{_n zydLda-+43fMSaB+7i}H1Z29Vyg&*wu_4LNtM>=ejyl68055h*p8{FLQ%-j!n;%-%(%Rif5me}1oB;ir%rSiFg-x=?#eWpAE--8incJ7ZX zx=P$yp0}en;VAQV<#)a(->Z1?rFx6UWKO0d;>R+m0~G~vybjZ9$w_H z4F8}%?VZ_cY2@2E&w$818~s7fGbpbmd|v3Kwy_>f z=tDeYi})r+4cII^WbpdVnC7C@r)Sb02Ywv(J5Sj@PQ2ldh%XgB`W*6@1d?}YYu)N* z{PuX7i~dIcgSOr!gH!a0Ln!5BKFNM1<(mX2YwN_cw61xFHD8qdIN*zVQLZn8^6h)X zm-4 z2KEnfzFqOJ(8uW~d4|ivfxLUi?q|D$e6Ea~OsVt--AuiS{jq;W)shzgy$R&6-~~Xw zy$f;G@NS3K(tHoTYjL#Piq4&pKcR7u!*i`D&j8Lg_Xoi#V(texkha`fyxV=KKgeFo z_bD&R{Xxvz(I14*3w%-bhO74#di3~S8N9yxNrAM-Nu(aV>ZP6;=H>UZY3~fqHuvaN zFZC$puh4gf-+Aj*XJ?1!>IZF}kTv7Jo{O%c9zAl%7s*G@KCidrTs?W@pH+@_e|5X| zKyTs^x(7$zntf*X{<-u&xKlj5!P^?>d*$2d!#;yO&j&<>-;Qap^ojjG(nt5`!GYv? z`?oqTs`#Q$^vpDQmmH$IJ}_@*?gw}>iu-|cWzHe1J&y7Mn7>z!lD|?tdiGixJ}>59 z;ap+f&K?uyszr#0ceg%QBlJD^VKeSW2l9sVoB^EeQQAKUZ#es%vu13jxu~;vcwK4U z?o2&;&LOjJ!bk4G*NOY_BcrQYq#Lk9N)_aJ%`xCa+E z>(QINOA(nc51AO$H7e!9S@bEJCqnGAD z-U`k1`IYkRhpM_r{tDa==BoWsap}Z}YoF4*eHHZw&Akbum#X$S-|D?{koGRI-}#_; zOqk!kskZv`Eb4hF|KJ6BUtvF3PxoMM&_|k6r1+wn$&WK>d-eJT>N_KU1s^>;aqT)} zQ;+_?pa7m^a>NdUGb2?zp4$r z5%p>8CCfX+Lq;FR?8kX}`A%!gc*mq^nlGyOSIA%8vGk2SDY?FUokI>M-^5$QZ{I=r zt9vzmE?s-B9vZFl?W*U6ce{~i2o>)VIFRspO`&t8JiLa-$Udr{v_7)f{uA9{qOVY~$U&pYo#c0)YDg4kUW1=y~CP zFkHS@-VOQmz48*ywvm&;yWRe0d-2g5UV!rbv(I?n+9BN9o^w58DlK2cD!;SgiL>p| z^L~)`&fH5?d*@ur^>Gdv|AX)XFs~0D6Y#Hi@4R(vvhYQd7X5ejNWU}0tu?&i!(`5I zL2`YY>KbK_QzUuOqKLepX~M0={L0|B8+#o1agY}s>344S@X*izB)*A2E}h@ZY_HB@H>wllHB97HY2WGIB}!mm;4{;d&T*7a3Bv=Iob7f z`?LQVpWBo}X6{FK|F+s|iFdp5#PQzQp6jC;^I$wB5<@P=c4Rchl){aokzz;9PQdhn3D==W8n$Yt4!OXIbFaMRdy;cO>~cZof`cweO#COo7+i2dL+ zuN;}TWADt~@cPbMsXsV=`>*Tkk^;#;xNLPY^}KS$mpaPt-0Y>o{QzHd6zy@CC!>0) z2A=`0zzdw1)ggeQ_)WqnZ6`N-iad%M3i<6m*l3-c?? z+c9T=j~?%K_J-qJF=zW9o|OTS;diBv!}%-pab70>pyJ6W4rH*2vyF3=LYyLaE!n&D zpvQ!J^n9-Hy+Zzq`3#FBCxcvHD0u;%G3{~Sn|N8?S4+LU=zRqar1CCJ?sQYUmdw9m zpVzFSJv47eFSYG(58*TLoB?~BH{2ZUzO3qUBt-iMnJ;>yqVmM3YkS%x_ZUlioc~de z9$Yo{58``;UMe_{&cbiUxk{k-m3fcDc?R~;Gp7i9oMo#AFN?NLiEk3lHaJE1IzMhR zze27rSbSdG$AQ-p`F8HnqmKj5Hs%bY{mxL&tNC1X$hpmYu>@~4~`dHbusnm z8z_JE#LU!5qo_CWuslN;<&eRX;rA8zS8DJ4=^9n_=0L0*7N>d_l~QDfe2{$2$xnVcGsv$3r9SkvV|nlltp z&ns8&o!1ZT+%v?bmd;hVKXWQxLdg5%iN^*+dVSRS_l{NE< zcI=O&{FR!Eju1Y>?a&W>e${z~86_@Rk0ie%JQ>9ot!R_h!+S{Tq$i}0W4y0goA%D; z{vf_r@Oj~Vl}5R~{lr5?Z-Tu`>`Ps~deE{3fm3!^k_`!&g@qx&j2rgEBR8vL#`)&d#;IF3m!81 zgQKsVJ8`$-zxijRkHh(^$BrDWs&9JJ?Mk!$U^mJ$ly@9q>f>+@8Jr?`Ee$Wg-0=FC zTb4Iszlj`2-lZJZ(UiXmES^n0ueYs{`dpcP^m`U&Sn`Ooy=DBz17}jc{o1j7@>+tc z#{I#(vTMg$QBLNgf!oGEHQkB&I6P<#Dn73K^$SNy(G@AiD{ zn^65h@cND=ye#)%NOiHp8TSzbwv0^+Yns>3r;K_)ji7E_+0zniCYzw z`KQu9OMFWBqSH$rf8bnIS>BC#Nj!18JHC4>int$tXg&k@?daoRkHg+2<)c3l>0{!H z!Z#6>*|Y*&wfWjde_gmA=%pfml~41lt=ePq$FcmfDLGSw*T+6DBPX+YeAnqMDA)G| z@vqRMXZ}?$nYY8|rQYq#tzAO-cD%3dYOWgggEzG=)hVfEe5uY~895m@nloIG_Z8+0 z=y@HkdQx&S_+ITv{DJn)=KsORbWR4n3BwCu>~T&cyhw9V=0Kj795VZzlWlsbJZCWS z?U&Y1CeC(2GatQit{TNh@2fo~@GhknhUvcZ59EnsemnMqJQv0M3OO0YLq;D5+z-QJ z!oCURwM?Hha>y#(cLuMId-Uy+AI%Cb>3ZbLDtqdsg0s!@_K`8q&+YEtt{JDO+w$Gk zCTm;TJM+1k+$poqn>vU5dhAKb^=)(=-|&~pJ;JSp7Xa@o!^4ZYXt=LM^F_N7uWydW zBqygUwemk0M15yISJ(CX3b{Ut1fr=&V4lbad58oS<)8$sJS2c;xScpdqiO;~j)YbI8 z3O@ev)fUc9&(#mgpO8P}MzoV<82P+bPZ~*_?NP*Uzd&;a^qs-$<2?>~6Tgwq>$j)_ zvF9x#q(_haAkRg?zv3KnE7{{<&hTERjl%svo$?z6tix!;gda zRoL+_X^+!FbGG3z39K%4I7vQw@EO4UP(6CXN1s9*$VltdcsuIRqd&-VhET7ZppPT6 zULh~aeP@G*ta5z?6E@DcCq4Q{ zXJ%*~@`V%EC4c2EKCkIIhs<68?mL@(sd!(pHypV>hh~1~tUiC&@AlV(`(dS=jLIP^ zpBI0x(4!BcoD6zi=nrD=?526h@Wkb4o(z0mLBwz8`4xJpPfc$@-X-K@=J~D}+O_9Y z^6f`L_IEuKn;W{VdloYZqa9iU6r=M@!ncOM6&l{ds17?bE;=9N(Ar|ih@uilLcM04N za3G(_*j@0KyhJO8Wvsl3(1 zL;g0QSIRD#Uva)2d4?wGd8xeUBh+{PVBbpW(a$G7L*@FZ+sD%0`9Af$HtU=W_Xj&t z{z}b78@fjX4SQ$sWH^7N zIFL7Mg7w}R{C4Ka6jA<)`_6g#e-Pg*)yKiP;(0s2ub8u)IkZ#HP?y@8;FG=fmJs*j zE$ySfx%)W_HL((R7n)!>)U#Yq1R+?X-Hv!Hz`*EzLi;uo9`F8a`h@Ka7Yn2yZ z2K8~?T{cJWao``^q&Y<&Nlpe{OZX;?_tj44_F3P?M%-eVCdk}kP!z z$7Clb@=XNfOim4;oD6#OxwLoYK90)ufvX1Y2cN6!HPcS^*t@IQ9;b}{2f^!8o;dLO zkiQy0{lPoLZwIfhmF#g;Zvq@hcgppF&j7w?jpStBls?YPl2%!rk_RVvSa%7x7Tk|= znKSUYYKXf_9ux2xRBr;l3G8v;i7U=dD2+WDDmi546e&K#ILb4$>adya!Ik6%0B2j} z8Ne4s4jJ?Ik0UlLZn9)8-X4(~6yo)0-#Xb3D(*)(dE&U23a(m~@Y`1|`a*j2@Ohnb zFEQn>jCVWyIJRB@crBSP>NR8)`JK`8LjDRo?V()Wt@&Um-4_qiEb8?`6)vgKXr zJG=MZ-Er8h=rhIp=NDaF^Yrp^$svQ^4!-E5PT9KWg&gvU$k8E*#Qgvd8FPm4Ovja9 z6R!__XVu4fO5W}Ji(VF=7x$eFuG(<&nBZKw z+w@X7FPh!wZ=Syfd=TCkbC>#qqe7N=$M?%3pH~_2MbVoWXpNEHM8L_r)SJNnAm`h` zlfk=v+rV&-_ncZ^`Tba4SwPMN$}>zO{#CY~Gw@urWpr*_s^x(6CeWj2Pn?k#<@pu9 zSL%J`P5wd5ub9ukK6-oNA>&*re*2uFG8^yG3v)-tRMPtjeVpI>|JAL9-Jz-{k4!hc zufW;9BfeBM7lnTi{XykRZRNb3@(e0}#a>Hmsg-!h@LFz`9{qmmc`>J`E%m%+iPth( z=Iwsl$B`$FIos-gFk854Du>Ko0Q8+Bt#2>0HtW%2kHa}+m2c-fL&DLR((>${Dc>bH zlGhTvJ~h7rrwDV=`*GgH+5Ufgor{0Z_51&KWX7aPw9Lv!AGMT}qp@BY9+;3NiA7s%nrZX-Bn1-{1f6 zx;AQl57e0D;E!BVU3E@EY^xWhX<(Cw3a7nH? z-{cg3zx)UN9PKI3Fson&aUg@~J-E5zn(!IW^8){hy-WNbgwLz~TBtU^;<+gICYUFq zc*uhjZ&QD;Ht0X&@&`>PZ#XA5y0?#5nVPd&<_w*>6=^=N56weO zt(0#cKkQra(JO8(?*|{$-h(<18D0SR2iMA6lzU#_K*E>G+z)VykY{+-{o2snNsVFG zG_Kl1hPf!u8MyC^dpmo>F>hBsuV&$FtG%-|{|fIxYaViY?-KSnZ}ttK9(_SY0C_EK zt$Ot8-mZJY!IJ@Jn|rBGQl4SqikB>33jYdy9Ol+?Ui4WxuYL*okJTOr=N0D}^!pV& zy!Xhwy~g~Z@ML&DsC*O57v=dC`z9)#*Y+PTo;Yx}o8t4ON6-5~owE%da=Ca+%I&@} zd{@@JRK=5lFEy5Y6U-N7?uVK)V2{IdQRXw0Hszf6m7W(oCYXz+8@%D@5B?arihT4c zFN$6&^6l^s4op9t>|p7Xl9A~_c~O<-DSkSjj_zXr~-vb(FThEK? zKCj6w1;nl8JcH^xBZsW|I6S{nzKN)^iX$=A;|#gJ*Ai;XM-%^$z4L#_6USbF`_f*L z^Xfv;4azgXKM2k?a>(jG2%lGTKNqKMSLe`NG&9pV#nw`l{Al|A{AK0lli{^*+nw`x zb=cce)`Z)Ue~{;*%tPK>|NGh5ltX@XWt3%>^t|Akz`gw*>N{_v?+Sh#6RmY3dI$5BVLt*|lck^?_UKXz7rWL!6?J(>tzq zc53eDFfyO=qMVcIA>MHKQdN&0{lU@1DPlf@-t%Hk5pu}z4}z-(KEv)Kab>^fdaOEN zX|r?^Z#eG<`MlyBvih!=`!V3c-s6E*Ib`tLalZnuFV`#HZ)wDbOEPHA(3|oM^CT~- z&qd**2Ukt?op+Kq9A1Fc=8xpwjy}!};$Nw`=t<(L`D@-K=6=BMeAC>PFfaOQ(DTG^ zS3G3CUoi&~Ipi4e0)Q|281)Bve#QGi8_jE}^6gbwHI#1$PbO2iAMm9jf3=zNqL{ZM zFZvmA)pUNliSi6Q7u`N(O}M>PFO_@roM+%3eW13-$P>8FyNEq26{IbIn? zTs8DknOmzoyzF-dR}DR{pD5SYhkO&xd2jB%PkO1pY5hU=<9N__1wI3FKmL#&J?CU} z4=;0yl!upj$hv%?G1(IjdB^YteW%mC9Uk84p<<9G4vjsG%Az!IOt8V*AhG#-s2pz@_Fsa zw`e(J@Q~pJc-Hi>c@=r$&`Wip|Dbg*wRXMl&NsC8AovV?Uh(~k{m!aCs5nJl2JT0f zZd(R?qj`AQN6-C1<;Ov;5A*hpDQo3^Rd%&A<*$(IyBTjwygpZY4}#Y>k@5^GhkQ4m z0esOvXg|p3758ztM-Oj!Z^M7ko_t=@B+r0*d$ake#BXEAYusAZ$ANe0MN5M4uT&11 z{WzF2;6KRzLG~^+oZ3(kRxnj~edyz0&fsvx-|@}9??}GA(3C;_!S)^#&+HiKJ0mB< zedjI2*@lOg`-AhNe+znn=2y2gPn>gVn`OV|iHp-byqJqB&bHz+C~r9C?Jp%iY^g{& zmvye_)`1_-Os4&ye!qejpkQ9nw6+np((}^&&R-=zY51G= z#FH7)&b!2U2Ia?LKEv$O+pczXy4|ni$W7CVrSA;yQWnjxDxD|PT{?5^K&^Pg@gG$4 z_SXMn@2q@Y%Dcq=LEPKhj@q0S5 z=|enZ%o*TaN-bTTGlKG>HU^JLGrb2D2NHZyc*D{2V(!QEowec(A4T_e-s3P28Tl*Z zWWZ;D7XaLk-864!ULW(2xj#7f^fo!K3Z+Mn95VA{(y5oK{La|pzzYD*wmxTIj|uuX zn2YucbQ<$7^6c>j%9wu15Sg0~5cC9WV2%H(L5%*nfQHu9%H> zG#6b+`754_;y?HnaUgjv%0921!r5k?j9*6Ja(hc4akf<-hyR0alIw%VWU*him%FFG zOXIcRDo^6|VLymoD)VHR`(f7RqRr+;;`NP?Jr3qq-1D;L_2IjM*OK!LyvJ#v?~3zR zJZIouD*A)W>l;sfoQb2-eS6M7zjz|~yf)7()b8!fzgkOqQRJ_{L*{%t`0Yhgvcp>z zJ}10Bl|x2O2Imz#ahSI^%YP8uTAWuQ3$qFXHdQgTk1RK&$}hwrTgZm`A!+NhP(i^-+HJs4OpGb4b$tC<-AT5lqv?{@K}B7cQFP7L{- z(Z@0JWZ(^tq5IXN#J^fTB$@II@X_C*?~31p%x?$(O6M~~(%#vpVYh4FfiA*T1GlzL z^O&@?>3(O-udsJ!91MQg0QmL}@se3fi( zaZbs|bT9mb?(LYjGpDGL_Be_sgYOD^XXF{+OI1F4gY1g!1i=(R&b{IOTUP zKmH{7CeX+63H>JaDXadV+T*A`PKBlKs;jwily4tP9LO$K9+QT!b(89b4t2K^Zf)nh zd&Rp1KEq!JF4J5zy}!?>wTADCc{1#yS3DW;+rLVFT=EQh&uevfheZQT$IYvjelL86 zPTdMM-^3`&Gl17OeW&lS?DN~2T6=aGJjc5rc)R3eFc$^)gY)fEHSWhE@h;7y{FR!E z@_B_`DmX>TkApo9`h(zpbfCR6di1!rxAe0gnLn*$-VK^FaLH*?u+QNy#CDC!@G(?3>^|4)Yn%^8#NKTs3eYl{XyRTIP!`(0pF~ zhy%$U6ZS4G6JDP>uh=(%|Dbvgvd1K!d|v*J^J$O69LRLZUm=Gaf`Xd9p3P;9scXOrZ!s+8hjJ%F>#@J`*_L8{2jK5I7K+G;E98e z-YfY*%i)wWS-%$5QQ!HXb5{TH)bqNu*xP)^)Eu8r`S#X+YR;g%;VLJCKF)5t?<9YP zoQ!pE0)6NC;)!E^`=#}M}SoS~FBMYoCj!M)U0L;mVb%8O!t_08ysR`>Q& znz!SArTT-MzgnC5K;enXS*PE<>fq$?Nb`u|X$ABjRQz`C(KAoxC)(pYWO+&B)}D~_ z3VmmI;w}qk+nMqV$cyfy{~+d~iZ5#WkqvQck!N5pfRXzF-^Ak=DrJv@oQ&dZhgDs_ zHru7UXTDby<&g0n#GCVN ziHP>w;MGff6Yc%ZJZG3fbI}wZA9)Yz`$0e9Kn82xaK){KAE(0g;epP?+2;4)b;|YW z_xAAJ_vE>xjZW@ksZ9Ak>wHlI%|+2mWp6moMd8QUJk4?B?SA(<6d|BG%=dQi`gTkiBixT<>7`=c?wpctHE##M9eo`52aW$h%tddAkDfV@ zoynJqdHZnpUxwySst>!S?FT&qyNz)h_@3+NhPdhqxnJpAwN{P$F=E^$&5!eS>_pAO zi@r0swcxj7KloVo?vf`D1y(i6y?voyV#NDP)|v}U9pih_dl30{YyTkl4E!EMUKC!- z(y8uq?Z~@yh;n_30~svyE7iwA-x)k){)oCGKBJ1;J35alKVL915^IF<5O|CB>681Vh*vHm}4`$6kI4t(?j)4!na>aENN?&=T13xNF~czu&b zrS*T0_*Xcuz`ueQpeuPTA0R&td*W`&TokLfic zzEqWOH&ZVa{z3NeenFgVJ%1JG(%UoJYl+|Dh>w=6p`47-8;<)Gd&BV`WWO`!?f#DQ zgj0m`D$~#(RDB%ge&D;pdl3CWW1fL~^eqYV+wuAWi6?_O!$Wd!=RS^+fAyjE9z?G1 zM9uy39~_h1%W^p7>#R$}zta1I-REC2_~=_k*gw+2Y3J3D)1Os7K->@X2M4B?=bPGj zm*AUF_p7s`3&^{4m3s7+oLA|7bw_*?>!>%;jrdn(G8ZkQ@2Wf9ud-$D%>1iSD_7;D zmPSh7xoAL@%-cVdxhU>e-+An^n@xM?ceV2>SIaYWc53U_Hlj$&i>mKRpI`k{^nF%E z%D*flB!AW8#Ah|O#J@tW?|R&!#9H$hnlp3}Z#aAt%tL1W)vR@)+ItXuQRTJ7eh~R9 z_)^hJ#d{EaXT=vSFL~@xSXI678I;!&a|U=VcM6~3I^`Msq?fAxgLVe~mHMs}p8@?r z-s7lTU)ySMYxmQfVUzR+`MfgvoiS&y`6zJ5>~;339h1t5CxbZyb8C?o?PJ9$+A!&= z_`EO|#d}csoiV?{dyvm7aEidcLeC3%hF01SqK^Zv8u#e`OCA&KaYBhx#JoP$OFb9q z?f>2A0-B3nIXhRlwXxLmLVxge@_Dss+>gR#t>zNi56%*<+B$>BMEB$TYVgD%&v1-# z$mn_Lb5Zt9e4gCdVwaL5{XzH#6Rr@S$sxv|k#^QG!JnPjWIbJRvV+7B*T@uAh6;JFEX7=VY*Vo+G{qBM0(XQ`qr~PJb$VQFXt!>52cH4CKIW=n-fsL4b{8K#_*W_?bC9@dxL?iQ-SfnWnwGZVlxJuR`&0N=Pg>=# zjQ4irWM*l;)Pg}nspqBUR}+Z?ss9I69|v;=_N8(k2YFF5?FWmgKM3y<{Da(^z?O-7%)`w&8En{HjR1x39O_4>DH`^LFJiVZNx@=XLV%-XL!{_Re|jczw8Ec~E~4 zzEqxH@qh5Ogv-)PRsTWcMd$7AB>5}udBF=Xf#&VrqcVy6f&CzRcvb#tw)DJ={3{dX zuYT|tGi>{m^l%%>i>}fBgU;mhVy>Fk$~cR^cz8K~#eC7?;4+QR0R9!{uQ)G?{h)u+ zLgF)|mCo1tgL*G@sHsKx4BvUYV;5eVa5AO*o&0BrFUs7HPswZ9$*EOx$Sv`=Oq0#$ zi6?`(D0qE8ooPJqW6}3nl`_8qS50w>uF`%G_bZ*(ha58J`rsc_UI5h}gvSKAzRN*T z(RZlt%)JTjJ7a$3FZZjLgtN_j(O{Qe25v3)QmywmIIm_cx3dH;_s^JFP*hQO%2B(w zyU(?cFVy_b8!0b}`<2=cGWWyCRfEq9=M{1?@R%Tnym`>n9!=!6#JpYgap1?{9CD?r zyLc^?Zvx(M@EMp}tKYAf&v5g=Pnw4}Tf1NVQw}*xJV z6@1YrqIzq79OSRiqgV6x5Xm94*Am|FnCQlYJLbWOXJRLZT6`?>9;`U-w`Ih*LH({@B{J! zfG5N6!N(3&mi)`$iGz0u9$t6>u*dly%^8{!Vxlhwy)U(x?oYGmI0i8uR+PIBoj}?-F_w!IDGX zHYH7)U;SuxZ)cCmODh)tXd; zFBP6R%&)-N*58AUld*`*P}y{%P-w{FUO?F0_nXnVPezbiv`vXG`jxq&I>0Am*aBi_#4_ znIzc{s$3s>^x?w4vKzTY_*XgyQq39urhEH);`4%ku)FM?bw3XKCUl-mbamyCNSa?A zB)(|Cy09J1M;*`AHwN@rH)!OzOY$FN{~+(3C$tPCetXQu*41An{YT67F%KCY6ZY_~ zHE$t)JLU{;iYJc!ILxgDp8@+p=C{AA&D%4jm&)hW&1*r#DN=I=^yqze9XS5zg{duh z)W-q;3O;(wuYMDL`^`p&bItT!A%6w_)%DznvP$9(SQ`{|fhZ_IZ6$^T3Jl z-CZ<(`~R{o7u5>~GRum8)f8VaZ;Qq$3L~Ev?{U6N9%#t*ouxSg_XojezQ-pQDN^sjDvxsFGbrwd z%E=&yY~4%6dk}kPba|U>L4;=U1wQ!>i@%k37__ySc6=$30S6TF3Jur6%eOCd}n?T=r zmf_ycew;U@M{h6OT2n-{U!GTY&)L%RVsAM5IQXu>DdJq;vLUN{#)Y0G&UO=Vid5g3 z^H<0rD{iggY*!FpRL!rrk7Mn}>6&*>+AFdjWIv9Yw`UnR+Xd3cG4k7y>r|cem^`eI@>fGmADGk06Q{gOJQq#Mc||-X@Wk=GUFS0tFKaOuFDs2HAx~Uu&yuFM z$h-8*`d0FWk6gJTC!;i+d=p)WtL8&{=OgCl#A7m^I7NKFsvsY|&e@(#K6>1*)E)=( zc5rJ|o}uQbAgm(s^FiuTTL62Be233yE4;Wf^$c+Q~aqPSnF`4v2Iw;HFD*AjV# zX7PqAzcc%BaBsI04kWzcj~@!A_h3)Y4PLQyzk+XqIot1$$HY*t2WR_UnltG4_S&MavOY|?-!eA2Yg(7Q zdDNSLj~@5-ziB^sQRAvTG_ZnlGHT8Muchiuz#Bfb$Cg1a7&t|kxAT8c`Ek@-G+sO= z=y}19gTAvxdZ`^K&+s<&COXPo6z@S>;$P`}hDh-b+7S=={Uzpz7|JuS&&!iI+q`$y z^P=c^Aul>fdK2J&T&7+s`0emb1n-!&t`p^ujdO0B{UH8>s+Y<< z8SvYy$wzXZe5t$h z%{0FPXM6mxZBx>zN3XcG?6m}EdoJY}yjDhALXw&^USBDlS3#P03BJ@V!r8|Bs(sHZ ztu(@Lzw+__A#(N7qvq#KEpl&HerM#6k#C333%-e?G8eUdH_`BE{KePLa{)1-~=i zgNG8Y$K9YFJ$&@=rH1XkC$DST=;Yp(_bAtQ?Le*M`X(Ck?U*y@_p8~bKd$sBOwV*q zaUjn2UaMZJ`mQi<{~>ase^omlJ-(}8E59>xGND%UtCHX#@B0Sd>bbosS8_5fsk4%z zEnX{EP)>%u0IEM|+~bTJ_N|t0-%CBOP8NIGJEKRh`Zzon<(`+~Ghi-?o)`0EnERpo zonO%M3_4Gyi~I+9kMmpZzYIJXn~!+!yvd5I#+)Mb2jO?FJnpwER^uTX|AXwufj4}u zxzJ=s9+O?ft;Jjv{=sSVA2fPQW;jOn-7$Q})CY;Py~ccWNn%8_-zKjfo}uzx?Jem~ z{C3Xu1?(z6{=|hz@*c#!9ey0-kZXcQiq}&209G&w!rS6H%`w)XH2`&x^w6 zg?szUbY3BUb@RZ_(L0ea_)5+2gQ(5Pck-CxiYVdzU!Rz~7bfc_GgbD|_b%%3m3KUc7f^emmw2$jKy? z@5+BB{d1bPXUlt#y-Uo$f{z~gE55hq^xx||k$m**;nnvz*4`!S9zFjD`F;g%EpmOx zw=)m(DMo{m@acs zv)3~pxn|i$u4!yI{zKEA1O}Jf5z2PZ=%sW zH1V_{f2I0^HstfN_VD%+Zmr@Wqd)l0@W{T?9i6V!ohqR7DtNgac})BYiYw|g?gx6Q z@B(lSneXl3GXzvUd1y~bf2;cy=dX1B)iczah>gA&G=lgH;2~pv1^$(~x1&cNT|G|t z3=akliW-%0)qF7VI`s$fUA44*q4{yx!^?hWc;cAfu6n89$rQ{hmYmEE>e2TW4kUB7 z6SVx*MCzqte)S=7)%3kH=2xprQ-$Advl=;MaEdU$x`#O1{9R%1%yS0#QrETnu9njt zr@bHNV{Pxuz6n0Bz-Pd{9ef7(yp%6hc`a|7CenG8spYSJmOak?l-`tQ=$!W^@!PQ< z>tSGN1eqUzBN`&~T-b{^9&@ZWS^fhY4< zSiz*rLq`z59r^Yyd9GGGnM-HJ*Bzk$Am*a%OT9xqFPo9A+PwY$@3mBWXYgc{&x_}x zDO%49ew@k2Hl8nOy3@1!;6U%P;NqB)Wv%AIW!qzn{Xyn_Ab*9tXj0DF(%FZ9*Zer} z52g}dl(`?si-H5G=Ivj~{YvFUot;`_F3Nd^n#K3cPxqZPDnoo;$`i*q8N3IPzk=5i z`$6<^I4}Ar`RLs>P7(MFdQOIUGR##&o`1_RYl>nMw89CeFsi>h4TYsqfp zF;Tu$<(ud^|J>r&f$~~tR)^Ycrw-=6L28$9}Hg?Xz=hd5BW|)Bk}sMA50Q&czgbpnqLJ=A15K=19}f) zeuX^4Jn@F-RfHD2o)J#x74o9&T|!YoqfnR(f&OM zUZ0KTiDRx>NAl6bYYEQwUgD~iYI|q)d3`@RpXOJb>q{c9<*cM;%@fCa=TNQhjQ=3= z3|$7>_9|@JbbivYfSol~eH`Q&u*WGL@b%~^{@+GsEInc#qH)#0DS{UOIpim_{$QWN zk0~ePY{-kYnZ}rF7k8V#R_o*ZeCFnX^OR@kL-#A||DfvQ)S6!-uVuF}4-P!wI!4>$ zxUbr0aZGYZoxLuAxF48{qDRmB!DjQI#47p^f>Q)9z@g)B>{?8FXY>cbL+1Zr2+gnJ z{FWKKmbhQB$K)DuikR2OobC2KFP&S9`_+S<{w__|{HvZkRBrH?==b)`Zpp;IVt%{I zGvMBSTbnb$YYAWKpTe#6)_h(Av>tsU?VVpBo(%gYto=BbWZsV6gzNl^!mWiDfcXsI z^&J;)cn8auk{30f46fa4_qE}Eg+2~^^t>NrujOiuC(}Z?zBi7&CHJc^`mW%2hA*`! zIGA$CWlcHaiEBv>NLpkWxpGy`>eBhL$5A=tbegyG{7OHsFc;-~yS7Q-?FGbd zA8W2%e9!z`-*MtyLSB?TaeQwFp8>v9JDVZxxN4qJeFL4xJT$P<_0@(&)q9Ra6R+=} zrOo0@^Q*R_$cq}eA9u{pXq;`uzY3uD;Hjh$mZik~@U-GHFbA^4WG8tB#M?h9ucLE1nGJWbQP!%KS>@WPYdn zReZHq!+Wj|4(vwrcFtc}pI1GpKiDnJE7?QaJJ(Q;KC{2Kfvcu+GW;Io9zF9J@Llno zA%wgDdXFAlHFdvYpVwT?@66{F=I#6*Zv!f$U~eLCqy%iIb`P6_R{8}dkr~c<})at z7khZ0vHA~ABfm5DIEsJOhjM-3evE4p7%ZGOMBiTXIq z{QzehUI64|(3>cxIfLFGL?1`>=(&#r?g#UbF=uF%Jr3^e;MU?l=pmeeh_=-yg|ch-rl0^2f=69>^6mb6Ek<*FTSh7ikSue8G*!sVobBN> zXJ}}&)Al&b{kTT^!3EXc)E~Tiex-j`%x@npo;bzpQ@&K!`IqnN>AQl*MD=m1MwbX@8+~VMJ_CI8B{FB=@2XYfUvZC~^H<0-;JniB?eK<| z4ETEVB>(RtS1P;YjWz4t3mx}Xhyq0f&KgCUWeRIVd z{vY>WhvrlM>ixt^!f(g?YOm{9;vp{-&bHlMd4}QRE(OI>u8({4;B4c(Vy|U;4rEZ( z?Q1h#`gpGOTCDk2{=NJ13r>KzbSI9Gj(S9&D(;=mU<*Q`R^!@n>8m~|7 z2cI>4Xii`HUF2l{s?mi5I@7$J|AStVX8>n=%CU{-i>Qw?%eypqYfQY{WEYD~%KFUv3qPxzZFOG3 zkHh&Z_5$EPSaarY;?{D$U3r&OkA4GrOmxn6SD7=!R#%XRm*?%wXRx8UD0~yl{b)(J zt$8iYG;bfE{RjUm=M~y^%B>5}$#KA|eJiN@4Q9ZBe86gFmD(dBarFebCrZn@> zB?%F6ejA8~jCniyIQkxk`RxN_kCUYF8JK^CoD9AzJIP-;r`kwfl(`=|Uld%m%bF(+ z??G!0q#N}I!GXLW??Iz~5cw(>&z9DApz~_7_)?Y6YwWOUk6m_Q zwI=e!S##AwDJO&81pb5SyV5--W9u%HcL_Wh-s7nJRU6IQIVbZW<@%JzWFPf$_`5@laBoj6 zFVBC<(0AtE1pDamALPC>^6hJVCydIFdpmoVFuxj=+?{&#_n$akV`uX`?VaIW;`7Sr zcaEywedduP3mb9ep<-&HTlUtvF(PJL&O zxh2|v@cOAd`VV4$#d8L5)s!cW=c2f`!{-$zeH`>AkZJ*u`GY=X3 zE97Kel%AJ8-LH^m;J!2G+i`D4u21n9YAJ_YAGV2l6V~Syyy4)BUZ*(&crx%VVa}j@ z^!N{gTf5`xY^|4yT%YoWhY_Da^>MgA2p>KAgZv+yb2zhfdCtqSAFLq{FTN}8<0!r; zyy07d!@Ro=Zk3)F=iB*y#d{p(^HTTrqvUta4BsmA_R!kIlgrEB$$vWiRB{L5i|Tn% z<}=);`PDCr?=kdJFH(Py`B&VdpS`=+iBD@<+g=pk1bP#f%wrNfiQmp#HT3A=U4n1I zRy=VwCzI?c*T;DV+^@{yUAk#*O?Z=fUipJ&^!QuzJFEFsoyLI#p8-8D z&dH!R(HNFH>Cd6hx!Vi>%GmQ#_jWzk_tWBulhl84Ojg^zF)(*xSMW`s@2tE_=nu|vX(0Yp1$mbgpFz*bfU^z$)idOG zM$e0Rea8Kut#G!%7gc-buI;$B$jK;=iT)luNPe6KEhoe0mEyPa-q}vLwVzh@EL=}} zoD<2$?)2D?5aJ+4=W)a@(|NU#6!mYs%U_dZt<3ui zKPP@Wdi3f&Xk#8r-xdCY%DV*a2Yg<7{_6gJ=y{DboL9&*ppS!`%zoj?1PT9&=j|$o zta38gJHJjGNdH|?8y(hsM&5AG>~~8B9`dhhy5{fF+cVc|k>ApYkJ`-{{2V)8`Qub^ zghxbJ2In=f&P7KChTBn!5CSQ+);EBC9>E2G5ch5jJt zuZ&&*#TV^5_>TCzX70SO-gY(naguY=WslRT+m->}jUMm+Q{;-JpO~L9*%T;q0PHsu*!CO(78 z$$%%Ld=r<*V`7}QmtLJqxjwuHc|VAIJNr`6$5H1M{Da7genaoU0xRDH@}m46R6Z~6 zrQ$!BziekrL2$UY3-u<}Q%>gc`Znr2>-To$n?Mek`B%1dZ%0n%U3w3)$3<eq1^C zbm`SD#Bb;RppBXPIQpD{`J(^CzZyn8uZFO{;tnTX(s+F;f2E&SJiiLuVf&Hoxtoo? zRvuoSUoF?ZtG(he`LFOrmB)nh?Z*2R^V{dq{R;g->~Xk1I3pvVU<>&sW)cT-`_$fZ zZR1N#spex#7Km0%aIWnryZ{}^=jFcY z&)jJ80=(LAz_s_ld&YQ(Cl3BW_J)H~^tte_z<~t!<2&uV;<>2mP24fvi7%K}FwJ3P zQ@?whw#vLcC$j_bMF*tsC9h>TdE$JC*VnRei0Na>i!x89OSgjh-_E{ycr|%=inJQ;jf!G`@H=VX-6OWm*9 zdoA_ziuqTQwVcfVVxK0?c5L(|;q^5(+H3zo$E1pO`F3~O5282mv-u@B$NQfpzjHC= z88YNO7@+N)!M|$LzAN54?~K_TJcrJ!t>WSRb^RU6A-_x<$a&%g*i7Cf=C?nqd6$g$ zcKC4|v_1|zao}Gu2eOgQEAZRXd|w~6y8nC56C}@2O?lDL$u4R4SrBzUX%HnD{z6k$;eVUbtUb&qd)4?=`n$d;!hd6aV2oxM;=8mJ^zf zp68-`Z#VLghm9+s{~+d9de2MmO@IT*{43mkMqz`0dI!al2>t z!L!8gj6KeF>EmF|zt z+l_C?-Z_eReTvV(??J`cX1}w}lfnIpeO^fsha`XHCf;zJv(4|pFs(nxTs6*L*=xK$ z})=@%3q<6qvoRc4^A4D=6k=^AB4xGRqpMfr#~Uz1kc+CO8zQ@dS2Wg zv|aQJ@kN=>uz5h&Zky|`-ZgKZahER@y$Qv?;{70UGT1xo_bcSve>q!H-?3ZCfGXKL zBiGj|-xczr@H_L~86IBragZ0~d%Mjets}Nh+o<)tcrFT_40DRGcV-VSdo96du(x^M z{p!$7ldgyT9rs@1ujW^Tv)zZh;W6Yf`8~I<^l?Jh1@CB~9=(5$^@B!^s|kvYzM0Ts zek$>7thbhD;QgS{y9A#X-`mlnkI`~6$cvU~UVy(&ZK?1t@Fy=okocXM1F8Ht9pVd2 z+2;3%hrG$a7sZ?*e#HpO>7?e>i#Ga^hZlT?WZ4gbtETtp-{^6x(N^Z7$jR)r^j-B^ z?!vP2Bhgy^3iB)Uao*Q>$iv+2Y>r8f9y#PbmV?ww&v1%19CDyXg&Hu$sw=Ed71JI`n{dGY9Zc*!EeXp)4hF%mTzai z=uDb3C@%o>WD4rL3J>|m$Tdrkn4dS@S(p=EO!s!>qu-PNwAROYP37X_zi0qt=Lh*Q+^gTqGw!b8?`$l!jIyFNV7 zMVpIqe~|fCHa2=*6!&(ucSg_afOC5P$)nPIdyt10IT`G6kdt9=IC99?J2MZtoaUn8 z_x9&a z%?sCtZzaCyyLKUzzf!qAhm^IMeF{IUoPWAR`Z$|4FTjPx_s>uFoj5A9f2H%Jy7Olm z51g0#6>>7%AN;@eI4<;E`83S0K6oUe>@V6obB~_$?Kg;j1y9@pnlmWAsD<*Pj|%@v z@6qoizUX!OuJpZgA?1*9zhZCrU#AKx0)t7Q2Pjwg>v^TOwa&oDvwqVVJB`$4{6G54cP?pG!9 zUGaMm9$xI7RbCYD!C3OdWtE0&9LR62dK2LFO&az;@`f)znLzxj)5#qz4)PyVoNXij z3fx-j^9plO=BmLP9nT#(kXlvw3`iXOu^vi{|rEduPsz&RRE% z=Ay{;A%}doZvy*4a3I;cg!vWsCNOWmNu2HWedn!HcMqS_H^|ZE%8gSS$rBeU`F4NF z^{IS2`e z#)G)E7tKe>N55Ws4{{#|d>`SyLwd7Wa5c`g*gLwGR*xjMI3IIXYe&}Yk7V(UAP~1l0#N|9B^w@PA1Id`nB0r|30+8 zq+fRYic#dp!M&aH4BW?=7+MuOD6!dmOWWh1N8d{SLFIRT*l@qvv#2uWxOCpHb<=frQViiSq3>Bis66@67qD9O6J4xgWc!ms+gx z+t(O!$oyTcq5GB5KdAc7{2we5o{aLPGWTQJ&fnJGrtb=zqIB91Zm#dztw3_fYnFaY zJY?qe;k#12KKA1nIgs!!!H-is?b69u9OK(~ok< zk=2z)qRalseONf#$jPwJYpV7hY?AK^{=wyxZ%2=Q#;!#hTeSQY{|D=-H?fZPI7f-! zZfli8X096NuXKL9;}t*3$*8#~JiJRH;)RFYNc=0#U$KuqdgIJp`;Yg(@J36X_8&yw z86Fcpuh4gfA173}YWpk?Q;&YP|BG0hbz;EYVpWUKo#Fwgi6MD~!_c)5*uJkqlUo{cxQ;x$%&q156?-kgZ$Il%E<71{0eIe?t<4!aY2Kv<@ul+KS@ExK9{7p& z&U$a6$BEBtn%hRW|1xybqz37w;=FPXyk|@w@h3O|ziJ^LJvfkN z`mS(q$A6H|E9LWgjJRqc8lU0)C7A|K9QMwfZx0l&W!Unm8DRw*EB?Ogykf7Vnlr%f z{HSmsM-ZO@z0`fjpRnQ~xA*X}FBKjW<`m()T2!{{NIZFX!IOC~@Jacu^n5$!SJ)3a z<#kVcCE3mLLCTq|OGP&h{B&kg-9F+JG5>0e+1A`hz6sUG2|0a2JSOl0Je9sX-%@Ut z-h|Jv67r=Ep&ase#3@oa8P1C$Clip=D&8gJ`jjvAZJLX6t}le<4DiJ19LP8=*N2`L z<_up`t}l&x6Rn1MJNAPX^6)aJNX^@MF3NL;miRlS3FaE&6ipnJ*8hFyv2``teh_`UF}>OtP{N29zG zekLD1dR}~9{onJeO6P3y@OGPjdGT1Yt+^q-cwYWA$B|9_x;kyWI*+_diqD{*SKwcb z9acu1qGxG82roeO=*Unu&9czv%WcTIE2 zn?rd9^qp~UzbXDf^d^|!zRz`BLlot&{>bgSYPY3Rl3i*TaX*xgUhx@>xjyE}7<;Mh z^H=M<7WyR!x0d%f;6MgA+FrSFY7_Z!aKAEo;sPD#5MLA?-cr*V^M}Nf$!pi6x8{Bz z&yeqC8M2%>Mb8iiGBUcBa(y#;G|F5Q`$7Ap3d_UvUF|utsQMM^d37~-;*f6#4;da_ z%o*m0=6oB_GMWa4b&y!vJR8#`xcK6>1*wu{GP2l<`xT@^QNIzRDP;LaN16uq)Cg*cGx zn`5Z^5ZbKR_{$1^H(|#xufKe@m(oC1LmU5=BFjsr~1wZj!)kezp;7sS4qP) z?gw(nJa3QjGZFU#Ib^rF+orxt??Kxu*H3M!m{|~z;lJF`;x9d~Yp3k51W4amy$6p- zk6z8Mbe_zYh8*%#&0_*ik?Id3FFK>g&33&B-aGF-5?ei1@}lTFv(JnBgTHEX2KWbY zZ*Tt|RC!V5iQ7D{SbFrzW5VYZ-h;?9zze`!wbChB;mr$QFuiZiT6#8eivQQbzxstd zCdgk!i^l{$dd|0Z8yxDrC3q*zMGNS>nninOc*F4@wC2gE`4zsa$^PF(X3%%lwlGut zIPciasa<+zd}XS!+pc6codG`}m&Ynvb6Q&ihl`iHF>k_JhdDAkVO2 z(v7gc;@;Q#&gy=}^LE^?xIf5!XZAa9pm}?AbQ7IdU&l@$AAON_fj8~Tp#v>j%y+|2JhN^ z{7Lb|StKvY_jbmAt6hzgp_JCB`qb8DBLTwcD5yi4%oY#!h@* zFA}Hd7WL@AiS?2GAp7XyU4l3K6-&$t&+OeL0}h4Iyxp4LuICxBcdjF!7xz*Fm(Qg6 zmGQjlEu3w5Ef@IZdEM_hi{69QJegR^i|QU;<}-k+7EN9N@MMtdYbC!k-`hPXhg^On zuKKlx{p1AzS8a5{ujcm>|Bm~MINO+uV(%QWyJKFDv{A{uCC^ZQ;G(v7hTr+};_;Ga zD5iXSNAf#=R_Ru_HnSt;+tnTi|3UbjONEDg)SR~TyT~d2{~Nt^K!6&xjVYr2;M*|;#DT=#8TWSIombZ1N)4sGGje?@&yY`hXK)}t5-$Mz z2lf3RdJ~;2j>LfkpF#DVxsL;0AA5MgL*7SRHTL7+yYfozp5~MnvD@{;7d3b2J(xG? zZ$tj-;XoJd-o96S6MEm-I)4>Oz0~ItPsdIPHT$Ft+2}T{<=+<$9G|%>dZYcC6ZBm@ zp8YO)0g!K3Jeea)G7X$<{XMAX8GJKl735XiIAwDs&~aYhcZQeAcg1_0MSi*B;e|JR zPsyXi>%)JL--9a8z&-lGnjhyshTQ~R{p^c60d2#3G8uH zom2aOY-xc%QF&8y*AfKT8RR{VH z9;bV|lauWuEh9>_o|nqWr1n4TJhtwV@Q}IZg?u}FUdS_Geg$srU&Mjr95UwZ6|Unb zCle!kXZ8YckG@gnqRjnhF%L{Ut9jy>hs=FvuT^1a{lUocE1AmgL__^r#Xyl z*7#S*Um=HlI{69O51NPrsrc(#ITk{#<4Y$5u z!8gHu9OYdqsCVhMc|i4OZ}Lqb&w%^Y)+yfP9}KHCpEOCX4?JY{P2fG)k#aIyr{&GN zBOa4`=jZs2YnSU|K7(so7y7O~sj;^i>RvbWEqM>(Klpgm!-1X2@2tM7KZM`T97y(t zV~;bO{5ZH@Vcw4ZAnxs$U%_k1-X)%2abA>r6UOffc?Nwy$UU!VlI!!$2rk&9&9Bsc zaEmF^d_=gl577Ne&$lDb(6V}=##LjF$wc8Hv)}o}aT^8&^tjg8>D=w3;X4ATmx`PW z`%+FLk`O$Fb%!VBW5B z$jCErkABLqf+=r>-x1#gcrxFO-b|dL_WeQhoptXL@(erZJ%~9&k=FCdIX~mr)SbVr zzmpm$z6p3OFP|-zK8}7~A>R&99J~PFY=ejVRQgG}U%i#ttMF6u0ysI@J<_awSIie( z>+3x#%fPKg9|sHrM|~bJ66SS642ugjn&ATYBC*ILLcjaB)oOvSxE3dBa1zT?gML?#J|F z(|2B3e>*iaDbn(qfhWU!2K1d3pCK*0Y|1$8yt0rd4*Nm%9tGq4u`+*+ct&F$$315`w3{8|5 zMV^8CgUBI=dv~?+S~jN!Cq>I#6#c=1yLiZ|KL`&mdi1I{;Y4``^t^&kzjL*tfq%uE zZTKdb`vE?K;$Nxrin+C|2EHiw=pU2+U^#i0uy?MbJ`VHx{C7Bf)bhif#@DU90O(Cj z3^n_#7_z}_T1)>6`!#PkJaI<9^Mjtbl0!xxhxdc<0)T%7etRIj2f2?ktDvCbPmNov z?(G@c{HmvCs7pQZ8TJxS26+bl5AM@AMJs$JgjR{iggtT0Rcrmh-mu5v^NKk|%18gW zw#QLiHKP{*_xAU+zBBluS;X1qc{_5*TO=>q(dli&-WhX-CuKj#??LY4DDM*ZSLoxw zms)!^@^EVD%AA*J&S0Hq;Qrta16M7?yW8Mq>ZMLQHjOxt>`P@&9C{PZ)bsK-c$eU# z_a62Q{RhEAM*iv)c}%h?&ww1V>d|jDaJDgT$A6H0sTYgB&Du|S23I<-n1?(}=Iy#K zRe1r%G{jo@Qmy?s@H@jBuJ+C<&!GIy>V9S9A)}XyoDAky*blPDgy-#c^dE$e9`p9l zy?Fp#I>W z2EVg@zq(9$(fY7Fnv1r!ozOVjYCl+8^cQ(8vBw!VDu=$SF=jj2<2a1GLwlULr$4TA z*XE+w4<^xn5OY!8W1@I{IIpnB`JKGs$uehP&Nl8>xVO8KZ=xh->#}C?JHrcbTk@iQ z2Jh1HoUGE>hyOU6SKqB$9?coP*LZ!{t{t3C7Aff}vng!VB=XmP1Bw zqFHjtDa4b(y}g3wSKw@qu*9$EPk!fss@v2{%^@HCQO%E|e5u$EZlS$%&~kfA;Bvo= zpn|-LhEuJ-&Z0dI_*b^#OEpKt`mK|`Gx+TlCH=GGR(MfwB1*WmuM=n6tnvE5Z@)x- zoEg-2#(pqxU1#FSz+%)85AyIxzGHWK#(y@}wYD9fwTn^5N!`%=+&2EQHicAQsx ziTlBO9Qd6(5&ueYYg>i0-K_1M(Ra?2-bBNJpNhV-%D4AAah!O4zYkq6_bc!j9=FQ% z?IoWV=VZ{M*Ylzq28|#ez4ANj^LE9*(s_N%XShY)@VC4c5?>VmL3qQrPkoy>+u&an z$oz`Gs{qQ2I$fFWxS;Rb!?#WKnA^5!tL9w-p8@ms&?-;qrH&##1AkZW0_c2Eea-;C zv)*?`FBSYNwZ{SX1N^H_WBwKRDD|Bc{|fuTwzd;B_tAb(?+>zXVq)Ds%C~3v-Z%fk zVsCSsN#*+ZyqZJ#cFrO5e=w2q?d9T2g(r@C6X(cdf;j`{uh1WaFBSViya(T~@&fSv zO7Z%-NiTKEG5?)4>)VLe_lm}EFRXXzmN(#s(c}Ej3r~jo&fq}q)jV z_p4UZIPwDI`i>u!MLcBiuk^g=eHsthPUZ~od3~68fjE%;0-d!v!#>Ji@p%RQ6?&;Q z25cb@&vL9sspyGb$o;Yx}!EZe75q5Nf$T1G(QmVUF1kTpfP>B%+MEI373bS+N7@+X?RwuCdz_?`A;Q@% zplomX&&Bf;)&ZvJY?`>O2`W^lfEmXhxa#{U*!+@ ze)M?%A0t=Gd9^mYh;qmkcC%`e#OL*;<{#AmgZ3jgPb;3+5N~IGjd;k}G-ptp?Mp>p zw|ftMs(Iqjcjo;dpH~k_j~?F@-`l}MMvva^uKl3#J;?Jb#J``Sc zUCZ@>v(5Y~H#)B(X^->!sS@Hd_%F97Z#bV<9SnR?^qt|ySsby%FWak+=S-Jd!YNXD zQHM1PHU{k4cl>eUs-ZuqeDut3e?j_#Hs@@lKM4Pz-XCP&1bf5BXuLkW2Z!BtZ&!UB z>~T~d=U&Rmw3%)bpP_Yx%_GiEW#SE2yuJbH75S!e)5+Pj@7R5B)tgu?-f;G%{(5#E z@!LnQjHdtK70NTHdArU-E->U7kQY@R6V4$or{2UBjRT3jbDHdN)`V}LGIrQ^nsKoW)jQhZY zuA>_kR+n4tac0wdu$AWR8wZV`dpmp+%tJQ$qzu{QHkG&^*4`z>t)1oK=9%lY*l$V1 zhtf;szH@~5=Nq3@`TsNIpl76ozuo9 z_p-d-Zhocu&g0Fk!e>AZ+1Y8w)ll-$xgzThA_o!@aizZOBZg9Di5fx8FzetKsgyYMwZCzv@c; z!SM}I+J8`aEpfkcpnQ9t^d|Uz1)mr8gPYu5r+It8t{CDJG52G4$rI8aL@zbk&n!6^ z2Rg5e+**IhA%_sBXfy3`;CFu4>bqhu0QjOewOn5uaX+}1S}7i0@UJF^ekJ|E2#s5N zv(e@pczt?a6kY&uiWI-SPvDbLuV~(I_N9W)0N;cQ<&gVO-}z_iQ#;{UCa& z;HoLVvs+NRXEeXUcZL1n`-zv$V-p^Y`WNxr znFFcsotdi^vZICkgXpD>7*|U<P%m{s^d<6! z&!l;KtL8D;XP93ZJ#p+Gtd}_h^F`6eaiRC1o8|qKs;rBYL!Ml>*Ey%Z_oz(Y9`k=9 zZtd;(Ey97sc?AyS{e|l??Ngj2e+ABVsQ6Ni-xc#ki-?EJoT8Mw@}ihCAb+Lvuhd+$ znR;H+j?LJ4iSq3WDTjpONU9`Ma*AOCZatCoIZ9%OP@v?jbvxN6K7)%jP< zDdHZz;)`-0=fdLq=4bhiCr{kux}T^waaP+8^1S`i8XKEo%)09t6;NuEjfpbT;CScTJ!r$Oc8N@Z+W>HxN5pLoV`o>{R&*Qr)l0E6MZr0rEwc1 zFKYjh|Bmo=_Ng6{_E`S4>Pl|B@R-00kbQo8QFkiGv{Da81^LYi%w(c=$q4^c=?eOqk zq8|Nt+B^Hm{R%yL=C||wig|tTJ7bTd{LYHs&N*cG=)qO9NizB-m@mpbFZgl(Cv%2E z+7IqK9=I!VV}~`zlSW(OR`k!VD0!54GGQ+FdFFdX`X$j^6#l{9;>QVew7K&4sr-td z0>6wvL%zNKl%19rRr#wAmLx{R%6<^N3Cyq5ch$B!cB9{}1JuXaMZA``rSHr+ z8SHWRymC%$wH&haT6H6Le%bybQCcq*{=snxm*{?__@X>#n7`X8?>_3$GpDHWz>jAp z*Ht*L?LTo;ns3kf=ZOP}Tp#>{t^GRFyb zWoA47Ao2|0w_}fkdwXGUsFv$fy$Q}UfU}M7O21#Re{hJ&c2Sz#+xOT7)uxSKxm1 zmHE{NS`Hcfs}nUnPs}C{?<>h3mVGHdWSuX%As!RV8M3JFJeKx2+@t6DmFlH3_XGDU zQ+e{qpxO$%uRVNZKRDR*k@N@8(s#9`{#S!H9KO_A@_DU2|8`Sj&+hH=418~AZ}`+> zzB_+i-$eVt(JPaPFZvsKEzz5pKsgzni)KlW9{WLfmyl=pBH7tum$FVckZ)h@q;Vkc z#5b8Hntw9%QpZ#Nsz&$>qm#R)-9y}3_Ia`2S>>-(&#OZ2?TRO3m((#eRD7w-RYOh& z+*){-z9H_1i8$M;=hd?MtE3lc-oEFqd;8%f8FF5gY2I+;kb@0;QTzwtn}CP+?crtO z$Kk#6Li!H|8GQ7MsF%thLxM9`e1xS+n74wk6RpUMmcrxrUVQy`=`MJJhM`hD~@RH;iaBoM?%h})`pZJtoM>oQ)lnc*lHO>pM^D(IosU z5W7e%h`PTLnX_nnxtyKCM(2Jg}h@&Z%}2a@g zJ8AE%l<(~`lj1Eei8q}8gPcRgz5N*VozsQizRxb8HbwT%$RRsOzCFy~F=4(a z_XpLS0bDiqhU0$qa`Jr|Uljg9o-@>IoT5I$XTaVWdmM1K*_Rro@vju0VL|k5*$+;T zTpzpu6~fuZy&ayoO1fX6N8gKbeeFGQxVJlAsXJ9%F{2<*3wZ&YXg~NFaf&d%`a*nO>|Ls&{UFY(=ZMe1erJ4F z$hR|B4IUFducqysaV+cn&ZfqmuEay;Tpzd}=npFX6@01SKw{4D>PoYCEsL!949vg6 ze-OS@_77Ub!^?YT^l>#zR&;dK;VP-Rp+tPP!fT z2l3lqOL&6v4Db)WM&A{9$mru(^V_Fu`KzXcM#^7J7Cr;=4CTjX?TXxJzvc_d^*xfk zSNQG7Gbj#ZI^`MEdr-f(<9?+$keFZDT?wMSbMe%N40BQ3+ub|^Ty9?rsCq)!2Kwcd^_&#$hW%}W|DX56y^GoB!~R1hxf3p zD)ZL|0eJd$5u?MYGn0XdFn+$;3oA8u~aqzv3Kn z^hR6ai{d|6QPTg=oT@*s&2i~YerIq$vdo1h`*?Ta$;_qm3VDVg&1=d0_G_m)T=8{W z(0BXrol|?wbs#?u_za84ml`5|oINF;*)c1;h=<1r{@5$|YH$NY5P$)nQ5WAZn7 zEx~VpCHWre530QA)uHPR9uw>b_4i=Kkp;x9Wv&|MMKQlpy;S66?$G^eeCSG_#35VU zCevKhpL$+jXt_R}Cj-tlJSG{!X8^B{dC1J`Yv1#7&^Sfzb8TclxHMvs%&(Zwu#fi6 z;C^5(>c6W(^DbGmoDApN^}MK>i|Y5QJ=9A@kDj?|uW9~4oLA^O>-j6MWOv#RvTwqN z_JiX`t(823x?jN?-q~r})j8UEmN$sk_lxu<*b@i7s6KB8R}JS?D|wfc$0UmK z4B)Ey(ESQtfX6QEqx==0S8*#I&wi(5;Gv+Z295gx-vs+{z;EA4`#}ftc?F3t75hQF z2iXh2a|ZZQ!Dl$CeOJc!U_9Ng?8IX-fadM`c?G|7MD!K%JL7%@z9_s)4yhfI$}JDC zs>_Wkt2h!o@@((^hK6>P2kQdeS?d|gnoNu?I{UHAbdlzQY z{pu9;oz;289^NeShJ&;HQ{-6xA4YE$j|uw+@gEGJ`4xM^ab97Mqx?APyy`O8rkB1S zL=HJhJaPK`s+!(|E6GQX_aOTR%jvvI)HsmbAN;6tF7@c)o7h5q=T_oh^^m=DrL#}n z1ucKYJQ-uI5BGNFi{iUde1=NNi^hm=0&@m_4?3Q!Ctja%@2q_E%DV)f4D#(STVhu{ znq6TvZ(roM%LO+z;let?`{g|H1Kf7qxu5{$0WEY-{tp`yZ6+Lm%e? z@%s7%b{pe1@IBX88se+pJrY;;hj5CVQ)dzX$~u1qpV!mc{EGKDm@^2U zo6aljosH)e_@dlPRUAmn+m&zPI?bnMf*zn5%RhDz$u!#9EVpw6w;|AXLPf%~C+UU(1gmt3DK2nDS!0~y$4t1c&&`F1SYkmUR)or^Yvr7=eIXC z_UvKkJM(#kd3&>PihRWj5KCOOHRKIfUVw?jXMm61cF~&f9aF{(``+Unnv0^3^EBld zz!!BF-$ZAbU*X<^#0l%Gl^eWGQ z?+WJ?IFRAXr)Go{PMbJsmqVvBBJ`;;LcZuJ@gb zwfvPnXW;$dj;32QX8;cw9LO!pb_l;6eP{S4W=OvM?!7agSKu?Cmul<}a&O{EL#_|^ ztN$w}gC2baeOEU%A3f$*9rGf0cR%qNc>(?yx^YrH&98pZI7QgwaBqU=SCtlt^q`qj4Z#)Ox9d#g7ADs>-)VZL}jFJ%!dD>bz)dU$Th)Lp~p_nqO` z@k-sPLd|Q5xu}0ZQAORU4p;mf=l6X_JaOP5V?T&_`-7eVF1M})(fo?{gQ`c5{vhWW zct7|oaf(z<203K=bFD{%iBn{kRB7o$9ux3na9+8S$K2o!+wz8gP4oHBK-EA z^dEddcrw4~_eiW}I{DW7k7gen-TC4TE zlqZfoCcz%tsW;JLDxkizItP_+qFKu`fUEW^dE!)$e#43*nGb@$=B}fKgRS4TTk3ta)Y z*ry@}`geD8F>uv5hup#N9=z8$tykXg7u|mjh>-mt`h$A@s$9;&&*ZyOerM)DMjGY} z@GimcJloDz>(S#JoJ09`^&VW{S}JqV?eXba&kMc@m1p?dre(tGixhslC!K@LRl^?V zer(v{x_}ql^M_CCb&qn$xL?_8|0}!)_4ySzMQYy8-_;n}JNJ_L)fqS zsJwRe&HBUI-Wh!yDK$t6C+_u=b=&%iw7Z)iUldunyro}3Zl z1z@h4<-Pr$)=O2nK6v78Q=UQX2fJj4lsc1d0^ilUUiX53jXFYpoM$8Y`S&nYG%~9kZeD-jFp*In`@Q60Qf+sFaIFQ&oPfMLg z+>cqQ{xWap9CDF1XW+SL=J-d5ha4>ZL1SK&=T~{c{rHmR?W*Uc@(i38HQuj25Ka-# z8QS|M(8mF<4}E9ms&T&kmUx#^26>H3o%vAM70EMzCu1|VMVq(lb5ZOERZd3t#GyB# zc*xu79K2e+(2y6kd=GXfzccS~*lUR#GV%;r7rl(ojK)xNhKHd*9 zPv)}8J@%;xSLx%xyTtp!0b2elXm`s|8}d6dzkQ*2!{JL!@H88I^vYub-^78!L5JrV zya0L*xnF3pN}8*W;-m^>zdly4t)cyHm;G;jYb z!B%+4>;*`qxv26@zzcwUJ9COU9P`~BvZaIcQh(bVRa|;x=><39^S3HOVuK zBhEIsYUt4`UZ3KsWrXB=dr_`$u6$SEA!9#?{}uesyXv~yH&Y)6b5ZyQIe(@4gFgwc zuOexn<_(9}QsqT&2j7gUimNeAiv3%}lm7M=-X-1-+EULeKs>xNdTks2BHgcAB!@gX zD2eh6Q(K-A4kZ3p+)Fk3ofS_;`Mj6|8EDt?^PSUK<#Tgq(02vj1oyl;l5axqc`*+; z(&s(HIrxEaicV4f%71S;^_{bw6UhsJoJ_N+k$Mv^yKf!t^Ot^S<}=_u$UgeP5tCvs z)BFl~QGG5tPkO0+DTfU266OpQ4jF?cj(c}zx3H@g{Hr$FI|rWmtfF`RX7PD}Q>1$> zu^+VDJL|oP1yvh}&w!qn)w-HYzY_1`TQ2Twx+lCoaMj@R0#63_tNX;i!a4X$W@B=2 z!Xono@ef`%aEg@A%Zj|=9>l*2tX*|#McIKI_?_(y-X(qyqUWW5S3SehW_pcVKd8## z&H7qxKZx(D*M^SS;iWxKo~mgPJ_Gl0FlPY29XT1~+j-7#V{`wtm1b*;{1y8LmB)nn z4C>yl@>i|m$ALE-ejNA*;iF&TV_Nv(@(fdMyj^rZ%8Q1QZ-VopYVXY459CGt?7}Sa z?H@0Xr#{XjL*|OlYk%QWn<7__r~g$1^#{T0%X3bUJq~=Sd~dh(r7{N+9^T(6e+8Zl z?{V0d%HNgFLp~w^5cw>xv1jF zJQeXM?FTEUM-T1?`<*dwhc{gB;~?J-Z}>gab2M*H^fYO?KK48F{0e(#><8Jqlr=ue zXT`#QEl)P>i0=^HJ9OW)!ZDGq)9pH4zd=6wnUoiuBk#dmr){tM&|GxSw7#Ks(fRRd zrYf3?A}r-*qnUi80$hZpA{xV1jL8i*$YPLaJf7mX_3 zcVwCPQk~rT`45Yj5L-+BLFRspc4;;EQePKtE&s3BYdK|Hs?6J&*9Y$s?pM1c-;Q3Y zF(Y%4zw`p&qwqnC=_#BS=*voAHbd`|A$bZ>`$Fi_(Z;XPP5Z9u3E z@vp$^i_&t)=+T!IKDBAd>IvrmBy?CGk>$7dpypk&?4@207)9?vc;c*%;k)Wcy;SgI z)V%#b&qpnC$STjE&)YF)fG-vE_8QF-2fs7=IJjT2CysM{;ES?1d=qgX;RS#vPVJr5 zToilfWW)Sw2i@D(7p=;8V@&X*h(68~zi-3WuQ>XbzEt=on16*n4(G4B5T610E6y`qw%`=O@2vbd z$jKltiaCRg)r$r`1A8r9wR2F-uP|@7r8z?o`EhPH`VyZ3Jehms1^92|TS0N2t4D5i z_O>_&nX6V&IOuTjxj%2rp?rI^&kC)VihDb};oS4eE)SHR*F2iHXVW?8Zx`yiw_RS8 zzbp80g2guh@6s9JY_pF(Tjp2D_2C>`EIu#p(IbZ(u%)v_FLmjKHySE>K03_7?Fs+C zMR>$sqWhJ84qAEv4pEQ3q4JW(zrz3O5^*5m4QH>VC8r4cL3ja7TK-D+n5cPsHTfpM z{os4M`d?ki^rd@yVN(nBywo{ZK;P9|kM1MddgnK7y*Tx_U&$5XKw^)xCL_6MQPmZT zo)^5~$RVrwmF|fHza2Sb#ew8=5br_cMVqNV*pud>Ja7L=n=>FU$~k22J2PJtdmP2T zdQ|wL+)Kqd2#-mtxl3{YsqA9B^x~AGA5ARj&7ufW-6KMwz| z^zRDiAbTx2FFK=Sy0&)?^4Kn30OgzDyy#})_0`h+D%w1D&FYNQq7dQ~b$7}c`VIN$ znTKp6Juh%Sz-M5e7tcl2oI!sN=F9v_&tDx9zcc%B2FU*^w6xD(`cj8Q^z-lO<}5z1 ziX)4Pf7f!z%#*>KVS2CJ;V-&hH}nUm$bQhXQ2t8iA!9#?yeNACFlVU9Szfl9_BfV(oOQ$()j5#-U5yhz4tO$e9e?j)L6cP< z?oI5bo|noYycbJ{M*0(htOwc|N{H+vAjxhZokLFk4AxD!h6&_yjWcm%sAU?y<H}YhyLJT@=YKo17GU%#FJ5Z2Ie6*9}V6eK)ngP z2f@EG?gzndXTLN4SKu>XkE6UxiZ6PO_Bfnx*SWReA@ezi9(`v+UetJRHxUo{>w!~7 z7kF<9XJRFkxEa**!rmFa3AJ}-J_G(&@GhZ8j~udp!ZP#NHEYBZ z#~jG&2~)_I%6^ErArZ+Lv!2Q+8UJtpwcceQUJK12S3yTbhdR}Hx17Jy@^MMmG_*edBX>+Ehny;>UlMik6wQdj)?n`c*trl ziaY~+^k>Lp0!|Tp6N)biJ_CAQ=+T3R4E~jmo!#|6=sh?+HCW5HV=k)yuh2`yeh|67 zrNX~r?-I|ixJSR(FlS&Nz20}e5-`Sn8+mx!zOy>U_bY44GyJi6VR7Y=a5@J$e>Egx zV(b-DmGlSUOJzR}&cTN^j7@Ykms5Wb_bbk~>z+7#SLo4$huqbomkOWPvnk~{E6Wne z?+ku>vG@8A+hrr-KQX0{&kG#LE>8KxLvH^b5V5N5YD4ZtlOf!rXzlYRs*@E4>N6w=-wkJ1EXGX=FCt+tJ5~ zB(B;RbL?u@O$Q2}J{)lF-VI+zmmwMBqb1juM}8c=M}LES6YL-4xhVH0_}-3Qs`0x5 zuMhh{_IdIB3Uh|2tXX?2gWh2I%-2F}SWDc(=__JchK4(npb$<#+R z3ikuwRiBf=rQJ0T@A>r01ve|NYTOUr5R`$6tIa}L?K$0^T=En9UekowLZqYJ$^ zk~duM(WCDSZ+JNEoyUyP7tUk6!f$jq`R#r(ELo zVJ^x&FXj|s&Ja{b)OeFMLPe=#=w(d|KL{xy+#*!Z=@Wu@`i&aqyAUmA>+H!d#UUn z>^Wi%c}%viY&Pv!S-7MqaE{#D*<*t5O7Ucnlet#C{Z?nEeDS4n{>o|@_RipJ@1t`N zo;Y>C0}ibn_!*{ z=aBvEZ0LT4IRkrm*+-kCjd@H;cV{bb^EDf860CTpNn*9@+DbMh>ya&5$bI}&z*0O()`#7`cy8@p9{Hu*cA#@Hp zIOPvLPxEyq?P2`;axKB2ifloJ_CD9 zddU2WbI8AHUdzj-NwI@1=2vR(e1Ld;%IC%J!O;d^s=8lobM}@wgW}2Pz0}(`f*gAd z**boS&q|B=7564OO0LiH9Q@;S$Lllf7P}T}IplQ7w_`tuo)@?uxL!#1~~ggUX9`cFMn1R~}n8dflY=Ao!w# zr9W8S)1x8$LgkUL;#-^h(SFcg>v`SN`p#;P13m-x&iUk{*Za|BCm{=y`={z6qRzYCp*NEAWsj3SBqFt{!9lBEf3? zV&WmAKM0RWlINtLYVumP#=f)omw*@DbG1GWzAM{gJK;cD=C8Ov$b3=cui!DUvNHa! z2AC^_Q&dBH9P|glt^Fy+lfJ8*1qIae>T18UZl3UD;I&kK9Grv5Um@3*rtJqWgirN5 zNB8zz;>m0*N+7Np^N=yW;&af_kE8a^D&LO(6~3!Q<~KB-*S1^Tg|qz~@sL#x8GYyd z;^D=d;f!!=!GWBArnt_5-h+8`zfydLjG2?hZKU@g_JdaozD)l}<1=7C$owmKm*9!} zIPQw{ox8a`Dqetc$|0lgtaCq%y@~L}&BQ5!kKXcr#e7kH&JZAT(Ras3NsperOFO3R z8x!j4Z)Z#QcE4Od>P-Ylt`FP~=GGPw4|!+&I@3||T7u7jzO(aJFA@-)is=>U$i_uhg7jz%WORTMHiYpTS$bei`+Wi;dO4YWkm? zTiPSrk-V0ex1&G!(^=2@gNFMR=2uP8xs-25e{h$9`@wx2ya!iNZ{oaxe+3RC{#WNG zC&94*(l6~~WOY#Ckr02DTxF6`z2hhAdDI-;U6J4ANh}Q>C zoH_^D!>jvokdsmQtAEp+p^JUn6W|nWTX2`W;rL&n@0=q0LGX~lf#myDx9oYPeNTQW zdmQ#!R>fVV?+RQse^;0@phv%-a>y&l3-D#4E#(=| z$HD)qi#ESvFTiEus&W2Ge-FYpf&E~Y?D?gA=(|F`J=3c(__wH|ahFW)u|sH&qrBmk zy;Q!pBj3(kwX4#j|9jl|$Vow~J>xZA->kh$vsxv8^*8Z(-MKMG-h=2ng9FKZ9PqDZ z*x4KSSBlTToNZsq$-rZRUMlkKD=qj8;MU?jIL>@p?pMaXGv^uL#{st%`77*k;Ku={ zs7rF2neXjtk5eW*nWx1Ia5Jhwe5vp*wcq3DzSND6SEalloB zf6z~IGP-YK>(Fl}O!oVh?pMzfudm2^lH`!%wYez%SNfd6f%xsnwlu&I`QODwblKvp(SIEg=-VXoZ<7-RJ_6fGh!Qvmh z*Z4Mh;*h_BkNzKV)sgN&Ydu#IuMhlo|GkT{I!I3DDdN@!kk^vu3`UR1u4%i*M7Ykh z>nJ%H_LwlID8Kx7?S8de%gL~R5FAMEJ1egxd*YU5%_2XJS4%GCue`+X44>EafEV5K zXf6sr&g|X6n#V-n1iF4?5Xd}9^Qc3GV-PFq`4@36MPP0KiF0H4Dg0Cw^nhAGAk zYBluedCp+(R4}x9!eqaT;j34CYI=^otDW8xC4ZH8Dpt#1bxO*h9CGlP;yOqBrYG#i z=1ku%J$i6I*hgPM-xd7M-;gKnjYP){-N;9OvZkf&WtVHCa=dP7Ts8h*vDY%*U;9re1FI_#D?(LYj!7Mf!d4~4#sR`cUv@63H1crDeO z0lieruR_jzTH&0(In9>l?cg(*DbG+!_x2+14Iv$t{UiPud3fPnDi~_-wBuGyb%Zt- z#r#TnOp2PC$&a%muvmQb#Y=Vs`g`;m(b#)W(?;SHVUNT2c9oOC{Hj2>wJOiRJ$k$c zF&8z_9!K{AV19*ryW$jKKRAgvMckV>=rE=J$Fq%KMGufZ4s&aDe!Jq<>b=yWY4GDH{#B$;*7#oSdK2Sh-tM!vLb$ca zi+Trr5&2?VgQ+DpT=MP8M-NXN=dX-@XU-vmvpqhx&QuY1C+Y_Iaj+l6oPqbwuO$wk z|COE>1+Oo4W-sj=wCs5uC;uSxufTyck=GI)6ZIZMe{e13+c6ha9uwZqAZk{Xygzz`s&`9QeGLQ-t{yeDoK>*AusvbI6lNS8F}5136C% zPiB+sop;wcNd79}2pyJ7>T;EpW)>g$`5f3joMacCnG4N!pY0dx- zukz8q?0zjEY;jxc9n*-obL8PopnJQ{DLPF&Wc_}H|CRE@f!C*UeV8+_&kLMw%o))0 z@|XRfkKH@2d*!==*AjDv#XeayXFyJ72yttdt$xk?Swe^P;aLIl9^^TLXVBTm7pU*d zUQ7J18X7wYp8>o+aJGM?Tp#-fIp40hYJWuCrv4x}+iKq4lfJ8iNf*+8EVxtoBl&TV zZ{Iw#J9)!-KbWWSkiox#*Rq3w*N43`_a=}RZKFO8@(h;VCFEq_AB4w*?^p17frmWT zBTsmJ_^$E;gFU*BXr{jN4Dvg-Ci^Eun#Zq6BF=X0|KooZTpLFmNR_|ZNOK0|qqm{C zDEB5V$2=l@29=Y!Snw_NCSFVIvB8nvgNiS@)$3O9?WjX>*G;d-J`*v}zpLA0!^)_S zQ%+np&bQkq2W|=6-E!29a(!9ScfK!NHSZv^r)gw?^W>IiFO^Y$u*3TQB)l#>WaMNT zDA&j5Am;7p(KG*QzH5M;HO_*ZvmkHh~fy+@BZ!~PrLx&i|YHq4;|75d5qgcb5Z4+K&}t{!T0L|+xfiq=OmP^JT<>|kIi=j-A3>7 zP9bmjCmLrP_p5EU>Ll04K6>;f;NjhJaYs`t@kR5AtEPGr@TDRr1I{+*WYl-Hq39jr zKz8|`|0{hiiuo1$ot+I_HT0eNf2Hp2Du=B2qU=j`qVLN2O`E(%ypyG=4k#=+Vc)yq!6a!|8w3elDu^I8PAwV=ndR`&szBtfL3W{3<{= z+lt=~PLVN(yl+gA@Q_nYpBVBC4dkPTH=O6~iq9}q=AxWK2ConK_FB4MP49J2ySI0e z{1x{m7PZSWTs9qz`%TNYE1#E|GbrxIm9vxRe&snXl{k9L5I)K8+X>l2yE)|=czx;|WS`frrn|i#9x+Gb6y=H+fVpbe4`MEg z`Bjwp)irA~))g(3|5g6bZ)lIRmUw;E#I03(9J9O!|C?xQc1TJUe*13m(eIePbwRV_ zMIWMbF#Rt%Mf(2=_ba>ye~bDgu0}j@oWEjkIJ`^kIosF|!efH_73Yw7euaBG?{R#E ztA=|!=Iuid2cG+r=2zLolQ}|s2CHZn;vt8NFO~g+ItLPb(N5al8U4ZB_;k9rNBg`x zzK`(w*b|5E3jeDmS-yJ@TJYPyiX0x-V!9I>x%jH)wFD0td4VG zqSo`;D&D0}YxsuYs0ICJx)!#K90M1!;L(de`|bE-afB(ug&>^lP$Ql zMqes;GVqv8ShL6+lyHx@YVZ%@-ri%x9FHRLT5kV~{vh(Avn7AEp=bg12jQa!SB-mK zcn`uyKY8>y@((h`pzZw~Tl()h1bNYz)lW!o0zI#X<$q=5_3fem z73Np$wM0%PaMkqGxzwAm)#jr8LhYh+<2RWOi^rtD#wmh_*Vv=y-b4=N8JM%J_fnOA z&|17p@bH2sW1Nff{0bhEkK=w9kI85`2X%hC;(pYeom_vwA!CsHxJ@&AQVzLQI7OTn zMLvMezlQ2y!zfbdqLs!mRD?^wAa2X?w(3p%TpdQf)Uj)pngbHbzTBh0lO|`*E3zV%~n9_Bh}amF5iByy4)<1RA{I_^w)Me$^u^W2QU#aWHR( zhnG3quM=N1nC|V&>)RrGoXf;jgWuV>cZT1Y`R$xTR{5(jl9Rb(dLiz=4*KiWHU zUevArBW*v}elBX{x7#Jzn?EPcc4Aqa+^>)q-9%o1Pqh7@Wv&l<9Q0Cg4o*LwLEKtL z@ejg}gFX&?6Fnv0uI}wB*Oy*2U-pC8J5TjHAD*_N$}}pzjd(K1_2GY&ApSw*WbDL8 z&-`}yCJLu#FSu`!XV7!V@LC#s6U_ZUZvt}$Oy-Q^&W)BWPk<#%1X{bBUcUc zcH|kbcgFmR{e$oVz#Fdk49rynUlhL7f_NM4JvdwLSN^#^sR80$3J{M8@(g=4PaNh~ z+)G7H23|{e;^4J>+5F#xX4>O`tET%VzScY@HIiq*dl399+}rImZta015yTfY`kj@B z_gBiv{Dl3hw1~&# z`|wGA-%Z#$)X6FP)^FAGC?_+Xyi3;WYBFbOJ};vu4n2DI4{~n;9uxR+^qh>EGjJaV z_x59^k@2ldHwq7VKXE@)-&uJ~{#6lrrby##s~ob*i>3&_{buE5@_7xQ`xW-ioRa}h z26Iu&8GfSt6?ihPl#^LRJuk&6>YN-vJedyPH_)8nCHLzA;TFCL^d^k`L3o#rS>v~ZTdTMq%x|Abd4-=DySTXy+uzee;}p3F_k(+0w%^|-ujN=R-;SKjRQg{bhdk)ghbN}*jmf%C-ldq; zPtqQTIgo08#dA^IubAKNPjk_28eeqos#&SNx!Z*UiMc3yEx~7i*K(Q9X1TY|p?v$0 zO;M{~HGh%Nye>LxCiNze>q8&MOE^XC`R$l91j=`X9zFLa+=%GN*GJtA-t6`J zs8KFfR`AikQ`+T!=43X~cct=In78ZRCEVLP5}!fe<8WSdM?1bK`@AgsIQ$;O9w+N! zev>ujuaqB$xgW^M-0fY`ly=eQ_;kuMfZzVA#=k0%oXli{7Xbc2%o)mU{AJF7yeRvf z!71WCj?UQzza2gLbjph!6b|HN@x;B6*nLA+`Cs9F^()=4emCU$x|4^O`Rz;Oe#Jd{ z@Y~g#VU+u?0Sgwl#x@dH4ZJ?=aqwLk--FSs2XER}`1ixU=bCQJc61(+NxZ%z%Qu*c z;@g%D4BbJTZS+!+7ggL^V~?IW+q04`MEQ&^$o9U%?Z{J}>m0ub7SyXIuALB8TiCKKc)n&ZS=! zULWpP%>B4TdC~in7e!7+@vofoH>TN3UKCulMC}~hE`6L&H1CqixBtJq;qaKKdArUj zVqYq9GD{6UuYBrxeL3MRzw_Z647@((egsLr9bQY-^TM0~T(!0*Y{*A{hvrx8F`1&B zgMU8LY_@x0UW!;4|E#{vh{K^PD}k zJq~j}qG`?muO;{l+?&AsihUD`&)`e>cJ`Qn*B4~S_4&$v&?VHC{#W4EVt$3b^Bs*Z zx?=Tc^H&M?)-BJPPXDX7Xnqy%`Bu=G$dPe(wf`0PSC-t`gXVr~Z*5*!TybOx?Q!}K z>+Uwd|LKT{j=L}28KbT$=QuT}Y=)auc zO+NagrV-*X!M&Y%G6`k-a)xX9_I|`w+g;b$zD@G&ydSKgTp#xb-Ro=5{$6>F=AzEh z$C)Q{2He}>$6-E$o8|>LAo;7s!oRXguul$>{FTbdu!ooDqEmtrwfA7Cxg!m`S$Dl9uwxb z>)ekZ+T(yHqqw#3d9mNwM)QU<2U7Q#fK!AVGCaJvx9i>|mA|?fd`sq6@J(>8PkG|N zLsmX7c*EhF@ay&7@Rx#G&kK{!;Cj(Dh`@M6TDKEZL^dq5r zr|ldQ;X13G$AstY_^$LlPJhjh6H9yNzJ^?%k*kI|!_Z4pi2Jd|a}vD=n@sm(!)bnn zJOk%N`F_QF=NsaiaBnzZ=nr0_J`U#@xJM7ZDChc=7eH~<^qdTO6Zo#!!|NbDdYAmv zw9a%6KAW=N!k3C(YV*>U;y*H_QQsL}0DIv;a;^^?NceGPXn9d^iVEZ$R2~zxADn)C zx^Rlno6zU&Jimf372J>KsXw^G+f(*9t4_t1RpdOKawf5p_y-^K#I?mwG+iYRuLtdM zFmG4>!T!=qHRjuucM19Srz0K{Kh8u0|EipNsqDwGp>q(qKK!qghj)r$&cJ;fo{M5X z_yP6k@gC%Rd)&g0mTxrW#&@D#>Moj#+F!4y{oqVPP6qEm%&!)>?j5sJxF4L8St1-r zo?jt{yj1g;;JX?kz6tg&ji5aadJ~GPrh80m#7DoF<_sR>iQ~TWo9%k3$csKKe&^Q= z=inuoGjRTj`J&8|=`glg(etX-{DY(7KPEl{zN-Ssw{w3G9uwuGN6%}g@cI;|NbPaJXW;(etdeV)_mk%& zETQ~Wisqx|zVkQXDJwpqd^_@@`kVoI(V;146K&0%lTyTVE-d zo^Ro!$NlOcakhEh4sPvugBQStIFMVs?#R8}jrgJtZjZ~Hp^WygeP`|u z!W+)_D>c7b=(?NwILs;fXt`-&r0j9H=e56Zu>7x7-}z~px1*OjScxxNb8<81sN z`$62>`x3WS`6l>12){FX!xdi?bI}VEwh^B}an)WUP7(Zr%)jDZD)?7wex-X%kQX)X zalmKL^H<6fx1KoLR?AXCiY4F9`@xEwp~S7_-o){W`Db=YZ$k0zIIk4FDqnUoh@NO=bE+r4D( zJiX;_mkzb_(SM+Eij?0O^Y(n=elV{ObB0?6UZ3J2^Envmx^K)*Ehlqi`SOJ^KAR1_ zi8Auzz&{8dJ@*HvN{`+%=*!3#;{G(XNnVtD^n7n;uVtmVKjmZ&QvM2k9HWo^hVU8C zO9iJ$c`f^yKcw$U_lA3o+c2|t*u|JB&-}Z>S@NQ%#YdkYJQ)vq4_Yl7 zA^t(;6m_L@aQ*|m3H0bO7d4)P$TNV?;6!Y=3m<_R>vLNE1G@=g3s9^SgBV{w;F zZn4iue^7bi_`5RZ`go6n{M9w$x8E^6PhNl|@#8SBZz*vL)xI_$HY9;Vaw^dg6#dN2=a|U^remvVCy;NtKw>xDAm-dmqGje^6)SG}8U~t6a{$0ewtLGWuUFw?b zw`Jb$=A*Whzseat#{Fu*V)CUj4;fswJZCTBi~5Lvko!3M3!gdcf9@{%oil09pmH+c z^|8mqhUTIwFRI>y{9SSW3cghI2aiy`ee3u~Xg?TmZYc4Pm#==={6#{WmS;c?8GdK( zP2hip9{o#N9|zppK)PR5%KQrdE6f>;`F7nC$2l4Nudv5?HSsa?fusxRKhggRJQ<@m z9Ph!L1v{oUzhe7jNBiA%^UoZs7?__*erM(3Ro*4^CN`2606ApLuk`oef~u6Fq>OQE zmYIDMtkzxmAN@go54O*@w#VS?e)~h;qR)m!3zM7iR#hA!)q&^ zxUF=*T0(Qtk#T1%c*tt+e42PNoWGiP?$(XD(#KJ`KIGdKPe#q#XBxZ!+o(scIFRbQ zQe3s(LxLP{5f2&p_88eaFU|7ZTQ2!_la@nfzw>qRhO>VVJY?lD!JGl}E5#QDr^q;O zXP%7iF?l1=*?d^|qTF}hK=Z5KVL!x7HQkMGjowP%mCDKB-VV+-`h$Nb4=;KX@7sJU zxxQApwS|ZQzq~!YG1?Wqh?G?m<?=Kb{I8Ic0l)n=@sN%0LC(ow&X6q}NcMTDzH|2Qw|m`hY^J=Zkz33ALG>O)FO|PO)~8u_BQx9jI1_Ri|wj$B`%cY27` zvge3X^lkW5zb_{g5>Mur>X52UMe!MKYr@Sz;$713S92eD4|XGOIPXg!@sIHRRjTn=tyFmDlna>Eqan&uhr0eU$6F zN9Q2(uN0r5U+8X{U(IUAfz*2w;4^SerhN_>ygqza;4?g-dE)S0A=fuW;}m@rIfDGo zixyuA80-GtaGzdx#A}KBmD&%o$AtIJ%>8iF@(f$OekKm&NtuiG*zj87qjU~lBJYxW z{UPzu_XxX6-laRy`Py8Rd#TtvcS`DL{y#0>9$dT6rh1^K#uo+mgL_`s4}!Ceb8r>S z8ITuMp14BFw+Cw974!PoKM4L6axyyiV~wFVfnF-}+dnalifo1=DllWI3$sVVhquJWBtbiXjc-L3~%(J2SuiMS2e^|KPa9N6i&TWH7(Ne$Z;!$Om%B*Q@7Or57b+Ojxs+ zdZ|2Tz`Y&kAiMy3mTZ%p4ElrWe}y>%_zb=YG3Hm+tj$<2y$RjBguOHP4A|qijs9+6 ziA`{A!l~G@gE>zT_oH)Cy6gwx#{mZta|XU&U5Rm$yePP}%45P@HPuV~4|#Zv`$2ek z;dkyYdsx#(ef58k%mU+M2b>~T1MwWltK<_!3* z^tmYJ3}rT74V*eUk8;Q_#vjrAgPdo$R=u!l9r>NH$5A|)%@>QcUg}@*kinDjExAfQ zdP{DtgCW<)UI4vE&%M;roWG0DYZL7UgQ)M!^LB8yu^+q|;}o`OrpLInL5CbX>Ms%Z z1Nkdw^8{^vg>&#Fm-N~661U$Tc z;sxNjDD#jpXZR+3!-`|3=i+Ubtq&>oo89`q&uAnxrvznZ&hI(=7)tF~uM zF}(+i;x|x_UgfXuQckA4@X1ZltH)7~UgxUiJFgtMTITKC^HM%~aBEwR29p=SD&e4c zAob`!II@KLgZ+o~aO?ko*V4#maN6*?#utUxa$@~Q4(kU^CBHLxea*z#Moz}+NuB$_ z{z2TYFu#hU`_*}EeueL9198;3CxiRd{iT~j^1Y{w{(9g(n|ZbIv>!ySuZ!76<_yTU;~WGB zGT&k@$~%{ATd*PL3TE@tE{+xaYOBxQ@<2<)gQu{ot%# z?@?Yfa&cqqeT^q$nL}DS0Qu;VZ|5F8`0cyKEOvd{&f)s6rwhvc zh$mxh4p`-z>YH02y@>@{UX<@w$hU*nhyGyTxu*^vD12hm^3|`<|4Q{HFmF#BX%a6$ z6TJtoiYE@(L%3i&I=>tpZI|0O<~QYJhZ+}m~T2k!06Rl}SiSMKfbd7Sge4qL_c`J7=*%27v@sO5pSNtpX(QhaUqrS6#4zh>WGT&}K6<1cS z%^7$uI{(bBI(y2=fY)dAO@P-|Nj#Z9Y0mIr(g5?=#NHe1vKN*1IQe=eADx3%*T%AX~)C*cpM$h#z51D=R#{3n0smS$Ni8s6<+Sc@@>1xbF zVJS1G(s!kDGMvA{-nmcw0vuQu5 zyq3?!os0COTwkuUm&_T!DMB9yJY?LjR9+O^kJyDrrAM#(CivbS&~7ez^R(@CKglz= z5U-Cp+sniYaGU&t?8m`=(0V=h2bo*Tz6s13RNr}wcmdkJLoapS7F*&WKT0`d_5xJ) zbfG!JQ{=~a@I9!!OU$js{R-zGIFM7wkE7?1E%!Ls55mI>Uf&1giEGbiIBr_A;-c&a zyAZE0M0*c%u8(sv@B-j|rSF}=>%*J@ednzgryLI``6<(S-Ry)V<~P=uh_hW+op;MY z&cO@e>xj=_$!`b$3iI|en%7c&SICRzlh5mZ{A5#2Ob_yTq37j6{z2|}>3y8Onm0UC z_*a)qlVkrD@dVA=nJ2?uOZ2?hH-Wvg-p64deG}!7@xS78kbSBDFMd1cMU{UL9+Tfs z=atcZL_h(~!U7o|lXHb;-%}k^dF?&JC1jsMWld1BV@Wphw>+IndCXQ2RlZ zZ=XtDfNkWpESLQt_BhK7^LFfU*u#rC1Nwvg$s5l6EACAgJ-pfFf1YmpInZvNYuT9X z(*}lGM;8dco%8L?fqdrhel3TLoQ&eCDIY!j&de9R8ZgGtOPwP*nF`Ij#P@dPo4~xC z_k)W2aYDQR_^vR&`X>F7VSdFv`WoUC-Hpy)uuXian2R2#{$MAWGr&KH9CC@4>w_2I zQ~6)9ZvuQ#{$IVW`BK3b1!udNe5u*T*O)$4sc`eyD zabaMIO=#_kQ>)}X$b07j`NxE_t)GK7rirGSn1{nQ%=8|&VbF&Tlj<*vfAFKEzUDU* zyUBj=^O~l%u`acvwt4*#{AW~U+%Kl_vBM(z)BUQ#@Ll14g?T&pSMcyEuO)n`@R-26 zWb}C2A|=@_~xaXLbiKP z9(`WRGqj&yao-v5LChKMkZCj z_@YZLl(oxW!4n6srEjmy;jg-13W!>KH@1oL44xMIL2w{3XTbl8y#T5=fjL7w@kKd* zrF)m)^HO{C1VULf@I+gTAS=S9LJ^t@2Ox z5l#{B2l>0&5x-tI+gr!?qWo1L^#?8e&K2bI0tXVk)Uz~izpr`2eS6(*?C^c-(K+&6 zF$Z#K@!liRTK;Mv^}N7U`!(ub@Mf=i%8T+j=$PGI^8%>*RfR)3an+bF3SVlz)_2zD z?RuUeUUGewUV!8kKRl4@!*{jR>`Og*aMi%SLf;vC=bhU9s=$KZ?pJa-(`p^(`jY5> zb*;LPzAN+xtHaY)d~A9lzHMn*NFnjtk?Vuci*qs^&Gy2-3L#INv6qT@JMvfXO>hpG zIosUF>A9h6_PkQ(lmDveU^R;T&Uf1NQo-4VHynAs5 z$RYE0#W@-Doz+|ve9;)6n1%mZzRt8ma((a*`ch6NhdjLCi}KzX^Y)I{XWB)$?jk=9 zJaH-7oB>=lE#wb^JX#2fK}!=aDOW9Ai#~y-VOg zHf#T@l%j=ISHZP)eo;i?TrhlB&$LuDYZTRTnALMy^ebmR=yc|D#Qs6}omGz>erM!loJ03a+of^Uz(eNuAb2u?0YHV!#&i0re`EfcXl$jr2 zdwuh=;(g>@I;h>--;h2I^BMGcyZT>MCS6FsSkPE`QF^H!;>Vd{YSQ|H=+W=e?(Lix zReT2a<9HhS&PJ~#pM&W|X3C4|`$6taupdW12faU)?S$U=4D~&IT z`IYm@<25!`qiHUBJNO2jgV;L{Bwy+WT8|zaNcd8bznc9Qe9`-+f5e?L^t>#+;mBX% ze-%u*KIL8F?+Up-^itXHe3SlH(^G?1SrMPXFSo#=m)a+^qx7BOcLx8;*Rkn_|GA;W z7hSsgHS;N&U-^k|Vw>|S>N|fCIV$cB@!NkEZtY#|yXuf^YvHxj^He09NE^+?qk+7d)JjiRQ&Oybk?W4^ZhSGZwd{NHHAb-`mbfn~D znERpU`jC@RoFYH+<1n9Lo=1U2Zz9B_%Lto3c}-at-y*&!zANT_C_heD`mQ_;{lS!w zopQelketjj#M$PY%%>JUFL-$2iK`($&eU=12Yuu)k^F;}oFe4gPmmv{PI_KdaW#|| zbtTR=_vm?k1?~sVLH{j5#AmqEI8*ZN@X_;r5P1fZ#*<I&C9!QFU(zR}CH$qbE+yMNiY50e+mJa&J$N`4zkX zVVchi^LBXR=54VPo(%X`Wk(_}xHTM<9=-am(8oc~i#@!3PR=Xsp6y7vzADP~!AB3@ zMCzc)<2KFg9(FCp&D0j(6uou9&go6B*ge_7{{6bS;)&zAD7@jZWeKN(YWLcFJJ4-( zv3E*{)v_1l-fql`hSL0M?3!if*#^!w&l!w48E}f=AJlzby2pfj6Y%ibJM9>Hc7nHI zE(*S=hwPnskMnfOSBchU2km|ZKaR@DAkQ$-R2$i2$Ar1gwzIx|`*aSy2h|=2-__02Hss;O9%n$P4e{HnmdBGHrw@5} z=MuLTy$RLFnN1u>_FBTj%e@KaA&=2GkX8mS0Ql|PADq}wM&2b?{~=m0)p!m*a`K&0 zd&;*zDtjE=yQH`u;340M-o7A*da255Ie_}k;6Og3akka`3i+$&jUA8V{kN2CSza6-b zyi4p$MV>)F2i1245AR*!_4O2QxXvk3J^CKn9>>b+pIV-Q{W$Osc5`zXcChCg4NEVS zktdGNLA&I+G{54!C_KDZ0wNYSYyT_ckn^3r=p4lVO81zcHxXd)F2QTLUU)L9m&!e_ zxw&uCIjH-cJ6xY-7vZ{d%=^=Nhjxn26Rw)#Gk_-pF97q~;RWDaA2{3F#EpFph>&{;MNv6t_^yJra}b;&)pzEcjLM6Gv%Mx`J@K!=li|E5d*aaZN)zuA_i^|=_=Wff z`%8aNdE)f^Rrj#XGunQs?Y2@-8*gy**3#SLi#d`&C&_kA?{H4>F&D`-A8^ zvwtvm_>1n>sYlO#9Lv2ke^;N;IXIYj$oOA@CxiXq%7sTHFX~M5t1!vQ=zE;Gt7fJK zP`=&zdVt-0;&}7HLteB=G%epqjwMo@&yYXvg&zpA7?3@gSDA6h_k(; zsjauAe-JsDImChF9zA>F0=0a*>f0{rCcB8qWG@h$1%>Y_}-433_QHn$85g; zv(ZQV&N`pLQ|?!I4_0V-hF6KJhF&VXmI0C%jkoZ`F%Q}9`c3Mk&Ln<2@>g?nb7;&tRpH8R1|GwAEc;o6)*orA`?D9^8O4o1qn9ejqT zBHRtWRO|;^+fLT>IvGlSXZUfD>$^fcnTd42@)WH6%&+{ko|okw2Ywvo^U^&gGfJ*ywrKl7_5yHk z;+qMcv>!wt2OeI`uQ-3TlJ+=iKL~F)_no`aoIyVaFURz1$3w<`@L<6^=noU#^t&9MwBoqw`S{kQ zn?mxvy+)rS4=?;U*yDVWXeaMM@cQ8MVs0(Y!IpRr@x;Lk!1pWW$sDA6dv~or$nz`a zUwte*8QtfldK2Ij*(7vKp0~w!x1IF7vZa^GzKQ=vz7-VjnMiwQ%-g{!YFT%NI7LrN zkG@g*ILjo5jNXJg2iZptJ_F|M=y~Bi2p)2W>$9mxU)1g#ME)wku|e+b?Ryi?UYgvp z-FfB6B+n@p^LBh!?6qt?I#)Q5;A|`ZAack9hjn|P=LK&#d|vFeRQdL=#YYb>K$_+; z;k;-r`BHHX;=AIWmz{wF$$Omh11F8%DfcUQEzuw3J&w+S?d5%z;GQl8;X@NZGa;(nyODCP_hCZwElEQ%D2BS ze&?e2^^}u|_Q@FEU$|=QF#!h>a|Yzw!M~c?lB4w|M#MFln%eE1;Y&rn9XVw70_eVp z?$k@gT(m0ghP(%H4u00|e^s4cQ*f*DvV2$id$3vKK(-kAI2mcSNnOk*6Gx=HpR=lL z<*9k{ztTOt>m`S*INOQ?2~N?y!{XE$A&K1p>n^1hxa|N z+r-)C{vhU}?6p+BRM)j-=8g#+l7lp#mrt+k;V)5d0-iW(fF=-euZ8t z`p)PN@;Qk7RqMJ)y0?QTgZChNm$1h{FSR{q8~1i=EziKbKI9pYzvBCq%3sxu8tu|5 zbJ0%X$Kl?@pOrtJ^{PKW`$6oTFB7-+UUc4q-08Mso9Vj>J#(yLKzuv4A#eD2>f?adhx}E>#a)tbFSMA8qBn8BcX3k&or6Xn z{RWFYj@moFAbn@fGXztvuZ;SGoz1pMo70^0j}vFRE6uNPzf#-}^qrA!Kcw}Y*&7b- z2mV*QAAG^3o;XF!*`6HxWWb_=irj!dMzh2SNwzUm|)JpIT_@Tzlxk3 zwAwRvXPddTj}6=3)3YI*JiJCe19R0H=sh^b;CKF-yq1`^ zBZmw=1N&0P7<>~AHv%cwhaB=JvL963TAqt$x7$1CP!8GGF5Gq3m_3@`SkY`{|9CIMmTogGOJ_q@`g3k-*Aoy3;w0nE)s8?O? zw|!Rg2>B+uQy-@?=^M>!i8%xN2X$^Odo8=z@2m?w^DoKubufRK_*BY4`d`f*h7`@s^LUm@SF&OvL+A+v9y!*yTj(X031k>zm<7yD$7e|X3| z$D5L8Kp)4FCxae+{K(awlY-7i{ypx#+^-Y|67NCqMeT*F_Q&S%;tKIifPaPepyEJU z?s0-kow6M_ykX%DpG3WhN#i!p>=srV<4*lS_;Hlia?Y90WIx!E^6dx2H-U4I=M2`w zXIM*f(dr5Lly3)D4SZ2>AdTK7_`G<2#r;9>`tkzU-Z zG2Oy8&zv|egZj>wH4Y?vUY6Wi_$Khbs**fIf7;_5ByO#mxAS*}yeM)q;4^HM{}tx# zm@~kas`qhlZ|A(|iq)>04i*kR9C)tr#%#y_L*5-9>0?^>;qnYqu5i^##)Ol1sh;Md zcn`vt3hxrn8T_f|#rwfR;$NZfoH@R?<{w0V5c4a&mpVlDIIBE8g3d<{i)$f%`;~xK z+_Q$yqC5k8Ou&=TbI8~a;vDSi=ITEv!Xx$))t7`76c0LcSgEL3o$g z3ouzcCff}h$m7Hp1)t#y>3PAIian0ycl871WZLr?u*YekIm27W-=jGL`*HX^s5~YQ zjkwpln0S3N$QwQ> zj?I}~xZuxd87@qko>tsKuzAK)KUN#-k-h-Tz!JNS{+i`u!i_o zJZC_zPxVrBr*|0J^h7uNqPh?*FWOn-UnNLRM)e1=AH;jm)506RzGzLx*fr7Z_*eYD z$`?-@{LaYrbsy12Ts7Vgf+wSTUhGS?cPbEWE%T7uw7Dp9eTj4qGN-6xQU-a$=bhPE z=SW`5{OLL3F}Y0p!O7#&v_1~rgNKp^n8zhPyrFaU0(lRjmx_7&uTe)eU#k9HvELba z2JqXF7qvOo_FbcJKiI>IoXl5RZ-V)vJiq!v^IGzpfq8vPrI%V5Zx#KZZvyjn%l9DX zkay8JX!OxDSBL`hsxPkdw(B+Qq5hR&8~7)!L%<882(zaP~VJ^X>2t=C0hc zWLw~T4@ZNK{(9!Us=7ivWWKkd zyd85<#Y0BE-P81AOmFgruOD=n-h;O*e=0anJ}>lfx@5mo+VkXT=}qK#-3-1R^`ZC& znX}D3FZ{2-zj~0f4G!eb0bz?mO8$d(xid6gA9yn0e)t&r zgL(1k#Mwr!uOInR(WA#X7$bdW%&!iR7l7~Wlc|@A{1x(|d~b)x#OPgu&kMZ12=W39 zpn3ZdLHzdb9?12rmUFNz`6|5!-@91Q)JAy*%-iw5QhjISML8z} zZY}1bk>>IA9-LqGTlLmkPEI*P&rk3o?#CyTZ^yiS;^=P&zE6D7#In6P|42EN=s-MV zcue5o?L@pjcz7-OqML}{j-J=0f(yji=Jz1Z!GG6Sk;jDZS4Zd^3toH|VDJNsu$N8tm7uER;;9vbNxjy%X zvYxKPy0|?`IT`fmk(0?Hzq2*<==nW3LGuste(*#2Up*{-XZ)|&N3Z8aH>Y*b^6hcL zDf)V#$LO8j8$xW!3&318^ar_@8Y($t_J;qGIjv+Cc`ccL1wRh<&V0WL^5`+*Uhmya z=@)&Dzg_Y(`RMT;Oe7w1UG+Bd@P0eN-S1*}@`|H|xhV6H)&B}UFYx;CzX~S5v(1yO zujK!w-+3}|igceBcrxtaMgHnKc`a{6eMC7K^l{+fMgA)4!Uwd+(R=jD3!vVE;ftFo zC-W8gyx_H*YVlp+-frZo!MhY?xVQHSeP28#b{e07ee^~@PMGWaV|EH(G}ZLca`VCn zpDdYQG4})SLCmjoP7!mqdETybir`CS?~>k|;QgTPcZS#U3I9P6?y*jYbiYb0i`D!%yvKnz9QStS z6m`+|gR3)MBQL=H{aGyr|yuQal;uqpzY~D*A&V z!YP7(ko(RbOHKw{wSL16(Ral@diDZzl>H#i!RfuW4gV+ckkNM@8TUoxG>g46->-N- zsP}PrE~@&2@Zl5Ot!9Vzdi><+H`Km=;)V;pa zVdJ1l<2H%c(uz2c@LF06XB&A2-s7Cn?pM6Wai<(I-`h8cH=O4T@H?-k?@G^$&N!Yy zT(uq}f;{qsTMG^(cztT#j`iD(xF0Fx$N6gD0h`(6AH-brwB{fD zRO@-c6IZz4cC@X@&2)wGS3Ga`s=s*lPUTO+ReMAH&V5gQT4O`|LHKdt1^6otWE9OA z_M7b!tdr+&@gx2f&O!JlFc;;%^H-9;%5$F5^6aJkCj$0HNH2Bi>cPU$S%ScRUQ-mUzy9|G=4k0ORWZ<*ZB##;svnG z$<#=10y!DvMZtm0G5F}gfn=Txdw9W<;rmr+)i2e#w;Y|e4gE&?&bVJ8f3?GV67}fu zzvB7TY0C9&N$X8~2ITsfhiu#rrqaBfd40&qD4tA@4PCQCv^>MF#Qnf`g}w7L8mGv; zA%ZwXe{Alz_7L$!18Lsg`rW<8>Exr==M2U^j>@-Nt$(mb&m2hZJA==l`Z&Jicg`iR zCHjNGtKLqXo4d9AcDuYN<_uNz9^||zaxzaHE-!pSczxFE7iG=ayZ^+qm!=4(Xo~O| z@LgRG80DTnd>Zkuc#ng*s4>?!QTU?pm>`D?ULX5$UUs?PcDkm=$%UnzDJP@PMX`5= ze{k}+RPj4AS1oV4&DiEA9PM}1y>sT1ibwNPDTl0cw$aBye~|k);HssrI4^!2^&W&5 z0DMvQ0)Wr3`QomoyS*PC;qS3Cuy9Gy$|lo}l|@UoYuwt*i&KyLmt4xUUgwt(`2Y`D zeOJsE=(){Z3*@nuC1>dGuBn>dXZpdG?O0ExH z06qtYL_Fr-#lYF-dwZq1)!a3Cws-;Bb89)zpyutkw{wsF^Mu#P?+hL?_Rcs5GsiFU ziCK7jd9rB-d3Z5r0QW=j+h*M(q<_zJ~^HRN3YwF{`N6&N7$l?!J$OGld%+I!hTGfkuA4_*OXj!33t*XVN1nlk=AvrvoGV_-g;nc|R$2JG z-XagLRiADn=6dW1+_7Z4+}pPY&hdD7L}Twga&Nzu*_!N|u-rUm&AN=_qA-Ks86IAE z!;$OTNO@7@`jC@hP7!i__DSi6yl5-U8T9{^@(;4d<5{D1s^@{ zao~wl??LW6mlCg!b27|t$A0iG-LFQw*9I(J+!)(p8czJH1cQ&>fA8X~wsrqa7;Rp; z+VwAcsj0+gz`VU=%saGqhIa{k2J~@MPDbS!_R9SVerG-hv&Q!t66|>M#ysL|t9g6t zI`o|@PYk~FR?9ZpJ9`9uE%PhP+Z9j7TI+c!|KMWsF1<-TFDJ=gf!Bw;DBgoF$(&(6 z`RHFy>`&hC%fyqx9tV5pjgph$9z8gaHc#5w@2v|w^PdXm2fP5fFBM!hodfyxgo2@+ zoeDJX68eMS_0?*7XPkrFcg|bcYAV#`SDcgK`&A3^+b66^$Ve}mFTM%(m@r?I_c)wG z#@;!BI7P#0&S0CALHu_3oxxSJlD;$hote+TdC^NWXW%^!{#SSp!bgvum*PMkjk{!; z6gybsY{SEg{$M@vMVWu4_no(rAII1qL?0*5*}G-%r9&rX?2XQ9UU!DP0E0GtP&oK- z$hjLg<~#Nrk}Z6Ob*AF@=4Gy-h11?2v(R;xUB~M;=p1C<1adOQylCl|!f6AA*N46{ z=2y;?Lsr}m=GIzk{C4!{k(2S1yeNFBmh*Py^Qxfx75}d$X?|zsY=2DNCH6ZjZ#a0! z>bt`K>Wb{0`MXj)8F&E{51D;l@21(3j~+dGQ(3&mZ{JQ{fDy#&<37%gp>|F?Z`D*U zs!A?eoAD~~`dWzJKJ|Fk#eycQK917IQ9Uot_1){ei}F{hkAwYSQbvk+0l|-=g$b`^y~xQ(KTc0R19E-fA%g=MweA@l0lseb)T8JAAb7}{kN)k$ zzta8+Ts6*%^4y-S_M)8Y+h6c}=IK-$qqVv_vnQ@=O39zhSIG5UK6|t7a`87ghg6;+ zv~sA*A*1JYBleS|M#C(culmg#A$(rgU&;5Cb$ZZNpS@PcI?uPadFVVQZH5Wrj)T4E ze8Fdk&a>QjGUZic%-YeaM;{=(;j67am^ben|fy=VW-_&igC#9^4P~ zyi9wk;6N^>-h})P!V{%Ka7cqW4GHsr?mv^ysA`FPd1Ds`d=Xw@d%ve^kC*`cj!wgzupA z58^&3J#pB#gR{L;D(&1JGb8OqvtB#Yn9-Onu~mU zsIw3Gyukfn4=?@)1BfTn`|4ZPk&hu8syoDbJxjy#9Vb8#MQRWo!oJJ7rRZuS#&lU1l|1hjgxDYW%c*EHf$M-?zGhC+L zMDB&X?RSSfY{IR@{)#;&(rYPwso1w?J+Noc=NaaxeLLoMaJG>{hR=&Rkj$-RuO)i{ zz$s!c0KDNXqUYsg{5$O#Ldd%WFM!PTabA?Y0C-<%oNa!tdJFy)dwAhpYV?bVx})ay zo_q!i^5fv{TqC$2{2kPM^j1B3UWLT#L;eam8Rn{CFUo!#hY+jyUDO|36&_FX6>}ha z=0$zQo&h--_)>9qMsLD&ZbvVbeO`F3OugX))t-UpcHX!1eUQE3%y0iT=Ms6tnOi$o zc>%gjdi3Qghs@u>=QAq{j8z83lPOViJ9sj(=T+QtrQz+v8M^<$E89Hwd8#`O_E(zU z8F|ru(bP7rT2z^W_TglGA@JWtM8rOw+L=peJYji&Umh@ z(zXb{^WNsZP_H((8oH=dGF+X3x_D4jNFT2 z-yWcP^sfl+2ky?uA#+Yl zXSu)PyeRjb!Byk`!F|e)!#ret&+A_L5|NXU9LRnHcZ@wZW48Byh1ZgK$ml!c?#x~Q zm&`9yZHUj{K%Thwnr(G+JNr_lCk}laaEkQY+Mc|=HsK#+KaS);Mp6C>eP?}M)LcAQ zoa^gQ&y`W_890Y*wee)ibYm>Nubw^>NS-+4`l7uI;fGe`8VZwp(H#f#mApGk9y0h> z@R&&M2k#l+#{pL@IXucMZ~D+NzIN?5yz8GOujR_MuNY6ISO{qp*Ao18*~gJQ8F1D5({~U&WW29h$?wc}oFONdRodnE7oIru2PMB9`}Q2#Gt3s8 zBCU@D?-G1oHfh$Xmx}*E@Y}hU3O>W9!jA)QxZI0oh`C+PSNc1S><{X_meOO=w5@}3 zGVFKO_vmGB;xBZ^VIO_9g+J{Xs;KX*=hngtz~1nkq4(5%Fvru4_6)e=NDkz1<7;#u zTq=C2-1E}&kdZ@XULSm^{6C0ZYVY(w>f>npE6uy~74@CL*+#BUyn9l&;#BAD&_A{B=x$g{4k=_&MWIUMm zea=smXK+(@=Ye63x}KNS)cd3Q*_1ag6*(Cz+B5K8H06|kQ#JK*OuvKfqL*6O(o9}U z%eH`@Y?+DqM9EEJ$mqvInRK*Gjl&UhYVlp8-WJmi{gLq!xJ7A@p+vpFDkiefwXUD z?uX3D=(%d>J9~+J`y%=u)Vu(EcP=vFY-`^^E#J;L8TclcvprXM0q!M6MK$?NaxHXO zIJlku2mSU2ZnaLgPO0gUze2v9bA6mcX0PQ-O13o#l4AY<+YSPdfA(}5aH_mAK|0_r6KHaMp@eCzl(f3=dZS1 zD5d8LcO2~7y9~vv%c9Ey{D}Ke*1q+^Lh8|DFUmP&^t_rI@+p7CcO3M*kQZgX=(kRl z;<=JMWSg|CY^S25wISkt)j6fqBcF1}^9(b0yedbR>wBAcGR#B9UKD+2cue38mp(7%K(Z$eTs7?5m)hOD zu~cv%<5UjWtD^42Xxd-t{|DiT1Fw(s4D81NpMl?3J^h35@XGrj`@CLsv{0Tn+;N5r zKhDj%%d{8eyEA-V;32og7l-cfu$bCCs<%x=v;Ucss*l6mkEAN2_zrTO0ngQEBHym@ zMd2~Q{))LD+#dvIyMy*uB_b!IeFyIk+0&jY_zZZiBo7%r`kji`$9`w@Qgefg7t9lU z2KG(he^BGefX^U%UdXrKPo8VIsP2RE?)+o%InkRKm|>^x&J)GH{d(-jNmpoYAK}|? z=3_bzr1TGB&#>fAczw)On@@X&9h5_^JF!Uh=y`4*eJDtIc;mei6sKr7`Mg5aUeurN zIOfIx!54*x7d@}N5A-Im7xfkVD?LvJ{PtSvJImgLc&g zzPdZh`HFiJoNveb3ON}(SK#$YPLZ73cZxm^^6fhV0^RJ#-yO2IJ@>*})W>O0Un2a2 z*<~Sg$6>A-=VY{e``k(OPE{8E#DV1biaor;i$2r!oq5l|{lP(EzN%Gz9Nck68mFcX z&ghdLBJvF2A)}Z2hxiV{Ybm+4;J0&5rkd_J@_&$X$nY+4UR2(lQzzs*yrJXP^4<9< z+FxOAUt;&jnC;Uey^Ok?OwWA#;w)eCE@96gbIAH$YL4O1Dr0!G>f_y@80{0_7^nQFNakd`{%b{G~LAvAI`qTZD^oE05 zi=Nkv#Bq_sd>>N2)M}dBWiM6VA4G4WiF&CWhH**X$GFmWu*hMa@-CTEj~?7w_$K^B zz8${QB*jD4`0bo$@Gtbp3Zk40`v);!NiP7rOS_V@DKCouLFO~y?hJ1@IFMRil)d4p zf`9b_<*(3pmYgEV7v(&|V~PWbyR)Oo{~-Iknw8fQ`KyOArkikUd2Z)i-zAZ6XFm?+ ztM{pw%DxF(;}@xa$*d?ysWQ^sUhgz#QW@pjCnSGj$QC&n^iriCN4~E*(|-~BE6oeQ z`zz-4Ne}N9dS6MuGxArNy8pp%W-RdjE+RAGDBTCy3n2SAMm1ld=QXtGi(0=kA2eIr zSUk~9`Sw=o48N-tc$a?*;u9`>Ba<;sEthWeyqMCERg* zg%<#M(U%0T@2<{AkLQZ{?U8ww8&8RRJNE}U-@a{ntXC5G2aA)fW9@_|Zlk(8BZus{Z=vGJEOjdiC|7r9=0GCP zkb7a#@r5FXtZ^V^u8;47nF&X9^A+ayCB$cdfACCdZ{1#$d41r?u-_S8fQQ01i9N$S zm1n3GcW3Oc9#MX0<}=_v2;am=>JL`g&mlg;^78A7FN*IV`zB z{3^opdT;!k?t@QLUeu@l9`&6!6aR|&4B!-TAIHbq^6JggMKz15mx{i#5BVmTQ-r)I z^6ipaYY^|NC2B9q?605Ep5GixPNr%UZ1Uf)6J;w zEd7J7BG)J9cG(|n7Chu3#6yPH(m~Aa%qgn1-9-B<`>;#me{iSZi^_bv193n4P_7UA zcFr?=@8oK7AB2ydb27N&fK#M#wzcn|+>5dw2mgbT11bLpvA+sX^Hr$MyM()QrU|bP z^A&pmdfsuCoOw_A2f?jHp5Y?#`Xpz&(InT$|AT{0o@(sqp5oXvvB140=nu*v&rBQ} zIo#KNro*^8`;_1{%Vo%hI^71;1A)W-={c5xDRrVp1Eqc4_2%FRo8|U%4=!bOJ#33 zo-23(m@mqAoP#vCFE2kp^A$MT$X|^Wdf_+h|s66d~6q`-9StgTAxmK#mohZSWbQ&mB0j zy!^%%$Mpvlx0bz2;EPTcy$SdS!N20X=mNpNilluz=NW3mzMbdxC=nB1c8U^V&Z*$W^!kZ*Jr>b&7? z!jHpVOU)bJH@(aFk#maE&9&54`MGW`B%)7nc~_+dr|O^;Y*cyQOQ*^ z-*`&okoSvyd(Y`kCQsP!f?Xujg!1bYDjh|f?>oFe3} za%(*PV1JN3ytv~$m0MZz+#z4}KZsl(yq569@!Y=L*^7D;l3UArQRGGcP`!!r zg$^Os@rB8`qDTL(=y~DpjJcijqVQVs9mid_ze3OJ9{mqu-~OiXhWqag5*}XuAFL!l z4!E^_XFjgW^>J?k9+NK`2c3LdaBHR45*}V~iopHQczwvXgHt5?&L?Vz7iDMnq5Ktl zOic5lyuZp!_%33B_ct?kjkOtA(sH>W;_!yDb(^QHiy;p$=NWQ|*JnR|2|ZWL7X|-n zDeW1`+BYjt9G3{HT)$=+>z0~yy$LT&O^H-98h35+VcHT35R=e!XZoxw?_9zIw zAK#JeX1KI+VAy8QS<|+RuC{f*c2RMPhLdk%K>pIoAu2D*|APEbfcaOAeo;~PX>LC&JSICG=5{`Jv5NBTyk}tl;8@X{!2XKAgIR)GyLaK<$q|%8 zzJ7YAxZ^Ni6n>oFPxlu6LH-VwC2uesRr~fJ@_CIRULW#TdafFtEAX$}0_$VOC*3i0 zC%zqZm3paquVtTfvy?jHaN4))d40%Vv4{8f*t_aGc#@tg&LKN0uA1~(%A5?(R|Vvw zHxs>ye^8!5^Le4~4DS-&SEe~+=JoOZs^{F!Ts7Qr?8gVV?Go=R^iqozUv%q*H>gL? zcO3BgHVU4M?Z84^A7{Mojw7Ed?hhi*kRyDle0OF(!(GMw$e{ip_E$fhZT%nnc5u~j zALRWN_q+zq96s)#{p+oQ@Ga_jUG|HKYEA4mJeTxcjGOAwKXb8G_`KK? zH!8Q5a>#$&2(lYY`z!Rkn6u41Wb`I1uKpqNSH5e^#e4-GGV)h%J6F)$4iE1p!$+%9 zsPBwk>K*D2^4tzi5$F1llabtyIe`s|-)@>iM*d22)o{l-Ah>EHDKDz!+j-BR=hoV1 zyh=T=f^m8o~Bg?KVeba%#{0elAZ zCXi=fo($(j$-v2l5xUq$ta zF&7thZl`?v0=2(lZ@89k*PbinMWe|_kG$vz^3lU%(nLNl@ENcdMZTSLG6TdN2RxY@ z>ORO`0C0-#CwIgbQ2q)$WVyfMoDAl6E?l=uIipe)|g?N4V zAJlk#>;z3-L&V$y$S3^Wv)-lGcadc_6H4f=M(>mIYqzvO?2Jq z@+Rfm;qwZjyEFGvIoHR%ROYwK{MD_XeD^CNCo}kDsOk^me-Iv%i?nBe#{|4S=8K~5 zyi2^V%2lop-@$*=`)YO78sdxcz8yWUza}3uq*I>Zoaj4qUUc2&SHyh~d4`2Od=r?j z_&dn&E9tc~?M;N~_E)$M!W*v7$*>;>UVw4JkHg%LV4ANwMp;fRB%TbOE8dIFnU+O- z(Myy=ZXo{E--)Znd{J|=zbXHqR8}3HkQG!n~T>o zO)$TGp7*LeLL?N zZi(j#dr`g*O20Gq?e~e-2X5^!%D4BX=L($dv{SxKHR_II5%)s!aYK5-dDWZX{8dg_ z%H~(sMH2^-?>Ly-v1h=1wU_<}^}Pwq?RulAxjl)ow#J+Il`7b4aR4kYe4a|E{* z{XyKFk!JvB8-5)02c_Scz2VHi!rYFY*Su>#2@mgw;(rjF?V%?>Z|v%RnfRjU;~W-!6R<@6&hCQu&?zsOP2k z(Z5Z7oDm^iaeI>28;-479UkRXF#TcT)&|u-eWRlJ_!7XjpKXeEVq`pV5^(IF8J`?$x@LIy>g}yU<6Zj7D9S1%y@Q~S;D*5e$!Y)#- zuU+|Z`r7PiUV7%Z@WfeD-x(g>HK+WVswsbk9(~U|13YoRHUuBeElb`!b6tcnkh}nI z?tAO_JH)NE8^6?Tr{Xj06@BNsl#}s1?m^yg?SBw?(Vetsz#ZrNi0p*JhKb4DD>8%2 z7tAJaIJ}mIst*cY$owL;pV3_K8G`8UjJ+s)^zHHYMXrx|$aN~uAo&afg)j9Vic`d# z?OzP9CXN$%QS=8R&mBAxUjD}xhxH#B&FHy8zFm5kkiUwg?;w1s*k6HL%X?8R-yTms z`mu-nb)0SHU%|sGIos?p0S{U3+y5-rhx=fL;i&4RqBns(13y>bK+0a~a~It@OC1s> ztW`N==?yoXujKpccl8~tv42c(ie^);@0$1@e35z+=;PqI0>AxZx)0)xgMGWc@7zJp zRWb39(W4JO^I5H9(I)bkoJxJ+Pki)vuJrtN{15)p5H9u%+#hV*=Cg19@pmtjwzm%% zG~UmxMEwuKYYCqhxN7fSSa95XU*oo}bib4+V4IB;10#Vl6g`1QX53yd39AS zaf+;{M-Pul=$X>y{x%)tqt|?14~acPjqTiPjc0GxT_S!v?-|$&0FQ~xUtNp+RQTve z3jg5ituZRkP$T-z0b7ze^9@_LLUeF zt6#{&i}{N4?ci)%691~F*HZFixIehf{r8D4ioSCw<=bDQ|3U8KV1LCuuN-k7)bp>< z^TKllucd98rSYrOXDKg=J;MiLZU>)Xyzp9rf5rLsCbbuh7Tgb>uXeY$tNoSa_3?9c zUwGmc?`zy0+laS&;5XhSLXWAOJyE1`<><7?z=Zo%Yn- zXY?kNzVoKwjukH^A2wtq{D(M@1!MbDo*{(p&eFRi@6PN?4O0B8ovU|8mj(E^4W{ql z7P=2!CVo45^lOQK#olnlGMV{UDi$X{_@ zbZ}UP=X}xgx~P0!=%unJjyaH8e-M6W@EO4CgD(~L!I~qH;y!3Dc*vNq;4zW?!M4Qv zbRXniYLSU|3EbMawNK>MmOObVxW0w_&hT1-vz?=Q^c5n9tmiWzFS?{Kucr018TGt) zZkKl)c;c{cXa3bQA}2GIzJsRucKGO-`vDIx@2?h-#{_v%zB^-XzizU>LcSe-oQsB| zYA=fKAou8T$6+2adh~bdek8ADw(u_fOwZL_UC#@>3C@e6H-WqJ2L&lr$>NSPcTy?+ z52BAF`-8<}ZAITXTX2f}s7Jq;d=uDTNj^i#>JG!M)w>0!Nb@@*&%ph`wIbgx?>KxP zJZyL|xno6ca0z_}B@Y?B35_T758^;t+gLx*KBbIueLPe2K23OwXi$}=nsOq#o9!VdD7RGsjth{@|BPu&07 zUuEd}gPcQdA-}Vi@`kf76@KSX=Y1-Fg`U@H;)@O)vqX58YDG?ldtQ29>bkkEI->aqP$oz&(2Q54v7EB=`&u zg8LnrLNihD)FzlM~|M@?)DDy zn8#|kV}c(2n<6jD_d#vn-bZjhB&Ue`gRYbQ%AN^Cg)g@yN1=QcU;cGX1%x22B9~IA)%!~5A9rr=*(eqw3 zzvh%N}0t5t zdg5in$CN|f;eL7I6yZw^rTd`lP5fMZyY465`wIDXH_@9YB%X|xZ|CQVb20|<58{q9 zSNsoRf3-g0Ld0y^U)j>Wo$ol)M4kcM5B5!TtN%gVoh7#xdr{^yNZ&-1@wIiv&6~=C z4qt1?Zy7MKfO5#}T|$3Q-Unr_uSz^uRwkSx^ao4meFeTKbJdtHI_KI?XYUBE8hZ5X zo6x?4vggH~xRJhnXF7{Mj?5v~7_9~Win$-~O@Jr!w&J(zJ-i1`EUH+M*R|o_)OSV> z8P65GOGDM%uFY52Gx(B^etS)z@OcHT@u2(_czr&!XW+YYQF4~y(^U!Kag>vhe&+)v zPl`Q*g)S#kKsjXa`nX3AFTf<%9WEXsC!_bJUQ^$}8tSD=PuyX8t_s|*Oq@hLuSY4* zzQC<{A$Mc2pqfw&)XzQWwj{44nA#|fTH zF1@d0zP*$3qSCtr{uMu0=+T3-Eq(Ox<1lC2T<}G;zBA`!z(dA&5ZqeM$(X0u5~m2> z@O$F9x~e=Te8)+eu*<=%^ZARFC;W)3hUbd+3}X%j5~pY=?XMETkFDBZ*qz)JH!`G@ z<}1nTLx0eBjTQCiy$W~LT%(>B@}hee4yCyr{B}clB<-()=zmap;-=Gg5MBWAMcq0} z9135bKNaXrtP6ppWc;d{>{wenD@TCr;=L)$#ey*f99Q{EH z!|nK8#M$QW;8KylvP!cxo)Wn}nP-ss_P@|x6rMQvCa}M{+7Q~q!z=#>`Hr(IpqzRW zru{)YSIlqsQ~WF0cP<*+Ao@5OXWMPk_f8eWDU$z#HuS!N*D_GYlhNlz`G2sEINP{8 zf1mSV+A!m5sRLAh5d16tAB1;_xwXh&u^)%|qB7UlOXSRrZIk~&zB^A) zejMgN^1hw@gUHEXz6v>0+H6POaNM2S1y5#RSgr|Arja;BBgp4vOWY4r@6xA9KN{vH zJ{kGAZ(rr3=l{Xsf?MmibqT$%@IMG1GIBEQ6B^27u`>L(c9#S zLvLbr_$RBf4aKwAg~_X>OO?5BLX}Co_Y*0O)z~cToEu?4L0+ z^-<%Yv-WF8K?sPp4sZkPKjN6~i%w-(;;2l))h^`Y;4e#Z8(0|xG(9z8s9 zxa0Ve-&x)VJBfcK`3&$)V1Ff_E8Ly0Y`ZV^?eN4cJ^UN}56T=edrX)wD)(3HwS<2V z?0xZ^@C2k)!NG`uaogUI!@E8m2^=fzw#v zJv9FQkll12gdYb!dgPG7fkX}&edofl4dgM&N;qQpE6we@7tERTjZ=+*U2tkH_NYT)%L`Msqj~@K1gKB>T4=?7cY;hk1x3*Z!SKu=&p&mVa z6W}w9Q@x2wi*Mcl+q(IYR4{T1&SY6Q17qP%5`!}<@6 z<|gwM??o^9MUih}yx@x(MUVdA39pLS9{+>jGf1vlQuwE8&(Je3x=49Uuy1GX2l5Pi z1!p_Q@Ck9Y^*;I<`X9u75S$|LkilmtpyvucuNRU!Xn)1rk2eRmwORhqaWrVJpSt7x zP8`U{b8fBs?H?a!OTN^ZYR{1G{_DgS9WBj{Q=WnMSD4$uDUx}H+|jPWH^KRK z!=52f@EMpVgYO{r4B#PuYvLbdKMwkXJh$V01z#%o?bu&=>|1m^=fZo$>+`3)Xc2Kg zMI2fc6aM$mfN9JGg2a>F(U%RAu4Uw1)hHn6Gdj+(h0b z+?~Pe<2?iT4A?W^KIl3vSNKxVcjlZ7xN7JRYWu5q+}nh2Vy5EtA=hU*xA#sD*t%qI z=TYk){%G?VyiNEf*u(oxjJxO$!jHq8qHf(@6#T2(!apc`skjevZ-Twy*U7u&o#nH} z(&$HbXWVflpJDI9Ve0OTUh0_9Msj7k=j}hL4h3gm;N~GWtEkM`;Z?KNa60 zUn+X3oI^(b3cNnNuh65nrTi7{&N<4L3O+;9si3C)7GFCpn6#7T_6d}~LXRH#EA|h9 z-;Uk{IFQ}xjkIsydZCQGOU&8E`wHIhV$pYIpO=@q<4jlcRbfltfkk8MXUzBhE+RYO z6E(Nvj)VRn&+X4;o=LSd_D##o9#M3hax#+F2cOrU=da|RVLEX?;G;iId{K?F{gYvi zZf?hOb?``7`7L^1A%~3qVCPY*A8xlT9=y%v71y8rqNCaq+eA)g4c(p5cgAxiJ-qt= zLB9v?&JH2(_2@gZj~;V7a(&1lFH(MI@UODT8y@YICp^69JHx}ve&? z7ka7R#Eeh6r@P}|-wwaCet(7c75EIU)OTjTv)nVZ>3sA$k(0q4M|uIk*#=LB`-8Iu51GA|%&pxuw)emix;t}kBG%|9yi0Ehz9{Dz%0+LY zT=boB$I*K9@DDNva@pbLhC-UJnAeBigvM`g5d14cRZYP&nP*ci$!iJz)iUDAbdra+ zm^hHL4VP992;1U0d)g+!{kTKl!8-aM1ZR60dBd@9zdmt`>JNg4tnJ&Q1rHf_XZEGC zH=OT-lON!!8AOksd3}~Nx6AnoeH`o=wohN~l@k8(sw_ika(A3lNE!K^7g<{qr>N)u zAb5TM%frie=b-x0hYprF=O(V5tot7vbJ4xC$RT;c8u|{p&>bf_>XPUWG7s5La3Jx% zx=Fp%m~*eSerWG7&VJ@F-)B{Suub?T2Ie!!o|o(oN*}$FMc={{n< z$|(yz+}yB}a>xxc=Fz^L`R&=kyB4@l`r4`5Vu|7ufwRr~cFgUXFSStQkT0*a*ZFb4 z7rm&ygZx~<3jj~tm5KT8Eh2|JBk|eD5x)Huw|4)L=<@4o&*1+6S8aLJ-Na7A^GWq4 z-@$6i$*|uUd(r(R&mQuvze_#(EtKnH{~&({WgiE827l$@g?DL3%}w&rKj>YGReJ`% z`muro32!)im(U-SK6>1pUr4$~dC@Dv!`s%@ecqCC$k<=;cQCyCAoWr)w`*Lr2j_Nh zAdzo(Fn*Nw9rXv{4QIY6bBg8}+LJqkhZlQ>-OV9qj*0(4?mK@_V5IkzDM}N*3F!@It{VC{=no=?TqNcz?T!PVm)Qodlz8JSl#@Ze zy@dSE-_rkJX2Pe+6UV+(f10nfeY^CABY)LC%6e)6y{|fTIhpLywUigVRd=cQ>zu=B z4#pY6W5UnXB+*N4iT#K;+hcX_E6Eqdo&k9VQ?I3bU)`l%>YTu}#8m?alAkLJ;>l<| zFZQK!PUchcO?1YM4B1UyOAqTl!fR=oZ@+%J_f=2p<<5Id=BriVE5&?uOTDkqAH@EO zxgXDn@1Sw+e9@cGyy0(&?_hw)A%g?SJmhHMiR(vsQQQa7OT81cRhPeFKhED32NHV* za6dG@DBf4>9|W&YyE}uk4KIMkL#|ReWXm`gv2TYLpm3}my|0!Y&Mh-+evSSI;RRT{ zZvpvn%vB#p>kqOg4%`pqkinB-K7(QNCgQ5W=Y{`J$X}N1j2>?Q-A#cy4XU({y(Rza2RlRQ()HN8L7UxgO#DB(<2-UQxN<;#zl>R_Lv>s=eXg%j z$F0TOE_ukqine51q}i&wGkg>54KF2rJ9~KX9mJji&lUKhaxdza(n(x3&*Pgeyw`qb z$Rp!@hyz)ydJ_R|4^#e1@I z$hVtK)%?!je*D=T2YFGx_>$y} zhGUfL+cJIl7;n3~H~gvZJWBXdIfq=M^6i?>3;C-i^6=vB%)JTeALLvg`h&>zIgWd1 z<}l%jJ3{%Zt;$C~_~hG_Hu-}x{+{Znd=u!U)+t`!&nsOG=7!ez;vRk+% zM9(WL;Zwr|+P4=km_O;P(*XDUmLx2U4_#2Dp?*w6JK`h>i(zdP4L~B_gDNKlzY*iR?air z*Yzg)k;mi&`EjIgqDJugBxf7G3B0d12oEoM^w^7nFS<_W4fmw~LG*DXPX^yX>=|SZ znV%~!@`fYdj+_j9Uc490AA5d=tM`u)Yw3Tmd&Q>UUBvxBu1|X6O#6d!e^u&{7upry zu5vPb$KgDK><{971->ZuqGoi**-HO|*tcVUg`A8jUzGXn+@nXH0bYQeE)!jw{oao1 zr1^?-$jO4&S54j}%izwUA|1b7K38TNu)jio5ZghEso;uto;vij~??Cc*x`F?wm^e zc5oo||AVX6I_Dmw_f;$1ahQjUyK@meS9|Dx5ZsTJ(|I*Pg$s$-$KS!7*vlUKEqw|d0q0G>blj%XYj2y%OA`r-;U?% zBjcm%Tgc~yJVTZJW4gKhF!8VWxw16-N8?b{^8){>A?N4fKSZ8kvxJs(~D&%5FO<9Eh<#eN)r!Ef(0 z6x04H$jxrN*-*J>;9e^8+rfc^cgc2Ok@z3X5?nRDqKA!~bv=OTys+Rq%O^6gQgm&*Be@Y~HqP6mFQ=%~BIzxq1H zo$?ImrOMs}_nqU{N+12*8@_griaZ1M4A{4Why20hWzPQA-B)g(&KG+I$wP)00KU`^ z=MN^AE_4d%j@wP#kHqj;6P^t2IC1n`S!|5V^Qky+;^~WTcE0PdZo=xh3j_ZhGcoCo ziEl#gMY)fo&x_7#trc9gXCr4MUNL-_bSw5&&^GsL6DKM^PR~2e&pNN=ps*iTx*P6M zkA9a&*A$BND-J}0=f-79i*UdvRP+u0iq@6r(Bi&i#U z+E_nve@cniU%487CSD)B;oys+=hakvHs_PHN5!5YtkVAEsm9Lk$&Od)eT5!9_zdXd zfZtweKc_YFT=kI^cR_4_R_+!BxX^1+E%$GT`;ep4ThHt%Vl=-$Cxt zOHR?wWNYGn;D69d>_w47W`6tPti^@Jg5R#c4{kDiyegUYSMq<5bA7S&KL`)6PsRQd z&tG)w+~tr&9+R)h8-6!2ChBLuDX#f0Z&9A1`{Y>t*!}|x zT7DD%gYczlcO3BByN1|}4|XdQK6b8pCQ;>qwGhxrUCg0o%R(nma3*fYRC2!1=} zE8LxNA540{KPWjx7L;d@+>eKe&yX{^&UUuw(SIvEyt3!Tp19xXJ_x__WfPtZ_i@-i z2+p>=4_cCsK40Zz+6A8hKCc~uvkiVb=Bp{9KZqVZxV1h12OX(LALhK++ERFzJVgGg zrypnc!eNx_lf4OW)tDy(KhCJ!RcoghPb==nSnAOu-_Cnc_`Hxq{+sJ|7q7v$+bn6n>aF?~ERORmqs#=(W?0U((#ZxT1!5eeB_#6ZkFpyxQoF^RjEci^t%5ZCz^L z4sSTPYI+YZ=SBIsGVSB&xgT{jx1&ctkM7R%re#w83OpI~=#g(n{_4HvK=r=jJwp}c z83LNBExvYgC;k=s&UzocEpfI(Xn*zhb#b(3_*wZgNJ#kuZ0y*UWe^Hco+I)End^h=)GofmZ`@C zxjswcKptL|B))^_O|aMU>D-vL(~T!ntT#sF`B&^e@$AL+nSzJu`NT=JVD z@(gA_$UBZzN{x7~*b@g{AMe}2XE>a6E%sheuKNu&Um=G)K=}tR7hkWta`v@rhix;+ zmx`X(OyS4jUaIB|$9=Gu@n0e@iaY~yGR%QQFZB~cX2Ln@J2S73bI7>kF!!TL^d^w& z8>r*;F~42&TE0np2J|Lkgx6Be?f4%w&0m!+aGUgg%m__QS?KG;_G`vtr+V)5N=k|G~bBTZ?=<@>lGmpF1s! z_6&Gm-B+9<$!9>1z7KigkY`}-N0mi`)9gv*3pNrD89bREgx^`~c_D`!WAqW+TCG1A z;8qq;5?#9bzM*t=N%U^zUE==WFT!K;n#lE;&h6lAznFY~#b)8hfyZRcsYK%SNp3Cp zq9Mes?HbiBdS0Eh7nS_>TIB`!UiTdgt9+QawRm4`r@iQrq(;N*^uA(VA2{2!#BU#| z<}2Q}w<-=K{s-?1?nf$p2N!fcLwN@9MgN`hvMwjncEguEym4NMD%Zze%TQ-;amRsg zB2ak4{fO5my-Rps$-OA&uZE5BwYx(elUniupzjPm!{Um%6Hi|BAa6MMSJ+>HClemk zqX?gIUT!qm$r%MAM$Zd0d~FkNZANxjyU}z!$wTaf)L{_ZN+WiTm+T#^1>| z0d6h$SA2J#N1Sb*uUg|vLJK@Bg~tT*)iBDpTPZ$6D)Fzrb#j}uoAwOg6s=GAuecBP zBTf| zSGYTGcfS>MJ@#1AkA^vkqv?+0M0rv8abn0v&;G$BZEt8KbDan-<+ z*?(lkxz}3j6u%wbrB9QtslKzB>Ur`1U|m`R^>HpyPKJB*>|N?4u3Cvl#}w-)ENwo} zc}#4KXHv&x))lO*T6@Z`sfu#Q#S69sx3739`4dBii8q{kUg+a2Cr%OiI17)zdtn!O z!4j)O`^1)%%m*Ht7%g zsr?o5?bcTpQJ!HBaUjc*HyS=A4rKoHLBbP<{MF;)KFIqkz7G~qUerDCYthGvq5Kv1 zox9J&?+oq-INKw8_$Jtw`nupiwglx-f6zkgMIYQ>A&wzb9bGG@8Ghkpo<&e2YuW@U?QTM^hg1_jVD{vs;T{3$DJ$inwW*Zt; z+KWAdg#W?r^A^W^>5gL|eDpFe zioUbVGidKC^its;)cQC*Iosf0<)}N3)|4Mpl&N)@+@(gEEtxfU_`nlcAFq3k9*`DsxGDd$$J+HR9{}!Li`7q7FI5V|h z#(?}_mA|?=vB3Ri(5=`*NtX??$dA*1rsKFe`x&h(1P_^WGVlWU?X@`8N!*VD$}_-Y za@+7?(i!F9o!|M)#RJ4u>(=$?+3&oBdh~H#3FOBqNj9fEL&fAU=S9{wSARQQQnRSg z$0Xl=>$I8h@S;aA=XU-dbQt5OzJu`aG7lMh(HeDkM!p?;QGE`15qWswiSrj;0O$41 z!V@R=?IZq#huqx_ULSm3_Bx(StC-vI9nAC`687Jf?rP5fZ#a7NfwaHcEP4~{;YALa zbA5Y=e+BM`+>7oU+n4&z@X_OcFv_?jr9J)1wzu}ZDei;JfkfZA(@?s47kNyEjBguK z-u}*?a*BRwC~oPi@>g=cGW`x3iPv|EIFOrZZkKye^QlE1J3>3+?yY?J5g zX}O~h(7wGz{13vzJHUh|!(IU78Q8nTz6tR9mY=IT5?+3DizE5GuouPLjvoD+l)qw6 zT)W8irHQ*UbGE_1I+gOOF>38Ix}F#BMNK_%$cys6y`UyQ%vb)xmkRHamvxwPg}CFC zksk-#4{+76zgk-V}}#ezT{|T_C=%p$?(eF`Gbi2A^D=X{s)7|!@DWlCe6zDZ(Xhrd(pX*ipfVmkvQ9YA4HEHc~Njb_@^?j@JF7@OT?WMh_S4+u zV{UgN{uO+w*o(eG+*i~!2<``aEmOlkT(!wilx!*XSJ*SKe-L~IfAv4e`70mmP~wZq z|G~p_$9aeT2iquzTvakUH%@WYm{atIn6D)FgMAbKo6jJ5$h>c7zw<@Ik)%IF-}!GQ zd{NHHe3NsTJiM;N>*N1H$=S}Qo)>a`oNsreUaIsiC0C`M@@uNG_(t#<;9WXq*eG%` zlGhh;czs#A%E^Ey<3am&cmeqCtmXRfKgiy2{d@(kn%1Mo{%Vf*4-x4J$5fsH97ynF zkdvtrJ$mc3cd`c;eOc>wrlPsG;)~vmH#4{z8dp9Xmg%`*+Q!j`Y+Y&Jj`<3CQO@(MJummbv&8FTpO@@Aa~}tH zXY{<-H&H`5WaMPD`RZs=b8LIi7WdyLzC^u=K_|l~*Y^s|SLcL}9{j6Jlb)C4GqA@5 z+}hz{FZyrF^?`pC)KsDK@cuRVGsF6XAIKAjy(m0!_zrT)X&#qDxjytJ{1p#bdI5$BetT~l^C!Be z6nPZU|Ddbdi{d*t@7j-NTd6kzpBH-pBu|F(qP!Qyckn6VKq7zDb1%vqNZF&`;qoH! zWZH=X*%0FzxPETpgk2`QJ}rNRyr}-}e0Wus;B0eF2Apl?^)Y9A>6$mPmKJWSxpTVv ziZ5}B%7o7=Q@pRXio5d?n%nt4_%Fd{s5|kLdSBtWnjrR9+?#l7@LlRnc<)`hwU?ON zC9ltoax${#1up<|w$XQ%z0`jB50P&|_EH;lIb`q|_&bPRD*QO$Gf19{%!}fV!_U?C zPO~SKEyxb;q8>f-8Q`PG{~+_*nfno#(w6?)wm0^99p7}JobEV*Zo2}?qDxkH8j8q| z6XZ6Kdh}c9j>F$U&dKomO6FwH$9XW{9zc6h+;RS`<1+;6yq40Bga1Kzc(ceG{`1)u zvA^Q~LGT&aKR8)=m-IdQ5u)!by-WBG@_Z$qE6nZE!~1+vgW`)yAAPj)@FFMk>>;1} zcG|bUGkpd5=sABSb24~e{UPQnczC66qJ?^1oM&(lzEtKEd`+LIVbTa!>36%Vq1a=+?yu8 z;@IB(WuyJcw=4UJJ`SF%#^OK3{tEwtxtse+B;_da0a~$x{5QOJ{G?HK{%h_E({mgHL|Z z*g^bu^t|{!_<9dd9Q=dan_!QL|6Yq@UFX{=hm3sta`C>Z7kg2?7eMnnb55p}dK21n z1)dCisoLEc-$5_px3h;AeP;{es_8k9k|(3(WcJhD`Bmc=v~Oom9J~OWXQ+?)tKheT zt5)c;Q1FlgDBsR`(d)uv!h6vH^goEZv);oCemgkZee)mIeFu5pj=r;;ujU9o1Ls9G zt{V6ZGS7fsD(7U7zuFYsMc+Z(aWr2lc*q;bj|2ap^y7fvj=4R=tsvli+KcX3{eE;o zz%sXfKfb5DXgcw)q$loM$}^;$N~k(e@T|_~g?&5rR~oMm zcO1-DS)TK0Zg&+q8TLEFYiXzY&XXLk=sYH}Kj@_9_GsF-gC`^JgO)$sCat>oe@rgUnUq{1u+7vIVY_&N;nr zv9u}qR7%wcba#f&3-2rLrE-schet{1ZRHKeb0z0?@EMT5!oFSR+u=)nQE|3EPP$C{ zcD=_0`}W_qIIpiVTBX>e`)v&rJumh<<2xw#?Z}H7b-6z2A6y~!qL{A&>>S2yp1#U! zW%$umnZg_X{=%}!QO=7ff0bY3N4!1@W55~@@|ZARG@R!4;UVVai95C`A$*166#3VW zJ+!~%DZ1mZj~@Sn%oj!9d0yaGA}@+P19CFR^}$CE&bD>Bl}UdPy$N`i>$=SyH3OQu%52BBQ zUMl$QlGoQ^xNDM=VPC5MR$IC|v(KyQNUX>+4AgxGwLAlJ)i}=}Iosg3^W4twD|?!+ zI+O2_Ck}g2^inam!c%x8Gn^^z{%j_)AySDbJ6tcc6&*l;%G zZ^i`TKr#;*+z;Gw;2*@@xo7@rN6nJLH?o3hZs)tR{*J@n!7SozGrt{qQSJ}2FO~bw za=v0O0Ph*@sr(f@Cd`x3=Bwd?&%m4_XZjy}I&xOxCBxyQKVn;gw!8nTcrrni1M}@N zW{?-)JJFkv`z!V?&7%C3o_`fczEtMPAb+)={s)&(z8zjm^l`MF*SB=X0iR*n;Y{k| zu-_Sd=Ql*20sE^G!Tn(G5`3w6u5vGU9)F9tYWjRTxN3dH{_1>0M#3kCmy+98Y!2SF zU=HQ4mNu) zO~8+{kMix@cMdS&^=W+^?5~E0SP*CX6LrV&wQHmKs!DOykdpzo_BrA+7$&5O=L$YA zc$YN4vp(Oh_X02vS^82BE3O)Q0UkXWR5>8Of5r^zUHUeqIh1^yNK&hUnBzEDm*uVA+w z0q?2);4aa3-rJsg!Grc!W*a!yr_Z-@ABQ>HFC<&VZ3y1GVCJO%IK6KX)MTXh)!59h zQmw^v1rFrC=3c^Ml21MQxrU26e!Kg%|DL^DcY(OI((f#L^s<*KxwY&C;O7cF8ND~0 z`B$9l%M;!u_Eg2bvc%3mQbioK}z zz5@4ypQ~oUDU!TC75W zI#+BWkI8q+3&1}5-n#GL9?G|O3O^3-8IY6V?;y`t(!&cc0G=!G`jC_PUHF}Q&h2ik z%gGxKU+Mv|zY5&jany?XINNEy0uR}=mx{gU0nvBi5^8!~vjuUlC_naW@75;;hdE6EpqEwz6}|NLb%U-5Ib zl{_ZQZ#Vr8O79ZiodfB8rT2M7^w^6cFFHo-uh5&ob0zuh`W*6+RoT=Zy{Y(I5zq3-THo_t>1 z^OD?JvtyD|#J!2w^2#G|=Vr9l*^f|t9C%D_3U4^PmL~)Ul5@zCtA_p6N46QG=L`N7 zKUYPe1s>K@+eh`c*-g*YqeYvud+EI4k}t}8QS7ha;l*1X_p+)U^7v50Z5Aa3Th`nfGOMk&<_^;q>gImj-B5)v$ zDksy2yy3jR+TGlb`0dhb>2CP%%Av|fk6tQr$lv6AOm`f)zmnWqueo8! z?Dsdsu4czw0Tk?Whc#Fkf+Ck}ZA@EM+sd^PbZ^_}rQcy;0=<+ZfU zAC&P*>SM<0wEyP(P~1jbHSm!6j&q6fqS!N(cyx;UV9=R=)($P&oZXx9?avXvJ?T_% z(>{yyPOp=*569-aq)sVjepW*u%vx)oh55r%Q&En8^*7)t{d4U6IrQTQ4H(@6D3|(}0MxFut zcFr@j)cs6(2KXlWi~JROsV@;%P5uwM(sMOn=CE-eid-M&cDZK=+UhIrIPAy4{)*=- z_F6Lc1GzqL-F=Y#IM|CmnCn9?mG3y=>p6xl9JiOq36p9|bwaxp~n{W_wJA7U>1*@qyvESl5 zr`gnZ*1VRy7wxP1gYY}Y8oi18;YGPV?$HO)dV1Xh z>O_)?J<{WEUuw@&+e@cRCY zt0wcJ7gjzZ{DbU?178$(9GP#&zTNbFrS<3sh&@Bhx&230l>fTLd3}wp?~EQjJiL>{ z-I?ch<}k>XQOc4Kr{2dIW`=G`t!hP^_n%n!>bd9o@T1b0Q_y=*vL9TDZ=ud24 zyVi8Jg*e-s>yvqglZ{>7f1{ktouF&fADor=OymgP0W*iI?;v|4DQpiJRcT!=l1yRp}RfWr&vAF+h$Mm zGMzVE?;otB?;v|E;Y-aRujMx?*B3_lcJSMo1Bt!p7V^YBJl@Z(G@xAMWb_^rEq}#* zoYc+PWy=o#)=<#Wf8Y+fMLmzO9nD(80g zh8H=^>wNCw{u6!`%kwPN`wD$$Kfz~^T(wPxoyk@vygv3Wc~TA;IhlaM?c(k%c{1F` z!FLdxq9MWyfIg1=4r&}o>=_JVzS8rMw^QF49ur@&zp6BjTL0^oSjGK-$Hdupw8&q9 z*T?%SdF1Sm4DHEX@p+-89$mt_By)Wlw-#Q2rGLWf!}|(% zXYQpgBfm3zsmL=lk=K%WGX06qzYsvX5dB;f>yuJXtJ2!mmpF6a_WHj}0z}dzf2b}G> zoy88Tg@5n`;?~AQUG#g!HP6M9INKJ-{7v##@_mIo1M~XOAH<&FmUynPXMh*LE`MOg ztBR`z{~)|e;Ps(L|Fg*TAukG_m&UCH54pGS@X9;`_zW_C1rDU%KlrQS6s;is75)eL zeFgs@`*DzGD5Sh-u;`_}d*Kb@Gk~+b(l}+^I^kV{7eMCQ*C(7XJfCbOJSMysaC8JRFu-B4(UYuu`EVv)c7X=S_&4ld^Z+4DW`F7?M$^Dh=58{pkKTa9lapb-o-$Bl| z`>VN~=PNJkaKUG=q`fG7sqmO^k3OJ&4DlIOh`cEB?L1#0-)@|{K=cRilXr>pqR1h0 zk6!Yxexv`v7@cnd-f-mlZi>9XJVyL&efXS=Y+9p`@fk<{!_yP~2`dwBPt;ZUq4qf+xe@!GY8t z+)0;dr>Q6-?WX{BZ()2 z@1W!%<9)^X_D!C1r)7>lWII#MSL~a3IKw7C+=Q!kOZ*SYzO$C=V_qNoCXi<^J7ytr zec-o;)1CqEEB+1^JIw8Tmbf1=d1jPv$NwPuIPA3?D!zk2} zo*_v1yf}x9{M8=HA%g>n`3gOH2jYIfH*uh3bZ#v1WGpvE=XsHLiT9$Z#3_0qsoij& z{s&)jEl~YIaMf(nyNw@`Zz59kCfExwQuv+u?hHPI6y=Jor*HEZPToXhm8Nh6v~T!=j3LR>*N2y{IUI2FO|JZa=yZS5O-(v2jLChwYtkt zw0ci;k>Iz(6X$vS&3(UayPF=E5@~!@|TVjXd1`6q0e2kJ95Q+n;K{_VooYO-=BqIm-{{OIP7!+af2DnU1aWJT z>%;rX;+T){T5^97d64JRJ*Q;}}OLuNjM#_M~#vQNG}eFr~G`%ZZ+57_3=|6otA zCG)SAo;g-KvM4jVPg*Y>uP;DwAUQ8OKKZy|UBZtMuEM*7{vdo_Yv_*SlVY~v=WRaw z<{y9eLW#)r!Iz5O#4hqK1=9PfO!?^Vr!NuD)yA@j!|t5G2mFIF zhdjyks(4=^*M}axVL~DGo#DrkJedUI)@oi$_$CbDhiGosb0FEn>qGrPbEBW=rCui< zGWvsi77h)uiQl2_&fLejO?d|JWDr^xLAucdY$L|3UDtkn1Z_erGwi-yc%ezLomUm$r3Lj~;V-Mp>vXf92+VA!422 zY|9?~K8s~diIl&3f$|L2X}Q#!Sazngd4Ns(D66SEs5fD5m__+^}C7JMM#$ zQ^da1Sn(YUAP+A%+p%5=;h(KaH|$O}iyuyRoCs%6>)uzdpDwHk5I(P^^c~zzo;dVU z*_VpBJ%ip?%&qmayL-dC-sRByC1Y~q*1l{!o6^2vW!_sAb%K8-b25L`<@z*F9Q(X1 zQx1w;Up4V$YVAi--+6T8?8HXO^|6O{GV$Bdn?N6j^Hc46gjvnK7*O?r9P8eQ8Mn35Al#4#%vS(c5u}!sE-4#8t>cX+-@d3aYfXl zUrhZ$@EPQuA<=~UG3HP;eFtCBaf=_2UI6w@)EtRA zH@mf(yh~0x9x~qtk&`*uIEcQ3xDPrR>w4tdv1hO+|6muruZlg)h|hrjAoskiDBmvk zqUhtuI}YdD`F{{RWV?Yo#s6T+<{8A3f!ESg?AyC&&%l1?Ek~es9C8Nj8NOG2=Ss?7;qHw6)p7EeF!uwTBDrT67`EAS z_O$HLAKA{j)^zqJdBgD?d@a>3V_<$z58njtIC?$<@}kkyAN*b22LtIo$nUFsdS69G z-6KB3Hzxcm_~^kYIz@X??LHV}_b}zJOgTjrimQg_%JYBZ`Y^XkA3gS0oRi`2Ab2vE zuh2`y9fxx==;I*Yj{H?S<&YcwrYa7k{2w$=IjGCG&up!+e~i9^V+9WxIb{A0nw$MY zmuGM?9(*8&oJIVrxrUD9dz9-#{;Hqgw{w3G-X-R2r<@9CsP_LzYCVXouW;<*B+Nb{viUSDLM#m3VqQx(5GK=hrFXE25zT9qU2 zIFf&5rMR^|YpjeuYZhnu73SC6I&FT{hxVd-$YWAU{42Q^<$U|IhdwA7LwwOMRF8hl z#f8M{Gb&#yzppS~$$h);-XQYC9V8AUdR{T-X3_gf^QEHijOU8;40x{aKgi!f%0eUzMb!b@H-GS+*-M3fNuicaO9A=kHdZEi^4zHvqx`cHp#L1Pq`oLcShg&8gU@O zZ%6)0_MLaQyyW@|Jy#uu=V`uLBXY>hlL2QNygtlV@GfcdmGqb}zg_Ql*1X}^xBF2J z+4`zC%~!kWe~_Q6MCy4xO8Iub`e)T%^xr968&>H0I7YF*0-BoSKXGoq z|KW9ey0EJ6Cc?{MWIMgDnEL@Q z0OuKU%I6BNCHPl9)bj$r9sR+w8D67*a@}baUY~l*ShY*XlfizlbC3Q(>NM)(tnz(l z{Hh_J6ZZo-nbYE2Nxw7vIOt8tJ&xpy%DpqbgXm509;b`w<5-@()9f$&&hz71lbcn( z9llia`4#8-_7J~adQ9ZGg2x2jCH7i&CI6s*u^)X0d45$y`73|LXRy@e`aYV_SM71C z1qTv6FLO>&y4nwNFO_*RP0iNCt%Vl==jtiJ7nR;6M~?yI^SVfLQJ%LWepU8Q-u>a9)AgZmOOD^ zrTjsD=d12xT-rL0)j1uFqTa;BG-uVD;2bjhykwpMa|ZP2!717*(P9 zAUH+L>(e+zp5)<0-}x!>0(7Omv)tpL=Y<|UINL9$e4j8q%;25YDc}Ad@=g3Dr@Cwy z`JLfig3k-S)U}2(F=zN#%&)L_mh-EyWxnJExK4Rd><71+an;yIAMDUf=SsfYkI}q6 zQ2235#Qf?znqO&r2J>?T9`e&GudYwpw)5b^Gm}J*9v%}9@}+{at-r6{Cr**MhZp>I z&R=29Fxg;Xz_~(iqD}Y*|3Nuqb1zkL zKhX2)R``UD-_AXH&NF0*`4!Ja_4m~`Dlht7+AzxXVa~wwcFaY!`wBT3^ip|`!*kI@ z#Z{X{`F7^@m5KX`Iosf0wGdYgJ+FrR`x#-T7^4UIog;|PAi1?X7ad7n%TnUEqmN@C zUf+wvt?fd-iGNVvnYka}A#3xi9N($qGlzU8czy7g)T9qIzLoa)8pp!WN|&Qw)!AB% za{1GJz0dvd-xUYamGbSi!n-t|_@bCIfU_-gGUyME7T-bH^Wq#b`h&d3kv@9nUj?1I zdn1^7UXtIAyr|?Mqvv(&_(s+90{<#lcmX)ifcuKy?FZ>Qm?yZk{JzrWSMXZyQTg^X z@9AMD65Lao3~kBt)%&Wc+43y*gE&`KG{5>o*T-=g?%?4T_)M&K@;SrTDZkTq@EZLO zGN(x6U-7(M&u3t+n(TRLejM~B)_3xy!tabZL+Oll5f>!Vc{=UMw;yi=oexNsjc{{(a z*gps#JbNUddHM&r9>Ac7C_Bj~?^( z4bIC)rFhR!ejLmhz^%o+ojFAzd0jKCO?!0ncKDr9Q$$bZpVI*?<@3Dw-Wb*dB`%?C;9EG z%hM^>haUY!%E|EFxhL@%w$gWybA7>HPdwxwl(|0iQj6(3DEl~vMc=thc;d`CkjyCp zza72_a3HaF=A2BOm|r2+=Xw3o$(v@pKKu`Y*9X4nRrfq`UtKVGB)ukb&}?{@SiUiZ8tI7Kg~oDzFy$^DqUGg0&h;kD%dLCIBH=(m)5Uhw0% z&f7e5*O>R+0x2i6k$5s%o?(;Xejq0!^9*II(TWS4`pVQu1=K9tQ@geR9 z=lWWS&wxG7W8~q*`|86iYw}uZeH_Ve&mx}}a>$x@NpgzdF+mPl?s3rblHWo00^nS2 zR&&vO^6Dk^VFk1 zP2MH$Q6UC&GFrApsKTwHTTm1}?KQ22&21=k0-%XW;!{Ddl9AL@%T~ z19OVNfrQ6|Igl|0K|6P=zB79C|D|3kyy5jhuX}zp!oOEzv+dbd;SOe@LFO&_#5ThF&D-BY8~|^@NU1GR6_Svhv-e{G=+YHaqKT;nD@2g#ctA_m`=IxQ>U1}5OiuX9bF6u*k zhRh**iCcS@`p%eNf!8PJSNI=<*Ank5<`m(+>M71uf|@hv_c+{lE)slE^ypt!{HtQ+ zcSa8RpQg8riNwEpc7OP(#v5S{y(!PY|ATz4G@gv)i-s<7@hj|n(M z$hYI&&UsPpr7}+jUI6|Mrh5Abz9_tw;K^)IJ$lR;kQc2H-f-q2JWw@KPDY8^~c;N-W`^tRp%=s(ikiqNY z-o(!|XV|Fn?cj^TKR8Et!`~sU8s=AlURy)9#BW~OMtf)U2c3vpn@il5UN zJMyCF(aXIv=a7f5J)UN7v`o*>>Q(YB?VXuhi+THfnv2#idOT`9`3K>NJAd+S?YYu3 z*|o%Pmpw1=WJZ(U8FNvY>w8-Ho%P%g@cNKvuspP@dtE;Ht5ADb%YZ z36R**^!u$$+QO@;sr``nk4EP`X)2#1|bJaw1 z2JTJZef3wxLzdq`+*dePZ_xjs=9}m{G;hKJ>P^5$kNg#Tcs2gjd-QG(qrG!+^$qe5 zMv1v7_@b-DyM4oi-a|vpyq4pQ|57<*%&+inH+avWx#&I7n~*(v?mHvT@Nj<+-$B`< zKalcgVsm(&&mZpY#1~~AGX4h#7(b()7r)#05eE|QtBaIp;B&=!2EMP5ld(zfX4W6v zCH@D|o8Z0k4>9lhpP0CHn62X$+T-wk5Z-Xf{Q$R?`B$YOJLp_-9|yd?_C96gF@blf zP5HcVU;Tc%u(79O!LVN@PVqnc&|GwvRfzBc@SH*GJ8u*3cKC7hJeg_ZGX?(&eP`^Q zC7%I#(XZ=lEZkjw7o4JlDd!Dd!k7Bg@I6i*O$&sF*G+jXdCq{msN4^>CEqoSQoht; z=NWC!o!@)Jk378pQTcYfufVP4zBBWXF>lAaoqJx=8~$Equ)42Gs&5>(q332labr=C`xo8FL2d zAJp>gk(Kr|7e(LsboK?BGr%`to|8cz2f4nEL3VZ(%IC%ZgMSs=5A+9r)bWtf^Wr%J z&qc4EjySNc+>kqIMJ&x3c#o4W?kjjqwyK_2X-Ek1+qbo>KR5HRf5ion>zkJM)nH8{bOnDfn04)Y({!a=GrFPdVg$ zDGj=LJ9sjQ%4-S!6~2SW$=IB|)9gz*8Qu?KE_#yoIO(G{IKLzKSJG>lxcs@C%CZ;s z2b0Hy`B%tatuqv-c1`LtuVUtoF;Q*-ww4XI)%%J$MRSTv={v~&!H`S?mcq@E^&oy^=;yBl*e_z21P)AAJ$p=eKe*1T(Z}&Fp=Kv;MOt^8Toehd4bO$^P=Ai&Nh1V z!_HS9@u!{_^N^+2vYC4H@B*Ypf3`T+ural3Qa{Bh;vPNn4Cv7Z3jg2@)yLueU}qOsK;~`gB zopSXi|DeVxl6xHXT7px={Pxz2i}?W+K8HU#SKiXn=gE-)URwoM4f%H74?gI#js6E` zS6s}uST)C#WE`hFyqg>C97~6to;cP2|6*1Ur-=8?lFz_%hT;G7e-L{d$teO?4Y@x2 z58kZ3L_8U1;k9JGDCe&VeQt)|Nvut|YM7AxVytW6V;*k9cR6{{-Z^&L&Gr3P?j!F~ z2>A!wf4NWmcJtnZ7kT1JoxR#doZopQM9r^~D2ELH;2rXL!3!X{A2vz-=WV4Nvg{8A z(EJMLN^;fE^8!x>^LF?LabHOvJ?2-zr=Ax+uQ!bU7P-E?M}|_Kf%)z5QmkSpqJ=PVBWrpa>zgH`h!{J#@tCO<{Lw)NAD~6qAk>S=6pNmSIjATVq}Z( zTKduZD%ccDz6s7D7dJXM7L$kfOw8IP2Su)r{|9%H7XUslysx^d{vf>J$jNk!m3h%= zhF=%;qMp|}xKBkHI3>hjS(0R|gkosyV|aV(PTJ!+q!J!sjLX&XOl%CH&6thGX6yy=~vYs53rIJDmm( z@9HsF_;HYvxf@QnM<>&0EdA?*G*XDmmGaSi7{13v96Hom?;cXF|BJ&?5mosF?VZ8DLSFP%;%6yWly?a}dOla+iw-33(qkC`dEqx`8ZH~wx>%9QXDY7QMD8H}3Rm-Mx z6>pYrmp!lJ&vFb|pMSKJ?j&+CeKxBo!?!8P>00-ph# zqMsD^W4G`EuqW<_>ZKyz{wV(-?{UBv)$a%4OU2&#ntPs4Gv!4u873wVkL@4W(_;Yf z8RE|DIT%ZQoNDq-@ZOnw^vH`M&+veH6YTSX7hp!)voybg-&y1J!E1?g#U2v_<&Zab z%E`cM2~S+$G8?1+GXKm#^2Av;_}R{P+dgIo^}N7C=KK}kS53mZ^juD&n2RFcF1<_e zO~4y2`3&#^z(3ebejISsxJS>uRL-|oIXykxfw;AACtspG!`)7KQHR3E1z(i;4ChWx zBfm56op~<$fO-?M=M{3|+nPQqfAzxJ-9;(N!#izs>5QC+)&;MoenEa5AOBOjzOyzL zMQ>sb-B-wq_RyWH;-%ZkH{nD%8SDo;&qet=sCmQJF8PAy?K$KzIZgLfn&^3bmu6$^ zmY$h4P~BJ7#J?(vY@vDk1?8LI9zFLa_6q(LxN6Qv57l+C_?ye0;(aB#AG{xgHyq~* zd2F=aDd=UF6e5#Dg#JBK zXW2`=7&MmrIGA670~wlUmtkRIp3HpV4VOIRVX8OLqkGQqG$C3H= zwnuyi;q&627v`e&g&$B(2EEh<_YLH=WPbZ|u}_oVS@xadxBX7}cKA|5^MZC*Qf~tJ ztFfMS*G8FbGG55$9)C9Xc+bA{W1f| z8*U|XGBV%ZnJ0rh19*K&%f|~}Dm*6o-oz=9lZmGLO8yVp915Uwg?BslgYn|N+T%1p z_`E!m&kIl7hvau|@A#H_^wKw>?+?O9kNe7r-tF9@N3IX^EACBjPNs|T1m$E>sXth0 zb(-ew>mphg{5|!6VXeqBaF70q;B3o$`<486D|}VHJxF;0ik9w(FAE9u>OJyqpO49p zgZm2H5820o7XX|h^yt}_%5#Qw5v4PHM*r6=hy0&3TVuEMEZPqu*9R|v1?7UtC0AIvh8>Nt?n3lJuJUJ=V?WX>)AsQM0hcrj<# zHnTs?ukgOY{3>7PwX7y@`0M2J0#63KKI{jXtA^eL_Jhca%Ke~)$TQeeZvy?n_-)mq z=k@gP?jG#NsW*I)ay_v*{6n8V-AB3DT6|sCPv?olTomv2-%k2m-)+Bsh{yOmUx%pr zMV{1mX73U_CR+txG%Kqc@vqPyG*vA<7E%9+)fwW}!V`!8LGID>{AyF<<sr+NEE;o${OW-FbmQu4&1?~HtVfJ4iTIj8=*|I>&3IN-Ol zCl32T{tluy!RLzKS2BOaIb`&m6VJTew9mzzm@^>H zz}}@zbgtN!3Qo~Gv>%i{ue98?=AxK0aK0VBROZ&=T=DzL zQ^zTioNfLeM6M70!HCMfg0s!}cK9YP5C;;RBKYVhHzks9LUPq)Z=!Qf2Am?yua9EzUaeTwe4f#+-BL@HC#WwN%aRMukRN5CXi>?KC|Dv zu1Q6yxrWcxydAyNkW~wuqh==SB5i0RM_Q+B?S?C#^7u z9zEvm$jP9WD*MiBh}Xv)NQ3CnA17X4ZkDt9AG8|WHg@x@;>hMCOM?e-Yo&)*a%&~8 zkLT^ZkN!h>mu?gP>Wbm*t(E>k_)?FX-cZ~R@Q}HW6Q_J$mbyFx?*}nwkoiqF6~o_zH94%Ub{1HaqviTxnnSKt)k-L7%Ahn?SZ#Bb+< zf;RGoBQIK0HiZ0xArAe9=1oZSONlWe< zOqhQaRcWW@?K0OV?<>wd{*svJ#%S*E}1@yL8v^3VAJ! zf-frn2Qj~rUV!Ile#Kr(eqUuNFF?hZNVg!{jw?;a3#!9(ygu{?uOIK$@SgHoqCW_3 zEqW6lPUuH-hOkpZ_g9rYmy@vkO_A&4KF;*ELg(~R29l_Z53g zGAV})?gxCS$RYFls)POqr;Yx>^;4^e`X$GfS5*|fy!KyWkE3xv*b^svUKiE7eV^!g z)lnaZ-&gR4_f{f^01s?i}Ah`F6}1LYMhw2GM;b`Bx9~8T!z7 zFjV9jB)2xUAc%Ti^4-pUXMJAOmv}O5U(Ma&pVvL3-MG)pKUhP49P|gdHvvzaJXilv zoFZ@_YsrsuxAy!=uj{px7tNvk6>}hwzp@_JMto73Z-+0Hy-V;~2G&x17}@?@}ghSw52WW3wq^Xm8o`73ZgIDf@lHFyC` zqY9m8(!3r2gUs1RPG%2%2ay+Lz9{md?4y5}`{7HRZS zIFcs=9x{4f|1x!~T2SD-v-Zf)^V4)*fS1X;#C@Cs>ZRJAZTl*CM|hr1Mi)~Rd3g1{ z3CtN@h@D6t-dl-x$Yb(%v3GvI^6^54H6CewjbEgn%&r%HXPFn3@2dxtXJEc4bJaM{ zz&shPmnw68a?SuRz!~b}lr~;G9eZG9`AXV5gR?F3?dT7(7eMCv;NcAwc~RNNL607s zZT2<1hQlVKk{=2z^S zkbRt+$4jc`5TC)u7`SXU-B&ls=Y>5EbBZ{BC3!M9SHZ+pdzSV%;B3c={UGM;@TJQA zV7u@x$zCe==xsA>P1Up?gx7KbaUh>kTs6Gg|D-(*^BFL2*L%a^F+slFOK{cTqrb4o zlYI0g;(rkHcFDg&&+EAI(VO=NUl#K#T$a;5@^lJiJ?lkKUGgsqpZ^3lOr*S9rs_HoRvW>$XF9 zEw^;q4{Drk>~WB9Ur_Mg&V5IoCGV2vqnF>o*J$2;U-%}1^SaUd3Uh|&ZF@!ES)c3s zlFk+PCaf)v2p%$fc)3U4SbLFtshv4RoM!;12>*je6!+tV;6N^;@8Exxk6!lZr7zW- z`+?qszDJL91zsOKCYWEL=fyl3y(dockj-;4oa=K>ZC#K<^Y-sutF40TjVdQ&C;kWj zNxVMk^D2$JOW#5C=$WeqKaS)fgMS5XEq@2um&$ntkl9P-0GFV6KX@LL@H_2N~AEveSxe-Qma z_V9*~cL_Z&%Ku(VGY&Z#esLPMF$OEg$Tsp# zfCE{d-<}aberNDS-zPo;<_tefobLZ)OeXD}^&H5#^(kV0g}JCL&9A9v$RnGF+S{Y;9hHbCW&v3G{wc^37&a9;(JAIG}EU(H3q>&r46 zT%1h*gU)oWf~l8^?;ze+?6uq@JaGoWLk6GWK5@3kcoqr&AohdEUvaJvdz{?|b(a$V}gA9rIR;mFO{Ck-k1KAaco-eHFkxO!Z*RaRQVkQ2U71JWWO``?VK0Y=2zg> z;yc(*bA}Uw1IbE7+&s+E#|@Z*`=$3(mNlg9)eUiNu0R}Fm}J-3$c ztIyTDJw*7O*<*saD0{>A9eL*bOyX>t=VahZ?cDR?9{o+qGsF_F?=QpcsXu6*Zy&AW zw?84y)%V$#OaH9BeA46ke&r3vTvX26t%zF-uO;&t*b}GaMJ2x-{XyOjo{jPL|6$_h zVO<@!G+v_ppyma@ef6%$^*K?`3%R~Fl|yzEJ^J_PeTBIw{s&FD+2s-BAMD{+C^(Qj zZBo)_L%=+X1J>SAX#xMM8y`s7?R$9Ky3Y&uu$iTjCi$n2ws#{`}@ z<`f~x1)rC#;1t0d zj`>xl;q%4D=vcoE6Z)t<4(3I&iUjYv?d-h z&)aK^1F1IwFMucYywD$fmimLtft0-o=3hNl*poPr2k3wBW^J9Ai%uDzEj)32UxBM8 zJ#k-?Zvr0P?*y+e_1N6{PpwXi-URn?GRV91^TdsUQv|*!JSLp$tIvP0!uN14c}#kY z45j-D+>gSgpTrldoQ%wiKFA0%B^uvYu`+kH=y~zHeVYHzF{_D(+(!8;^am@I&r5r^ z%bd((cCCY~$-9KPDDtAr*=GMB@12qB8)$q}@Z04c=XcTb()@$ni7)C!J$lYx)#&)5 z9lzXDJem2_OO^gX=JgFFZY}2R8n>4DqCu4FOC;{cqr6M(4M#7P{W#zh;X8;t1NMV@ zzGyz}oxx{#e*d1bzle7`_q_N!$R1wg`bKv00&veOguLOBf0dyA2i=C-c?=4Cmh$b= z!+Y6%tV^5D@5~-v_Qc`cj{6FnqMlLpi#!aLhNh&=kwvp?XnwVweDr-v^0O?`yBohv zdwK0AMayX3j&lXR=sJ-X#kpcHKv&0d@&bT=g&w{1hR-^jb8frn-|_=1rXS8Xn_n>xc}M&v@>*K< z-PBTeZrb6%iVOJ`tNiKRzC3pg<@z+fs5xhw{W#(EDaTB-$N5*9ov}-LR+cMy0k(;` zDCZekJMD4sKL~E^&)Iv@pZfpu8QcQxb^SqbYX`2ZHFg*u%LvR1+tE&(qRzR#A4Go; z9^My(C(dFub8DZ<`K)X(?FV_z5bu{tT(y$aE@pjadO03fu7udmb=x&& z%S;#Lqv!k;bBaP~kArvn1mm}~cLuk1`1z@AB{aW+C+=nPrN+ly40@e7MUU#G;#|d^ znc7sTdtb5F68!e7f`5fxDtZ&+(w?TA4DzDCot$=kkA2RNDY~2td|vntvTq{v#6PKz z(}m6zd=n|;4Tl#1-f-lv^m7JqKZ+Z#osKw=U2e*Ki@t+xB7Y^hY9U^w5uKKZu?e=Ix)S_ccyV>#pVu$X}u7rRA@X7wzuhO1%kiYZJw};{G7> zMelavY|HN;JiOBH99v+y`rnin9h|efY?#Pj^&Yx`@(f=r&J^4acub^siFth??86C#YjLwvksW}*@d=qZE?;v={*W6!oY3uk`UC*P@mEFYq>T~j1 z-lF{=c*s8E*9t$*1K|Z|A8Rq#-fru)dCD7ZCpg>2suargF%KF1tG^MS0XZ4Yw{IdJ zeP(%j?i(wTjB`vK85hatl}CMN=C|{_9ero;WZ;{SeH_W_o3J7+H~XPJ4tf*p9|WJ_ zG`+7<>Ao7#iO(Rt0PJ1D-uXA`rE0#^Teas!PKI-RS`PUN&D(F#`^qDEn0mKM9`e=o ze_2^+v@=;}gy#kCX#1+m*;~#2y*?T-*7G8HEhT3gy;S)>=)W_LxV2-+KZqQ1=&9x# z{thlfb7?<#P`$6VQ7;vJ=NrdMssoCB)qXHg_?-i7=TWW?oTB2?9LirU^vj*lpZbHi zuXZaAWc#Y5f>}F1JMzr=ciRe`(?*%Ry~BP;_-jhLp(!~rt}bW{%|-7uTT*{e&lio| zR&#K^y00WxP4Yz>$P=dj95Pkpkf$12$qT@F2F~?;T{EC0H_Ix$ zyYXn+ORDFE|3S{lw5i_2G{HlbJ^B^7ZxC0_a@B==|B4ypiRJYL29;-cc;5aa^_`a@%&#UVKO6fOmA{H7J_FC&Elu5MeuaEHax%pu z+&!-aeGu2Cczw)6<{UCOkl^*bZH!w!G-nsR+wa}*cNj4AL(0kEzLGiQ9W)n}ob5T} zqerff`_2{(fuirsoNeUW7e_Dl%b74>Xb{cYr7v}?$RV@Oi@gBp-rlN@1OC-m@_At{ zD)U#|AG|Kkm6y&p0Y47-qUht`zH0B_xhUpWZ;IYTUFpr*`jeBce__9B$aK1|IDf@F z8TJCmdAponao?Fey!=0?@kN=}S2XN#$Bm6wiQkU7XuQ}D!atZtTs7u?NH4%A@f$-T zXf9e#^LEK+SZEx-B3NS(FCD=ybn-?>hCOwb?H=VT;LhUZtv^?_3aFTle- zFZ>Vk-Wl(!rOThn*;_VL_?`QxxoD1|IJFCLKj0tqSKL}(!6{-s1Nc|q$!r_*$;>|U zy3l?Qy;SrE!6~|TBkFZ|AuQ)Hz16>`Y;RgWG$FX`c(OFl1i4y5EE zOJAzyiL0@1)+%o}@>lDIOjXz5)lbRmW9BAE%W1gWQ`~wd9a4-;VcHsBS;V z{42h%G|u)$;fcfEnLWJb`F1(K`hfBbEgf&TTyo#&b2I#Q;%6zB4IatQ(Ya#(pnSI@ z&tPM+&Uio1e}|Re_31g=`aFZ?;kE3gkOQH{{IfLY?anB3ARB(#?mj!0d zE-tJ7ojfKnZWUw7XAV@kKK>3$AN{jp-Y&m`((jBs1JB!EPH8pVO-_uv5cE3b8Je4| z&$fOQz9W$OIOcOvcrEQc{-X2nveyzGUieax7wxrXT-sCAqX(bi!pSM*clIH#rOY9l z`*FUe9=%<97kalNCu1Ow3HmtY;=YnxHJR&cP`!x+@_F&Q9sh&O7u^)HJ-(E3$jFPr zkMn?h6SEHcRn$>$0-iYJ8L)RQc|=}xb;OPt9;45=ergq}dZ{NUhs^o*xhJ;Meo)`@ z`jyU=*Z7X6_vGzFV-GxAVDz z*HZ4CeaVlbd3X4JdyIE%-R0+p}sTvgOb;$<=er5v^O40 z8?km*Q3~w`cN71Lef0Prgl_`5KKA2ia|Y%>;ycK>KAfv?P z;@0Mm@b7h7dkmkALKa$`ZzM*&ilb{D1X)6u5FMN^-?)6T4#2y z^gJ2!-oz{9$GI1NUAM<^@EAyaXPN5*_k%sW;9tpn`xwv5<8s{5IryWogM7T1^){FgUBKC`wG5P z^qn=2$(M$;OMZ%(PVXz3XOP?vczAg~m~(DhORMl&ei~n}w8OArX@$s(-WOf~Kbnj3 zynO}j2l;))`Svq0YsinoJ^INHeFwW3*}H`MN}G$y-URsVYn0cLJtp9{gD3Oknr?;j z#Jl}9nqM`B-x57~?xphoAkI|+`6jB3)+Rf#$FVxwOnn@1)zI^5HvCQWozWk}oT2JS z;Lf-Li`6Ggqjh`d#v9=dz39F&M1Q_G+psCMOH$u?6)MkgOT62IM7~|lui$syq05UR zf0f|3VZxI`=Q`ZHG56GSk+M_weK7xJQ#-_CvK*F1|y%;w!yHH(i?8dP)&C&)44Q9(d-UKGU7~zDzJuIL zwO4Zn^l`9vetzv9;dkcwRXODuc;1ejOy}>Q-Zud+Kv_$hSzdIL@LFoz56N#|K=bxB zy50omMR_jTz5sj%bMMj%Yrj+TtFNh-iu=lP9P``3frKY6*Vl9WI{F`!IT`#9V(;8} ze#QLui-yT!-u~3^YVy&8Cxd)@wXv(oGGkue9D29k6?PyNEo4^wDS0oFUGyaKe*A z!yRsmUg|TdHxXA5pnCNBIRp6ZmuQdUOYNq8`Z$(mIhk->PDVe!V*VBLMUm^n{7QPm$59R$Ts3&&yxTTACy!d`?HP7D zVMI!+;34CG@ZDZ5&F#Nf9SYhJLVg_dyyUs^B2E$W+i_npzn!@s?8jlw_V~1cYTmB* z#5oDSGxsLs`zk{H51QXsntzac6PRBi&#;Mlsj^3}-B+By>dYyUJ$ji#eoy%a=Tm=h zUHRMt*H3R~?BQ4>@>krOuviHGmB;9xT|co37rqJZrFKcr&T^sqYP;Aw^SoVp0oM6W z8J|0(+TL5duTGosuX-K*mV92EZ-;jYp19|T1BtvSyZ~CR5A*imyby7&(3_B6%X@~A ziieE6s5Tb`SB-fxS}*lO;q$_G&{pvoBxgHR^ao|n3%ow&Uum3e^l@T|ha5^h`sauz zgM2%B6YTQ>UzFzzQ^~{oz;G`)o;cew&j61J-&Zxnfm}$wiHS|MPA=q$gKvVrgXrV@ z?*6Li<6w`|L;2{LvyJ{>j^Hy$F97ZV0?)7}~RE9O8pOe=tI3Whinu4cI*dbzCG{U4C>KqJY;+a_5XusX@12% zuPus)jQt>Tef&SjejIRXyW6!6vKd!2YqOfSV=g*ne2(y#+@U$cUeO=yQ5a7BK?@7? zCNO7U4kYtMWzP#7Nbp6Oe+BLbJSG-r?>74>u38KAoqtG}N^pP5oxPUUG;aqF8Qvw2M|gPe(*GczEB2*6x#kTWXSMN{%cJ&y&UF1c-GOX752f2^KIb_VQELPR! z`&GP4zKIsI_Z8+0=+SE)Uh{eTTEQu@cPt)uW}>J6uQ4l@93~$<_zd7*eQFi<&>qLR zy8{vhV<9fIF3J#oml zvnNjTT7rKCZ#eom@?4qs=<6td7?w&;0*-yZGu3C-J?*Y~C1e)Jj|?$9WD^v~ujAbxv?cwaqFxjqy9 z4{G<7_HGBSPkMMIU$lyP6S9xPeP{IO?}e`?{*{fzH|pIk{Wup(Z`NKqIr;j2(erYO zx+CWF8kjFTAg=P+k;0FQdrGls0yAELEO3aEjPBVea$7cMx97 z9G%CcZIJc2qFGxaZzowB-Zorb6?fvuwH%CG{LxF`7`8+ z3nosH^U!?3RRj0KI%$B|52EJDMT`$$^`UNp>M}P1wk!Jw67QNJ~?jQNwB`*NH zOV7j(4(#eNc=)GIQ=4MXR8w9QeH<+(W2xp0m|y+xJcHHhZ%uEK7oeu>nf(!`ZruoT z=rc55@nm>@g+9)FH(y)3hChy%sJSS13 zrKXNr?md;}?fh=Pq(W%Qn=_Biv& zYq?>BU#~mV#|aX99O?7gbuf{5GVF=Neh@ii&R^kw&`S7Hr8j(k`l;-T!n?%Wk8I%u z(E2#&JDd0D`8&9zYQ?cR^|e+fT&IsNo3S>+nz(AKmERffEB1LAb0-p4tzGrJwzRZU zu21s%@NUQas;9~`%&KV6^>J2}N73FHJY?|t;9Y{>S#oR7xPD^hG3g?_0NCTmyeQ_P zE#w8z_vrb)Vvh;uuO1`*)!{l@i&v(l1#UFqG<^Q&3p z;bngNU7EL}=f!zZIdA9xLA={}KX~^>kQu*S+v7+tfc9?ZJr4IKu*X6Es_#4-^28ll zyeN8+U+#nfLj%P83f?8jlfj%p=G(bP|DEvTpf`a%&TQ4k!JGl!B^Tih*YcuI3$G>j zo%!8vVSzlu2bJAAdEz+F@aP^r=i4!7cy!+xeVommINSf7ST@W~c*EC}FDLE?^JL(6 z{)jv#4kIJf`wD#=$wTH`pPaWR2tGsPfy<}M8toi65Bo29Oja*Br2Yqa-Y$J!!-Ur| zhj@MRTw(8wyeQAFdeXd|=M3l%qBl{KK9D$&?3*}BJmkynMQV@3y;MiRZ;w4wdoW6H zYb|x}EB1LI&mehyCk0n+v$HpOOyD21r2JLF^5=3sEgQZ+;8e?vIW%X0kDl|Qwn_cW zaxxpMLlh60xgWQVTj}=B?8o_f@uKJ?zYixko8=i~u21dIc^>os~54}|8e&Bx)9uwrRn6u5k3E88^-kIm3m@{CH1J3rv zpg1vam-DM4;o$|ZuXWW)(`&{h%LhMnUnNI>u{fLZqLN$dPw#g0y!bm9y6ipCOSKWc z3FgV@eG`3#hKc{dU1iVb#4Ufzc+_OA_Bd19%AA){4jEoc%&*|bfzNAJuls7=&i55K zkj!tNtmaoTC)1dCE4+}rOOmsV`)WMpuYMxGGq`H#O-vh~Dc)E5`PE~p@4Qa%uh4e} zw-$NP)2^PQcg)D3_tn?xJJ^|9%Q=~glxM(xuwCV3IDf?)NSip z)zd}7mx}q-@+C(MBT}swuB9CE53bc#{`DEh7FN|3Jx}>7TXA2_6*=U#v1Q8de38x- ze5rdyz8$_)=E+Du4(==IF}a!eIr(w$9ps#hkGX#nJ75vV~i&l`|**d8tbshC4 z7SMgg-lZY?Ys=i|-7Y;Q;B0%VyeQ8Z?u&VQ!HC&9uO++ywc=dK{8j(qj>Lh){~+F1 z;B3P`Xe0Qd4uw6bm-?CL<1nWPd{J;9>xeJgZUNnkYA-a zgY2cU-x)mQD(XA){EB^Eb{WBWq2$Lwoc z;CBvIc~SJ8zg)bA_JhdvA%{Fi%-iMsN^**%hnKzK8c&ALl{Obe{%VZz1o7!D;r>prDzuPfqz95~aT1I{rq+xr`LoF9#d%Tv`wBjKjkC@76+Ch9rJ_eK`_7Vo#k~pa2e-|bEcS!? z-UM?$z?0d2?J>$=qLDzX53-Lb)zG#wP?gW>i{?wx%MqW$s z`i=?j67DPMF~PZdiT2KMahHSs?pZwIUE*JXQzZL?{a4;tA4mN5NlklcKRA%MweaI$ zF8ZO*@9v{r+B=TbJwZGf=8GaHgIu57J9`la@{*WefiDW~hx9x5Ey>O5LVM?7qL<2? zZErK57x$fuhV^hPY`lJYuIQ!8?;yXgz>@*L9lccWuQ0#D|De;z2ZG-|hvrwD>ytTT zITzLV3>^zH&A4i&s+~nIiMeQQRzKojNl%>I}C;ZMlzZyaBEBGexz8ZSITJ*e*n%*+Ti=G#F zGVEPq-$b^dG}SW6bza%b$}#T~U$p7?N7Z41QxvS;?aXJW7_(z$-+6XPMbsaJC+?$% z_@erJJ9-nP&Pzuv_x7SZ1Ljw8YCp(4*4^l{h=VD6(wUKD)M=A@Fy;#t5v z2lMu_VO_{aA0_x#m|wLk?-Kh5L%cSNo)>(nH>7lyy5wk%j5Q9>S*-Na|mIFMa*oFed$E$P0J+z+Qt_Z8+0oReXGyUa82T=dVx z1G@K>tMKD!d{Il2eMVqj_>R`EYy=M(9$u~I1-__(JiNnc?;KlTO>@x&D&LOz6>>7z z55kwaIn^?0;Jk9tceWN@OTWy3Wj2&=htKQwaZB>!zzfiyeDs;*;l;V)`BfO@83t3X z?_Vm<08bqF?djfAm5*L}Oqg5S{#EFX;5<9x6uGYaeSIQ%;*di|o&jD<^al^7{E^ra zp5ybU`>T`}eNyRRWt{V6Zk>pD?e_!$ZO7;gsMbGO+;y{A4 zjlFZoiBGRt+jSY-GIqzVfB~Reewl@npc+_M!Pzv93pt_tiM%UE+5;bBa=<_fyZy zI?07{ee&I2R2^LGn;Ass$}cmBJaO?fyni>3m-i=MeyU;Qmzj^FXWKHXTZE2zKId3-xyXb`9I=c@&90hnQx*a z<3fHw#q`4;o!cU~wIv}RQ{TB*%tfCVd7u0^vkv=`ABXv(rra#Sfy93B)WqrjKat;g zRI25|H7b7vULSHY>`TRcHK63{nt94&vSrqW$hM>w^5Y=S0B?A$>N}sNJr4ZN@X>Rw zPvi9=e}(y#tH|}q{FUBg!aQX3otf7c9oI_vD|jv8F@b;ZImNAoZ(^a?56bUg-h>4- zzXHD<@2l-IcZ`|u_MXmT61vPn_djUcFo$w7?8n(e|AXvH|T4>DIx_HioJ{EFucD6$hqiR;`QNu#d~M&A>RocQ zCQQBCHxaj%d#O%Q>wLY)8_qrY^Tho?9|z|ulK5BHr3A+7Yb=mX#gHGMM5$Mo&Xu*Uy$|0{M z5AOgvS24sXV!!ij@>-(j1y3CFkl``eO7C{|52Ei}KzW8+H|EkF2YH5Z#uKIos}>db z?yMnC9Q=c_H*w$4Dm-yTir*f#BbYqA%oo*o$mkDhe1<>CKiEJVNIhS)KY8NJbA8-P zg@2HHUY5ce9&zFb%|&g~yBUwAy|lK{%)?tYBP+s+?yHqc&eHn|z0|VCOQ&NFtSX0p zFwPiiYSDcMna?176Z~!u@p2S>9BqE3%^4bq-(EKC`-wA%FZ!inq{typ9ev7mw^b0$ zMfViFpnCN94u+oigm``I$AKq~{mz-fH-Wx$BXK|AwM4$1dtS%t+B@XAx}Nw|%C83R z`;1fQ#$I{p>7wE@M&)i;HANz2KwFO~f`;K_v0ef11^OpxmX zPX=5y&Wkc`-a6j}tdQal@br+nXb+jLh z6W(z4(PJ*kdz??=i@fkXVe=tq;2OoFbIPA3G)*e)R2CX+C z-|fiB;D7LFP4J0Nui4mH3opRkBunwW%JQ8ye(jL`_8!;kbvzmPys*c?cM$XTtM2(e z_riZq{3@l6@>j^W>wWasMSrkMMhMLrxQ_$w#~(rO$5|}Je(-y-A1rqEQ63Y{w`)Fn z*&mGaGe+;H|3RxH7vgNkxcS<4Q8^jxokNuu0KU}siGMX|X`mLYP zpYrVir-tvZr2A^T@tDb4an;cCN~4?%^RMoU`4#qqym#has^s-?UUag`Ux8bz_wc^z zVlDbOn2Wxt<1<{M-UMx4Jl%FvWlO8*;%&f_Qr3;eI+^D z-lNNBtf9U0Vd10yiTDiQU%fB$@(j4IJ`nyv z$&=B1^x$l}4XH+Ns(RWUc{*rvDI9KH}KOHlVI7PR_yB!?JfMo%he$=Dy(%^49&u!P3&C0`z`PBly zeA?q6ejNtY8^=fSHdbZ`OaQY5%-x>Qs_B-z;9x~6b zWWJr}4Cv7#Cxf{t`p$T_Un;#(d+Fp9+7C`2zuLEN)VW38hF0NsMh;oq<8P z+UT+wnGseCUrs$_ShM8En0NfYpIADqyW_^jKTglpaX&1GQzUy{dcU(aZL@%|nXasqeq!$2t9Oi7Z*RnO$({OoF_o#f|N#oZK zsTO%r@ULpp-HflN^;y%UFuL+d;kEpOd|tP7oNWg+XQ)0HD|+`b7(GkmG|4uY$8??!-wvv{{N4;eX`tuuFyndcTL{LaY91TV9s z`4#$uH-r~p+n9=(eHC96{XxvHLWF-1?{>^Z`CJ94?;y_^WZxP4!B;&ujhNZ%ezWD- z_OHSOpJA_YpyCw4KL}qc<_wx22j}YVE>^U6o-6Jv^ioe#UbOz?y$kF(DAiKyQe zO*7n2ZKr%YxN7M}l|G7regY9tS=8&5!is!0(Ja1AG(Q^I|Um^V@$AJ$m-z6prwvxhT)C;G5X1 z+dF#-Z#X=0*bicUbuT{8g9xF5_Zx>{b=1q}jxGFf1 zKTq_e{FUZQ^&b7bYmHSveY(gqurGC8mW$wvb|X$v3Hc_#zdBF6KFqIb?5AD-^<)$6 zo&VP!r#bw3;uk6PhKb29#tsN{R6Z}vMe)9>HrmkpihEx7Rjv=5ZE!!}Fn}q0PvW|{M91AMbU?7KPde;cwcpGxPH8p{5XNjELC3g&*NPie1#vUpU7W* zwK!S#ZVx*(WdGhyJY>Ds5;+<0knbfY#MOyjs^njV=XIrXh5L$o6X*}R(z|_l>_o~T zqc;Jrn&h`fRdy}xCHjM!9|wE}qPw@VJ>mX@|YJw}FmZ3?M~FBCoc(vV=UCr38- z+0l|q@Ai6eUyY|8ebj+^%3qZZJ1zW!@GeRI75EGx^~;VesoGmKbnSQ4o8WU5Nj)#* zMZs@Ju8+Nz=sSZagLBnfdrt7%Cko$0Z}NFD51H>PJ->Yq<=gijjNW#0{Xmg#=ROX5 zc#Ft45p6aXMIQ(6cI=(w$nPw@0MG3YJJtBe9CF3XZDV2|F~9Q9^rQV?*9JeDUu_ZZ zE4d$BPdwy1H^NUnv%j|N>6~QBA$P3e|3TbW8=Z~hn>d{?Jf+QWFF7{ua?og+Gu&;q z)baZ8KZssx4SBuVPIEBJ9{j9)e6 zb9+z9^`SShkNh}~uX()iecBJo{FUs{bDja*+C5GlV()xyz1zy&sy6{IKy$Mn?VaoB zzIs4?XWrv*4q5spzBNstT;DFU`zpzAiFmg&rwBY5KjFuL*D{>AYBGO?|3S_nGY@&4 zVZZWC1UuX%KhDmwVU)iDw-!D6YU<<295V7(_h^3AAo5rDgpVHHB@5GD9rpwIs{zA% zdh{m`FLE+B5}U+a6#GG^qtW8N0#61xWOz(?kK-+TUd$Ke9zAk>?BR7TS=WiP9ZK8} zFXCS@XL~JiAn`v4Kh9$6*~I-ot}k527X?=hxjy7%J`(?f?HR$M zkCP?(gPa$A$Nz_z>?H>buTY-ByhmR}z6s>p(M#q3LH-W1e-Ph6=>@>NeJb&8h2&Eo9?kn&`U-fJViivAc=St>~x$i8! zOR_)6_f_wqA5BOU|AX+D^qu$d%#X)>;1+0WN$)G<`Y>+~TlOCLospCAvyCC{2fl;s zqfeo^D0~ym)T1}gA)B8oaJF%-+LEILPe$gDgLU((Ki9|8esE&bKBoc7!wWtGdwBnF zcmMzRo!8O(ioM|-sZB{+A`5198TVjNck%*YE(-n?=2zyqKJaAVF#-2u6}{VWuIwE* zlXr=|0ExPHyX1?)H?fsCMJ1F&1`oNTPZ{O<{OG<4FeMTXIjel$fh**R+a!7utB3>H zL7q6wuegtccRTm!k?X^`lK+DpV>itzjcg|W;IE7NM6FTWT5z^CA3f&nQI$Q99;o&($;4(k@AH-Y~=L+6%XF6BR{lL5(=j!f_VDi!H^X>9n6*-$mr4j#% zy_T3Wz{884m*xeqGVK)}UU-+LkdGd`KK7Wfe^BeC@?4a?OYGros{Qq(4|$jHZhtcB z*G1C|t<>{Eo`L<&bIH4e`-=GtHARc7RtWwT`h&>zF$YrS`nsF_4=$wsV7=fo;C&@M zaazwS%&UjuU-^?40P`#4`lQbbduQ;kIM3itJ}>mVexP|fxN4O}FRwkC_L$Ks{X@~m z*(P{0?8oUyWgordK+1Rf57`IO`xrgb?AP=x46k%19y0jt`94i5FAD!4&lx=Ee~|N6 zyN$MVUxn;w`^xrgW3#`=U&YhAeU#3-ggFD}`fR8_n7DjMPPOnmvnLMocFY;dQY|RY zuyah5;Hs5Y2bkrM;dizpUY|B^hu?WI<=gvFABX+U+TI!Y_L?KlouARRQMbo=U}#Q` zr+hp92b-wp#d!v6;uKw5pFm!KDZ2R;=Ix&=PaNLu{}g*?@EPEDKHVvQg*k(KUzJ8~ zoMknxV^CMSPpI$QU)Q7GU6e|BQO+}Lp?5pHODmTAD!3or9LvZzv4%V*@Z*?sYuT5| zJY?`iIoH>!xV4w_TQWjT^Nk)WQgYXqhab3hx}?$0v255W;deeH@}l6k!$%LkD9;)G zExv;?e`RmiF5d03HzD)w@RNkx&RqVJ5J7x)ZW)JtXW65j1$^}DQ2xlW_|%3|Tm)F1qXJaHw&lesMZ2j5ze zVDvXxtva9YUon$Baqtg<`@!$_(xuyVdmQ#!YB^-iUx6>mb5S3~Lzdnp_Qa(M|KNYp zY>nO0*JeFc@|DOl@VS!QT5uq-cjlbTdEp#8J;+Ltgf}=^@rwH$TRdG{+ZKT zO$)@Ffjzv;Rcrgo=IkAsUyb#wBQHR^;l-4n5~hTu)4W}BYmq}fZg#G2-w1Q)Ot=&fIrqkBKF5Kd^TOPbR>29{HW&1wfu*Kl!{0C-f3L8Rj!+eH`?>z*WN@$KX99 z>_oyVDR&KRYVZ7>@X>>-hW#LOwy__KrTLZg@V>3?EA2aooDBO1!BvxU2F%+f_v7NC z$;8=yIM=7|O{CIXbeHmZ!D9lx=>Nq`5q@W$i^{!oi0J|4kbUXh-qPoZkwISNAzR`% z3-1#9CM^1vwtPh1@Ot500{;r!+J@65#AlE``oo5oQmxdyy^8kEDOI~@-j45}^zd$@ z|3ORQK*B$WzB71z)5d4hef8VP+alM8K28teF*!m!8RYu(o;Z95CBGe9HElmA-|fs* zL#~f`eXr1WkbM*KJ1FN2m|srl%JpgacIma`95VLKemiUFJBarcczx1i5-0c!c(+?p{wkDu6Z@z~Z@zc# zPxCA0s)2`$JOlbT;6Tbg&Ytw2vwu|_$m}6gM4rLZU}3mRJ})crKgiytbvpkb^6ki9 zVec$?eUby&x$g{5oWIF()%pAY#TVuGmCRqk=Y{`4-s52J%zK;##y3}_<*q9aKk&!t zjg8$O;y`k~{fz5QtFU_0v87deik>Hadsph?fG2}Jj^*Gs%3nPoUf(6fXZYNH()BAR z|E#@8Ib`G+;7jGcGyFK$-9Pkc4*xUpi*IcgDHW{Da^?!V?Ey>X#zl&b?G{isb(wdh|SR=RQt~$RYnVrB%(Z*u!f@ z^LF&;;k9J{p!B6aOP>LpIl`-0oU4sX zE8;hXyzkY0WP6{jEjh&NV-GL>2jNTA^V`9b(f$YZ{3~NsmEb^j)A{I`&rmw6So8-k zF6v2pXZamOe-OOB0XpBrKg790Z$kgR!n>V0ka%B#Q?$ouMZJkI@g4N*_0foNo)?2+ z;~pg6RXG{_4`MEgbH%;XH+9?(-Vbt42LFTQsT~Uk&f7ZkW8yP(X}Dp={qPgL2{{)9 zzg_ZV*u&e8-dCaIF&Rub8Jw#`;hVs_-8d?pyq14Wc|h+g%&(BYy3=fXwpHY>uy=k+ zaBCCJOm5okLKcR!SBpIFYsja{44lz-XZT2_zdVxd@Sbe?6oxarM73x zF)cE_zG8Lm%JOKzt=%a0IID(r+W5oQbk-Pv8bJftBcuIIoLWOSv`@w<{W0ijp+*M@%w}dYn~#|-Ns@EYLCG;4 zh1C09LUdALbS`OWmL$x~nAyy1W{fpHjA&>dGm%K?cY8da*LA&K@9g{gAFkW=x?a!c z<8i;=CSKp$4i8&@sOfrSVR?J<@b)%-A#%v*rRw)M$X_)_Wz8*|Y&N__$3y1#)v@^3 zlDB2YSHv6*tSu+Mv&QQKU(|Fzm`2=>P_f6EuItgyBJM{H<*z;oE2O=1v&dh;YstO| z><3edgZBOJbXI*A%3pa<4jFrA^F_(Tt%WB}{twFe758x@Ulg7=+*h1$=lcp?OZEc5 z6NkAdJiJ}90?K7!?UJNvwl>w8!E=rL!QpypR6!{3N$Bu){}ucpx6x%Ggb;%v+Bp!5RZ zfAEzy`KuugR%Qol9FF*xKbB>eG>Y~(@R&&71o#YTQy(J^q|A$&4R3vxJ#h!C9P^Xa z{~&r32}ftt?zA{XJ$jkHVtzaOCi3fRPcPiJvUs(~Gnn=V_4zCGagb-|I_RP5r82iR z#5ii@@{FY71^cd?F0AjU?yJOq@#FBEfqkhq2`QVEF6r+p>uXc3Lqtxd9eLtR_c+>I6y9*@AAFg3eKFL> zK~4tWK`qbF*w{+kTIqL|{Pwor!OJH5K})kkHTFjW%R3Vf89gtYtA^cw(0!Fkedn&i z!+V$JSLk_x-_HKQH0sea4;dblKzg^25qq3BW4|;cFFhOV=3Om(Uf_$uyEI~Dl+jDf z8RU2HG2sm_TGnhRYLk5|=EA0pK$O|yqaE0zGpMZd)cesPsy_(6DEc_~ACx(n5aNrn$As@I z?hoD)+*)n#+$pPbl1qF~U!e~+tvL&Mxr-UNE7o0~q?an&M>BPiF0 zJVT-2i;f|W$ybIqsh0|VJAVfaqL+HqWX`~T9R3cXKR8y*ub79-=jspIJ0pM9HOVe( zR(bCuUkhFz`%?GETsC|p_zZZrYx(xR)JwHV^cS3Mi$yLMN*{E{J@LDpj z5BJsfqF46LICbx)FL~n7oA_c$s`3v?uG$TqZ-TiWcwgbZ3hh@s>{H>JNKzcgC!J>6 z-JyK@cAB@pFYYV$#Cf*l5~oP|Ciq+pQyyM3;hSKt+Fz=VvqN~7hR8%0I3-@rnsg27a89utK`8io*&8Pi8ai2d9_S zYd&cMa5Vz<{hA(4oMcfjcA~WJM%qFhdi1??Cl?mt4{#$UD zzJtsu!hKaE=2!W`mnw6}n74NjJY?Kgrv5?f2YK&o+DlC#4&*{(0QGT@7hNa%&NHb$ zh~7lbvQpaPppR47v{Bqw$o0YJh5n%Q<8V%3#K=_zr%lda3%k=)#hJGJCJuV>FLzmpE(v z4E4Us8aUMTn(w?&vt_ppLx_L1!aaI$k-b~XfQ#D?`)mu-^(Nq>_Y=Jd)4j9Sch3zj{2ITs{XFyKIM)U`))O9H$wMv>J}+>JHj~fG zi{|av4}x2}wm4|t_0##(OXXbO%BA1XygenTi1vf!7J;>~M-7UH+@8Du@bGR`UVv=s zP2gNWU$AX-u+|szPd-; z+Hs;cVd@1KPW*OoKm5fW2RRvV)$kqE@>kQXEw0@@wy828cM|1f{AfQ2Z@ASpZ{hRe zJr3@xIO4as>u6%ejk*hc{I5`uN@6)Z>Xk zK5kq5i^7V?KdAAqwlrm&pF$oJ{c7=GcZqPEX~`| z$AKqK>rLe8yq3)UxJ>-3%=25w=Y_l|JaMh$T`Hs;^1U7dgCfuXvyeRYf@IT1hTAnk&kAwLYc*vu4 zTs3$BWUg;-%%6sjqMhh}P;x&i_D7an%Y1fK1?Ai2{OS(nWVE^HpqO8&N6-Gjmx!wt zqRYwPe^BPz!Tms9RQ?ZsLVFx<>f>Zp`sYre`wH)N@Q{(eLeJ|n(WB3gP1Sh;{OlS; zkDmL^KgOA@2^ah;dh>p2lItD9DAH^iGO9C&`#&`@~hoJ z?{@eHWxgFbnQEH1=hXjode*+R#c`Bpm=X6-^at0G7ogLi*=mmipBK&*_za~@Y3IET zO)9&(p;`2CR%EO$UO@kYm|y*-yy03O2RRx4+BoW^zLtC>-p**2upzYvaf$OYXmi=2!K*FBhCl`y!!_@%?!CQs;>t{ZNPN%4=CeejM#P2>w+# z&D${-wH6)|$=NpbE@|9aoGb24{1o>tLP+$$YtFtsy72J>^B=qxTZ; zcJ}aE7=3BpjywbSqI3IgBVQ`#MZXm9tDeODKrfZQgZw{;a|Q2`2l;W(qsM)P{UG+v z=sQcFmrrTn`gWA->qGPQU6f~#-f(%YZbsY=%<}ki$h!_Lt=|*3mOZ>r6Q_v%&f{+ z@zKfxqUw(tc6NK3_JjBzgvaEDF5ixGHJ7|g+)L%0 z%*7MqZtS#8ryf1?uP|@ty>pA;Uwu>6D}P<8MM6jN@G|$qpK>yq*AoAO{|$EaJ~wKU zb7#SUOfHVk_;BSy;xn)p0Otx`0PwHayQKNN^xkmp4{{%89?eC;lQDg_V~^u1INQvV zc{TY*!P#~&=?{X}hxyeQv3GW-`)Vxl+b00&a~anMUe4jH*V+4JhI_*Z?YKX|F2p8f|v*KxK-8_W%N>3{ir|{$aHDo>Y2frdd!^(Ugpq|&l>MC(x$^9U6KbW&URPfsyAMw%e zBp!0h>YwA@F-EU=Mtlbwg@;$mAxjVMynfz-tEPPiC1=}`c*t75J;ktJ_;H>j?g#dR zl`7AGUaB@1jTL*G*Xe(7LFjegA+GrYCw9NrXmPIPKtQQ4c`d=My-D--aPoOM4Cvz0 zSI57?yB)m=cmeQk?Cj<}+O0;7xmH^im%iow43F(OlK%WD&nz&P8#q zq<4w;IGmFKU$mRZx3eEd_6PlsAF1k+? z9usgskY`{%19&pnJM&z$hPWTxqfZuGHCyuIjP*VjoVfImVPLG)qNJc=PxqlG9V;z- zsPDX!<_y;KKgfIM;<}DDW|Zs867wtWd5s^jrvF}B*BdpIzdD(=Pw}thToic*&R@Yt z51$wFWU6TIjGmX)cjmk(e5uS;s}=nAS7Xjl4jFvWi7ow!e-*j0W%cp6VaBB^o=M+T z)Sq~L;6O@F5jc<*f`0|BT8C?Y>3S3FiQ}9MbJaFad}_|aMO$K53vW0)Ciowec?S5M zdG9=ZTb1Gz8R>uUO~EN*KEotkAE!g&L*rg!uT{4*=bOCSIVXd>Xg%dc;RWE{1n2rp z^9<+@ruBDI9LR^U_mp?3BlWz#rTdEggW%RKtB4ohLB6jn7QOaJzWu7;^?kT9)aavn zsTEch@uZGfK`PIHyeRu|w0FDQ4_56DF1bnkE9*FG z%3rk}Fh6&%aZ>jjy05@*mwRXA+p))aN%Xv!FB-Ap#q{k(FA4tDlTMpPh7tdYxoXUl zncdINy7lTEnlnsJ2@u|JKRQ?V4hGZvO7`g4j{_e)ya2OKJ+JH0<2&drc*yWt8b!YS zQT~;MiPw_(SD~Vp+SR46;%xJN5Z)#DCVC#3Q*KN1_CCfh5`Iam6+9XCtu$n$DgPjIAkPG+(79@kTq`(5+r|3|=Zbq1 zDXD#F-d3sYMRFc^LFf=D{Lp+ICtXS?hEuk$UJ1t8_qqiyMh0UI2d!mFgCh> zNI##BXlwLs!`m@Gg?k1VmDf_v8Q5#tpY}LD zry42O_wmRu`X6LZoWq>W6U&FqA>YKUV_D?k4O(tyoV9#XioZBl%>5{#{a}7x8ce7Y}-L|2IgOlr``n4Rj7%F7keD;P4NF{S}snqk*`p&K7$Jtsp>-g7Ij`=C6HVN&>W5Rg`{vSlX9rJdc zi}IWy$GKD2g8JIi!TVMfCuaO-WrWc;?m^-;k!QeMl=JP9ei6Roi3IcSa8R@g$q9 z`6fLta6k5|`4#sEUCC>?P;iRScMe?dU21jk;o19*le=dTPbM_9Df*saaLmu-wd7tZ z&)diS_YPjiR*V}0w?VaJH*MA4WRl9jCSM+h%m+B+-IN++4i<}I2GI+O3 zpBLr~PJ4F{r$~MWy(uR%t|i~zAb5Rm#55Tiqa#8u`MyWKiMx%K=UP;MQ0@nJi1(Ev zovQ&MA4Jy@za88U&F>6vIQLSe&+F0&_ZwBB@67!{d6mtgVA?FaU4?gcDfMstSts_zW%67w0@=f!gdId9j#gWRKse~>wl@DC0n z@6zqSn-TkCE*VCXFSUzHU&YxDEBTvz^n73WQ*WZ7aVqr(HSZF5$lz?}+q<{Ce6jNI z^lcFvAFlq1JiM4+p*MkCAM=o9&x`*D!70-FajCE-*RK6Ye75@*)IfJY4@ZJsF zN+*f^!dlOEk)fW7{yOun>$n{CTGroiR{$QZkJGZC%>eb}W+T@VI zf#kk3yi3>*cI}!~U#IfzVMagdc}*#sEWAsymkM4Vd|vtfGu^rky4PcC(^{Q}H_Z6K z%D9ZQ;-GzhozAVdC10vOhYTLFtLo7kDz=LpvYv-*Yh(6YtBKdL`~2jo<3}W^`|1*L zwtE>ritm!tiTGFFk%yP(?eIHaGPu$n2lFfRCeX);EV-Wf%&H1{Utx~}Z@6hMmG3Lh z0E6=5aBqS+kg|`X{~wgRKFcV3;qyYz>u+8D3Va41m6Ms@?^E?1l>dY9P4IV+_s$XG zzJe!ihv3O@kN%|KA;V+xwrk$N3Ei6;o6lMu^rm^c74c-4QxsACVZ(0XGd$(nEpxy-k5AI3$E$u?V-Q5@3GNs}jX3w;s z=E%2Wzc8dNJsmvW``1x<&YfsKIB(zT;#C>{Ss6)toGa9$=lx*ULH=%={WpgdEqf^T zIPg0+_1GfbSC`xT4}#Zc*EQSu_fccW@B9_6bfM@MI>Z_~+(S-aKY4 z`p#P>_7Q%ZFQ}KgN$?qbPrXdJKHOKwMBf=anei>zkL0f|`Hpa1KhV4T9nl|@+*;(X z!sx!joB=&A=Jm06NzO&l^TPZJ@AeV#eaT~j95SCP=6*a*`Kwm?4i-*sdC%fG8|C3m zCEo;l!;$O5`-;yM?kny)^WHgEdRm z@TN@l7?IR}FZt-fL&hG5b26QY&v3Y=RrPUhQLb-v^a~-+`q;QUGoXTeUfi4Dd^`KR zWN%{Qz+u#*$N!*wU%`)qUaHm~gpXeOyc)z@G)%{>WnZe~6k*;zh2~ejDkme)6~Ei_ zV$%(uFEK6%Q#oW`^6>Uck6tm%_)}af^}Mhj#QQ3C@N#!|(VKXnyi1e1H#N4NH5dCq z$wNjCIaKBPz}a>Y{lSL7bsjf|40331_H|7U^}a&i`83VjYlJ5bd4}#Z7ey~MU+taI z^ZG{lozbJ0?{?f*@DC;hSu7e9dyx8rV+E&3<6rR}2mL|L^|5yec~RL*-7376vX6s# z`-4P(;o-&n3inkfnlpg=v0C_2Q-caUJ%;|`xYJ@r?TVvIE2^@ciO*oI{DY;`^Rl2l zPGgjr+Bsya31JT2_Y>pJBkovDEW|$AmeM_toBcO85JXE%Y6nMc#1k(R04t zMd$PSh`6;kBkmHf?;Yw7K7M50Bky*2;^e$tazC1)vga1jebwHkl)i(#iBn`_{E7TH z%V;jD-B)^^%xN8;LHiDBUI6R|na{x74}M<-Do)X)LsQDGZD^w21pI@NFUlShckk-p zq@@Q$o&h{$aEfHEk9`w3SKxlkJib-%McL=oLj3mb^HYV##Es^ne^772(Ks@`OHyaG z$C2J8{vRA2?G*B~kF85T;fbrFyeNB2-~~Y6nZ1@-V&0D41bX!2McHNX zdJ|5H`yqW3(rbyH7x<#^}dd zau@wU^d``E9_xLE-dDM*m-;^K2budJ`#9*OV($!2QNH4;JybjyzOSTr34WZM`Yv6w zovW!gv5NAd=8MvF`F4#bBmV~vSIsi zP3qBqC^$t=5l<$B-dEts>=5}YQ;!LJ^wK|wcf0%!VvnagQF{583nDNxlhq zmrVUQwbYyN>V8}Foq0b9{uR$f!Tm7J$$UlJ+I1fP8}gRU@7zu8ap1?1o;dsu%KOT6 zk2CZ5cVa&X{#Dy~d$DI)P)pRA z7+#AxN%^bggE!evpn1FRwqQC}=+XD5`wHiZ=U1HTOEMG*ZY}ax(}@GA-BGVH{9zA+q;C^WNcF9#^PLcjx@%$>D{5as&Hbn=MhnG3qcwhOeb0vLV?4t*#sJG9n zAtR%&8up5w7raZH>zh^HCCe_!CBBDof5Ojce-`{>@*RAFd=pK?DZ+QqoH&roDN0MV zNw75j67Q5;l^tCXe{`nuO{@)SCa-1c(%-1>oa@}VYj*u#r)Td=E?${2T=-Ja$C2Lf z&N|LEa(&=mp*K+^dR{df%vO8HMH>I3@(dbZRQnFX?>uhkNySxTo=gX$HF0Z^Z!Z&k z2Ih-`CxiVUzJu_kjxqTTg8RX~RPbbw>)YgUTl8_XUMe_{@?6QBjP&qgKbTG4aOsJY z{C4pAWDc44&YZu}?koNdGOv&4SMa4GFN%D-^ziyGpOWHFK6O0$=o&@bIShA3Y*-Y8T-jyeIMuR?oGw*;*HH{JW~2 z`Rh_G6Kq6IhTm78SyVfYR(ogu4r+7J>!;`KTSMOPaGGD?-7e=>n72#LHqMnk*Edo4 zCUCCMA4IP2`srNpzJhN8IT<|pK?T-q=p?WZ=hXFY=;%U&-EtGnaa;Wl za(^&Lc*Buz|JdX9zj(vB?~J@Cyq3}vXLit>^6i_%yS+L3zTtIou8_YP-{O3+g8Vq< zYh+%uQoXNwQ2wf|$At4&H;?62dgpql%vf$|^j$tp%-fk;i@Yd!G6k{8g4YL+iR|N4 z7Co=HYT(IK5>Ezv27b4{8q-2^(aVCf&An7V;?{DW;cp$MNSiY-PX;}&YeR*@LuS4xa>$w&067_cUvXYk%eUv!{0e!|RN^z3dQ7$mZf)Kp zJQ+P-v^mjV_2{M7GF#=4_Z!|M&Nk-lCxy=oduPnA+KIU+dw7}qkw-jaGlPqvc5#>a znS!(JPTUXhWcDQVCGS#4@;kTfd0jHNML!?%tWO7*egk%Nb8QH#t{@I1pQ{ekOO-x) zaJKn<1s<~W(dX#6YSL@@yqLE$XM6p~D6b_84lGGF@$e$o*LIJCd^^vtz^yH%b0znK z?6s8r!5u1p^>B6M#%bGjDLw=ESLk`&H9U--A9~GqDEV<38ZFMXk{<_NOPLqlzCS{F zczJ$x$#5X1F5*F8hQ|$+Z-?I*{y}@=-h|VXL*^cRa{uuok_Asj<9?v$^+{de@o%X& z(JrCA@dw5I0B5_}(RJt+&lDY3&CUDFr~>Eq*aN zb54f)gP4nI9$x+q!V@R=IJxzmL=G8q2Khe-z9{(Z0pxeyPXB|*Bi&pd(9uw@HabNM?IaT;lF=v1`+)AA*@Q`PUyeRTl(rf83$1JK?7_1xTIRO?O}6e~`}=`%>Y@>0tam{&nG-n5lSu>@k5K=eJ-F@6&W& zf!7BwKwQR0D}$+*3U4@geV>ru8M(gVHhc!~MVYgWJOjMp{}EmQ^qoz)A8!%|vViu3 z6|{E-R}DR{_D0Ku^{GAbk5tV(UTWeEN00sw@;l49XzhuA6#wc2tyU(BS5IuT6SLhEm()$YU zcK!}>e^B!eGOrJLQ8$`1n9*Ex&C=@N@!r3TDiS&5IfAPert(+trNR@}S@+{a-r z0Q1{Zg)dd+MX`4V2U7N(zp1e@dsD|l22Td_t4=O`2kcV5ROVk<5KjjCLABq2 z)X>nz=w|vK^awE0oB@1Mp0~4)UYobyQGT2y3l7q`(sQ=0uKi8@LFN=8*9Tr7_*c`c zL;7tQRyNUIoGbXe7J02x`F7@CEnV@B@n_=oAcJ?lDzI~(Ms>!`Gd|rJN51IK_*gGSK4F6zLY)jO} zxmzYbc+cXw4mv&qzJuJyLEl-QlaYMUbHc-G+cmGgPV8|$Tp4T(jJub3eZyqoOMO7S zi30Tq}pc*W9h4X?&p5?|C! z%tg63fj*9n$TJ+S3e@>h(I3?KqS!lsF8&AgduPlUq=y&pc2kdu*2h7v@1IPERaI&~ z_~_ml?{>@?n9qQB`;Nmih=+WP_Bi+sa(|GyYUn$EwPdxSB=#Y3)tGwPpll_Xp z%iJdh99N#WtG;i$<_?_H{XWeZBu{3y(SFtK%y}iB?_Z$n(Zd_g@Alh*0|~w;_Ri?h zkBEPYyq4g8T%$f!9!-RCGzdq zJ8zv>I&6Nw8P;ak?jHM?di2PP`iuWTZI4qT&edAO=S$)iEcD7A`Giw|-5t?OMc-NS zuk<-&ez(6)eVowHTHm3rxdSJ5f6&-`*7~4#X#nMrzcBV(Ri7Cw{5bFr!V`yldkxJ+ zAN4LVPo^^AcjBtabHyH$8p<2M7+NzsO$|daf07QSkcY zoI%dpqouR&$3@n{yyH;XhpsW+*i^+xOwu&bIlgti}f&EP;*h_udbZyKFqj|t{i%>CF$ zd4`Wi+B?m#yMNQ?)Jv+5!}BZf+Xq~9Ybm5T1K#bg#WWilh0iN%;1u$CAuk%Z-dcDq z`F#~u?Mj>?zIp% zG0`swkI7Ea$3dwFQ zez&tXyz;O&d6$lfzB9gqGii_0$0=jvBI3z#zPqWm31FST@H zpE+hxTVfLpUoTlA-tE3>kHdZ(_FD40oxS1k`-6AM3!vxq;oUAhaks_19Xw?CasF65mi&X*JGZ_EpBM7&cwZrZ)sDQD<;pjK zxhVWNc()&*_m%b?ye8)D{JvrjFZMV^!79{pERvKKkd+DkwXTr z5AXKieYI+jBe`m?$C@unp}zAO$8w9AwQ=Ofc`f<-_>M+P@_8Mi`4#7{*u(pPczv9| zO4j+jI48roK7L=BQ%+`+M?KBkuTY+WeG_}SeW<>JZTC3vF3J1qesq)JjhLUqJp*Fh zmk-XipV0E6%3u8??kna%@|*$lE5m}lf~$r;4&Lo;_k(_PuE70}d{OQ@gNOX(k{FS1 zH|=>L*TQfp(UI6q`{}J=6M>&wP=QWJ{&hVJn zWpz&aDBj-qdBPdNt<`#7=nryl0zP_+;Vs00#D1`MetN1^g01mr{A)D7iY312Z(@(b zerMzvn1>8rAI=r{SGccy<62C-;hhHsxn=u*7Pe7%E%QYm2mC9~Lo>=QZZKc%E%@!= z^N!ef1sv55n(^UMl)HEwmqGj|ujJ+;`^hAb2wH zd7(%DO^vnLActE+)_Ob$tP?(Z?mPDpo;aSjgR^Z;?mAox^&RB=RUDnG z7l>QSUI6f~c<;=99Q_`rFxD*UsX2ucw-1|5p13>Y$C*U;RiKWC%-oOSi4KaZ#(rn? z2a#t$UbH{uWN@zF4ac0pqb1Kimb?JJ>bM{8x^5ikMe}y%Kw{nwUf*At5wsutuwhrX zz5_bB^z-RYe1@-LZczS;bI6Bc_?{U}@x2WVQ zaf;ycf-lwVAiUudx)%(5)3wfbzTzQc&Ja7ez<#XY^=bJlcE{-NW`i&k}hrH|Xki1KC$dB_r@fpsfT~zNY(>>0E zs2uW5z?X`>^S4#K$rEQ~{5k#=;Y(#6vg^=K$P4gl>|w)7>e2r`D%-hB*DUhUGpC4s z6IVA(DVwBnGJbBw{>7BP%2jz$Shs0GIsq-%J9*6hN@H@APeES*4Z5Fc> z_v0AtaWcjG3iI{{&sq=9o?JNh?!V;Q!P%DnL3qReuO2<-qTm#Pv(5j5m@^=Mg2nCPW~hs-%-?45rJ ze>3Ke!F<{5(7$|#xaJLRs$ zckKz68+&b&`g_p4o#zb5i^AunaX)x|rQZ+YT>Yl@gR+m~;r;8VeDNKeE4Ux*iL2Q# zUFEMh&%n990_qR?QEvi0FUjkhA@U5!i~dR65BY9?l&c1At;T_DNBzMgRsP4zM1L@s zeDwGZ)`;GO^gG|BcRTX!BWd0~o46m~w}US#xwXiPYWb@$>d}KQx?B7Y`jna-H1*>! zPX;-e1oxQ1g~S)F(9K0<9|ykFS$5r=az-xnGE#rAAl53%e$Lj3p9~9D{44mp*q4gl z1bXzmAN)+rMJ1mBc~RU~;MVe-f#+8i^c{pJZmgIyNInBNkhSz3oZh{jdS2ju*w7wF z&#mQLAN~hTc{2JvPPe4@$zvk9YUmI0yB)bc_QYk)Et=f=-b3OP1&Q}nJJIvvcRSx# zyvO11Aoe&O#FJsZ=-hoNy7`snwUm3Dt;CbTyIuMw_<6`6-__HF_3gWEcK&75 zc+r~}MEv$5PmiHLJMOaZu3bSsdhX-2PgtAUTg|T|zg==`!ToR_k)rZf?04=XczxWX zH#ZyVa80)#ig|lqnv3Fp&`;&tHx3*w{LYwPod_QvU~pePILm&5@LGb0 zjQ>H+=XIO-4B+*FFN$8Oxrs0J=COiGuUwy$>C3H+KFg=k{OTXUDbn+=@NP$*p@-mX zGp7g~NSVLlJr4L+;EVnoKA8H>A>{MgIB=TUJIj2#^wCRRALshskG^i$6LUM_mhgGC zw!*u_JY?Kg@Wdg1C3DCgBT_^k$8_Glh2B?s-vqpt>;*V2di1i7bC&)GbLczx*XcR? zl8RTF^t_P2V(y3Z@V1aQe2c#y`6ddQHlFuXzEnO}c(?0)^yh-tEdA0jH1>Yvx}ZEy z59+1DV}kwQ0GeO1-}&pRxyOs@y4W;PUbJBDUFti70|`EZJXiP*avz89tGVS*P%m{T zc}yB8hb;LFGB3*a)lJ&tXx=4o)!?-R4|zWICNRI!^ZM9Fk8{OdOK`URPc_iqSwC;L zit00Gi|~env(5Y~+*iCG%oZM#dHp^W^DFe7d49ENWOw2e1)O?;xN1vRyi4bbIYr>s zBF})lD0{KqMQskMXQQeXS};|iE&z- z`D%OzClLP%9$wD3qnDaXy;SBiFb|pMqT1eh_P#omlL1d=jmTf63ExCHakkMPl-v*a zoiS%%emi>skdx8!49UdV#=QNqN&ae1dG{mV*EF{dcepYn$Kw|Dysn7;AUNB*x{Ydx zsxH@YAobqxDcv`b*D^fxZgdOrWLyJQx-T1CNP8U4^)asxTs1ydcSX-j;~`s#Jq~in zme=Zad{O4fpg-7-`0ecBHIFJFK7(;Vh&Wg90%-RYd|r{nf#kfX^q9aK-bB6$cmX)y zZrXRgk-4yB*Z$z@u?-dET|!=zdlQmd%jb%@YS<4t8h4BP3Oz5$t?fSl58+*sUI6yv zd|lOtaxz+v9$tVQ7N>|?i~QB#*!{}8l<(X@dBay^yiapc$^FP6K0_DkdGY)TeH?f# zJ5!$FQSJvg+jXb&gco2e?VaWSAUr0&kvAOgE174|@}m5{Vy~q(XJEhcF6!gd6IX3_ zf&=BRx@65N??JtZcgSnWoNewqgHt4XUc1!ZnfX^UOIr_^ooi~G&^>S9+cX!Id-&`Olp@ET;A=-_mu0yyB#^?&tt9{Mv=#)GjWPs8$zmg?O#w* zpV@2G=f+ke_Jf#TfwK)h!!Xy&cW2E5w~ zL@yQGTI7&JLjP3xcKA|#N@uONq8`1z@66x9r+r=|9y0nk_X9ILZVq|F!P@LlO|K(C z#6t#O6!R;7UrDbe{Dbh(qc_2GhURBG+ib3zd;Dwa4_YSJiXQzkz?=1Q4axMxUGQ5_^GYpPx73b<##|jI- z+U2yz!G6$|@(ew7JY>BehjV@7Mx^%NW$UiIOUR4jePx?9zr5R#A8MLghtYk7_tie) z^+~Q8@>db$cjo;ddza)M2j^<2;HtG42E?2WcMC|MT%Yti!#4r0+7iY6;CK7_k>NCN zPdDU>_Z7Gw(*+M1J$meM(8ux3&C_vfw@&OSJaOpbgnDfn`J|Jd@R)!v3J)(lCYXz| zH=O&<;{twBUI5daj9;lg%|&+_pAvIXeGVDtir-gv1J`-{J>*T|K%(yqe*5tFr*&RS z?hpRo?{@fcz$t2jEu$PVyq4CB2E=}ASh@6E@Hp?2Cb_=&j1St(+mUZ) z4&)ZPuVfzwJQ>qHPPp);f`^P8a-Q?A)T7t;QZ*kvI7Kff{}gXyv`~3b@UJWs_XBy+ zON$?ypE=cKL}ve;wqt4U3=Sl|gCD4QJAA2{e^BGebftMa?kn_C@owKGdK1z=h+H4{ z=pTsQguC)C;lA?O7DDeU=^vE&E15&a|6qP>I~`}6{e#nUr>FQTKaSov0iF!cufANe zjQq~MoMt}4RWqae3cU&LJFlQQgUrc*vz@K_IG8hFKPdZyJL!Lr{W$2+v&V${&dez~ zT+{2wEXuc!G?_Duy|IhDOUYAv%s;=_o%Vy^6oIpizBBJ}GEyxQIv9VX{1x}S%EkX6 zdK28E|6R-(z^z3NS?@8CzSPa?zLNXFt>odgqh9KI!L0?qo%=Xzly@mlaJG4V1x`^B z<@&H6L@!lxKWq}xQhVorTQ&3ewmK^t%ja6(%cK9nRuexC`0X{+OFfnLIr+T05w8#2 zS_`wc9j*<@^0*avCt`QZb;JA714Hb6y6QMZp^ATHoj7Z~cd4b~$-M1)o&E>!sazj= z^y6Ayrd*$X&Hz7-X-)>`isx6Qsy~Q6&Wv0y@;l4>3VUZS>v{b?8&*WViNe@a!To@b z9&=G2;;Q{y{wh!LMWxr$^uCh0KJ0P+ml>vV$l%sGQeG7P!GDA&4*7QGessv{PCl>7 zgtKYqMXrx|eOfQIDJn| zsqQP}khwo7y#R9FevkO=>`Toh?uX>{VegE0yYvr&e}%ns%p-mr^l@Yk*_ZeXUMVvK zp8-93c*A?nX&03jyVkH*{13tlfO7@^;EOs=(F`%a+9dMrn#Y9SS28ck-$BkZV9v08 zf286RX?X@cuMhJpo-<&6)z-U&?;ze+9^^6M_Z9qu%vF^BK(< zWN*Tn=I!=sE{eS~JiPbGM}K*7xB014$B$Uu{|j5!8<$j0#=-c0{NqWsS#!#J3STPv z&JBV8M0`Pr;6R9-63Z?rBe9-{n^>I#y^9$m`4>(2SJDf>xjt~!uy-!cj?=xbWX}upEBKw!qqkMLKK9Z3ivK|^ zC!=w;dEV~l_QarjJxZI_l8>HweG#JXJb&Lc@xB75=r{Gg((WBD_M)QRA zsXg-#R|V1hszt|>8LgX(el9pge-xZe+mXWSp(SzT{o_|&Sp%ta-i?T=Kx z348~UZ{HttLzi#=uBM0LY@?6E{y}&F_E3Ki=St@Kw0C=%@Z)5pS|_xp9=+V-Y_s^y z(QRms@J(Re{wQaAdf6r7s=?=# zKwf~_)0@dRq36~j*N3@iwPTq@knj)c?<@FmkZ;F+kbU$TPX>Gj=6>83`73+lNAY$^ zowH_}_;J8PJ`i)s;6nK;yxVtl8{4p;dN*;l`>xtaJQ?uYk!L{9YokeT;zYPdKpb%( zxkvA}Z2|e6HE%ff&VhE_MK6`#?eGuEcRS{yI9Hfop_eM>3^v!M3Z6_~nv2528!h+@ z0d}{AhnMqLd|!D{FLhGOCj0oo@x;H9o;bX(@IQF3(d?X=$TL{SZ8tu*>S|_G$u{!i zfX@KG^MH_1;=Z~QxXI(H$n|wUGKc0@*yHRLKKhF%+;8l(&FJqsB3a(g3Hd*mM)@nAU&TMN$N7fN6?*jGs^y7!J95a{y!|qHO!(a{ z`-4wXZ$kP9!N08>o6}W1*sFw;3B;M^72V2kHZ=BdYS9nZhPKM7F@}ltL z@cRmV9Pr!ueRWs7+u85Tz6s=z_1-0(GnnhRADYLc_Z&;LA7pR1%wO#&dV#!_N8+s3 zEZjIl=O0AAUFJoDMZSG9p~NS2@ld zyB5%#0o)JpMfu$hZtXhid377)=T;=ovU+Q%6KiF%IrQoW?P=64;3F#k{oFWVAJHu-UpO^H}gHwba z{bk?bt{VnU?|!Gzg81$J>pLWx#qBmaP#*^z$k7eEy7eCLm`fj@mqR`xuA1BrzNP#) z%-QDq3fx-s=;hw|G0I=*ee}}9YcF_x=y~xTC!ltl;K`)u{Lai(v+Y_SJSNLCMy(7X zpI7aM>1Ce8XLx9`ch-Dfru{+qQb$r=boRc#=zS$~$k;ne&bG(U6OL6D-nA=_E+G!& zPsEd1o7#tbUa~ilNB0%`o$){Dq5OmR4&uIoj~<*NoSAHDq2Yr@% zr%cZ+sQjDuIP?2$9acKgp3YU0VZSbi437!+gU=FI4Y|I^jZ?SnIo$u^gqB>jAH=&| z%QH+-xjyu9T8*C@?N|Mi8CtS^|Dx&-$rESW=DuPcGID*7ADLg?;Srt;-dCyp-9}_g z?L7a_#iI=^vCUCga|_7tY->|Wc~N<;(08uT?Qz_PZdQ97&R^;0?J30x86#FMGJ5Iq z428t~2p~QK`%NS5+#%vz`M9e!KL5>+}@y5^&)>2MZVNz zL!scR;X8;t!>waE>V1V?D&|+<$-u+Q?{?;@NiRSz(Z`XzzG22+;+j@RZJegOmfZ6~ z{tD*`-tc!_Hw~Po^M>QT!o2-&>d|A5BRz31iM%N1ulRk1b2YELqu|MWk?^bHK<*{K za}sfD!P(~jLCfLI1Y^jkc~h&~STSLM1s4(3!UaRfnv(JME z^nF(CF}51PDe^6~JlJx!sc{nRotY#uCG3Dlj1W-ujPk|Q-mJ98_lnfLw;=jpNl^v z?g#z{nJ3fMN6+(ic*D2R`>NP8DaeZQ?W;ttk2%}$0soOihEc!Uh z-Nyx-2!AuC!O$Y^EA(;L8$N^bqE>M`j8D@Zr*i*7k-zHP=6`Ub$93W%gV(n}dj`U%?CTK=B#iiOZqh1oBtSJxXYgGo|c``XA&ugXSM(KEnZ> zkN$U>GsGM<3jP&%eQQ(g$w&WbT|45*~S!^dd#KwfZb}3!<-`K^-13Z`h)NS=-=(J?g@i8*-sQ6UgWQSqVM3d z@X_PGV*V9!$n3{yXfzi-dh{lC8lPTuJu|YTe1BxM3wZ%LxIF9AKV*z9e|3Gx+YXjy zU)S_L5?F4h=I!A1-QImk$Jqu?2LFT7YiTJs+nBefq}qxeeI@0u_}%U@bgO4#kkz8s zV-KocY977Y<=%Ouu8+exWX_A~=L~tYALPC>`%+sHXAoD7`_5W#g7fX*GsyXs?4_FK zMSJBRsq$0z70#99$-racLEKu&{Wz1hE5VU?ecZ=^$7G1wkFs8^Iy~-H1$nLKMwe!+)Eu#+z;l9?$YgX^n6htJA1|1M!p^Mt4~DF3-hZ@mHxTY zQhb+N8fPr`61+a?;VqlkV~$N!c5J#~ALSX=kL>9*)9&`ofKx9DJ_ERF=%w-=r>%#V z{W$Nq=8zYF`R#!shwQNGpUed%dx$54TwiCGr+i)v858}7;b6>P5%&W#iPwi5a(A)E z`9S2an9qQBd$P#MTqX`Adi3C`fdjd`N^s-vsguYeb$Q-?>xQ zT)MB;78}VE2mj#Z4bv1~l=#z=33s z3C-urA{3m_LLc2RUTD zFZC($Zs*>_!?O<>J-ZhS971`9=IDEdH)2jwA18Kjp8a^iRfBiwNAjgIrwDmb>0Ls9 z5c4a`sJ?>N=VfiBa>$$)#opQUebrm|=<#k3^U5Ng%x&^Jg9910Vz}x};J%umyy3`S zVa~wsE1WCl6fvKn)wo;ryx>cfxjy)DE(y+drpL`8Z#tOMxsu*+%-c_;T_OLV+l?yX ze!!Pnv)IkhEbITmA$kgfpW;~^Mc3Zjo7aY$vR#i&Xs&$g$w^6 zb8F!->8$fR^LH@Nt&5mn!NUtb4*w7GyZ!R%&Gj9+<~jd1YK-@J!L3Dqa5K%@Ked>p z=I!VYS|_B_xteplxUQ4ULz=gv@63K2_Ia&UIb_NGko`fPi@qa#^!oP|_*a#bzuLJ! zgnC|hx7(_{v(|SG6+9Wybldc(E-poQYr z{!Kpm;C@>LrwH#WoU2IUs!6_Rg~-WB4kY-ZIF&I=ZiZh#yG%LCo7f zBfoQr_#b2rw0F)UF95i;$jNB;RmS;Gn(h&&XtS7$qCZ&d?<@GC>&{O(G)ed- zn17Y3yq1`YN{@-`(Iel!P55!jvtLdAA-;prDj_qqm&h|Pw-$5JM%7DQD>&P3H!hyI zBY4P;#^LcjlB}{q${#y&w5FwXm^fFMGk`Cu^#^6H?{DHW$UaW<0qb+O8@;=)A2`DG zqHjcKBYg+4cjouiq?Z1oNB@xWqR}h*kvE*Z0GwxF9x`)^@V=U-xF0ier>FQVw=nuG zpOP{&cN1|SeXVDU{vf}vkdr}Pl>0a{PdV@XtmvimuoWK|kHlH3x#)Q6(R02Xd4?vM zi@xicCH8}s2YpIsu5X`csq&dV)$*eFA8d}y49fEyKlB&!JFhspq+(aLQ}WO8?de>Z z=8)kHN4_1sRQOWio2a53^4;C%3w}?lN^m3(uPyx#!Z-0Q`3G+W)<^6U^Q&j5M-Ogo zIC>3Cyp+7xfo8yv*cfL*kgYfVk zuIWj8oUX!`dM2%=;2+U*LcRO-@@J;aEdAB-O{6An{ac7x1%qL(`Ukhk#BOCB=w+wJJS8t46c@EVcpgV)k!=vl`y@xI!T z?VS8`d^_Ru0%v;*`BG)i3!Lo>i+jvpGu2agE&mYr)lSvNSy2AOk#B3P{!g9^_zdCI zJCw(Sd41^PV19+1409myZig4ZU2wKd`_8-{tRnuE*7GvWi+)NzFVkFKfa=j(3GWj2 zI98MwWnU_M^!Ok2C!f~~=}{}*H6ABls+Zs)BQF{&`p$ZOJNvw>4^CHoXYgbqOFpAL z&MwL!JNUd3@&S2FZbmec9|yTUJQ1rn${g&9P$)SA-V;-@$P=&Y!ry`)tA4v|ZF6 zgfEr(4A?uvW5WC^_F96^pm9HtZ|}2ex6zz<$kMwsMfjaV$mcagdBfr1edXeI%E`1_ z^IhCXV!WX}up_6V=&|L=ZK<7|V^p!w**L)Ls=I9K2lnfAPx&j4R4_RjHxjqV;I z&wyMX_noH+?@}P~88B~udR3j^^^IxR-tE}|?OmStc{XHR^m)UitYuW6ImM66Mg3KeK8Lt!UTQ9C9hDoqmgZOBZ1-~Vr#S=O zS5E2SD@LffsK%3tCm%gH+h)t=hF%xGi93x}=UUqIarFMdjxNvmJRjmlbJ4&5<=rlQ z6U=8o4tckZ`yoBN;EOV^k2yu~F6qC6_#Zq?yuNfXZ^xVg|AUxcqO6Mrt(_Ko|nwYFsBG}h6v*ax;;)O>JRF&^c{SUdh`(+y|z^yb`l)OIB~8TgvaD< z;y^YuTAph?z?>rNar)7HwVmcyRpLA7Onv7AF@Hte3tZ=MW5^JiUxBmTLHM0DZmrCV zrc8Ze{w12XH%1lA&C%Ug%xCD3U|~E-dmQ<``nPxcZ=-TW{t7wd<*GjjpBH=L&`S+) zD-?54_FD2>6uk-gKlrFG6?q2UJENESkbLyu)>ixr_X8di$Qt(pob9^IXI5<|uG&nIletTI(c!L_e8a{3O7dioX8^w){XyAx23HN- z4|w9h7v(&I#=qjdvwkirxgX#_%3L4&alpU&QoY;37nOX5MLG^7_Riyke-PXc)7}K< z87vP@FP**KGSND&(&)JAZ@RC-t3PbmLtX&(hRYuP$J8IRFgvXCJ9Ceo{m$cWRM@WT z?=m8D>SKZf2_L=W^&#JmbA>qra>(HI;eQZ&oZlTkqCHMK>ZK+uy+C;ed?gr-*XMzm6K`eKt68=^?|w zSnEZJL8YE!hMsb)BoA+NMJ2u4t%*~F{h-F#E}}WZgV=F~^NV}TPZoV=_Dx_ex<~W} z(Z_)ofZtaeDc1)dz1-vU8?e3G*oNThUHe0nZvuN~@EOd-T$FqC;B130I-YzJ%&px) zJ};cB;WQW3-tC<0ixr$A3*~o)kN)PdoJzmksVTn8t&HBwy;FR0vnuZ#GrQ(%JxA@G zi$o5Yzk`^I`U&m_pDXq|SJAvZ%RYW^obs4(t`GY`a6hbQ&M%^LF(3Z$*$&i8JzUj` z_Bb7AE-L$jn2WlRkKWX4nQxNo#d#O@CdkFPV6K_KJrLPEihVwlyyRdK2bqF6uDHGAdtq0Ybbs(!0Gu-B;+* z|3I8=%&*|#mE782bbV*!ucp%cihW+~#JRdb+z;eM@g0==!HdKf#k-w-sd`RPALXO} z!Gu=Ow*MN#qUBukS$p!ExT_f>#nxM&|mo|G^4+ zw_6)+C?|t_JMyCD&plM!TAp7eO&u@hR~Jt-?7mR&ds?OFdGURPzBBgD>&WND`SzFE z{11ky`^rZ6otgUqzq3BqC;3U)0^-U|AQL07CD&|!~P}Fl#}U6?{@UODvO>?Un1rV;l$Z?zUa}CYach* z;65SXr|=;$cMZ)ZdmQv}d`tZ)FRJB`L#oF%l+*vScEc_gU3qu>;Q zCj)+ajq*FE(SDHM?K#Sqx|Q~W_zpfgzmo4Oy?5y(?QuBYu6e`JoA4@|aOmUng-tCz z9vkH6w%Na!_RcxWJ`KzApX1h9?42i3AIE(4bX{H)b5YE%PK_Gx{abLdcwcdELh_Jh zPR53~AMAxHjZtkGrDp z{E_NSRFW@s`~Jw1>zU82svv$lzuVbI556eQ75EH;1P_^gUQWc>Mt_j`?a!uH74_d6 zNZzGhs^^6~1Ahm%4hyEa=nZ*sdLtKO}^=4qMM=fz{zom24Im?bX z+n6(OP6qrd@Q~%X;ywFNGZ^wPb-$7SH&Em(&=OsDY%vIBP$ar7D@BEg|3-E9GtB{f# znNO|SVQd#?_5bD8dgy%gI}dwN-yvZQe~wvQ z^H%eA_QV}nvSh(xuZ)p>oV+Q2B{|#tKgeE7@cOc-H!&gLXC1#Cy$NsPi#8t!AP+Bi z$jHeoEZMm~RC!FECO!l54BU4{u8+M-bBWhCQt^;6Z-;LJ{z2{!x*HzEHbv#CbEWlC z+Y=A@W$F)N-d=9;o1@FnGS8Jk?H9djGQYB;IRkq1$hSured8V^UfVFaZ1SP?BHtdU zINSUkM4rLa=QWdjsq9_4embwdqi*jk&sB-1tFDg&4&+bdOHCK=_D{tBU_SNeCBI$& zzQT7<%fYrJB#_4L_-V)tlcq`_| z@QLIFV7};1krxI3O3RCK4jF#ur3(&_H{62ygQhuT+*dqjkbY_L16JC(mO zWVozcK%Tg|4HL_zl6R?zJaL7hKPcx{=nraqhKGqWh%X90gDGG1ztndIw^rYqFetxs zJL1Xo5&c2-0_gXH@P@;eih27VC+_U7DL9$-MM7`UADm0`t5)LmZ6vPRo|x-~kdJ^u7t~2jSrbpCOCh z?aXiI@1Wc}!#_BR<_uk3`uaF2{uTT<%x7S3tsQwxc-}7aqB7UV@AhWmw=<`xoz1p7 zzvJIiPR1gkBfZmLO6MpB1 zv11Jv7WbULdg_D`tNZU&K6-gybxGw4ehzsIISMT<|YVUmU#JC%kw(0%dMy#cGJA0QTp8k(=v)-O)GowB-W*o6tVOpU#CI_B{FFnJ%dT!{PV|in zGk&mgIrZpePDaaL!AB4OpzKY6f3-b(V4J)sxV0_MS}HGq-bcS%afv(;J!THonh0lxc zEAU01ORuCpj-H4774;^}M9(Xv-&C4k!GG(eYTrT3+js5{sdiI6FZQMGBVHf&ILuW;zMb=;eFbNmdtNdxiari} zshErMchK7AvpPSKZwFuWX#7CxrN-0Vx!Q3waf-lafG-vKD{$2~-_GxLxp(F{!v^B@ z;X9b?|0(?s!be|7duMQp($7zQgsV13yxVh~f1_S%;?jeLH-s0!oxGO5#1~}_B;Hrp z4<4+Vb9@W&WWfD^ABTPP-RGxFb*Da#)|-$VNWF*mI?dZ}MtrIG4DiHh@2eYPE{gY+ zAKh1X8>b473B2JAh9NP>!^bN>j%klxa%;Jd)5~eY$Z%aw2Dv`|AGEr5>sYq#JLr`% zJ-3kfSJ*p)-wy8*czw))%pBRrX@*_nP5)Cb?%hd!=l>|44Dt+kU$Gb9xcCmDmn!EB zX4JSbd!|1+}?{?|61h-c6rJBw~!IQZp-t9YW*Q)QJmg~d2-G=g4 z-zk3kiFl_r`F72h3jUS!n6QU8tvJr4=Qa6I`uWnP#vV@$@+ZGDxN6L2ka%x8r@qb5Zo2+b0H4&kOS_ znUe`6&h{JBA52jBcDWx!FEy&o{0cne=mlRbNfvWa{tkK*4>?zHYo!+edmJC}9Yl{_ z>kqCMeP`YeG6xdh!HA91DbIkpX#C)qHhyRJ0$_dx?g#r)F=x0*T(ywuG34QecS+;7 zOP-AMP4HY4?<>w*3R8` z?Q{WoEq@v1>0M1edd#nqg@;$}2jzFrB4J%>&-`zzW*sl7v$MJXZ2RFGCl}AXBXTnE z4@&;kW#Qq~{s%cPig&x#AB2zo#gKl~qh}BAj{PAzZY_LXtp_XwXB#;geqX^GE`9Wv zw^ylmJAVhIZvuN9&R-o|vf5Bg-@y{{TCx{FyRRgtNb(uVhLsRcM(zhA>0C)q95|4R zR=j6C9@o5jk@5oI-M*ao?T`ArEJU6G`SvRE4=yCXvui_TH%EG3NpE<4#I3+h#BaAW zJ4Ej*@Q~l9-UN6u4Ug<`Bu@rjOLO`k^gsS>RiFHn)Q$-j#-HO~POi+3u1GlQQ@g|B zSMu<}3xFIl=dYNvU7&jOS}zs(_K(jOG@1A8IB2$8j(-{5SB1+qhZXz#sX4<0{fttJgAb`kpF8j^$|1K#H|Tow z$X~(lJZ0M=(evVbJ9x;AH)q&6IAx5CRGv8e4>JGCTXEIAs5jxid`gO+@J)bQ%lvlE zUwuKnRQBV@{vhrvZ7#a|aDT^rbSNA+%%%@fmh@dul*O!TrFy zoq5QZGjP5=h~^CF(KEljUffrb*T-Bn_L$&1XlqkaH~aXZDtpx*#JP&0JOl5Y*~1I| z75kk}kIHwp>AJc8%IW!(lfiy)2J!mjeWm4)nSZr4EZ0AXc*xqj9iF%_^3kUj&)xT@ z_zoh^fE=>6cQ)lS$p1lY&S3NZ_&OK=p6mYq@5qd4Rt{m5>QYiJ<&tu_ND^jdNi(yf zjhtpDhS@%!%`kRy$Z4~K8P*zRW~?QWl9WqnrTQwTF-l{ZF+2QjkLUCGdcWVF>H7T- z@7w$Rd_SL$$Ng^Li{d-@+oH~M)1;5%cC+eC4f!SxQIFm!`4QPOC?EZukQ(BCaK2sT zWZ+#ga<-L!5IwK#fkWJG1k4FNSZvdrzgXMi6^ z-JRE@Inchnf6Bg0lkBhH$3f4_+QMT3t{S}Ivo+s@<-Xmjn-k@)bgmjaylUT$zVj9F zhL2tmul2mZXE;aygDaLClRJ*SZ#NUay?@GS;kS3m`LcAn+;QMbMIT4q2f5@yddc4EU#_>vm-@TL7k$#-X{_sjGMCYH3(xHpZ+NNJceW<3niu7euTT!z zm=^`Nmfu&=o0|+CUgVJ3N54s%+u1*exqWUQUjzS2@%nIgHu?v9NDleq#U{!1aSoaH zqEX9730KXZJiNTWLaq<{E7hAA>b8~kS1mu=X!DiIUzHx7ugzCa2v-gMLG*FZALM-d zd*rp`oXlA*-_Ca&aMk#Ir8wL0m?&@fdf~SthrGCW#Yw-aeKwW!T&2!zCO*Sz>Up7$ z!yc14TD~28QO?Q0kHdRW#eqa0N6lBtM}KAG1LCT^N#8;Aopb3v$X)>Um@vN`eH=Bn zgC}#6d=u+5o{S^yueP@855f~yM%<6OvWvNA)62=ji@oTKJ>7`chy4|Ewm+sm&a>2` ze?t2XhVA@weJ^^h+P2Cw{9EH+y+pkU_R+s>F<*h#$M?Yi&F6*vRmI|TEiVc_1Mjc0 zOMJ6@Q+=1&nFE%2rv_zh(dKsLG07qCQrx^q*|+06$nUF#8ebG1-hR|~HWRn@Tj}G# zKRArOgY0+K`R(vcpqCm&oNdxsOv#Ts8P{n5zcv2lfm*G%rBhNfY@daCg?9 zE8KBxo@pGOPkkKt=+R5%`yhV@Z;L1HVO}4etJ;uTQHNtMXnRrc`mh&8Z$jU<^Zy|4 z+rin!9Y=Xg;055E41Dx>u5|xkN#6720cYza&k(Nl=xxYHUo>ov+;Q++F;9m72a!YO zo)+GwPgMkI7NJSMjr>9qUM-+7Cm~aqtgolwEWer^`Z0~ zbg$b-{C4(DfQMWY^_O^f@g3}PVzvbb68Wo(#BT@pBgNo}gD`I9^}O&um_mC7 z=JhGx1bdgjL-rW@UGIH1fmLR@JEND{MtrHCmChv2ww0Fa1D`?fJ0EZyPyWG6a>p4d zUI64|?5qa6{yF4*&$}VFqRL~hn%)w=sN&YL&#U{&gLHQe+%R2ueayeY`>KKRSGYUJ zdzi=@j=ku#q6M3qv>rX)S1K>ce&>%V*T?=r%e(WmRKI0*X1`_AQvIoq1Aco%pY6kT zPwq0SO>_>;?aUYLLOf*L2hmGi^f$Ra4fdMPi|;tdA-igK zXO(Yf{uS@9;G2Ml*VSB3^VRLLE0XKO+zt;fda3AnaUZ9hmTzay_9@~sn2O_123GAO z?g#dw&62-5AL>TlaLfB3d&8?J*SGO}-v7sInXlb(IM49r3iG-(`7sDBlN@ z$Qy3GFok%1mA&_p-GfF z`-9+Lv2TL!&OND@8YOoezB{)~3ZXrN^)GeQOC9ERP20DB^H7gIVzbTaFXP`dM=XDC zZL#bbTKAo;qg`k1nEc_ex#Tg?^9;;qKu+ebQ`Xh~_OmSf&hVv1lHb{K-_D$(FygnX z-URQj2F2E!nq%gKSJT`Mo=o%4cJduWZ$i(>jHMhh_zW+Ejivn6pHX*0GCZ#i`HyR3 z%U72kwa8ypre7ituj=EZdArQLyvR**eSg>Uf{z~gE9DP@IULn?X0jdQ!|JEw@pq{t??DxUaP z@Z+>0uO;^F*k57Kzn|TcNQn45Poc0Xh^{GA%xF5JX z_ZChOya0-S1-~=zMRCX3GVnk2KgjQ^Z;7*w_mz9ozze&N2Nunz-o!}p(f8GOGFxPB zS2<)nS99opkolsVzry~CpDWdy*fF`M?AsMjrf0utP7O7F@s493Uu=GA<#p*hKfG@@&h3^x`m)3yXn%F(%($E7 zjw#yy3Vvs`XRvw(d(oMvkCBHLy@{{Hmx{YH<|_}Gui*0nw-!7Z^apu=h5jJ>yzst4 ze-ORY`&}O!6zIM!s30P9X$$$hwg$}<5AQmnr)X|hKKdWT!^>PX?Atwjf293YR&6!;QWMvW(sD9W z3n$BbrTos=w`bG7eTMrZly6_J`Md%thm8Cc_ze6VtQ`@@`V^d`{rV($`jw&5RSKEoB$IJyrq2aqCzo|AW|z z;ydV1bGt2Z)dIA=C^*~bJ6lEP$E`9QUA#p4IBGAdcrx&qpzq9k2KJ@4zT@z7g}o?z z^ma*Y;`f@LUU{8*6A?7Gg9C}XGxAp-Q?8GDUhE%K9uxNCT+Y2!c8NSD$n~ilGPob; zc`^562l-Nak$=$M{NIGP?@PX2aX;8U$Q~2;CKR7R&-H<`Jy7dS zppS$9!S=-KQ~4`*nXiyTRvzBpa(|#+Dm-!RsE_j{an-I%fAC|?M~|Eg=NaIe0Jj#t ziFVrmV150&t&eRIoGH14DbR(`Mf`>M?XKOdo=;lo8Vrm)oR{f!AGy|&fiB4jBPaC zBfs-&;`6fHx3kZSeG}ZH??v8lNE22I%IT79zi~O%#7D$>M12>#WTjoyV*q&K0swc99%%y%5c{lNYT zbGv_hlz0J<7v=nwx;x{!vLfyW-*Ez~;>9=djW)OI-$Bb>DsnQOBa_89@mJXe@dEJN zj{MbU)T94hJSG(~U#a}nc=F?@I}SW?!8EsHFX}~k2GyGw9D6!)678?RDSE;1Tw&i1 zJ_GuL@P>Eqx1RV6hZm=ke-L|yLh?=6X?q6T2RVPWl{iIu&&w`4D{h_X;Ntjs;XWHj zJucrt^irP@j|q6lmRvRPMHQc+I^b2eEaBGTKFHps{hE(n-Eq`+uuA?1m3QgWOWn!u z+$s4DEr$&L)p+tvygf2So+~TUU3sp+LuPI*drUYls{03jqP%F@%x21qs`nN9Qm@N> z5c~FNTAl&>E4}ZG-URz`TKDMHbG3Y3ik26J-`U;wJoTMl63(_e?XMIM8Jr^QMbpxH z=X_E+_jKNkjt&o=wi}V@l{foNw2f&jd3e$D;vBNh7v=ei{W!Qg>;HrNT!Am@Ec|xn zs{KKnZRFbniL1t*IQE8vQ&dm6K9#>>pBM7&R`ea@eEW0d!P;|$Tpyk*?xm`HJO2-= z-UN6u;EQsuZw}?Jcy3>5I<|O`%E zkmvT;;$5k(uG@o*P;Pt8AM3(RwW>=);o7at5F`v9k?03f9nR7C^K?ULkz#RuX znWnCJv{fJDjoZ0t@0}1{W-*GTsT@Vf=xF7HW;O>m~m95n)u73>K_)u?xy-N$u?LNG4 z=k@hZt}G>9pYrf3K0|fDT=^gTK5~5U3Xi3Ow{@S;^x}o$;{ipX=gitaDreB#uJ{b> zAH;m6@>jgSLcU$U4{~1g+QtV-fx<&h&wo$rrSf+WyuMcqd4_#9{#A*@li{A1E#*Z| zP)??;gY7eo!?$~7%x;kRO6?gAI=bDwe5SVS0`Ull4c6Z)@a^JHJHGZ_eit<@X z?z5@3HKKelF(ZdT6)@RqS0?NsN&+xJMyt;}P0DYW)5cdPz+6b+e z%G{5MzHou#);X$cy5A#d*<6tv@(ha>(FpD?S6hgXnn$ zYjZpJ?a1|YB!0W%$*Aw(3hAY8qdddUqsRDO3Qbt@xoKdW&BC;qd4~5@eDQ9|$+RPW zJ9|tDZ?uu;DwDk7;Pr8D0v=w=eEaBxPRZ?M-wsX@<}2{}%44sYMjP&f@TDFkuchnC z5_8*lo1_`kcgA-RT(zNYR|CSr?-}w8=E2$BJ&AwiM|WrNWQ;!gK&QuB@sNAW+M)6K zkY@-m+?|mZy(PWW0(q`f4tXK7g1rEi z{HvL#KP`35Sws9Qc;djnQqL7QMXhr(oa+NmCRy8GJxIDr`zz!b8e8RL6#q)^J73;t zwQ5>?w0Xpe6%YLn{v!85%3y_V=Z--@b;y=-!$o)>%*2PoIK=kNmJZ11OBpW-3oK8X2>bA9ML zgMWpbjP6TaK>Mrzm>1Lkpqj7nTrKojN_$bA-#%O0Uj=CID=+Q2LXW<+7a)pq$bnAv zHU4J@lo#doTN|}}r1>l2iv|?!A;0ss?n?(R_i(2@!vncHbAJ$COL%zUAH-f1c~Njb zu)iuL4=*^|x;LEtgX}R;dd`B2ID3~`-*MPu!alEwP0wE_JwBD5EBI2;^V&x_nS0{L(Z7S7XW%^p z_*W`_#eQez6d@-Q+GiK}age`44moySq~zQ0)J$*X#|e|XDEH_sJ-p1V)xDPNOJ&Y> z)Xv?)Z|_O`EA%FKZU+xpan+EM;ap!g^WnrZ=~r@ZKlEIo?|e6U+w4p)yAh4#OGUoD zr=jo6&lURz@xDr>z36AA6^1*G;uNir{@?>`ZpZ%0c~Fr1cIi7aza6~^>_x%9;v6#f zarlmNvvN!ABhFj<|1f%@?>X^1H!lER6nh5b8PIoT{uSr1dgUA~eUEs^*3UEv|H_z? zNw>J;lqL2gz9{A^a3C%D?Ux^Vu8QeC=xUg+tdH6VXM4EY72*`(eFcvRdi3z6zHnim z9mgo;=lmtnSp7#vdB{xsCt)5~ZM&Ci? z+cCEvT)b>vs80s*MHR0Pe1_M}U&l8KPX>M*=E+PB{#y7~$jP8LF;%#=(}^bopI7gd zHw|9P-o&l#E8jtIKfq^rmHIf3OK)N{-JO3-Kc8DqJmgZEuhhO>aX;V%=%D3Ab>Bp> zx7v z-aLQiuJ#>d53lmk|Ht(jakl@8Iv9J+^p<#dx$hi7yuRPpKe=+B**@MbX$IwFtcWkl z-$D3snoI*?D=F8fKUcm*k(*oOeTDrM=dS{_{T2Fy*fY3l+*szj`Q|$SM87W5t?tJ_ka##`;dR|lBq)S?d;(_ajA2w`yldH>OMH0{5Z@hYK-18 zJKyU8`3FDHa>#8G+nB#j7?@I&xvY5U$q;#8v4>aRGoVM$_rdol-#%(ZlyJ6@L*_jL z-<|o6lS7_3_$HdV?rd0deyY}^SNV3lub6*zcC@F~^MWT1J}=!zkN1@^&j1fE@(l3N z=MZNb+>b}K{-B5O85B>(nZAQvPkeEyr3HBgwP&~@c~K|fi^50GoFdHa*oy`gSRJ)$ zHDBE&FM#snux|pKZM)T9QvM2_xc=pVm7cgV?wG*xOgv$p2uV z*7M>%4*Jf<=ZgK#nRa^D{w!sXE1wA3jQYY)z~|fL+1I)n>gF> z<4o81SHG{1BL3A_%@b$*ALP6!@>k05{8qx_;&=W__E%}d>(le1*_6KmuaEN#*k2(h zb1LD*loGi+?<4OL=S9J1SS5aE<{|U{;Bw-s@ty(C6?n+tesGT-zEs^u-$e73y5nHJ zdV_Mvv*bI-ew<(E?u@-C@}llTznA^h(&A$AdBGFMdj|AU`ME+bRrv>%cL_XX_`JYF z?rDB2!714}BY01b6Q2^V4||5YAvfuLg}J?_zsuOi2b8;vs#|of=%3v$>uwhVqQdXeckqYE3Bd^(w-!8P>_wGtg8k0in|OcJ0*k#U_q?Xj zbA>+6G`iyiFMBsNIBRoB9r0wqL*~9S{5ZzEDDKYv={|`6L3rYjZ{MeJAi+b%cd*e^ z9}^jVAz-B2hJn7F?^yKcOT;&^_wWMZY(GBW(XrkA{~0zarrLBU_Eyy0kPXxyY%#pA zMr*za7LF+r$6aNZ18NElZ_nqN)?x^vv;7k2;$a>HEkeaBEVy~J;$Gi~Mi+ZW- zwZxu5=ZmUds^S!3-;O)ZV2!iQdxitjqdz74EAV8dpZ%+5x>Hx_4<25eY}y`Y9o>^S zkaOj^!v4xDHH7Yiiu++-?Mptd{K-A2@4VJjzBrD)gP5=G)C6mLQS2G4S4WZ;;Q0#^ zn{vA^9lXNeA4DJLRmn40(VhXGIGgwb=AJ8Wub;Q`z~M03Ga!G3{@}Qn%hY$Sqr0=> zY%5MtZ`zBhJOg@O;1n^huXVni^H;pT()sN{RmIxf*?QrixFh1V{C;#caUgLYT$P`+ zZup9YWTc*BoNzP*LGwa8yJ#7(507vFJo{uT4v-_*W?%x_2j3i~VY z+ui6s_=x1&(W4Ki`(O!iYZXsM?HQ`1=Y@T{JM9^^(cO6;PGq> z{KTT?HHYS_+lD?4@7v+W;XMQPqVO)EKd3ys-TV1F-K+6EJFxu2yyshu4edE8`u9y_S0@*9R|vvv}gJnGQ)WmHDEWuh7SNH2L*}C(H*ZFZ!@|X`IR0 zR2Q8$JA?KNP7e8!zXDec9uviZchk6hPxpX1v%f2Dl;_vwdP^}Otef5m+q^d|7WVn0r?i+f!d@nn8q-)Chp`6l>% z)$(&g{p8MD4LoFUKh&N9bGypP@V;Gn;_9{im1SPEPKXO|s2sAR`OAbs zDTSGd#W5#m2wybM!o$mc9Q+Snm$@C>58rs3RljZYFZ4XNnYgu1M^kS1*ul%l zH}Mk9SGYS{a%-o{o}o3Ti1`eh7yVQGgWw_a`>I1mN6H~%FM2iiX4%yVeb<9gYYiF z6F1@JCAmAJH^Kcu=GN+WXUy#vP2)8F6?&=IU$J)y-@!J-zfyY!?60cCN6+sob;o&* zIFO1b!#Np#uF!YJ9cRhBPc%PH!Q?{9A=^~{MSBJ>x;sxNemnA4Hq`UN+}@q`qG{v> zV2=s*SIUp0crxr=LSEG91*j)}JN67)WZ&L$WO_m11_zC^9lG;?#%JiL-Er`~(z&%? zTzc%pygh9)x+K3&cjq6|f6c8g`@b`9YjeB$AJlsj%vJlmw1;87Qaoht(dWy){Zj+C z7G6u8f2DiF(M!#1XzALK_@ep57tL8(5Rn!1o_qU2*4=U&HX3@VVdl{*maR(`PaJ$+ z=g8-U`AT_sJ!LPt^yISQqRfG<_6)5#keoxt+@41JcJ@u2OFxj<%h1R9RQnFz)4WUl zsgKihK$&lfkY{kO+w0P2KzriJs5_3Hze1h?J$h#g&i2JKZZ|6&llo2?nd*;-^e5rU}Wi0(LB0FfhyURoVLA^)My$St)@ThRrb`Ym%w|IELZ~vP7gUl(KPV<$G z#U1A|`RLt7uJ60g(evi7XX?lsuHILAZvtKb_AYTBr(f8Um!qw{2n>F%6EJY@L1 zUW$Dn-X-*%r^($}aX)Z(zOjDc&Z5K7=iaI-rQQVQEA~6zj=B@F$@2zrioU$`nDE=# z3jmKvE%lub8unMrt<9Ug#jEXz`%gPL?79(r`is)X>5k(-`}PkrmlemJoKf|m&AHxg zL-Qx4Yu+WrfpnBT1M&=tFIq+4LEek9#{@j&X4$s~x<5APzLsyt-5EJ#?5|QOe`V>L zKn_{=O~5~B><_|^gI;O~@%kP-Z9_dT_J&`i@1Wbrb$$2KcaZrE%)f#!b@rY|PkeRh zLCbK>8;%~m+B1Ly2|mL<;%q-5-@)kyzNqqAGPhRmc`2Sur1T z-$6?cFTR7|U)7wlr#yrD9|Q*yejI!Ud-U_ye5uc^ja>eQ`J|SU`MbSnDDg#&UVzr~ z)qLVJpqJXqzdyaN4#)nX`3H5M7kDy{n=2B(ZFP6%{nbQMLtJBY#_a80jl*q-Qv^?3 zx10@WHi_-bUnjhfvOhDXc-hGS*^A=tjC}iHQ|gi*Lno3KK=o47@)KI^+wmPlABTMt ziZ8m|aCe?eo;dEM>f8@JS9o8s$AsthwKH=jc}h-3-JMSx_E*8;A7noccrx(Ochc^I z=+PtJj$Y~$=8<&AQMtZYG(KJxz_Bi1)lpOadTDbYasx$G!%uAL(rM<8Gv156h3i*^A;ksCdZ8w{!mLEahaFC&L^_58v-cZ|m=1nA=%K>7{aBl)r=U0$i8(74xsy?~J|ZHqGy>zpvm8SG+#ODYE1H82mWks>Nyj!5*`AO)eNVzt0r=cGbUYK6-nz|FWs60a|Z@ zzk@xbkMqgmCGx&P&&!{ltAAMh4>JEs`3JuhKMwQTkwdPpx2CxrJQ-_xuK11v|6qyB zlLOid4;gz=~ehOW=#DI}Y|&oNwp3y-M?j zb1#)SMYub2PDanSV}AwzpozTUn6KKIZ4x&KuTS|>!N0mkIT_^*Pw#ukaoo)-(xcb& zSIn)o%*iPKpw2_K>Oz*2n#M!Ph zwZzQTya1f*!+g~)sl|Md@}kUdS3G3Si^97Ezw<`v4<5Vp_=y?hqvt!$chvI&PsTW3 zfrrfhgY3uo;Kp?NA4G2g`>U6vj}sz(XZXAtiPy(`(eEjTysfs1e5rb#;VSv)*O4z3 z`}XXhLh8}8hZj8LhOWCB*3lj3GVw(*U-2D>y-R9tS9u2ZJL~&)&NDce?S-=)bo#>^ zZ5^zh(YdwoT5`TUweKNAzP&W@N%IJLU(MU|=!p~ZTp=%t{T2Hr;G^$7V4w87!o}}w zOM8aEg61RkztqJT4{dN3*wC^DL&cRNdrSDupJuj>1 z-m|t(E;8_B6#q)!x7$ACAJlVwM(+~yMXjhm$aiP-aoCs2`>V+Civh2@Z5Zg+xt4sX z;C{4`{M8?HcUHbs#o6YZjLKixSbZk{gOA93h3BfK>E?t|c}EiGPlGO%i|%@4iD4K0|oYNp-7mnnb6-f;HgR10VO4e{_^Bp*HegUVyF z!@#ZOJwr~zX7b}G{uSmc-d{2QO8K1+n_h`)S&&LS`X77mA#ZrBc6Wv^Ro$HxPbPPE zU9`1n9OYz`H+4YLINy%@pz>O>-x+*S_N9W?$J`Iji^Aun?#{Rm9yP6^ z95VNv;df^LAoil_KIru~KCi5$1%Jb>1rJ&A+u`%tR_jRIT6f={LQ|Jim|l%*r28Op zee54hko}c|xlLkvnrqIdk{4yJn)2{gEqa7_GHYos%KR(balqLIPiE$xZYPdQz8xN3 z=E;nq{gpG_2j8k&D4Zfa*XLhgbrk&esp7Q^GjNI$JYuEiRig3R-=MuHdzaK52YFH6 zx5MY9ya0NxFL0T)IbhkeR6p^C`wLITRXn`qv~S-$s%yU>${~ZR#<{)|@%F1jHwTb6 zd;;a$6Un>uFXF1H{T29EEk9eg%3mo?5%TSw#`f@kQM^lP-#%31^|j0BoIE_Ck9mLM zx%A)Yj^q9ix0dI2)uRtS{rN-QaFfNJ0lidkwz0qZ-`o%QQdJ)Zygro|#T`faCg7t- z4%zrz%@NLaj&Rk$XW%>o=daMC*Llco6X?;S=f(Rg@Z0Uyxb`*FL~cQB9kqRjn3&x`Zz%DZ$j z-d6JM4|_}+W9AupUhsx9|H>|YKk-E)C?_*UFyl|wUCc~(lOtM?t}29@*T(he|qBLiuY-L=d`q5IiDK%SGn5U zuKQA%`vL#px9NuyyPC%)bWC>2c#nMaxZ~UyvPpdOJh%6zJVTkwTXhTQeFgs@_U&z? zk7M)8U-i@Ej>FFt=JvOP<2;rR-rW7&roI=-s5jBF3irW>^U4IyuL>??gx5a$jKPJ0PygRP3UH>NIa`~c)35w z`F8G2@DIk(-2P+laYJ`X-}$I%^^$X;Z~J~bdV7Bd;>mDc zRQGwQoQ%r#F<+E@Ud$KGYuI@H9okRlAgXHvJ><<8&~1O!$3;o)_muk!N^n zz-||hx-iMNKS6tjc1ZynLJA&8u5aVO5wgEhcbsKf4q4ALs6GzzS9Q{x0RKw _}6 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

+i_@>H*?kwIuLd6#YSGeQ!@EuA1R7 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�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 miPNkoYWQ(@?+hL?zE_TYTe$74 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 _}6 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
" + html += "\ + \ + " + } }) if (setting.can_order) { @@ -1072,6 +1089,10 @@ function addTableRow(add_glyphicon) { var table = row.parents('table') var isArray = table.data('setting-type') === 'array' + console.log("------------------------"); + console.log("table = " + table.name + " " + table.id); + console.log("isArray = " + isArray); + var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS) if (!isArray) { @@ -1115,11 +1136,17 @@ function addTableRow(add_glyphicon) { var table = row.parents("table") var setting_name = table.attr("name") var full_name = setting_name + "." + key + + console.log("table = " + table); + console.log("setting_name = " + setting_name); + console.log("full_name = " + full_name); + row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) row.removeClass("inputs") _.each(row.children(), function(element) { if ($(element).hasClass("numbered")) { + console.log("A"); // Index row var numbers = columns.children(".numbered") if (numbers.length > 0) { @@ -1127,40 +1154,70 @@ function addTableRow(add_glyphicon) { } else { $(element).html(1) } - } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { - $(element).html("") - } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { + } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { + console.log("B"); + $(element).html("") + } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { + console.log("C"); // Change buttons var anchor = $(element).children("a") anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES) anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES) } else if ($(element).hasClass("key")) { + console.log("D"); var input = $(element).children("input") $(element).html(input.val()) input.remove() } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { + console.log("E"); // Hide inputs - var input = $(element).children("input") + console.log("element = " + element); + var input = $(element).find("input") + + var val = input.val(); + if (input.attr("type") == "checkbox") { + val = input.is(':checked'); + $(element).children().hide(); + } + input.attr("type", "hidden") if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length var key = $(element).attr('name') + console.log("row_index = " + row_index); + console.log("key = " + key); + // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length + + console.log("num_columns = " + num_columns); + + console.log("input = " + JSON.stringify(input)); + console.log("new name = " + setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")); + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + + } else { input.attr("name", full_name + "." + $(element).attr("name")) } input.attr("data-changed", "true") - $(element).append(input.val()) + // if the input is a bootstrapSwitch, we need to move this input up to where it will be found + var inputElement = $(input).detach(); + $(element).append(inputElement); + + console.log("input.val() = " + val); + + $(element).append(val) } else { + console.log("F"); console.log("Unknown table element") } }) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 0ca0cf8232..23bb3e7abf 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -44,7 +44,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() : QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH); descriptionFile.open(QIODevice::ReadOnly); - QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll()); + QJsonParseError parseError; + QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError); if (descriptionDocument.isObject()) { QJsonObject descriptionObject = descriptionDocument.object(); @@ -63,8 +64,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() : } static const QString MISSING_SETTINGS_DESC_MSG = - QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.") - .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH); + QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") + .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset); static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6; QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection, From c2858f847baea7f9fe8e8f5e161772f98e659ef0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 31 May 2016 16:57:23 -0700 Subject: [PATCH 0266/1237] Partition vive trackpad by center, x, and y (not just center v outer). --- interface/resources/controllers/vive.json | 8 ++++---- .../src/controllers/StandardControls.h | 6 ++++-- plugins/openvr/src/ViveControllerManager.cpp | 20 ++++++++++++------- plugins/openvr/src/ViveControllerManager.h | 2 +- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 60a46ba3ce..df78ce7e0d 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -1,16 +1,16 @@ { "name": "Vive to Standard", "channels": [ - { "from": "Vive.LY", "when": "Vive.LSOuter", "filters": ["invert"], "to": "Standard.LY" }, - { "from": "Vive.LX", "when": "Vive.LSOuter", "to": "Standard.LX" }, + { "from": "Vive.LY", "when": "Vive.LSY", "filters": ["invert"], "to": "Standard.LY" }, + { "from": "Vive.LX", "when": "Vive.LSX", "to": "Standard.LX" }, { "from": "Vive.LT", "to": "Standard.LT" }, { "from": "Vive.LeftGrip", "to": "Standard.LB" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, - { "from": "Vive.RY", "when": "Vive.RSOuter", "filters": ["invert"], "to": "Standard.RY" }, - { "from": "Vive.RX", "when": "Vive.RSOuter", "to": "Standard.RX" }, + { "from": "Vive.RY", "when": "Vive.RSY", "filters": ["invert"], "to": "Standard.RY" }, + { "from": "Vive.RX", "when": "Vive.RSX", "to": "Standard.RX" }, { "from": "Vive.RT", "to": "Standard.RT" }, { "from": "Vive.RightGrip", "to": "Standard.RB" }, diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index 79c23bc6ee..4e0269bef1 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -44,7 +44,8 @@ namespace controller { LS_TOUCH, LEFT_THUMB_UP, LS_CENTER, - LS_OUTER, + LS_X, + LS_Y, RIGHT_PRIMARY_THUMB, RIGHT_SECONDARY_THUMB, @@ -53,7 +54,8 @@ namespace controller { RS_TOUCH, RIGHT_THUMB_UP, RS_CENTER, - RS_OUTER, + RS_X, + RS_Y, LEFT_PRIMARY_INDEX, LEFT_SECONDARY_INDEX, diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 6e75454b5f..f006c436e1 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -284,20 +284,24 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u } // pseudo buttons the depend on both of the above for-loops - partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_OUTER); - partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_OUTER); + partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_X, controller::LS_Y); + partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_X, controller::RS_Y); } } } -void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int outerPseudoButton) { +void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int xPseudoButton, int yPesudoButton) { // Populate the L/RS_CENTER/OUTER pseudo buttons, corresponding to a partition of the L/RS space based on the X/Y values. const float CENTER_DEADBAND = 0.6f; + const float DIAGONAL_DIVIDE_IN_RADIANS = PI / 4.0f; if (_buttonPressedMap.find(sButton) != _buttonPressedMap.end()) { float absX = abs(_axisStateMap[xAxis]); float absY = abs(_axisStateMap[yAxis]); - bool isCenter = (absX < CENTER_DEADBAND) && (absY < CENTER_DEADBAND); // square deadband - _buttonPressedMap.insert(isCenter ? centerPseudoButton : outerPseudoButton); + glm::vec2 cartesianQuadrantI(absX, absY); + float angle = glm::atan(cartesianQuadrantI.y / cartesianQuadrantI.x); + float radius = glm::length(cartesianQuadrantI); + bool isCenter = radius < CENTER_DEADBAND; + _buttonPressedMap.insert(isCenter ? centerPseudoButton : ((angle < DIAGONAL_DIVIDE_IN_RADIANS) ? xPseudoButton :yPesudoButton)); } } @@ -460,9 +464,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(RS, "RS"), // Differentiate where we are in the touch pad click makePair(LS_CENTER, "LSCenter"), - makePair(LS_OUTER, "LSOuter"), + makePair(LS_X, "LSX"), + makePair(LS_Y, "LSY"), makePair(RS_CENTER, "RSCenter"), - makePair(RS_OUTER, "RSOuter"), + makePair(RS_X, "RSX"), + makePair(RS_Y, "RSY"), // triggers makePair(LT, "LT"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index bd5d4a39f4..e14a10a94e 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -61,7 +61,7 @@ private: void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand); void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand); - void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int outerPseudoButton); + void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton); class FilteredStick { public: From eddfe4847a1aee5016b22953dcba198744d4da39 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 1 Jun 2016 14:22:54 +1200 Subject: [PATCH 0267/1237] Select nothing by default when navigate up/down directory structure --- interface/resources/qml/dialogs/FileDialog.qml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index df609d2432..c8748443ef 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -393,11 +393,8 @@ ModalWindow { } fileTableView.selection.clear(); - if (model.count > 0 && fileTableView.activeFocus) { - fileTableView.currentRow = 0; - fileTableView.selection.select(0); - d.update(); - } + fileTableView.currentRow = -1; + d.update(); } } From 4da1c0ac4d6bacf4df8b85f76e5181aeda4daa0e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 1 Jun 2016 14:25:07 +1200 Subject: [PATCH 0268/1237] Click on blank part of dialog to deselect item --- .../resources/qml/dialogs/FileDialog.qml | 20 ++++++++++++++++--- .../resources/qml/windows-uit/ModalFrame.qml | 3 +++ .../resources/qml/windows-uit/ModalWindow.qml | 2 ++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index c8748443ef..41f8db3dba 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -79,6 +79,9 @@ ModalWindow { fileTableModel.folder = initialFolder; iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; + + // Clear selection when click on external frame. + frameClicked.connect(function() { d.clearSelection(); }); } Item { @@ -87,6 +90,13 @@ ModalWindow { height: pane.height anchors.margins: 0 + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + drag.target: root + onClicked: d.clearSelection() + } + Row { id: navControls anchors { @@ -228,6 +238,12 @@ ModalWindow { fileTableModel.folder = homeDestination; return true; } + + function clearSelection() { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + update(); + } } FolderListModel { @@ -392,9 +408,7 @@ ModalWindow { rows++; } - fileTableView.selection.clear(); - fileTableView.currentRow = -1; - d.update(); + d.clearSelection(); } } diff --git a/interface/resources/qml/windows-uit/ModalFrame.qml b/interface/resources/qml/windows-uit/ModalFrame.qml index 44c0b6a456..211353b5f3 100644 --- a/interface/resources/qml/windows-uit/ModalFrame.qml +++ b/interface/resources/qml/windows-uit/ModalFrame.qml @@ -27,6 +27,8 @@ Frame { readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y + signal frameClicked(); + anchors { fill: parent topMargin: -frameMarginTop @@ -47,6 +49,7 @@ Frame { anchors.fill: parent drag.target: window enabled: window.draggable + onClicked: window.frameClicked(); } Item { diff --git a/interface/resources/qml/windows-uit/ModalWindow.qml b/interface/resources/qml/windows-uit/ModalWindow.qml index f429e98ac3..144165e4e1 100644 --- a/interface/resources/qml/windows-uit/ModalWindow.qml +++ b/interface/resources/qml/windows-uit/ModalWindow.qml @@ -22,5 +22,7 @@ Window { property int colorScheme: hifi.colorSchemes.light property bool draggable: false + signal frameClicked(); + anchors.centerIn: draggable ? undefined : parent } From b8c80e222286a16ef56a5ecfcf7c95aa04a9918e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 31 May 2016 20:39:16 -0700 Subject: [PATCH 0269/1237] Revert "refresh API info during re-connect - case 570" This reverts commit e8d7f9614aed57bad25f940c82282c25b91ed92e. --- libraries/networking/src/AddressManager.cpp | 44 +++------------------ libraries/networking/src/AddressManager.h | 9 +---- libraries/networking/src/DomainHandler.cpp | 18 ++++++--- libraries/networking/src/DomainHandler.h | 10 ++--- libraries/networking/src/NodeList.cpp | 14 ++----- 5 files changed, 28 insertions(+), 67 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 80989acd2c..1a452999e4 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -144,21 +144,12 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // 4. domain network address (IP or dns resolvable hostname) // use our regex'ed helpers to figure out what we're supposed to do with this - if (handleUsername(lookupUrl.authority())) { - // handled a username for lookup - - // in case we're failing to connect to where we thought this user was - // store their username as previous lookup so we can refresh their location via API - _previousLookup = lookupUrl; - } else { + if (!handleUsername(lookupUrl.authority())) { // we're assuming this is either a network address or global place name // check if it is a network address first bool hostChanged; if (handleNetworkAddress(lookupUrl.host() - + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { - - // a network address lookup clears the previous lookup since we don't expect to re-attempt it - _previousLookup.clear(); + + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { // If the host changed then we have already saved to history if (hostChanged) { @@ -174,16 +165,10 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ - // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info - _previousLookup = lookupUrl; - // no place name - this is probably a domain ID // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { - // store this place name as the previous lookup in case we fail to connect and want to refresh API info - _previousLookup = lookupUrl; - // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger); @@ -195,13 +180,9 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); - // a path lookup clears the previous lookup since we don't expect to re-attempt it - _previousLookup.clear(); - // if this is a relative path then handle it as a relative viewpoint handlePath(lookupUrl.path(), trigger, true); emit lookupResultsFinished(); - return true; } @@ -295,7 +276,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; - emit possibleDomainChangeRequired(domainHostname, domainPort, domainID); + emit possibleDomainChangeRequired(domainHostname, domainPort); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); @@ -334,10 +315,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty()) { - // make sure we don't re-handle an overriden path if this was a refresh of info from API - if (trigger != LookupTrigger::AttemptedRefresh) { - handlePath(overridePath, trigger); - } + handlePath(overridePath, trigger); } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; @@ -620,7 +598,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress); - emit possibleDomainChangeRequired(hostname, port, QUuid()); + emit possibleDomainChangeRequired(hostname, port); return hostChanged; } @@ -640,13 +618,6 @@ void AddressManager::goToUser(const QString& username) { QByteArray(), nullptr, requestParams); } -void AddressManager::refreshPreviousLookup() { - // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history) - if (!_previousLookup.isEmpty()) { - handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh); - } -} - void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } @@ -658,10 +629,7 @@ void AddressManager::copyPath() { void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { // if we're cold starting and this is called for the first address (from settings) we don't do anything - if (trigger != LookupTrigger::StartupFromSettings - && trigger != LookupTrigger::DomainPathResponse - && trigger != LookupTrigger::AttemptedRefresh) { - + if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) { if (trigger == LookupTrigger::Back) { // we're about to push to the forward stack // if it's currently empty emit our signal to say that going forward is now possible diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index a3aaee3ba2..643924ff5c 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -48,8 +48,7 @@ public: Forward, StartupFromSettings, DomainPathResponse, - Internal, - AttemptedRefresh + Internal }; bool isConnected(); @@ -90,8 +89,6 @@ public slots: void goToUser(const QString& username); - void refreshPreviousLookup(); - void storeCurrentAddress(); void copyAddress(); @@ -102,7 +99,7 @@ signals: void lookupResultIsOffline(); void lookupResultIsNotFound(); - void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID); + void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort); void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID); void locationChangeRequired(const glm::vec3& newPosition, @@ -155,8 +152,6 @@ private: quint64 _lastBackPush = 0; QString _newHostLookupPath; - - QUrl _previousLookup; }; #endif // hifi_AddressManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 1efcfc7f27..4f85296f03 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -28,8 +28,16 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), + _uuid(), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), + _assignmentUUID(), + _connectionToken(), + _iceDomainID(), + _iceClientID(), + _iceServerSockAddr(), _icePeer(this), + _isConnected(false), + _settingsObject(), _settingsTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -97,7 +105,7 @@ void DomainHandler::hardReset() { softReset(); qCDebug(networking) << "Hard reset in NodeList DomainHandler."; - _pendingDomainID = QUuid(); + _iceDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); _sockAddr.clear(); @@ -131,7 +139,7 @@ void DomainHandler::setUUID(const QUuid& uuid) { } } -void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { +void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded @@ -163,8 +171,6 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const // grab the port by reading the string after the colon _sockAddr.setPort(port); } - - _pendingDomainID = domainID; } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { @@ -175,7 +181,7 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - _pendingDomainID = id; + _iceDomainID = id; HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); @@ -337,7 +343,7 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation); - if (_icePeer.getUUID() != _pendingDomainID) { + if (_icePeer.getUUID() != _iceDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; _icePeer.reset(); } else { diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 226186f1d0..bcee7668d1 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -58,8 +58,8 @@ public: const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - - const QUuid& getPendingDomainID() const { return _pendingDomainID; } + + const QUuid& getICEDomainID() const { return _iceDomainID; } const QUuid& getICEClientID() const { return _iceClientID; } @@ -94,7 +94,7 @@ public: }; public slots: - void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid()); + void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); void processSettingsPacketList(QSharedPointer packetList); @@ -136,11 +136,11 @@ private: HifiSockAddr _sockAddr; QUuid _assignmentUUID; QUuid _connectionToken; - QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection + QUuid _iceDomainID; QUuid _iceClientID; HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; - bool _isConnected { false }; + bool _isConnected; QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 082200fccc..16a4083b08 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, - &_domainHandler, &DomainHandler::setSocketAndID); + &_domainHandler, &DomainHandler::setHostnameAndPort); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); @@ -355,14 +355,6 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } - - if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { - // if we aren't connected to the domain-server, and we have an ID - // (that we presume belongs to a domain in the HF Metaverse) - // we request connection information for the domain every so often to make sure what we have is up to date - - DependencyManager::get()->refreshPreviousLookup(); - } } void NodeList::handleDSPathQuery(const QString& newPath) { @@ -470,7 +462,7 @@ void NodeList::handleICEConnectionToDomainServer() { LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), - _domainHandler.getPendingDomainID()); + _domainHandler.getICEDomainID()); } } @@ -483,7 +475,7 @@ void NodeList::pingPunchForDomainServer() { if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" - << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID()); + << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); } else { if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat From 922468ce1c3777b9cd89fac03da8d94d7a86fa55 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 31 May 2016 20:47:58 -0700 Subject: [PATCH 0270/1237] fix cut and paste error with extraneous namespace qualifiers. --- plugins/openvr/src/ViveControllerManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index e14a10a94e..f788e3f37c 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -61,7 +61,7 @@ private: void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand); void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand); - void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton); + void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton); class FilteredStick { public: From 3119b654dc9b0053c5c1bdee7668246ded0fcb87 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 31 May 2016 20:50:12 -0700 Subject: [PATCH 0271/1237] additional revert related change --- libraries/networking/src/AddressManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 1a452999e4..1b7ed11cce 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -362,7 +362,7 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) { if (errorReply.error() == QNetworkReply::ContentNotFoundError) { // if this is a lookup that has no result, don't keep re-trying it - _previousLookup.clear(); + //_previousLookup.clear(); emit lookupResultIsNotFound(); } From c4aed1ce063f2678f4f5c9a10fc1e14c1eb98d74 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 1 Jun 2016 16:16:08 +1200 Subject: [PATCH 0272/1237] Remove "Open Folder" button --- .../resources/qml/dialogs/FileDialog.qml | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 41f8db3dba..5173252f38 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -212,8 +212,9 @@ ModalWindow { function update() { var row = fileTableView.currentRow; + openButton.text = root.selectDirectory && row === -1 ? "Choose" : "Open" + if (row === -1) { - openFolderButton.enabled = false; return; } @@ -224,7 +225,6 @@ ModalWindow { } else { currentSelection.text = ""; } - openFolderButton.enabled = currentSelectionIsFolder } function navigateUp() { @@ -637,16 +637,6 @@ ModalWindow { Keys.onReturnPressed: okAction.trigger() KeyNavigation.up: selectionType KeyNavigation.left: selectionType - KeyNavigation.right: openFolderButton - } - - Button { - id: openFolderButton - text: "Open Folder" - enabled: false - action: openFolderAction - KeyNavigation.up: selectionType - KeyNavigation.left: openButton KeyNavigation.right: cancelButton } @@ -654,7 +644,7 @@ ModalWindow { id: cancelButton action: cancelAction KeyNavigation.up: selectionType - KeyNavigation.left: openFolderButton + KeyNavigation.left: openButton KeyNavigation.right: fileTableView.contentItem Keys.onReturnPressed: { canceled(); root.enabled = false } } @@ -663,8 +653,15 @@ ModalWindow { Action { id: okAction text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open") - enabled: currentSelection.text ? true : false - onTriggered: okActionTimer.start(); + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && fileTableView.currentRow === -1) { + okActionTimer.start(); + } else { + fileTableView.navigateToCurrentRow(); + } + } } Timer { @@ -724,12 +721,6 @@ ModalWindow { } } - Action { - id: openFolderAction - text: "Open Folder" - onTriggered: { fileTableView.navigateToCurrentRow(); } - } - Action { id: cancelAction text: "Cancel" From 1624146fc2ecf6abf508b03769f81ff2220158ae Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 31 May 2016 20:39:16 -0700 Subject: [PATCH 0273/1237] Revert "refresh API info during re-connect - case 570" This reverts commit e8d7f9614aed57bad25f940c82282c25b91ed92e. --- libraries/networking/src/AddressManager.cpp | 44 +++------------------ libraries/networking/src/AddressManager.h | 9 +---- libraries/networking/src/DomainHandler.cpp | 18 ++++++--- libraries/networking/src/DomainHandler.h | 10 ++--- libraries/networking/src/NodeList.cpp | 14 ++----- 5 files changed, 28 insertions(+), 67 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 80989acd2c..1a452999e4 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -144,21 +144,12 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // 4. domain network address (IP or dns resolvable hostname) // use our regex'ed helpers to figure out what we're supposed to do with this - if (handleUsername(lookupUrl.authority())) { - // handled a username for lookup - - // in case we're failing to connect to where we thought this user was - // store their username as previous lookup so we can refresh their location via API - _previousLookup = lookupUrl; - } else { + if (!handleUsername(lookupUrl.authority())) { // we're assuming this is either a network address or global place name // check if it is a network address first bool hostChanged; if (handleNetworkAddress(lookupUrl.host() - + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { - - // a network address lookup clears the previous lookup since we don't expect to re-attempt it - _previousLookup.clear(); + + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { // If the host changed then we have already saved to history if (hostChanged) { @@ -174,16 +165,10 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ - // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info - _previousLookup = lookupUrl; - // no place name - this is probably a domain ID // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { - // store this place name as the previous lookup in case we fail to connect and want to refresh API info - _previousLookup = lookupUrl; - // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger); @@ -195,13 +180,9 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); - // a path lookup clears the previous lookup since we don't expect to re-attempt it - _previousLookup.clear(); - // if this is a relative path then handle it as a relative viewpoint handlePath(lookupUrl.path(), trigger, true); emit lookupResultsFinished(); - return true; } @@ -295,7 +276,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; - emit possibleDomainChangeRequired(domainHostname, domainPort, domainID); + emit possibleDomainChangeRequired(domainHostname, domainPort); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); @@ -334,10 +315,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty()) { - // make sure we don't re-handle an overriden path if this was a refresh of info from API - if (trigger != LookupTrigger::AttemptedRefresh) { - handlePath(overridePath, trigger); - } + handlePath(overridePath, trigger); } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; @@ -620,7 +598,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress); - emit possibleDomainChangeRequired(hostname, port, QUuid()); + emit possibleDomainChangeRequired(hostname, port); return hostChanged; } @@ -640,13 +618,6 @@ void AddressManager::goToUser(const QString& username) { QByteArray(), nullptr, requestParams); } -void AddressManager::refreshPreviousLookup() { - // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history) - if (!_previousLookup.isEmpty()) { - handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh); - } -} - void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } @@ -658,10 +629,7 @@ void AddressManager::copyPath() { void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { // if we're cold starting and this is called for the first address (from settings) we don't do anything - if (trigger != LookupTrigger::StartupFromSettings - && trigger != LookupTrigger::DomainPathResponse - && trigger != LookupTrigger::AttemptedRefresh) { - + if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) { if (trigger == LookupTrigger::Back) { // we're about to push to the forward stack // if it's currently empty emit our signal to say that going forward is now possible diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index a3aaee3ba2..643924ff5c 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -48,8 +48,7 @@ public: Forward, StartupFromSettings, DomainPathResponse, - Internal, - AttemptedRefresh + Internal }; bool isConnected(); @@ -90,8 +89,6 @@ public slots: void goToUser(const QString& username); - void refreshPreviousLookup(); - void storeCurrentAddress(); void copyAddress(); @@ -102,7 +99,7 @@ signals: void lookupResultIsOffline(); void lookupResultIsNotFound(); - void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID); + void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort); void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID); void locationChangeRequired(const glm::vec3& newPosition, @@ -155,8 +152,6 @@ private: quint64 _lastBackPush = 0; QString _newHostLookupPath; - - QUrl _previousLookup; }; #endif // hifi_AddressManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 1efcfc7f27..4f85296f03 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -28,8 +28,16 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), + _uuid(), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), + _assignmentUUID(), + _connectionToken(), + _iceDomainID(), + _iceClientID(), + _iceServerSockAddr(), _icePeer(this), + _isConnected(false), + _settingsObject(), _settingsTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -97,7 +105,7 @@ void DomainHandler::hardReset() { softReset(); qCDebug(networking) << "Hard reset in NodeList DomainHandler."; - _pendingDomainID = QUuid(); + _iceDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); _sockAddr.clear(); @@ -131,7 +139,7 @@ void DomainHandler::setUUID(const QUuid& uuid) { } } -void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { +void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded @@ -163,8 +171,6 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const // grab the port by reading the string after the colon _sockAddr.setPort(port); } - - _pendingDomainID = domainID; } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { @@ -175,7 +181,7 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - _pendingDomainID = id; + _iceDomainID = id; HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); @@ -337,7 +343,7 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation); - if (_icePeer.getUUID() != _pendingDomainID) { + if (_icePeer.getUUID() != _iceDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; _icePeer.reset(); } else { diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 226186f1d0..bcee7668d1 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -58,8 +58,8 @@ public: const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - - const QUuid& getPendingDomainID() const { return _pendingDomainID; } + + const QUuid& getICEDomainID() const { return _iceDomainID; } const QUuid& getICEClientID() const { return _iceClientID; } @@ -94,7 +94,7 @@ public: }; public slots: - void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid()); + void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); void processSettingsPacketList(QSharedPointer packetList); @@ -136,11 +136,11 @@ private: HifiSockAddr _sockAddr; QUuid _assignmentUUID; QUuid _connectionToken; - QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection + QUuid _iceDomainID; QUuid _iceClientID; HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; - bool _isConnected { false }; + bool _isConnected; QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 082200fccc..16a4083b08 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, - &_domainHandler, &DomainHandler::setSocketAndID); + &_domainHandler, &DomainHandler::setHostnameAndPort); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); @@ -355,14 +355,6 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } - - if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { - // if we aren't connected to the domain-server, and we have an ID - // (that we presume belongs to a domain in the HF Metaverse) - // we request connection information for the domain every so often to make sure what we have is up to date - - DependencyManager::get()->refreshPreviousLookup(); - } } void NodeList::handleDSPathQuery(const QString& newPath) { @@ -470,7 +462,7 @@ void NodeList::handleICEConnectionToDomainServer() { LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), - _domainHandler.getPendingDomainID()); + _domainHandler.getICEDomainID()); } } @@ -483,7 +475,7 @@ void NodeList::pingPunchForDomainServer() { if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" - << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID()); + << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); } else { if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat From 8586e91a3b78e37f54384721990b979caff96c3e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 31 May 2016 20:50:12 -0700 Subject: [PATCH 0274/1237] additional revert related change --- libraries/networking/src/AddressManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 1a452999e4..1b7ed11cce 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -362,7 +362,7 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) { if (errorReply.error() == QNetworkReply::ContentNotFoundError) { // if this is a lookup that has no result, don't keep re-trying it - _previousLookup.clear(); + //_previousLookup.clear(); emit lookupResultIsNotFound(); } From b65938b6316802dc69e1a5a3dea3452e7f148601 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 1 Jun 2016 16:23:26 +1200 Subject: [PATCH 0275/1237] Adjust default and minimum sizes --- interface/resources/qml/dialogs/FileDialog.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 5173252f38..015a192185 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -26,10 +26,10 @@ import "fileDialog" ModalWindow { id: root resizable: true - implicitWidth: 640 - implicitHeight: 480 + implicitWidth: 480 + implicitHeight: 360 - minSize: Qt.vector2d(388, 240) + minSize: Qt.vector2d(360, 240) draggable: true HifiConstants { id: hifi } From f0133013c267ae319c660e8083546c16880faf00 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 1 Jun 2016 07:30:26 -0700 Subject: [PATCH 0276/1237] typo --- plugins/openvr/src/ViveControllerManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index f006c436e1..ba0ac88dfb 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -290,7 +290,7 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u } } -void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int xPseudoButton, int yPesudoButton) { +void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int xPseudoButton, int yPseudoButton) { // Populate the L/RS_CENTER/OUTER pseudo buttons, corresponding to a partition of the L/RS space based on the X/Y values. const float CENTER_DEADBAND = 0.6f; const float DIAGONAL_DIVIDE_IN_RADIANS = PI / 4.0f; @@ -301,7 +301,7 @@ void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxi float angle = glm::atan(cartesianQuadrantI.y / cartesianQuadrantI.x); float radius = glm::length(cartesianQuadrantI); bool isCenter = radius < CENTER_DEADBAND; - _buttonPressedMap.insert(isCenter ? centerPseudoButton : ((angle < DIAGONAL_DIVIDE_IN_RADIANS) ? xPseudoButton :yPesudoButton)); + _buttonPressedMap.insert(isCenter ? centerPseudoButton : ((angle < DIAGONAL_DIVIDE_IN_RADIANS) ? xPseudoButton :yPseudoButton)); } } From 036fd0c4af8277276fdaf359362b464a082c3826 Mon Sep 17 00:00:00 2001 From: Peter Clemenko III Date: Wed, 1 Jun 2016 10:35:36 -0400 Subject: [PATCH 0277/1237] Update the build docs for a better OpenSSL security posture. There are around 19 CVEs in the current recommended version. --- BUILD.md | 4 ++-- BUILD_WIN.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index c1ccd3193e..e89933fa7d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,8 +2,8 @@ * [cmake](http://www.cmake.org/cmake/resources/software.html) ~> 3.3.2 * [Qt](http://www.qt.io/download-open-source) ~> 5.5.1 -* [OpenSSL](https://www.openssl.org/community/binaries.html) ~> 1.0.1m - * IMPORTANT: Using the recommended version of OpenSSL is critical to avoid security vulnerabilities. +* [OpenSSL](https://www.openssl.org/community/binaries.html) ~> 1.0.1 (highest letter) + * IMPORTANT: Using the recommended version of OpenSSL is critical to avoid security vulnerabilities. Make sure to check regularly to see if there is a new version. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) ####CMake External Project Dependencies diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 25e20952ca..a80c279798 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -73,8 +73,8 @@ Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll QSslSocket: cannot resolve SSL_get0_next_proto_negotiated To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](http://slproweb.com/products/Win32OpenSSL.html): -* Win32 OpenSSL v1.0.1q -* Win64 OpenSSL v1.0.1q +* Win32 OpenSSL v1.0.1 (highest letter) +* Win64 OpenSSL v1.0.1 (highest letter) Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. From b171b5e9eb75e1c0c85b69e90ef083bad84d26ce Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 1 Jun 2016 13:40:46 -0700 Subject: [PATCH 0278/1237] update blocky --- .../DomainContent/Home/blocky/blocky.js | 12 +++++----- .../DomainContent/Home/blocky/wrapper.js | 22 +++++-------------- .../DomainContent/Home/reset.js | 6 ++--- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/blocky/blocky.js b/unpublishedScripts/DomainContent/Home/blocky/blocky.js index c7e35d15e7..8eae31a439 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/blocky.js +++ b/unpublishedScripts/DomainContent/Home/blocky/blocky.js @@ -178,7 +178,9 @@ var TARGET_BLOCKS_POSITION = { this.createPlayableBlocks(arrangement); } - this.busy = false; + Script.setTimeout(function() { + _this.busy = false; + }, 1000) }, findBlocks: function() { @@ -257,15 +259,15 @@ var TARGET_BLOCKS_POSITION = { }, update: function() { - if (this.busy === true) { + if (_this.busy === true) { return; } var BEAM_TRIGGER_THRESHOLD = 0.075; var BEAM_POSITION = { - x: 1098.5159, - y: 460.0490, - z: -66.3012 + x: 1098.4424, + y: 460.3090, + z: -66.2190 }; var leftHandPosition = MyAvatar.getLeftPalmPosition(); diff --git a/unpublishedScripts/DomainContent/Home/blocky/wrapper.js b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js index dfddbaff5d..5ae7d4592e 100644 --- a/unpublishedScripts/DomainContent/Home/blocky/wrapper.js +++ b/unpublishedScripts/DomainContent/Home/blocky/wrapper.js @@ -10,30 +10,20 @@ // -var RESETTER_POSITION = { - x: 1098.5159, - y: 460.0490, - z: -66.3012 -}; - BlockyGame = function(spawnPosition, spawnRotation) { var scriptURL = "atp:/blocky/blocky.js"; var blockyProps = { - type: 'Box', + type: 'Model', + modelURL:'atp:/blocky/swiper.fbx', name: 'home_box_blocky_resetter', - color: { - red: 0, - green: 0, - blue: 255 - }, dimensions: { - x: 0.25, - y: 0.25, - z: 0.25 + x: 0.2543, + y: 0.3269, + z: 0.4154 }, - rotation:Quat.fromPitchYawRollDegrees(-0.0029,32.9983,-0.0021), + rotation:Quat.fromPitchYawRollDegrees(-9.5165,-147.3687,16.6577), script: scriptURL, userData: JSON.stringify({ "grabbableKey": { diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index a15c0c4d87..ffa26fb8b3 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -342,9 +342,9 @@ }); var blocky = new BlockyGame({ - x: 1098.5159, - y: 460.0490, - z: -66.3012 + x: 1098.4424, + y: 460.3090, + z: -66.2190 }) print('HOME after creating scripted entities') From 84fa4402fd62772f247486b70018f75eb25f38c5 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 1 Jun 2016 14:03:49 -0700 Subject: [PATCH 0279/1237] Click on full trigger, activate on partial trigger. --- .../controllers/handControllerPointer.js | 94 +++++++++++++++---- 1 file changed, 78 insertions(+), 16 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index ca3b5e8cf2..5cbde82654 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -47,22 +47,66 @@ function TimeLock(expiration) { } var handControllerLockOut = new TimeLock(2000); -// Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. -function LatchedToggle(onFunction, offFunction, state) { - this.getState = function () { - return state; +function Trigger() { + // This part is copied and adapted from handControllerGrab.js. Maybe we should refactor this. + var that = this; + that.TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + that.TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab + that.TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab + that.TRIGGER_OFF_VALUE = 0.15; + that.rawTriggerValue = 0; + that.triggerValue = 0; // rolling average of trigger value + that.triggerPress = function (value) { + that.rawTriggerValue = value; }; - this.setState = function (on) { - if (state === on) { - return; - } - state = on; - if (on) { - onFunction(); - } else { - offFunction(); - } + that.updateSmoothedTrigger = function () { // e.g., call once/update for effect + var triggerValue = that.rawTriggerValue; + // smooth out trigger value + that.triggerValue = (that.triggerValue * that.TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - that.TRIGGER_SMOOTH_RATIO)); }; + // Current smoothed state, without hysteresis. Answering booleans. + that.triggerSmoothedGrab = function () { + return that.triggerValue > that.TRIGGER_GRAB_VALUE; + }; + that.triggerSmoothedSqueezed = function () { + return that.triggerValue > that.TRIGGER_ON_VALUE; + }; + that.triggerSmoothedReleased = function () { + return that.triggerValue < that.TRIGGER_OFF_VALUE; + }; + + // This part is not from handControllerGrab.js + that.state = null; // tri-state: falsey, 'partial', 'full' + that.update = function () { // update state, called from an update function + var state = that.state; + that.updateSmoothedTrigger(); + + // The first two are independent of previous state: + if (that.triggerSmoothedGrab()) { + state = 'full'; + } else if (that.triggerSmoothedReleased()) { + state = null; + // These depend on previous state: + // null -> squeezed ==> partial + // full -> !squeezed ==> partial + // Otherwise no change. + } else if (that.triggerSmoothedSqueezed()) { + if (!state) { + state = 'partial'; + } + } else if (state === 'full') { + state = 'partial'; + } + that.state = state; + }; + // Answer a controller source function (answering either 0.0 or 1.0), with hysteresis. + that.partial = function () { + return that.state ? 1.0 : 0.0; // either 'partial' or 'full' + }; + that.full = function () { + return (that.state === 'full') ? 1.0 : 0.0; + } } // VERTICAL FIELD OF VIEW --------- @@ -257,28 +301,41 @@ setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // CONTROLLER MAPPING --------- // +var leftTrigger = new Trigger(); +var rightTrigger = new Trigger(); +var activeTrigger = rightTrigger; var activeHand = Controller.Standard.RightHand; function toggleHand() { if (activeHand === Controller.Standard.RightHand) { activeHand = Controller.Standard.LeftHand; + activeTrigger = leftTrigger; } else { activeHand = Controller.Standard.RightHand; + activeTrigger = rightTrigger; } } var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); Script.scriptEnding.connect(clickMapping.disable); +// Gather the trigger data for smoothing. +clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); +clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); +// The next two lines will be removed soon. Right now I want both trigger and button so we can compare. clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick); +// Full somoothed trigger is a click. +clickMapping.from(rightTrigger.full).to(Controller.Actions.ReticleClick); +clickMapping.from(leftTrigger.full).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); -clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(function (on) { +// Partial smoothed trigger is activation. +clickMapping.from(rightTrigger.partial).to(function (on) { if (on && (activeHand !== Controller.Standard.RightHand)) { toggleHand(); } }); -clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(function (on) { +clickMapping.from(leftTrigger.partial).to(function (on) { if (on && (activeHand !== Controller.Standard.LeftHand)) { toggleHand(); } @@ -359,6 +416,8 @@ function update() { if (!Window.hasFocus()) { // Don't mess with other apps return turnOffVisualization(); } + leftTrigger.update(); + rightTrigger.update(); var controllerPose = Controller.getPoseValue(activeHand); // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) if (!controllerPose.valid) { @@ -390,6 +449,9 @@ function update() { return turnOffVisualization(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). + if (!activeTrigger.state) { + return turnOffVisualization(); // No trigger (with hysteresis). + } updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); } From f082201941c14d1370648eb6fa42357fc01455fb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 1 Jun 2016 15:32:27 -0700 Subject: [PATCH 0280/1237] working toward a grid on domain-server settigns page --- .../resources/describe-settings.json | 2 +- .../resources/web/settings/js/settings.js | 64 +++++++------------ 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index e0767b0256..a856666bc9 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.2, + "version": 1.3, "settings": [ { "name": "metaverse", diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 1628844d15..52337db601 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -1089,10 +1089,6 @@ function addTableRow(add_glyphicon) { var table = row.parents('table') var isArray = table.data('setting-type') === 'array' - console.log("------------------------"); - console.log("table = " + table.name + " " + table.id); - console.log("isArray = " + isArray); - var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS) if (!isArray) { @@ -1136,17 +1132,11 @@ function addTableRow(add_glyphicon) { var table = row.parents("table") var setting_name = table.attr("name") var full_name = setting_name + "." + key - - console.log("table = " + table); - console.log("setting_name = " + setting_name); - console.log("full_name = " + full_name); - row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) row.removeClass("inputs") _.each(row.children(), function(element) { if ($(element).hasClass("numbered")) { - console.log("A"); // Index row var numbers = columns.children(".numbered") if (numbers.length > 0) { @@ -1155,69 +1145,59 @@ function addTableRow(add_glyphicon) { $(element).html(1) } } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { - console.log("B"); $(element).html("") } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { - console.log("C"); // Change buttons var anchor = $(element).children("a") anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES) anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES) } else if ($(element).hasClass("key")) { - console.log("D"); var input = $(element).children("input") $(element).html(input.val()) input.remove() } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { - console.log("E"); // Hide inputs - console.log("element = " + element); var input = $(element).find("input") - - var val = input.val(); - if (input.attr("type") == "checkbox") { - val = input.is(':checked'); - $(element).children().hide(); + var isCheckbox = false; + if (input.hasClass("toggle-checkbox")) { + input = $(input).parent().parent(); + isCheckbox = true; } - input.attr("type", "hidden") + var val = input.val(); + if (isCheckbox) { + val = $(input).find("input").is(':checked'); + // don't hide the checkbox + } else { + input.attr("type", "hidden") + } if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length var key = $(element).attr('name') - console.log("row_index = " + row_index); - console.log("key = " + key); - // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length - console.log("num_columns = " + num_columns); - - console.log("input = " + JSON.stringify(input)); - console.log("new name = " + setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")); - - input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) - - + if (isCheckbox) { + $(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + } else { + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + } } else { input.attr("name", full_name + "." + $(element).attr("name")) } - input.attr("data-changed", "true") - - // if the input is a bootstrapSwitch, we need to move this input up to where it will be found - var inputElement = $(input).detach(); - $(element).append(inputElement); - - console.log("input.val() = " + val); - - $(element).append(val) + if (isCheckbox) { + $(input).find("input").attr("data-changed", "true"); + } else { + input.attr("data-changed", "true"); + $(element).append(val); + } } else { - console.log("F"); console.log("Unknown table element") } }) From ae0c8a96f2bd5883865e1b6f606ee266e38d46cf Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 1 Jun 2016 15:33:24 -0700 Subject: [PATCH 0281/1237] code to unpack permissions into a usable data structure --- domain-server/src/AgentPermissions.h | 40 +++++++++++++++++++ .../src/DomainServerSettingsManager.cpp | 40 +++++++++++++++++++ .../src/DomainServerSettingsManager.h | 7 ++++ 3 files changed, 87 insertions(+) create mode 100644 domain-server/src/AgentPermissions.h diff --git a/domain-server/src/AgentPermissions.h b/domain-server/src/AgentPermissions.h new file mode 100644 index 0000000000..50c90581c1 --- /dev/null +++ b/domain-server/src/AgentPermissions.h @@ -0,0 +1,40 @@ +// +// AgentPermissions.h +// domain-server/src +// +// Created by Seth Alves on 2016-6-1. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AgentPermissions_h +#define hifi_AgentPermissions_h + +class AgentPermissions { +public: + AgentPermissions(QMap perms) { + _id = perms["permissions_id"].toString(); + canConnectToDomain = perms["id_can_connect"].toBool(); + canAdjustLocks = perms["id_can_adjust_locks"].toBool(); + canRezPermanentEntities = perms["id_can_rez"].toBool(); + canRezTemporaryEntities = perms["id_can_rez_tmp"].toBool(); + canWriteToAssetServer = perms["id_can_write_to_asset_server"].toBool(); + }; + + QString getID() { return _id; } + + bool canConnectToDomain { false }; + bool canAdjustLocks { false }; + bool canRezPermanentEntities { false }; + bool canRezTemporaryEntities { false }; + bool canWriteToAssetServer { false }; + +protected: + QString _id; +}; + +using AgentPermissionsPointer = std::shared_ptr; + +#endif // hifi_AgentPermissions_h diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 23bb3e7abf..0d6214d702 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -26,6 +26,9 @@ #include "DomainServerSettingsManager.h" +#define WANT_DEBUG 1 + + const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; const QString DESCRIPTION_SETTINGS_KEY = "settings"; @@ -190,12 +193,49 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _configMap.loadMasterAndUserConfig(argumentList); } } + + if (oldVersion < 1.3) { + // This was prior to the permissions-grid in the domain-server settings page + + } } + unpackPermissions(); + // write the current description version to our settings appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion); } +void DomainServerSettingsManager::unpackPermissions() { + QList permsHashList = valueOrDefaultValueForKeyPath(AGENT_PERMISSIONS_KEYPATH).toList(); + foreach (QVariant permsHash, permsHashList) { + AgentPermissionsPointer perms { new AgentPermissions(permsHash.toMap()) }; + _agentPermissions[perms->getID()] = perms; + } + + #ifdef WANT_DEBUG + qDebug() << "--------------- permissions ---------------------"; + QHashIterator i(_agentPermissions); + while (i.hasNext()) { + i.next(); + AgentPermissionsPointer perms = i.value(); + qDebug() << i.key() + << perms->canConnectToDomain + << perms->canAdjustLocks + << perms->canRezPermanentEntities + << perms->canRezTemporaryEntities + << perms->canWriteToAssetServer; + } + #endif +} + +AgentPermissionsPointer DomainServerSettingsManager::getPermissionsForName(QString name) const { + if (_agentPermissions.contains(name)) { + return _agentPermissions[name]; + } + return nullptr; +} + QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) { const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index d6dd5070a9..4d305b2b5b 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -19,6 +19,7 @@ #include #include +#include "AgentPermissions.h" const QString SETTINGS_PATHS_KEY = "paths"; @@ -27,6 +28,7 @@ const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users"; const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access"; +const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; class DomainServerSettingsManager : public QObject { Q_OBJECT @@ -41,6 +43,8 @@ public: QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } + AgentPermissionsPointer getPermissionsForName(QString name) const; + private slots: void processSettingsRequestPacket(QSharedPointer message); @@ -58,6 +62,9 @@ private: HifiConfigVariantMap _configMap; friend class DomainServer; + + void unpackPermissions(); + QHash _agentPermissions; }; #endif // hifi_DomainServerSettingsManager_h From 1dca62f752067786afe90c5ea779c6bc32005f6d Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 1 Jun 2016 17:51:53 -0700 Subject: [PATCH 0282/1237] Introducing a new technique for normal packing and fixing a bug on the normal buffer format --- .../src/RenderableProceduralItemShader.h | 49 +++++++++----- libraries/render-utils/src/DeferredBuffer.slh | 66 +++++++++++++++++++ .../render-utils/src/DeferredBufferRead.slh | 2 +- .../render-utils/src/DeferredBufferWrite.slh | 24 +------ .../render-utils/src/FramebufferCache.cpp | 5 +- 5 files changed, 107 insertions(+), 39 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableProceduralItemShader.h b/libraries/entities-renderer/src/RenderableProceduralItemShader.h index 1afd3bc608..4762f619bf 100644 --- a/libraries/entities-renderer/src/RenderableProceduralItemShader.h +++ b/libraries/entities-renderer/src/RenderableProceduralItemShader.h @@ -25,24 +25,43 @@ layout(location = 2) out vec4 _fragColor2; // the alpha threshold uniform float alphaThreshold; -uniform sampler2D normalFittingMap; -vec3 bestFitNormal(vec3 normal) { - vec3 absNorm = abs(normal); - float maxNAbs = max(absNorm.z, max(absNorm.x, absNorm.y)); +vec2 signNotZero(vec2 v) { + return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); +} - vec2 texcoord = (absNorm.z < maxNAbs ? - (absNorm.y < maxNAbs ? absNorm.yz : absNorm.xz) : - absNorm.xy); - texcoord = (texcoord.x < texcoord.y ? texcoord.yx : texcoord.xy); - texcoord.y /= texcoord.x; - vec3 cN = normal / maxNAbs; - float fittingScale = texture(normalFittingMap, texcoord).a; - cN *= fittingScale; - return (cN * 0.5 + 0.5); +vec2 float32x3_to_oct(in vec3 v) { + vec2 p = v.xy * (1.0 / (abs(v.x) + abs(v.y) + abs(v.z))); + return ((v.z <= 0.0) ? ((1.0 - abs(p.yx)) * signNotZero(p)) : p); } +vec3 oct_to_float32x3(in vec2 e) { + vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); + if (v.z < 0) { + v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); + } + return normalize(v); +} + +vec3 snorm12x2_to_unorm8x3(vec2 f) { + vec2 u = vec2(round(clamp(f, -1.0, 1.0) * 2047.0 + 2047.0)); + float t = floor(u.y / 256.0); + + return floor(vec3( + u.x / 16.0, + fract(u.x / 16.0) * 256.0 + t, + u.y - t * 256.0 + )) / 255.0; +} + +vec2 unorm8x3_to_snorm12x2(vec3 u) { + u *= 255.0; + u.y *= (1.0 / 16.0); + vec2 s = vec2( u.x * 16.0 + floor(u.y), + fract(u.y) * (16.0 * 256.0) + u.z); + return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); +} float mod289(float x) { return x - floor(x * (1.0 / 289.0)) * 289.0; @@ -322,7 +341,7 @@ void main(void) { } vec4 diffuse = vec4(_color.rgb, alpha); - vec4 normal = vec4(normalize(bestFitNormal(_normal)), 0.5); + vec4 normal = vec4(packNormal(normalize(_normal)), 0.5); _fragColor0 = diffuse; _fragColor1 = normal; @@ -355,7 +374,7 @@ void main(void) { float emissiveAmount = getProceduralColors(diffuse, specular, shininess); _fragColor0 = vec4(diffuse.rgb, 1.0); - _fragColor1 = vec4(bestFitNormal(normalize(_normal.xyz)), 1.0 - (emissiveAmount / 2.0)); + _fragColor1 = vec4(packNormal(normalize(_normal.xyz)), 1.0 - (emissiveAmount / 2.0)); _fragColor2 = vec4(specular, shininess / 128.0); } )SCRIBE"; diff --git a/libraries/render-utils/src/DeferredBuffer.slh b/libraries/render-utils/src/DeferredBuffer.slh index aed89b30d0..b9c65a3bff 100755 --- a/libraries/render-utils/src/DeferredBuffer.slh +++ b/libraries/render-utils/src/DeferredBuffer.slh @@ -51,4 +51,70 @@ float packUnlit() { return FRAG_PACK_UNLIT; } + +vec2 signNotZero(vec2 v) { + return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); +} + +vec2 float32x3_to_oct(in vec3 v) { + vec2 p = v.xy * (1.0 / (abs(v.x) + abs(v.y) + abs(v.z))); + return ((v.z <= 0.0) ? ((1.0 - abs(p.yx)) * signNotZero(p)) : p); +} + + +vec3 oct_to_float32x3(in vec2 e) { + vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); + if (v.z < 0) { + v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); + } + return normalize(v); +} + +vec3 snorm12x2_to_unorm8x3(vec2 f) { + vec2 u = vec2(round(clamp(f, -1.0, 1.0) * 2047.0 + 2047.0)); + float t = floor(u.y / 256.0); + + return floor(vec3( + u.x / 16.0, + fract(u.x / 16.0) * 256.0 + t, + u.y - t * 256.0 + )) / 255.0; +} + +vec2 unorm8x3_to_snorm12x2(vec3 u) { + u *= 255.0; + u.y *= (1.0 / 16.0); + vec2 s = vec2( u.x * 16.0 + floor(u.y), + fract(u.y) * (16.0 * 256.0) + u.z); + return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); +} + +uniform sampler2D normalFittingMap; + +vec3 bestFitNormal(vec3 normal) { + vec3 absNorm = abs(normal); + float maxNAbs = max(absNorm.z, max(absNorm.x, absNorm.y)); + + vec2 texcoord = (absNorm.z < maxNAbs ? + (absNorm.y < maxNAbs ? absNorm.yz : absNorm.xz) : + absNorm.xy); + texcoord = (texcoord.x < texcoord.y ? texcoord.yx : texcoord.xy); + texcoord.y /= texcoord.x; + vec3 cN = normal / maxNAbs; + + float fittingScale = texture(normalFittingMap, texcoord).a; + cN *= fittingScale; + + return (cN * 0.5 + 0.5); +} + +vec3 packNormal(in vec3 n) { + return snorm12x2_to_unorm8x3(float32x3_to_oct(n)); +} + +vec3 unpackNormal(in vec3 p) { + return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); +} + + <@endif@> diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index 569063955d..7714859f5a 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -100,7 +100,7 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { frag.obscurance = texture(obscuranceMap, texcoord).x; // Unpack the normal from the map - frag.normal = normalize(frag.normalVal.xyz * 2.0 - vec3(1.0)); + frag.normal = unpackNormal(frag.normalVal.xyz); frag.roughness = frag.normalVal.a; // Diffuse color and unpack the mode and the metallicness diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 2be38fbea3..e869f32dc6 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -17,24 +17,6 @@ layout(location = 0) out vec4 _fragColor0; layout(location = 1) out vec4 _fragColor1; layout(location = 2) out vec4 _fragColor2; -uniform sampler2D normalFittingMap; - -vec3 bestFitNormal(vec3 normal) { - vec3 absNorm = abs(normal); - float maxNAbs = max(absNorm.z, max(absNorm.x, absNorm.y)); - - vec2 texcoord = (absNorm.z < maxNAbs ? - (absNorm.y < maxNAbs ? absNorm.yz : absNorm.xz) : - absNorm.xy); - texcoord = (texcoord.x < texcoord.y ? texcoord.yx : texcoord.xy); - texcoord.y /= texcoord.x; - vec3 cN = normal / maxNAbs; - - float fittingScale = texture(normalFittingMap, texcoord).a; - cN *= fittingScale; - return (cN * 0.5 + 0.5); -} - // the alpha threshold const float alphaThreshold = 0.5; @@ -55,7 +37,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness discard; } _fragColor0 = vec4(albedo, packShadedMetallic(metallic)); - _fragColor1 = vec4(bestFitNormal(normal), clamp(roughness, 0.0, 1.0)); + _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(emissive, occlusion); } @@ -65,7 +47,7 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float r discard; } _fragColor0 = vec4(albedo, packLightmappedMetallic(metallic)); - _fragColor1 = vec4(bestFitNormal(normal), clamp(roughness, 0.0, 1.0)); + _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(emissive, 1.0); } @@ -74,7 +56,7 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { discard; } _fragColor0 = vec4(color, packUnlit()); - _fragColor1 = vec4(bestFitNormal(normal), 1.0); + _fragColor1 = vec4(packNormal(normal), 1.0); //_fragColor2 = vec4(vec3(0.0), 1.0); // If unlit, do not worry about the emissive color target } diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 3223ee5535..2d322b1726 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -59,8 +59,8 @@ void FramebufferCache::createPrimaryFramebuffer() { _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create()); - // auto colorFormat = gpu::Element::COLOR_RGBA_32; auto colorFormat = gpu::Element::COLOR_SRGBA_32; + auto linearFormat = gpu::Element::COLOR_RGBA_32; auto width = _frameBufferSize.width(); auto height = _frameBufferSize.height(); @@ -70,7 +70,8 @@ void FramebufferCache::createPrimaryFramebuffer() { _primaryFramebuffer->setRenderBuffer(0, _primaryColorTexture); _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); From 44fe7c11443ddab37f97ec8a85ffad17cc3831d9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 2 Jun 2016 21:26:33 +1200 Subject: [PATCH 0283/1237] Add Entities.getChildrenIDs() method --- .../entities/src/EntityScriptingInterface.cpp | 21 +++++++++++++++++++ .../entities/src/EntityScriptingInterface.h | 1 + 2 files changed, 22 insertions(+) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 15c2bffd80..d09fc60d9b 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1121,6 +1121,27 @@ QStringList EntityScriptingInterface::getJointNames(const QUuid& entityID) { return result; } +QVector EntityScriptingInterface::getChildrenIDs(const QUuid& parentID) { + QVector result; + if (!_entityTree) { + return result; + } + + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(parentID); + if (!entity) { + qDebug() << "EntityScriptingInterface::getChildrenIDs - no entity with ID" << parentID; + return result; + } + + _entityTree->withReadLock([&] { + entity->forEachChild([&](SpatiallyNestablePointer child) { + result.push_back(child->getID()); + }); + }); + + return result; +} + QVector EntityScriptingInterface::getChildrenIDsOfJoint(const QUuid& parentID, int jointIndex) { QVector result; if (!_entityTree) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 2f5446874b..8ae6a77dab 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -171,6 +171,7 @@ public slots: Q_INVOKABLE int getJointIndex(const QUuid& entityID, const QString& name); Q_INVOKABLE QStringList getJointNames(const QUuid& entityID); + Q_INVOKABLE QVector getChildrenIDs(const QUuid& parentID); Q_INVOKABLE QVector getChildrenIDsOfJoint(const QUuid& parentID, int jointIndex); signals: From ce7fa5146529d1b243c6bee06464ecd5f935381e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 2 Jun 2016 19:59:41 +1200 Subject: [PATCH 0284/1237] Make checkboxes in user permissions table editable --- .../resources/web/settings/js/settings.js | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 52337db601..f5b7a61d2a 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -232,6 +232,16 @@ $(document).ready(function(){ badgeSidebarForDifferences($(this)); }); + + // Bootstrap switch in table + $('#' + Settings.FORM_ID).on('switchChange.bootstrapSwitch', 'input.toggle-checkbox', function () { + // Bootstrap switches in table: set the changed data attribute for all rows. + var row = $(this).closest('tr'); + row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); + updateDataChangedForSiblingRows(row, true); + }); + + $('.advanced-toggle').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) @@ -966,16 +976,30 @@ function makeTable(setting, keypath, setting_value, isLocked) { colName = keypath + "." + rowIndexOrName + "." + col.name; } - // setup the td for this column - html += ""; + if (isArray && col.type === "checkbox") { - // add the actual value to the td so it is displayed - html += colValue; + html += "" + html += ""; + } else { + + // setup the td for this column + html += ""; + + // add the actual value to the td so it is displayed + html += colValue; + + // for values to be posted properly we add a hidden input to this td + html += ""; + + html += ""; + } - html += ""; }) if (!isLocked && !setting.read_only) { From e24ba4caf67e370c95fb33001110f14150cc7c9b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 2 Jun 2016 19:59:53 +1200 Subject: [PATCH 0285/1237] Code tidying --- domain-server/resources/web/settings/js/settings.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index f5b7a61d2a..35e9d64d84 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -968,11 +968,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { colValue = rowIsObject ? row[col.name] : row; colName = keypath + "[" + rowIndexOrName + "]" + (rowIsObject ? "." + col.name : ""); } else { - if (col.type === "checkbox") { - colValue = (row[col.name] ? "X" : "") - } else { - colValue = row[col.name]; - } + colValue = row[col.name]; colName = keypath + "." + rowIndexOrName + "." + col.name; } @@ -1043,13 +1039,11 @@ function makeTableInputs(setting) { _.each(setting.columns, function(col) { if (col.type === "checkbox") { html += "" - // html += "
" html += "\ From 7bd29c13b0fd28ec27252adf8caa15995ef73185 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 09:09:25 -0700 Subject: [PATCH 0286/1237] corrected oculus touch rotation and converted hand pose to avatar coordinate system --- .../oculus/src/OculusControllerManager.cpp | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 09ab6ec159..822b751ec4 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -220,12 +220,51 @@ void OculusControllerManager::TouchDevice::focusOutEvent() { void OculusControllerManager::TouchDevice::handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX auto poseId = hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND; auto& pose = _poseStateMap[poseId]; + + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + pose.translation = toGlm(handPose.ThePose.Position); - pose.rotation = toGlm(handPose.ThePose.Orientation); + pose.rotation = toGlm(handPose.ThePose.Orientation)*touchToHand; pose.angularVelocity = toGlm(handPose.AngularVelocity); pose.velocity = toGlm(handPose.LinearVelocity); + + // transform into avatar frame + glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + pose = pose.transform(controllerToAvatar); } controller::Input::NamedVector OculusControllerManager::TouchDevice::getAvailableInputs() const { From 0035774eb27099076a06c9fed670ed82a8946b3a Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 25 May 2016 14:57:34 -0700 Subject: [PATCH 0287/1237] Fix for models exported from Daz3D. The way post rotations were being applied was incorrect. I guess we've never tried a model that used the Maya Rotate Axis property before... --- libraries/fbx/src/FBXReader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 12cd0b0a91..e6f2741098 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -759,7 +759,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS model.preTransform = glm::translate(rotationOffset) * glm::translate(rotationPivot); model.preRotation = glm::quat(glm::radians(preRotation)); model.rotation = glm::quat(glm::radians(rotation)); - model.postRotation = glm::quat(glm::radians(postRotation)); + model.postRotation = glm::inverse(glm::quat(glm::radians(postRotation))); model.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) * glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot); // NOTE: angles from the FBX file are in degrees From 99846677421f44e2a5fe1caea508cc1c431e9ddf Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 10:09:41 -0700 Subject: [PATCH 0288/1237] touch properly sets pose.valid and further corrected hand rotation --- .../oculus/src/OculusControllerManager.cpp | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 822b751ec4..f19282a8f2 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -183,6 +183,8 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control ++numTrackedControllers; if (REQUIRED_HAND_STATUS == (tracking.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { handlePose(deltaTime, inputCalibrationData, hand, tracking.HandPoses[hand]); + } else { + _poseStateMap[hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND].valid = false; } }); using namespace controller; @@ -250,6 +252,25 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, // the rotation to align the Touch axes with those of the hands is: // // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + auto poseId = hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND; auto& pose = _poseStateMap[poseId]; @@ -257,10 +278,15 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); static const glm::quat touchToHand = yFlip * quarterX; + static const float sign = (hand == ovrHand_Left ? 1.0f : -1.0f); + static const glm::quat signedQuarterZ = glm::angleAxis(sign * PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat signedRotationOffset = glm::inverse(signedQuarterZ) * touchToHand; + pose.translation = toGlm(handPose.ThePose.Position); - pose.rotation = toGlm(handPose.ThePose.Orientation)*touchToHand; + pose.rotation = toGlm(handPose.ThePose.Orientation)*signedRotationOffset; pose.angularVelocity = toGlm(handPose.AngularVelocity); pose.velocity = toGlm(handPose.LinearVelocity); + pose.valid = true; // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; From c9ea85e659302e8df170aa9beb460087a0275278 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 10:50:22 -0700 Subject: [PATCH 0289/1237] fixed left hand rotation --- plugins/oculus/src/OculusControllerManager.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index f19282a8f2..2fd48bb611 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -278,12 +278,16 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); static const glm::quat touchToHand = yFlip * quarterX; - static const float sign = (hand == ovrHand_Left ? 1.0f : -1.0f); - static const glm::quat signedQuarterZ = glm::angleAxis(sign * PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat signedRotationOffset = glm::inverse(signedQuarterZ) * touchToHand; + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ) * touchToHand; + + auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); pose.translation = toGlm(handPose.ThePose.Position); - pose.rotation = toGlm(handPose.ThePose.Orientation)*signedRotationOffset; + pose.rotation = toGlm(handPose.ThePose.Orientation)*rotationOffset; pose.angularVelocity = toGlm(handPose.AngularVelocity); pose.velocity = toGlm(handPose.LinearVelocity); pose.valid = true; From a8228d42ee9efeccd0548809e14a9e3130c51eef Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 2 Jun 2016 11:57:24 -0700 Subject: [PATCH 0290/1237] Improve max users explanation --- domain-server/resources/describe-settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 53d062d4bd..cac0d28e1e 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -111,7 +111,7 @@ { "name": "maximum_user_capacity", "label": "Maximum User Capacity", - "help": "The limit on how many avatars can be connected at once. 0 means no limit.", + "help": "The limit on how many users can be connected at once (0 means no limit). Avatars connected from the same machine will not count towards this limit.", "placeholder": "0", "default": "0", "advanced": false From 6ca02dcad24d4a1a1fa195120c6e24c47509252c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 12:11:13 -0700 Subject: [PATCH 0291/1237] expose Controller.triggerHapticPulse to javascript (currently does nothing) --- .../controllers/src/controllers/InputDevice.h | 5 +- .../src/controllers/ScriptingInterface.cpp | 104 +++++++++--------- .../src/controllers/ScriptingInterface.h | 6 +- .../src/controllers/UserInputMapper.cpp | 8 ++ .../src/controllers/UserInputMapper.h | 1 + 5 files changed, 71 insertions(+), 53 deletions(-) diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index afb1f7d1f7..3c8e728f4d 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -51,10 +51,13 @@ public: float getValue(const Input& input) const; float getValue(ChannelType channelType, uint16_t channel) const; - Pose getPoseValue(uint16_t channel) const; + Pose getPoseValue(uint16_t channel) const; const QString& getName() const { return _name; } + // By default, Input Devices do not support haptics + virtual bool triggerHapticPulse(float strength, float duration, bool leftHand) { return false; } + // Update call MUST be called once per simulation loop // It takes care of updating the action states and deltas virtual void update(float deltaTime, const InputCalibrationData& inputCalibrationData) {}; diff --git a/libraries/controllers/src/controllers/ScriptingInterface.cpp b/libraries/controllers/src/controllers/ScriptingInterface.cpp index c2e64ca19e..a7d9270ddf 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.cpp +++ b/libraries/controllers/src/controllers/ScriptingInterface.cpp @@ -76,69 +76,73 @@ controller::ScriptingInterface::ScriptingInterface() { namespace controller { - QObject* ScriptingInterface::newMapping(const QString& mappingName) { - auto userInputMapper = DependencyManager::get(); - return new MappingBuilderProxy(*userInputMapper, userInputMapper->newMapping(mappingName)); - } + QObject* ScriptingInterface::newMapping(const QString& mappingName) { + auto userInputMapper = DependencyManager::get(); + return new MappingBuilderProxy(*userInputMapper, userInputMapper->newMapping(mappingName)); + } - void ScriptingInterface::enableMapping(const QString& mappingName, bool enable) { - auto userInputMapper = DependencyManager::get(); - userInputMapper->enableMapping(mappingName, enable); - } + void ScriptingInterface::enableMapping(const QString& mappingName, bool enable) { + auto userInputMapper = DependencyManager::get(); + userInputMapper->enableMapping(mappingName, enable); + } - float ScriptingInterface::getValue(const int& source) const { - auto userInputMapper = DependencyManager::get(); - return userInputMapper->getValue(Input((uint32_t)source)); - } + float ScriptingInterface::getValue(const int& source) const { + auto userInputMapper = DependencyManager::get(); + return userInputMapper->getValue(Input((uint32_t)source)); + } - float ScriptingInterface::getButtonValue(StandardButtonChannel source, uint16_t device) const { - return getValue(Input(device, source, ChannelType::BUTTON).getID()); - } + float ScriptingInterface::getButtonValue(StandardButtonChannel source, uint16_t device) const { + return getValue(Input(device, source, ChannelType::BUTTON).getID()); + } - float ScriptingInterface::getAxisValue(StandardAxisChannel source, uint16_t device) const { - return getValue(Input(device, source, ChannelType::AXIS).getID()); - } + float ScriptingInterface::getAxisValue(StandardAxisChannel source, uint16_t device) const { + return getValue(Input(device, source, ChannelType::AXIS).getID()); + } - Pose ScriptingInterface::getPoseValue(const int& source) const { - auto userInputMapper = DependencyManager::get(); - return userInputMapper->getPose(Input((uint32_t)source)); - } - - Pose ScriptingInterface::getPoseValue(StandardPoseChannel source, uint16_t device) const { - return getPoseValue(Input(device, source, ChannelType::POSE).getID()); - } + Pose ScriptingInterface::getPoseValue(const int& source) const { + auto userInputMapper = DependencyManager::get(); + return userInputMapper->getPose(Input((uint32_t)source)); + } - QVector ScriptingInterface::getAllActions() { - return DependencyManager::get()->getAllActions(); - } + Pose ScriptingInterface::getPoseValue(StandardPoseChannel source, uint16_t device) const { + return getPoseValue(Input(device, source, ChannelType::POSE).getID()); + } - QString ScriptingInterface::getDeviceName(unsigned int device) { - return DependencyManager::get()->getDeviceName((unsigned short)device); - } + QVector ScriptingInterface::getAllActions() { + return DependencyManager::get()->getAllActions(); + } - QVector ScriptingInterface::getAvailableInputs(unsigned int device) { - return DependencyManager::get()->getAvailableInputs((unsigned short)device); - } + QString ScriptingInterface::getDeviceName(unsigned int device) { + return DependencyManager::get()->getDeviceName((unsigned short)device); + } - int ScriptingInterface::findDevice(QString name) { - return DependencyManager::get()->findDevice(name); - } + QVector ScriptingInterface::getAvailableInputs(unsigned int device) { + return DependencyManager::get()->getAvailableInputs((unsigned short)device); + } - QVector ScriptingInterface::getDeviceNames() { - return DependencyManager::get()->getDeviceNames(); - } + int ScriptingInterface::findDevice(QString name) { + return DependencyManager::get()->findDevice(name); + } - float ScriptingInterface::getActionValue(int action) { - return DependencyManager::get()->getActionState(Action(action)); - } + QVector ScriptingInterface::getDeviceNames() { + return DependencyManager::get()->getDeviceNames(); + } - int ScriptingInterface::findAction(QString actionName) { - return DependencyManager::get()->findAction(actionName); - } + float ScriptingInterface::getActionValue(int action) { + return DependencyManager::get()->getActionState(Action(action)); + } - QVector ScriptingInterface::getActionNames() const { - return DependencyManager::get()->getActionNames(); - } + int ScriptingInterface::findAction(QString actionName) { + return DependencyManager::get()->findAction(actionName); + } + + QVector ScriptingInterface::getActionNames() const { + return DependencyManager::get()->getActionNames(); + } + + bool ScriptingInterface::triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand) const { + return DependencyManager::get()->triggerHapticPulse(device, strength, duration, leftHand); + } void ScriptingInterface::updateMaps() { QVariantMap newHardware; diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index f30212a09e..7f7120a907 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -77,12 +77,14 @@ namespace controller { Q_INVOKABLE QVector getDeviceNames(); Q_INVOKABLE int findAction(QString actionName); Q_INVOKABLE QVector getActionNames() const; - + Q_INVOKABLE float getValue(const int& source) const; Q_INVOKABLE float getButtonValue(StandardButtonChannel source, uint16_t device = 0) const; Q_INVOKABLE float getAxisValue(StandardAxisChannel source, uint16_t device = 0) const; Q_INVOKABLE Pose getPoseValue(const int& source) const; - Q_INVOKABLE Pose getPoseValue(StandardPoseChannel source, uint16_t device = 0) const; + Q_INVOKABLE Pose getPoseValue(StandardPoseChannel source, uint16_t device = 0) const; + + Q_INVOKABLE bool triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand = true) const; Q_INVOKABLE QObject* newMapping(const QString& mappingName = QUuid::createUuid().toString()); Q_INVOKABLE void enableMapping(const QString& mappingName, bool enable = true); diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index c1ee3ce36c..2c9138e120 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -336,6 +336,14 @@ QVector UserInputMapper::getActionNames() const { return result; } +bool UserInputMapper::triggerHapticPulse(uint16 deviceID, float strength, float duration, bool leftHand) { + Locker locker(_lock); + if (_registeredDevices.find(deviceID) != _registeredDevices.end()) { + return _registeredDevices[deviceID]->triggerHapticPulse(strength, duration, leftHand); + } + return false; +} + int actionMetaTypeId = qRegisterMetaType(); int inputMetaTypeId = qRegisterMetaType(); int inputPairMetaTypeId = qRegisterMetaType(); diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index 9c79415b6e..ac60247e8e 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -89,6 +89,7 @@ namespace controller { void setActionState(Action action, float value) { _actionStates[toInt(action)] = value; } void deltaActionState(Action action, float delta) { _actionStates[toInt(action)] += delta; } void setActionState(Action action, const Pose& value) { _poseStates[toInt(action)] = value; } + bool triggerHapticPulse(uint16 deviceID, float strength, float duration, bool leftHand); static Input makeStandardInput(controller::StandardButtonChannel button); static Input makeStandardInput(controller::StandardAxisChannel axis); From 642438a2596c8ea6d4d7f3fb7b55c11deb7d4545 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 12:18:56 -0700 Subject: [PATCH 0292/1237] fix tabs --- .../controllers/src/controllers/InputDevice.h | 6 +- .../src/controllers/ScriptingInterface.cpp | 106 +++++++++--------- .../src/controllers/ScriptingInterface.h | 6 +- .../src/controllers/UserInputMapper.cpp | 10 +- .../src/controllers/UserInputMapper.h | 2 +- 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index 3c8e728f4d..cc158497e0 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -51,12 +51,12 @@ public: float getValue(const Input& input) const; float getValue(ChannelType channelType, uint16_t channel) const; - Pose getPoseValue(uint16_t channel) const; + Pose getPoseValue(uint16_t channel) const; const QString& getName() const { return _name; } - // By default, Input Devices do not support haptics - virtual bool triggerHapticPulse(float strength, float duration, bool leftHand) { return false; } + // By default, Input Devices do not support haptics + virtual bool triggerHapticPulse(float strength, float duration, bool leftHand) { return false; } // Update call MUST be called once per simulation loop // It takes care of updating the action states and deltas diff --git a/libraries/controllers/src/controllers/ScriptingInterface.cpp b/libraries/controllers/src/controllers/ScriptingInterface.cpp index a7d9270ddf..97b7a9ddd9 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.cpp +++ b/libraries/controllers/src/controllers/ScriptingInterface.cpp @@ -76,73 +76,73 @@ controller::ScriptingInterface::ScriptingInterface() { namespace controller { - QObject* ScriptingInterface::newMapping(const QString& mappingName) { - auto userInputMapper = DependencyManager::get(); - return new MappingBuilderProxy(*userInputMapper, userInputMapper->newMapping(mappingName)); - } + QObject* ScriptingInterface::newMapping(const QString& mappingName) { + auto userInputMapper = DependencyManager::get(); + return new MappingBuilderProxy(*userInputMapper, userInputMapper->newMapping(mappingName)); + } - void ScriptingInterface::enableMapping(const QString& mappingName, bool enable) { - auto userInputMapper = DependencyManager::get(); - userInputMapper->enableMapping(mappingName, enable); - } + void ScriptingInterface::enableMapping(const QString& mappingName, bool enable) { + auto userInputMapper = DependencyManager::get(); + userInputMapper->enableMapping(mappingName, enable); + } - float ScriptingInterface::getValue(const int& source) const { - auto userInputMapper = DependencyManager::get(); - return userInputMapper->getValue(Input((uint32_t)source)); - } + float ScriptingInterface::getValue(const int& source) const { + auto userInputMapper = DependencyManager::get(); + return userInputMapper->getValue(Input((uint32_t)source)); + } - float ScriptingInterface::getButtonValue(StandardButtonChannel source, uint16_t device) const { - return getValue(Input(device, source, ChannelType::BUTTON).getID()); - } + float ScriptingInterface::getButtonValue(StandardButtonChannel source, uint16_t device) const { + return getValue(Input(device, source, ChannelType::BUTTON).getID()); + } - float ScriptingInterface::getAxisValue(StandardAxisChannel source, uint16_t device) const { - return getValue(Input(device, source, ChannelType::AXIS).getID()); - } + float ScriptingInterface::getAxisValue(StandardAxisChannel source, uint16_t device) const { + return getValue(Input(device, source, ChannelType::AXIS).getID()); + } - Pose ScriptingInterface::getPoseValue(const int& source) const { - auto userInputMapper = DependencyManager::get(); - return userInputMapper->getPose(Input((uint32_t)source)); - } + Pose ScriptingInterface::getPoseValue(const int& source) const { + auto userInputMapper = DependencyManager::get(); + return userInputMapper->getPose(Input((uint32_t)source)); + } + + Pose ScriptingInterface::getPoseValue(StandardPoseChannel source, uint16_t device) const { + return getPoseValue(Input(device, source, ChannelType::POSE).getID()); + } - Pose ScriptingInterface::getPoseValue(StandardPoseChannel source, uint16_t device) const { - return getPoseValue(Input(device, source, ChannelType::POSE).getID()); - } + QVector ScriptingInterface::getAllActions() { + return DependencyManager::get()->getAllActions(); + } - QVector ScriptingInterface::getAllActions() { - return DependencyManager::get()->getAllActions(); - } + QString ScriptingInterface::getDeviceName(unsigned int device) { + return DependencyManager::get()->getDeviceName((unsigned short)device); + } - QString ScriptingInterface::getDeviceName(unsigned int device) { - return DependencyManager::get()->getDeviceName((unsigned short)device); - } + QVector ScriptingInterface::getAvailableInputs(unsigned int device) { + return DependencyManager::get()->getAvailableInputs((unsigned short)device); + } - QVector ScriptingInterface::getAvailableInputs(unsigned int device) { - return DependencyManager::get()->getAvailableInputs((unsigned short)device); - } + int ScriptingInterface::findDevice(QString name) { + return DependencyManager::get()->findDevice(name); + } - int ScriptingInterface::findDevice(QString name) { - return DependencyManager::get()->findDevice(name); - } + QVector ScriptingInterface::getDeviceNames() { + return DependencyManager::get()->getDeviceNames(); + } - QVector ScriptingInterface::getDeviceNames() { - return DependencyManager::get()->getDeviceNames(); - } + float ScriptingInterface::getActionValue(int action) { + return DependencyManager::get()->getActionState(Action(action)); + } - float ScriptingInterface::getActionValue(int action) { - return DependencyManager::get()->getActionState(Action(action)); - } + int ScriptingInterface::findAction(QString actionName) { + return DependencyManager::get()->findAction(actionName); + } - int ScriptingInterface::findAction(QString actionName) { - return DependencyManager::get()->findAction(actionName); - } + QVector ScriptingInterface::getActionNames() const { + return DependencyManager::get()->getActionNames(); + } - QVector ScriptingInterface::getActionNames() const { - return DependencyManager::get()->getActionNames(); - } - - bool ScriptingInterface::triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand) const { - return DependencyManager::get()->triggerHapticPulse(device, strength, duration, leftHand); - } + bool ScriptingInterface::triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand) const { + return DependencyManager::get()->triggerHapticPulse(device, strength, duration, leftHand); + } void ScriptingInterface::updateMaps() { QVariantMap newHardware; diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index 7f7120a907..cce159a9a7 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -77,14 +77,14 @@ namespace controller { Q_INVOKABLE QVector getDeviceNames(); Q_INVOKABLE int findAction(QString actionName); Q_INVOKABLE QVector getActionNames() const; - + Q_INVOKABLE float getValue(const int& source) const; Q_INVOKABLE float getButtonValue(StandardButtonChannel source, uint16_t device = 0) const; Q_INVOKABLE float getAxisValue(StandardAxisChannel source, uint16_t device = 0) const; Q_INVOKABLE Pose getPoseValue(const int& source) const; - Q_INVOKABLE Pose getPoseValue(StandardPoseChannel source, uint16_t device = 0) const; + Q_INVOKABLE Pose getPoseValue(StandardPoseChannel source, uint16_t device = 0) const; - Q_INVOKABLE bool triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand = true) const; + Q_INVOKABLE bool triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand = true) const; Q_INVOKABLE QObject* newMapping(const QString& mappingName = QUuid::createUuid().toString()); Q_INVOKABLE void enableMapping(const QString& mappingName, bool enable = true); diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 2c9138e120..c7c62ad7d9 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -337,11 +337,11 @@ QVector UserInputMapper::getActionNames() const { } bool UserInputMapper::triggerHapticPulse(uint16 deviceID, float strength, float duration, bool leftHand) { - Locker locker(_lock); - if (_registeredDevices.find(deviceID) != _registeredDevices.end()) { - return _registeredDevices[deviceID]->triggerHapticPulse(strength, duration, leftHand); - } - return false; + Locker locker(_lock); + if (_registeredDevices.find(deviceID) != _registeredDevices.end()) { + return _registeredDevices[deviceID]->triggerHapticPulse(strength, duration, leftHand); + } + return false; } int actionMetaTypeId = qRegisterMetaType(); diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index ac60247e8e..b23657fff7 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -89,7 +89,7 @@ namespace controller { void setActionState(Action action, float value) { _actionStates[toInt(action)] = value; } void deltaActionState(Action action, float delta) { _actionStates[toInt(action)] += delta; } void setActionState(Action action, const Pose& value) { _poseStates[toInt(action)] = value; } - bool triggerHapticPulse(uint16 deviceID, float strength, float duration, bool leftHand); + bool triggerHapticPulse(uint16 deviceID, float strength, float duration, bool leftHand); static Input makeStandardInput(controller::StandardButtonChannel button); static Input makeStandardInput(controller::StandardAxisChannel axis); From 69971a3439163e912a5c677edcf6796ec234687f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 12:20:05 -0700 Subject: [PATCH 0293/1237] fix one more tab --- libraries/controllers/src/controllers/ScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index cce159a9a7..015ec25454 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -77,7 +77,7 @@ namespace controller { Q_INVOKABLE QVector getDeviceNames(); Q_INVOKABLE int findAction(QString actionName); Q_INVOKABLE QVector getActionNames() const; - + Q_INVOKABLE float getValue(const int& source) const; Q_INVOKABLE float getButtonValue(StandardButtonChannel source, uint16_t device = 0) const; Q_INVOKABLE float getAxisValue(StandardAxisChannel source, uint16_t device = 0) const; From e6cc1fabe0d97006024e38de4a498b1f364fa860 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 2 Jun 2016 12:38:54 -0700 Subject: [PATCH 0294/1237] Do not send stale vive data as valid after deactivating. --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index dba8fca208..a74e5458af 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -96,6 +96,11 @@ void OpenVrDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { + // Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and + // we don't want ViveControllerManager to consider old values to be valid. + for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + _trackedDevicePose[i].bPoseIsValid = false; + } releaseOpenVrSystem(); _system = nullptr; } From 83f2c723eb3749a7c939429fd9724142fcd0c993 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 2 Jun 2016 13:15:30 -0700 Subject: [PATCH 0295/1237] collect permissions into their own data structure --- .../src/AssignmentClientMonitor.cpp | 4 +- .../resources/describe-settings.json | 6 + domain-server/src/AgentPermissions.h | 40 -- domain-server/src/DomainGatekeeper.cpp | 361 +++++++----------- domain-server/src/DomainGatekeeper.h | 6 +- domain-server/src/DomainServer.cpp | 3 +- .../src/DomainServerSettingsManager.cpp | 53 ++- .../src/DomainServerSettingsManager.h | 6 +- libraries/networking/src/AgentPermissions.cpp | 43 +++ libraries/networking/src/AgentPermissions.h | 79 ++++ libraries/networking/src/LimitedNodeList.cpp | 37 +- libraries/networking/src/LimitedNodeList.h | 13 +- libraries/networking/src/Node.cpp | 15 +- libraries/networking/src/Node.h | 15 +- libraries/networking/src/NodeList.cpp | 19 +- 15 files changed, 361 insertions(+), 339 deletions(-) delete mode 100644 domain-server/src/AgentPermissions.h create mode 100644 libraries/networking/src/AgentPermissions.cpp create mode 100644 libraries/networking/src/AgentPermissions.h diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 322fe6e57e..8ba253d549 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -286,8 +286,8 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer()->addOrUpdateNode - (senderID, NodeType::Unassigned, senderSockAddr, senderSockAddr, false, false); + matchingNode = DependencyManager::get()->addOrUpdateNode(senderID, NodeType::Unassigned, + senderSockAddr, senderSockAddr); auto childData = std::unique_ptr { new AssignmentClientChildData(Assignment::Type::AllTypes) }; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index a856666bc9..b82a3ae1b1 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -135,6 +135,12 @@ "label": "Write Assets", "type": "checkbox", "default": false + }, + { + "name": "id_can_connect_past_max_capacity", + "label": "Ignore Max Capacity", + "type": "checkbox", + "default": false } ] } diff --git a/domain-server/src/AgentPermissions.h b/domain-server/src/AgentPermissions.h deleted file mode 100644 index 50c90581c1..0000000000 --- a/domain-server/src/AgentPermissions.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// AgentPermissions.h -// domain-server/src -// -// Created by Seth Alves on 2016-6-1. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_AgentPermissions_h -#define hifi_AgentPermissions_h - -class AgentPermissions { -public: - AgentPermissions(QMap perms) { - _id = perms["permissions_id"].toString(); - canConnectToDomain = perms["id_can_connect"].toBool(); - canAdjustLocks = perms["id_can_adjust_locks"].toBool(); - canRezPermanentEntities = perms["id_can_rez"].toBool(); - canRezTemporaryEntities = perms["id_can_rez_tmp"].toBool(); - canWriteToAssetServer = perms["id_can_write_to_asset_server"].toBool(); - }; - - QString getID() { return _id; } - - bool canConnectToDomain { false }; - bool canAdjustLocks { false }; - bool canRezPermanentEntities { false }; - bool canRezTemporaryEntities { false }; - bool canWriteToAssetServer { false }; - -protected: - QString _id; -}; - -using AgentPermissionsPointer = std::shared_ptr; - -#endif // hifi_AgentPermissions_h diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index b940d46849..c80969c8b4 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -26,7 +26,7 @@ using SharedAssignmentPointer = QSharedPointer; DomainGatekeeper::DomainGatekeeper(DomainServer* server) : _server(server) { - + } void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID, @@ -38,7 +38,7 @@ void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) { auto it = _pendingAssignedNodes.find(tempUUID); - + if (it != _pendingAssignedNodes.end()) { return it->second.getAssignmentUUID(); } else { @@ -64,57 +64,57 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSenderSockAddr(), + sendConnectionDeniedPacket(protocolVersionError, message->getSenderSockAddr(), DomainHandler::ConnectionRefusedReason::ProtocolMismatch); return; } - + if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) { qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection."; return; } - + static const NodeSet VALID_NODE_TYPES { NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer }; - + if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) { qDebug() << "Received an invalid node type with connect request. Will not allow connection from" << nodeConnection.senderSockAddr << ": " << nodeConnection.nodeType; return; } - + // check if this connect request matches an assignment in the queue auto pendingAssignment = _pendingAssignedNodes.find(nodeConnection.connectUUID); - + SharedNodePointer node; - + if (pendingAssignment != _pendingAssignedNodes.end()) { node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QString username; QByteArray usernameSignature; - + if (message->getBytesLeftToRead() > 0) { // read username from packet packetStream >> username; - + if (message->getBytesLeftToRead() > 0) { // read user signature from packet packetStream >> usernameSignature; } } - + node = processAgentConnectRequest(nodeConnection, username, usernameSignature); } - + if (node) { // set the sending sock addr and node interest set on this node DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); nodeData->setSendingSockAddr(message->getSenderSockAddr()); nodeData->setNodeInterestSet(nodeConnection.interestList.toSet()); nodeData->setPlaceName(nodeConnection.placeName); - + // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node); @@ -125,16 +125,16 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointerdequeueMatchingAssignment(it->second.getAssignmentUUID(), nodeConnection.nodeType); - + if (matchingQueuedAssignment) { qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID) << "matches unfulfilled assignment" @@ -149,124 +149,85 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID); return SharedNodePointer(); } - + // add the new node SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); - + DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + // set assignment related data on the linked data for this node nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); nodeData->setWasAssigned(true); - + // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); - + // always allow assignment clients to create and destroy entities - newNode->setIsAllowedEditor(true); - newNode->setCanRez(true); - + AgentPermissions userPerms; + userPerms.canAdjustLocks = true; + userPerms.canRezPermanentEntities = true; + newNode->setPermissions(userPerms); return newNode; } const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; -const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors"; -const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers"; +// const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors"; +// const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, const QByteArray& usernameSignature) { - + auto limitedNodeList = DependencyManager::get(); - - bool isRestrictingAccess = - _server->_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); - + + // start with empty permissions + AgentPermissions userPerms(username); + userPerms.setAll(false); + // check if this user is on our local machine - if this is true they are always allowed to connect QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress(); bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); - - // if we're using restricted access and this user is not local make sure we got a user signature - if (isRestrictingAccess && !isLocalUser) { - if (!username.isEmpty()) { - if (usernameSignature.isEmpty()) { - // if user didn't include usernameSignature in connect request, send a connectionToken packet - sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); - - // ask for their public key right now to make sure we have it - requestUserPublicKey(username); - - return SharedNodePointer(); - } - } + if (isLocalUser) { + userPerms |= _server->_settingsManager.getPermissionsForName("localhost"); } - - bool verifiedUsername = false; - - // if we do not have a local user we need to subject them to our verification and capacity checks - if (!isLocalUser) { - - // check if we need to look at the username signature - if (isRestrictingAccess) { - if (isVerifiedAllowedUser(username, usernameSignature, nodeConnection.senderSockAddr)) { - // we verified the user via their username and signature - set the verifiedUsername - // so we don't re-decrypt their sig if we're trying to exempt them from max capacity check (due to - // being in the allowed editors list) - verifiedUsername = true; - } else { - // failed to verify user - return a null shared ptr - return SharedNodePointer(); - } - } - - if (!isWithinMaxCapacity(username, usernameSignature, verifiedUsername, nodeConnection.senderSockAddr)) { - // we can't allow this user to connect because we are at max capacity (and they either aren't an allowed editor - // or couldn't be verified as one) + + if (!username.isEmpty() && usernameSignature.isEmpty()) { + // user is attempting to prove their identity to us, but we don't have enough information + sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); + // ask for their public key right now to make sure we have it + requestUserPublicKey(username); + if (!isLocalUser) { return SharedNodePointer(); } } - - // if this user is in the editors list (or if the editors list is empty) set the user's node's isAllowedEditor to true - const QVariant* allowedEditorsVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); - QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); - - // if the allowed editors list is empty then everyone can adjust locks - bool isAllowedEditor = allowedEditors.empty(); - - if (allowedEditors.contains(username, Qt::CaseInsensitive)) { - // we have a non-empty allowed editors list - check if this user is verified to be in it - if (!verifiedUsername) { - if (!verifyUserSignature(username, usernameSignature, HifiSockAddr())) { - // failed to verify a user that is in the allowed editors list - - // TODO: fix public key refresh in interface/metaverse and force this check - qDebug() << "Could not verify user" << username << "as allowed editor. In the interim this user" - << "will be given edit rights to avoid a thrasing of public key requests and connect requests."; - } - - isAllowedEditor = true; - } else { - // already verified this user and they are in the allowed editors list - isAllowedEditor = true; + + if (username.isEmpty()) { + // they didn't tell us who they are + userPerms |= _server->_settingsManager.getPermissionsForName("anonymous"); + } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { + // they are sent us a username and the signature verifies it + userPerms |= _server->_settingsManager.getPermissionsForName(username); + userPerms |= _server->_settingsManager.getPermissionsForName("logged-in"); + } else { + // they sent us a username, but it didn't check out + requestUserPublicKey(username); + if (!isLocalUser) { + return SharedNodePointer(); } } - - // check if only editors should be able to rez entities - const QVariant* editorsAreRezzersVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH); - - bool onlyEditorsAreRezzers = false; - if (editorsAreRezzersVariant) { - onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool(); + + if (!userPerms.canConnectToDomain) { + return SharedNodePointer(); } - - bool canRez = true; - if (onlyEditorsAreRezzers) { - canRez = isAllowedEditor; + + if (!userPerms.canConnectPastMaxCapacity && !isWithinMaxCapacity()) { + // we can't allow this user to connect because we are at max capacity + sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr, + DomainHandler::ConnectionRefusedReason::TooManyUsers); + return SharedNodePointer(); } QUuid hintNodeID; @@ -285,24 +246,23 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect return true; }); - + // add the connecting node (or re-use the matched one from eachNodeBreakable above) SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID); - + // set the edit rights for this user - newNode->setIsAllowedEditor(isAllowedEditor); - newNode->setCanRez(canRez); + newNode->setPermissions(userPerms); // grab the linked data for our new node so we can set the username DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + // if we have a username from the connect request, set it on the DomainServerNodeData nodeData->setUsername(username); - + // also add an interpolation to DomainServerNodeData so that servers can get username in stats nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(newNode->getUUID()), username); - + return newNode; } @@ -310,11 +270,11 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node QUuid nodeID) { HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr; SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID); - + if (connectedPeer) { // this user negotiated a connection with us via ICE, so re-use their ICE client ID nodeID = nodeConnection.connectUUID; - + if (connectedPeer->getActiveSocket()) { // set their discovered socket to whatever the activated socket on the network peer object was discoveredSocket = *connectedPeer->getActiveSocket(); @@ -325,15 +285,15 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node nodeID = QUuid::createUuid(); } } - + auto limitedNodeList = DependencyManager::get(); - + SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeID, nodeConnection.nodeType, nodeConnection.publicSockAddr, nodeConnection.localSockAddr); - + // So that we can send messages to this node at will - we need to activate the correct socket on this node now newNode->activateMatchingOrNewSymmetricSocket(discoveredSocket); - + return newNode; } @@ -343,21 +303,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, // it's possible this user can be allowed to connect, but we need to check their username signature QByteArray publicKeyArray = _userPublicKeys.value(username); - + const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); - + if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) { // if we do have a public key for the user, check for a signature match - + const unsigned char* publicKeyData = reinterpret_cast(publicKeyArray.constData()); - + // first load up the public key into an RSA struct RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size()); - + QByteArray lowercaseUsername = username.toLower().toUtf8(); QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()), QCryptographicHash::Sha256); - + if (rsaPublicKey) { int decryptResult = RSA_verify(NID_sha256, reinterpret_cast(usernameWithToken.constData()), @@ -365,29 +325,29 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, reinterpret_cast(usernameSignature.constData()), usernameSignature.size(), rsaPublicKey); - + if (decryptResult == 1) { qDebug() << "Username signature matches for" << username << "- allowing connection."; - + // free up the public key and remove connection token before we return RSA_free(rsaPublicKey); _connectionTokenHash.remove(username); - + return true; - + } else { if (!senderSockAddr.isNull()) { qDebug() << "Error decrypting username signature for " << username << "- denying connection."; sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginError); } - + // free up the public key, we don't need it anymore RSA_free(rsaPublicKey); } - + } else { - + // we can't let this user in since we couldn't convert their public key to an RSA key we could use if (!senderSockAddr.isNull()) { qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; @@ -402,86 +362,35 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, DomainHandler::ConnectionRefusedReason::LoginError); } } - + requestUserPublicKey(username); // no joy. maybe next time? return false; } -bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr) { - - if (username.isEmpty()) { - qDebug() << "Connect request denied - no username provided."; - - sendConnectionDeniedPacket("No username provided", senderSockAddr, - DomainHandler::ConnectionRefusedReason::LoginError); - - return false; - } - - QStringList allowedUsers = - _server->_settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList(); - - if (allowedUsers.contains(username, Qt::CaseInsensitive)) { - if (!verifyUserSignature(username, usernameSignature, senderSockAddr)) { - return false; - } - } else { - qDebug() << "Connect request denied for user" << username << "- not in allowed users list."; - sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr, - DomainHandler::ConnectionRefusedReason::NotAuthorized); - - return false; - } - - return true; -} - -bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature, - bool& verifiedUsername, - const HifiSockAddr& senderSockAddr) { +bool DomainGatekeeper::isWithinMaxCapacity() { // find out what our maximum capacity is - const QVariant* maximumUserCapacityVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); + const QVariant* maximumUserCapacityVariant = + valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; if (maximumUserCapacity > 0) { unsigned int connectedUsers = _server->countConnectedUsers(); if (connectedUsers >= maximumUserCapacity) { - // too many users, deny the new connection unless this user is an allowed editor - - const QVariant* allowedEditorsVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); - - QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); - if (allowedEditors.contains(username)) { - if (verifiedUsername || verifyUserSignature(username, usernameSignature, senderSockAddr)) { - verifiedUsername = true; - qDebug() << "Above maximum capacity -" << connectedUsers << "/" << maximumUserCapacity << - "but user" << username << "is in allowed editors list so will be allowed to connect."; - return true; - } - } - - // deny connection from this user qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; - sendConnectionDeniedPacket("Too many connected users.", senderSockAddr, - DomainHandler::ConnectionRefusedReason::TooManyUsers); - return false; } - + qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, allowing new connection."; } - + return true; } void DomainGatekeeper::preloadAllowedUserPublicKeys() { - const QVariant* allowedUsersVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH); - QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList(); - + QStringList allowedUsers = _server->_settingsManager.getAllNames(); + if (allowedUsers.size() > 0) { // in the future we may need to limit how many requests here - for now assume that lists of allowed users are not // going to create > 100 requests @@ -496,11 +405,11 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "publicKeyJSONCallback"; - + const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key"; - + qDebug() << "Requesting public key for user" << username; - + DependencyManager::get()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, callbackParams); @@ -508,38 +417,38 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) { QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); - + if (jsonObject["status"].toString() == "success") { // figure out which user this is for - + const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key"; QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING); - + if (usernameRegex.indexIn(requestReply.url().toString()) != -1) { QString username = usernameRegex.cap(1); - + qDebug() << "Storing a public key for user" << username; - + // pull the public key as a QByteArray from this response const QString JSON_DATA_KEY = "data"; const QString JSON_PUBLIC_KEY_KEY = "public_key"; - + _userPublicKeys[username] = QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()); } } } -void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, +void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, DomainHandler::ConnectionRefusedReason reasonCode) { // this is an agent and we've decided we won't let them connect - send them a packet to deny connection QByteArray utfString = reason.toUtf8(); quint16 payloadSize = utfString.size(); - + // setup the DomainConnectionDenied packet - auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, + auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, payloadSize + sizeof(payloadSize) + sizeof(uint8_t)); - + // pack in the reason the connection was denied (the client displays this) if (payloadSize > 0) { uint8_t reasonCodeWire = (uint8_t)reasonCode; @@ -547,7 +456,7 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H connectionDeniedPacket->writePrimitive(payloadSize); connectionDeniedPacket->write(utfString); } - + // send the packet off DependencyManager::get()->sendPacket(std::move(connectionDeniedPacket), senderSockAddr); } @@ -555,20 +464,20 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr) { // get the existing connection token or create a new one QUuid& connectionToken = _connectionTokenHash[username.toLower()]; - + if (connectionToken.isNull()) { connectionToken = QUuid::createUuid(); } - + // setup a static connection token packet static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID); - + // reset the packet before each time we send connectionTokenPacket->reset(); - + // write the connection token connectionTokenPacket->write(connectionToken.toRfc4122()); - + // send off the packet unreliably DependencyManager::get()->sendUnreliablePacket(*connectionTokenPacket, senderSockAddr); } @@ -576,33 +485,33 @@ void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const const int NUM_PEER_PINGS_BEFORE_DELETE = 2000 / UDP_PUNCH_PING_INTERVAL_MS; void DomainGatekeeper::pingPunchForConnectingPeer(const SharedNetworkPeer& peer) { - + if (peer->getConnectionAttempts() >= NUM_PEER_PINGS_BEFORE_DELETE) { // we've reached the maximum number of ping attempts qDebug() << "Maximum number of ping attempts reached for peer with ID" << peer->getUUID(); qDebug() << "Removing from list of connecting peers."; - + _icePeers.remove(peer->getUUID()); } else { auto limitedNodeList = DependencyManager::get(); - + // send the ping packet to the local and public sockets for this node auto localPingPacket = limitedNodeList->constructICEPingPacket(PingType::Local, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(localPingPacket), peer->getLocalSocket()); - + auto publicPingPacket = limitedNodeList->constructICEPingPacket(PingType::Public, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(publicPingPacket), peer->getPublicSocket()); - + peer->incrementConnectionAttempts(); } } void DomainGatekeeper::handlePeerPingTimeout() { NetworkPeer* senderPeer = qobject_cast(sender()); - + if (senderPeer) { SharedNetworkPeer sharedPeer = _icePeers.value(senderPeer->getUUID()); - + if (sharedPeer && !sharedPeer->getActiveSocket()) { pingPunchForConnectingPeer(sharedPeer); } @@ -613,24 +522,24 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointergetMessage()); - + NetworkPeer* receivedPeer = new NetworkPeer; iceResponseStream >> *receivedPeer; - + if (!_icePeers.contains(receivedPeer->getUUID())) { qDebug() << "New peer requesting ICE connection being added to hash -" << *receivedPeer; SharedNetworkPeer newPeer = SharedNetworkPeer(receivedPeer); _icePeers[receivedPeer->getUUID()] = newPeer; - + // make sure we know when we should ping this peer connect(newPeer.data(), &NetworkPeer::pingTimerTimeout, this, &DomainGatekeeper::handlePeerPingTimeout); - + // immediately ping the new peer, and start a timer to continue pinging it until we connect to it newPeer->startPingTimer(); - + qDebug() << "Sending ping packets to establish connectivity with ICE peer with ID" << newPeer->getUUID(); - + pingPunchForConnectingPeer(newPeer); } else { delete receivedPeer; @@ -640,18 +549,18 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer message) { auto limitedNodeList = DependencyManager::get(); auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*message, limitedNodeList->getSessionUUID()); - + limitedNodeList->sendPacket(std::move(pingReplyPacket), message->getSenderSockAddr()); } void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer message) { QDataStream packetStream(message->getMessage()); - + QUuid nodeUUID; packetStream >> nodeUUID; - + SharedNetworkPeer sendingPeer = _icePeers.value(nodeUUID); - + if (sendingPeer) { // we had this NetworkPeer in our connecting list - add the right sock addr to our connected list sendingPeer->activateMatchingOrNewSymmetricSocket(message->getSenderSockAddr()); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 09e3b04ed7..352f84385e 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -66,11 +66,7 @@ private: bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr); - bool isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature, - bool& verifiedUsername, - const HifiSockAddr& senderSockAddr); + bool isWithinMaxCapacity(); bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index f6fbb3f470..ea3d953c63 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1084,8 +1084,7 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings const QString RESTRICTED_ACCESS_FLAG = "restricted"; - domainObject[RESTRICTED_ACCESS_FLAG] = - _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); + domainObject[RESTRICTED_ACCESS_FLAG] = _settingsManager.getAllNames().length() > 0; // figure out the breakdown of currently connected interface clients int numConnectedUnassigned = 0; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 0d6214d702..df299734cf 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -110,6 +110,8 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // This was prior to the introduction of security.restricted_access // If the user has a list of allowed users then set their value for security.restricted_access to true + const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users"; + const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access"; QVariant* allowedUsers = valueForKeyPath(_configMap.getMergedConfig(), ALLOWED_USERS_SETTINGS_KEYPATH); if (allowedUsers @@ -207,10 +209,47 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } void DomainServerSettingsManager::unpackPermissions() { - QList permsHashList = valueOrDefaultValueForKeyPath(AGENT_PERMISSIONS_KEYPATH).toList(); - foreach (QVariant permsHash, permsHashList) { + bool foundLocalhost = false; + bool foundAnonymous = false; + bool foundLoggedIn = false; + + // XXX check for duplicate IDs + + QVariant* permissions = valueForKeyPath(_configMap.getMergedConfig(), AGENT_PERMISSIONS_KEYPATH); + if (!permissions->canConvert(QMetaType::QVariantList)) { + qDebug() << "failed to extract permissions from settings."; + return; + } + + // QList permissionsList = permissions->toList(); + + QVariantList* permissionsList = reinterpret_cast(permissions); + + foreach (QVariant permsHash, *permissionsList) { AgentPermissionsPointer perms { new AgentPermissions(permsHash.toMap()) }; - _agentPermissions[perms->getID()] = perms; + QString id = perms->getID(); + foundLoggedIn |= (id == "localhost"); + foundAnonymous |= (id == "anonymous"); + foundLoggedIn |= (id == "logged-in"); + _agentPermissions[id] = perms; + } + + // if any of the standard names are missing, add them + if (!foundLocalhost) { + AgentPermissionsPointer perms { new AgentPermissions("localhost") }; + perms->setAll(true); + _agentPermissions["localhost"] = perms; + *permissionsList += perms->toVariant(); + } + if (!foundAnonymous) { + AgentPermissionsPointer perms { new AgentPermissions("anonymous") }; + _agentPermissions["anonymous"] = perms; + *permissionsList += perms->toVariant(); + } + if (!foundLoggedIn) { + AgentPermissionsPointer perms { new AgentPermissions("logged-in") }; + _agentPermissions["logged-in"] = perms; + *permissionsList += perms->toVariant(); } #ifdef WANT_DEBUG @@ -229,11 +268,13 @@ void DomainServerSettingsManager::unpackPermissions() { #endif } -AgentPermissionsPointer DomainServerSettingsManager::getPermissionsForName(QString name) const { +AgentPermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { if (_agentPermissions.contains(name)) { - return _agentPermissions[name]; + return *(_agentPermissions[name].get()); } - return nullptr; + AgentPermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; } QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 4d305b2b5b..3012e06c4d 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -25,9 +25,6 @@ const QString SETTINGS_PATHS_KEY = "paths"; const QString SETTINGS_PATH = "/settings"; const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; - -const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users"; -const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; class DomainServerSettingsManager : public QObject { @@ -43,7 +40,8 @@ public: QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } - AgentPermissionsPointer getPermissionsForName(QString name) const; + AgentPermissions getPermissionsForName(const QString& name) const; + QStringList getAllNames() { return _agentPermissions.keys(); } private slots: void processSettingsRequestPacket(QSharedPointer message); diff --git a/libraries/networking/src/AgentPermissions.cpp b/libraries/networking/src/AgentPermissions.cpp new file mode 100644 index 0000000000..0d3483975b --- /dev/null +++ b/libraries/networking/src/AgentPermissions.cpp @@ -0,0 +1,43 @@ +// +// AgentPermissions.cpp +// libraries/networking/src/ +// +// Created by Seth Alves on 2016-6-1. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include "AgentPermissions.h" + +AgentPermissions& AgentPermissions::operator|=(const AgentPermissions& rhs) { + this->canConnectToDomain |= rhs.canConnectToDomain; + this->canAdjustLocks |= rhs.canAdjustLocks; + this->canRezPermanentEntities |= rhs.canRezPermanentEntities; + this->canRezTemporaryEntities |= rhs.canRezTemporaryEntities; + this->canWriteToAssetServer |= rhs.canWriteToAssetServer; + this->canConnectPastMaxCapacity |= rhs.canConnectPastMaxCapacity; + return *this; +} + +QDataStream& operator<<(QDataStream& out, const AgentPermissions& perms) { + out << perms.canConnectToDomain; + out << perms.canAdjustLocks; + out << perms.canRezPermanentEntities; + out << perms.canRezTemporaryEntities; + out << perms.canWriteToAssetServer; + out << perms.canConnectPastMaxCapacity; + return out; +} + +QDataStream& operator>>(QDataStream& in, AgentPermissions& perms) { + in >> perms.canConnectToDomain; + in >> perms.canAdjustLocks; + in >> perms.canRezPermanentEntities; + in >> perms.canRezTemporaryEntities; + in >> perms.canWriteToAssetServer; + in >> perms.canConnectPastMaxCapacity; + return in; +} diff --git a/libraries/networking/src/AgentPermissions.h b/libraries/networking/src/AgentPermissions.h new file mode 100644 index 0000000000..d6598b8cbf --- /dev/null +++ b/libraries/networking/src/AgentPermissions.h @@ -0,0 +1,79 @@ +// +// AgentPermissions.h +// libraries/networking/src/ +// +// Created by Seth Alves on 2016-6-1. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AgentPermissions_h +#define hifi_AgentPermissions_h + +#include +#include +#include +#include +#include + +class AgentPermissions; +using AgentPermissionsPointer = std::shared_ptr; + +class AgentPermissions { +public: + AgentPermissions() { _id = QUuid::createUuid().toString(); } + AgentPermissions(const QString& name) { _id = name; } + AgentPermissions(QMap perms) { + _id = perms["permissions_id"].toString(); + canConnectToDomain = perms["id_can_connect"].toBool(); + canAdjustLocks = perms["id_can_adjust_locks"].toBool(); + canRezPermanentEntities = perms["id_can_rez"].toBool(); + canRezTemporaryEntities = perms["id_can_rez_tmp"].toBool(); + canWriteToAssetServer = perms["id_can_write_to_asset_server"].toBool(); + canConnectPastMaxCapacity = perms["id_can_connect_past_max_capacity"].toBool(); + } + + QString getID() { return _id; } + + // the initializations here should match the defaults in describe-settings.json + bool canConnectToDomain { true }; + bool canAdjustLocks { false }; + bool canRezPermanentEntities { false }; + bool canRezTemporaryEntities { false }; + bool canWriteToAssetServer { false }; + bool canConnectPastMaxCapacity { false }; + + void setAll(bool value) { + canConnectToDomain = value; + canAdjustLocks = value; + canRezPermanentEntities = value; + canRezTemporaryEntities = value; + canWriteToAssetServer = value; + canConnectPastMaxCapacity = value; + } + + QVariant toVariant() { + QMap values; + values["permissions_id"] = _id; + values["id_can_connect"] = canConnectToDomain; + values["id_can_adjust_locks"] = canAdjustLocks; + values["id_can_rez"] = canRezPermanentEntities; + values["id_can_rez_tmp"] = canRezTemporaryEntities; + values["id_can_write_to_asset_server"] = canWriteToAssetServer; + values["id_can_connect_past_max_capacity"] = canConnectPastMaxCapacity; + return QVariant(values); + } + + AgentPermissions& operator|=(const AgentPermissions& rhs); + friend QDataStream& operator<<(QDataStream& out, const AgentPermissions& perms); + friend QDataStream& operator>>(QDataStream& in, AgentPermissions& perms); + +protected: + QString _id; +}; + +const AgentPermissions DEFAULT_AGENT_PERMISSIONS; + +#endif // hifi_AgentPermissions_h diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 9efe51183e..274d022ab5 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -52,7 +52,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short _numCollectedPackets(0), _numCollectedBytes(0), _packetStatTimer(), - _thisNodeCanRez(true) + _permissions(AgentPermissions()) { static bool firstCall = true; if (firstCall) { @@ -130,17 +130,25 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) { } } -void LimitedNodeList::setIsAllowedEditor(bool isAllowedEditor) { - if (_isAllowedEditor != isAllowedEditor) { - _isAllowedEditor = isAllowedEditor; - emit isAllowedEditorChanged(isAllowedEditor); - } -} -void LimitedNodeList::setThisNodeCanRez(bool canRez) { - if (_thisNodeCanRez != canRez) { - _thisNodeCanRez = canRez; - emit canRezChanged(canRez); +void LimitedNodeList::setPermissions(const AgentPermissions& newPermissions) { + bool emitIsAllowedEditorChanged { false }; + bool emitCanRezChanged { false }; + + if (_permissions.canAdjustLocks != newPermissions.canAdjustLocks) { + emitIsAllowedEditorChanged = true; + } + if (_permissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) { + emitCanRezChanged = true; + } + + _permissions = newPermissions; + + if (emitIsAllowedEditorChanged) { + emit isAllowedEditorChanged(_permissions.canAdjustLocks); + } + if (emitCanRezChanged) { + emit canRezChanged(_permissions.canRezPermanentEntities); } } @@ -515,7 +523,7 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) { SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - bool isAllowedEditor, bool canRez, + const AgentPermissions& permissions, const QUuid& connectionSecret) { NodeHash::const_iterator it = _nodeHash.find(uuid); @@ -524,14 +532,13 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t matchingNode->setPublicSocket(publicSocket); matchingNode->setLocalSocket(localSocket); - matchingNode->setIsAllowedEditor(isAllowedEditor); - matchingNode->setCanRez(canRez); + matchingNode->setPermissions(permissions); matchingNode->setConnectionSecret(connectionSecret); return matchingNode; } else { // we didn't have this node, so add them - Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, isAllowedEditor, canRez, connectionSecret, this); + Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret, this); if (nodeType == NodeType::AudioMixer) { LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 5a3c10e8c3..c3378a4cf8 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -104,12 +104,10 @@ public: const QUuid& getSessionUUID() const { return _sessionUUID; } void setSessionUUID(const QUuid& sessionUUID); - bool isAllowedEditor() const { return _isAllowedEditor; } - void setIsAllowedEditor(bool isAllowedEditor); + void setPermissions(const AgentPermissions& newPermissions); + bool isAllowedEditor() const { return _permissions.canAdjustLocks; } + bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; } - bool getThisNodeCanRez() const { return _thisNodeCanRez; } - void setThisNodeCanRez(bool canRez); - quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); } QUdpSocket& getDTLSSocket(); @@ -137,7 +135,7 @@ public: SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - bool isAllowedEditor = false, bool canRez = false, + const AgentPermissions& permissions = DEFAULT_AGENT_PERMISSIONS, const QUuid& connectionSecret = QUuid()); bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; } @@ -300,8 +298,7 @@ protected: int _numCollectedBytes; QElapsedTimer _packetStatTimer; - bool _isAllowedEditor { false }; - bool _thisNodeCanRez; + AgentPermissions _permissions; QPointer _initialSTUNTimer; diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 1e1cec2413..94f32a4e01 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -16,6 +16,7 @@ #include "Node.h" #include "SharedUtil.h" +#include "AgentPermissions.h" #include #include @@ -47,7 +48,7 @@ const QString& NodeType::getNodeTypeName(NodeType_t nodeType) { } Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, - const HifiSockAddr& localSocket, bool isAllowedEditor, bool canRez, const QUuid& connectionSecret, + const HifiSockAddr& localSocket, const AgentPermissions& permissions, const QUuid& connectionSecret, QObject* parent) : NetworkPeer(uuid, publicSocket, localSocket, parent), _type(type), @@ -57,8 +58,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, _clockSkewUsec(0), _mutex(), _clockSkewMovingPercentile(30, 0.8f), // moving 80th percentile of 30 samples - _isAllowedEditor(isAllowedEditor), - _canRez(canRez) + _permissions(permissions) { // Update socket's object name setType(_type); @@ -78,15 +78,12 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } - QDataStream& operator<<(QDataStream& out, const Node& node) { out << node._type; out << node._uuid; out << node._publicSocket; out << node._localSocket; - out << node._isAllowedEditor; - out << node._canRez; - + out << node._permissions; return out; } @@ -95,9 +92,7 @@ QDataStream& operator>>(QDataStream& in, Node& node) { in >> node._uuid; in >> node._publicSocket; in >> node._localSocket; - in >> node._isAllowedEditor; - in >> node._canRez; - + in >> node._permissions; return in; } diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 3927672319..abc3cfa181 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -27,13 +27,14 @@ #include "NodeType.h" #include "SimpleMovingAverage.h" #include "MovingPercentile.h" +#include "AgentPermissions.h" class Node : public NetworkPeer { Q_OBJECT public: Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - bool isAllowedEditor, bool canRez, const QUuid& connectionSecret = QUuid(), + const AgentPermissions& permissions, const QUuid& connectionSecret = QUuid(), QObject* parent = 0); bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; } @@ -58,11 +59,10 @@ public: void updateClockSkewUsec(qint64 clockSkewSample); QMutex& getMutex() { return _mutex; } - void setIsAllowedEditor(bool isAllowedEditor) { _isAllowedEditor = isAllowedEditor; } - bool isAllowedEditor() { return _isAllowedEditor; } - - void setCanRez(bool canRez) { _canRez = canRez; } - bool getCanRez() { return _canRez; } + void setPermissions(const AgentPermissions& newPermissions) { _permissions = newPermissions; } + AgentPermissions getPermissions() const { return _permissions; } + bool isAllowedEditor() const { return _permissions.canAdjustLocks; } + bool getCanRez() const { return _permissions.canRezPermanentEntities; } friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node); @@ -81,8 +81,7 @@ private: qint64 _clockSkewUsec; QMutex _mutex; MovingPercentile _clockSkewMovingPercentile; - bool _isAllowedEditor; - bool _canRez; + AgentPermissions _permissions; }; Q_DECLARE_METATYPE(Node*) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 16a4083b08..b9f6da8c00 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -527,7 +527,7 @@ void NodeList::processDomainServerList(QSharedPointer message) DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); QDataStream packetStream(message->getMessage()); - + // grab the domain's ID from the beginning of the packet QUuid domainUUID; packetStream >> domainUUID; @@ -543,14 +543,9 @@ void NodeList::processDomainServerList(QSharedPointer message) packetStream >> newUUID; setSessionUUID(newUUID); - quint8 isAllowedEditor; - packetStream >> isAllowedEditor; - setIsAllowedEditor((bool) isAllowedEditor); + // pull the permissions/right/privileges for this node out of the stream + packetStream >> _permissions; - quint8 thisNodeCanRez; - packetStream >> thisNodeCanRez; - setThisNodeCanRez((bool) thisNodeCanRez); - // pull each node in the packet while (packetStream.device()->pos() < message->getSize()) { parseNodeFromPacketStream(packetStream); @@ -577,10 +572,9 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { qint8 nodeType; QUuid nodeUUID, connectionUUID; HifiSockAddr nodePublicSocket, nodeLocalSocket; - bool isAllowedEditor; - bool canRez; + AgentPermissions permissions; - packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> isAllowedEditor >> canRez; + packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions; // if the public socket address is 0 then it's reachable at the same IP // as the domain server @@ -591,8 +585,7 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { packetStream >> connectionUUID; SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket, - nodeLocalSocket, isAllowedEditor, canRez, - connectionUUID); + nodeLocalSocket, permissions, connectionUUID); } void NodeList::sendAssignment(Assignment& assignment) { From 83cba2dd82cbd3d4abcea7fcd6b328727e6bd762 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 2 Jun 2016 13:28:36 -0700 Subject: [PATCH 0296/1237] start on code to convert from settings version 1.2 to 1.3 --- domain-server/src/DomainGatekeeper.cpp | 2 -- .../src/DomainServerSettingsManager.cpp | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index c80969c8b4..cc631abfb7 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -173,8 +173,6 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo } const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; -// const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors"; -// const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index df299734cf..aa1297c3de 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -101,6 +101,11 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList double oldVersion = appSettings.value(JSON_SETTINGS_VERSION_KEY, 0.0).toDouble(); if (oldVersion != _descriptionVersion) { + const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users"; + const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access"; + const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors"; + const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers"; + qDebug() << "Previous domain-server settings version was" << QString::number(oldVersion, 'g', 8) << "and the new version is" << QString::number(_descriptionVersion, 'g', 8) << "- checking if any re-mapping is required"; @@ -110,8 +115,6 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // This was prior to the introduction of security.restricted_access // If the user has a list of allowed users then set their value for security.restricted_access to true - const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users"; - const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access"; QVariant* allowedUsers = valueForKeyPath(_configMap.getMergedConfig(), ALLOWED_USERS_SETTINGS_KEYPATH); if (allowedUsers @@ -198,7 +201,17 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList if (oldVersion < 1.3) { // This was prior to the permissions-grid in the domain-server settings page + // bool isRestrictingAccess = valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); + // const QVariant* allowedEditorsVariant = valueForKeyPath(getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); + + // const QVariant* editorsAreRezzersVariant = valueForKeyPath(getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH); + // bool onlyEditorsAreRezzers = false; + // if (editorsAreRezzersVariant) { + // onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool(); + // } + + // XXX } } From d010dc9699179c33f7b80a0dca2e75675633c37c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 2 Jun 2016 15:08:16 -0700 Subject: [PATCH 0297/1237] keep localhost line from being added every save. added some debugging print stuff --- domain-server/src/DomainGatekeeper.cpp | 4 +++ .../src/DomainServerSettingsManager.cpp | 2 +- libraries/networking/src/AgentPermissions.cpp | 25 +++++++++++++++++++ libraries/networking/src/AgentPermissions.h | 4 ++- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index cc631abfb7..5503bbb5b0 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -169,6 +169,10 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.canAdjustLocks = true; userPerms.canRezPermanentEntities = true; newNode->setPermissions(userPerms); + + qDebug() << "----------------------------"; + qDebug() << "AC perms are" << userPerms; + return newNode; } diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index aa1297c3de..de35188f54 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -241,7 +241,7 @@ void DomainServerSettingsManager::unpackPermissions() { foreach (QVariant permsHash, *permissionsList) { AgentPermissionsPointer perms { new AgentPermissions(permsHash.toMap()) }; QString id = perms->getID(); - foundLoggedIn |= (id == "localhost"); + foundLocalhost |= (id == "localhost"); foundAnonymous |= (id == "anonymous"); foundLoggedIn |= (id == "logged-in"); _agentPermissions[id] = perms; diff --git a/libraries/networking/src/AgentPermissions.cpp b/libraries/networking/src/AgentPermissions.cpp index 0d3483975b..a2b46f49d5 100644 --- a/libraries/networking/src/AgentPermissions.cpp +++ b/libraries/networking/src/AgentPermissions.cpp @@ -10,6 +10,7 @@ // #include +#include #include "AgentPermissions.h" AgentPermissions& AgentPermissions::operator|=(const AgentPermissions& rhs) { @@ -41,3 +42,27 @@ QDataStream& operator>>(QDataStream& in, AgentPermissions& perms) { in >> perms.canConnectPastMaxCapacity; return in; } + +QDebug operator<<(QDebug debug, const AgentPermissions& perms) { + debug.nospace() << "[permissions: " << perms.getID() << " --"; + if (perms.canConnectToDomain) { + debug << " connect"; + } + if (perms.canAdjustLocks) { + debug << " locks"; + } + if (perms.canRezPermanentEntities) { + debug << " rez"; + } + if (perms.canRezTemporaryEntities) { + debug << " rez-tmp"; + } + if (perms.canWriteToAssetServer) { + debug << " asset-server"; + } + if (perms.canConnectPastMaxCapacity) { + debug << " ignore-max-cap"; + } + debug.nospace() << "]"; + return debug.nospace(); +} diff --git a/libraries/networking/src/AgentPermissions.h b/libraries/networking/src/AgentPermissions.h index d6598b8cbf..4ad381959a 100644 --- a/libraries/networking/src/AgentPermissions.h +++ b/libraries/networking/src/AgentPermissions.h @@ -35,7 +35,7 @@ public: canConnectPastMaxCapacity = perms["id_can_connect_past_max_capacity"].toBool(); } - QString getID() { return _id; } + QString getID() const { return _id; } // the initializations here should match the defaults in describe-settings.json bool canConnectToDomain { true }; @@ -76,4 +76,6 @@ protected: const AgentPermissions DEFAULT_AGENT_PERMISSIONS; +QDebug operator<<(QDebug debug, const AgentPermissions& node); + #endif // hifi_AgentPermissions_h From b36b1491af7d55b027cab5cf9bd7782416af3d73 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 3 Jun 2016 10:11:13 +1200 Subject: [PATCH 0298/1237] Restrict marking table rows dirty to table row switch events --- domain-server/resources/web/settings/js/settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 35e9d64d84..6f6e1c9bd5 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -234,7 +234,7 @@ $(document).ready(function(){ // Bootstrap switch in table - $('#' + Settings.FORM_ID).on('switchChange.bootstrapSwitch', 'input.toggle-checkbox', function () { + $('#' + Settings.FORM_ID).on('switchChange.bootstrapSwitch', 'input.table-checkbox', function () { // Bootstrap switches in table: set the changed data attribute for all rows. var row = $(this).closest('tr'); row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); @@ -976,7 +976,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" html += " Date: Fri, 3 Jun 2016 10:27:38 +1200 Subject: [PATCH 0299/1237] Make editibility of switch columns be configurable --- domain-server/resources/describe-settings.json | 6 ++++++ domain-server/resources/web/settings/js/settings.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index b82a3ae1b1..6ddc75bcef 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -110,36 +110,42 @@ "name": "id_can_connect", "label": "Connect", "type": "checkbox", + "editable": true, "default": true }, { "name": "id_can_adjust_locks", "label": "Lock/Unlock", "type": "checkbox", + "editable": true, "default": false }, { "name": "id_can_rez", "label": "Rez", "type": "checkbox", + "editable": true, "default": false }, { "name": "id_can_rez_tmp", "label": "Rez Temp", "type": "checkbox", + "editable": true, "default": false }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", "type": "checkbox", + "editable": true, "default": false }, { "name": "id_can_connect_past_max_capacity", "label": "Ignore Max Capacity", "type": "checkbox", + "editable": true, "default": false } ] diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 6f6e1c9bd5..0e34f1af24 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -972,7 +972,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { colName = keypath + "." + rowIndexOrName + "." + col.name; } - if (isArray && col.type === "checkbox") { + if (isArray && col.type === "checkbox" && col.editable) { html += "" html += " Date: Wed, 25 May 2016 12:16:39 -0700 Subject: [PATCH 0300/1237] Compact Domain heartbeat JSON --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 052a7c0fec..231e9ba014 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1121,7 +1121,7 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; - QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); + QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), AccountManagerAuth::Required, From 87e27d9570b9419802ddbed6f664d42baf912db8 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 25 May 2016 12:16:02 -0700 Subject: [PATCH 0301/1237] Factor out metadata generation from heartbeat --- domain-server/src/DomainServer.cpp | 79 ++++++++++++++++-------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 231e9ba014..ae3c26a34d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1067,62 +1067,69 @@ void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) sendHeartbeatToMetaverse(newPublicSockAddr.getAddress().toString()); } - -void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { - const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - - auto nodeList = DependencyManager::get(); - const QUuid& domainID = nodeList->getSessionUUID(); - - // setup the domain object to send to the data server - const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; - const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; - - QJsonObject domainObject; - if (!networkAddress.isEmpty()) { - domainObject[PUBLIC_NETWORK_ADDRESS_KEY] = networkAddress; - } - - domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; - - // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings - const QString RESTRICTED_ACCESS_FLAG = "restricted"; - - domainObject[RESTRICTED_ACCESS_FLAG] = - _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); - - // figure out the breakdown of currently connected interface clients - int numConnectedUnassigned = 0; - QJsonObject userHostnames; - +QVariantMap getMetadata() { static const QString DEFAULT_HOSTNAME = "*"; + auto nodeList = DependencyManager::get(); + int numConnectedUnassigned = 0; + QVariantMap userHostnames; + + // figure out the breakdown of currently connected interface clients nodeList->eachNode([&numConnectedUnassigned, &userHostnames](const SharedNodePointer& node) { - if (node->getLinkedData()) { - auto nodeData = static_cast(node->getLinkedData()); + auto linkedData = node->getLinkedData(); + if (linkedData) { + auto nodeData = static_cast(linkedData); if (!nodeData->wasAssigned()) { ++numConnectedUnassigned; // increment the count for this hostname (or the default if we don't have one) - auto hostname = nodeData->getPlaceName().isEmpty() ? DEFAULT_HOSTNAME : nodeData->getPlaceName(); + auto placeName = nodeData->getPlaceName(); + auto hostname = placeName.isEmpty() ? DEFAULT_HOSTNAME : placeName; userHostnames[hostname] = userHostnames[hostname].toInt() + 1; } } }); - static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + QVariantMap metadata; + static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; + metadata[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; + static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; + metadata[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; - QJsonObject heartbeatObject; - heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; - heartbeatObject[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; + return metadata; +} - domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; +void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { + auto nodeList = DependencyManager::get(); + const QUuid& domainID = nodeList->getSessionUUID(); + + // Setup the domain object to send to the data server + QJsonObject domainObject; + + if (!networkAddress.isEmpty()) { + static const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; + domainObject[PUBLIC_NETWORK_ADDRESS_KEY] = networkAddress; + } + + static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; + domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; + + // Add a flag to indicate if this domain uses restricted access - + // for now that will exclude it from listings + static const QString RESTRICTED_ACCESS_FLAG = "restricted"; + domainObject[RESTRICTED_ACCESS_FLAG] = + _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); + + // Add the metadata to the heartbeat + static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + domainObject[DOMAIN_HEARTBEAT_KEY] = QJsonObject::fromVariantMap(getMetadata()); QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); + static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, From 18696144f1c29e383d0bd2056395c6f8573fa19a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 25 May 2016 12:23:47 -0700 Subject: [PATCH 0302/1237] Move metadata generation to DomainMetadata --- domain-server/src/DomainMetadata.cpp | 51 ++++++++++++++++++++++++++++ domain-server/src/DomainMetadata.h | 21 ++++++++++++ domain-server/src/DomainServer.cpp | 35 ------------------- domain-server/src/DomainServer.h | 1 + 4 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 domain-server/src/DomainMetadata.cpp create mode 100644 domain-server/src/DomainMetadata.h diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp new file mode 100644 index 0000000000..f584198b51 --- /dev/null +++ b/domain-server/src/DomainMetadata.cpp @@ -0,0 +1,51 @@ +// +// DomainMetadata.cpp +// domain-server/src +// +// Created by Zach Pomerantz on 5/25/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include "DomainMetadata.h" + +#include +#include + +#include "DomainServerNodeData.h" + +QVariantMap getMetadata() { + static const QString DEFAULT_HOSTNAME = "*"; + + auto nodeList = DependencyManager::get(); + int numConnectedUnassigned = 0; + QVariantMap userHostnames; + + // figure out the breakdown of currently connected interface clients + nodeList->eachNode([&numConnectedUnassigned, &userHostnames](const SharedNodePointer& node) { + auto linkedData = node->getLinkedData(); + if (linkedData) { + auto nodeData = static_cast(linkedData); + + if (!nodeData->wasAssigned()) { + ++numConnectedUnassigned; + + // increment the count for this hostname (or the default if we don't have one) + auto placeName = nodeData->getPlaceName(); + auto hostname = placeName.isEmpty() ? DEFAULT_HOSTNAME : placeName; + userHostnames[hostname] = userHostnames[hostname].toInt() + 1; + } + } + }); + + QVariantMap metadata; + + static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; + metadata[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; + + static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; + metadata[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; + + return metadata; +} diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h new file mode 100644 index 0000000000..3cd160ce85 --- /dev/null +++ b/domain-server/src/DomainMetadata.h @@ -0,0 +1,21 @@ +// +// DomainMetadata.h +// domain-server/src +// +// Created by Zach Pomerantz on 5/25/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#ifndef hifi_DomainMetadata_h +#define hifi_DomainMetadata_h + +#include + +QVariantMap getMetadata(); + +// TODO: Encapsulate +class DomainMetadata { }; + +#endif // hifi_DomainMetadata_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index ae3c26a34d..f05dfd2c6d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1067,41 +1067,6 @@ void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) sendHeartbeatToMetaverse(newPublicSockAddr.getAddress().toString()); } -QVariantMap getMetadata() { - static const QString DEFAULT_HOSTNAME = "*"; - - auto nodeList = DependencyManager::get(); - int numConnectedUnassigned = 0; - QVariantMap userHostnames; - - // figure out the breakdown of currently connected interface clients - nodeList->eachNode([&numConnectedUnassigned, &userHostnames](const SharedNodePointer& node) { - auto linkedData = node->getLinkedData(); - if (linkedData) { - auto nodeData = static_cast(linkedData); - - if (!nodeData->wasAssigned()) { - ++numConnectedUnassigned; - - // increment the count for this hostname (or the default if we don't have one) - auto placeName = nodeData->getPlaceName(); - auto hostname = placeName.isEmpty() ? DEFAULT_HOSTNAME : placeName; - userHostnames[hostname] = userHostnames[hostname].toInt() + 1; - } - } - }); - - QVariantMap metadata; - - static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; - metadata[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; - - static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; - metadata[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; - - return metadata; -} - void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { auto nodeList = DependencyManager::get(); const QUuid& domainID = nodeList->getSessionUUID(); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index c39e405380..0f93c50468 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -26,6 +26,7 @@ #include #include "DomainGatekeeper.h" +#include "DomainMetadata.h" #include "DomainServerSettingsManager.h" #include "DomainServerWebSessionData.h" #include "WalletTransaction.h" From fd77f8401cda9912b15751090faebd57805aaf0c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 3 Jun 2016 10:40:32 +1200 Subject: [PATCH 0303/1237] Style new code like old --- .../resources/web/settings/js/settings.js | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 0e34f1af24..29bb430893 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -973,27 +973,13 @@ function makeTable(setting, keypath, setting_value, isLocked) { } if (isArray && col.type === "checkbox" && col.editable) { - html += "" - html += ""; } else { - - // setup the td for this column - html += ""; - - // add the actual value to the td so it is displayed - html += colValue; - - // for values to be posted properly we add a hidden input to this td - html += ""; - - html += ""; + // Use a hidden input so that the values are posted. + html += "" + + colValue + ""; } }) @@ -1039,12 +1025,8 @@ function makeTableInputs(setting) { _.each(setting.columns, function(col) { if (col.type === "checkbox") { html += "" - html += ""; } else { html += "\ Date: Wed, 25 May 2016 12:35:52 -0700 Subject: [PATCH 0304/1237] Encapsulate metadata in DomainMetadata --- domain-server/src/DomainMetadata.cpp | 12 ++++++------ domain-server/src/DomainMetadata.h | 15 ++++++++++++--- domain-server/src/DomainServer.cpp | 2 +- domain-server/src/DomainServer.h | 2 ++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index f584198b51..224fe1939b 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -15,7 +15,7 @@ #include "DomainServerNodeData.h" -QVariantMap getMetadata() { +void DomainMetadata::generate() { static const QString DEFAULT_HOSTNAME = "*"; auto nodeList = DependencyManager::get(); @@ -39,13 +39,13 @@ QVariantMap getMetadata() { } }); - QVariantMap metadata; - static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; - metadata[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; + _metadata[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; - metadata[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; + _metadata[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; - return metadata; +#if DEV_BUILD + qDebug() << "Regenerated domain metadata - users:" << _metadata; +#endif } diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index 3cd160ce85..5096f1bb3e 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -12,10 +12,19 @@ #define hifi_DomainMetadata_h #include +#include -QVariantMap getMetadata(); +class DomainMetadata { +public: + QVariantMap toVariantMap() { generate(); return _metadata; } + QJsonObject toJSON() { generate(); return QJsonObject::fromVariantMap(_metadata); } -// TODO: Encapsulate -class DomainMetadata { }; +protected slots: + // TODO: Connect appropriate signals to obviate JIT generation + void generate(); + +protected: + QVariantMap _metadata; +}; #endif // hifi_DomainMetadata_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index f05dfd2c6d..9b1fbb1d84 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1090,7 +1090,7 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // Add the metadata to the heartbeat static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; - domainObject[DOMAIN_HEARTBEAT_KEY] = QJsonObject::fromVariantMap(getMetadata()); + domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.toJSON(); QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 0f93c50468..0a1df41f50 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -168,6 +168,8 @@ private: DomainServerSettingsManager _settingsManager; + DomainMetadata _metadata; + HifiSockAddr _iceServerSocket; std::unique_ptr _iceServerHeartbeatPacket; From 079f6af2cc3c07d881c23d01c2da32f2eb6866b0 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 2 Jun 2016 16:14:46 -0700 Subject: [PATCH 0305/1237] Fixing the gamma correction for the debug view of occlusion --- libraries/render-utils/src/DebugDeferredBuffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 6dfec30b16..dc46b5d897 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -83,7 +83,7 @@ static const std::string DEFAULT_NORMAL_SHADER { static const std::string DEFAULT_OCCLUSION_SHADER{ "vec4 getFragmentColor() {" " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" - " return vec4(vec3(frag.obscurance), 1.0);" + " return vec4(vec3(pow(frag.obscurance, 1.0 / 2.2)), 1.0);" " }" }; From 681da201fc7dfcec6f9649be1b11972ccab97657 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 2 Jun 2016 16:25:14 -0700 Subject: [PATCH 0306/1237] COrrect the name of the Unlit dbug view --- scripts/developer/utilities/render/framebuffer.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index 0d8d85cc32..1612f59276 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -33,7 +33,7 @@ Column { "Roughness", "Metallic", "Emissive", - "Shaded/Lightmapped/Unlit", + "Unlit", "Occlusion", "Lightmap", "Lighting", From cc85ce9c01c4bb3c07ee6d30167011771e8f3232 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 21 May 2016 23:47:37 -0700 Subject: [PATCH 0307/1237] fix crash on click on non-visible web-entity --- .../entities-renderer/src/RenderableWebEntityItem.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index f0244d0e3f..8298dbcec5 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -226,10 +226,15 @@ void RenderableWebEntityItem::setSourceUrl(const QString& value) { } void RenderableWebEntityItem::setProxyWindow(QWindow* proxyWindow) { - _webSurface->setProxyWindow(proxyWindow); + if (_webSurface) { + _webSurface->setProxyWindow(proxyWindow); + } } QObject* RenderableWebEntityItem::getEventHandler() { + if (!_webSurface) { + return nullptr; + } return _webSurface->getEventHandler(); } From bf734395e5db8a3eba70658d22e089725e5eed2f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 27 May 2016 12:14:33 -0700 Subject: [PATCH 0308/1237] More invisible web entity fixes --- interface/src/Application.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 48b418b93c..065eea14a8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -994,7 +994,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : if (_keyboardFocusedItem != entityItemID) { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; auto properties = entityScriptingInterface->getEntityProperties(entityItemID); - if (EntityTypes::Web == properties.getType() && !properties.getLocked()) { + if (EntityTypes::Web == properties.getType() && !properties.getLocked() && properties.getVisible()) { auto entity = entityScriptingInterface->getEntityTree()->findEntityByID(entityItemID); RenderableWebEntityItem* webEntity = dynamic_cast(entity.get()); if (webEntity) { @@ -1049,6 +1049,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } }); + connect(this, &Application::aboutToQuit, [=]() { + _keyboardFocusedItem = UNKNOWN_ENTITY_ID; + if (_keyboardFocusHighlight) { + _keyboardFocusHighlight->setVisible(false); + } + }); + // Make sure we don't time out during slow operations at startup updateHeartbeat(); From 3ca158265a23f4a5b26fc19278c436b3a5465e23 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 2 Jun 2016 16:44:13 -0700 Subject: [PATCH 0309/1237] Make sure getVelocityChange returns the velocityChange --- libraries/physics/src/CharacterController.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 1c8a9b50b5..30fb3536ca 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -401,11 +401,10 @@ glm::vec3 CharacterController::getLinearVelocity() const { } glm::vec3 CharacterController::getVelocityChange() const { - glm::vec3 velocity(0.0f); if (_rigidBody) { - velocity = bulletToGLM(_rigidBody->getLinearVelocity()); + return bulletToGLM(_velocityChange); } - return velocity; + return glm::vec3(0.0f); } void CharacterController::clearMotors() { From 8b76af531bf326a0e707ce4760f4600218611784 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 16:52:17 -0700 Subject: [PATCH 0310/1237] made touch hand orientation more comfortable, fixed all the touch key mappings, exposed capacitive touch keys for use later --- .../resources/controllers/oculus_touch.json | 36 ++++++-- .../oculus/src/OculusControllerManager.cpp | 86 +++++++++++++++---- 2 files changed, 98 insertions(+), 24 deletions(-) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index cc8c2f8bdc..8cb512a526 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -1,22 +1,42 @@ { "name": "Oculus Touch to Standard", "channels": [ - { "from": "OculusTouch.LY", "filters": "invert", "to": "Standard.LY" }, - { "from": "OculusTouch.LX", "to": "Standard.LX" }, + { "from": "OculusTouch.A", "to": "Standard.A" }, + { "from": "OculusTouch.B", "to": "Standard.B" }, + { "from": "OculusTouch.X", "to": "Standard.X" }, + { "from": "OculusTouch.Y", "to": "Standard.Y" }, + + { "from": "OculusTouch.LY", "filters": "invert", "to": "Standard.LY" }, + { "from": "OculusTouch.LX", "to": "Standard.LX" }, { "from": "OculusTouch.LT", "to": "Standard.LT" }, + { "from": "OculusTouch.LS", "to": "Standard.LS" }, + { "from": "OculusTouch.LG", "to": "Standard.LG" }, + { "from": "OculusTouch.LeftGrip", "to": "Standard.LB" }, + { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, - { "from": "OculusTouch.RY", "filters": "invert", "to": "Standard.RY" }, - { "from": "OculusTouch.RX", "to": "Standard.RX" }, - + { "from": "OculusTouch.RY", "filters": "invert", "to": "Standard.RY" }, + { "from": "OculusTouch.RX", "to": "Standard.RX" }, { "from": "OculusTouch.RT", "to": "Standard.RT" }, - { "from": "OculusTouch.RB", "to": "Standard.RB" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, + { "from": "OculusTouch.RG", "to": "Standard.RG" }, + { "from": "OculusTouch.RightGrip", "to": "Standard.RB" }, + { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, { "from": "OculusTouch.RightApplicationMenu", "to": "Standard.Start" }, - { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, - { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" } + { "from": "OculusTouch.LeftPrimaryThumbTouch", "to": "Standard.LeftPrimaryThumbTouch" }, + { "from": "OculusTouch.LeftSecondaryThumbTouch", "to": "Standard.LeftSecondaryThumbTouch" }, + { "from": "OculusTouch.RightPrimaryThumbTouch", "to": "Standard.RightPrimaryThumbTouch" }, + { "from": "OculusTouch.RightSecondaryThumbTouch", "to": "Standard.RightSecondaryThumbTouch" }, + { "from": "OculusTouch.LeftPrimaryIndexTouch", "to": "Standard.LeftPrimaryIndexTouch" }, + { "from": "OculusTouch.RightPrimaryIndexTouch", "to": "Standard.RightPrimaryIndexTouch" }, + { "from": "OculusTouch.LSTouch", "to": "Standard.LSTouch" }, + { "from": "OculusTouch.RSTouch", "to": "Standard.RSTouch" }, + { "from": "OculusTouch.LeftThumbUp", "to": "Standard.LeftThumbUp" }, + { "from": "OculusTouch.RightThumbUp", "to": "Standard.RightThumbUp" }, + { "from": "OculusTouch.LeftIndexPoint", "to": "Standard.LeftIndexPoint" }, + { "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" } ] } diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 2fd48bb611..e84fdcbfc7 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -120,14 +120,14 @@ static const std::vector> BUTTON_MAP { ovrButton_B, B }, { ovrButton_LThumb, LS }, { ovrButton_RThumb, RS }, - { ovrButton_LShoulder, LB }, - { ovrButton_RShoulder, RB }, + //{ ovrButton_LShoulder, LB }, + //{ ovrButton_RShoulder, RB }, } }; static const std::vector> TOUCH_MAP { { - { ovrTouch_X, LEFT_SECONDARY_THUMB_TOUCH }, + { ovrTouch_X, LEFT_PRIMARY_THUMB_TOUCH }, { ovrTouch_Y, LEFT_SECONDARY_THUMB_TOUCH }, - { ovrTouch_A, RIGHT_SECONDARY_THUMB_TOUCH }, + { ovrTouch_A, RIGHT_PRIMARY_THUMB_TOUCH }, { ovrTouch_B, RIGHT_SECONDARY_THUMB_TOUCH }, { ovrTouch_LIndexTrigger, LEFT_PRIMARY_INDEX_TOUCH }, { ovrTouch_RIndexTrigger, RIGHT_PRIMARY_INDEX_TOUCH }, @@ -173,6 +173,11 @@ void OculusControllerManager::RemoteDevice::focusOutEvent() { } void OculusControllerManager::TouchDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + // Check past values of button map for hysteresis before clearing map + const float HYSTERESIS_OFFSET = -0.1f; + float LEFT_HYSTERESIS_OFFSET = _buttonPressedMap.find(LEFT_GRIP) != _buttonPressedMap.end() ? HYSTERESIS_OFFSET : 0.0f; + float RIGHT_HYSTERESIS_OFFSET = _buttonPressedMap.find(RIGHT_GRIP) != _buttonPressedMap.end() ? HYSTERESIS_OFFSET : 0.0f; + _poseStateMap.clear(); _buttonPressedMap.clear(); @@ -206,6 +211,16 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control _buttonPressedMap.insert(pair.second); } } + // Map pressed hand triggers to grip buttons + // This is temporary in order to support the grab/equip scripts + const float handTriggerThreshold = 0.9f; + if (inputState.HandTrigger[ovrHand_Left] >= handTriggerThreshold + LEFT_HYSTERESIS_OFFSET) { + _buttonPressedMap.insert(LEFT_GRIP); + } + if (inputState.HandTrigger[ovrHand_Right] >= handTriggerThreshold + RIGHT_HYSTERESIS_OFFSET) { + _buttonPressedMap.insert(RIGHT_GRIP); + } + // Touches for (const auto& pair : TOUCH_MAP) { if (inputState.Touches & pair.first) { @@ -261,7 +276,7 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, // // An approximate offset for the Touch can be obtained by inspection: // - // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) // // So the full equation is: // @@ -280,14 +295,26 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); - static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ) * touchToHand; - static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ) * touchToHand; + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 2.0f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); + glm::quat rotation = toGlm(handPose.ThePose.Orientation); + pose.translation = toGlm(handPose.ThePose.Position); - pose.rotation = toGlm(handPose.ThePose.Orientation)*rotationOffset; + pose.translation += rotation * translationOffset; + pose.rotation = rotation * rotationOffset; pose.angularVelocity = toGlm(handPose.AngularVelocity); pose.velocity = toGlm(handPose.LinearVelocity); pose.valid = true; @@ -300,27 +327,54 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, controller::Input::NamedVector OculusControllerManager::TouchDevice::getAvailableInputs() const { using namespace controller; QVector availableInputs{ - // Trackpad analogs + // buttons + makePair(A, "A"), + makePair(B, "B"), + makePair(X, "X"), + makePair(Y, "Y"), + + // trackpad analogs makePair(LX, "LX"), makePair(LY, "LY"), makePair(RX, "RX"), makePair(RY, "RY"), - // trigger analogs + + // triggers makePair(LT, "LT"), makePair(RT, "RT"), - makePair(LB, "LB"), - makePair(RB, "RB"), + // trigger buttons + //makePair(LB, "LB"), + //makePair(RB, "RB"), + // side grip triggers + makePair(LG, "LG"), + makePair(RG, "RG"), + makePair(LEFT_GRIP, "LeftGrip"), + makePair(RIGHT_GRIP, "RightGrip"), + + // joystick buttons makePair(LS, "LS"), makePair(RS, "RS"), + makePair(LEFT_HAND, "LeftHand"), makePair(RIGHT_HAND, "RightHand"), - makePair(LEFT_PRIMARY_THUMB, "LeftPrimaryThumb"), - makePair(LEFT_SECONDARY_THUMB, "LeftSecondaryThumb"), - makePair(RIGHT_PRIMARY_THUMB, "RightPrimaryThumb"), - makePair(RIGHT_SECONDARY_THUMB, "RightSecondaryThumb"), + makePair(LEFT_PRIMARY_THUMB_TOUCH, "LeftPrimaryThumbTouch"), + makePair(LEFT_SECONDARY_THUMB_TOUCH, "LeftSecondaryThumbTouch"), + makePair(RIGHT_PRIMARY_THUMB_TOUCH, "RightPrimaryThumbTouch"), + makePair(RIGHT_SECONDARY_THUMB_TOUCH, "RightSecondaryThumbTouch"), + makePair(LEFT_PRIMARY_INDEX_TOUCH, "LeftPrimaryIndexTouch"), + makePair(RIGHT_PRIMARY_INDEX_TOUCH, "RightPrimaryIndexTouch"), + makePair(LS_TOUCH, "LSTouch"), + makePair(RS_TOUCH, "RSTouch"), + makePair(LEFT_THUMB_UP, "LeftThumbUp"), + makePair(RIGHT_THUMB_UP, "RightThumbUp"), + makePair(LEFT_INDEX_POINT, "LeftIndexPoint"), + makePair(RIGHT_INDEX_POINT, "RightIndexPoint"), + + makePair(BACK, "LeftApplicationMenu"), + makePair(START, "RightApplicationMenu"), }; return availableInputs; } From 41a8e9fa919ce155642c633cdedfaabca3c9530d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 2 Jun 2016 16:53:52 -0700 Subject: [PATCH 0311/1237] missed a protocol change --- domain-server/src/DomainServer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 6467951bcf..5ad6862b62 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -789,8 +789,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif extendedHeaderStream << limitedNodeList->getSessionUUID(); extendedHeaderStream << node->getUUID(); - extendedHeaderStream << (quint8) node->isAllowedEditor(); - extendedHeaderStream << (quint8) node->getCanRez(); + extendedHeaderStream << node->getPermissions(); auto domainListPackets = NLPacketList::create(PacketType::DomainList, extendedHeader); From f6644ad2bb4692f75bcf8f78ed41c2b8aaf1e2c3 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 2 Jun 2016 16:58:48 -0700 Subject: [PATCH 0312/1237] change artemis url --- unpublishedScripts/DomainContent/Home/reset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index ffa26fb8b3..9b00bc647a 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -63,7 +63,7 @@ var TRANSFORMER_URL_WILL = 'atp:/dressingRoom/will_T.fbx'; - var TRANSFORMER_URL_STYLIZED_FEMALE = 'atp:/dressingRoom/stylized_female.fbx'; + var TRANSFORMER_URL_STYLIZED_FEMALE = 'atp:/dressingRoom/ArtemisJacketOn.fbx'; var TRANSFORMER_URL_PRISCILLA = 'atp:/dressingRoom/priscilla.fbx'; From 85055d82bfe9342cc297d0c0329cb6711aeadc5a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 2 Jun 2016 17:05:58 -0700 Subject: [PATCH 0313/1237] Regenerate Domain metadata on user (dis)connect --- domain-server/src/DomainMetadata.cpp | 2 +- domain-server/src/DomainMetadata.h | 9 ++++--- domain-server/src/DomainServer.cpp | 36 +++++++++++++++++++--------- domain-server/src/DomainServer.h | 2 ++ 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 224fe1939b..c45a07e646 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -15,7 +15,7 @@ #include "DomainServerNodeData.h" -void DomainMetadata::generate() { +void DomainMetadata::usersChanged() { static const QString DEFAULT_HOSTNAME = "*"; auto nodeList = DependencyManager::get(); diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index 5096f1bb3e..f92c40ce61 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -16,12 +16,11 @@ class DomainMetadata { public: - QVariantMap toVariantMap() { generate(); return _metadata; } - QJsonObject toJSON() { generate(); return QJsonObject::fromVariantMap(_metadata); } + QVariantMap toVariantMap() { return _metadata; } + QJsonObject toJSON() { return QJsonObject::fromVariantMap(_metadata); } -protected slots: - // TODO: Connect appropriate signals to obviate JIT generation - void generate(); +public slots: + void usersChanged(); protected: QVariantMap _metadata; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9b1fbb1d84..5a9fc816c5 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -97,6 +97,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); + // update the metadata when a user (dis)connects + connect(this, &DomainServer::userConnected, &_metadata, &DomainMetadata::usersChanged); + connect(this, &DomainServer::userDisconnected, &_metadata, &DomainMetadata::usersChanged); + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -767,12 +771,16 @@ QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) { } void DomainServer::handleConnectedNode(SharedNodePointer newNode) { - - DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + DomainServerNodeData* nodeData = static_cast(newNode->getLinkedData()); + // reply back to the user with a PacketType::DomainList sendDomainListToNode(newNode, nodeData->getSendingSockAddr()); - + + // if this node is a user (unassigned Agent), signal + if (newNode->getType() == NodeType::Agent && !nodeData->wasAssigned()) { + emit userConnected(); + } + // send out this node to our other connected nodes broadcastNewNode(newNode); } @@ -1890,11 +1898,10 @@ void DomainServer::nodeAdded(SharedNodePointer node) { } void DomainServer::nodeKilled(SharedNodePointer node) { - // if this peer connected via ICE then remove them from our ICE peers hash _gatekeeper.removeICEPeer(node->getUUID()); - DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); + DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); if (nodeData) { // if this node's UUID matches a static assignment we need to throw it back in the assignment queue @@ -1906,15 +1913,22 @@ void DomainServer::nodeKilled(SharedNodePointer node) { } } - // If this node was an Agent ask DomainServerNodeData to potentially remove the interpolation we stored - nodeData->removeOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, - uuidStringWithoutCurlyBraces(node->getUUID())); - // cleanup the connection secrets that we set up for this node (on the other nodes) foreach (const QUuid& otherNodeSessionUUID, nodeData->getSessionSecretHash().keys()) { SharedNodePointer otherNode = DependencyManager::get()->nodeWithUUID(otherNodeSessionUUID); if (otherNode) { - reinterpret_cast(otherNode->getLinkedData())->getSessionSecretHash().remove(node->getUUID()); + static_cast(otherNode->getLinkedData())->getSessionSecretHash().remove(node->getUUID()); + } + } + + if (node->getType() == NodeType::Agent) { + // if this node was an Agent ask DomainServerNodeData to remove the interpolation we potentially stored + nodeData->removeOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, + uuidStringWithoutCurlyBraces(node->getUUID())); + + // if this node is a user (unassigned Agent), signal + if (!nodeData->wasAssigned()) { + emit userDisconnected(); } } } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 0a1df41f50..acda550ce5 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -92,6 +92,8 @@ private slots: signals: void iceServerChanged(); + void userConnected(); + void userDisconnected(); private: void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid()); From 8a2685325b41b9e032b9a85f71846acfa0b24b15 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 3 Jun 2016 12:06:51 +1200 Subject: [PATCH 0314/1237] Make localhost, anonymous, and logged-in rows non-deletable --- domain-server/resources/describe-settings.json | 5 ++++- .../resources/web/settings/js/settings.js | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 6ddc75bcef..7ee45d5ccd 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -148,7 +148,10 @@ "editable": true, "default": false } - ] + ], + + "non-deletable-row-key": "permissions_id", + "non-deletable-row-values": ["localhost", "anonymous", "logged-in"] } ] }, diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 29bb430893..44b2c692d2 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -919,6 +919,9 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" + setting.help + "" } + var nonDeletableRowKey = setting["non-deletable-row-key"]; + var nonDeletableRowValues = setting["non-deletable-row-values"]; + html += ""; @@ -961,6 +964,8 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" } + var isNonDeletableRow = false; + _.each(setting.columns, function(col) { if (isArray) { @@ -972,6 +977,9 @@ function makeTable(setting, keypath, setting_value, isLocked) { colName = keypath + "." + rowIndexOrName + "." + col.name; } + isNonDeletableRow = isNonDeletableRow + || (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1); + if (isArray && col.type === "checkbox" && col.editable) { html += "" } - html += "" + if (isNonDeletableRow) { + html += ""; + } else { + html += ""; + } } html += "" From 5c293646b960007ab8dfff83712909db57f84861 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 2 Jun 2016 17:22:39 -0700 Subject: [PATCH 0315/1237] Segment metadata users --- domain-server/src/DomainMetadata.cpp | 17 +++++++++++------ domain-server/src/DomainMetadata.h | 10 ++++++++-- domain-server/src/DomainServer.cpp | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index c45a07e646..c92303ee7b 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -15,6 +15,14 @@ #include "DomainServerNodeData.h" +const QString DomainMetadata::USERS_KEY = "users"; +const QString DomainMetadata::USERS_NUM_KEY = "num_users"; +const QString DomainMetadata::USERS_HOSTNAMES_KEY = "users_hostnames"; + +DomainMetadata::DomainMetadata() : + _metadata{{ USERS_KEY, {} }} { +} + void DomainMetadata::usersChanged() { static const QString DEFAULT_HOSTNAME = "*"; @@ -39,13 +47,10 @@ void DomainMetadata::usersChanged() { } }); - static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; - _metadata[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; - - static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; - _metadata[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; + QVariantMap users = {{ USERS_NUM_KEY, numConnectedUnassigned }, { USERS_HOSTNAMES_KEY, userHostnames }}; + _metadata[USERS_KEY] = users; #if DEV_BUILD - qDebug() << "Regenerated domain metadata - users:" << _metadata; + qDebug() << "Regenerated domain metadata - users:" << users; #endif } diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index f92c40ce61..ae6dfe10a1 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -15,9 +15,15 @@ #include class DomainMetadata { + static const QString USERS_KEY; + static const QString USERS_NUM_KEY; + static const QString USERS_HOSTNAMES_KEY; + public: - QVariantMap toVariantMap() { return _metadata; } - QJsonObject toJSON() { return QJsonObject::fromVariantMap(_metadata); } + DomainMetadata(); + + QJsonObject get() { return QJsonObject::fromVariantMap(_metadata); } + QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS_KEY].toMap()); } public slots: void usersChanged(); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 5a9fc816c5..c24fd02727 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1098,7 +1098,7 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // Add the metadata to the heartbeat static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; - domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.toJSON(); + domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.getUsers(); QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); From 8c13ff4ee15ba0ab7b2c8d8b9de3aef2fff0da66 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 31 May 2016 20:23:58 -0700 Subject: [PATCH 0316/1237] Pinned UI support --- interface/resources/qml/AddressBarDialog.qml | 9 +- interface/resources/qml/AssetServer.qml | 4 +- interface/resources/qml/Browser.qml | 4 +- interface/resources/qml/InfoView.qml | 2 +- interface/resources/qml/LoginDialog.qml | 11 +- interface/resources/qml/QmlWebWindow.qml | 4 +- interface/resources/qml/QmlWindow.qml | 2 +- interface/resources/qml/ToolWindow.qml | 10 +- .../qml/controls-uit/AttachmentsTable.qml | 2 +- interface/resources/qml/desktop/Desktop.qml | 74 ++- .../resources/qml/dialogs/FileDialog.qml | 4 +- .../resources/qml/dialogs/MessageDialog.qml | 4 +- .../qml/dialogs/PreferencesDialog.qml | 4 +- .../resources/qml/dialogs/QueryDialog.qml | 2 +- .../qml/dialogs/preferences/AvatarBrowser.qml | 2 +- .../qml/hifi/dialogs/AttachmentsDialog.qml | 4 +- .../qml/hifi/dialogs/ModelBrowserDialog.qml | 2 +- .../qml/hifi/dialogs/RunningScripts.qml | 7 +- .../hifi/dialogs/attachments/Attachment.qml | 2 +- .../qml/hifi/dialogs/attachments/Vector3.qml | 2 +- .../qml/windows-uit/DefaultFrame.qml | 119 ---- .../resources/qml/windows-uit/Fadable.qml | 60 -- interface/resources/qml/windows-uit/Frame.qml | 133 ----- .../resources/qml/windows-uit/ModalFrame.qml | 98 ---- .../resources/qml/windows-uit/ModalWindow.qml | 28 - .../resources/qml/windows-uit/Window.qml | 343 ------------ .../resources/qml/windows/DefaultFrame.qml | 118 ++-- interface/resources/qml/windows/Fadable.qml | 33 +- interface/resources/qml/windows/Frame.qml | 102 +++- .../resources/qml/windows/HiddenFrame.qml | 2 +- .../resources/qml/windows/ModalFrame.qml | 110 +++- .../resources/qml/windows/ModalWindow.qml | 24 +- interface/resources/qml/windows/Window.qml | 308 ++++++++-- interface/src/Application.cpp | 3 +- interface/src/ui/OverlayConductor.cpp | 61 +- interface/src/ui/OverlayConductor.h | 2 +- .../src/display-plugins/CompositorHelper.cpp | 56 +- .../src/display-plugins/CompositorHelper.h | 23 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 49 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 54 +- libraries/ui/src/ErrorDialog.cpp | 4 - libraries/ui/src/ErrorDialog.h | 1 - libraries/ui/src/OffscreenQmlDialog.cpp | 2 +- libraries/ui/src/OffscreenUi.cpp | 34 +- libraries/ui/src/OffscreenUi.h | 9 + libraries/ui/src/QmlWindowClass.cpp | 3 +- libraries/ui/src/Tooltip.cpp | 4 - libraries/ui/src/Tooltip.h | 2 - tests/ui/qml/main.qml | 529 +++++++++--------- tests/ui/qmlscratch.pro | 3 +- 50 files changed, 1003 insertions(+), 1469 deletions(-) delete mode 100644 interface/resources/qml/windows-uit/DefaultFrame.qml delete mode 100644 interface/resources/qml/windows-uit/Fadable.qml delete mode 100644 interface/resources/qml/windows-uit/Frame.qml delete mode 100644 interface/resources/qml/windows-uit/ModalFrame.qml delete mode 100644 interface/resources/qml/windows-uit/ModalWindow.qml delete mode 100644 interface/resources/qml/windows-uit/Window.qml diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7f107e44e9..a48804faba 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -20,9 +20,10 @@ Window { objectName: "AddressBarDialog" frame: HiddenFrame {} + hideBackground: true - visible: false - destroyOnInvisible: false + shown: false + destroyOnHidden: false resizable: false scale: 1.25 // Make this dialog a little larger than normal @@ -145,14 +146,14 @@ Window { if (addressLine.text !== "") { addressBarDialog.loadAddress(addressLine.text) } - root.visible = false; + root.shown = false; } Keys.onPressed: { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.visible = false + root.shown = false event.accepted = true break case Qt.Key_Enter: diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 370bc92d81..e975037b5f 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -15,7 +15,7 @@ import Qt.labs.settings 1.0 import "styles-uit" import "controls-uit" as HifiControls -import "windows-uit" +import "windows" import "dialogs" Window { @@ -23,7 +23,7 @@ Window { objectName: "AssetServer" title: "Asset Browser" resizable: true - destroyOnInvisible: true + destroyOnHidden: true implicitWidth: 384; implicitHeight: 640 minSize: Qt.vector2d(200, 300) diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 89ab333a0d..efa2063fe8 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -11,13 +11,13 @@ Window { HifiConstants { id: hifi } title: "Browser" resizable: true - destroyOnInvisible: true + destroyOnHidden: true width: 800 height: 600 property alias webView: webview Component.onCompleted: { - visible = true + shown = true addressBar.text = webview.url } diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index c5dba7e1f3..0f17a88614 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import Hifi 1.0 as Hifi import "controls-uit" -import "windows-uit" as Windows +import "windows" as Windows Windows.Window { id: root diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 1b25b75608..f5030cb88d 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -22,8 +22,9 @@ Window { width: loginDialog.implicitWidth // FIXME make movable anchors.centerIn: parent - destroyOnInvisible: false - visible: false + destroyOnHidden: false + hideBackground: true + shown: false LoginDialog { id: loginDialog @@ -268,8 +269,8 @@ Window { } } - onVisibleChanged: { - if (!visible) { + onShownChanged: { + if (!shown) { username.text = "" password.text = "" loginDialog.statusText = "" @@ -282,7 +283,7 @@ Window { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.visible = false; + root.shown = false; event.accepted = true; break; diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index ae052879db..09c3bd7f28 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -13,7 +13,7 @@ import QtQuick.Controls 1.4 import QtWebEngine 1.1 import QtWebChannel 1.0 -import "windows-uit" as Windows +import "windows" as Windows import "controls-uit" as Controls import "styles-uit" @@ -22,7 +22,7 @@ Windows.Window { HifiConstants { id: hifi } title: "WebWindow" resizable: true - visible: false + shown: false // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false property alias source: webview.url diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 0420cd2e88..7be747a3ad 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -14,7 +14,7 @@ Windows.Window { HifiConstants { id: hifi } title: "QmlWindow" resizable: true - visible: false + shown: false focus: true property var channel; // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index aaff43b146..faa96fac5a 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -15,7 +15,7 @@ import QtWebEngine 1.1 import QtWebChannel 1.0 import Qt.labs.settings 1.0 -import "windows-uit" +import "windows" import "controls-uit" import "styles-uit" @@ -24,9 +24,9 @@ Window { resizable: true objectName: "ToolWindow" destroyOnCloseButton: false - destroyOnInvisible: false + destroyOnHidden: false closable: true - visible: false + shown: false title: "Edit" property alias tabView: tabView implicitWidth: 520; implicitHeight: 695 @@ -142,7 +142,7 @@ Window { return; } } - visible = false; + shown = false; } function findIndexForUrl(source) { @@ -172,7 +172,7 @@ Window { var tab = tabView.getTab(index); if (newVisible) { - toolWindow.visible = true + toolWindow.shown = true tab.enabled = true } else { tab.enabled = false; diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index ce93b8f4df..7d0280b72d 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -15,7 +15,7 @@ import QtQuick.XmlListModel 2.0 import "../styles-uit" import "../controls-uit" as HifiControls -import "../windows-uit" +import "../windows" import "../hifi/models" TableView { diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 62a72e3d8c..73f8a17bb0 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -52,6 +52,7 @@ FocusScope { readonly property real menu: 8000 } + QtObject { id: d @@ -93,6 +94,17 @@ FocusScope { return item; } + function findMatchingChildren(item, predicate) { + var results = []; + for (var i in item.children) { + var child = item.children[i]; + if (predicate(child)) { + results.push(child); + } + } + return results; + } + function isTopLevelWindow(item) { return item.topLevelWindow; } @@ -106,19 +118,9 @@ FocusScope { } function getTopLevelWindows(predicate) { - var currentWindows = []; - if (!desktop) { - console.log("Could not find desktop for " + item) - return currentWindows; - } - - for (var i = 0; i < desktop.children.length; ++i) { - var child = desktop.children[i]; - if (isTopLevelWindow(child) && (!predicate || predicate(child))) { - currentWindows.push(child) - } - } - return currentWindows; + return findMatchingChildren(desktop, function(child) { + return (isTopLevelWindow(child) && (!predicate || predicate(child))); + }); } function getDesktopWindow(item) { @@ -227,19 +229,9 @@ FocusScope { } function getRepositionChildren(predicate) { - var currentWindows = []; - if (!desktop) { - console.log("Could not find desktop"); - return currentWindows; - } - - for (var i = 0; i < desktop.children.length; ++i) { - var child = desktop.children[i]; - if (child.shouldReposition === true && (!predicate || predicate(child))) { - currentWindows.push(child) - } - } - return currentWindows; + return findMatchingChildren(desktop, function(child) { + return (child.shouldReposition === true && (!predicate || predicate(child))); + }); } function repositionAll() { @@ -265,6 +257,35 @@ FocusScope { } } + property bool pinned: false + property var hiddenChildren: [] + + function togglePinned() { + pinned = !pinned + } + + onPinnedChanged: { + + if (pinned) { + hiddenChildren = d.findMatchingChildren(desktop, function(child){ + return !d.isTopLevelWindow(child) && child.visible; + }); + + hiddenChildren.forEach(function(child){ + child.visible = false; + }); + } else { + hiddenChildren.forEach(function(child){ + if (child) { + child.visible = true; + } + }); + hiddenChildren = []; + } + } + + onShowDesktop: pinned = false + function raise(item) { var targetWindow = d.getDesktopWindow(item); if (!targetWindow) { @@ -422,7 +443,6 @@ FocusScope { event.accepted = false; } - function unfocusWindows() { var windows = d.getTopLevelWindows(); for (var i = 0; i < windows.length; ++i) { diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 015a192185..96676e986a 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -18,7 +18,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import ".." import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" import "fileDialog" @@ -724,7 +724,7 @@ ModalWindow { Action { id: cancelAction text: "Cancel" - onTriggered: { canceled(); root.visible = false; } + onTriggered: { canceled(); root.shown = false; } } } diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 30f492e36a..d390ea08bf 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -14,7 +14,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" import "messageDialog" @@ -24,7 +24,7 @@ ModalWindow { implicitWidth: 640 implicitHeight: 320 destroyOnCloseButton: true - destroyOnInvisible: true + destroyOnHidden: true visible: true signal selected(int button); diff --git a/interface/resources/qml/dialogs/PreferencesDialog.qml b/interface/resources/qml/dialogs/PreferencesDialog.qml index 40cc713397..398e0abd8e 100644 --- a/interface/resources/qml/dialogs/PreferencesDialog.qml +++ b/interface/resources/qml/dialogs/PreferencesDialog.qml @@ -13,14 +13,14 @@ import QtQuick.Controls 1.4 import "../controls-uit" as HifiControls import "../styles-uit" -import "../windows-uit" +import "../windows" import "preferences" Window { id: root title: "Preferences" resizable: true - destroyOnInvisible: true + destroyOnHidden: true width: 500 height: 577 property var sections: [] diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index 0c7772dc94..05cb347169 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -14,7 +14,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" ModalWindow { id: root diff --git a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml index e5bc9b80ef..90d2dc5284 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1 -import "../../windows-uit" as Windows +import "../../windows" as Windows import "../../controls-uit" as Controls import "../../styles-uit" diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 437e02e149..7b3a368bb0 100755 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -6,7 +6,7 @@ import QtQuick.Controls.Styles 1.4 import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" import "attachments" Window { @@ -16,7 +16,7 @@ Window { width: 600 height: 600 resizable: true - destroyOnInvisible: true + destroyOnHidden: true minSize: Qt.vector2d(400, 500) HifiConstants { id: hifi } diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index b2de108545..29f0498f59 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -9,7 +9,7 @@ import "../models" import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" Window { id: root diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 31bb553809..7dc7654d9a 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -15,14 +15,14 @@ import Qt.labs.settings 1.0 import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" Window { id: root objectName: "RunningScripts" title: "Running Scripts" resizable: true - destroyOnInvisible: true + destroyOnHidden: true implicitWidth: 400 implicitHeight: isHMD ? 695 : 728 minSize: Qt.vector2d(200, 300) @@ -34,6 +34,9 @@ Window { property var runningScriptsModel: ListModel { } property bool isHMD: false + onVisibleChanged: console.log("Running scripts visible changed to " + visible) + onShownChanged: console.log("Running scripts visible changed to " + visible) + Settings { category: "Overlay.RunningScripts" property alias x: root.x diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index 1277c459ce..04e3934535 100755 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -8,7 +8,7 @@ import "." import ".." import "../../../styles-uit" import "../../../controls-uit" as HifiControls -import "../../../windows-uit" +import "../../../windows" Item { height: column.height + 2 * 8 diff --git a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml index e1d7b6d4a3..3d109cc2a5 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControls -import "../../../windows-uit" +import "../../../windows" Item { id: root diff --git a/interface/resources/qml/windows-uit/DefaultFrame.qml b/interface/resources/qml/windows-uit/DefaultFrame.qml deleted file mode 100644 index 84f435480b..0000000000 --- a/interface/resources/qml/windows-uit/DefaultFrame.qml +++ /dev/null @@ -1,119 +0,0 @@ -// -// DefaultFrame.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtGraphicalEffects 1.0 - -import "." -import "../styles-uit" - -Frame { - HifiConstants { id: hifi } - - Rectangle { - // Dialog frame - id: frameContent - - readonly property int iconSize: hifi.dimensions.frameIconSize - readonly property int frameMargin: 9 - readonly property int frameMarginLeft: frameMargin - readonly property int frameMarginRight: frameMargin - readonly property int frameMarginTop: 2 * frameMargin + iconSize - readonly property int frameMarginBottom: iconSize + 11 - - anchors { - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 - } - radius: hifi.dimensions.borderRadius - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - } - - Row { - id: controlsRow - anchors { - right: parent.right; - top: parent.top; - topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title - rightMargin: frameContent.frameMarginRight; - } - spacing: frameContent.iconSize / 4 - - HiFiGlyphs { - // "Pin" button - visible: false - text: (frame.pinned && !pinClickArea.containsMouse) || (!frame.pinned && pinClickArea.containsMouse) ? hifi.glyphs.pinInverted : hifi.glyphs.pin - color: pinClickArea.containsMouse && !pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: pinClickArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - onClicked: { frame.pin(); mouse.accepted = false; } - } - } - - HiFiGlyphs { - // "Close" button - visible: window ? window.closable : false - text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close - color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: closeClickArea - anchors.fill: parent - hoverEnabled: true - onClicked: window.visible = false; - } - } - } - - RalewayRegular { - // Title - id: titleText - anchors { - left: parent.left - leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x - right: controlsRow.left - rightMargin: frameContent.iconSize - top: parent.top - topMargin: frameContent.frameMargin - } - text: window ? window.title : "" - color: hifi.colors.white - size: hifi.fontSizes.overlayTitle - } - - DropShadow { - source: titleText - anchors.fill: titleText - horizontalOffset: 2 - verticalOffset: 2 - samples: 2 - color: hifi.colors.baseGrayShadow60 - visible: (window && window.focus) - cached: true - } - } -} - diff --git a/interface/resources/qml/windows-uit/Fadable.qml b/interface/resources/qml/windows-uit/Fadable.qml deleted file mode 100644 index 34990c2147..0000000000 --- a/interface/resources/qml/windows-uit/Fadable.qml +++ /dev/null @@ -1,60 +0,0 @@ -// -// Fadable.qml -// -// Created by Bradley Austin Davis on 15 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtGraphicalEffects 1.0 - -import "../styles-uit" - -// Enable window visibility transitions -FocusScope { - id: root - HifiConstants { id: hifi } - - Component.onCompleted: { - fadeTargetProperty = visible ? 1.0 : 0.0 - } - - // The target property to animate, usually scale or opacity - property alias fadeTargetProperty: root.opacity - // always start the property at 0 to enable fade in on creation - fadeTargetProperty: 0 - // DO NOT set visible to false or when derived types override it it - // will short circuit the fade in on initial visibility - // visible: false <--- NO - - // Some dialogs should be destroyed when they become - // invisible, so handle that - onVisibleChanged: { - // If someone directly set the visibility to false - // toggle it back on and use the targetVisible flag to transition - // via fading. - if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { - var target = visible; - visible = !visible; - fadeTargetProperty = target ? 1.0 : 0.0; - return; - } - } - - // The actual animator - Behavior on fadeTargetProperty { - NumberAnimation { - duration: hifi.effects.fadeInDuration - easing.type: Easing.InOutCubic - } - } - - // Once we're transparent, disable the dialog's visibility - onFadeTargetPropertyChanged: { - visible = (fadeTargetProperty != 0.0); - } -} diff --git a/interface/resources/qml/windows-uit/Frame.qml b/interface/resources/qml/windows-uit/Frame.qml deleted file mode 100644 index 9519a44cf0..0000000000 --- a/interface/resources/qml/windows-uit/Frame.qml +++ /dev/null @@ -1,133 +0,0 @@ -// -// Frame.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtGraphicalEffects 1.0 - -import "../styles-uit" -import "../js/Utils.js" as Utils - -Item { - id: frame - HifiConstants { id: hifi } - - default property var decoration - - property bool gradientsSupported: desktop.gradientsSupported - - readonly property int frameMarginLeft: frameContent.frameMarginLeft - readonly property int frameMarginRight: frameContent.frameMarginRight - readonly property int frameMarginTop: frameContent.frameMarginTop - readonly property int frameMarginBottom: frameContent.frameMarginBottom - - // Frames always fill their parents, but their decorations may extend - // beyond the window via negative margin sizes - anchors.fill: parent - - children: [ - focusShadow, - decoration, - sizeOutline, - debugZ, - sizeDrag - ] - - Text { - id: debugZ - visible: DebugQML - text: window ? "Z: " + window.z : "" - y: window ? window.height + 4 : 0 - } - - function deltaSize(dx, dy) { - var newSize = Qt.vector2d(window.width + dx, window.height + dy); - newSize = Utils.clampVector(newSize, window.minSize, window.maxSize); - window.width = newSize.x - window.height = newSize.y - } - - RadialGradient { - id: focusShadow - width: 1.66 * window.width - height: 1.66 * window.height - x: (window.width - width) / 2 - y: window.height / 2 - 0.375 * height - visible: gradientsSupported && window && window.focus && pane.visible - gradient: Gradient { - // GradientStop position 0.5 is at full circumference of circle that fits inside the square. - GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity - GradientStop { position: 0.333; color: "#1f000000" } // black, 12% opacity - GradientStop { position: 0.5; color: "#00000000" } // black, 0% opacity - GradientStop { position: 1.0; color: "#00000000" } - } - cached: true - } - - Rectangle { - id: sizeOutline - x: -frameMarginLeft - y: -frameMarginTop - width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 - height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 - color: hifi.colors.baseGrayHighlight15 - border.width: 3 - border.color: hifi.colors.white50 - radius: hifi.dimensions.borderRadius - visible: window ? !pane.visible : false - } - - MouseArea { - // Resize handle - id: sizeDrag - width: hifi.dimensions.frameIconSize - height: hifi.dimensions.frameIconSize - enabled: window ? window.resizable : false - hoverEnabled: true - x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 - y: window ? window.height + 4 : 0 - property vector2d pressOrigin - property vector2d sizeOrigin - property bool hid: false - onPressed: { - //console.log("Pressed on size") - pressOrigin = Qt.vector2d(mouseX, mouseY) - sizeOrigin = Qt.vector2d(window.content.width, window.content.height) - hid = false; - } - onReleased: { - if (hid) { - pane.visible = true - frameContent.visible = true - hid = false; - } - } - onPositionChanged: { - if (pressed) { - if (pane.visible) { - pane.visible = false; - frameContent.visible = false - hid = true; - } - var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); - frame.deltaSize(delta.x, delta.y) - } - } - HiFiGlyphs { - visible: sizeDrag.enabled - x: -11 // Move a little to visually align - y: window.modality == Qt.ApplicationModal ? -6 : -4 - text: hifi.glyphs.resizeHandle - size: hifi.dimensions.frameIconSize + 10 - color: sizeDrag.containsMouse || sizeDrag.pressed - ? hifi.colors.white - : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) - } - } -} diff --git a/interface/resources/qml/windows-uit/ModalFrame.qml b/interface/resources/qml/windows-uit/ModalFrame.qml deleted file mode 100644 index 211353b5f3..0000000000 --- a/interface/resources/qml/windows-uit/ModalFrame.qml +++ /dev/null @@ -1,98 +0,0 @@ -// -// ModalFrame.qml -// -// Created by Bradley Austin Davis on 15 Jan 2016 -// 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 -// - -import QtQuick 2.5 - -import "." -import "../controls-uit" -import "../styles-uit" - -Frame { - HifiConstants { id: hifi } - - Rectangle { - id: frameContent - - readonly property bool hasTitle: window.title != "" - - readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x - readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x - readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) - readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y - - signal frameClicked(); - - anchors { - fill: parent - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.lightGrayText80 - } - radius: hifi.dimensions.borderRadius - color: hifi.colors.faintGray - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - enabled: window.draggable - onClicked: window.frameClicked(); - } - - Item { - visible: frameContent.hasTitle - anchors.fill: parent - anchors { - topMargin: -parent.anchors.topMargin - leftMargin: -parent.anchors.leftMargin - rightMargin: -parent.anchors.rightMargin - } - - Item { - width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 0) - x: (parent.width - width) / 2 - - onWidthChanged: window.titleWidth = width - - HiFiGlyphs { - id: icon - text: window.iconText ? window.iconText : "" - size: window.iconSize ? window.iconSize : 30 - color: hifi.colors.lightGray - visible: text != "" - anchors.verticalCenter: title.verticalCenter - anchors.left: parent.left - } - RalewayRegular { - id: title - text: window.title - elide: Text.ElideRight - color: hifi.colors.baseGrayHighlight - size: hifi.fontSizes.overlayTitle - y: -hifi.dimensions.modalDialogTitleHeight - anchors.right: parent.right - } - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - height: 1 - color: hifi.colors.lightGray - } - } - } -} diff --git a/interface/resources/qml/windows-uit/ModalWindow.qml b/interface/resources/qml/windows-uit/ModalWindow.qml deleted file mode 100644 index 144165e4e1..0000000000 --- a/interface/resources/qml/windows-uit/ModalWindow.qml +++ /dev/null @@ -1,28 +0,0 @@ -// -// ModalWindow.qml -// -// Created by Bradley Austin Davis on 22 Jan 2016 -// 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 -// - -import QtQuick 2.5 - -import "." - -Window { - id: window - modality: Qt.ApplicationModal - destroyOnCloseButton: true - destroyOnInvisible: true - frame: ModalFrame { } - - property int colorScheme: hifi.colorSchemes.light - property bool draggable: false - - signal frameClicked(); - - anchors.centerIn: draggable ? undefined : parent -} diff --git a/interface/resources/qml/windows-uit/Window.qml b/interface/resources/qml/windows-uit/Window.qml deleted file mode 100644 index d614b21ce2..0000000000 --- a/interface/resources/qml/windows-uit/Window.qml +++ /dev/null @@ -1,343 +0,0 @@ -// -// Window.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 - -import "." -import "../styles-uit" - -// FIXME how do I set the initial position of a window without -// overriding places where the a individual client of the window -// might be setting the position with a Settings{} element? - -// FIXME how to I enable dragging without allowing the window to lay outside -// of the desktop? How do I ensure when the desktop resizes all the windows -// are still at least partially visible? -Fadable { - id: window - HifiConstants { id: hifi } - - // The Window size is the size of the content, while the frame - // decorations can extend outside it. - implicitHeight: content ? content.height : 0 - implicitWidth: content ? content.width : 0 - x: desktop.invalid_position; y: desktop.invalid_position; - enabled: visible - - signal windowDestroyed(); - - property int modality: Qt.NonModal - readonly property bool topLevelWindow: true - property string title - // Should the window be closable control? - property bool closable: true - // Should the window try to remain on top of other windows? - property bool alwaysOnTop: false - // Should hitting the close button hide or destroy the window? - property bool destroyOnCloseButton: true - // Should hiding the window destroy it or just hide it? - property bool destroyOnInvisible: false - // FIXME support for pinned / unpinned pending full design - // property bool pinnable: false - // property bool pinned: false - property bool resizable: false - property bool gradientsSupported: desktop.gradientsSupported - property int colorScheme: hifi.colorSchemes.dark - - property vector2d minSize: Qt.vector2d(100, 100) - property vector2d maxSize: Qt.vector2d(1280, 800) - - // The content to place inside the window, determined by the client - default property var content - - property var footer: Item { } // Optional static footer at the bottom of the dialog. - - function setDefaultFocus() {} // Default function; can be overridden by dialogs. - - property var rectifier: Timer { - property bool executing: false; - interval: 100 - repeat: false - running: false - - onTriggered: { - executing = true; - x = Math.floor(x); - y = Math.floor(y); - executing = false; - } - - function begin() { - if (!executing) { - restart(); - } - } - } - onXChanged: rectifier.begin(); - onYChanged: rectifier.begin(); - - // This mouse area serves to raise the window. To function, it must live - // in the window and have a higher Z-order than the content, but follow - // the position and size of frame decoration - property var activator: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin - propagateComposedEvents: true - acceptedButtons: Qt.AllButtons - enabled: window.visible - onPressed: { - //console.log("Pressed on activator area"); - window.raise(); - mouse.accepted = false; - } - } - - // This mouse area serves to swallow mouse events while the mouse is over the window - // to prevent things like mouse wheel events from reaching the application and changing - // the camera if the user is scrolling through a list and gets to the end. - property var swallower: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin - hoverEnabled: true - acceptedButtons: Qt.AllButtons - enabled: window.visible - onClicked: {} - onDoubleClicked: {} - onPressAndHold: {} - onReleased: {} - onWheel: {} - } - - // Default to a standard frame. Can be overriden to provide custom - // frame styles, like a full desktop frame to simulate a modal window - property var frame: DefaultFrame { } - - // Scrollable window content. - property var pane: Item { - property bool isScrolling: scrollView.height < scrollView.contentItem.height - property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) - property int scrollHeight: scrollView.height - - anchors.fill: parent - anchors.rightMargin: isScrolling ? 11 : 0 - - Rectangle { - id: contentBackground - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 11 : 0 - color: hifi.colors.baseGray - visible: modality != Qt.ApplicationModal - } - - LinearGradient { - visible: gradientsSupported && modality != Qt.ApplicationModal - anchors.top: contentBackground.bottom - anchors.left: contentBackground.left - width: contentBackground.width - 1 - height: 4 - start: Qt.point(0, 0) - end: Qt.point(0, 4) - gradient: Gradient { - GradientStop { position: 0.0; color: hifi.colors.darkGray } - GradientStop { position: 1.0; color: hifi.colors.darkGray0 } - } - cached: true - } - - ScrollView { - id: scrollView - contentItem: content - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - verticalScrollBarPolicy: Qt.ScrollBarAsNeeded - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 1 : 0 - anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 - - style: ScrollViewStyle { - - padding.right: -7 // Move to right away from content. - - handle: Item { - implicitWidth: 8 - Rectangle { - radius: 4 - color: hifi.colors.white30 - anchors { - fill: parent - leftMargin: 2 // Finesse size and position. - topMargin: 1 - bottomMargin: 1 - } - } - } - - scrollBarBackground: Item { - implicitWidth: 10 - Rectangle { - color: hifi.colors.darkGray30 - radius: 4 - anchors { - fill: parent - topMargin: -1 // Finesse size - bottomMargin: -2 - } - } - } - - incrementControl: Item { - visible: false - } - - decrementControl: Item { - visible: false - } - } - } - - Rectangle { - // Optional non-scrolling footer. - id: footerPane - anchors { - left: parent.left - bottom: parent.bottom - } - width: parent.contentWidth - height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 - color: hifi.colors.baseGray - visible: footer.height > 0 - - Item { - // Horizontal rule. - anchors.fill: parent - - Rectangle { - width: parent.width - height: 1 - y: 1 // Stop displaying content just above horizontal rule/=. - color: hifi.colors.baseGrayShadow - } - - Rectangle { - width: parent.width - height: 1 - y: 2 - color: hifi.colors.baseGrayHighlight - } - } - - Item { - anchors.fill: parent - anchors.topMargin: 3 // Horizontal rule. - children: [ footer ] - } - } - } - - children: [ swallower, frame, pane, activator ] - - Component.onCompleted: { - window.parentChanged.connect(raise); - raise(); - setDefaultFocus(); - centerOrReposition(); - } - Component.onDestruction: { - window.parentChanged.disconnect(raise); // Prevent warning on shutdown - windowDestroyed(); - } - - onVisibleChanged: { - if (!visible && destroyOnInvisible) { - destroy(); - return; - } - if (visible) { - raise(); - } - enabled = visible - - if (visible && parent) { - centerOrReposition(); - } - } - - function centerOrReposition() { - if (x == desktop.invalid_position && y == desktop.invalid_position) { - desktop.centerOnVisible(window); - } else { - desktop.repositionOnVisible(window); - } - } - - function raise() { - if (visible && parent) { - desktop.raise(window) - } - } - - function pin() { -// pinned = ! pinned - } - - // our close function performs the same way as the OffscreenUI class: - // don't do anything but manipulate the targetVisible flag and let the other - // mechanisms decide if the window should be destroyed after the close - // animation completes - // FIXME using this close function messes up the visibility signals received by the - // type and it's derived types -// function close() { -// console.log("Closing " + window) -// if (destroyOnCloseButton) { -// destroyOnInvisible = true -// } -// visible = false; -// } - - function framedRect() { - if (!frame || !frame.decoration) { - return Qt.rect(0, 0, window.width, window.height) - } - return Qt.rect(frame.decoration.anchors.leftMargin, frame.decoration.anchors.topMargin, - window.width - frame.decoration.anchors.leftMargin - frame.decoration.anchors.rightMargin, - window.height - frame.decoration.anchors.topMargin - frame.decoration.anchors.bottomMargin) - } - - Keys.onPressed: { - switch(event.key) { - case Qt.Key_Control: - case Qt.Key_Shift: - case Qt.Key_Meta: - case Qt.Key_Alt: - break; - - case Qt.Key_W: - if (window.closable && (event.modifiers === Qt.ControlModifier)) { - visible = false - event.accepted = true - } - // fall through - - default: - // Consume unmodified keyboard entries while the window is focused, to prevent them - // from propagating to the application - if (event.modifiers === Qt.NoModifier) { - event.accepted = true; - } - break; - } - } -} diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index c58f9ca545..242209dbe0 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -1,20 +1,48 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 +import QtGraphicalEffects 1.0 import "." -import "../controls" +import "../styles-uit" Frame { - id: frame - - property bool wideTopMargin: (window && (window.closable || window.title)); + HifiConstants { id: hifi } Rectangle { - anchors { margins: -iconSize; topMargin: -iconSize * (wideTopMargin ? 2 : 1); } - anchors.fill: parent; - color: "#7f7f7f7f"; - radius: 3; + // Dialog frame + id: frameContent - // Allow dragging of the window + readonly property int iconSize: hifi.dimensions.frameIconSize + readonly property int frameMargin: 9 + readonly property int frameMarginLeft: frameMargin + readonly property int frameMarginRight: frameMargin + readonly property int frameMarginTop: 2 * frameMargin + iconSize + readonly property int frameMarginBottom: iconSize + 11 + + anchors { + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius + + // Enable dragging of the window MouseArea { anchors.fill: parent drag.target: window @@ -22,48 +50,70 @@ Frame { Row { id: controlsRow - anchors { right: parent.right; top: parent.top; rightMargin: iconSize; topMargin: iconSize / 2; } - spacing: iconSize / 4 - FontAwesome { - visible: false - text: "\uf08d" - style: Text.Outline; styleColor: "white" - size: frame.iconSize - rotation: !frame.parent ? 90 : frame.parent.pinned ? 0 : 90 - color: frame.pinned ? "red" : "black" + anchors { + right: parent.right; + top: parent.top; + topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title + rightMargin: frameContent.frameMarginRight; + } + spacing: frameContent.iconSize / 4 + + HiFiGlyphs { + // "Pin" button + visible: window.pinnable + text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin + color: pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white + size: frameContent.iconSize MouseArea { + id: pinClickArea anchors.fill: parent + hoverEnabled: true propagateComposedEvents: true - onClicked: { frame.pin(); mouse.accepted = false; } + onClicked: window.pinned = !window.pinned; } } - FontAwesome { + + HiFiGlyphs { + // "Close" button visible: window ? window.closable : false - text: closeClickArea.containsMouse ? "\uf057" : "\uf05c" - style: Text.Outline; - styleColor: "white" - color: closeClickArea.containsMouse ? "red" : "black" - size: frame.iconSize + text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close + color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white + size: frameContent.iconSize MouseArea { id: closeClickArea anchors.fill: parent hoverEnabled: true - onClicked: window.visible = false; + onClicked: window.shown = false; } } } - Text { + RalewayRegular { + // Title id: titleText - anchors { left: parent.left; leftMargin: iconSize; right: controlsRow.left; rightMargin: iconSize; top: parent.top; topMargin: iconSize / 2; } + anchors { + left: parent.left + leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x + right: controlsRow.left + rightMargin: frameContent.iconSize + top: parent.top + topMargin: frameContent.frameMargin + } text: window ? window.title : "" - elide: Text.ElideRight - font.bold: true - color: (window && window.focus) ? "white" : "gray" - style: Text.Outline; - styleColor: "black" + color: hifi.colors.white + size: hifi.fontSizes.overlayTitle + } + + DropShadow { + source: titleText + anchors.fill: titleText + horizontalOffset: 2 + verticalOffset: 2 + samples: 2 + color: hifi.colors.baseGrayShadow60 + visible: (window && window.focus) + cached: true } } - } diff --git a/interface/resources/qml/windows/Fadable.qml b/interface/resources/qml/windows/Fadable.qml index 0352966bd0..38cd4bf1f9 100644 --- a/interface/resources/qml/windows/Fadable.qml +++ b/interface/resources/qml/windows/Fadable.qml @@ -1,8 +1,18 @@ +// +// Fadable.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 -import "." -import "../styles" + +import "../styles-uit" // Enable window visibility transitions FocusScope { @@ -13,6 +23,7 @@ FocusScope { fadeTargetProperty = visible ? 1.0 : 0.0 } + property var completionCallback; // The target property to animate, usually scale or opacity property alias fadeTargetProperty: root.opacity // always start the property at 0 to enable fade in on creation @@ -33,6 +44,13 @@ FocusScope { fadeTargetProperty = target ? 1.0 : 0.0; return; } + + // Now handle completions + if (completionCallback) { + completionCallback(); + completionCallback = undefined; + } + } // The actual animator @@ -43,8 +61,17 @@ FocusScope { } } - // Once we're transparent, disable the dialog's visibility onFadeTargetPropertyChanged: { visible = (fadeTargetProperty != 0.0); } + + function fadeIn(callback) { + completionCallback = callback; + fadeTargetProperty = 1.0; + } + + function fadeOut(callback) { + completionCallback = callback; + fadeTargetProperty = 0.0; + } } diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 20bf669b9a..9519a44cf0 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -1,24 +1,42 @@ -import QtQuick 2.5 +// +// Frame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// -import "../controls" +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "../styles-uit" import "../js/Utils.js" as Utils Item { id: frame + HifiConstants { id: hifi } + + default property var decoration + + property bool gradientsSupported: desktop.gradientsSupported + + readonly property int frameMarginLeft: frameContent.frameMarginLeft + readonly property int frameMarginRight: frameContent.frameMarginRight + readonly property int frameMarginTop: frameContent.frameMarginTop + readonly property int frameMarginBottom: frameContent.frameMarginBottom + // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes anchors.fill: parent - // Convenience accessor for the window - property alias window: frame.parent - readonly property int iconSize: 24 - default property var decoration; - children: [ + focusShadow, decoration, sizeOutline, debugZ, - sizeDrag, + sizeDrag ] Text { @@ -35,57 +53,81 @@ Item { window.height = newSize.y } + RadialGradient { + id: focusShadow + width: 1.66 * window.width + height: 1.66 * window.height + x: (window.width - width) / 2 + y: window.height / 2 - 0.375 * height + visible: gradientsSupported && window && window.focus && pane.visible + gradient: Gradient { + // GradientStop position 0.5 is at full circumference of circle that fits inside the square. + GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity + GradientStop { position: 0.333; color: "#1f000000" } // black, 12% opacity + GradientStop { position: 0.5; color: "#00000000" } // black, 0% opacity + GradientStop { position: 1.0; color: "#00000000" } + } + cached: true + } + Rectangle { id: sizeOutline - width: window ? window.width : 0 - height: window ? window.height : 0 - color: "#00000000" - border.width: 4 - radius: 10 - visible: window ? !window.content.visible : false + x: -frameMarginLeft + y: -frameMarginTop + width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 + height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 + color: hifi.colors.baseGrayHighlight15 + border.width: 3 + border.color: hifi.colors.white50 + radius: hifi.dimensions.borderRadius + visible: window ? !pane.visible : false } MouseArea { + // Resize handle id: sizeDrag - width: iconSize - height: iconSize + width: hifi.dimensions.frameIconSize + height: hifi.dimensions.frameIconSize enabled: window ? window.resizable : false - x: window ? window.width : 0 - y: window ? window.height : 0 + hoverEnabled: true + x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 + y: window ? window.height + 4 : 0 property vector2d pressOrigin property vector2d sizeOrigin property bool hid: false onPressed: { - console.log("Pressed on size") + //console.log("Pressed on size") pressOrigin = Qt.vector2d(mouseX, mouseY) sizeOrigin = Qt.vector2d(window.content.width, window.content.height) hid = false; } onReleased: { if (hid) { - window.content.visible = true + pane.visible = true + frameContent.visible = true hid = false; } } onPositionChanged: { if (pressed) { - if (window.content.visible) { - window.content.visible = false; + if (pane.visible) { + pane.visible = false; + frameContent.visible = false hid = true; } var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); frame.deltaSize(delta.x, delta.y) } } - FontAwesome { + HiFiGlyphs { visible: sizeDrag.enabled - rotation: -45 - anchors { centerIn: parent } - horizontalAlignment: Text.AlignHCenter - text: "\uf07d" - size: iconSize / 3 * 2 - style: Text.Outline; styleColor: "white" + x: -11 // Move a little to visually align + y: window.modality == Qt.ApplicationModal ? -6 : -4 + text: hifi.glyphs.resizeHandle + size: hifi.dimensions.frameIconSize + 10 + color: sizeDrag.containsMouse || sizeDrag.pressed + ? hifi.colors.white + : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) } } - } diff --git a/interface/resources/qml/windows/HiddenFrame.qml b/interface/resources/qml/windows/HiddenFrame.qml index 2621b71eed..3d3fd047e2 100644 --- a/interface/resources/qml/windows/HiddenFrame.qml +++ b/interface/resources/qml/windows/HiddenFrame.qml @@ -2,7 +2,7 @@ import QtQuick 2.5 import "." -Frame { +Item { id: frame Item { anchors.fill: parent } diff --git a/interface/resources/qml/windows/ModalFrame.qml b/interface/resources/qml/windows/ModalFrame.qml index eb4641bc75..211353b5f3 100644 --- a/interface/resources/qml/windows/ModalFrame.qml +++ b/interface/resources/qml/windows/ModalFrame.qml @@ -1,36 +1,98 @@ +// +// ModalFrame.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// 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 +// + import QtQuick 2.5 import "." -import "../controls" +import "../controls-uit" +import "../styles-uit" Frame { - id: frame + HifiConstants { id: hifi } - Item { - anchors.fill: parent + Rectangle { + id: frameContent - Rectangle { - id: background - anchors.fill: parent - anchors.margins: -4096 - visible: window.visible - color: "#7f7f7f7f"; - radius: 3; + readonly property bool hasTitle: window.title != "" + + readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) + readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y + + signal frameClicked(); + + anchors { + fill: parent + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom } - Text { - y: -implicitHeight - iconSize / 2 - text: window.title - elide: Text.ElideRight - font.bold: true - color: window.focus ? "white" : "gray" - style: Text.Outline; - styleColor: "black" + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.lightGrayText80 + } + radius: hifi.dimensions.borderRadius + color: hifi.colors.faintGray + + // Enable dragging of the window + MouseArea { + anchors.fill: parent + drag.target: window + enabled: window.draggable + onClicked: window.frameClicked(); + } + + Item { + visible: frameContent.hasTitle + anchors.fill: parent + anchors { + topMargin: -parent.anchors.topMargin + leftMargin: -parent.anchors.leftMargin + rightMargin: -parent.anchors.rightMargin + } + + Item { + width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 0) + x: (parent.width - width) / 2 + + onWidthChanged: window.titleWidth = width + + HiFiGlyphs { + id: icon + text: window.iconText ? window.iconText : "" + size: window.iconSize ? window.iconSize : 30 + color: hifi.colors.lightGray + visible: text != "" + anchors.verticalCenter: title.verticalCenter + anchors.left: parent.left + } + RalewayRegular { + id: title + text: window.title + elide: Text.ElideRight + color: hifi.colors.baseGrayHighlight + size: hifi.fontSizes.overlayTitle + y: -hifi.dimensions.modalDialogTitleHeight + anchors.right: parent.right + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: hifi.colors.lightGray + } } } - - - - } - diff --git a/interface/resources/qml/windows/ModalWindow.qml b/interface/resources/qml/windows/ModalWindow.qml index 32443e70e3..144165e4e1 100644 --- a/interface/resources/qml/windows/ModalWindow.qml +++ b/interface/resources/qml/windows/ModalWindow.qml @@ -1,14 +1,28 @@ +// +// ModalWindow.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// 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 +// + import QtQuick 2.5 import "." Window { - id: root - anchors.centerIn: parent + id: window modality: Qt.ApplicationModal destroyOnCloseButton: true destroyOnInvisible: true - frame: ModalFrame{} + frame: ModalFrame { } + + property int colorScheme: hifi.colorSchemes.light + property bool draggable: false + + signal frameClicked(); + + anchors.centerIn: draggable ? undefined : parent } - - diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 3abdbacc64..ed1b820fc8 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -1,9 +1,20 @@ +// +// Window.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import "." -import "../styles" +import "../styles-uit" // FIXME how do I set the initial position of a window without // overriding places where the a individual client of the window @@ -15,16 +26,36 @@ import "../styles" Fadable { id: window HifiConstants { id: hifi } + + // + // Signals + // + signal windowDestroyed(); + + // + // Native properties + // + // The Window size is the size of the content, while the frame // decorations can extend outside it. implicitHeight: content ? content.height : 0 implicitWidth: content ? content.width : 0 x: desktop.invalid_position; y: desktop.invalid_position; - enabled: visible + children: [ swallower, frame, pane, activator ] - signal windowDestroyed(); + // + // Custom properties + // property int modality: Qt.NonModal + // Corresponds to the window shown / hidden state AS DISTINCT from window visibility. + // Window visibility should NOT be used as a proxy for any other behavior. + property bool shown: true + // FIXME workaround to deal with the face that some visual items are defined here, + // when they should be moved to a frame derived type + property bool hideBackground: false + visible: shown + enabled: visible readonly property bool topLevelWindow: true property string title // Should the window be closable control? @@ -34,17 +65,23 @@ Fadable { // Should hitting the close button hide or destroy the window? property bool destroyOnCloseButton: true // Should hiding the window destroy it or just hide it? - property bool destroyOnInvisible: false - // FIXME support for pinned / unpinned pending full design - // property bool pinnable: false - // property bool pinned: false + property bool destroyOnHidden: false + property bool pinnable: true + property bool pinned: false property bool resizable: false + property bool gradientsSupported: desktop.gradientsSupported + property int colorScheme: hifi.colorSchemes.dark + property vector2d minSize: Qt.vector2d(100, 100) - property vector2d maxSize: Qt.vector2d(1280, 720) + property vector2d maxSize: Qt.vector2d(1280, 800) // The content to place inside the window, determined by the client default property var content + property var footer: Item { } // Optional static footer at the bottom of the dialog. + + function setDefaultFocus() {} // Default function; can be overridden by dialogs. + property var rectifier: Timer { property bool executing: false; interval: 100 @@ -65,20 +102,15 @@ Fadable { } } - - onXChanged: rectifier.begin(); - onYChanged: rectifier.begin(); - // This mouse area serves to raise the window. To function, it must live // in the window and have a higher Z-order than the content, but follow // the position and size of frame decoration property var activator: MouseArea { width: frame.decoration.width height: frame.decoration.height - x: frame.decoration.anchors.margins + x: frame.decoration.anchors.leftMargin y: frame.decoration.anchors.topMargin propagateComposedEvents: true - hoverEnabled: true acceptedButtons: Qt.AllButtons enabled: window.visible onPressed: { @@ -94,7 +126,7 @@ Fadable { property var swallower: MouseArea { width: frame.decoration.width height: frame.decoration.height - x: frame.decoration.anchors.margins + x: frame.decoration.anchors.leftMargin y: frame.decoration.anchors.topMargin hoverEnabled: true acceptedButtons: Qt.AllButtons @@ -106,71 +138,241 @@ Fadable { onWheel: {} } - // Default to a standard frame. Can be overriden to provide custom // frame styles, like a full desktop frame to simulate a modal window property var frame: DefaultFrame { } + // Scrollable window content. + // FIXME this should not define any visual content in this type. The base window + // type should only consist of logic sized areas, with nothing drawn (although the + // default value for the frame property does include visual decorations) + property var pane: Item { + property bool isScrolling: scrollView.height < scrollView.contentItem.height + property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) + property int scrollHeight: scrollView.height - children: [ swallower, frame, content, activator ] + anchors.fill: parent + anchors.rightMargin: isScrolling ? 11 : 0 + Rectangle { + id: contentBackground + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 11 : 0 + color: hifi.colors.baseGray + visible: !window.hideBackground && modality != Qt.ApplicationModal + } + + + LinearGradient { + visible: !window.hideBackground && gradientsSupported && modality != Qt.ApplicationModal + anchors.top: contentBackground.bottom + anchors.left: contentBackground.left + width: contentBackground.width - 1 + height: 4 + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.darkGray } + GradientStop { position: 1.0; color: hifi.colors.darkGray0 } + } + cached: true + } + + ScrollView { + id: scrollView + contentItem: content + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + verticalScrollBarPolicy: Qt.ScrollBarAsNeeded + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 1 : 0 + anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 + + style: ScrollViewStyle { + + padding.right: -7 // Move to right away from content. + + handle: Item { + implicitWidth: 8 + Rectangle { + radius: 4 + color: hifi.colors.white30 + anchors { + fill: parent + leftMargin: 2 // Finesse size and position. + topMargin: 1 + bottomMargin: 1 + } + } + } + + scrollBarBackground: Item { + implicitWidth: 10 + Rectangle { + color: hifi.colors.darkGray30 + radius: 4 + anchors { + fill: parent + topMargin: -1 // Finesse size + bottomMargin: -2 + } + } + } + + incrementControl: Item { + visible: false + } + + decrementControl: Item { + visible: false + } + } + } + + Rectangle { + // Optional non-scrolling footer. + id: footerPane + anchors { + left: parent.left + bottom: parent.bottom + } + width: parent.contentWidth + height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 + color: hifi.colors.baseGray + visible: footer.height > 0 + + Item { + // Horizontal rule. + anchors.fill: parent + + Rectangle { + width: parent.width + height: 1 + y: 1 // Stop displaying content just above horizontal rule/=. + color: hifi.colors.baseGrayShadow + } + + Rectangle { + width: parent.width + height: 1 + y: 2 + color: hifi.colors.baseGrayHighlight + } + } + + Item { + anchors.fill: parent + anchors.topMargin: 3 // Horizontal rule. + children: [ footer ] + } + } + } + + // + // Handlers + // Component.onCompleted: { window.parentChanged.connect(raise); - raise(); - centerOrReposition(); + setDefaultFocus(); + d.centerOrReposition(); + d.updateVisibility(shown); } Component.onDestruction: { window.parentChanged.disconnect(raise); // Prevent warning on shutdown windowDestroyed(); } - function centerOrReposition() { - if (x == desktop.invalid_position && y == desktop.invalid_position) { - desktop.centerOnVisible(window); - } else { - desktop.repositionOnVisible(window); - } - } + onXChanged: rectifier.begin(); + onYChanged: rectifier.begin(); + + onShownChanged: d.updateVisibility(shown) onVisibleChanged: { - if (!visible && destroyOnInvisible) { - destroy(); - return; - } - if (visible) { - raise(); - } enabled = visible - if (visible && parent) { - centerOrReposition(); + d.centerOrReposition(); } } + QtObject { + id: d + + readonly property alias pinned: window.pinned + readonly property alias shown: window.shown + readonly property alias modality: window.modality; + + function getTargetVisibility() { + if (!window.shown) { + return false; + } + + if (modality !== Qt.NonModal) { + return true; + } + + if (pinned) { + return true; + } + + if (desktop && !desktop.pinned) { + return true; + } + + return false; + } + + // The force flag causes all windows to fade back in, because a window was shown + readonly property alias visible: window.visible + function updateVisibility(force) { + if (force && !pinned && desktop.pinned) { + // Change the pinned state (which in turn will call us again) + desktop.pinned = false; + return; + } + + var targetVisibility = getTargetVisibility(); + if (targetVisibility === visible) { + return; + } + + if (targetVisibility) { + fadeIn(function() { + if (force) { + window.raise(); + } + }); + } else { + fadeOut(function() { + if (!window.shown && window.destroyOnHidden) { + window.destroy(); + } + }); + } + } + + function centerOrReposition() { + if (x == desktop.invalid_position && y == desktop.invalid_position) { + desktop.centerOnVisible(window); + } else { + desktop.repositionOnVisible(window); + } + } + + } + + // When the desktop pinned state changes, automatically handle the current windows + Connections { target: desktop; onPinnedChanged: d.updateVisibility() } + + function raise() { if (visible && parent) { desktop.raise(window) } } - function pin() { -// pinned = ! pinned + function setPinned() { + pinned = !pinned } - // our close function performs the same way as the OffscreenUI class: - // don't do anything but manipulate the targetVisible flag and let the other - // mechanisms decide if the window should be destroyed after the close - // animation completes - // FIXME using this close function messes up the visibility signals received by the - // type and it's derived types -// function close() { -// console.log("Closing " + window) -// if (destroyOnCloseButton) { -// destroyOnInvisible = true -// } -// visible = false; -// } - function framedRect() { if (!frame || !frame.decoration) { return Qt.rect(0, 0, window.width, window.height) @@ -180,7 +382,6 @@ Fadable { window.height - frame.decoration.anchors.topMargin - frame.decoration.anchors.bottomMargin) } - Keys.onPressed: { switch(event.key) { case Qt.Key_Control: @@ -189,10 +390,9 @@ Fadable { case Qt.Key_Alt: break; - case Qt.Key_W: if (window.closable && (event.modifiers === Qt.ControlModifier)) { - visible = false + shown = false event.accepted = true } // fall through diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 48b418b93c..c63973985a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2057,7 +2057,8 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_X: if (isShifted && isMeta) { auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootContext()->engine()->clearComponentCache(); + offscreenUi->togglePinned(); + //offscreenUi->getRootContext()->engine()->clearComponentCache(); //OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 83d729779c..54dba229e3 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -96,7 +96,7 @@ void OverlayConductor::updateMode() { myAvatar->reset(true, false, false); } if (_wantsOverlays) { - setEnabled(!nowDriving, false); + setEnabled(!nowDriving); } _driving = nowDriving; } // Else haven't accumulated enough time in new mode, but keep timing. @@ -114,18 +114,14 @@ void OverlayConductor::updateMode() { case SITTING: { // enter the SITTING state // place the overlay at origin - Transform identity; - qApp->getApplicationCompositor().setModelTransform(identity); + qApp->getApplicationCompositor().setModelTransform(Transform()); break; } case STANDING: { // STANDING mode is not currently used. // enter the STANDING state // place the overlay at the current hmd position in world space auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - Transform t; - t.setTranslation(extractTranslation(camMat)); - t.setRotation(glm::quat_cast(camMat)); - qApp->getApplicationCompositor().setModelTransform(t); + qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); break; } @@ -139,54 +135,21 @@ void OverlayConductor::updateMode() { } -void OverlayConductor::setEnabled(bool enabled, bool toggleQmlEvents) { - +void OverlayConductor::setEnabled(bool enabled) { if (enabled == _enabled) { return; } - if (toggleQmlEvents) { // Could recurse on us with the wrong toggleQmlEvents flag, and not need in the !toggleQmlEvent case anyway. - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, enabled); - } - _enabled = enabled; // set the new value - + auto offscreenUi = DependencyManager::get(); + offscreenUi->setPinned(!_enabled); // if the new state is visible/enabled... - if (_enabled) { - // alpha fadeIn the overlay mesh. - qApp->getApplicationCompositor().fadeIn(); - - // enable mouse clicks from script - qApp->getOverlays().enable(); - - // enable QML events - if (toggleQmlEvents) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(true); - } - - if (_mode == STANDING) { - // place the overlay at the current hmd position in world space - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - Transform t; - t.setTranslation(extractTranslation(camMat)); - t.setRotation(glm::quat_cast(camMat)); - qApp->getApplicationCompositor().setModelTransform(t); - } - } else { // other wise, if the new state is hidden/not enabled - // alpha fadeOut the overlay mesh. - qApp->getApplicationCompositor().fadeOut(); - - // disable mouse clicks from script - qApp->getOverlays().disable(); - - // disable QML events - if (toggleQmlEvents) { // I'd really rather always do this, but it looses drive state. bugzid:501 - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(false); - } - } + if (_enabled && _mode == STANDING) { + // place the overlay at the current hmd position in world space + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); + qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); + } } bool OverlayConductor::getEnabled() const { diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 99f4b56584..1ec66663a4 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -17,7 +17,7 @@ public: ~OverlayConductor(); void update(float dt); - void setEnabled(bool enable, bool toggleQmlEvents = true); + void setEnabled(bool enable); bool getEnabled() const; private: diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index f9d527de8f..d4fff1b976 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -346,7 +346,7 @@ bool CompositorHelper::calculateRayUICollisionPoint(const glm::vec3& position, c auto relativePosition = vec3(relativePosition4) / relativePosition4.w; auto relativeDirection = glm::inverse(glm::quat_cast(UITransform)) * direction; - float uiRadius = _oculusUIRadius; // * myAvatar->getUniformScale(); // FIXME - how do we want to handle avatar scale + float uiRadius = _hmdUIRadius; // * myAvatar->getUniformScale(); // FIXME - how do we want to handle avatar scale float instersectionDistance; if (raySphereIntersect(relativeDirection, relativePosition, uiRadius, &instersectionDistance)){ @@ -407,60 +407,6 @@ void CompositorHelper::updateTooltips() { //} } -static const float FADE_DURATION = 500.0f; -static const float FADE_IN_ALPHA = 1.0f; -static const float FADE_OUT_ALPHA = 0.0f; - -void CompositorHelper::startFadeFailsafe(float endValue) { - _fadeStarted = usecTimestampNow(); - _fadeFailsafeEndValue = endValue; - - const int SLIGHT_DELAY = 10; - QTimer::singleShot(FADE_DURATION + SLIGHT_DELAY, [this]{ - checkFadeFailsafe(); - }); -} - -void CompositorHelper::checkFadeFailsafe() { - auto elapsedInFade = usecTimestampNow() - _fadeStarted; - if (elapsedInFade > FADE_DURATION) { - setAlpha(_fadeFailsafeEndValue); - } -} - -void CompositorHelper::fadeIn() { - _fadeInAlpha = true; - - _alphaPropertyAnimation->setDuration(FADE_DURATION); - _alphaPropertyAnimation->setStartValue(_alpha); - _alphaPropertyAnimation->setEndValue(FADE_IN_ALPHA); - _alphaPropertyAnimation->start(); - - // Sometimes, this "QPropertyAnimation" fails to complete the animation, and we end up with a partially faded - // state. So we will also have this fail-safe, where we record the timestamp of the fadeRequest, and the target - // value of the fade, and if after that time we still haven't faded all the way, we will kick it to the final - // fade value - startFadeFailsafe(FADE_IN_ALPHA); -} - -void CompositorHelper::fadeOut() { - _fadeInAlpha = false; - - _alphaPropertyAnimation->setDuration(FADE_DURATION); - _alphaPropertyAnimation->setStartValue(_alpha); - _alphaPropertyAnimation->setEndValue(FADE_OUT_ALPHA); - _alphaPropertyAnimation->start(); - startFadeFailsafe(FADE_OUT_ALPHA); -} - -void CompositorHelper::toggle() { - if (_fadeInAlpha) { - fadeOut(); - } else { - fadeIn(); - } -} - glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const glm::vec3& headPosition) const { glm::mat4 result; if (isHMD()) { diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index c0b53b329e..868beec53f 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -38,7 +38,6 @@ const float MAGNIFY_MULT = 2.0f; class CompositorHelper : public QObject, public Dependency { Q_OBJECT - Q_PROPERTY(float alpha READ getAlpha WRITE setAlpha) Q_PROPERTY(bool reticleOverDesktop READ getReticleOverDesktop WRITE setReticleOverDesktop) public: static const uvec2 VIRTUAL_SCREEN_SIZE; @@ -75,13 +74,6 @@ public: void setModelTransform(const Transform& transform) { _modelTransform = transform; } const Transform& getModelTransform() const { return _modelTransform; } - void fadeIn(); - void fadeOut(); - void toggle(); - - float getAlpha() const { return _alpha; } - void setAlpha(float alpha) { _alpha = alpha; } - bool getReticleVisible() const { return _reticleVisible; } void setReticleVisible(bool visible) { _reticleVisible = visible; } @@ -113,7 +105,7 @@ public: void setReticleOverDesktop(bool value) { _isOverDesktop = value; } void setDisplayPlugin(const DisplayPluginPointer& displayPlugin) { _currentDisplayPlugin = displayPlugin; } - void setFrameInfo(uint32_t frame, const glm::mat4& camera) { _currentCamera = camera; _currentFrame = frame; } + void setFrameInfo(uint32_t frame, const glm::mat4& camera) { _currentCamera = camera; } signals: void allowMouseCaptureChanged(); @@ -127,7 +119,6 @@ private: DisplayPluginPointer _currentDisplayPlugin; glm::mat4 _currentCamera; - uint32_t _currentFrame { 0 }; QWidget* _renderingWidget{ nullptr }; //// Support for hovering and tooltips @@ -143,17 +134,7 @@ private: float _textureFov { VIRTUAL_UI_TARGET_FOV.y }; float _textureAspectRatio { VIRTUAL_UI_ASPECT_RATIO }; - float _alpha { 1.0f }; - float _prevAlpha { 1.0f }; - float _fadeInAlpha { true }; - float _oculusUIRadius { 1.0f }; - - quint64 _fadeStarted { 0 }; - float _fadeFailsafeEndValue { 1.0f }; - void checkFadeFailsafe(); - void startFadeFailsafe(float endValue); - - int _reticleQuad; + float _hmdUIRadius { 1.0f }; int _previousBorderWidth { -1 }; int _previousBorderHeight { -1 }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index d34b698410..d9ee979777 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -445,25 +445,18 @@ void OpenGLDisplayPlugin::compositeOverlay() { useProgram(_program); // check the alpha - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - // Overlay draw - if (isStereo()) { - Uniform(*_program, _mvpUniform).Set(mat4()); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); - }); - } else { - // Overlay draw - Uniform(*_program, _mvpUniform).Set(mat4()); + // Overlay draw + if (isStereo()) { + Uniform(*_program, _mvpUniform).Set(mat4()); + for_each_eye([&](Eye eye) { + eyeViewport(eye); drawUnitQuad(); - } + }); + } else { + // Overlay draw + Uniform(*_program, _mvpUniform).Set(mat4()); + drawUnitQuad(); } - Uniform(*_program, _alphaUniform).Set(1.0); } void OpenGLDisplayPlugin::compositePointer() { @@ -471,24 +464,16 @@ void OpenGLDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); useProgram(_program); - // check the alpha - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); - if (isStereo()) { - for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); - }); - } else { + Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); + if (isStereo()) { + for_each_eye([&](Eye eye) { + eyeViewport(eye); drawUnitQuad(); - } + }); + } else { + drawUnitQuad(); } Uniform(*_program, _mvpUniform).Set(mat4()); - Uniform(*_program, _alphaUniform).Set(1.0); } void OpenGLDisplayPlugin::compositeScene() { diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 4e594d89ed..1616dcdb77 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -254,23 +254,15 @@ void HmdDisplayPlugin::compositeOverlay() { using namespace oglplus; auto compositorHelper = DependencyManager::get(); - // check the alpha useProgram(_program); - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - _sphereSection->Use(); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)); - auto mvp = _eyeProjections[eye] * modelView; - Uniform(*_program, _mvpUniform).Set(mvp); - _sphereSection->Draw(); - }); - } - Uniform(*_program, _alphaUniform).Set(1.0); + _sphereSection->Use(); + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)); + auto mvp = _eyeProjections[eye] * modelView; + Uniform(*_program, _mvpUniform).Set(mvp); + _sphereSection->Draw(); + }); } void HmdDisplayPlugin::compositePointer() { @@ -280,25 +272,19 @@ void HmdDisplayPlugin::compositePointer() { // check the alpha useProgram(_program); - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - // Mouse pointer - _plane->Use(); - // Reconstruct the headpose from the eye poses - auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); - auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); - auto mvp = _eyeProjections[eye] * reticleTransform; - Uniform(*_program, _mvpUniform).Set(mvp); - _plane->Draw(); - }); - } - Uniform(*_program, _alphaUniform).Set(1.0); + // Mouse pointer + _plane->Use(); + // Reconstruct the headpose from the eye poses + auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); + auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); + auto mvp = _eyeProjections[eye] * reticleTransform; + Uniform(*_program, _mvpUniform).Set(mvp); + _plane->Draw(); + }); } void HmdDisplayPlugin::internalPresent() { diff --git a/libraries/ui/src/ErrorDialog.cpp b/libraries/ui/src/ErrorDialog.cpp index ab36ef8d36..fcd73b4cc0 100644 --- a/libraries/ui/src/ErrorDialog.cpp +++ b/libraries/ui/src/ErrorDialog.cpp @@ -22,10 +22,6 @@ QString ErrorDialog::text() const { return _text; } -void ErrorDialog::setVisible(bool v) { - OffscreenQmlDialog::setVisible(v); -} - void ErrorDialog::setText(const QString& arg) { if (arg != _text) { _text = arg; diff --git a/libraries/ui/src/ErrorDialog.h b/libraries/ui/src/ErrorDialog.h index 665090da1a..38954714a7 100644 --- a/libraries/ui/src/ErrorDialog.h +++ b/libraries/ui/src/ErrorDialog.h @@ -30,7 +30,6 @@ public: QString text() const; public slots: - virtual void setVisible(bool v); void setText(const QString& arg); signals: diff --git a/libraries/ui/src/OffscreenQmlDialog.cpp b/libraries/ui/src/OffscreenQmlDialog.cpp index 43514c4761..2d1ca20876 100644 --- a/libraries/ui/src/OffscreenQmlDialog.cpp +++ b/libraries/ui/src/OffscreenQmlDialog.cpp @@ -17,7 +17,7 @@ OffscreenQmlDialog::~OffscreenQmlDialog() { } void OffscreenQmlDialog::hide() { - static_cast(parent())->setVisible(false); + parent()->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, false); } QString OffscreenQmlDialog::title() const { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index dfd9056703..fa1a31d196 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -121,32 +121,28 @@ void OffscreenUi::show(const QUrl& url, const QString& name, std::functionfindChild(name); } + if (item) { - item->setVisible(true); + QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).write(true); } } void OffscreenUi::toggle(const QUrl& url, const QString& name, std::function f) { QQuickItem* item = getRootItem()->findChild(name); - // Already loaded? - if (item) { - emit showDesktop(); - item->setVisible(!item->isVisible()); + if (!item) { + show(url, name, f); return; } - load(url, f); - item = getRootItem()->findChild(name); - if (item && !item->isVisible()) { - emit showDesktop(); - item->setVisible(true); - } + // Already loaded, so just flip the bit + QQmlProperty shownProperty(item, OFFSCREEN_VISIBILITY_PROPERTY); + shownProperty.write(!shownProperty.read().toBool()); } void OffscreenUi::hide(const QString& name) { QQuickItem* item = getRootItem()->findChild(name); if (item) { - item->setVisible(false); + QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).write(false); } } @@ -345,6 +341,20 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q return waitForInputDialogResult(createInputDialog(icon, title, label, current)); } +void OffscreenUi::togglePinned() { + bool invokeResult = QMetaObject::invokeMethod(_desktop, "togglePinned"); + if (!invokeResult) { + qWarning() << "Failed to toggle window visibility"; + } +} + +void OffscreenUi::setPinned(bool pinned) { + bool invokeResult = QMetaObject::invokeMethod(_desktop, "setPinned", Q_ARG(QVariant, pinned)); + if (!invokeResult) { + qWarning() << "Failed to set window visibility"; + } +} + void OffscreenUi::addMenuInitializer(std::function f) { if (!_vrMenu) { _queuedMenuInitializers.push_back(f); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 5a16b49491..e1d552c978 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -28,6 +28,8 @@ class VrMenu; +#define OFFSCREEN_VISIBILITY_PROPERTY "shown" + class OffscreenUi : public OffscreenQmlSurface, public Dependency { Q_OBJECT @@ -44,6 +46,13 @@ public: void setNavigationFocused(bool focused); void unfocusWindows(); void toggleMenu(const QPoint& screenCoordinates); + + + // Setting pinned to true will hide all overlay elements on the desktop that don't have a pinned flag + void setPinned(bool pinned = true); + + void togglePinned(); + bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 396d716cda..b8834f0549 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -163,8 +163,7 @@ void QmlWindowClass::setVisible(bool visible) { QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible)); } else { DependencyManager::get()->executeOnUiThread([=] { - targetWindow->setVisible(visible); - //emit visibilityChanged(visible); + targetWindow->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); }); } } diff --git a/libraries/ui/src/Tooltip.cpp b/libraries/ui/src/Tooltip.cpp index 3c0902b378..94e04f34b6 100644 --- a/libraries/ui/src/Tooltip.cpp +++ b/libraries/ui/src/Tooltip.cpp @@ -47,10 +47,6 @@ void Tooltip::setImageURL(const QString& imageURL) { } } -void Tooltip::setVisible(bool visible) { - QQuickItem::setVisible(visible); -} - QString Tooltip::showTip(const QString& title, const QString& description) { const QString newTipId = QUuid().createUuid().toString(); diff --git a/libraries/ui/src/Tooltip.h b/libraries/ui/src/Tooltip.h index d1c7330a74..5e884a7aea 100644 --- a/libraries/ui/src/Tooltip.h +++ b/libraries/ui/src/Tooltip.h @@ -39,8 +39,6 @@ public: static void closeTip(const QString& tipId); public slots: - virtual void setVisible(bool v); - void setTitle(const QString& title); void setDescription(const QString& description); void setImageURL(const QString& imageURL); diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 97f0c0a613..3b2cc264c8 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -5,7 +5,7 @@ import Qt.labs.settings 1.0 import "../../../interface/resources/qml" //import "../../../interface/resources/qml/windows" -import "../../../interface/resources/qml/windows-uit" +import "../../../interface/resources/qml/windows" import "../../../interface/resources/qml/dialogs" import "../../../interface/resources/qml/hifi" import "../../../interface/resources/qml/hifi/dialogs" @@ -17,6 +17,267 @@ ApplicationWindow { width: 1280 height: 800 title: qsTr("Scratch App") + toolBar: Row { + id: testButtons + anchors { margins: 8; left: parent.left; top: parent.top } + spacing: 8 + property int count: 0 + + property var tabs: []; + property var urls: []; + + // Window visibility + + Button { + text: "restore all" + onClicked: { + for (var i = 0; i < desktop.windows.length; ++i) { + desktop.windows[i].shown = true + } + } + } + Button { + text: "toggle blue visible" + onClicked: { + blue.shown = !blue.shown + } + } + Button { + text: "toggle blue enabled" + onClicked: { + blue.enabled = !blue.enabled + } + } + + Button { + text: "toggle desktop" + onClicked: desktop.toggleVisible() + } + + // Error alerts + /* + Button { + // Message without title. + text: "Show Error" + onClicked: { + var messageBox = desktop.messageBox({ + text: "Diagnostic cycle will be complete in 30 seconds", + icon: hifi.icons.critical, + }); + messageBox.selected.connect(function(button) { + console.log("You clicked " + button) + }) + } + } + Button { + // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. + text: "Show Long Error" + onClicked: { + desktop.messageBox({ + informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", + text: "Baloney", + icon: hifi.icons.warning, + detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" + }); + } + } + */ + + // query + /* + // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? + Button { + text: "Show Query" + onClicked: { + var queryBox = desktop.queryBox({ + text: "Have you stopped beating your wife?", + placeholderText: "Are you sure?", + // icon: hifi.icons.critical, + }); + queryBox.selected.connect(function(result) { + console.log("User responded with " + result); + }); + + queryBox.canceled.connect(function() { + console.log("User cancelled query box "); + }) + } + } + */ + + // file dialog + + Button { + text: "Open Directory" + property var builder: Component { + FileDialog { selectDirectory: true } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + Button { + text: "Open File" + property var builder: Component { + FileDialog { + title: "Open File" + filter: "All Files (*.*)" + //filter: "HTML files (*.html);;Other(*.png)" + } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + /* + */ + + // tabs + /* + Button { + text: "Add Tab" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo" }); + desktop.toolWindow.showTabForUrl("Foo", true); + } + } + + Button { + text: "Add Tab 2" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo 2" }); + desktop.toolWindow.showTabForUrl("Foo 2", true); + } + } + + Button { + text: "Add Tab 3" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo 3" }); + desktop.toolWindow.showTabForUrl("Foo 3", true); + } + } + + Button { + text: "Destroy Tab" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.removeTabForUrl("Foo"); + } + } + */ + + // Hifi specific stuff + /* + Button { + // Shows the dialog with preferences sections but not each section's preference items + // because Preferences.preferencesByCategory() method is not stubbed out. + text: "Settings > General..." + property var builder: Component { + GeneralPreferencesDialog { } + } + onClicked: { + var runningScripts = builder.createObject(desktop); + } + } + + Button { + text: "Running Scripts" + property var builder: Component { + RunningScripts { } + } + onClicked: { + var runningScripts = builder.createObject(desktop); + } + } + + Button { + text: "Attachments" + property var builder: Component { + AttachmentsDialog { } + } + onClicked: { + var attachmentsDialog = builder.createObject(desktop); + } + } + Button { + // Replicates message box that pops up after selecting new avatar. Includes title. + text: "Confirm Avatar" + onClicked: { + var messageBox = desktop.messageBox({ + title: "Set Avatar", + text: "Would you like to use 'Albert' for your avatar?", + icon: hifi.icons.question, // Test question icon + //icon: hifi.icons.information, // Test informaton icon + //icon: hifi.icons.warning, // Test warning icon + //icon: hifi.icons.critical, // Test critical icon + //icon: hifi.icons.none, // Test no icon + buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, + defaultButton: OriginalDialogs.StandardButton.Ok + }); + messageBox.selected.connect(function(button) { + console.log("You clicked " + button) + }) + } + } + */ + // bookmarks + /* + Button { + text: "Bookmark Location" + onClicked: { + desktop.inputDialog({ + title: "Bookmark Location", + icon: hifi.icons.placemark, + label: "Name" + }); + } + } + Button { + text: "Delete Bookmark" + onClicked: { + desktop.inputDialog({ + title: "Delete Bookmark", + icon: hifi.icons.placemark, + label: "Select the bookmark to delete", + items: ["Bookmark A", "Bookmark B", "Bookmark C"] + }); + } + } + Button { + text: "Duplicate Bookmark" + onClicked: { + desktop.messageBox({ + title: "Duplicate Bookmark", + icon: hifi.icons.warning, + text: "The bookmark name you entered alread exists in yoru list.", + informativeText: "Would you like to overwrite it?", + buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.Yes + }); + } + } + */ + + } + HifiConstants { id: hifi } @@ -35,249 +296,12 @@ ApplicationWindow { } */ - Row { - id: testButtons - anchors { margins: 8; left: parent.left; top: parent.top } - spacing: 8 - property int count: 0 - - property var tabs: []; - property var urls: []; - - Button { - // Shows the dialog with preferences sections but not each section's preference items - // because Preferences.preferencesByCategory() method is not stubbed out. - text: "Settings > General..." - property var builder: Component { - GeneralPreferencesDialog { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Running Scripts" - property var builder: Component { - RunningScripts { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Attachments" - property var builder: Component { - AttachmentsDialog { } - } - onClicked: { - var attachmentsDialog = builder.createObject(desktop); - } - } - - /* - Button { - text: "restore all" - onClicked: { - for (var i = 0; i < desktop.windows.length; ++i) { - desktop.windows[i].visible = true - } - } - } - Button { - text: "toggle blue visible" - onClicked: { - blue.visible = !blue.visible - } - } - Button { - text: "toggle blue enabled" - onClicked: { - blue.enabled = !blue.enabled - } - } - */ - Button { - // Replicates message box that pops up after selecting new avatar. Includes title. - text: "Confirm Avatar" - onClicked: { - var messageBox = desktop.messageBox({ - title: "Set Avatar", - text: "Would you like to use 'Albert' for your avatar?", - icon: hifi.icons.question, // Test question icon - //icon: hifi.icons.information, // Test informaton icon - //icon: hifi.icons.warning, // Test warning icon - //icon: hifi.icons.critical, // Test critical icon - //icon: hifi.icons.none, // Test no icon - buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, - defaultButton: OriginalDialogs.StandardButton.Ok - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // Message without title. - text: "Show Error" - onClicked: { - var messageBox = desktop.messageBox({ - text: "Diagnostic cycle will be complete in 30 seconds", - icon: hifi.icons.critical, - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. - text: "Show Long Error" - onClicked: { - desktop.messageBox({ - informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", - text: "Baloney", - icon: hifi.icons.warning, - detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" - }); - } - } - Button { - text: "Bookmark Location" - onClicked: { - desktop.inputDialog({ - title: "Bookmark Location", - icon: hifi.icons.placemark, - label: "Name" - }); - } - } - Button { - text: "Delete Bookmark" - onClicked: { - desktop.inputDialog({ - title: "Delete Bookmark", - icon: hifi.icons.placemark, - label: "Select the bookmark to delete", - items: ["Bookmark A", "Bookmark B", "Bookmark C"] - }); - } - } - Button { - text: "Duplicate Bookmark" - onClicked: { - desktop.messageBox({ - title: "Duplicate Bookmark", - icon: hifi.icons.warning, - text: "The bookmark name you entered alread exists in yoru list.", - informativeText: "Would you like to overwrite it?", - buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, - defaultButton: OriginalDialogs.StandardButton.Yes - }); - } - } - /* - // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? - Button { - text: "Show Query" - onClicked: { - var queryBox = desktop.queryBox({ - text: "Have you stopped beating your wife?", - placeholderText: "Are you sure?", - // icon: hifi.icons.critical, - }); - queryBox.selected.connect(function(result) { - console.log("User responded with " + result); - }); - - queryBox.canceled.connect(function() { - console.log("User cancelled query box "); - }) - } - } - */ - Button { - text: "Open Directory" - property var builder: Component { - FileDialog { selectDirectory: true } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - - Button { - text: "Open File" - property var builder: Component { - FileDialog { - title: "Open File" - filter: "All Files (*.*)" - //filter: "HTML files (*.html);;Other(*.png)" - } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - - Button { - text: "Add Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo" }); - desktop.toolWindow.showTabForUrl("Foo", true); - } - } - - Button { - text: "Add Tab 2" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 2" }); - desktop.toolWindow.showTabForUrl("Foo 2", true); - } - } - - Button { - text: "Add Tab 3" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 3" }); - desktop.toolWindow.showTabForUrl("Foo 3", true); - } - } - - Button { - text: "Destroy Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.removeTabForUrl("Foo"); - } - } - - } - /* Window { id: blue closable: true visible: true resizable: true - destroyOnInvisible: false + destroyOnHidden: false width: 100; height: 100 x: 1280 / 2; y: 720 / 2 @@ -296,32 +320,33 @@ ApplicationWindow { Component.onDestruction: console.log("Blue destroyed") } } - */ - /* + + + Rectangle { width: 100; height: 100; x: 100; y: 100; color: "#00f" } + Window { id: green alwaysOnTop: true + frame: HiddenFrame{} + hideBackground: true closable: true visible: true resizable: false x: 1280 / 2; y: 720 / 2 - Settings { - category: "TestWindow.Green" - property alias x: green.x - property alias y: green.y - property alias width: green.width - property alias height: green.height - } width: 100; height: 100 - Rectangle { anchors.fill: parent; color: "green" } + Rectangle { + color: "#0f0" + width: green.width; + height: green.height; + } } - +/* Window { id: yellow - objectName: "Yellow" closable: true visible: true resizable: true + x: 100; y: 100 width: 100; height: 100 Rectangle { anchors.fill: parent @@ -329,7 +354,7 @@ ApplicationWindow { color: "yellow" } } - */ +*/ } Action { diff --git a/tests/ui/qmlscratch.pro b/tests/ui/qmlscratch.pro index 417d7dad5b..95be6a480e 100644 --- a/tests/ui/qmlscratch.pro +++ b/tests/ui/qmlscratch.pro @@ -18,6 +18,7 @@ DISTFILES += \ qml/*.qml \ ../../interface/resources/qml/*.qml \ ../../interface/resources/qml/controls/*.qml \ + ../../interface/resources/qml/controls-uit/*.qml \ ../../interface/resources/qml/dialogs/*.qml \ ../../interface/resources/qml/dialogs/fileDialog/*.qml \ ../../interface/resources/qml/dialogs/preferences/*.qml \ @@ -25,9 +26,9 @@ DISTFILES += \ ../../interface/resources/qml/desktop/*.qml \ ../../interface/resources/qml/menus/*.qml \ ../../interface/resources/qml/styles/*.qml \ + ../../interface/resources/qml/styles-uit/*.qml \ ../../interface/resources/qml/windows/*.qml \ ../../interface/resources/qml/hifi/*.qml \ ../../interface/resources/qml/hifi/dialogs/*.qml \ ../../interface/resources/qml/hifi/dialogs/preferences/*.qml \ ../../interface/resources/qml/hifi/overlays/*.qml - From ed5d904b3d87b69c39fa2812dcd5e47623a6b28c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 2 Jun 2016 09:50:58 -0700 Subject: [PATCH 0317/1237] Hide overlays when driving --- interface/resources/qml/desktop/Desktop.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 73f8a17bb0..c3b1b0ad6e 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -264,6 +264,10 @@ FocusScope { pinned = !pinned } + function setPinned(newPinned) { + pinned = newPinned + } + onPinnedChanged: { if (pinned) { From 854bf82631664b329bf2682c093cb5bcda87aaf2 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 2 Jun 2016 17:37:36 -0700 Subject: [PATCH 0318/1237] Raise and stretch collision sounds threshold --- interface/src/avatar/AvatarManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index ddadcb3909..567602cd34 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -322,7 +322,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents const auto characterController = myAvatar->getCharacterController(); const float avatarVelocityChange = (characterController ? glm::length(characterController->getVelocityChange()) : 0.0f); const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange; - const float MIN_AVATAR_COLLISION_ACCELERATION = 0.01f; + const float MIN_AVATAR_COLLISION_ACCELERATION = 0.3f; // ~ 1km/h ==> sound volume = 0.09 const bool isSound = (collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION); if (!isSound) { @@ -330,7 +330,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents } // Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for. const float energy = velocityChange * velocityChange; - const float COLLISION_ENERGY_AT_FULL_VOLUME = 0.5f; + const float COLLISION_ENERGY_AT_FULL_VOLUME = 1.0; const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME); // For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object, From 7ed35399e9e6405f511102c46e268374ccdd9145 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 2 Jun 2016 18:01:23 -0700 Subject: [PATCH 0319/1237] make the application of redundant physics update idempotent to improve action glitches --- libraries/entities/src/EntityItem.cpp | 95 ++++++++++++++++++++++----- libraries/entities/src/EntityItem.h | 16 +++++ 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index dc5fbf7d8e..64b6a2c655 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -687,15 +687,80 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } } { // When we own the simulation we don't accept updates to the entity's transform/velocities - // but since we're using macros below we have to temporarily modify overwriteLocalData. - bool oldOverwrite = overwriteLocalData; - overwriteLocalData = overwriteLocalData && !weOwnSimulation; - READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, updatePositionFromNetwork); - READ_ENTITY_PROPERTY(PROP_ROTATION, glm::quat, updateRotationFromNetwork); - READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, updateVelocityFromNetwork); - READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocityFromNetwork); - READ_ENTITY_PROPERTY(PROP_ACCELERATION, glm::vec3, setAcceleration); - overwriteLocalData = oldOverwrite; + // we also want to ignore any duplicate packets that have the same "recently updated" values + // as a packet we've already recieved. This is because we want multiple edits of the same + // information to be idempotent, but if we applied new physics properties we'd resimulation + // with small differences in results. + + // Because the regular streaming property "setters" only have access to the new value, we've + // made these lambdas that can access other details about the previous updates to suppress + // any duplicates. + + // Note: duplicate packets are expected and not wrong. They may be sent for any number of + // reasons and the contract is that the client handles them in an idempotent manner. + auto lastEdited = lastEditedFromBufferAdjusted; + auto customUpdatePositionFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + bool simulationChanged = lastEdited > _lastUpdatedPositionTimestamp; + bool valueChanged = value != _lastUpdatedPositionValue; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + if (shouldUpdate) { + updatePositionFromNetwork(value); + _lastUpdatedPositionTimestamp = lastEdited; + _lastUpdatedPositionValue = value; + } + }; + + auto customUpdateRotationFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::quat value){ + bool simulationChanged = lastEdited > _lastUpdatedRotationTimestamp; + bool valueChanged = value != _lastUpdatedRotationValue; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + if (shouldUpdate) { + updateRotationFromNetwork(value); + _lastUpdatedRotationTimestamp = lastEdited; + _lastUpdatedRotationValue = value; + } + }; + + auto customUpdateVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + bool simulationChanged = lastEdited > _lastUpdatedVelocityTimestamp; + bool valueChanged = value != _lastUpdatedVelocityValue; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + if (shouldUpdate) { + updateVelocityFromNetwork(value); + _lastUpdatedVelocityTimestamp = lastEdited; + _lastUpdatedVelocityValue = value; + } + }; + + auto customUpdateAngularVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + bool simulationChanged = lastEdited > _lastUpdatedAngularVelocityTimestamp; + bool valueChanged = value != _lastUpdatedAngularVelocityValue; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + if (shouldUpdate) { + updateAngularVelocityFromNetwork(value); + _lastUpdatedAngularVelocityTimestamp = lastEdited; + _lastUpdatedAngularVelocityValue = value; + } + }; + + auto customSetAcceleration = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + bool simulationChanged = lastEdited > _lastUpdatedAccelerationTimestamp; + bool valueChanged = value != _lastUpdatedAccelerationValue; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + if (shouldUpdate) { + setAcceleration(value); + _lastUpdatedAccelerationTimestamp = lastEdited; + _lastUpdatedAccelerationValue = value; + } + }; + + READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, customUpdatePositionFromNetwork); + READ_ENTITY_PROPERTY(PROP_ROTATION, glm::quat, customUpdateRotationFromNetwork); + READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, customUpdateVelocityFromNetwork); + READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, customUpdateAngularVelocityFromNetwork); + READ_ENTITY_PROPERTY(PROP_ACCELERATION, glm::vec3, customSetAcceleration); + + } READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, updateDimensions); @@ -922,13 +987,11 @@ void EntityItem::simulate(const quint64& now) { qCDebug(entities) << " ********** EntityItem::simulate() .... SETTING _lastSimulated=" << _lastSimulated; #endif - if (!hasActions()) { - if (!stepKinematicMotion(timeElapsed)) { - // this entity is no longer moving - // flag it to transition from KINEMATIC to STATIC - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; - setAcceleration(Vectors::ZERO); - } + if (!stepKinematicMotion(timeElapsed)) { + // this entity is no longer moving + // flag it to transition from KINEMATIC to STATIC + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + setAcceleration(Vectors::ZERO); } _lastSimulated = now; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 2c6dc9b74d..96297a09ce 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -550,6 +550,22 @@ protected: bool _clientOnly { false }; QUuid _owningAvatarID; + + // physics related changes from the network to suppress any duplicates and make + // sure redundant applications are idempotent + glm::vec3 _lastUpdatedPositionValue; + glm::quat _lastUpdatedRotationValue; + glm::vec3 _lastUpdatedVelocityValue; + glm::vec3 _lastUpdatedAngularVelocityValue; + glm::vec3 _lastUpdatedAccelerationValue; + + quint64 _lastUpdatedPositionTimestamp = 0; + quint64 _lastUpdatedRotationTimestamp = 0; + quint64 _lastUpdatedVelocityTimestamp = 0; + quint64 _lastUpdatedAngularVelocityTimestamp = 0; + quint64 _lastUpdatedAccelerationTimestamp = 0; + + }; #endif // hifi_EntityItem_h From 912b35693b32f5302bbdf32a7b9bac531341dc47 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 2 Jun 2016 18:13:33 -0700 Subject: [PATCH 0320/1237] added vive single pulse haptics --- plugins/openvr/src/ViveControllerManager.cpp | 18 ++++++++++++++++++ plugins/openvr/src/ViveControllerManager.h | 2 ++ 2 files changed, 20 insertions(+) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 6e75454b5f..6b19646512 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -442,6 +442,24 @@ void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = avatarPose.transform(controllerToAvatar); } +// Vive Controllers do not support duration +bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, bool leftHand) { + auto handRole = leftHand ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand; + auto deviceIndex = _system->GetTrackedDeviceIndexForControllerRole(handRole); + + if (_system->IsTrackedDeviceConnected(deviceIndex) && + _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && + _trackedDevicePose[deviceIndex].bPoseIsValid) { + // the documentation says the third argument to TriggerHapticPulse is duration + // but it seems to instead be strength, and is between 0 and 3999 + // https://github.com/ValveSoftware/openvr/wiki/IVRSystem::TriggerHapticPulse + const float MAX_HAPTIC_STRENGTH = 3999.0f; + _system->TriggerHapticPulse(deviceIndex, 0, strength*MAX_HAPTIC_STRENGTH); + return true; + } + return false; +} + controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableInputs() const { using namespace controller; QVector availableInputs{ diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index bd5d4a39f4..c108d3087f 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -56,6 +56,8 @@ private: void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; + bool triggerHapticPulse(float strength, float duration, bool leftHand) override; + void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); void handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand); void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand); From f2e5b6056fa996d8a08483e199794b5423554f79 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 3 Jun 2016 14:03:19 +1200 Subject: [PATCH 0321/1237] Make table switches smaller --- domain-server/resources/web/settings/js/settings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 44b2c692d2..3ddf9722fb 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -982,7 +982,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { if (isArray && col.type === "checkbox" && col.editable) { html += ""; } else { // Use a hidden input so that the values are posted. @@ -1037,8 +1037,8 @@ function makeTableInputs(setting) { _.each(setting.columns, function(col) { if (col.type === "checkbox") { html += ""; + + ""; } else { html += ""; } else { // Use a hidden input so that the values are posted. @@ -1037,7 +1039,7 @@ function makeTableInputs(setting) { _.each(setting.columns, function(col) { if (col.type === "checkbox") { html += ""; } else { html += "
" + rowIndexOrName + "" + "" + "
" - + "" - + "\ Date: Thu, 2 Jun 2016 19:12:37 -0700 Subject: [PATCH 0322/1237] Limit collision injectors count to 3 --- interface/src/avatar/AvatarManager.cpp | 12 +++++++++++- interface/src/avatar/AvatarManager.h | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 567602cd34..72deb6340a 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -337,7 +337,17 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // but most avatars are roughly the same size, so let's not be so fancy yet. const float AVATAR_STRETCH_FACTOR = 1.0f; - AudioInjector::playSound(collisionSound, energyFactorOfFull, AVATAR_STRETCH_FACTOR, myAvatar->getPosition()); + + _collisionInjectors.remove_if([](QPointer& injector) { + return !injector || injector->isFinished(); + }); + + static const int MAX_INJECTOR_COUNT = 3; + if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) { + auto injector = AudioInjector::playSound(collisionSound, energyFactorOfFull, AVATAR_STRETCH_FACTOR, + myAvatar->getPosition()); + _collisionInjectors.emplace_back(injector); + } myAvatar->collisionWithEntity(collision); return; } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 1cd295d69f..9be186301d 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -25,6 +25,7 @@ #include "AvatarMotionState.h" class MyAvatar; +class AudioInjector; class AvatarManager : public AvatarHashMap { Q_OBJECT @@ -94,6 +95,8 @@ private: bool _shouldShowReceiveStats = false; + std::list> _collisionInjectors; + SetOfAvatarMotionStates _motionStatesThatMightUpdate; SetOfMotionStates _motionStatesToAddToPhysics; VectorOfMotionStates _motionStatesToRemoveFromPhysics; From e9b359feb1afcb7ecbc27edd15fa1c8859355422 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 2 Jun 2016 19:15:19 -0700 Subject: [PATCH 0323/1237] Make QML overlays disappear, use fade for non-window types --- interface/resources/qml/desktop/Desktop.qml | 39 ++++++++++++++----- .../resources/qml/windows/ModalWindow.qml | 2 +- scripts/system/libraries/toolBars.js | 2 +- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index c3b1b0ad6e..241cd91611 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -268,24 +268,45 @@ FocusScope { pinned = newPinned } - onPinnedChanged: { + property real unpinnedAlpha: 1.0; + Behavior on unpinnedAlpha { + NumberAnimation { + easing.type: Easing.Linear; + duration: 300 + } + } + + state: "NORMAL" + states: [ + State { + name: "NORMAL" + PropertyChanges { target: desktop; unpinnedAlpha: 1.0 } + }, + State { + name: "PINNED" + PropertyChanges { target: desktop; unpinnedAlpha: 0.0 } + } + ] + + transitions: [ + Transition { + NumberAnimation { properties: "unpinnedAlpha"; duration: 300 } + } + ] + + onPinnedChanged: { if (pinned) { + // recalculate our non-pinned children hiddenChildren = d.findMatchingChildren(desktop, function(child){ return !d.isTopLevelWindow(child) && child.visible; }); hiddenChildren.forEach(function(child){ - child.visible = false; + child.opacity = Qt.binding(function(){ return desktop.unpinnedAlpha }); }); - } else { - hiddenChildren.forEach(function(child){ - if (child) { - child.visible = true; - } - }); - hiddenChildren = []; } + state = pinned ? "PINNED" : "NORMAL" } onShowDesktop: pinned = false diff --git a/interface/resources/qml/windows/ModalWindow.qml b/interface/resources/qml/windows/ModalWindow.qml index 144165e4e1..f79dc9084f 100644 --- a/interface/resources/qml/windows/ModalWindow.qml +++ b/interface/resources/qml/windows/ModalWindow.qml @@ -16,7 +16,7 @@ Window { id: window modality: Qt.ApplicationModal destroyOnCloseButton: true - destroyOnInvisible: true + destroyOnHidden: true frame: ModalFrame { } property int colorScheme: hifi.colorSchemes.light diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index 9efe533457..f021f456bf 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -256,7 +256,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit y: y - ToolBar.SPACING }); } - this.save(); + //this.save(); } this.setAlpha = function(alpha, tool) { From e2fa7340240f2beee7b2fc31f35f95c7691d8810 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 2 Jun 2016 19:15:55 -0700 Subject: [PATCH 0324/1237] Set collision threashold to walking speed --- interface/src/avatar/AvatarManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 72deb6340a..68d2eca2c0 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -322,7 +322,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents const auto characterController = myAvatar->getCharacterController(); const float avatarVelocityChange = (characterController ? glm::length(characterController->getVelocityChange()) : 0.0f); const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange; - const float MIN_AVATAR_COLLISION_ACCELERATION = 0.3f; // ~ 1km/h ==> sound volume = 0.09 + const float MIN_AVATAR_COLLISION_ACCELERATION = 2.4f; // walking speed const bool isSound = (collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION); if (!isSound) { @@ -330,7 +330,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents } // Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for. const float energy = velocityChange * velocityChange; - const float COLLISION_ENERGY_AT_FULL_VOLUME = 1.0; + const float COLLISION_ENERGY_AT_FULL_VOLUME = 10.0f; const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME); // For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object, From 4e49f3f94297a54d8021cca6cf77325be1cef855 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 2 Jun 2016 19:54:25 -0700 Subject: [PATCH 0325/1237] Fix browser and update dialogs --- interface/resources/qml/Browser.qml | 67 ++++++++++++++---------- interface/resources/qml/UpdateDialog.qml | 4 +- tests/ui/qml/main.qml | 11 ++++ 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index efa2063fe8..01e1bb7680 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -2,8 +2,8 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebEngine 1.1 -import "controls" -import "styles" +import "controls-uit" +import "styles-uit" import "windows" Window { @@ -15,7 +15,9 @@ Window { width: 800 height: 600 property alias webView: webview - + x: 100 + y: 100 + Component.onCompleted: { shown = true addressBar.text = webview.url @@ -30,15 +32,9 @@ Window { Item { id:item - anchors.fill: parent - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: webview.top - color: "white" - } - + width: pane.contentWidth + implicitHeight: pane.scrollHeight + Row { id: buttons spacing: 4 @@ -46,25 +42,37 @@ Window { anchors.topMargin: 8 anchors.left: parent.left anchors.leftMargin: 8 - FontAwesome { - id: back; text: "\uf0a8"; size: 48; enabled: webview.canGoBack; + HiFiGlyphs { + id: back; + enabled: webview.canGoBack; + text: hifi.glyphs.backward color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goBack() } } - FontAwesome { - id: forward; text: "\uf0a9"; size: 48; enabled: webview.canGoForward; + + HiFiGlyphs { + id: forward; + enabled: webview.canGoForward; + text: hifi.glyphs.forward color: enabled ? hifi.colors.text : hifi.colors.disabledText - MouseArea { anchors.fill: parent; onClicked: webview.goBack() } + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } - FontAwesome { - id: reload; size: 48; text: webview.loading ? "\uf057" : "\uf021" - MouseArea { anchors.fill: parent; onClicked: webview.loading ? webview.stop() : webview.reload() } + + HiFiGlyphs { + id: reload; + enabled: webview.canGoForward; + text: webview.loading ? hifi.glyphs.close : hifi.glyphs.reload + color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } } - Border { + Item { + id: border height: 48 - radius: 8 anchors.top: parent.top anchors.topMargin: 8 anchors.right: parent.right @@ -86,15 +94,18 @@ Window { onSourceChanged: console.log("Icon url: " + source) } } - - TextInput { + + TextField { id: addressBar anchors.right: parent.right anchors.rightMargin: 8 anchors.left: barIcon.right anchors.leftMargin: 0 anchors.verticalCenter: parent.verticalCenter - + focus: true + colorScheme: hifi.colorSchemes.dark + placeholderText: "Enter URL" + Component.onCompleted: scriptsModel.filterRegExp = new RegExp("^.*$", "i") Keys.onPressed: { switch(event.key) { case Qt.Key_Enter: @@ -110,7 +121,7 @@ Window { } } - WebView { + WebEngineView { id: webview url: "http://highfidelity.com" anchors.top: buttons.bottom @@ -119,7 +130,7 @@ Window { anchors.left: parent.left anchors.right: parent.right onLoadingChanged: { - if (loadRequest.status == WebEngineView.LoadSucceededStatus) { + if (loadRequest.status === WebEngineView.LoadSucceededStatus) { addressBar.text = loadRequest.url } } @@ -127,7 +138,7 @@ Window { console.log("New icon: " + icon) } - profile: desktop.browserProfile + //profile: desktop.browserProfile } diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 4cb5b206c6..52fdab25f9 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -3,8 +3,8 @@ import QtQuick 2.3 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 import QtGraphicalEffects 1.0 -import "controls" -import "styles" +import "controls-uit" +import "styles-uit" import "windows" Window { diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 3b2cc264c8..d752734de4 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -105,6 +105,16 @@ ApplicationWindow { } */ + // Browser + + Button { + text: "Open Browser" + onClicked: builder.createObject(desktop); + property var builder: Component { + Browser {} + } + } + // file dialog Button { @@ -358,6 +368,7 @@ ApplicationWindow { } Action { + id: openBrowserAction text: "Open Browser" shortcut: "Ctrl+Shift+X" onTriggered: { From 1075ba1599160728f35ea40c4616873f95ebf636 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 3 Jun 2016 08:01:09 -0700 Subject: [PATCH 0326/1237] CR feedback --- libraries/entities/src/EntityItem.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 96297a09ce..4a691462ab 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -559,11 +559,11 @@ protected: glm::vec3 _lastUpdatedAngularVelocityValue; glm::vec3 _lastUpdatedAccelerationValue; - quint64 _lastUpdatedPositionTimestamp = 0; - quint64 _lastUpdatedRotationTimestamp = 0; - quint64 _lastUpdatedVelocityTimestamp = 0; - quint64 _lastUpdatedAngularVelocityTimestamp = 0; - quint64 _lastUpdatedAccelerationTimestamp = 0; + quint64 _lastUpdatedPositionTimestamp { 0 }; + quint64 _lastUpdatedRotationTimestamp { 0 }; + quint64 _lastUpdatedVelocityTimestamp { 0 }; + quint64 _lastUpdatedAngularVelocityTimestamp { 0 }; + quint64 _lastUpdatedAccelerationTimestamp { 0 }; }; From e2c4b4d3064707ac5310e755566f8c51e23ce252 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 3 Jun 2016 10:17:08 -0700 Subject: [PATCH 0327/1237] added haptic support to sdl and touch (needs testing) --- plugins/hifiSdl2/src/Joystick.cpp | 11 ++++++- plugins/hifiSdl2/src/Joystick.h | 3 ++ plugins/hifiSdl2/src/SDL2Manager.cpp | 2 +- .../oculus/src/OculusControllerManager.cpp | 31 +++++++++++++++++++ plugins/oculus/src/OculusControllerManager.h | 11 +++++++ 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index a109656489..136c021913 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -21,9 +21,10 @@ Joystick::Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameControl InputDevice("GamePad"), _sdlGameController(sdlGameController), _sdlJoystick(SDL_GameControllerGetJoystick(_sdlGameController)), + _sdlHaptic(SDL_HapticOpenFromJoystick(_sdlJoystick)), _instanceId(instanceId) { - + SDL_HapticRumbleInit(_sdlHaptic); } Joystick::~Joystick() { @@ -31,6 +32,7 @@ Joystick::~Joystick() { } void Joystick::closeJoystick() { + SDL_HapticClose(_sdlHaptic); SDL_GameControllerClose(_sdlGameController); } @@ -62,6 +64,13 @@ void Joystick::handleButtonEvent(const SDL_ControllerButtonEvent& event) { } } +bool Joystick::triggerHapticPulse(float strength, float duration, bool leftHand) { + if (SDL_HapticRumblePlay(_sdlHaptic, strength, duration) != 0) { + return false; + } + return true; +} + controller::Input::NamedVector Joystick::getAvailableInputs() const { using namespace controller; static const Input::NamedVector availableInputs{ diff --git a/plugins/hifiSdl2/src/Joystick.h b/plugins/hifiSdl2/src/Joystick.h index e2eaeaef8b..4f785f85ce 100644 --- a/plugins/hifiSdl2/src/Joystick.h +++ b/plugins/hifiSdl2/src/Joystick.h @@ -36,6 +36,8 @@ public: virtual QString getDefaultMappingConfig() const override; virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void focusOutEvent() override; + + bool triggerHapticPulse(float strength, float duration, bool leftHand) override; Joystick() : InputDevice("GamePad") {} ~Joystick(); @@ -52,6 +54,7 @@ public: private: SDL_GameController* _sdlGameController; SDL_Joystick* _sdlJoystick; + SDL_Haptic* _sdlHaptic; SDL_JoystickID _instanceId; }; diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index 0bdb68f830..09e783864c 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -49,7 +49,7 @@ SDL_JoystickID SDL2Manager::getInstanceId(SDL_GameController* controller) { } void SDL2Manager::init() { - bool initSuccess = (SDL_Init(SDL_INIT_GAMECONTROLLER) == 0); + bool initSuccess = (SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) == 0); if (initSuccess) { int joystickCount = SDL_NumJoysticks(); diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 09ab6ec159..3151db1d90 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -55,6 +55,11 @@ bool OculusControllerManager::activate() { userInputMapper->registerDevice(_touch); } + _leftHapticTimer.setSingleShot(true); + _rightHapticTimer.setSingleShot(true); + connect(&_leftHapticTimer, SIGNAL(timeout()), this, SLOT(stopHapticPulse(true))); + connect(&_rightHapticTimer, SIGNAL(timeout()), this, SLOT(stopHapticPulse(false))); + return true; } @@ -105,6 +110,12 @@ void OculusControllerManager::pluginFocusOutEvent() { } } +void OculusControllerManager::stopHapticPulse(bool leftHand) { + if (_touch) { + _touch->stopHapticPulse(leftHand); + } +} + using namespace controller; static const std::vector> BUTTON_MAP { { @@ -228,6 +239,26 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, pose.velocity = toGlm(handPose.LinearVelocity); } +bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, float duration, bool leftHand) { + auto handType = (leftHand ? ovrControllerType_LTouch : ovrControllerType_RTouch); + if (ovr_SetControllerVibration(_parent._session, handType, 1.0f, strength) != ovrSuccess) { + return false; + } + + if (leftHand) { + _parent._leftHapticTimer.start(duration); + } else { + _parent._rightHapticTimer.start(duration); + } + + return true; +} + +void OculusControllerManager::TouchDevice::stopHapticPulse(bool leftHand) { + auto handType = (leftHand ? ovrControllerType_LTouch : ovrControllerType_RTouch); + ovr_SetControllerVibration(_parent._session, handType, 0.0f, 0.0f); +} + controller::Input::NamedVector OculusControllerManager::TouchDevice::getAvailableInputs() const { using namespace controller; QVector availableInputs{ diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index 980e1286f8..20e5726dff 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -10,6 +10,7 @@ #define hifi__OculusControllerManager #include +#include #include #include @@ -32,6 +33,9 @@ public: void pluginFocusOutEvent() override; void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; +private slots: + void stopHapticPulse(bool leftHand); + private: class OculusInputDevice : public controller::InputDevice { public: @@ -64,6 +68,11 @@ private: void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; + bool triggerHapticPulse(float strength, float duration, bool leftHand) override; + + private: + void stopHapticPulse(bool leftHand); + private: void handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose); int _trackedControllers { 0 }; @@ -73,6 +82,8 @@ private: ovrSession _session { nullptr }; ovrInputState _inputState {}; RemoteDevice::Pointer _remote; + QTimer _leftHapticTimer; + QTimer _rightHapticTimer; TouchDevice::Pointer _touch; static const QString NAME; }; From 3e919cb7ac4d78b3661b020e9f577a0978c680eb Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 3 Jun 2016 10:42:02 -0700 Subject: [PATCH 0328/1237] cleanup --- .../controllers/handControllerPointer.js | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 5cbde82654..e70f704ce7 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -27,6 +27,7 @@ // UTILITIES ------------- // +function ignore() { } // Utility to make it easier to setup and disconnect cleanly. function setupHandler(event, handler) { @@ -106,7 +107,7 @@ function Trigger() { }; that.full = function () { return (that.state === 'full') ? 1.0 : 0.0; - } + }; } // VERTICAL FIELD OF VIEW --------- @@ -229,10 +230,9 @@ function updateSeeking() { } averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); - if (!lookAt2D) { - // FIXME - determine if this message is useful but make it so it doesn't spam the - // log in the case that it is happening - //print('Cannot seek without lookAt position'); + if (!lookAt2D) { // If this happens, something has gone terribly wrong. + print('Cannot seek without lookAt position'); + isSeeking = false; return; } // E.g., if parallel to location in HUD var copy = Reticle.position; @@ -305,7 +305,7 @@ var leftTrigger = new Trigger(); var rightTrigger = new Trigger(); var activeTrigger = rightTrigger; var activeHand = Controller.Standard.RightHand; -function toggleHand() { +function toggleHand() { // unequivocally switch which hand controls mouse position if (activeHand === Controller.Standard.RightHand) { activeHand = Controller.Standard.LeftHand; activeTrigger = leftTrigger; @@ -314,6 +314,13 @@ function toggleHand() { activeTrigger = rightTrigger; } } +function makeToggleAction(hand) { // return a function(0|1) that makes the specified hand control mouse when 1 + return function (on) { + if (on && (activeHand !== hand)) { + toggleHand(); + } + }; +} var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); Script.scriptEnding.connect(clickMapping.disable); @@ -321,25 +328,14 @@ Script.scriptEnding.connect(clickMapping.disable); // Gather the trigger data for smoothing. clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); -// The next two lines will be removed soon. Right now I want both trigger and button so we can compare. -clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick); -clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick); -// Full somoothed trigger is a click. +// Full smoothed trigger is a click. clickMapping.from(rightTrigger.full).to(Controller.Actions.ReticleClick); clickMapping.from(leftTrigger.full).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); // Partial smoothed trigger is activation. -clickMapping.from(rightTrigger.partial).to(function (on) { - if (on && (activeHand !== Controller.Standard.RightHand)) { - toggleHand(); - } -}); -clickMapping.from(leftTrigger.partial).to(function (on) { - if (on && (activeHand !== Controller.Standard.LeftHand)) { - toggleHand(); - } -}); +clickMapping.from(rightTrigger.partial).to(makeToggleAction(Controller.Standard.RightHand)); +clickMapping.from(leftTrigger.partial).to(makeToggleAction(Controller.Standard.LeftHand)); clickMapping.enable(); // VISUAL AID ----------- @@ -374,6 +370,7 @@ function turnOffVisualization(optionalEnableClicks) { // because we're showing c } var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { + ignore(controllerPosition, controllerDirection, hudPosition2d); // Show an indication of where the cursor will appear when crossing a HUD element, // and where in-world clicking will occur. // @@ -408,21 +405,21 @@ function updateVisualization(controllerPosition, controllerDirection, hudPositio function update() { var now = Date.now(); if (!handControllerLockOut.expired(now)) { - return turnOffVisualization(); - } // Let them use mouse it in peace. + return turnOffVisualization(); // Let them use mouse it in peace. + } if (!Menu.isOptionChecked("First Person")) { - return turnOffVisualization(); - } // What to do? menus can be behind hand! - if (!Window.hasFocus()) { // Don't mess with other apps - return turnOffVisualization(); + return turnOffVisualization(); // What to do? menus can be behind hand! + } + if (!Window.hasFocus()) { + return turnOffVisualization(); // Don't mess with other apps } leftTrigger.update(); rightTrigger.update(); var controllerPose = Controller.getPoseValue(activeHand); // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) if (!controllerPose.valid) { - return turnOffVisualization(); - } // Controller is cradled. + return turnOffVisualization(); // Controller is cradled. + } var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: @@ -430,9 +427,9 @@ function update() { var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { - // FIXME - determine if this message is useful but make it so it doesn't spam the - // log in the case that it is happening - //print('Controller is parallel to HUD'); + if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here + print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. + } return turnOffVisualization(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); From d591561a883efa4beec2551c0a2edf9bb2ffd5ea Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 09:53:07 -0700 Subject: [PATCH 0329/1237] remove RenderableModelEntityItem::_points use ShapeInfo::_points instead --- .../src/RenderableModelEntityItem.cpp | 21 ++++++++++--------- .../src/RenderableModelEntityItem.h | 1 - libraries/shared/src/ShapeInfo.h | 1 + 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7b51283bac..151ac9cb8a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -612,7 +612,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { const FBXGeometry& renderGeometry = _model->getFBXGeometry(); const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry(); - _points.clear(); + QVector>& points = info.getPoints(); + points.clear(); unsigned int i = 0; // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect @@ -675,9 +676,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // add next convex hull QVector newMeshPoints; - _points << newMeshPoints; + points << newMeshPoints; // add points to the new convex hull - _points[i++] << pointsInPart; + points[i++] << pointsInPart; } } @@ -691,22 +692,22 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. AABox box; - for (int i = 0; i < _points.size(); i++) { - for (int j = 0; j < _points[i].size(); j++) { + for (int i = 0; i < points.size(); i++) { + for (int j = 0; j < points[i].size(); j++) { // compensate for registration - _points[i][j] += _model->getOffset(); + points[i][j] += _model->getOffset(); // scale so the collision points match the model points - _points[i][j] *= scale; + points[i][j] *= scale; // this next subtraction is done so we can give info the offset, which will cause // the shape-key to change. - _points[i][j] -= _model->getOffset(); - box += _points[i][j]; + points[i][j] -= _model->getOffset(); + box += points[i][j]; } } glm::vec3 collisionModelDimensions = box.getDimensions(); info.setParams(type, collisionModelDimensions, _compoundShapeURL); - info.setConvexHulls(_points); + info.setConvexHulls(points); info.setOffset(_model->getOffset()); } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index d2de45f538..339c907532 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -103,7 +103,6 @@ private: QVariantMap _currentTextures; QVariantMap _originalTextures; bool _originalTexturesRead = false; - QVector> _points; bool _dimensionsInitialized = true; AnimationPropertyGroup _renderAnimationProperties; diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 1632d22450..6d1edbc261 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -60,6 +60,7 @@ public: const glm::vec3& getHalfExtents() const { return _halfExtents; } const glm::vec3& getOffset() const { return _offset; } + QVector>& getPoints() { return _points; } const QVector>& getPoints() const { return _points; } uint32_t getNumSubShapes() const; From 654468619cf08bca961f86c25f1645473e966ea1 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 10:31:54 -0700 Subject: [PATCH 0330/1237] swap order of if-else blocks --- .../src/RenderableModelEntityItem.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 151ac9cb8a..7b2494e1e0 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -599,11 +599,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType type = getShapeType(); - if (type != SHAPE_TYPE_COMPOUND) { - ModelEntityItem::computeShapeInfo(info); - info.setParams(type, 0.5f * getDimensions()); - adjustShapeInfoByRegistration(info); - } else { + if (type == SHAPE_TYPE_COMPOUND) { updateModelBounds(); // should never fall in here when collision model not fully loaded @@ -709,6 +705,10 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { info.setParams(type, collisionModelDimensions, _compoundShapeURL); info.setConvexHulls(points); info.setOffset(_model->getOffset()); + } else { + ModelEntityItem::computeShapeInfo(info); + info.setParams(type, 0.5f * getDimensions()); + adjustShapeInfoByRegistration(info); } } From 5d9e320dd749d3e14ea7b672f49c76c5ff9367df Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 10:48:02 -0700 Subject: [PATCH 0331/1237] purge SHAPE_TYPE_LINE --- libraries/entities/src/LineEntityItem.h | 2 -- libraries/entities/src/PolyLineEntityItem.h | 2 -- libraries/shared/src/ShapeInfo.h | 3 +-- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 4d63562cf7..a37d17262e 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -60,8 +60,6 @@ class LineEntityItem : public EntityItem { const QVector& getLinePoints() const{ return _points; } - virtual ShapeType getShapeType() const { return SHAPE_TYPE_LINE; } - // never have a ray intersection pick a LineEntityItem. virtual bool supportsDetailedRayIntersection() const { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index cf7b14dbf1..59c4ec592c 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -78,8 +78,6 @@ class PolyLineEntityItem : public EntityItem { virtual bool needsToCallUpdate() const { return true; } - virtual ShapeType getShapeType() const { return SHAPE_TYPE_LINE; } - // never have a ray intersection pick a PolyLineEntityItem. virtual bool supportsDetailedRayIntersection() const { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 6d1edbc261..86a6c410de 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -38,8 +38,7 @@ enum ShapeType { SHAPE_TYPE_CAPSULE_Z, SHAPE_TYPE_CYLINDER_X, SHAPE_TYPE_CYLINDER_Y, - SHAPE_TYPE_CYLINDER_Z, - SHAPE_TYPE_LINE + SHAPE_TYPE_CYLINDER_Z }; class ShapeInfo { From c066b4d4ac37320d59c75840134cf1fad0c984e0 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 10:54:05 -0700 Subject: [PATCH 0332/1237] purge collision shape for ParticleEffectEntityItem --- libraries/entities/src/ParticleEffectEntityItem.cpp | 12 ------------ libraries/entities/src/ParticleEffectEntityItem.h | 4 ---- 2 files changed, 16 deletions(-) diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index a7bd0038e6..d96c42fd7b 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -304,7 +304,6 @@ EntityItemProperties ParticleEffectEntityItem::getProperties(EntityPropertyFlags COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); - COPY_ENTITY_PROPERTY_TO_PROPERTIES(shapeType, getShapeType); // FIXME - this doesn't appear to get used COPY_ENTITY_PROPERTY_TO_PROPERTIES(maxParticles, getMaxParticles); COPY_ENTITY_PROPERTY_TO_PROPERTIES(lifespan, getLifespan); COPY_ENTITY_PROPERTY_TO_PROPERTIES(isEmitting, getIsEmitting); @@ -342,7 +341,6 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(maxParticles, setMaxParticles); SET_ENTITY_PROPERTY_FROM_PROPERTIES(lifespan, setLifespan); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isEmitting, setIsEmitting); @@ -406,7 +404,6 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch READ_ENTITY_PROPERTY(PROP_EMITTING_PARTICLES, bool, setIsEmitting); } - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); READ_ENTITY_PROPERTY(PROP_MAX_PARTICLES, quint32, setMaxParticles); READ_ENTITY_PROPERTY(PROP_LIFESPAN, float, setLifespan); READ_ENTITY_PROPERTY(PROP_EMIT_RATE, float, setEmitRate); @@ -474,7 +471,6 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_COLOR; - requestedProperties += PROP_SHAPE_TYPE; requestedProperties += PROP_MAX_PARTICLES; requestedProperties += PROP_LIFESPAN; requestedProperties += PROP_EMITTING_PARTICLES; @@ -518,7 +514,6 @@ void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_EMITTING_PARTICLES, getIsEmitting()); - APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); APPEND_ENTITY_PROPERTY(PROP_MAX_PARTICLES, getMaxParticles()); APPEND_ENTITY_PROPERTY(PROP_LIFESPAN, getLifespan()); APPEND_ENTITY_PROPERTY(PROP_EMIT_RATE, getEmitRate()); @@ -584,13 +579,6 @@ void ParticleEffectEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } -void ParticleEffectEntityItem::updateShapeType(ShapeType type) { - if (type != _shapeType) { - _shapeType = type; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } -} - void ParticleEffectEntityItem::integrateParticle(Particle& particle, float deltaTime) { glm::vec3 atSquared = (0.5f * deltaTime * deltaTime) * particle.acceleration; glm::vec3 at = particle.acceleration * deltaTime; diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 4538a1bb43..34d3c1c9e1 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -95,9 +95,6 @@ public: void setAlphaSpread(float alphaSpread); float getAlphaSpread() const { return _alphaSpread; } - void updateShapeType(ShapeType type); - virtual ShapeType getShapeType() const { return _shapeType; } - virtual void debugDump() const; bool isEmittingParticles() const; /// emitting enabled, and there are particles alive @@ -281,7 +278,6 @@ protected: QString _textures { DEFAULT_TEXTURES }; bool _texturesChangedFlag { false }; - ShapeType _shapeType { SHAPE_TYPE_NONE }; float _timeUntilNextEmit { 0.0f }; From 7858ef84b326cba1afb6af15e06f97afd0e4302f Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 11:11:10 -0700 Subject: [PATCH 0333/1237] don't copy points onto itself --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7b2494e1e0..db0c51f8d1 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -703,7 +703,6 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 collisionModelDimensions = box.getDimensions(); info.setParams(type, collisionModelDimensions, _compoundShapeURL); - info.setConvexHulls(points); info.setOffset(_model->getOffset()); } else { ModelEntityItem::computeShapeInfo(info); From 211bbb88e6ce567d41eab85fafa02726d9297bbf Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 11:44:25 -0700 Subject: [PATCH 0334/1237] purge SHAPE_TYPE_ELLIPSOID which wasn't used --- libraries/entities/src/EntityItemProperties.cpp | 3 +-- libraries/shared/src/ShapeInfo.cpp | 12 ------------ libraries/shared/src/ShapeInfo.h | 5 ++--- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ba39727ff9..89bf9f1a21 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -88,7 +88,7 @@ void EntityItemProperties::setLastEdited(quint64 usecTime) { _lastEdited = usecTime > _created ? usecTime : _created; } -const char* shapeTypeNames[] = {"none", "box", "sphere", "ellipsoid", "plane", "compound", "capsule-x", +const char* shapeTypeNames[] = {"none", "box", "sphere", "plane", "compound", "capsule-x", "capsule-y", "capsule-z", "cylinder-x", "cylinder-y", "cylinder-z"}; QHash stringToShapeTypeLookup; @@ -101,7 +101,6 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_NONE); addShapeType(SHAPE_TYPE_BOX); addShapeType(SHAPE_TYPE_SPHERE); - addShapeType(SHAPE_TYPE_ELLIPSOID); addShapeType(SHAPE_TYPE_PLANE); addShapeType(SHAPE_TYPE_COMPOUND); addShapeType(SHAPE_TYPE_CAPSULE_X); diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 0974c88e73..41800cc841 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -64,14 +64,6 @@ void ShapeInfo::setSphere(float radius) { _doubleHashKey.clear(); } -void ShapeInfo::setEllipsoid(const glm::vec3& halfExtents) { - _url = ""; - _type = SHAPE_TYPE_ELLIPSOID; - _halfExtents = halfExtents; - _points.clear(); - _doubleHashKey.clear(); -} - void ShapeInfo::setConvexHulls(const QVector>& points) { _points = points; _type = (_points.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; @@ -146,10 +138,6 @@ bool ShapeInfo::contains(const glm::vec3& point) const { switch(_type) { case SHAPE_TYPE_SPHERE: return glm::length(point) <= _halfExtents.x; - case SHAPE_TYPE_ELLIPSOID: { - glm::vec3 scaledPoint = glm::abs(point) / _halfExtents; - return glm::length(scaledPoint) <= 1.0f; - } case SHAPE_TYPE_CYLINDER_X: return glm::length(glm::vec2(point.y, point.z)) <= _halfExtents.z; case SHAPE_TYPE_CYLINDER_Y: diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 86a6c410de..979ab782d0 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -30,7 +30,6 @@ enum ShapeType { SHAPE_TYPE_NONE, SHAPE_TYPE_BOX, SHAPE_TYPE_SPHERE, - SHAPE_TYPE_ELLIPSOID, SHAPE_TYPE_PLANE, SHAPE_TYPE_COMPOUND, SHAPE_TYPE_CAPSULE_X, @@ -38,7 +37,8 @@ enum ShapeType { SHAPE_TYPE_CAPSULE_Z, SHAPE_TYPE_CYLINDER_X, SHAPE_TYPE_CYLINDER_Y, - SHAPE_TYPE_CYLINDER_Z + SHAPE_TYPE_CYLINDER_Z, + SHAPE_TYPE_STATIC_MESH }; class ShapeInfo { @@ -49,7 +49,6 @@ public: void setParams(ShapeType type, const glm::vec3& halfExtents, QString url=""); void setBox(const glm::vec3& halfExtents); void setSphere(float radius); - void setEllipsoid(const glm::vec3& halfExtents); void setConvexHulls(const QVector>& points); void setCapsuleY(float radius, float halfHeight); void setOffset(const glm::vec3& offset); From 9dc0fa77962dfaa299adbb0209936b2d95244d3f Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 12:20:57 -0700 Subject: [PATCH 0335/1237] don't automatically clear ShapeInfo points --- libraries/shared/src/ShapeInfo.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 41800cc841..9c1e5c3816 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -23,7 +23,6 @@ void ShapeInfo::clear() { void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString url) { _type = type; - _points.clear(); switch(type) { case SHAPE_TYPE_NONE: _halfExtents = glm::vec3(0.0f); @@ -52,7 +51,6 @@ void ShapeInfo::setBox(const glm::vec3& halfExtents) { _url = ""; _type = SHAPE_TYPE_BOX; _halfExtents = halfExtents; - _points.clear(); _doubleHashKey.clear(); } @@ -60,7 +58,6 @@ void ShapeInfo::setSphere(float radius) { _url = ""; _type = SHAPE_TYPE_SPHERE; _halfExtents = glm::vec3(radius, radius, radius); - _points.clear(); _doubleHashKey.clear(); } @@ -74,7 +71,6 @@ void ShapeInfo::setCapsuleY(float radius, float halfHeight) { _url = ""; _type = SHAPE_TYPE_CAPSULE_Y; _halfExtents = glm::vec3(radius, halfHeight, radius); - _points.clear(); _doubleHashKey.clear(); } From bce8879d7cb9e6a2cf3aed1051fbfd9850911846 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 12:24:28 -0700 Subject: [PATCH 0336/1237] remove ShapeInfo::clearPoints() use getPoints() by ref and then clear --- libraries/shared/src/ShapeInfo.h | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 979ab782d0..c853666d90 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -62,7 +62,6 @@ public: const QVector>& getPoints() const { return _points; } uint32_t getNumSubShapes() const; - void clearPoints () { _points.clear(); } void appendToPoints (const QVector& newPoints) { _points << newPoints; } int getMaxNumPoints() const; From eff59d3fd376b835247d1b5be621c2d480e87ecd Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 1 Jun 2016 12:34:06 -0700 Subject: [PATCH 0337/1237] build compound sub meshes shapes with less copying --- .../src/RenderableModelEntityItem.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index db0c51f8d1..033da416be 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -617,7 +617,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { - QVector pointsInPart; + points.push_back(QVector()); + QVector& pointsInPart = points[i]; // run through all the triangles and (uniquely) add each point to the hull unsigned int triangleCount = meshPart.triangleIndices.size() / 3; @@ -667,14 +668,10 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { if (pointsInPart.size() == 0) { qCDebug(entitiesrenderer) << "Warning -- meshPart has no faces"; + points.pop_back(); continue; } - - // add next convex hull - QVector newMeshPoints; - points << newMeshPoints; - // add points to the new convex hull - points[i++] << pointsInPart; + ++i; } } From 2a3da60ed492fb2890deaa096e250e90fc2cdf0a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 2 Jun 2016 09:16:32 -0700 Subject: [PATCH 0338/1237] remove some magic numbers --- .../src/RenderableModelEntityItem.cpp | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 033da416be..7b3b3c3efe 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -610,10 +610,12 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { QVector>& points = info.getPoints(); points.clear(); - unsigned int i = 0; + uint32_t i = 0; // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. + const uint32_t TRIANGLE_STRIDE = 3; + const uint32_t QUAD_STRIDE = 4; foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { @@ -621,14 +623,12 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { QVector& pointsInPart = points[i]; // run through all the triangles and (uniquely) add each point to the hull - unsigned int triangleCount = meshPart.triangleIndices.size() / 3; - for (unsigned int j = 0; j < triangleCount; j++) { - unsigned int p0Index = meshPart.triangleIndices[j*3]; - unsigned int p1Index = meshPart.triangleIndices[j*3+1]; - unsigned int p2Index = meshPart.triangleIndices[j*3+2]; - glm::vec3 p0 = mesh.vertices[p0Index]; - glm::vec3 p1 = mesh.vertices[p1Index]; - glm::vec3 p2 = mesh.vertices[p2Index]; + uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); + assert(numIndices % TRIANGLE_STRIDE == 0); + for (uint32_t j = 0; j < numIndices; j += TRIANGLE_STRIDE) { + glm::vec3 p0 = mesh.vertices[meshPart.triangleIndices[j]]; + glm::vec3 p1 = mesh.vertices[meshPart.triangleIndices[j + 1]]; + glm::vec3 p2 = mesh.vertices[meshPart.triangleIndices[j + 2]]; if (!pointsInPart.contains(p0)) { pointsInPart << p0; } @@ -641,17 +641,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } // run through all the quads and (uniquely) add each point to the hull - unsigned int quadCount = meshPart.quadIndices.size() / 4; - assert((unsigned int)meshPart.quadIndices.size() == quadCount*4); - for (unsigned int j = 0; j < quadCount; j++) { - unsigned int p0Index = meshPart.quadIndices[j*4]; - unsigned int p1Index = meshPart.quadIndices[j*4+1]; - unsigned int p2Index = meshPart.quadIndices[j*4+2]; - unsigned int p3Index = meshPart.quadIndices[j*4+3]; - glm::vec3 p0 = mesh.vertices[p0Index]; - glm::vec3 p1 = mesh.vertices[p1Index]; - glm::vec3 p2 = mesh.vertices[p2Index]; - glm::vec3 p3 = mesh.vertices[p3Index]; + numIndices = (uint32_t)meshPart.quadIndices.size(); + assert(numIndices % QUAD_STRIDE == 0); + for (uint32_t j = 0; j < numIndices; j += QUAD_STRIDE) { + glm::vec3 p0 = mesh.vertices[meshPart.quadIndices[j]]; + glm::vec3 p1 = mesh.vertices[meshPart.quadIndices[j + 1]]; + glm::vec3 p2 = mesh.vertices[meshPart.quadIndices[j + 2]]; + glm::vec3 p3 = mesh.vertices[meshPart.quadIndices[j + 3]]; if (!pointsInPart.contains(p0)) { pointsInPart << p0; } From c73757f740b5accc961723d2de6d741d0d68074c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 3 Jun 2016 09:17:44 -0700 Subject: [PATCH 0339/1237] use many colors for collision hull rendering --- libraries/render-utils/src/Model.cpp | 50 ++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 8011b0c4c8..ded1184c24 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -35,7 +35,46 @@ int vec3VectorTypeId = qRegisterMetaType >(); float Model::FAKE_DIMENSION_PLACEHOLDER = -1.0f; #define HTTP_INVALID_COM "http://invalid.com" -model::MaterialPointer Model::_collisionHullMaterial; +const int NUM_COLLISION_HULL_COLORS = 24; +std::vector _collisionHullMaterials; + +void initCollisionHullMaterials() { + // generates bright colors in red, green, blue, yellow, magenta, and cyan spectrums + // (no browns, greys, or dark shades) + float component[NUM_COLLISION_HULL_COLORS] = { + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.2f, 0.4f, 0.6f, 0.8f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 0.8f, 0.6f, 0.4f, 0.2f + }; + _collisionHullMaterials.reserve(NUM_COLLISION_HULL_COLORS); + + // each component gets the same cuve + // but offset by a multiple of one third the full width + int numComponents = 3; + int sectionWidth = NUM_COLLISION_HULL_COLORS / numComponents; + int greenPhase = sectionWidth; + int bluePhase = 2 * sectionWidth; + + // we stride through the colors to scatter adjacent shades + // so they don't tend to group together for large models + for (int i = 0; i < sectionWidth; ++i) { + for (int j = 0; j < numComponents; ++j) { + model::MaterialPointer material; + material = std::make_shared(); + int index = j * sectionWidth + i; + float red = component[index]; + float green = component[(index + greenPhase) % NUM_COLLISION_HULL_COLORS]; + float blue = component[(index + bluePhase) % NUM_COLLISION_HULL_COLORS]; + material->setAlbedo(glm::vec3(red, green, blue)); + material->setMetallic(0.02f); + material->setRoughness(0.5f); + _collisionHullMaterials.push_back(material); + } + } +} Model::Model(RigPointer rig, QObject* parent) : QObject(parent), @@ -1217,13 +1256,10 @@ void Model::segregateMeshGroups() { int totalParts = mesh.parts.size(); for (int partIndex = 0; partIndex < totalParts; partIndex++) { if (showingCollisionHull) { - if (!_collisionHullMaterial) { - _collisionHullMaterial = std::make_shared(); - _collisionHullMaterial->setAlbedo(glm::vec3(1.0f, 0.5f, 0.0f)); - _collisionHullMaterial->setMetallic(0.02f); - _collisionHullMaterial->setRoughness(0.5f); + if (_collisionHullMaterials.empty()) { + initCollisionHullMaterials(); } - _collisionRenderItemsSet << std::make_shared(networkMesh, partIndex, _collisionHullMaterial, transform, offset); + _collisionRenderItemsSet << std::make_shared(networkMesh, partIndex, _collisionHullMaterials[partIndex % NUM_COLLISION_HULL_COLORS], transform, offset); } else { _modelMeshRenderItemsSet << std::make_shared(this, i, partIndex, shapeID, transform, offset); } From f17330ea82a3cde66d7fd686cc2ebaf4398ac39b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 3 Jun 2016 09:18:40 -0700 Subject: [PATCH 0340/1237] diff method for capping convex hull point count --- libraries/physics/src/ShapeFactory.cpp | 87 +++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 7 deletions(-) diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 71b919b7ee..635df1054d 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -17,6 +17,54 @@ #include "ShapeFactory.h" #include "BulletUtil.h" +// These are the same normalized directions used by the btShapeHull class. +// 12 points for the face centers of a duodecohedron plus another 30 points +// for the midpoints the edges, for a total of 42. +const uint32_t NUM_UNIT_SPHERE_DIRECTIONS = 42; +static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = { + btVector3(btScalar(0.000000) , btScalar(-0.000000),btScalar(-1.000000)), + btVector3(btScalar(0.723608) , btScalar(-0.525725),btScalar(-0.447219)), + btVector3(btScalar(-0.276388) , btScalar(-0.850649),btScalar(-0.447219)), + btVector3(btScalar(-0.894426) , btScalar(-0.000000),btScalar(-0.447216)), + btVector3(btScalar(-0.276388) , btScalar(0.850649),btScalar(-0.447220)), + btVector3(btScalar(0.723608) , btScalar(0.525725),btScalar(-0.447219)), + btVector3(btScalar(0.276388) , btScalar(-0.850649),btScalar(0.447220)), + btVector3(btScalar(-0.723608) , btScalar(-0.525725),btScalar(0.447219)), + btVector3(btScalar(-0.723608) , btScalar(0.525725),btScalar(0.447219)), + btVector3(btScalar(0.276388) , btScalar(0.850649),btScalar(0.447219)), + btVector3(btScalar(0.894426) , btScalar(0.000000),btScalar(0.447216)), + btVector3(btScalar(-0.000000) , btScalar(0.000000),btScalar(1.000000)), + btVector3(btScalar(0.425323) , btScalar(-0.309011),btScalar(-0.850654)), + btVector3(btScalar(-0.162456) , btScalar(-0.499995),btScalar(-0.850654)), + btVector3(btScalar(0.262869) , btScalar(-0.809012),btScalar(-0.525738)), + btVector3(btScalar(0.425323) , btScalar(0.309011),btScalar(-0.850654)), + btVector3(btScalar(0.850648) , btScalar(-0.000000),btScalar(-0.525736)), + btVector3(btScalar(-0.525730) , btScalar(-0.000000),btScalar(-0.850652)), + btVector3(btScalar(-0.688190) , btScalar(-0.499997),btScalar(-0.525736)), + btVector3(btScalar(-0.162456) , btScalar(0.499995),btScalar(-0.850654)), + btVector3(btScalar(-0.688190) , btScalar(0.499997),btScalar(-0.525736)), + btVector3(btScalar(0.262869) , btScalar(0.809012),btScalar(-0.525738)), + btVector3(btScalar(0.951058) , btScalar(0.309013),btScalar(0.000000)), + btVector3(btScalar(0.951058) , btScalar(-0.309013),btScalar(0.000000)), + btVector3(btScalar(0.587786) , btScalar(-0.809017),btScalar(0.000000)), + btVector3(btScalar(0.000000) , btScalar(-1.000000),btScalar(0.000000)), + btVector3(btScalar(-0.587786) , btScalar(-0.809017),btScalar(0.000000)), + btVector3(btScalar(-0.951058) , btScalar(-0.309013),btScalar(-0.000000)), + btVector3(btScalar(-0.951058) , btScalar(0.309013),btScalar(-0.000000)), + btVector3(btScalar(-0.587786) , btScalar(0.809017),btScalar(-0.000000)), + btVector3(btScalar(-0.000000) , btScalar(1.000000),btScalar(-0.000000)), + btVector3(btScalar(0.587786) , btScalar(0.809017),btScalar(-0.000000)), + btVector3(btScalar(0.688190) , btScalar(-0.499997),btScalar(0.525736)), + btVector3(btScalar(-0.262869) , btScalar(-0.809012),btScalar(0.525738)), + btVector3(btScalar(-0.850648) , btScalar(0.000000),btScalar(0.525736)), + btVector3(btScalar(-0.262869) , btScalar(0.809012),btScalar(0.525738)), + btVector3(btScalar(0.688190) , btScalar(0.499997),btScalar(0.525736)), + btVector3(btScalar(0.525730) , btScalar(0.000000),btScalar(0.850652)), + btVector3(btScalar(0.162456) , btScalar(-0.499995),btScalar(0.850654)), + btVector3(btScalar(-0.425323) , btScalar(-0.309011),btScalar(0.850654)), + btVector3(btScalar(-0.425323) , btScalar(0.309011),btScalar(0.850654)), + btVector3(btScalar(0.162456) , btScalar(0.499995),btScalar(0.850654)) +}; btConvexHullShape* ShapeFactory::createConvexHull(const QVector& points) { @@ -66,15 +114,40 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin hull->addPoint(btVector3(correctedPoint[0], correctedPoint[1], correctedPoint[2]), false); } - if (points.size() > MAX_HULL_POINTS) { - // create hull approximation - btShapeHull shapeHull(hull); - shapeHull.buildHull(margin); + uint32_t numPoints = (uint32_t)hull->getNumPoints(); + if (numPoints > MAX_HULL_POINTS) { + // we have too many points, so we compute point projections along canonical unit vectors + // and keep the those that project the farthest + btVector3 btCenter = glmToBullet(center); + btVector3* shapePoints = hull->getUnscaledPoints(); + std::vector finalIndices; + finalIndices.reserve(NUM_UNIT_SPHERE_DIRECTIONS); + for (uint32_t i = 0; i < NUM_UNIT_SPHERE_DIRECTIONS; ++i) { + uint32_t bestIndex = 0; + btScalar maxDistance = _unitSphereDirections[i].dot(shapePoints[0] - btCenter); + for (uint32_t j = 1; j < numPoints; ++j) { + btScalar distance = _unitSphereDirections[i].dot(shapePoints[j] - btCenter); + if (distance > maxDistance) { + maxDistance = distance; + bestIndex = j; + } + } + bool keep = true; + for (uint32_t j = 0; j < finalIndices.size(); ++j) { + if (finalIndices[j] == bestIndex) { + keep = false; + break; + } + } + if (keep) { + finalIndices.push_back(bestIndex); + } + } + // we cannot copy Bullet shapes so we must create a new one... btConvexHullShape* newHull = new btConvexHullShape(); - const btVector3* newPoints = shapeHull.getVertexPointer(); - for (int i = 0; i < shapeHull.numVertices(); ++i) { - newHull->addPoint(newPoints[i], false); + for (uint32_t i = 0; i < finalIndices.size(); ++i) { + newHull->addPoint(shapePoints[finalIndices[i]], false); } // ...and delete the old one delete hull; From 3280a2b967ef3e8c9d9b307e8931132460137df5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 3 Jun 2016 09:50:14 -0700 Subject: [PATCH 0341/1237] fix indentation --- libraries/physics/src/ShapeFactory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 635df1054d..d667d1075d 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -116,8 +116,8 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin uint32_t numPoints = (uint32_t)hull->getNumPoints(); if (numPoints > MAX_HULL_POINTS) { - // we have too many points, so we compute point projections along canonical unit vectors - // and keep the those that project the farthest + // we have too many points, so we compute point projections along canonical unit vectors + // and keep the those that project the farthest btVector3 btCenter = glmToBullet(center); btVector3* shapePoints = hull->getUnscaledPoints(); std::vector finalIndices; From c2317adf06beb5f7d68c5a0e9321deb565b4768b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 3 Jun 2016 11:06:36 -0700 Subject: [PATCH 0342/1237] undo ambitious cleanup (try again another day) --- libraries/entities/src/LineEntityItem.h | 2 ++ libraries/entities/src/ParticleEffectEntityItem.cpp | 12 ++++++++++++ libraries/entities/src/ParticleEffectEntityItem.h | 4 ++++ libraries/entities/src/PolyLineEntityItem.h | 2 ++ 4 files changed, 20 insertions(+) diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index a37d17262e..6a5ef20bac 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -60,6 +60,8 @@ class LineEntityItem : public EntityItem { const QVector& getLinePoints() const{ return _points; } + virtual ShapeType getShapeType() const { return SHAPE_TYPE_NONE; } + // never have a ray intersection pick a LineEntityItem. virtual bool supportsDetailedRayIntersection() const { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index d96c42fd7b..a7bd0038e6 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -304,6 +304,7 @@ EntityItemProperties ParticleEffectEntityItem::getProperties(EntityPropertyFlags COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(shapeType, getShapeType); // FIXME - this doesn't appear to get used COPY_ENTITY_PROPERTY_TO_PROPERTIES(maxParticles, getMaxParticles); COPY_ENTITY_PROPERTY_TO_PROPERTIES(lifespan, getLifespan); COPY_ENTITY_PROPERTY_TO_PROPERTIES(isEmitting, getIsEmitting); @@ -341,6 +342,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(maxParticles, setMaxParticles); SET_ENTITY_PROPERTY_FROM_PROPERTIES(lifespan, setLifespan); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isEmitting, setIsEmitting); @@ -404,6 +406,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch READ_ENTITY_PROPERTY(PROP_EMITTING_PARTICLES, bool, setIsEmitting); } + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); READ_ENTITY_PROPERTY(PROP_MAX_PARTICLES, quint32, setMaxParticles); READ_ENTITY_PROPERTY(PROP_LIFESPAN, float, setLifespan); READ_ENTITY_PROPERTY(PROP_EMIT_RATE, float, setEmitRate); @@ -471,6 +474,7 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_COLOR; + requestedProperties += PROP_SHAPE_TYPE; requestedProperties += PROP_MAX_PARTICLES; requestedProperties += PROP_LIFESPAN; requestedProperties += PROP_EMITTING_PARTICLES; @@ -514,6 +518,7 @@ void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_EMITTING_PARTICLES, getIsEmitting()); + APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); APPEND_ENTITY_PROPERTY(PROP_MAX_PARTICLES, getMaxParticles()); APPEND_ENTITY_PROPERTY(PROP_LIFESPAN, getLifespan()); APPEND_ENTITY_PROPERTY(PROP_EMIT_RATE, getEmitRate()); @@ -579,6 +584,13 @@ void ParticleEffectEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } +void ParticleEffectEntityItem::updateShapeType(ShapeType type) { + if (type != _shapeType) { + _shapeType = type; + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } +} + void ParticleEffectEntityItem::integrateParticle(Particle& particle, float deltaTime) { glm::vec3 atSquared = (0.5f * deltaTime * deltaTime) * particle.acceleration; glm::vec3 at = particle.acceleration * deltaTime; diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 34d3c1c9e1..4538a1bb43 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -95,6 +95,9 @@ public: void setAlphaSpread(float alphaSpread); float getAlphaSpread() const { return _alphaSpread; } + void updateShapeType(ShapeType type); + virtual ShapeType getShapeType() const { return _shapeType; } + virtual void debugDump() const; bool isEmittingParticles() const; /// emitting enabled, and there are particles alive @@ -278,6 +281,7 @@ protected: QString _textures { DEFAULT_TEXTURES }; bool _texturesChangedFlag { false }; + ShapeType _shapeType { SHAPE_TYPE_NONE }; float _timeUntilNextEmit { 0.0f }; diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 59c4ec592c..3231e7c5e1 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -78,6 +78,8 @@ class PolyLineEntityItem : public EntityItem { virtual bool needsToCallUpdate() const { return true; } + virtual ShapeType getShapeType() const { return SHAPE_TYPE_NONE; } + // never have a ray intersection pick a PolyLineEntityItem. virtual bool supportsDetailedRayIntersection() const { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, From 8f9fc08226d3df4059f8f00403d588799815a060 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 3 Jun 2016 11:08:05 -0700 Subject: [PATCH 0343/1237] Add version number to avatar recording frame New recordings will have a version number of 1. A missing version field indicates the initial version of 0. Warn when playing back version 0 files which are no longer fully supported and fall back to default pose. Playback of version 1 files should work as expected. --- libraries/avatars/src/AvatarData.cpp | 47 +++++++++++++++++++++------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 16e4bd5437..2fa26a5f64 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1270,6 +1270,10 @@ static const QString JSON_AVATAR_DISPLAY_NAME = QStringLiteral("displayName"); static const QString JSON_AVATAR_ATTACHEMENTS = QStringLiteral("attachments"); static const QString JSON_AVATAR_ENTITIES = QStringLiteral("attachedEntities"); static const QString JSON_AVATAR_SCALE = QStringLiteral("scale"); +static const QString JSON_AVATAR_VERSION = QStringLiteral("version"); + +static const int JSON_AVATAR_JOINT_ROTATIONS_IN_RELATIVE_FRAME_VERSION = 0; +static const int JSON_AVATAR_JOINT_ROTATIONS_IN_ABSOLUTE_FRAME_VERSION = 1; QJsonValue toJsonValue(const JointData& joint) { QJsonArray result; @@ -1293,6 +1297,8 @@ JointData jointDataFromJsonValue(const QJsonValue& json) { QJsonObject AvatarData::toJson() const { QJsonObject root; + root[JSON_AVATAR_VERSION] = JSON_AVATAR_JOINT_ROTATIONS_IN_ABSOLUTE_FRAME_VERSION; + if (!getSkeletonModelURL().isEmpty()) { root[JSON_AVATAR_BODY_MODEL] = getSkeletonModelURL().toString(); } @@ -1359,6 +1365,15 @@ QJsonObject AvatarData::toJson() const { } void AvatarData::fromJson(const QJsonObject& json) { + + int version; + if (json.contains(JSON_AVATAR_VERSION)) { + version = json[JSON_AVATAR_VERSION].toInt(); + } else { + // initial data did not have a version field. + version = JSON_AVATAR_JOINT_ROTATIONS_IN_RELATIVE_FRAME_VERSION; + } + // The head setOrientation likes to overwrite the avatar orientation, // so lets do the head first // Most head data is relative to the avatar, and needs no basis correction, @@ -1424,20 +1439,28 @@ void AvatarData::fromJson(const QJsonObject& json) { // } // } - // Joint rotations are relative to the avatar, so they require no basis correction if (json.contains(JSON_AVATAR_JOINT_ARRAY)) { - QVector jointArray; - QJsonArray jointArrayJson = json[JSON_AVATAR_JOINT_ARRAY].toArray(); - jointArray.reserve(jointArrayJson.size()); - int i = 0; - for (const auto& jointJson : jointArrayJson) { - auto joint = jointDataFromJsonValue(jointJson); - jointArray.push_back(joint); - setJointData(i, joint.rotation, joint.translation); - _jointData[i].rotationSet = true; // Have to do that to broadcast the avatar new pose - i++; + if (version == JSON_AVATAR_JOINT_ROTATIONS_IN_RELATIVE_FRAME_VERSION) { + // because we don't have the full joint hierarchy skeleton of the model, + // we can't properly convert from relative rotations into absolute rotations. + quint64 now = usecTimestampNow(); + if (shouldLogError(now)) { + qCWarning(avatars) << "Version 0 avatar recordings not supported. using default rotations"; + } + } else { + QVector jointArray; + QJsonArray jointArrayJson = json[JSON_AVATAR_JOINT_ARRAY].toArray(); + jointArray.reserve(jointArrayJson.size()); + int i = 0; + for (const auto& jointJson : jointArrayJson) { + auto joint = jointDataFromJsonValue(jointJson); + jointArray.push_back(joint); + setJointData(i, joint.rotation, joint.translation); + _jointData[i].rotationSet = true; // Have to do that to broadcast the avatar new pose + i++; + } + setRawJointData(jointArray); } - setRawJointData(jointArray); } } From c1eab612418a81357e127f9918b4654413007a2f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 3 Jun 2016 11:22:06 -0700 Subject: [PATCH 0344/1237] sdl haptics aren't working, but might be a bug in sdl: http://stackoverflow.com/questions/23974908/why-is-sdl-hapticopenfromjoystick-not-working-in-sdl-2 --- plugins/hifiSdl2/src/Joystick.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index 136c021913..cf7dde371b 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -24,6 +24,9 @@ Joystick::Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameControl _sdlHaptic(SDL_HapticOpenFromJoystick(_sdlJoystick)), _instanceId(instanceId) { + if (!_sdlHaptic) { + qDebug(SDL_GetError()); + } SDL_HapticRumbleInit(_sdlHaptic); } From 247286c71f41ecc97dccdf57f7111175ad48c36e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 3 Jun 2016 11:55:33 -0700 Subject: [PATCH 0345/1237] Basic address bar toggle button. --- scripts/defaultScripts.js | 1 + scripts/system/goto.js | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 scripts/system/goto.js diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2a050d183e..4f6b5ca0e6 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -13,6 +13,7 @@ Script.load("system/progress.js"); Script.load("system/away.js"); Script.load("system/users.js"); Script.load("system/examples.js"); +Script.load("system/goto.js"); Script.load("system/edit.js"); Script.load("system/selectAudioDevice.js"); Script.load("system/notifications.js"); diff --git a/scripts/system/goto.js b/scripts/system/goto.js new file mode 100644 index 0000000000..75d9829905 --- /dev/null +++ b/scripts/system/goto.js @@ -0,0 +1,47 @@ +// +// goto.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("libraries/toolBars.js"); + +function initialPosition(windowDimensions, toolbar) { + return { + x: windowDimensions.x / 2 - Tool.IMAGE_WIDTH, + y: windowDimensions.y + }; +} +var toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.goto.toolbar", initialPosition, { + x: -Tool.IMAGE_WIDTH / 2, + y: -Tool.IMAGE_HEIGHT +}); +var button = toolBar.addTool({ + imageURL: Script.resolvePath("assets/images/tools/directory-01.svg"), + subImage: { + x: 0, + y: Tool.IMAGE_WIDTH, + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT + }, + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: 0.9, + visible: true, + showButtonDown: true +}); +function onMousePress (event) { + if (event.isLeftButton && button === toolBar.clicked(Overlays.getOverlayAtPoint(event))) { + DialogsManager.toggleAddressBar(); + } +}; +Controller.mousePressEvent.connect(onMousePress) +Script.scriptEnding.connect(function () { + Controller.mousePressEvent.disconnect(onMousePress) + toolBar.cleanup(); +}); From b88bba86721aae5c159eac8a94328cc2e3a3f802 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 3 Jun 2016 13:15:53 -0700 Subject: [PATCH 0346/1237] add code to convert older domain-settings to current style --- domain-server/src/DomainGatekeeper.cpp | 13 ++- .../src/DomainServerSettingsManager.cpp | 109 +++++++++++++----- .../src/DomainServerSettingsManager.h | 4 +- libraries/networking/src/AgentPermissions.cpp | 20 ++++ libraries/networking/src/AgentPermissions.h | 5 +- 5 files changed, 115 insertions(+), 36 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 5503bbb5b0..cf32e804dc 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -169,10 +169,6 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.canAdjustLocks = true; userPerms.canRezPermanentEntities = true; newNode->setPermissions(userPerms); - - qDebug() << "----------------------------"; - qDebug() << "AC perms are" << userPerms; - return newNode; } @@ -211,8 +207,13 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect userPerms |= _server->_settingsManager.getPermissionsForName("anonymous"); } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they are sent us a username and the signature verifies it - userPerms |= _server->_settingsManager.getPermissionsForName(username); - userPerms |= _server->_settingsManager.getPermissionsForName("logged-in"); + if (_server->_settingsManager.havePermissionsForName(username)) { + // we have specific permissions for this user. + userPerms |= _server->_settingsManager.getPermissionsForName(username); + } else { + // they are logged into metaverse, but we don't have specific permissions for them. + userPerms |= _server->_settingsManager.getPermissionsForName("logged-in"); + } } else { // they sent us a username, but it didn't check out requestUserPublicKey(username); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index de35188f54..9d5da9deb9 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -201,50 +201,105 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList if (oldVersion < 1.3) { // This was prior to the permissions-grid in the domain-server settings page - // bool isRestrictingAccess = valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); + bool isRestrictedAccess = valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); + QStringList allowedUsers = valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList(); + QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList(); + bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool(); - // const QVariant* allowedEditorsVariant = valueForKeyPath(getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); + _agentPermissions["localhost"].reset(new AgentPermissions("localhost")); + _agentPermissions["localhost"]->setAll(true); + _agentPermissions["anonymous"].reset(new AgentPermissions("anonymous")); + _agentPermissions["logged-in"].reset(new AgentPermissions("logged-in")); - // const QVariant* editorsAreRezzersVariant = valueForKeyPath(getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH); - // bool onlyEditorsAreRezzers = false; - // if (editorsAreRezzersVariant) { - // onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool(); - // } + if (isRestrictedAccess) { + // only users in allow-users list can connect + _agentPermissions["anonymous"]->canConnectToDomain = false; + _agentPermissions["logged-in"]->canConnectToDomain = false; + } // else anonymous and logged-in retain default of canConnectToDomain = true - // XXX + foreach (QString allowedUser, allowedUsers) { + // even if isRestrictedAccess is false, we have to add explicit rows for these users. + // defaults to canConnectToDomain = true + _agentPermissions[allowedUser].reset(new AgentPermissions(allowedUser)); + } + + foreach (QString allowedEditor, allowedEditors) { + if (!_agentPermissions.contains(allowedEditor)) { + _agentPermissions[allowedEditor].reset(new AgentPermissions(allowedEditor)); + if (isRestrictedAccess) { + // they can change locks, but can't connect. + _agentPermissions[allowedEditor]->canConnectToDomain = false; + } + } + _agentPermissions[allowedEditor]->canAdjustLocks = true; + } + + foreach (QString userName, _agentPermissions.keys()) { + if (onlyEditorsAreRezzers) { + _agentPermissions[userName]->canRezPermanentEntities = _agentPermissions[userName]->canAdjustLocks; + } else { + _agentPermissions[userName]->canRezPermanentEntities = true; + } + } + packPermissions(argumentList); + _agentPermissions.clear(); } } - unpackPermissions(); + unpackPermissions(argumentList); // write the current description version to our settings appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion); } -void DomainServerSettingsManager::unpackPermissions() { +void DomainServerSettingsManager::packPermissions(const QStringList& argumentList) { + // transfer details from _agentPermissions to _configMap + QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security"); + QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { + QVariantMap securityMap = security->toMap(); + QVariantList userList; + securityMap["permissions"] = userList; + _configMap.getUserConfig()["security"] = securityMap; + permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + } + + QVariantList* permissionsList = reinterpret_cast(permissions); + foreach (QString userName, _agentPermissions.keys()) { + *permissionsList += _agentPermissions[userName]->toVariant(); + } + persistToFile(); + _configMap.loadMasterAndUserConfig(argumentList); +} + +void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentList) { + // transfer details from _configMap to _agentPermissions; + bool foundLocalhost = false; bool foundAnonymous = false; bool foundLoggedIn = false; - // XXX check for duplicate IDs - - QVariant* permissions = valueForKeyPath(_configMap.getMergedConfig(), AGENT_PERMISSIONS_KEYPATH); - if (!permissions->canConvert(QMetaType::QVariantList)) { + QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { qDebug() << "failed to extract permissions from settings."; return; } - // QList permissionsList = permissions->toList(); + QList permissionsList = permissions->toList(); + // QVariantList* permissionsList = reinterpret_cast(permissions); - QVariantList* permissionsList = reinterpret_cast(permissions); - - foreach (QVariant permsHash, *permissionsList) { + foreach (QVariant permsHash, permissionsList) { AgentPermissionsPointer perms { new AgentPermissions(permsHash.toMap()) }; QString id = perms->getID(); foundLocalhost |= (id == "localhost"); foundAnonymous |= (id == "anonymous"); foundLoggedIn |= (id == "logged-in"); - _agentPermissions[id] = perms; + if (_agentPermissions.contains(id)) { + qDebug() << "duplicate name in permissions table: " << id; + _agentPermissions[id] |= perms; + } else { + _agentPermissions[id] = perms; + } } // if any of the standard names are missing, add them @@ -252,17 +307,20 @@ void DomainServerSettingsManager::unpackPermissions() { AgentPermissionsPointer perms { new AgentPermissions("localhost") }; perms->setAll(true); _agentPermissions["localhost"] = perms; - *permissionsList += perms->toVariant(); + // *permissionsList += perms->toVariant(); } if (!foundAnonymous) { AgentPermissionsPointer perms { new AgentPermissions("anonymous") }; _agentPermissions["anonymous"] = perms; - *permissionsList += perms->toVariant(); + // *permissionsList += perms->toVariant(); } if (!foundLoggedIn) { AgentPermissionsPointer perms { new AgentPermissions("logged-in") }; _agentPermissions["logged-in"] = perms; - *permissionsList += perms->toVariant(); + // *permissionsList += perms->toVariant(); + } + if (!foundLocalhost || !foundAnonymous || !foundLoggedIn) { + packPermissions(argumentList); } #ifdef WANT_DEBUG @@ -271,12 +329,7 @@ void DomainServerSettingsManager::unpackPermissions() { while (i.hasNext()) { i.next(); AgentPermissionsPointer perms = i.value(); - qDebug() << i.key() - << perms->canConnectToDomain - << perms->canAdjustLocks - << perms->canRezPermanentEntities - << perms->canRezTemporaryEntities - << perms->canWriteToAssetServer; + qDebug() << i.key() << perms; } #endif } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 3012e06c4d..8df0473eb3 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -40,6 +40,7 @@ public: QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } + bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); } AgentPermissions getPermissionsForName(const QString& name) const; QStringList getAllNames() { return _agentPermissions.keys(); } @@ -61,7 +62,8 @@ private: friend class DomainServer; - void unpackPermissions(); + void packPermissions(const QStringList& argumentList); + void unpackPermissions(const QStringList& argumentList); QHash _agentPermissions; }; diff --git a/libraries/networking/src/AgentPermissions.cpp b/libraries/networking/src/AgentPermissions.cpp index a2b46f49d5..afe523d15b 100644 --- a/libraries/networking/src/AgentPermissions.cpp +++ b/libraries/networking/src/AgentPermissions.cpp @@ -22,6 +22,19 @@ AgentPermissions& AgentPermissions::operator|=(const AgentPermissions& rhs) { this->canConnectPastMaxCapacity |= rhs.canConnectPastMaxCapacity; return *this; } +AgentPermissions& AgentPermissions::operator|=(const AgentPermissionsPointer& rhs) { + if (rhs) { + *this |= *rhs.get(); + } + return *this; +} +AgentPermissionsPointer& operator|=(AgentPermissionsPointer& lhs, const AgentPermissionsPointer& rhs) { + if (lhs && rhs) { + *lhs.get() |= rhs; + } + return lhs; +} + QDataStream& operator<<(QDataStream& out, const AgentPermissions& perms) { out << perms.canConnectToDomain; @@ -66,3 +79,10 @@ QDebug operator<<(QDebug debug, const AgentPermissions& perms) { debug.nospace() << "]"; return debug.nospace(); } +QDebug operator<<(QDebug debug, const AgentPermissionsPointer& perms) { + if (perms) { + return operator<<(debug, *perms.get()); + } + debug.nospace() << "[permissions: null]"; + return debug.nospace(); +} diff --git a/libraries/networking/src/AgentPermissions.h b/libraries/networking/src/AgentPermissions.h index 4ad381959a..9cbda08d92 100644 --- a/libraries/networking/src/AgentPermissions.h +++ b/libraries/networking/src/AgentPermissions.h @@ -67,6 +67,7 @@ public: } AgentPermissions& operator|=(const AgentPermissions& rhs); + AgentPermissions& operator|=(const AgentPermissionsPointer& rhs); friend QDataStream& operator<<(QDataStream& out, const AgentPermissions& perms); friend QDataStream& operator>>(QDataStream& in, AgentPermissions& perms); @@ -76,6 +77,8 @@ protected: const AgentPermissions DEFAULT_AGENT_PERMISSIONS; -QDebug operator<<(QDebug debug, const AgentPermissions& node); +QDebug operator<<(QDebug debug, const AgentPermissions& perms); +QDebug operator<<(QDebug debug, const AgentPermissionsPointer& perms); +AgentPermissionsPointer& operator|=(AgentPermissionsPointer& lhs, const AgentPermissionsPointer& rhs); #endif // hifi_AgentPermissions_h From 9292a9ce0b244579900f66143795ffb4ef29bd2f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 3 Jun 2016 13:56:32 -0700 Subject: [PATCH 0347/1237] Added MyAvatar.hmdLeanRecenterEnabled property Used to disable the 'room-scale' avatar re-centering code. Disabling this can prevent sliding when the avatar is supposed to be sitting or mounted on a stationary object. Also, removed a bunch of old, unused leaning and torso twisting code. --- interface/src/avatar/Avatar.cpp | 1 - interface/src/avatar/Avatar.h | 1 - interface/src/avatar/Head.cpp | 21 +-------------- interface/src/avatar/Head.h | 9 +------ interface/src/avatar/MyAvatar.cpp | 36 +++++++++----------------- interface/src/avatar/MyAvatar.h | 11 +++++--- interface/src/avatar/SkeletonModel.cpp | 5 ---- interface/src/ui/PreferencesDialog.cpp | 10 ------- libraries/animation/src/Rig.cpp | 14 ---------- libraries/animation/src/Rig.h | 6 ----- libraries/avatars/src/HeadData.cpp | 15 ----------- libraries/avatars/src/HeadData.h | 14 ---------- 12 files changed, 21 insertions(+), 122 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 3e22448386..f46a906af8 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -84,7 +84,6 @@ Avatar::Avatar(RigPointer rig) : _acceleration(0.0f), _lastAngularVelocity(0.0f), _lastOrientation(), - _leanScale(0.5f), _worldUpDirection(DEFAULT_UP_DIRECTION), _moving(false), _initialized(false), diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 79952e8f58..064f0a9533 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -210,7 +210,6 @@ protected: glm::vec3 _angularAcceleration; glm::quat _lastOrientation; - float _leanScale; glm::vec3 _worldUpDirection; float _stringLength; bool _moving; ///< set when position is changing diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 3af8b8a423..928f46facb 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -54,8 +54,6 @@ Head::Head(Avatar* owningAvatar) : _deltaPitch(0.0f), _deltaYaw(0.0f), _deltaRoll(0.0f), - _deltaLeanSideways(0.0f), - _deltaLeanForward(0.0f), _isCameraMoving(false), _isLookingAtMe(false), _lookingAtMeStarted(0), @@ -70,7 +68,6 @@ void Head::init() { void Head::reset() { _baseYaw = _basePitch = _baseRoll = 0.0f; - _leanForward = _leanSideways = 0.0f; } void Head::simulate(float deltaTime, bool isMine, bool billboard) { @@ -118,13 +115,6 @@ void Head::simulate(float deltaTime, bool isMine, bool billboard) { auto eyeTracker = DependencyManager::get(); _isEyeTrackerConnected = eyeTracker->isTracking(); } - - // Twist the upper body to follow the rotation of the head, but only do this with my avatar, - // since everyone else will see the full joint rotations for other people. - const float BODY_FOLLOW_HEAD_YAW_RATE = 0.1f; - const float BODY_FOLLOW_HEAD_FACTOR = 0.66f; - float currentTwist = getTorsoTwist(); - setTorsoTwist(currentTwist + (getFinalYaw() * BODY_FOLLOW_HEAD_FACTOR - currentTwist) * BODY_FOLLOW_HEAD_YAW_RATE); } if (!(_isFaceTrackerConnected || billboard)) { @@ -301,17 +291,13 @@ void Head::applyEyelidOffset(glm::quat headOrientation) { } } -void Head::relaxLean(float deltaTime) { +void Head::relax(float deltaTime) { // restore rotation, lean to neutral positions const float LEAN_RELAXATION_PERIOD = 0.25f; // seconds float relaxationFactor = 1.0f - glm::min(deltaTime / LEAN_RELAXATION_PERIOD, 1.0f); _deltaYaw *= relaxationFactor; _deltaPitch *= relaxationFactor; _deltaRoll *= relaxationFactor; - _leanSideways *= relaxationFactor; - _leanForward *= relaxationFactor; - _deltaLeanSideways *= relaxationFactor; - _deltaLeanForward *= relaxationFactor; } void Head::setScale (float scale) { @@ -419,8 +405,3 @@ float Head::getFinalPitch() const { float Head::getFinalRoll() const { return glm::clamp(_baseRoll + _deltaRoll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); } - -void Head::addLeanDeltas(float sideways, float forward) { - _deltaLeanSideways += sideways; - _deltaLeanForward += forward; -} diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index e4b8fefea5..33ea180d33 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -59,8 +59,6 @@ public: glm::vec3 getRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getUpDirection() const { return getOrientation() * IDENTITY_UP; } glm::vec3 getFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } - float getFinalLeanSideways() const { return _leanSideways + _deltaLeanSideways; } - float getFinalLeanForward() const { return _leanForward + _deltaLeanForward; } glm::quat getEyeRotation(const glm::vec3& eyePosition) const; @@ -91,8 +89,7 @@ public: virtual float getFinalYaw() const; virtual float getFinalRoll() const; - void relaxLean(float deltaTime); - void addLeanDeltas(float sideways, float forward); + void relax(float deltaTime); float getTimeWithoutTalking() const { return _timeWithoutTalking; } @@ -132,10 +129,6 @@ private: float _deltaYaw; float _deltaRoll; - // delta lean angles for lean perturbations (driven by collisions) - float _deltaLeanSideways; - float _deltaLeanForward; - bool _isCameraMoving; bool _isLookingAtMe; quint64 _lookingAtMeStarted; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6fdcd8f797..0f723d29e3 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -190,9 +190,6 @@ MyAvatar::MyAvatar(RigPointer rig) : if (!headData->getBlendshapeCoefficients().isEmpty()) { _headData->setBlendshapeCoefficients(headData->getBlendshapeCoefficients()); } - // head lean - _headData->setLeanForward(headData->getLeanForward()); - _headData->setLeanSideways(headData->getLeanSideways()); // head orientation _headData->setLookAtPosition(headData->getLookAtPosition()); } @@ -306,7 +303,7 @@ void MyAvatar::update(float deltaTime) { } Head* head = getHead(); - head->relaxLean(deltaTime); + head->relax(deltaTime); updateFromTrackers(deltaTime); // Get audio loudness data from audio input device @@ -574,16 +571,6 @@ void MyAvatar::updateFromTrackers(float deltaTime) { head->setDeltaYaw(estimatedRotation.y * magnifyFieldOfView); head->setDeltaRoll(estimatedRotation.z); } - - // Update torso lean distance based on accelerometer data - const float TORSO_LENGTH = 0.5f; - glm::vec3 relativePosition = estimatedPosition - glm::vec3(0.0f, -TORSO_LENGTH, 0.0f); - - const float MAX_LEAN = 45.0f; - head->setLeanSideways(glm::clamp(glm::degrees(atanf(relativePosition.x * _leanScale / TORSO_LENGTH)), - -MAX_LEAN, MAX_LEAN)); - head->setLeanForward(glm::clamp(glm::degrees(atanf(relativePosition.z * _leanScale / TORSO_LENGTH)), - -MAX_LEAN, MAX_LEAN)); } glm::vec3 MyAvatar::getLeftHandPosition() const { @@ -692,7 +679,6 @@ void MyAvatar::saveData() { settings.setValue("headPitch", getHead()->getBasePitch()); - settings.setValue("leanScale", _leanScale); settings.setValue("scale", _targetScale); settings.setValue("fullAvatarURL", @@ -809,7 +795,6 @@ void MyAvatar::loadData() { getHead()->setBasePitch(loadSetting(settings, "headPitch", 0.0f)); - _leanScale = loadSetting(settings, "leanScale", 0.05f); _targetScale = loadSetting(settings, "scale", 1.0f); setScale(glm::vec3(_targetScale)); @@ -2052,14 +2037,17 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput) { _desiredBodyMatrix = desiredBodyMatrix; - if (!isActive(Rotation) && shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix)) { - activate(Rotation); - } - if (!isActive(Horizontal) && shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix)) { - activate(Horizontal); - } - if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { - activate(Vertical); + + if (myAvatar.getHMDLeanRecenterEnabled()) { + if (!isActive(Rotation) && shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix)) { + activate(Rotation); + } + if (!isActive(Horizontal) && shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix)) { + activate(Horizontal); + } + if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { + activate(Vertical); + } } glm::mat4 desiredWorldMatrix = myAvatar.getSensorToWorldMatrix() * _desiredBodyMatrix; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d3da32e0ed..a938aea675 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -69,7 +69,6 @@ class MyAvatar : public Avatar { Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) - Q_PROPERTY(glm::vec3 leftHandPosition READ getLeftHandPosition) Q_PROPERTY(glm::vec3 rightHandPosition READ getRightHandPosition) Q_PROPERTY(glm::vec3 leftHandTipPosition READ getLeftHandTipPosition) @@ -84,6 +83,8 @@ class MyAvatar : public Avatar { Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) + Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) + public: explicit MyAvatar(RigPointer rig); ~MyAvatar(); @@ -123,9 +124,6 @@ public: void setRealWorldFieldOfView(float realWorldFov) { _realWorldFieldOfView.set(realWorldFov); } - void setLeanScale(float scale) { _leanScale = scale; } - float getLeanScale() const { return _leanScale; } - Q_INVOKABLE glm::vec3 getDefaultEyePosition() const; float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } @@ -163,6 +161,9 @@ public: Q_INVOKABLE bool getClearOverlayWhenDriving() const { return _clearOverlayWhenDriving; } Q_INVOKABLE void setClearOverlayWhenDriving(bool on) { _clearOverlayWhenDriving = on; } + Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } + Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + // get/set avatar data void saveData(); void loadData(); @@ -470,6 +471,8 @@ private: ThreadSafeValueCache _leftHandControllerPoseInSensorFrameCache { controller::Pose() }; ThreadSafeValueCache _rightHandControllerPoseInSensorFrameCache { controller::Pose() }; + bool _hmdLeanRecenterEnabled = true; + float AVATAR_MOVEMENT_ENERGY_CONSTANT { 0.001f }; float AUDIO_ENERGY_CONSTANT { 0.000001f }; float MAX_AVATAR_MOVEMENT_PER_FRAME { 30.0f }; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 5deeb545a1..889f0ef36b 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -106,10 +106,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { MyAvatar* myAvatar = static_cast(_owningAvatar); Rig::HeadParameters headParams; - headParams.enableLean = qApp->isHMDMode(); - headParams.leanSideways = head->getFinalLeanSideways(); - headParams.leanForward = head->getFinalLeanForward(); - headParams.torsoTwist = head->getTorsoTwist(); if (qApp->isHMDMode()) { headParams.isInHMD = true; @@ -131,7 +127,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { headParams.worldHeadOrientation = head->getFinalOrientationInWorldFrame(); } - headParams.leanJointIndex = geometry.leanJointIndex; headParams.neckJointIndex = geometry.neckJointIndex; headParams.isTalking = head->getTimeWithoutTalking() <= 1.5f; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index ce7bcc6323..6decef3240 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -129,16 +129,6 @@ void setupPreferences() { preference->setStep(1); preferences->addPreference(preference); } - { - auto getter = [=]()->float { return myAvatar->getLeanScale(); }; - auto setter = [=](float value) { myAvatar->setLeanScale(value); }; - auto preference = new SpinnerPreference(AVATAR_TUNING, "Lean scale (applies to Faceshift users)", getter, setter); - preference->setMin(0); - preference->setMax(99.9f); - preference->setDecimals(2); - preference->setStep(1); - preferences->addPreference(preference); - } { auto getter = [=]()->float { return myAvatar->getUniformScale(); }; auto setter = [=](float value) { myAvatar->setTargetScaleVerbose(value); }; // The hell? diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 9bba9ffc33..b21f5a0e84 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -931,11 +931,6 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { } void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { - if (params.enableLean) { - updateLeanJoint(params.leanJointIndex, params.leanSideways, params.leanForward, params.torsoTwist); - } else { - _animVars.unset("lean"); - } updateNeckJoint(params.neckJointIndex, params); _animVars.set("isTalking", params.isTalking); @@ -953,15 +948,6 @@ static const glm::vec3 X_AXIS(1.0f, 0.0f, 0.0f); static const glm::vec3 Y_AXIS(0.0f, 1.0f, 0.0f); static const glm::vec3 Z_AXIS(0.0f, 0.0f, 1.0f); -void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist) { - if (isIndexValid(index)) { - glm::quat absRot = (glm::angleAxis(-RADIANS_PER_DEGREE * leanSideways, Z_AXIS) * - glm::angleAxis(-RADIANS_PER_DEGREE * leanForward, X_AXIS) * - glm::angleAxis(RADIANS_PER_DEGREE * torsoTwist, Y_AXIS)); - _animVars.set("lean", absRot); - } -} - void Rig::computeHeadNeckAnimVars(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut, glm::vec3& neckPositionOut, glm::quat& neckOrientationOut) const { diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 891d9fdb92..e2193e8479 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -42,15 +42,10 @@ public: }; struct HeadParameters { - float leanSideways = 0.0f; // degrees - float leanForward = 0.0f; // degrees - float torsoTwist = 0.0f; // degrees - bool enableLean = false; glm::quat worldHeadOrientation = glm::quat(); // world space (-z forward) glm::quat rigHeadOrientation = glm::quat(); // rig space (-z forward) glm::vec3 rigHeadPosition = glm::vec3(); // rig space bool isInHMD = false; - int leanJointIndex = -1; int neckJointIndex = -1; bool isTalking = false; }; @@ -222,7 +217,6 @@ protected: void applyOverridePoses(); void buildAbsoluteRigPoses(const AnimPoseVec& relativePoses, AnimPoseVec& absolutePosesOut); - void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist); void updateNeckJoint(int index, const HeadParameters& params); void computeHeadNeckAnimVars(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut, glm::vec3& neckPositionOut, glm::quat& neckOrientationOut) const; diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 1aee85b2cd..72516d9740 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -31,9 +31,6 @@ HeadData::HeadData(AvatarData* owningAvatar) : _baseYaw(0.0f), _basePitch(0.0f), _baseRoll(0.0f), - _leanSideways(0.0f), - _leanForward(0.0f), - _torsoTwist(0.0f), _lookAtPosition(0.0f, 0.0f, 0.0f), _audioLoudness(0.0f), _isFaceTrackerConnected(false), @@ -132,12 +129,6 @@ QJsonObject HeadData::toJson() const { if (getRawOrientation() != quat()) { headJson[JSON_AVATAR_HEAD_ROTATION] = toJsonValue(getRawOrientation()); } - if (getLeanForward() != 0.0f) { - headJson[JSON_AVATAR_HEAD_LEAN_FORWARD] = getLeanForward(); - } - if (getLeanSideways() != 0.0f) { - headJson[JSON_AVATAR_HEAD_LEAN_SIDEWAYS] = getLeanSideways(); - } auto lookat = getLookAtPosition(); if (lookat != vec3()) { vec3 relativeLookAt = glm::inverse(_owningAvatar->getOrientation()) * @@ -171,12 +162,6 @@ void HeadData::fromJson(const QJsonObject& json) { if (json.contains(JSON_AVATAR_HEAD_ROTATION)) { setOrientation(quatFromJsonValue(json[JSON_AVATAR_HEAD_ROTATION])); } - if (json.contains(JSON_AVATAR_HEAD_LEAN_FORWARD)) { - setLeanForward((float)json[JSON_AVATAR_HEAD_LEAN_FORWARD].toDouble()); - } - if (json.contains(JSON_AVATAR_HEAD_LEAN_SIDEWAYS)) { - setLeanSideways((float)json[JSON_AVATAR_HEAD_LEAN_SIDEWAYS].toDouble()); - } if (json.contains(JSON_AVATAR_HEAD_LOOKAT)) { auto relativeLookAt = vec3FromJsonValue(json[JSON_AVATAR_HEAD_LOOKAT]); diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 535aa12847..af657339ba 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -68,17 +68,6 @@ public: const glm::vec3& getLookAtPosition() const { return _lookAtPosition; } void setLookAtPosition(const glm::vec3& lookAtPosition) { _lookAtPosition = lookAtPosition; } - - float getLeanSideways() const { return _leanSideways; } - float getLeanForward() const { return _leanForward; } - float getTorsoTwist() const { return _torsoTwist; } - virtual float getFinalLeanSideways() const { return _leanSideways; } - virtual float getFinalLeanForward() const { return _leanForward; } - - void setLeanSideways(float leanSideways) { _leanSideways = leanSideways; } - void setLeanForward(float leanForward) { _leanForward = leanForward; } - void setTorsoTwist(float torsoTwist) { _torsoTwist = torsoTwist; } - friend class AvatarData; QJsonObject toJson() const; @@ -89,9 +78,6 @@ protected: float _baseYaw; float _basePitch; float _baseRoll; - float _leanSideways; - float _leanForward; - float _torsoTwist; glm::vec3 _lookAtPosition; float _audioLoudness; From 9c63a6417e709dde9b2fa2fae80630802f34887f Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 3 Jun 2016 14:24:07 -0700 Subject: [PATCH 0348/1237] fix attached entities manager and add to default scripts --- scripts/defaultScripts.js | 1 + scripts/system/attachedEntitiesManager.js | 85 ++++++++++++----------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2a050d183e..f460ddf88f 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,3 +21,4 @@ Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); Script.load("system/dialTone.js"); +Script.load("system/attachedEntitiesManager.js"); \ No newline at end of file diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index 79c5973a6e..fcd48b664c 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -22,7 +22,7 @@ var MINIMUM_DROP_DISTANCE_FROM_JOINT = 0.8; var ATTACHED_ENTITY_SEARCH_DISTANCE = 10.0; var ATTACHED_ENTITIES_SETTINGS_KEY = "ATTACHED_ENTITIES"; var DRESSING_ROOM_DISTANCE = 2.0; -var SHOW_TOOL_BAR = true; +var SHOW_TOOL_BAR = false; // tool bar @@ -71,12 +71,11 @@ Script.scriptEnding.connect(scriptEnding); - // attached entites function AttachedEntitiesManager() { - var clothingLocked = true; + var clothingLocked = false; this.subscribeToMessages = function() { Messages.subscribe('Hifi-Object-Manipulation'); @@ -106,7 +105,7 @@ function AttachedEntitiesManager() { // ignore } else if (parsedMessage.action === 'release') { manager.handleEntityRelease(parsedMessage.grabbedEntity, parsedMessage.joint) - // manager.saveAttachedEntities(); + // manager.saveAttachedEntities(); } else if (parsedMessage.action === 'equip') { // manager.saveAttachedEntities(); } else { @@ -156,7 +155,8 @@ function AttachedEntitiesManager() { var wearProps = Entities.getEntityProperties(grabbedEntity); wearProps.parentID = MyAvatar.sessionUUID; wearProps.parentJointIndex = bestJointIndex; - + delete wearProps.localPosition; + delete wearProps.localRotation; var updatePresets = false; if (bestJointOffset && bestJointOffset.constructor === Array) { if (!clothingLocked || bestJointOffset.length < 2) { @@ -170,21 +170,23 @@ function AttachedEntitiesManager() { } Entities.deleteEntity(grabbedEntity); - grabbedEntity = Entities.addEntity(wearProps, true); + //the true boolean here after add entity adds it as an 'avatar entity', which can travel with you from server to server. + + var newEntity = Entities.addEntity(wearProps, true); + if (updatePresets) { - this.updateRelativeOffsets(grabbedEntity); + this.updateRelativeOffsets(newEntity); } } else if (props.parentID != NULL_UUID) { // drop the entity and set it to have no parent (not on the avatar), unless it's being equipped in a hand. if (props.parentID === MyAvatar.sessionUUID && (props.parentJointIndex == MyAvatar.getJointIndex("RightHand") || - props.parentJointIndex == MyAvatar.getJointIndex("LeftHand"))) { + props.parentJointIndex == MyAvatar.getJointIndex("LeftHand"))) { // this is equipped on a hand -- don't clear the parent. } else { var wearProps = Entities.getEntityProperties(grabbedEntity); wearProps.parentID = NULL_UUID; wearProps.parentJointIndex = -1; - delete wearProps.id; delete wearProps.created; delete wearProps.age; @@ -198,7 +200,6 @@ function AttachedEntitiesManager() { delete wearProps.owningAvatarID; delete wearProps.localPosition; delete wearProps.localRotation; - Entities.deleteEntity(grabbedEntity); Entities.addEntity(wearProps); } @@ -220,16 +221,16 @@ function AttachedEntitiesManager() { return false; } - this.toggleLocked = function() { - print("toggleLocked"); - if (clothingLocked) { - clothingLocked = false; - toolBar.setImageURL(Script.resolvePath("assets/images/unlock.svg"), lockButton); - } else { - clothingLocked = true; - toolBar.setImageURL(Script.resolvePath("assets/images/lock.svg"), lockButton); - } - } + // this.toggleLocked = function() { + // print("toggleLocked"); + // if (clothingLocked) { + // clothingLocked = false; + // toolBar.setImageURL(Script.resolvePath("assets/images/unlock.svg"), lockButton); + // } else { + // clothingLocked = true; + // toolBar.setImageURL(Script.resolvePath("assets/images/lock.svg"), lockButton); + // } + // } // this.saveAttachedEntities = function() { // print("--- saving attached entities ---"); @@ -246,27 +247,27 @@ function AttachedEntitiesManager() { // Settings.setValue(ATTACHED_ENTITIES_SETTINGS_KEY, JSON.stringify(saveData)); // } - this.scrubProperties = function(props) { - var toScrub = ["queryAACube", "position", "rotation", - "created", "ageAsText", "naturalDimensions", - "naturalPosition", "velocity", "acceleration", - "angularVelocity", "boundingBox"]; - toScrub.forEach(function(propertyName) { - delete props[propertyName]; - }); - // if the userData has a grabKey, clear old state - if ("userData" in props) { - try { - parsedUserData = JSON.parse(props.userData); - if ("grabKey" in parsedUserData) { - parsedUserData.grabKey.refCount = 0; - delete parsedUserData.grabKey["avatarId"]; - props["userData"] = JSON.stringify(parsedUserData); - } - } catch (e) { - } - } - } + // this.scrubProperties = function(props) { + // var toScrub = ["queryAACube", "position", "rotation", + // "created", "ageAsText", "naturalDimensions", + // "naturalPosition", "velocity", "acceleration", + // "angularVelocity", "boundingBox"]; + // toScrub.forEach(function(propertyName) { + // delete props[propertyName]; + // }); + // // if the userData has a grabKey, clear old state + // if ("userData" in props) { + // try { + // parsedUserData = JSON.parse(props.userData); + // if ("grabKey" in parsedUserData) { + // parsedUserData.grabKey.refCount = 0; + // delete parsedUserData.grabKey["avatarId"]; + // props["userData"] = JSON.stringify(parsedUserData); + // } + // } catch (e) { + // } + // } + // } // this.loadAttachedEntities = function(grabbedEntity) { // print("--- loading attached entities ---"); @@ -302,4 +303,4 @@ function AttachedEntitiesManager() { } var manager = new AttachedEntitiesManager(); -manager.subscribeToMessages(); +manager.subscribeToMessages(); \ No newline at end of file From 3a5d5aab0175667cd48901f5a80e3bdb25fc2f30 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 3 Jun 2016 14:33:50 -0700 Subject: [PATCH 0349/1237] Removed innocuous "AvatarData packet size mismatch" warning This should not have been a warning, it is expected behavior when a BulkAvatarData packet is filled with data from more then one avatar. --- libraries/avatars/src/AvatarData.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 16e4bd5437..283177c93c 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -632,13 +632,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { #endif int numBytesRead = sourceBuffer - startPosition; - - if (numBytesRead != buffer.size()) { - if (shouldLogError(now)) { - qCWarning(avatars) << "AvatarData packet size mismatch: expected " << numBytesRead << " received " << buffer.size(); - } - } - _averageBytesReceived.updateAverage(numBytesRead); return numBytesRead; } From 68819561175d8495be22975dafd39b784ced661e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 3 Jun 2016 14:57:46 -0700 Subject: [PATCH 0350/1237] Basic HMD toggle button. --- scripts/defaultScripts.js | 1 + .../assets/images/tools/hmd-switch-01.svg | 102 ++++++++++++++++++ scripts/system/hmd.js | 57 ++++++++++ 3 files changed, 160 insertions(+) create mode 100644 scripts/system/assets/images/tools/hmd-switch-01.svg create mode 100644 scripts/system/hmd.js diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2a050d183e..b10d1c6f28 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -13,6 +13,7 @@ Script.load("system/progress.js"); Script.load("system/away.js"); Script.load("system/users.js"); Script.load("system/examples.js"); +Script.load("system/hmd.js"); Script.load("system/edit.js"); Script.load("system/selectAudioDevice.js"); Script.load("system/notifications.js"); diff --git a/scripts/system/assets/images/tools/hmd-switch-01.svg b/scripts/system/assets/images/tools/hmd-switch-01.svg new file mode 100644 index 0000000000..15ecb02b6b --- /dev/null +++ b/scripts/system/assets/images/tools/hmd-switch-01.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js new file mode 100644 index 0000000000..277af68315 --- /dev/null +++ b/scripts/system/hmd.js @@ -0,0 +1,57 @@ +// +// hmd.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("libraries/toolBars.js"); + +var headset; // The preferred headset. Default to the first one found in the following list. +var displayMenuName = "Display"; +var desktopMenuItemName = "Desktop"; +['OpenVR (Vive)', 'Oculus Rift'].forEach(function (name) { + if (!headset && Menu.menuItemExists(displayMenuName, name)) { + headset = name; + } +}); + +function initialPosition(windowDimensions, toolbar) { + return { + x: windowDimensions.x / 2 + (2 * Tool.IMAGE_WIDTH), + y: windowDimensions.y + }; +} +var toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.hmd.toolbar", initialPosition, { + x: -Tool.IMAGE_WIDTH / 2, + y: -Tool.IMAGE_HEIGHT +}); +var button = toolBar.addTool({ + imageURL: Script.resolvePath("assets/images/tools/hmd-switch-01.svg"), + subImage: { + x: 0, + y: Tool.IMAGE_WIDTH, + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT + }, + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: 0.9, + visible: true, + showButtonDown: true +}); +function onMousePress (event) { + if (event.isLeftButton && button === toolBar.clicked(Overlays.getOverlayAtPoint(event))) { + var isDesktop = Menu.isOptionChecked(desktopMenuItemName); + Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); + } +}; +Controller.mousePressEvent.connect(onMousePress) +Script.scriptEnding.connect(function () { + Controller.mousePressEvent.disconnect(onMousePress) + toolBar.cleanup(); +}); From 55e5c1f6e0bdbe1e2e23d73296954eaaee371f0b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 2 Jun 2016 18:01:59 -0700 Subject: [PATCH 0351/1237] Declare metadata descriptors --- domain-server/src/DomainMetadata.cpp | 73 +++++++++++++++++++++++----- domain-server/src/DomainMetadata.h | 30 ++++++++++-- domain-server/src/DomainServer.cpp | 9 ++-- 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index c92303ee7b..0481e9911d 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -15,29 +15,77 @@ #include "DomainServerNodeData.h" -const QString DomainMetadata::USERS_KEY = "users"; -const QString DomainMetadata::USERS_NUM_KEY = "num_users"; -const QString DomainMetadata::USERS_HOSTNAMES_KEY = "users_hostnames"; +const QString DomainMetadata::USERS = "users"; +const QString DomainMetadata::USERS_NUM_TOTAL = "num_users"; +const QString DomainMetadata::USERS_NUM_ANON = "num_anon_users"; +const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames"; +// users metadata will appear as (JSON): +// { "num_users": Number, +// "num_anon_users": Number, +// "user_hostnames": { : Number } +// } -DomainMetadata::DomainMetadata() : - _metadata{{ USERS_KEY, {} }} { +const QString DomainMetadata::DESCRIPTORS = "descriptors"; +const QString DomainMetadata::DESCRIPTORS_DESCRIPTION = "description"; +const QString DomainMetadata::DESCRIPTORS_CAPACITY = "capacity"; // parsed from security +const QString DomainMetadata::DESCRIPTORS_RESTRICTION = "restriction"; // parsed from ACL +const QString DomainMetadata::DESCRIPTORS_MATURITY = "maturity"; +const QString DomainMetadata::DESCRIPTORS_HOSTS = "hosts"; +const QString DomainMetadata::DESCRIPTORS_TAGS = "tags"; +// descriptors metadata will appear as (JSON): +// { "capacity": Number, +// TODO: "hours": String, // UTF-8 representation of the week, split into 15" segments +// "restriction": String, // enum of either OPEN, RESTRICTED_HIFI, RESTRICTED_ACL +// "maturity": String, // enum corresponding to ESRB ratings +// "hosts": [ String ], // capped list of usernames +// "description": String, // capped description +// TODO: "img": { +// "src": String, +// "type": String, +// "size": Number, +// "updated_at": Number, +// }, +// "tags": [ String ], // capped list of tags +// } + +// metadata will appear as (JSON): +// { users: , descriptors: } +// +// it is meant to be sent to and consumed by an external API + +DomainMetadata::DomainMetadata() { + _metadata[USERS] = {}; + _metadata[DESCRIPTORS] = {}; +} + +void DomainMetadata::setDescriptors(QVariantMap& settings) { + // TODO + + QVariantMap descriptors; + + #if DEV_BUILD || PR_BUILD + qDebug() << "Regenerated domain metadata - descriptors:" << descriptors; +#endif } void DomainMetadata::usersChanged() { static const QString DEFAULT_HOSTNAME = "*"; auto nodeList = DependencyManager::get(); - int numConnectedUnassigned = 0; + int numConnected = 0; + int numConnectedAnonymously = 0; QVariantMap userHostnames; // figure out the breakdown of currently connected interface clients - nodeList->eachNode([&numConnectedUnassigned, &userHostnames](const SharedNodePointer& node) { + nodeList->eachNode([&numConnected, &numConnectedAnonymously, &userHostnames](const SharedNodePointer& node) { auto linkedData = node->getLinkedData(); if (linkedData) { auto nodeData = static_cast(linkedData); if (!nodeData->wasAssigned()) { - ++numConnectedUnassigned; + ++numConnected; + + // TODO: numConnectedAnonymously // increment the count for this hostname (or the default if we don't have one) auto placeName = nodeData->getPlaceName(); @@ -47,10 +95,13 @@ void DomainMetadata::usersChanged() { } }); - QVariantMap users = {{ USERS_NUM_KEY, numConnectedUnassigned }, { USERS_HOSTNAMES_KEY, userHostnames }}; - _metadata[USERS_KEY] = users; + QVariantMap users = { + { USERS_NUM_TOTAL, numConnected }, + { USERS_NUM_ANON, numConnectedAnonymously }, + { USERS_HOSTNAMES, userHostnames }}; + _metadata[USERS] = users; -#if DEV_BUILD +#if DEV_BUILD || PR_BUILD qDebug() << "Regenerated domain metadata - users:" << users; #endif } diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index ae6dfe10a1..e2f4674afc 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -14,16 +14,36 @@ #include #include -class DomainMetadata { - static const QString USERS_KEY; - static const QString USERS_NUM_KEY; - static const QString USERS_HOSTNAMES_KEY; +class DomainMetadata : public QObject { +Q_OBJECT + + static const QString USERS; + static const QString USERS_NUM_TOTAL; + static const QString USERS_NUM_ANON; + static const QString USERS_HOSTNAMES; + + static const QString DESCRIPTORS; + static const QString DESCRIPTORS_DESCRIPTION; + static const QString DESCRIPTORS_CAPACITY; + static const QString DESCRIPTORS_HOURS; + static const QString DESCRIPTORS_RESTRICTION; + static const QString DESCRIPTORS_MATURITY; + static const QString DESCRIPTORS_HOSTS; + static const QString DESCRIPTORS_TAGS; + static const QString DESCRIPTORS_IMG; + static const QString DESCRIPTORS_IMG_SRC; + static const QString DESCRIPTORS_IMG_TYPE; + static const QString DESCRIPTORS_IMG_SIZE; + static const QString DESCRIPTORS_IMG_UPDATED_AT; public: DomainMetadata(); QJsonObject get() { return QJsonObject::fromVariantMap(_metadata); } - QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS_KEY].toMap()); } + QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS].toMap()); } + QJsonObject getDescriptors() { return QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); } + + void setDescriptors(QVariantMap& settings); public slots: void usersChanged(); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c24fd02727..88c4e215b2 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -94,13 +94,13 @@ DomainServer::DomainServer(int argc, char* argv[]) : qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); - // make sure we hear about newly connected nodes from our gatekeeper - connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); - // update the metadata when a user (dis)connects connect(this, &DomainServer::userConnected, &_metadata, &DomainMetadata::usersChanged); connect(this, &DomainServer::userDisconnected, &_metadata, &DomainMetadata::usersChanged); + // make sure we hear about newly connected nodes from our gatekeeper + connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -116,6 +116,9 @@ DomainServer::DomainServer(int argc, char* argv[]) : optionallyGetTemporaryName(args); } + + // update the metadata with current descriptors + _metadata.setDescriptors(_settingsManager.getSettingsMap()); } DomainServer::~DomainServer() { From 209ace1b867ae8cf64678e3b983c5137e7407ec7 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 3 Jun 2016 11:11:06 -0700 Subject: [PATCH 0352/1237] Include anonymously connected user metadata --- domain-server/src/DomainMetadata.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 0481e9911d..75ff0d697c 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -85,7 +85,9 @@ void DomainMetadata::usersChanged() { if (!nodeData->wasAssigned()) { ++numConnected; - // TODO: numConnectedAnonymously + if (nodeData->getUsername().isEmpty()) { + ++numConnectedAnonymously; + } // increment the count for this hostname (or the default if we don't have one) auto placeName = nodeData->getPlaceName(); From e04c6e8c44f716ac8497b3e2d6a9f78a63a4b709 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 3 Jun 2016 14:36:43 -0700 Subject: [PATCH 0353/1237] Add description/hosts/rating to settings UI --- .../resources/describe-settings.json | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index cac0d28e1e..ba00392cd7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.2, + "version": 1.3, "settings": [ { "name": "metaverse", @@ -71,6 +71,76 @@ } ] }, + { + "name": "descriptors", + "label": "Description", + "help": "This data will be queryable from your server. It may be collected by High Fidelity and used to share your domain with others.", + "settings": [ + { + "name": "description", + "label": "Description", + "help": "A description of your domain (256 character limit)." + }, + { + "name": "maturity", + "label": "Maturity", + "help": "A maturity rating, available as a guideline for content on your domain.", + "default": "unrated", + "type": "select", + "options": [ + { + "value": "unrated", + "label": "Unrated" + }, + { + "value": "everyone", + "label": "Everyone" + }, + { + "value": "teen", + "label": "Teen (13+)" + }, + { + "value": "mature", + "label": "Mature (17+)" + }, + { + "value": "adult", + "label": "Adult (18+)" + } + ] + + }, + { + "name": "hosts", + "label": "Hosts", + "type": "table", + "help": "Usernames of hosts who can reliably show your domain to new visitors.", + "numbered": false, + "columns": [ + { + "name": "host", + "label": "Username", + "can_set": true + } + ] + }, + { + "name": "tags", + "label": "Tags", + "type": "table", + "help": "Common categories under which your domain falls.", + "numbered": false, + "columns": [ + { + "name": "tag", + "label": "Tag", + "can_set": true + } + ] + } + ] + }, { "name": "security", "label": "Security", From 2367cb199566d8054a30c3c0435ba9143b7d837d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 3 Jun 2016 15:07:21 -0700 Subject: [PATCH 0354/1237] fixing input mapping for vive/touch grip button/trigger, script needs fixing --- .../resources/controllers/oculus_touch.json | 6 +-- interface/resources/controllers/vive.json | 4 +- .../src/controllers/StandardControls.h | 8 ++-- .../oculus/src/OculusControllerManager.cpp | 21 +-------- plugins/openvr/src/ViveControllerManager.cpp | 2 +- .../system/controllers/handControllerGrab.js | 47 ++++++++++++++----- 6 files changed, 44 insertions(+), 44 deletions(-) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index 8cb512a526..b59bf54e5b 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -10,16 +10,14 @@ { "from": "OculusTouch.LX", "to": "Standard.LX" }, { "from": "OculusTouch.LT", "to": "Standard.LT" }, { "from": "OculusTouch.LS", "to": "Standard.LS" }, - { "from": "OculusTouch.LG", "to": "Standard.LG" }, - { "from": "OculusTouch.LeftGrip", "to": "Standard.LB" }, + { "from": "OculusTouch.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, { "from": "OculusTouch.RY", "filters": "invert", "to": "Standard.RY" }, { "from": "OculusTouch.RX", "to": "Standard.RX" }, { "from": "OculusTouch.RT", "to": "Standard.RT" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, - { "from": "OculusTouch.RG", "to": "Standard.RG" }, - { "from": "OculusTouch.RightGrip", "to": "Standard.RB" }, + { "from": "OculusTouch.RightGrip", "to": "Standard.RightGrip" }, { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 60a46ba3ce..dc3ca3755e 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -5,7 +5,7 @@ { "from": "Vive.LX", "when": "Vive.LSOuter", "to": "Standard.LX" }, { "from": "Vive.LT", "to": "Standard.LT" }, - { "from": "Vive.LeftGrip", "to": "Standard.LB" }, + { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, @@ -13,7 +13,7 @@ { "from": "Vive.RX", "when": "Vive.RSOuter", "to": "Standard.RX" }, { "from": "Vive.RT", "to": "Standard.RT" }, - { "from": "Vive.RightGrip", "to": "Standard.RB" }, + { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index 79c23bc6ee..d7eb3de2c2 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -66,9 +66,7 @@ namespace controller { RIGHT_SECONDARY_INDEX_TOUCH, RIGHT_INDEX_POINT, - LEFT_GRIP, LEFT_GRIP_TOUCH, - RIGHT_GRIP, RIGHT_GRIP_TOUCH, NUM_STANDARD_BUTTONS @@ -85,9 +83,9 @@ namespace controller { // Triggers LT, RT, - // Grips (Oculus touch squeeze) - LG, - RG, + // Grips + LEFT_GRIP, + RIGHT_GRIP, NUM_STANDARD_AXES, LZ = LT, RZ = RT diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index e84fdcbfc7..9eadb83ea7 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -173,11 +173,6 @@ void OculusControllerManager::RemoteDevice::focusOutEvent() { } void OculusControllerManager::TouchDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { - // Check past values of button map for hysteresis before clearing map - const float HYSTERESIS_OFFSET = -0.1f; - float LEFT_HYSTERESIS_OFFSET = _buttonPressedMap.find(LEFT_GRIP) != _buttonPressedMap.end() ? HYSTERESIS_OFFSET : 0.0f; - float RIGHT_HYSTERESIS_OFFSET = _buttonPressedMap.find(RIGHT_GRIP) != _buttonPressedMap.end() ? HYSTERESIS_OFFSET : 0.0f; - _poseStateMap.clear(); _buttonPressedMap.clear(); @@ -198,12 +193,12 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control _axisStateMap[LX] = inputState.Thumbstick[ovrHand_Left].x; _axisStateMap[LY] = inputState.Thumbstick[ovrHand_Left].y; _axisStateMap[LT] = inputState.IndexTrigger[ovrHand_Left]; - _axisStateMap[LG] = inputState.HandTrigger[ovrHand_Left]; + _axisStateMap[LEFT_GRIP] = inputState.HandTrigger[ovrHand_Left]; _axisStateMap[RX] = inputState.Thumbstick[ovrHand_Right].x; _axisStateMap[RY] = inputState.Thumbstick[ovrHand_Right].y; _axisStateMap[RT] = inputState.IndexTrigger[ovrHand_Right]; - _axisStateMap[RG] = inputState.HandTrigger[ovrHand_Right]; + _axisStateMap[RIGHT_GRIP] = inputState.HandTrigger[ovrHand_Right]; // Buttons for (const auto& pair : BUTTON_MAP) { @@ -211,16 +206,6 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control _buttonPressedMap.insert(pair.second); } } - // Map pressed hand triggers to grip buttons - // This is temporary in order to support the grab/equip scripts - const float handTriggerThreshold = 0.9f; - if (inputState.HandTrigger[ovrHand_Left] >= handTriggerThreshold + LEFT_HYSTERESIS_OFFSET) { - _buttonPressedMap.insert(LEFT_GRIP); - } - if (inputState.HandTrigger[ovrHand_Right] >= handTriggerThreshold + RIGHT_HYSTERESIS_OFFSET) { - _buttonPressedMap.insert(RIGHT_GRIP); - } - // Touches for (const auto& pair : TOUCH_MAP) { if (inputState.Touches & pair.first) { @@ -348,8 +333,6 @@ controller::Input::NamedVector OculusControllerManager::TouchDevice::getAvailabl //makePair(RB, "RB"), // side grip triggers - makePair(LG, "LG"), - makePair(RG, "RG"), makePair(LEFT_GRIP, "LeftGrip"), makePair(RIGHT_GRIP, "RightGrip"), diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 6e75454b5f..bddbb91316 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -342,7 +342,7 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint if (button == vr::k_EButton_ApplicationMenu) { _buttonPressedMap.insert(isLeftHand ? LEFT_APP_MENU : RIGHT_APP_MENU); } else if (button == vr::k_EButton_Grip) { - _buttonPressedMap.insert(isLeftHand ? LEFT_GRIP : RIGHT_GRIP); + _axisStateMap[isLeftHand ? LEFT_GRIP : RIGHT_GRIP] = 1.0f; } else if (button == vr::k_EButton_SteamVR_Trigger) { _buttonPressedMap.insert(isLeftHand ? LT : RT); } else if (button == vr::k_EButton_SteamVR_Touchpad) { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 06549a38b5..4f4b7e32ad 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -33,6 +33,9 @@ var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; +var GRIP_ON_VALUE = 0.9; +var GRIP_OFF_VALUE = 0.8; + var THUMB_ON_VALUE = 0.5; var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. @@ -271,6 +274,7 @@ function MyController(hand) { this.triggerValue = 0; // rolling average of trigger value this.rawTriggerValue = 0; + this.rawGripValue = 0; this.rawBumperValue = 0; this.rawThumbValue = 0; @@ -509,10 +513,10 @@ function MyController(hand) { var searchSphereLocation = Vec3.sum(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, - (this.triggerSmoothedGrab() || this.bumperSqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || (this.bumperSqueezed() || this.gripSqueezed())) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); if ((USE_OVERLAY_LINES_FOR_SEARCHING === true) && PICK_WITH_HAND_RAY) { this.overlayLineOn(handPosition, searchSphereLocation, - (this.triggerSmoothedGrab() || this.bumperSqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || (this.bumperSqueezed() || this.gripSqueezed())) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } } @@ -772,6 +776,10 @@ function MyController(hand) { _this.rawBumperValue = value; }; + this.gripPress = function(value) { + _this.rawGripValue = value; + }; + this.updateSmoothedTrigger = function() { var triggerValue = this.rawTriggerValue; // smooth out trigger value @@ -799,6 +807,14 @@ function MyController(hand) { return _this.rawBumperValue < BUMPER_ON_VALUE; }; + this.gripSqueezed = function() { + return _this.rawGripValue > GRIP_ON_VALUE; + }; + + this.gripReleased = function() { + return _this.rawGripValue < GRIP_OFF_VALUE; + }; + // this.triggerOrBumperSqueezed = function() { // return triggerSmoothedSqueezed() || bumperSqueezed(); // } @@ -820,13 +836,13 @@ function MyController(hand) { }; this.off = function() { - if (this.triggerSmoothedSqueezed() || this.bumperSqueezed()) { + if (this.triggerSmoothedSqueezed() || (this.bumperSqueezed() || this.gripSqueezed())) { this.lastPickTime = 0; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { this.setState(STATE_SEARCHING); - } else if (this.bumperSqueezed()) { + } else if (this.bumperSqueezed() || this.gripSqueezed()) { this.setState(STATE_HOLD_SEARCHING); } } @@ -839,11 +855,13 @@ function MyController(hand) { this.checkForStrayChildren(); + print("bumper: " + this.bumperReleased() + " grip: " + this.gripReleased()); + if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { this.setState(STATE_RELEASE); return; } - if (this.state == STATE_HOLD_SEARCHING && this.bumperReleased()) { + if (this.state == STATE_HOLD_SEARCHING && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); return; } @@ -1000,7 +1018,7 @@ function MyController(hand) { grabbableData = grabbableDataForCandidate; } } - if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.bumperSqueezed())) { + if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || (this.bumperSqueezed() || this.gripSqueezed()))) { // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. var near = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; var isPhysical = this.propsArePhysical(props); @@ -1166,7 +1184,7 @@ function MyController(hand) { }; this.continueDistanceHolding = function() { - if (this.triggerSmoothedReleased() && this.bumperReleased()) { + if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); return; @@ -1390,7 +1408,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("releaseGrab"); return; } - if (this.state == STATE_HOLD && this.bumperReleased()) { + if (this.state == STATE_HOLD && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); return; @@ -1504,7 +1522,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("releaseGrab"); return; } - if (this.state == STATE_CONTINUE_HOLD && this.bumperReleased()) { + if (this.state == STATE_CONTINUE_HOLD && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseEquip"); return; @@ -1633,7 +1651,7 @@ function MyController(hand) { }; this.nearTrigger = function() { - if (this.triggerSmoothedReleased() && this.bumperReleased()) { + if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; @@ -1643,7 +1661,7 @@ function MyController(hand) { }; this.farTrigger = function() { - if (this.triggerSmoothedReleased() && this.bumperReleased()) { + if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; @@ -1653,7 +1671,7 @@ function MyController(hand) { }; this.continueNearTrigger = function() { - if (this.triggerSmoothedReleased() && this.bumperReleased()) { + if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; @@ -1662,7 +1680,7 @@ function MyController(hand) { }; this.continueFarTrigger = function() { - if (this.triggerSmoothedReleased() && this.bumperReleased()) { + if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; @@ -1945,6 +1963,9 @@ mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress); mapping.from([Controller.Standard.RB]).peek().to(rightController.bumperPress); mapping.from([Controller.Standard.LB]).peek().to(leftController.bumperPress); +mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.gripPress); +mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.gripPress); + mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); From 09e0a2ced70d03fa1e4e17b74dbd9e25f41c89df Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 3 Jun 2016 14:59:58 -0700 Subject: [PATCH 0355/1237] Parse basic metadata into DomainMetadata --- domain-server/src/DomainMetadata.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 75ff0d697c..8fdbc2bbd1 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -10,6 +10,7 @@ #include "DomainMetadata.h" +#include #include #include @@ -35,7 +36,7 @@ const QString DomainMetadata::DESCRIPTORS_TAGS = "tags"; // descriptors metadata will appear as (JSON): // { "capacity": Number, // TODO: "hours": String, // UTF-8 representation of the week, split into 15" segments -// "restriction": String, // enum of either OPEN, RESTRICTED_HIFI, RESTRICTED_ACL +// "restriction": String, // enum of either open, hifi, or acl // "maturity": String, // enum corresponding to ESRB ratings // "hosts": [ String ], // capped list of usernames // "description": String, // capped description @@ -59,11 +60,25 @@ DomainMetadata::DomainMetadata() { } void DomainMetadata::setDescriptors(QVariantMap& settings) { - // TODO + const QString CAPACITY = "security.maximum_user_capacity"; + const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); + unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; - QVariantMap descriptors; + // TODO: Keep parity with ACL development. + const QString RESTRICTION = "security.restricted_access"; + const QString RESTRICTION_OPEN = "open"; + // const QString RESTRICTION_HIFI = "hifi"; + const QString RESTRICTION_ACL = "acl"; + const QVariant* isRestrictedVariant = valueForKeyPath(settings, RESTRICTION); + bool isRestricted = isRestrictedVariant ? isRestrictedVariant->toBool() : false; + QString restriction = isRestricted ? RESTRICTION_ACL : RESTRICTION_OPEN; - #if DEV_BUILD || PR_BUILD + QVariantMap descriptors = settings[DESCRIPTORS].toMap(); + descriptors[DESCRIPTORS_CAPACITY] = capacity; + descriptors[DESCRIPTORS_RESTRICTION] = restriction; + _metadata[DESCRIPTORS] = descriptors; + +#if DEV_BUILD || PR_BUILD qDebug() << "Regenerated domain metadata - descriptors:" << descriptors; #endif } From da023bfbe36d86c52a14b435651160459e3314fe Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 3 Jun 2016 15:40:04 -0700 Subject: [PATCH 0356/1237] leap --- scripts/system/controllers/leapHands.js | 527 ++++++++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 scripts/system/controllers/leapHands.js diff --git a/scripts/system/controllers/leapHands.js b/scripts/system/controllers/leapHands.js new file mode 100644 index 0000000000..1be0b1e5f6 --- /dev/null +++ b/scripts/system/controllers/leapHands.js @@ -0,0 +1,527 @@ +// +// leapHands.js +// examples +// +// Created by David Rowe on 8 Sep 2014. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example script that uses the Leap Motion to make the avatar's hands replicate the user's hand actions. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var leftTriggerValue = 0; +var rightTriggerValue = 0; + +var LEAP_TRIGGER_START_ANGLE = 15.0; +var LEAP_TRIGGER_END_ANGLE = 40.0; + +function getLeapMotionLeftTrigger() { + //print("left trigger = " + leftTriggerValue); + return leftTriggerValue; +} +function getLeapMotionRightTrigger() { + //print("right trigger = " + rightTriggerValue); + return rightTriggerValue; +} + +var leapHands = (function () { + + var isOnHMD, + LEAP_ON_HMD_MENU_ITEM = "Leap Motion on HMD", + LEAP_OFFSET = 0.019, // Thickness of Leap Motion plus HMD clip + HMD_OFFSET = 0.070, // Eyeballs to front surface of Oculus DK2 TODO: Confirm and make depend on device and eye relief + hasHandAndWristJoints, + handToWristOffset = [], // For avatars without a wrist joint we control an estimate of a proper hand joint position + HAND_OFFSET = 0.4, // Relative distance of wrist to hand versus wrist to index finger knuckle + handAnimationStateHandlers, + handAnimationStateFunctions, + handAnimationStateProperties, + hands, + wrists, + NUM_HANDS = 2, // 0 = left; 1 = right + fingers, + NUM_FINGERS = 5, // 0 = thumb; ...; 4 = pinky + THUMB = 0, + MIDDLE_FINGER = 2, + NUM_FINGER_JOINTS = 3, // 0 = metacarpal(hand)-proximal(finger) joint; ...; 2 = intermediate-distal joint + MAX_HAND_INACTIVE_COUNT = 20, + calibrationStatus, + UNCALIBRATED = 0, + CALIBRATING = 1, + CALIBRATED = 2, + CALIBRATION_TIME = 1000, // milliseconds + avatarScale, + avatarFaceModelURL, + avatarSkeletonModelURL, + settingsTimer, + HMD_CAMERA_TO_AVATAR_ROTATION = [ + Quat.angleAxis(180.0, { x: 0, y: 0, z: 1 }), + Quat.angleAxis(-180.0, { x: 0, y: 0, z: 1 }) + ], + DESKTOP_CAMERA_TO_AVATAR_ROTATION = + Quat.multiply(Quat.angleAxis(180.0, { x: 0, y: 1, z: 0 }), Quat.angleAxis(90.0, { x: 0, y: 0, z: 1 })), + LEAP_THUMB_ROOT_ADJUST = [Quat.fromPitchYawRollDegrees(0, 0, 20), Quat.fromPitchYawRollDegrees(0, 0, -20)]; + + function printSkeletonJointNames() { + var jointNames, + i; + + print(MyAvatar.skeletonModelURL); + + print("Skeleton joint names ..."); + jointNames = MyAvatar.getJointNames(); + for (i = 0; i < jointNames.length; i += 1) { + print(i + ": " + jointNames[i]); + } + print("... skeleton joint names"); + } + + function animateLeftHand() { + var ROTATION_AND_POSITION = 0; + + return { + leftHandType: ROTATION_AND_POSITION, + leftHandPosition: hands[0].position, + leftHandRotation: hands[0].rotation + }; + } + + function animateRightHand() { + var ROTATION_AND_POSITION = 0; + + return { + rightHandType: ROTATION_AND_POSITION, + rightHandPosition: hands[1].position, + rightHandRotation: hands[1].rotation + }; + } + + function finishCalibration() { + var avatarPosition, + handPosition, + middleFingerPosition, + leapHandHeight, + h; + + if (!isOnHMD) { + if (hands[0].controller.isActive() && hands[1].controller.isActive()) { + leapHandHeight = (hands[0].controller.getAbsTranslation().y + hands[1].controller.getAbsTranslation().y) / 2.0; + } else { + calibrationStatus = UNCALIBRATED; + return; + } + } + + avatarPosition = MyAvatar.position; + + for (h = 0; h < NUM_HANDS; h += 1) { + handPosition = MyAvatar.getJointPosition(hands[h].jointName); + if (!hasHandAndWristJoints) { + middleFingerPosition = MyAvatar.getJointPosition(fingers[h][MIDDLE_FINGER][0].jointName); + handToWristOffset[h] = Vec3.multiply(Vec3.subtract(handPosition, middleFingerPosition), 1.0 - HAND_OFFSET); + } + + if (isOnHMD) { + // Offset of Leap Motion origin from physical eye position + hands[h].zeroPosition = { x: 0.0, y: 0.0, z: HMD_OFFSET + LEAP_OFFSET }; + } else { + hands[h].zeroPosition = { + x: handPosition.x - avatarPosition.x, + y: handPosition.y - avatarPosition.y, + z: avatarPosition.z - handPosition.z + }; + hands[h].zeroPosition = Vec3.multiplyQbyV(MyAvatar.orientation, hands[h].zeroPosition); + hands[h].zeroPosition.y = hands[h].zeroPosition.y - leapHandHeight; + } + } + + MyAvatar.clearJointData("LeftHand"); + MyAvatar.clearJointData("LeftForeArm"); + MyAvatar.clearJointData("RightHand"); + MyAvatar.clearJointData("RightForeArm"); + + calibrationStatus = CALIBRATED; + print("Leap Motion: Calibrated"); + } + + function calibrate() { + var jointNames, + i; + + calibrationStatus = CALIBRATING; + + avatarScale = MyAvatar.scale; + avatarFaceModelURL = MyAvatar.faceModelURL; + avatarSkeletonModelURL = MyAvatar.skeletonModelURL; + + // Does this skeleton have both wrist and hand joints? + hasHandAndWristJoints = false; + jointNames = MyAvatar.getJointNames(); + for (i = 0; i < jointNames.length; i += 1) { + hasHandAndWristJoints = hasHandAndWristJoints || jointNames[i].toLowerCase() === "leftwrist"; + } + + // Set avatar arms vertical, forearms horizontal, as "zero" position for calibration + MyAvatar.setJointRotation("LeftForeArm", Quat.fromPitchYawRollDegrees(0.0, 0.0, 90.0)); + MyAvatar.setJointRotation("LeftHand", Quat.fromPitchYawRollDegrees(0.0, 90.0, 0.0)); + MyAvatar.setJointRotation("RightForeArm", Quat.fromPitchYawRollDegrees(0.0, 0.0, -90.0)); + MyAvatar.setJointRotation("RightHand", Quat.fromPitchYawRollDegrees(0.0, -90.0, 0.0)); + + // Wait for arms to assume their positions before calculating + Script.setTimeout(finishCalibration, CALIBRATION_TIME); + } + + function checkCalibration() { + + if (calibrationStatus === CALIBRATED) { + return true; + } + + if (calibrationStatus !== CALIBRATING) { + calibrate(); + } + + return false; + } + + function setIsOnHMD() { + isOnHMD = Menu.isOptionChecked(LEAP_ON_HMD_MENU_ITEM); + print("Leap Motion: " + (isOnHMD ? "Is on HMD" : "Is on desk")); + } + + function checkSettings() { + if (calibrationStatus > UNCALIBRATED && (MyAvatar.scale !== avatarScale + || MyAvatar.faceModelURL !== avatarFaceModelURL + || MyAvatar.skeletonModelURL !== avatarSkeletonModelURL + || Menu.isOptionChecked(LEAP_ON_HMD_MENU_ITEM) !== isOnHMD)) { + print("Leap Motion: Recalibrate..."); + calibrationStatus = UNCALIBRATED; + + setIsOnHMD(); + } + } + + function setUp() { + + wrists = [ + { + jointName: "LeftWrist", + controller: Controller.createInputController("Spatial", "joint_L_wrist") + }, + { + jointName: "RightWrist", + controller: Controller.createInputController("Spatial", "joint_R_wrist") + } + ]; + + hands = [ + { + jointName: "LeftHand", + controller: Controller.createInputController("Spatial", "joint_L_hand"), + inactiveCount: 0 + }, + { + jointName: "RightHand", + controller: Controller.createInputController("Spatial", "joint_R_hand"), + inactiveCount: 0 + } + ]; + + // The Leap controller's first joint is the hand-metacarpal joint but this joint's data is not used because it's too + // dependent on the model skeleton exactly matching the Leap skeleton; using just the second and subsequent joints + // seems to work better over all. + fingers = [{}, {}]; + fingers[0] = [ + [ + { jointName: "LeftHandThumb1", controller: Controller.createInputController("Spatial", "joint_L_thumb2") }, + { jointName: "LeftHandThumb2", controller: Controller.createInputController("Spatial", "joint_L_thumb3") }, + { jointName: "LeftHandThumb3", controller: Controller.createInputController("Spatial", "joint_L_thumb4") } + ], + [ + { jointName: "LeftHandIndex1", controller: Controller.createInputController("Spatial", "joint_L_index2") }, + { jointName: "LeftHandIndex2", controller: Controller.createInputController("Spatial", "joint_L_index3") }, + { jointName: "LeftHandIndex3", controller: Controller.createInputController("Spatial", "joint_L_index4") } + ], + [ + { jointName: "LeftHandMiddle1", controller: Controller.createInputController("Spatial", "joint_L_middle2") }, + { jointName: "LeftHandMiddle2", controller: Controller.createInputController("Spatial", "joint_L_middle3") }, + { jointName: "LeftHandMiddle3", controller: Controller.createInputController("Spatial", "joint_L_middle4") } + ], + [ + { jointName: "LeftHandRing1", controller: Controller.createInputController("Spatial", "joint_L_ring2") }, + { jointName: "LeftHandRing2", controller: Controller.createInputController("Spatial", "joint_L_ring3") }, + { jointName: "LeftHandRing3", controller: Controller.createInputController("Spatial", "joint_L_ring4") } + ], + [ + { jointName: "LeftHandPinky1", controller: Controller.createInputController("Spatial", "joint_L_pinky2") }, + { jointName: "LeftHandPinky2", controller: Controller.createInputController("Spatial", "joint_L_pinky3") }, + { jointName: "LeftHandPinky3", controller: Controller.createInputController("Spatial", "joint_L_pinky4") } + ] + ]; + fingers[1] = [ + [ + { jointName: "RightHandThumb1", controller: Controller.createInputController("Spatial", "joint_R_thumb2") }, + { jointName: "RightHandThumb2", controller: Controller.createInputController("Spatial", "joint_R_thumb3") }, + { jointName: "RightHandThumb3", controller: Controller.createInputController("Spatial", "joint_R_thumb4") } + ], + [ + { jointName: "RightHandIndex1", controller: Controller.createInputController("Spatial", "joint_R_index2") }, + { jointName: "RightHandIndex2", controller: Controller.createInputController("Spatial", "joint_R_index3") }, + { jointName: "RightHandIndex3", controller: Controller.createInputController("Spatial", "joint_R_index4") } + ], + [ + { jointName: "RightHandMiddle1", controller: Controller.createInputController("Spatial", "joint_R_middle2") }, + { jointName: "RightHandMiddle2", controller: Controller.createInputController("Spatial", "joint_R_middle3") }, + { jointName: "RightHandMiddle3", controller: Controller.createInputController("Spatial", "joint_R_middle4") } + ], + [ + { jointName: "RightHandRing1", controller: Controller.createInputController("Spatial", "joint_R_ring2") }, + { jointName: "RightHandRing2", controller: Controller.createInputController("Spatial", "joint_R_ring3") }, + { jointName: "RightHandRing3", controller: Controller.createInputController("Spatial", "joint_R_ring4") } + ], + [ + { jointName: "RightHandPinky1", controller: Controller.createInputController("Spatial", "joint_R_pinky2") }, + { jointName: "RightHandPinky2", controller: Controller.createInputController("Spatial", "joint_R_pinky3") }, + { jointName: "RightHandPinky3", controller: Controller.createInputController("Spatial", "joint_R_pinky4") } + ] + ]; + + handAnimationStateHandlers = [null, null]; + handAnimationStateFunctions = [animateLeftHand, animateRightHand]; + handAnimationStateProperties = [ + ["leftHandType", "leftHandPosition", "leftHandRotation"], + ["rightHandType", "rightHandPosition", "rightHandPosition"] + ]; + + setIsOnHMD(); + + settingsTimer = Script.setInterval(checkSettings, 2000); + + calibrationStatus = UNCALIBRATED; + + { + var mapping = Controller.newMapping("LeapmotionTrigger"); + mapping.from(getLeapMotionLeftTrigger).to(Controller.Standard.LT); + mapping.from(getLeapMotionRightTrigger).to(Controller.Standard.RT); + mapping.enable(); + } + } + + function moveHands() { + var h, + i, + j, + side, + handOffset, + wristOffset, + handRotation, + locRotation, + cameraOrientation, + inverseAvatarOrientation; + + for (h = 0; h < NUM_HANDS; h += 1) { + side = h === 0 ? -1.0 : 1.0; + + if (hands[h].controller.isActive()) { + + // Calibrate if necessary. + if (!checkCalibration()) { + return; + } + + // Hand animation handlers ... + if (handAnimationStateHandlers[h] === null) { + handAnimationStateHandlers[h] = MyAvatar.addAnimationStateHandler(handAnimationStateFunctions[h], + handAnimationStateProperties[h]); + } + + // Hand position ... + handOffset = hands[h].controller.getAbsTranslation(); + handRotation = hands[h].controller.getAbsRotation(); + + if (isOnHMD) { + + // Adjust to control wrist position if "hand" joint is at wrist ... + if (!hasHandAndWristJoints) { + wristOffset = Vec3.multiplyQbyV(handRotation, handToWristOffset[h]); + handOffset = Vec3.sum(handOffset, wristOffset); + } + + // Hand offset in camera coordinates ... + handOffset = { + x: -handOffset.x, + y: -handOffset.z, + z: -handOffset.y - hands[h].zeroPosition.z + }; + + // Hand offset in world coordinates ... + cameraOrientation = Camera.getOrientation(); + handOffset = Vec3.sum(Camera.getPosition(), Vec3.multiplyQbyV(cameraOrientation, handOffset)); + + // Hand offset in avatar coordinates ... + inverseAvatarOrientation = Quat.inverse(MyAvatar.orientation); + handOffset = Vec3.subtract(handOffset, MyAvatar.position); + handOffset = Vec3.multiplyQbyV(inverseAvatarOrientation, handOffset); + handOffset.z = -handOffset.z; + handOffset.x = -handOffset.x; + + + // Hand rotation in camera coordinates ... + handRotation = { + x: -handRotation.y, + y: -handRotation.z, + z: -handRotation.x, + w: handRotation.w + }; + + // Hand rotation in avatar coordinates ... + handRotation = Quat.multiply(HMD_CAMERA_TO_AVATAR_ROTATION[h], handRotation); + cameraOrientation = { + x: cameraOrientation.z, + y: cameraOrientation.y, + z: cameraOrientation.x, + w: cameraOrientation.w + }; + cameraOrientation = Quat.multiply(cameraOrientation, Quat.inverse(MyAvatar.orientation)); + handRotation = Quat.multiply(handRotation, cameraOrientation); // Works!!! + + } else { + + // Adjust to control wrist position if "hand" joint is at wrist ... + if (!hasHandAndWristJoints) { + wristOffset = Vec3.multiplyQbyV(handRotation, handToWristOffset[h]); + handOffset = Vec3.sum(handOffset, wristOffset); + } + + // Hand offset in camera coordinates ... + handOffset = { + x: -handOffset.x, + y: hands[h].zeroPosition.y + handOffset.y, + z: hands[h].zeroPosition.z - handOffset.z + }; + + // Hand rotation in camera coordinates ... + handRotation = { + x: handRotation.z, + y: handRotation.y, + z: handRotation.x, + w: handRotation.w + }; + + // Hand rotation in avatar coordinates ... + handRotation = Quat.multiply(DESKTOP_CAMERA_TO_AVATAR_ROTATION, handRotation); + } + + // Set hand position and orientation for animation state handler ... + hands[h].position = handOffset; + hands[h].rotation = handRotation; + + // Set finger joints ... + var summed = 0; + var closeAngle = 0; + for (i = 0; i < NUM_FINGERS; i += 1) { + for (j = 0; j < NUM_FINGER_JOINTS; j += 1) { + if (fingers[h][i][j].controller !== null) { + locRotation = fingers[h][i][j].controller.getLocRotation(); + var eulers = Quat.safeEulerAngles(locRotation); + closeAngle += eulers.x; + + summed++; + + if (i === THUMB) { + locRotation = { + x: side * locRotation.y, + y: side * -locRotation.z, + z: side * -locRotation.x, + w: locRotation.w + }; + if (j === 0) { + // Adjust avatar thumb root joint rotation to make avatar hands look better + locRotation = Quat.multiply(LEAP_THUMB_ROOT_ADJUST[h], locRotation); + } + } else { + locRotation = { + x: -locRotation.x, + y: -locRotation.z, + z: -locRotation.y, + w: locRotation.w + }; + } + MyAvatar.setJointRotation(fingers[h][i][j].jointName, locRotation); + } + } + } + + hands[h].inactiveCount = 0; + if (summed > 0) { + closeAngle /= summed; + } + + var triggerValue = (-closeAngle - LEAP_TRIGGER_START_ANGLE) / (LEAP_TRIGGER_END_ANGLE - LEAP_TRIGGER_START_ANGLE); + triggerValue = Math.max(0.0, Math.min(triggerValue, 1.0)); + + if (h == 0) { + leftTriggerValue = triggerValue; + } else { + rightTriggerValue = triggerValue; + + } + + } else { + + if (hands[h].inactiveCount < MAX_HAND_INACTIVE_COUNT) { + + hands[h].inactiveCount += 1; + + if (hands[h].inactiveCount === MAX_HAND_INACTIVE_COUNT) { + if (handAnimationStateHandlers[h] !== null) { + MyAvatar.removeAnimationStateHandler(handAnimationStateHandlers[h]); + handAnimationStateHandlers[h] = null; + leftTriggerValue = 0.0; + rightTriggerValue = 0.0; + } + } + } + } + } + } + + function tearDown() { + var h, + i, + j; + + Script.clearInterval(settingsTimer); + + for (h = 0; h < NUM_HANDS; h += 1) { + Controller.releaseInputController(hands[h].controller); + Controller.releaseInputController(wrists[h].controller); + if (handAnimationStateHandlers[h] !== null) { + MyAvatar.removeAnimationStateHandler(handAnimationStateHandlers[h]); + } + for (i = 0; i < NUM_FINGERS; i += 1) { + for (j = 0; j < NUM_FINGER_JOINTS; j += 1) { + if (fingers[h][i][j].controller !== null) { + Controller.releaseInputController(fingers[h][i][j].controller); + } + } + } + } + } + + return { + printSkeletonJointNames: printSkeletonJointNames, + setUp : setUp, + moveHands : moveHands, + tearDown : tearDown + }; +}()); + + +//leapHands.printSkeletonJointNames(); + +leapHands.setUp(); +Script.update.connect(leapHands.moveHands); +Script.scriptEnding.connect(leapHands.tearDown); From 27594e6df2bdc0bc8a0ce943cb52b823296f5ac9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 3 Jun 2016 17:07:08 -0700 Subject: [PATCH 0357/1237] remove afro --- .../kineticObjects/dressingRoomBricabrac.json | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json index aa4c944427..fdfb505846 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json @@ -549,44 +549,6 @@ "shapeType": "compound", "type": "Model", "userData": "{\"hifiHomeKey\":{\"reset\":true}}" - }, { - "name": "home_model_dressing_room_bricabrac", - "collisionsWillMove": 1, - "compoundShapeURL": "atp:/dressingRoom/afro.obj", - "created": "2016-05-17T23:58:24Z", - "dimensions": { - "x": 0.35398837924003601, - "y": 0.33958616852760315, - "z": 0.35055956244468689 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -5, - "z": 0 - }, - "id": "{d163c628-8247-4d13-a465-c1692825b4fe}", - "modelURL": "atp:/dressingRoom/afro.fbx", - "position": { - "x": 0.1300048828125, - "y": 0.9853515625, - "z": 0.4675140380859375 - }, - "queryAACube": { - "scale": 0.60292500257492065, - "x": -0.17145761847496033, - "y": 0.68388903141021729, - "z": 0.16605153679847717 - }, - "rotation": { - "w": 0.73046457767486572, - "x": 0.14757001399993896, - "y": 0.58101773262023926, - "z": -0.32719922065734863 - }, - "shapeType": "compound", - "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }], "Version": 57 } \ No newline at end of file From 2eaf401bd6b5391db41c592b46b3553c9a4b4928 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 4 Jun 2016 12:39:55 +1200 Subject: [PATCH 0358/1237] Fix spurious data being submitted from changed permissions in new row --- .../resources/web/settings/js/settings.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 3ddf9722fb..43a3c89534 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -232,16 +232,17 @@ $(document).ready(function(){ badgeSidebarForDifferences($(this)); }); - // Bootstrap switch in table $('#' + Settings.FORM_ID).on('switchChange.bootstrapSwitch', 'input.table-checkbox', function () { - // Bootstrap switches in table: set the changed data attribute for all rows. + // Bootstrap switches in table: set the changed data attribute for all rows in table. var row = $(this).closest('tr'); - row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); - updateDataChangedForSiblingRows(row, true); + if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. + row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); + updateDataChangedForSiblingRows(row, true); + badgeSidebarForDifferences($(this)); + } }); - $('.advanced-toggle').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) @@ -850,6 +851,7 @@ function reloadSettings(callback) { // setup any bootstrap switches $('.toggle-checkbox').bootstrapSwitch(); + $('.table-checkbox').bootstrapSwitch(); // add tooltip to locked settings $('label.locked').tooltip({ @@ -982,7 +984,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { if (isArray && col.type === "checkbox" && col.editable) { html += "" - + "" - + "\ @@ -1173,7 +1175,7 @@ function addTableRow(add_glyphicon) { // Hide inputs var input = $(element).find("input") var isCheckbox = false; - if (input.hasClass("toggle-checkbox")) { + if (input.hasClass("table-checkbox")) { input = $(input).parent().parent(); isCheckbox = true; } From 4fb8eac8ea7853b54b64a8a07e7a0034dbfe3488 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 3 Jun 2016 18:12:47 -0700 Subject: [PATCH 0359/1237] much better way of fixing script --- .../system/controllers/handControllerGrab.js | 77 +++++++------------ 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4f4b7e32ad..986a4c0722 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -33,9 +33,6 @@ var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; -var GRIP_ON_VALUE = 0.9; -var GRIP_OFF_VALUE = 0.8; - var THUMB_ON_VALUE = 0.5; var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. @@ -274,8 +271,7 @@ function MyController(hand) { this.triggerValue = 0; // rolling average of trigger value this.rawTriggerValue = 0; - this.rawGripValue = 0; - this.rawBumperValue = 0; + this.rawSecondaryValue = 0; this.rawThumbValue = 0; //for visualizations @@ -513,10 +509,10 @@ function MyController(hand) { var searchSphereLocation = Vec3.sum(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, - (this.triggerSmoothedGrab() || (this.bumperSqueezed() || this.gripSqueezed())) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); if ((USE_OVERLAY_LINES_FOR_SEARCHING === true) && PICK_WITH_HAND_RAY) { this.overlayLineOn(handPosition, searchSphereLocation, - (this.triggerSmoothedGrab() || (this.bumperSqueezed() || this.gripSqueezed())) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } } @@ -772,12 +768,8 @@ function MyController(hand) { _this.rawTriggerValue = value; }; - this.bumperPress = function(value) { - _this.rawBumperValue = value; - }; - - this.gripPress = function(value) { - _this.rawGripValue = value; + this.secondaryPress = function(value) { + _this.rawSecondaryValue = value; }; this.updateSmoothedTrigger = function() { @@ -799,28 +791,20 @@ function MyController(hand) { return this.triggerValue < TRIGGER_OFF_VALUE; }; - this.bumperSqueezed = function() { - return _this.rawBumperValue > BUMPER_ON_VALUE; + this.secondarySqueezed = function() { + return _this.rawSecondaryValue > BUMPER_ON_VALUE; }; - this.bumperReleased = function() { - return _this.rawBumperValue < BUMPER_ON_VALUE; + this.secondaryReleased = function() { + return _this.rawSecondaryValue < BUMPER_ON_VALUE; }; - this.gripSqueezed = function() { - return _this.rawGripValue > GRIP_ON_VALUE; - }; - - this.gripReleased = function() { - return _this.rawGripValue < GRIP_OFF_VALUE; - }; - - // this.triggerOrBumperSqueezed = function() { - // return triggerSmoothedSqueezed() || bumperSqueezed(); + // this.triggerOrsecondarySqueezed = function() { + // return triggerSmoothedSqueezed() || secondarySqueezed(); // } - // this.triggerAndBumperReleased = function() { - // return triggerSmoothedReleased() && bumperReleased(); + // this.triggerAndSecondaryReleased = function() { + // return triggerSmoothedReleased() && secondaryReleased(); // } this.thumbPress = function(value) { @@ -836,13 +820,13 @@ function MyController(hand) { }; this.off = function() { - if (this.triggerSmoothedSqueezed() || (this.bumperSqueezed() || this.gripSqueezed())) { + if (this.triggerSmoothedSqueezed() || this.secondarySqueezed()) { this.lastPickTime = 0; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { this.setState(STATE_SEARCHING); - } else if (this.bumperSqueezed() || this.gripSqueezed()) { + } else if (this.secondarySqueezed()) { this.setState(STATE_HOLD_SEARCHING); } } @@ -855,13 +839,11 @@ function MyController(hand) { this.checkForStrayChildren(); - print("bumper: " + this.bumperReleased() + " grip: " + this.gripReleased()); - if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { this.setState(STATE_RELEASE); return; } - if (this.state == STATE_HOLD_SEARCHING && (this.bumperReleased() || this.gripReleased())) { + if (this.state == STATE_HOLD_SEARCHING && this.secondaryReleased()) { this.setState(STATE_RELEASE); return; } @@ -1018,7 +1000,7 @@ function MyController(hand) { grabbableData = grabbableDataForCandidate; } } - if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || (this.bumperSqueezed() || this.gripSqueezed()))) { + if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.secondarySqueezed())) { // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. var near = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; var isPhysical = this.propsArePhysical(props); @@ -1184,7 +1166,7 @@ function MyController(hand) { }; this.continueDistanceHolding = function() { - if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); return; @@ -1408,7 +1390,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("releaseGrab"); return; } - if (this.state == STATE_HOLD && (this.bumperReleased() || this.gripReleased())) { + if (this.state == STATE_HOLD && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); return; @@ -1522,7 +1504,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("releaseGrab"); return; } - if (this.state == STATE_CONTINUE_HOLD && (this.bumperReleased() || this.gripReleased())) { + if (this.state == STATE_CONTINUE_HOLD && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseEquip"); return; @@ -1651,7 +1633,7 @@ function MyController(hand) { }; this.nearTrigger = function() { - if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; @@ -1661,7 +1643,7 @@ function MyController(hand) { }; this.farTrigger = function() { - if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; @@ -1671,7 +1653,7 @@ function MyController(hand) { }; this.continueNearTrigger = function() { - if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; @@ -1680,7 +1662,7 @@ function MyController(hand) { }; this.continueFarTrigger = function() { - if (this.triggerSmoothedReleased() && (this.bumperReleased() || this.gripReleased())) { + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; @@ -1960,11 +1942,10 @@ var mapping = Controller.newMapping(MAPPING_NAME); mapping.from([Controller.Standard.RT]).peek().to(rightController.triggerPress); mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress); -mapping.from([Controller.Standard.RB]).peek().to(rightController.bumperPress); -mapping.from([Controller.Standard.LB]).peek().to(leftController.bumperPress); - -mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.gripPress); -mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.gripPress); +mapping.from([Controller.Standard.RB]).peek().to(rightController.secondaryPress); +mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); +mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.secondaryPress); +mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.secondaryPress); mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); @@ -2057,4 +2038,4 @@ function cleanup() { Reticle.setVisible(true); } Script.scriptEnding.connect(cleanup); -Script.update.connect(update); +Script.update.connect(update); \ No newline at end of file From 54293fdad034eb35da373753e01b47598e32381b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 3 Jun 2016 18:39:43 -0700 Subject: [PATCH 0360/1237] Replace the linear interpolation embedded in the .WAV/.RAW loader with high quality polyphase resampling. When downsampling 48khz to 24khz, linear interpolation creates aliasing distortion that is quite audible. This change greatly improves the audio quality for all sound assets. --- libraries/audio/src/Sound.cpp | 55 ++++++++++------------------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 806e33819e..c3e936e33a 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -25,6 +25,8 @@ #include "AudioRingBuffer.h" #include "AudioLogging.h" +#include "AudioSRC.h" + #include "Sound.h" QScriptValue soundSharedPointerToScriptValue(QScriptEngine* engine, const SharedSoundPointer& in) { @@ -89,49 +91,22 @@ void Sound::downSample(const QByteArray& rawAudioByteArray) { // we want to convert it to the format that the audio-mixer wants // which is signed, 16-bit, 24Khz - int numSourceSamples = rawAudioByteArray.size() / sizeof(AudioConstants::AudioSample); + int numChannels = _isStereo ? 2 : 1; + AudioSRC resampler(48000, AudioConstants::SAMPLE_RATE, numChannels); - if (_isStereo && numSourceSamples % 2 != 0){ - // in the unlikely case that we have stereo audio but we seem to be missing a sample - // (the sample for one channel is missing in a set of interleaved samples) - // then drop the odd sample - --numSourceSamples; - } + // resize to max possible output + int numSourceFrames = rawAudioByteArray.size() / (numChannels * sizeof(AudioConstants::AudioSample)); + int maxDestinationFrames = resampler.getMaxOutput(numSourceFrames); + int maxDestinationBytes = maxDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample); + _byteArray.resize(maxDestinationBytes); - int numDestinationSamples = numSourceSamples / 2.0f; + int numDestinationFrames = resampler.render((int16_t*)rawAudioByteArray.data(), + (int16_t*)_byteArray.data(), + numSourceFrames); - if (_isStereo && numDestinationSamples % 2 != 0) { - // if this is stereo we need to make sure we produce stereo output - // which means we should have an even number of output samples - numDestinationSamples += 1; - } - - int numDestinationBytes = numDestinationSamples * sizeof(AudioConstants::AudioSample); - - _byteArray.resize(numDestinationBytes); - - int16_t* sourceSamples = (int16_t*) rawAudioByteArray.data(); - int16_t* destinationSamples = (int16_t*) _byteArray.data(); - - if (_isStereo) { - for (int i = 0; i < numSourceSamples; i += 4) { - if (i + 2 >= numSourceSamples) { - destinationSamples[i / 2] = sourceSamples[i]; - destinationSamples[(i / 2) + 1] = sourceSamples[i + 1]; - } else { - destinationSamples[i / 2] = (sourceSamples[i] + sourceSamples[i + 2]) / 2; - destinationSamples[(i / 2) + 1] = (sourceSamples[i + 1] + sourceSamples[i + 3]) / 2; - } - } - } else { - for (int i = 1; i < numSourceSamples; i += 2) { - if (i + 1 >= numSourceSamples) { - destinationSamples[(i - 1) / 2] = (sourceSamples[i - 1] + sourceSamples[i]) / 2; - } else { - destinationSamples[(i - 1) / 2] = ((sourceSamples[i - 1] + sourceSamples[i + 1]) / 4) + (sourceSamples[i] / 2); - } - } - } + // truncate to actual output + int numDestinationBytes = numDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample); + _byteArray.resize(maxDestinationBytes); } // From 280289045c91e43541e21de7bd3b74ed155b5a44 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 3 Jun 2016 18:48:23 -0700 Subject: [PATCH 0361/1237] Revert change to reverb settings. --- libraries/audio/src/AudioEffectOptions.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio/src/AudioEffectOptions.cpp b/libraries/audio/src/AudioEffectOptions.cpp index a9e4cbae6c..9d3ce9299b 100644 --- a/libraries/audio/src/AudioEffectOptions.cpp +++ b/libraries/audio/src/AudioEffectOptions.cpp @@ -32,7 +32,7 @@ static const QString LATE_MIX_LEFT_HANDLE = "lateMixLeft"; static const QString LATE_MIX_RIGHT_HANDLE = "lateMixRight"; static const QString WET_DRY_MIX_HANDLE = "wetDryMix"; -static const float BANDWIDTH_DEFAULT = 7000.0f; +static const float BANDWIDTH_DEFAULT = 10000.0f; static const float PRE_DELAY_DEFAULT = 20.0f; static const float LATE_DELAY_DEFAULT = 0.0f; static const float REVERB_TIME_DEFAULT = 2.0f; @@ -42,7 +42,7 @@ static const float ROOM_SIZE_DEFAULT = 50.0f; static const float DENSITY_DEFAULT = 100.0f; static const float BASS_MULT_DEFAULT = 1.5f; static const float BASS_FREQ_DEFAULT = 250.0f; -static const float HIGH_GAIN_DEFAULT = -12.0f; +static const float HIGH_GAIN_DEFAULT = -6.0f; static const float HIGH_FREQ_DEFAULT = 3000.0f; static const float MOD_RATE_DEFAULT = 2.3f; static const float MOD_DEPTH_DEFAULT = 50.0f; From 89bfa5bc0930da99113592f6c231ac6a94db10cb Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 3 Jun 2016 19:22:29 -0700 Subject: [PATCH 0362/1237] fixed typo --- libraries/audio/src/Sound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index c3e936e33a..764ade2661 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -106,7 +106,7 @@ void Sound::downSample(const QByteArray& rawAudioByteArray) { // truncate to actual output int numDestinationBytes = numDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample); - _byteArray.resize(maxDestinationBytes); + _byteArray.resize(numDestinationBytes); } // From 0ea1e7c6b53121ccb014ed585d59148288717719 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 3 Jun 2016 19:29:48 -0700 Subject: [PATCH 0363/1237] Adding acleaner way to do all the deferred frame transform manipulation --- .../render-utils/src/DebugDeferredBuffer.cpp | 14 +++ .../render-utils/src/DebugDeferredBuffer.h | 1 + .../src/DeferredFrameTransform.cpp | 70 ++++++++++++ .../render-utils/src/DeferredFrameTransform.h | 74 ++++++++++++ .../render-utils/src/DeferredTransform.slh | 95 ++++++++++++++++ .../render-utils/src/FramebufferCache.cpp | 22 ++++ libraries/render-utils/src/FramebufferCache.h | 8 +- .../render-utils/src/RenderDeferredTask.cpp | 7 ++ .../render-utils/src/SurfaceGeometry.slh | 62 ++++++++++ .../render-utils/src/SurfaceGeometryPass.cpp | 107 ++++++++++++++++++ .../render-utils/src/SurfaceGeometryPass.h | 75 ++++++++++++ .../src/debug_deferred_buffer.slf | 1 + .../render-utils/src/ssao_makeOcclusion.slf | 2 +- .../src/surfaceGeometry_makeCurvature.slf | 107 ++++++++++++++++++ .../utilities/render/framebuffer.qml | 1 + 15 files changed, 644 insertions(+), 2 deletions(-) create mode 100644 libraries/render-utils/src/DeferredFrameTransform.cpp create mode 100644 libraries/render-utils/src/DeferredFrameTransform.h create mode 100644 libraries/render-utils/src/DeferredTransform.slh create mode 100644 libraries/render-utils/src/SurfaceGeometry.slh create mode 100644 libraries/render-utils/src/SurfaceGeometryPass.cpp create mode 100644 libraries/render-utils/src/SurfaceGeometryPass.h create mode 100644 libraries/render-utils/src/surfaceGeometry_makeCurvature.slf diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 6dfec30b16..5de61df423 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -47,6 +47,7 @@ enum Slot { Lighting, Shadow, Pyramid, + Curvature, AmbientOcclusion, AmbientOcclusionBlurred }; @@ -138,6 +139,13 @@ static const std::string DEFAULT_PYRAMID_DEPTH_SHADER { " }" }; +static const std::string DEFAULT_CURVATURE_SHADER{ + "vec4 getFragmentColor() {" + " return vec4(texture(curvatureMap, uv).xyz, 1.0);" + //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" + " }" +}; + static const std::string DEFAULT_AMBIENT_OCCLUSION_SHADER{ "vec4 getFragmentColor() {" " return vec4(vec3(texture(obscuranceMap, uv).x), 1.0);" @@ -203,6 +211,8 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_SHADOW_SHADER; case PyramidDepthMode: return DEFAULT_PYRAMID_DEPTH_SHADER; + case CurvatureMode: + return DEFAULT_CURVATURE_SHADER; case AmbientOcclusionMode: return DEFAULT_AMBIENT_OCCLUSION_SHADER; case AmbientOcclusionBlurredMode: @@ -257,6 +267,7 @@ const gpu::PipelinePointer& DebugDeferredBuffer::getPipeline(Mode mode, std::str slotBindings.insert(gpu::Shader::Binding("lightingMap", Lighting)); slotBindings.insert(gpu::Shader::Binding("shadowMap", Shadow)); slotBindings.insert(gpu::Shader::Binding("pyramidMap", Pyramid)); + slotBindings.insert(gpu::Shader::Binding("curvatureMap", Curvature)); slotBindings.insert(gpu::Shader::Binding("occlusionBlurredMap", AmbientOcclusionBlurred)); gpu::Shader::makeProgram(*program, slotBindings); @@ -288,6 +299,8 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren RenderArgs* args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + batch.enableStereo(false); + const auto geometryBuffer = DependencyManager::get(); const auto framebufferCache = DependencyManager::get(); const auto textureCache = DependencyManager::get(); @@ -313,6 +326,7 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setResourceTexture(Lighting, framebufferCache->getLightingTexture()); batch.setResourceTexture(Shadow, lightStage.lights[0]->shadow.framebuffer->getDepthStencilBuffer()); batch.setResourceTexture(Pyramid, framebufferCache->getDepthPyramidTexture()); + batch.setResourceTexture(Curvature, framebufferCache->getCurvatureTexture()); if (DependencyManager::get()->isAmbientOcclusionEnabled()) { batch.setResourceTexture(AmbientOcclusion, framebufferCache->getOcclusionTexture()); } else { diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 521dc13e0a..0af6d589e9 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -59,6 +59,7 @@ protected: LightingMode, ShadowMode, PyramidDepthMode, + CurvatureMode, AmbientOcclusionMode, AmbientOcclusionBlurredMode, CustomMode // Needs to stay last diff --git a/libraries/render-utils/src/DeferredFrameTransform.cpp b/libraries/render-utils/src/DeferredFrameTransform.cpp new file mode 100644 index 0000000000..1cb85058d8 --- /dev/null +++ b/libraries/render-utils/src/DeferredFrameTransform.cpp @@ -0,0 +1,70 @@ +// +// DeferredFrameTransform.cpp +// libraries/render-utils/src/ +// +// Created by Sam Gateau 6/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "DeferredFrameTransform.h" + +#include "gpu/Context.h" +#include "render/Engine.h" + +DeferredFrameTransform::DeferredFrameTransform() { + FrameTransform frameTransform; + _frameTransformBuffer = gpu::BufferView(std::make_shared(sizeof(FrameTransform), (const gpu::Byte*) &frameTransform)); +} + +void DeferredFrameTransform::update(RenderArgs* args) { + + // Update the depth info with near and far (same for stereo) + auto nearZ = args->getViewFrustum().getNearClip(); + auto farZ = args->getViewFrustum().getFarClip(); + _frameTransformBuffer.edit().depthInfo = glm::vec4(nearZ*farZ, farZ - nearZ, -farZ, 0.0f); + + _frameTransformBuffer.edit().pixelInfo = args->_viewport; + + //_parametersBuffer.edit()._ditheringInfo.y += 0.25f; + + Transform cameraTransform; + args->getViewFrustum().evalViewTransform(cameraTransform); + cameraTransform.getMatrix(_frameTransformBuffer.edit().invView); + + // Running in stero ? + bool isStereo = args->_context->isStereo(); + if (!isStereo) { + // Eval the mono projection + mat4 monoProjMat; + args->getViewFrustum().evalProjectionMatrix(monoProjMat); + _frameTransformBuffer.edit().projection[0] = monoProjMat; + _frameTransformBuffer.edit().stereoInfo = glm::vec4(0.0f, (float)args->_viewport.z, 0.0f, 0.0f); + _frameTransformBuffer.edit().invpixelInfo = glm::vec4(1.0f / args->_viewport.z, 1.0f / args->_viewport.w, 0.0f, 0.0f); + + } else { + + mat4 projMats[2]; + mat4 eyeViews[2]; + args->_context->getStereoProjections(projMats); + args->_context->getStereoViews(eyeViews); + + for (int i = 0; i < 2; i++) { + // Compose the mono Eye space to Stereo clip space Projection Matrix + auto sideViewMat = projMats[i] * eyeViews[i]; + _frameTransformBuffer.edit().projection[i] = sideViewMat; + } + + _frameTransformBuffer.edit().stereoInfo = glm::vec4(1.0f, (float)(args->_viewport.z >> 1), 0.0f, 1.0f); + _frameTransformBuffer.edit().invpixelInfo = glm::vec4(1.0f / (float)(args->_viewport.z >> 1), 1.0f / args->_viewport.w, 0.0f, 0.0f); + + } +} + +void GenerateDeferredFrameTransform::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, DeferredFrameTransformPointer& frameTransform) { + if (!frameTransform) { + frameTransform = std::make_shared(); + } + frameTransform->update(renderContext->args); +} diff --git a/libraries/render-utils/src/DeferredFrameTransform.h b/libraries/render-utils/src/DeferredFrameTransform.h new file mode 100644 index 0000000000..b6c3667c28 --- /dev/null +++ b/libraries/render-utils/src/DeferredFrameTransform.h @@ -0,0 +1,74 @@ +// +// DeferredFrameTransform.h +// libraries/render-utils/src/ +// +// Created by Sam Gateau 6/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DeferredFrameTransform_h +#define hifi_DeferredFrameTransform_h + +#include "gpu/Resource.h" +#include "render/DrawTask.h" + +class RenderArgs; + +// DeferredFrameTransform is a helper class gathering in one place the needed camera transform +// and frame resolution needed for all the deferred rendering passes taking advantage of the Deferred buffers +class DeferredFrameTransform { +public: + using UniformBufferView = gpu::BufferView; + + DeferredFrameTransform(); + + void update(RenderArgs* args); + + UniformBufferView getFrameTransformBuffer() const { return _frameTransformBuffer; } + +protected: + + + // Class describing the uniform buffer with the transform info common to the AO shaders + // It s changing every frame + class FrameTransform { + public: + // Pixel info is { viemport width height and stereo on off} + glm::vec4 pixelInfo; + glm::vec4 invpixelInfo; + // Depth info is { n.f, f - n, -f} + glm::vec4 depthInfo; + // Stereo info + glm::vec4 stereoInfo{ 0.0 }; + // Mono proj matrix or Left and Right proj matrix going from Mono Eye space to side clip space + glm::mat4 projection[2]; + // Inv View matrix from eye space (mono) to world space + glm::mat4 invView; + + FrameTransform() {} + }; + UniformBufferView _frameTransformBuffer; + + +}; + +using DeferredFrameTransformPointer = std::shared_ptr; + + + + +class GenerateDeferredFrameTransform { +public: + using JobModel = render::Job::ModelO; + + GenerateDeferredFrameTransform() {} + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, DeferredFrameTransformPointer& frameTransform); + +private: +}; + +#endif // hifi_SurfaceGeometryPass_h diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh new file mode 100644 index 0000000000..25a62fca3d --- /dev/null +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -0,0 +1,95 @@ + +<@if not DEFERRED_TRANSFORM_SLH@> +<@def DEFERRED_TRANSFORM_SLH@> + +<@func declareDeferredFrameTransform()@> + +struct DeferredFrameTransform { + vec4 _pixelInfo; + vec4 _invPixelInfo; + vec4 _depthInfo; + vec4 _stereoInfo; + mat4 _projection[2]; + mat4 _invView; +}; + +uniform deferredFrameTransformBuffer { + DeferredFrameTransform frameTransform; +}; + +vec2 getWidthHeight(int resolutionLevel) { + return vec2(ivec2(frameTransform._pixelInfo.zw) >> resolutionLevel); +} + +vec2 getInvWidthHeight() { + return frameTransform._invPixelInfo.xy; +} + +float getProjScaleEye() { + return frameTransform._projection[0][1][1]; +} + +float getProjScale(int resolutionLevel) { + return getWidthHeight(resolutionLevel).y * frameTransform._projection[0][1][1] * 0.5; +} +mat4 getProjection(int side) { + return frameTransform._projection[side]; +} + +bool isStereo() { + return frameTransform._stereoInfo.x > 0.0f; +} + +float getStereoSideWidth(int resolutionLevel) { + return float(int(frameTransform._stereoInfo.y) >> resolutionLevel); +} + +ivec3 getStereoSideInfo(int xPos, int resolutionLevel) { + int sideWidth = int(getStereoSideWidth(resolutionLevel)); + return ivec3(xPos < sideWidth ? ivec2(0, 0) : ivec2(1, sideWidth), sideWidth); +} + +float evalZeyeFromZdb(float depth) { + return frameTransform._depthInfo.x / (depth * frameTransform._depthInfo.y + frameTransform._depthInfo.z); +} + +vec3 evalEyeNormal(vec3 C) { + //return normalize(cross(dFdy(C), dFdx(C))); + return normalize(cross(dFdx(C), dFdy(C))); +} + +vec3 evalEyePositionFromZeye(int side, float Zeye, vec2 texcoord) { + // compute the view space position using the depth + // basically manually pick the proj matrix components to do the inverse + float Xe = (-Zeye * (texcoord.x * 2.0 - 1.0) - Zeye * frameTransform._projection[side][2][0] - frameTransform._projection[side][3][0]) / frameTransform._projection[side][0][0]; + float Ye = (-Zeye * (texcoord.y * 2.0 - 1.0) - Zeye * frameTransform._projection[side][2][1] - frameTransform._projection[side][3][1]) / frameTransform._projection[side][1][1]; + return vec3(Xe, Ye, Zeye); +} + +ivec2 getPixelPosNclipPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out vec2 nclipPos, out ivec3 stereoSide) { + ivec2 fragPos = ivec2(glFragCoord.xy); + + stereoSide = getStereoSideInfo(fragPos.x, 0); + + pixelPos = fragPos; + pixelPos.x -= stereoSide.y; + + nclipPos = (vec2(pixelPos) + 0.5) * getInvWidthHeight(); + + return fragPos; +} + +<@endfunc@> + + + +<@endif@> \ No newline at end of file diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 2d322b1726..63ae7e521e 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -111,6 +111,13 @@ void FramebufferCache::createPrimaryFramebuffer() { _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); + _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + resizeAmbientOcclusionBuffers(); } @@ -245,6 +252,21 @@ gpu::TexturePointer FramebufferCache::getDepthPyramidTexture() { return _depthPyramidTexture; } +gpu::FramebufferPointer FramebufferCache::getCurvatureFramebuffer() { + if (!_curvatureFramebuffer) { + createPrimaryFramebuffer(); + } + return _curvatureFramebuffer; +} + +gpu::TexturePointer FramebufferCache::getCurvatureTexture() { + if (!_curvatureTexture) { + createPrimaryFramebuffer(); + } + return _curvatureTexture; +} + + void FramebufferCache::setAmbientOcclusionResolutionLevel(int level) { const int MAX_AO_RESOLUTION_LEVEL = 4; level = std::max(0, std::min(level, MAX_AO_RESOLUTION_LEVEL)); diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index 7c7c309572..0fb9b9b2ee 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -47,6 +47,9 @@ public: gpu::FramebufferPointer getDepthPyramidFramebuffer(); gpu::TexturePointer getDepthPyramidTexture(); + gpu::FramebufferPointer getCurvatureFramebuffer(); + gpu::TexturePointer getCurvatureTexture(); + void setAmbientOcclusionResolutionLevel(int level); gpu::FramebufferPointer getOcclusionFramebuffer(); gpu::TexturePointer getOcclusionTexture(); @@ -95,7 +98,10 @@ private: gpu::FramebufferPointer _depthPyramidFramebuffer; gpu::TexturePointer _depthPyramidTexture; - + + gpu::FramebufferPointer _curvatureFramebuffer; + gpu::TexturePointer _curvatureTexture; + gpu::FramebufferPointer _occlusionFramebuffer; gpu::TexturePointer _occlusionTexture; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 444c52623e..6fa98089be 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -26,6 +26,7 @@ #include "DebugDeferredBuffer.h" #include "DeferredLightingEffect.h" +#include "SurfaceGeometryPass.h" #include "FramebufferCache.h" #include "HitEffect.h" #include "TextureCache.h" @@ -92,6 +93,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto overlayTransparents = addJob("DepthSortOverlayTransparent", filteredNonspatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false)); const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET]; + // Prepare deferred, generate the shared Deferred Frame Transform + const auto deferredFrameTransform = addJob("EvalDeferredFrameTransform"); + // GPU jobs: Start preparing the deferred and lighting buffer addJob("PrepareDeferred"); @@ -104,6 +108,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Use Stencil and start drawing background in Lighting buffer addJob("DrawBackgroundDeferred", background); + // Opaque all rendered, generate surface geometry buffers + addJob("SurfaceGeometry", deferredFrameTransform); + // AO job addJob("AmbientOcclusion"); diff --git a/libraries/render-utils/src/SurfaceGeometry.slh b/libraries/render-utils/src/SurfaceGeometry.slh new file mode 100644 index 0000000000..cd9e4c8ac4 --- /dev/null +++ b/libraries/render-utils/src/SurfaceGeometry.slh @@ -0,0 +1,62 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/3/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include DeferredTransform.slh@> +<$declareDeferredFrameTransform()$> + + +uniform sampler2D depthMap; + +out vec4 outFragColor; + +void main(void) { + // Fetch normal and depth of current pixel + float4 samplePos = sourceTexture.SampleLevel(pointSampler, input.texUV, 0.0f); + float4 sampleNormal = depthTexture.SampleLevel(pointSampler, input.texUV, 0.0f); + + // Calculate the width scale. + float distanceToProjectionWindow = 1.0f / tan(0.5f * radians(fov)); + float scale = distanceToProjectionWindow / sampleNormal.w; + + // Calculate dF/du and dF/dv + float2 du = float2( 1.0f, 0.0f ) * UVfactor.x * screenPixel * scale; + float2 dv = float2( 0.0f, 1.0f ) * UVfactor.x * screenPixel * scale; + float4 dFdu = depthTexture.SampleLevel(linearSampler, input.texUV + du.xy, 0.0f) - + depthTexture.SampleLevel(linearSampler, input.texUV - du.xy, 0.0f); + float4 dFdv = depthTexture.SampleLevel(linearSampler, input.texUV + dv.xy, 0.0f) - + depthTexture.SampleLevel(linearSampler, input.texUV - dv.xy, 0.0f); + dFdu *= step(abs(dFdu.w), 0.1f); dFdv *= step(abs(dFdv.w), 0.1f); + + // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) + float dist = 1.0f; samplePos.w = 1.0f; + float2 centerOffset = ((input.texUV - 0.5f) * 2.0f); + float4 px = mul( samplePos + float4( dist, 0.0f, 0.0f, 0.0f ), matViewProj ); + float4 py = mul( samplePos + float4( 0.0f, dist, 0.0f, 0.0f ), matViewProj ); + float4 pz = mul( samplePos + float4( 0.0f, 0.0f, dist, 0.0f ), matViewProj ); + #ifdef INVERT_TEXTURE_V + centerOffset.y = -centerOffset.y; + #endif + px.xy = ((px.xy / px.w) - centerOffset) / scale; + py.xy = ((py.xy / py.w) - centerOffset) / scale; + pz.xy = ((pz.xy / pz.w) - centerOffset) / scale; + #ifdef INVERT_TEXTURE_V + px.y = -px.y; py.y = -py.y; pz.y = -pz.y; + #endif + + // Calculate dF/dx, dF/dy and dF/dz using chain rule + float4 dFdx = dFdu * px.x + dFdv * px.y; + float4 dFdy = dFdu * py.x + dFdv * py.y; + float4 dFdz = dFdu * pz.x + dFdv * pz.y; + + // Calculate the mean curvature + float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333f) * 100.0f; + return (float4( sampleNormal.xyz, meanCurvature ) + 1.0f) * 0.5f; +} diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp new file mode 100644 index 0000000000..ac3f04383b --- /dev/null +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -0,0 +1,107 @@ +// +// SurfaceGeometryPass.cpp +// libraries/render-utils/src/ +// +// Created by Sam Gateau 6/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "SurfaceGeometryPass.h" + +#include +#include + +#include "FramebufferCache.h" + +const int SurfaceGeometryPass_FrameTransformSlot = 0; +const int SurfaceGeometryPass_ParamsSlot = 1; +const int SurfaceGeometryPass_DepthMapSlot = 0; + +#include "surfaceGeometry_makeCurvature_frag.h" + +SurfaceGeometryPass::SurfaceGeometryPass() { +} + +void SurfaceGeometryPass::configure(const Config& config) { +} + +void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + + RenderArgs* args = renderContext->args; + + + auto framebufferCache = DependencyManager::get(); + auto depthBuffer = framebufferCache->getPrimaryDepthTexture(); + // auto normalBuffer = framebufferCache->getDeferredNormalTexture(); + // auto pyramidFBO = framebufferCache->getDepthPyramidFramebuffer(); + auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); + + QSize framebufferSize = framebufferCache->getFrameBufferSize(); + float sMin = args->_viewport.x / (float)framebufferSize.width(); + float sWidth = args->_viewport.z / (float)framebufferSize.width(); + float tMin = args->_viewport.y / (float)framebufferSize.height(); + float tHeight = args->_viewport.w / (float)framebufferSize.height(); + + + auto curvaturePipeline = getCurvaturePipeline(); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + + // _gpuTimer.begin(batch); + + batch.setViewportTransform(args->_viewport); + batch.setProjectionTransform(glm::mat4()); + batch.setViewTransform(Transform()); + + Transform model; + model.setTranslation(glm::vec3(sMin, tMin, 0.0f)); + model.setScale(glm::vec3(sWidth, tHeight, 1.0f)); + batch.setModelTransform(model); + + batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); + // batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); + + + // Pyramid pass + batch.setFramebuffer(curvatureFBO); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(args->getViewFrustum().getFarClip(), 0.0f, 0.0f, 0.0f)); + batch.setPipeline(curvaturePipeline); + batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, depthBuffer); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + }); + +} + +const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { + if (!_curvaturePipeline) { + auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(surfaceGeometry_makeCurvature_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SurfaceGeometryPass_FrameTransformSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("ambientOcclusionParamsBuffer"), SurfaceGeometryPass_ParamsSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), SurfaceGeometryPass_DepthMapSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + // state->setColorWriteMask(true, false, false, false); + + // Good to go add the brand new pipeline + _curvaturePipeline = gpu::Pipeline::create(program, state); + } + + return _curvaturePipeline; +} + diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h new file mode 100644 index 0000000000..5501f43659 --- /dev/null +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -0,0 +1,75 @@ +// +// SurfaceGeometryPass.h +// libraries/render-utils/src/ +// +// Created by Sam Gateau 6/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SurfaceGeometryPass_h +#define hifi_SurfaceGeometryPass_h + +#include + +#include "render/DrawTask.h" +#include "DeferredFrameTransform.h" + +class SurfaceGeometryPassConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(double gpuTime READ getGpuTime) +public: + SurfaceGeometryPassConfig() : render::Job::Config(true) {} + + double getGpuTime() { return gpuTime; } + + double gpuTime{ 0.0 }; + +signals: + void dirty(); +}; + +class SurfaceGeometryPass { +public: + using Config = SurfaceGeometryPassConfig; + using JobModel = render::Job::ModelI; + + SurfaceGeometryPass(); + + void configure(const Config& config); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); + +private: + typedef gpu::BufferView UniformBufferView; + + // Class describing the uniform buffer with all the parameters common to the AO shaders + class Parameters { + public: + // Resolution info + glm::vec4 resolutionInfo { -1.0f, 0.0f, 0.0f, 0.0f }; + // radius info is { R, R^2, 1 / R^6, ObscuranceScale} + glm::vec4 radiusInfo{ 0.5f, 0.5f * 0.5f, 1.0f / (0.25f * 0.25f * 0.25f), 1.0f }; + // Dithering info + glm::vec4 ditheringInfo { 0.0f, 0.0f, 0.01f, 1.0f }; + // Sampling info + glm::vec4 sampleInfo { 11.0f, 1.0f/11.0f, 7.0f, 1.0f }; + // Blurring info + glm::vec4 blurInfo { 1.0f, 3.0f, 2.0f, 0.0f }; + // gaussian distribution coefficients first is the sampling radius (max is 6) + const static int GAUSSIAN_COEFS_LENGTH = 8; + float _gaussianCoefs[GAUSSIAN_COEFS_LENGTH]; + + Parameters() {} + }; + gpu::BufferView _parametersBuffer; + + const gpu::PipelinePointer& getCurvaturePipeline(); + + gpu::PipelinePointer _curvaturePipeline; + + gpu::RangeTimer _gpuTimer; +}; + +#endif // hifi_SurfaceGeometryPass_h diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index b323836657..4c045b7e99 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -17,6 +17,7 @@ uniform sampler2D pyramidMap; uniform sampler2D occlusionMap; uniform sampler2D occlusionBlurredMap; +uniform sampler2D curvatureMap; in vec2 uv; out vec4 outFragColor; diff --git a/libraries/render-utils/src/ssao_makeOcclusion.slf b/libraries/render-utils/src/ssao_makeOcclusion.slf index 72424cad1e..01e44d0bb9 100644 --- a/libraries/render-utils/src/ssao_makeOcclusion.slf +++ b/libraries/render-utils/src/ssao_makeOcclusion.slf @@ -112,7 +112,7 @@ void main(void) { // From now on, ssC is the pixel pos in the side ssC.x -= side.y; - vec2 fragPos = (vec2(ssC) + 0.5) / getStereoSideWidth(); + vec2 fragPos = (vec2(ssC) + 0.5) / getStereoSideWidth(); // The position and normal of the pixel fragment in Eye space vec3 Cp = evalEyePositionFromZeye(side.x, Zeye, fragPos); diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf new file mode 100644 index 0000000000..ca221ae84a --- /dev/null +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -0,0 +1,107 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/3/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include DeferredTransform.slh@> +<$declareDeferredFrameTransform()$> + + +uniform sampler2D depthMap; + +out vec4 outFragColor; +/* +void main(void) { + float Zdb = texelFetch(depthMap, ivec2(gl_FragCoord.xy), 0).x; + float Zeye = -evalZeyeFromZdb(Zdb); + outFragColor = vec4(Zeye, 0.0, 0.0, 1.0); +} +*/ + +void main(void) { + // Pixel being shaded + ivec2 pixelPos; + vec2 nclipPos; + ivec3 stereoSide; + ivec2 framePixelPos = getPixelPosNclipPosAndSide(gl_FragCoord.xy, pixelPos, nclipPos, stereoSide); + + // Fetch the z under the pixel (stereo or not) + float Zdb = texelFetch(depthMap, pixelPos, 0).x; + float Zeye = -evalZeyeFromZdb(Zdb); + + + // The position and normal of the pixel fragment in Eye space + vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, nclipPos); + + vec3 worldPos = (frameTransform._invView * vec4(eyePos, 1.0)).xyz; + + vec3 moduloPos = fract(worldPos); + + outFragColor = vec4(moduloPos, 1.0); + /* + return; + + // Choose the screen-space sample radius + // proportional to the projected area of the sphere + float ssDiskRadius = -getProjScale() * getRadius() / Cp.z; + + + vec2 texUV = gl_FragCoord.xy * getInvWidthHeight(); + float Zdb = texelFetch(depthMap, ivec2(gl_FragCoord.xy), 0).x; + float Zeye = -evalZeyeFromZdb(Zdb); + + ivec3 stereoInfo = getStereoSideInfo(gl_FragCoord.x, 0); + + // World Pos + vec4 samplePos = evalEyePositionFromZeye(stereoInfo.x, ) + + // Calculate the width scale. + ./ Choose the screen-space sample radius + // proportional to the projected area of the sphere + // float ssDiskRadius = -getProjScale() * getRadius() / Cp.z; + + // float distanceToProjectionWindow = 1.0f / tan(0.5f * radians(fov)); + float scale = getProjScaleEye() / Zeye; + + vec2 viewportScale = scale * getInvWidthHeight(); + + // Calculate dF/du and dF/dv + vec2 du = vec2( 1.0f, 0.0f ) * viewportScale.x; + vec2 dv = vec2( 0.0f, 1.0f ) * viewportScale.y; + + vec4 dFdu = texture(depthMap, texUV + du.xy) - texture(depthMap, texUV - du.xy); + vec4 dFdv = texture(depthMap, texUV + dv.xy) - texture(depthMap, texUV - dv.xy); + dFdu *= step(abs(dFdu.w), 0.1f); dFdv *= step(abs(dFdv.w), 0.1f); + + // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) + float dist = 1.0f; samplePos.w = 1.0f; + vec2 centerOffset = ((input.texUV - 0.5f) * 2.0f); + vec4 px = mul( samplePos + vec4( dist, 0.0f, 0.0f, 0.0f ), matViewProj ); + vec4 py = mul( samplePos + vec4( 0.0f, dist, 0.0f, 0.0f ), matViewProj ); + vec4 pz = mul( samplePos + vec4( 0.0f, 0.0f, dist, 0.0f ), matViewProj ); + #ifdef INVERT_TEXTURE_V + centerOffset.y = -centerOffset.y; + #endif + px.xy = ((px.xy / px.w) - centerOffset) / scale; + py.xy = ((py.xy / py.w) - centerOffset) / scale; + pz.xy = ((pz.xy / pz.w) - centerOffset) / scale; + #ifdef INVERT_TEXTURE_V + px.y = -px.y; py.y = -py.y; pz.y = -pz.y; + #endif + + // Calculate dF/dx, dF/dy and dF/dz using chain rule + vec4 dFdx = dFdu * px.x + dFdv * px.y; + vec4 dFdy = dFdu * py.x + dFdv * py.y; + vec4 dFdz = dFdu * pz.x + dFdv * pz.y; + + // Calculate the mean curvature + float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333) * 100.0; + outFragColor = vec4( (meanCurvature + 1.0) * 0.5); + */ +} diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index 0d8d85cc32..e8122db8c9 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -39,6 +39,7 @@ Column { "Lighting", "Shadow", "Pyramid Depth", + "Curvature", "Ambient Occlusion", "Ambient Occlusion Blurred", "Custom Shader" From 09b0e3eaaf8df438115b0659d83217ee3927023b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 4 Jun 2016 09:07:34 -0700 Subject: [PATCH 0364/1237] use constants rather than bare strings for standard psuedo-account-names --- domain-server/src/DomainGatekeeper.cpp | 6 ++-- .../src/DomainServerSettingsManager.cpp | 36 +++++++++---------- libraries/networking/src/AgentPermissions.cpp | 5 +++ libraries/networking/src/AgentPermissions.h | 5 +++ 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index cf32e804dc..24334b5e2e 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -189,7 +189,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); if (isLocalUser) { - userPerms |= _server->_settingsManager.getPermissionsForName("localhost"); + userPerms |= _server->_settingsManager.getPermissionsForName(AgentPermissions::standardNameLocalhost); } if (!username.isEmpty() && usernameSignature.isEmpty()) { @@ -204,7 +204,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (username.isEmpty()) { // they didn't tell us who they are - userPerms |= _server->_settingsManager.getPermissionsForName("anonymous"); + userPerms |= _server->_settingsManager.getPermissionsForName(AgentPermissions::standardNameAnonymous); } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they are sent us a username and the signature verifies it if (_server->_settingsManager.havePermissionsForName(username)) { @@ -212,7 +212,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect userPerms |= _server->_settingsManager.getPermissionsForName(username); } else { // they are logged into metaverse, but we don't have specific permissions for them. - userPerms |= _server->_settingsManager.getPermissionsForName("logged-in"); + userPerms |= _server->_settingsManager.getPermissionsForName(AgentPermissions::standardNameLoggedIn); } } else { // they sent us a username, but it didn't check out diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 9d5da9deb9..36f030f249 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -206,15 +206,18 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList(); bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool(); - _agentPermissions["localhost"].reset(new AgentPermissions("localhost")); - _agentPermissions["localhost"]->setAll(true); - _agentPermissions["anonymous"].reset(new AgentPermissions("anonymous")); - _agentPermissions["logged-in"].reset(new AgentPermissions("logged-in")); + _agentPermissions[AgentPermissions::standardNameLocalhost].reset( + new AgentPermissions(AgentPermissions::standardNameLocalhost)); + _agentPermissions[AgentPermissions::standardNameLocalhost]->setAll(true); + _agentPermissions[AgentPermissions::standardNameAnonymous].reset( + new AgentPermissions(AgentPermissions::standardNameAnonymous)); + _agentPermissions[AgentPermissions::standardNameLoggedIn].reset( + new AgentPermissions(AgentPermissions::standardNameLoggedIn)); if (isRestrictedAccess) { // only users in allow-users list can connect - _agentPermissions["anonymous"]->canConnectToDomain = false; - _agentPermissions["logged-in"]->canConnectToDomain = false; + _agentPermissions[AgentPermissions::standardNameAnonymous]->canConnectToDomain = false; + _agentPermissions[AgentPermissions::standardNameLoggedIn]->canConnectToDomain = false; } // else anonymous and logged-in retain default of canConnectToDomain = true foreach (QString allowedUser, allowedUsers) { @@ -291,9 +294,9 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL foreach (QVariant permsHash, permissionsList) { AgentPermissionsPointer perms { new AgentPermissions(permsHash.toMap()) }; QString id = perms->getID(); - foundLocalhost |= (id == "localhost"); - foundAnonymous |= (id == "anonymous"); - foundLoggedIn |= (id == "logged-in"); + foundLocalhost |= (id == AgentPermissions::standardNameLocalhost); + foundAnonymous |= (id == AgentPermissions::standardNameAnonymous); + foundLoggedIn |= (id == AgentPermissions::standardNameLoggedIn); if (_agentPermissions.contains(id)) { qDebug() << "duplicate name in permissions table: " << id; _agentPermissions[id] |= perms; @@ -304,20 +307,17 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL // if any of the standard names are missing, add them if (!foundLocalhost) { - AgentPermissionsPointer perms { new AgentPermissions("localhost") }; + AgentPermissionsPointer perms { new AgentPermissions(AgentPermissions::standardNameLocalhost) }; perms->setAll(true); - _agentPermissions["localhost"] = perms; - // *permissionsList += perms->toVariant(); + _agentPermissions[perms->getID()] = perms; } if (!foundAnonymous) { - AgentPermissionsPointer perms { new AgentPermissions("anonymous") }; - _agentPermissions["anonymous"] = perms; - // *permissionsList += perms->toVariant(); + AgentPermissionsPointer perms { new AgentPermissions(AgentPermissions::standardNameAnonymous) }; + _agentPermissions[perms->getID()] = perms; } if (!foundLoggedIn) { - AgentPermissionsPointer perms { new AgentPermissions("logged-in") }; - _agentPermissions["logged-in"] = perms; - // *permissionsList += perms->toVariant(); + AgentPermissionsPointer perms { new AgentPermissions(AgentPermissions::standardNameLoggedIn) }; + _agentPermissions[perms->getID()] = perms; } if (!foundLocalhost || !foundAnonymous || !foundLoggedIn) { packPermissions(argumentList); diff --git a/libraries/networking/src/AgentPermissions.cpp b/libraries/networking/src/AgentPermissions.cpp index afe523d15b..49c3e74eba 100644 --- a/libraries/networking/src/AgentPermissions.cpp +++ b/libraries/networking/src/AgentPermissions.cpp @@ -13,6 +13,11 @@ #include #include "AgentPermissions.h" +QString AgentPermissions::standardNameLocalhost = QString("localhost"); +QString AgentPermissions::standardNameLoggedIn = QString("logged-in"); +QString AgentPermissions::standardNameAnonymous = QString("anonymous"); + + AgentPermissions& AgentPermissions::operator|=(const AgentPermissions& rhs) { this->canConnectToDomain |= rhs.canConnectToDomain; this->canAdjustLocks |= rhs.canAdjustLocks; diff --git a/libraries/networking/src/AgentPermissions.h b/libraries/networking/src/AgentPermissions.h index 9cbda08d92..087c3c6fc8 100644 --- a/libraries/networking/src/AgentPermissions.h +++ b/libraries/networking/src/AgentPermissions.h @@ -37,6 +37,11 @@ public: QString getID() const { return _id; } + // these 3 names have special meaning. + static QString standardNameLocalhost; + static QString standardNameLoggedIn; + static QString standardNameAnonymous; + // the initializations here should match the defaults in describe-settings.json bool canConnectToDomain { true }; bool canAdjustLocks { false }; From 849bee977af3d8b35b883c17216dd5801bcfcdc3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 2 Jun 2016 12:38:54 -0700 Subject: [PATCH 0365/1237] Do not send stale vive data as valid after deactivating. (cherry picked from commit e6cc1fabe0d97006024e38de4a498b1f364fa860) --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 2e0bebeadd..fe406cc29a 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -96,6 +96,11 @@ void OpenVrDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { + // Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and + // we don't want ViveControllerManager to consider old values to be valid. + for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + _trackedDevicePose[i].bPoseIsValid = false; + } releaseOpenVrSystem(); _system = nullptr; } From adf893ea1934e1376891b5f7c59127bdc0de86ea Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 4 Jun 2016 09:50:36 -0700 Subject: [PATCH 0366/1237] rename AgentPermissions to NodePermissions --- domain-server/src/DomainGatekeeper.cpp | 10 ++--- .../src/DomainServerSettingsManager.cpp | 44 +++++++++---------- .../src/DomainServerSettingsManager.h | 6 +-- libraries/networking/src/LimitedNodeList.cpp | 6 +-- libraries/networking/src/LimitedNodeList.h | 6 +-- libraries/networking/src/Node.cpp | 4 +- libraries/networking/src/Node.h | 10 ++--- libraries/networking/src/NodeList.cpp | 2 +- ...entPermissions.cpp => NodePermissions.cpp} | 24 +++++----- .../{AgentPermissions.h => NodePermissions.h} | 36 +++++++-------- 10 files changed, 74 insertions(+), 74 deletions(-) rename libraries/networking/src/{AgentPermissions.cpp => NodePermissions.cpp} (71%) rename libraries/networking/src/{AgentPermissions.h => NodePermissions.h} (69%) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 24334b5e2e..98ac30b8b2 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -165,7 +165,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo _pendingAssignedNodes.erase(it); // always allow assignment clients to create and destroy entities - AgentPermissions userPerms; + NodePermissions userPerms; userPerms.canAdjustLocks = true; userPerms.canRezPermanentEntities = true; newNode->setPermissions(userPerms); @@ -181,7 +181,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect auto limitedNodeList = DependencyManager::get(); // start with empty permissions - AgentPermissions userPerms(username); + NodePermissions userPerms(username); userPerms.setAll(false); // check if this user is on our local machine - if this is true they are always allowed to connect @@ -189,7 +189,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); if (isLocalUser) { - userPerms |= _server->_settingsManager.getPermissionsForName(AgentPermissions::standardNameLocalhost); + userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLocalhost); } if (!username.isEmpty() && usernameSignature.isEmpty()) { @@ -204,7 +204,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (username.isEmpty()) { // they didn't tell us who they are - userPerms |= _server->_settingsManager.getPermissionsForName(AgentPermissions::standardNameAnonymous); + userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they are sent us a username and the signature verifies it if (_server->_settingsManager.havePermissionsForName(username)) { @@ -212,7 +212,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect userPerms |= _server->_settingsManager.getPermissionsForName(username); } else { // they are logged into metaverse, but we don't have specific permissions for them. - userPerms |= _server->_settingsManager.getPermissionsForName(AgentPermissions::standardNameLoggedIn); + userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLoggedIn); } } else { // they sent us a username, but it didn't check out diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 36f030f249..9faf00ab56 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -206,29 +206,29 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList(); bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool(); - _agentPermissions[AgentPermissions::standardNameLocalhost].reset( - new AgentPermissions(AgentPermissions::standardNameLocalhost)); - _agentPermissions[AgentPermissions::standardNameLocalhost]->setAll(true); - _agentPermissions[AgentPermissions::standardNameAnonymous].reset( - new AgentPermissions(AgentPermissions::standardNameAnonymous)); - _agentPermissions[AgentPermissions::standardNameLoggedIn].reset( - new AgentPermissions(AgentPermissions::standardNameLoggedIn)); + _agentPermissions[NodePermissions::standardNameLocalhost].reset( + new NodePermissions(NodePermissions::standardNameLocalhost)); + _agentPermissions[NodePermissions::standardNameLocalhost]->setAll(true); + _agentPermissions[NodePermissions::standardNameAnonymous].reset( + new NodePermissions(NodePermissions::standardNameAnonymous)); + _agentPermissions[NodePermissions::standardNameLoggedIn].reset( + new NodePermissions(NodePermissions::standardNameLoggedIn)); if (isRestrictedAccess) { // only users in allow-users list can connect - _agentPermissions[AgentPermissions::standardNameAnonymous]->canConnectToDomain = false; - _agentPermissions[AgentPermissions::standardNameLoggedIn]->canConnectToDomain = false; + _agentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false; + _agentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false; } // else anonymous and logged-in retain default of canConnectToDomain = true foreach (QString allowedUser, allowedUsers) { // even if isRestrictedAccess is false, we have to add explicit rows for these users. // defaults to canConnectToDomain = true - _agentPermissions[allowedUser].reset(new AgentPermissions(allowedUser)); + _agentPermissions[allowedUser].reset(new NodePermissions(allowedUser)); } foreach (QString allowedEditor, allowedEditors) { if (!_agentPermissions.contains(allowedEditor)) { - _agentPermissions[allowedEditor].reset(new AgentPermissions(allowedEditor)); + _agentPermissions[allowedEditor].reset(new NodePermissions(allowedEditor)); if (isRestrictedAccess) { // they can change locks, but can't connect. _agentPermissions[allowedEditor]->canConnectToDomain = false; @@ -292,11 +292,11 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL // QVariantList* permissionsList = reinterpret_cast(permissions); foreach (QVariant permsHash, permissionsList) { - AgentPermissionsPointer perms { new AgentPermissions(permsHash.toMap()) }; + NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; QString id = perms->getID(); - foundLocalhost |= (id == AgentPermissions::standardNameLocalhost); - foundAnonymous |= (id == AgentPermissions::standardNameAnonymous); - foundLoggedIn |= (id == AgentPermissions::standardNameLoggedIn); + foundLocalhost |= (id == NodePermissions::standardNameLocalhost); + foundAnonymous |= (id == NodePermissions::standardNameAnonymous); + foundLoggedIn |= (id == NodePermissions::standardNameLoggedIn); if (_agentPermissions.contains(id)) { qDebug() << "duplicate name in permissions table: " << id; _agentPermissions[id] |= perms; @@ -307,16 +307,16 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL // if any of the standard names are missing, add them if (!foundLocalhost) { - AgentPermissionsPointer perms { new AgentPermissions(AgentPermissions::standardNameLocalhost) }; + NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLocalhost) }; perms->setAll(true); _agentPermissions[perms->getID()] = perms; } if (!foundAnonymous) { - AgentPermissionsPointer perms { new AgentPermissions(AgentPermissions::standardNameAnonymous) }; + NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameAnonymous) }; _agentPermissions[perms->getID()] = perms; } if (!foundLoggedIn) { - AgentPermissionsPointer perms { new AgentPermissions(AgentPermissions::standardNameLoggedIn) }; + NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLoggedIn) }; _agentPermissions[perms->getID()] = perms; } if (!foundLocalhost || !foundAnonymous || !foundLoggedIn) { @@ -325,20 +325,20 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL #ifdef WANT_DEBUG qDebug() << "--------------- permissions ---------------------"; - QHashIterator i(_agentPermissions); + QHashIterator i(_agentPermissions); while (i.hasNext()) { i.next(); - AgentPermissionsPointer perms = i.value(); + NodePermissionsPointer perms = i.value(); qDebug() << i.key() << perms; } #endif } -AgentPermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { +NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { if (_agentPermissions.contains(name)) { return *(_agentPermissions[name].get()); } - AgentPermissions nullPermissions; + NodePermissions nullPermissions; nullPermissions.setAll(false); return nullPermissions; } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 8df0473eb3..d82f5e588c 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -19,7 +19,7 @@ #include #include -#include "AgentPermissions.h" +#include "NodePermissions.h" const QString SETTINGS_PATHS_KEY = "paths"; @@ -41,7 +41,7 @@ public: QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); } - AgentPermissions getPermissionsForName(const QString& name) const; + NodePermissions getPermissionsForName(const QString& name) const; QStringList getAllNames() { return _agentPermissions.keys(); } private slots: @@ -64,7 +64,7 @@ private: void packPermissions(const QStringList& argumentList); void unpackPermissions(const QStringList& argumentList); - QHash _agentPermissions; + QHash _agentPermissions; }; #endif // hifi_DomainServerSettingsManager_h diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 274d022ab5..a4daff5d35 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -52,7 +52,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short _numCollectedPackets(0), _numCollectedBytes(0), _packetStatTimer(), - _permissions(AgentPermissions()) + _permissions(NodePermissions()) { static bool firstCall = true; if (firstCall) { @@ -131,7 +131,7 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) { } -void LimitedNodeList::setPermissions(const AgentPermissions& newPermissions) { +void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { bool emitIsAllowedEditorChanged { false }; bool emitCanRezChanged { false }; @@ -523,7 +523,7 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) { SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - const AgentPermissions& permissions, + const NodePermissions& permissions, const QUuid& connectionSecret) { NodeHash::const_iterator it = _nodeHash.find(uuid); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index c3378a4cf8..e4c3ae9dec 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -104,7 +104,7 @@ public: const QUuid& getSessionUUID() const { return _sessionUUID; } void setSessionUUID(const QUuid& sessionUUID); - void setPermissions(const AgentPermissions& newPermissions); + void setPermissions(const NodePermissions& newPermissions); bool isAllowedEditor() const { return _permissions.canAdjustLocks; } bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; } @@ -135,7 +135,7 @@ public: SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - const AgentPermissions& permissions = DEFAULT_AGENT_PERMISSIONS, + const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS, const QUuid& connectionSecret = QUuid()); bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; } @@ -298,7 +298,7 @@ protected: int _numCollectedBytes; QElapsedTimer _packetStatTimer; - AgentPermissions _permissions; + NodePermissions _permissions; QPointer _initialSTUNTimer; diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 94f32a4e01..7201b2fd9a 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -16,7 +16,7 @@ #include "Node.h" #include "SharedUtil.h" -#include "AgentPermissions.h" +#include "NodePermissions.h" #include #include @@ -48,7 +48,7 @@ const QString& NodeType::getNodeTypeName(NodeType_t nodeType) { } Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, - const HifiSockAddr& localSocket, const AgentPermissions& permissions, const QUuid& connectionSecret, + const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret, QObject* parent) : NetworkPeer(uuid, publicSocket, localSocket, parent), _type(type), diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index abc3cfa181..367e17d345 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -27,14 +27,14 @@ #include "NodeType.h" #include "SimpleMovingAverage.h" #include "MovingPercentile.h" -#include "AgentPermissions.h" +#include "NodePermissions.h" class Node : public NetworkPeer { Q_OBJECT public: Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - const AgentPermissions& permissions, const QUuid& connectionSecret = QUuid(), + const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(), QObject* parent = 0); bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; } @@ -59,8 +59,8 @@ public: void updateClockSkewUsec(qint64 clockSkewSample); QMutex& getMutex() { return _mutex; } - void setPermissions(const AgentPermissions& newPermissions) { _permissions = newPermissions; } - AgentPermissions getPermissions() const { return _permissions; } + void setPermissions(const NodePermissions& newPermissions) { _permissions = newPermissions; } + NodePermissions getPermissions() const { return _permissions; } bool isAllowedEditor() const { return _permissions.canAdjustLocks; } bool getCanRez() const { return _permissions.canRezPermanentEntities; } @@ -81,7 +81,7 @@ private: qint64 _clockSkewUsec; QMutex _mutex; MovingPercentile _clockSkewMovingPercentile; - AgentPermissions _permissions; + NodePermissions _permissions; }; Q_DECLARE_METATYPE(Node*) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index b9f6da8c00..3bcea740e7 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -572,7 +572,7 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { qint8 nodeType; QUuid nodeUUID, connectionUUID; HifiSockAddr nodePublicSocket, nodeLocalSocket; - AgentPermissions permissions; + NodePermissions permissions; packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions; diff --git a/libraries/networking/src/AgentPermissions.cpp b/libraries/networking/src/NodePermissions.cpp similarity index 71% rename from libraries/networking/src/AgentPermissions.cpp rename to libraries/networking/src/NodePermissions.cpp index 49c3e74eba..7edff601b1 100644 --- a/libraries/networking/src/AgentPermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -1,5 +1,5 @@ // -// AgentPermissions.cpp +// NodePermissions.cpp // libraries/networking/src/ // // Created by Seth Alves on 2016-6-1. @@ -11,14 +11,14 @@ #include #include -#include "AgentPermissions.h" +#include "NodePermissions.h" -QString AgentPermissions::standardNameLocalhost = QString("localhost"); -QString AgentPermissions::standardNameLoggedIn = QString("logged-in"); -QString AgentPermissions::standardNameAnonymous = QString("anonymous"); +QString NodePermissions::standardNameLocalhost = QString("localhost"); +QString NodePermissions::standardNameLoggedIn = QString("logged-in"); +QString NodePermissions::standardNameAnonymous = QString("anonymous"); -AgentPermissions& AgentPermissions::operator|=(const AgentPermissions& rhs) { +NodePermissions& NodePermissions::operator|=(const NodePermissions& rhs) { this->canConnectToDomain |= rhs.canConnectToDomain; this->canAdjustLocks |= rhs.canAdjustLocks; this->canRezPermanentEntities |= rhs.canRezPermanentEntities; @@ -27,13 +27,13 @@ AgentPermissions& AgentPermissions::operator|=(const AgentPermissions& rhs) { this->canConnectPastMaxCapacity |= rhs.canConnectPastMaxCapacity; return *this; } -AgentPermissions& AgentPermissions::operator|=(const AgentPermissionsPointer& rhs) { +NodePermissions& NodePermissions::operator|=(const NodePermissionsPointer& rhs) { if (rhs) { *this |= *rhs.get(); } return *this; } -AgentPermissionsPointer& operator|=(AgentPermissionsPointer& lhs, const AgentPermissionsPointer& rhs) { +NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs) { if (lhs && rhs) { *lhs.get() |= rhs; } @@ -41,7 +41,7 @@ AgentPermissionsPointer& operator|=(AgentPermissionsPointer& lhs, const AgentPer } -QDataStream& operator<<(QDataStream& out, const AgentPermissions& perms) { +QDataStream& operator<<(QDataStream& out, const NodePermissions& perms) { out << perms.canConnectToDomain; out << perms.canAdjustLocks; out << perms.canRezPermanentEntities; @@ -51,7 +51,7 @@ QDataStream& operator<<(QDataStream& out, const AgentPermissions& perms) { return out; } -QDataStream& operator>>(QDataStream& in, AgentPermissions& perms) { +QDataStream& operator>>(QDataStream& in, NodePermissions& perms) { in >> perms.canConnectToDomain; in >> perms.canAdjustLocks; in >> perms.canRezPermanentEntities; @@ -61,7 +61,7 @@ QDataStream& operator>>(QDataStream& in, AgentPermissions& perms) { return in; } -QDebug operator<<(QDebug debug, const AgentPermissions& perms) { +QDebug operator<<(QDebug debug, const NodePermissions& perms) { debug.nospace() << "[permissions: " << perms.getID() << " --"; if (perms.canConnectToDomain) { debug << " connect"; @@ -84,7 +84,7 @@ QDebug operator<<(QDebug debug, const AgentPermissions& perms) { debug.nospace() << "]"; return debug.nospace(); } -QDebug operator<<(QDebug debug, const AgentPermissionsPointer& perms) { +QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms) { if (perms) { return operator<<(debug, *perms.get()); } diff --git a/libraries/networking/src/AgentPermissions.h b/libraries/networking/src/NodePermissions.h similarity index 69% rename from libraries/networking/src/AgentPermissions.h rename to libraries/networking/src/NodePermissions.h index 087c3c6fc8..82584580cc 100644 --- a/libraries/networking/src/AgentPermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -1,5 +1,5 @@ // -// AgentPermissions.h +// NodePermissions.h // libraries/networking/src/ // // Created by Seth Alves on 2016-6-1. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_AgentPermissions_h -#define hifi_AgentPermissions_h +#ifndef hifi_NodePermissions_h +#define hifi_NodePermissions_h #include #include @@ -18,14 +18,14 @@ #include #include -class AgentPermissions; -using AgentPermissionsPointer = std::shared_ptr; +class NodePermissions; +using NodePermissionsPointer = std::shared_ptr; -class AgentPermissions { +class NodePermissions { public: - AgentPermissions() { _id = QUuid::createUuid().toString(); } - AgentPermissions(const QString& name) { _id = name; } - AgentPermissions(QMap perms) { + NodePermissions() { _id = QUuid::createUuid().toString(); } + NodePermissions(const QString& name) { _id = name; } + NodePermissions(QMap perms) { _id = perms["permissions_id"].toString(); canConnectToDomain = perms["id_can_connect"].toBool(); canAdjustLocks = perms["id_can_adjust_locks"].toBool(); @@ -71,19 +71,19 @@ public: return QVariant(values); } - AgentPermissions& operator|=(const AgentPermissions& rhs); - AgentPermissions& operator|=(const AgentPermissionsPointer& rhs); - friend QDataStream& operator<<(QDataStream& out, const AgentPermissions& perms); - friend QDataStream& operator>>(QDataStream& in, AgentPermissions& perms); + NodePermissions& operator|=(const NodePermissions& rhs); + NodePermissions& operator|=(const NodePermissionsPointer& rhs); + friend QDataStream& operator<<(QDataStream& out, const NodePermissions& perms); + friend QDataStream& operator>>(QDataStream& in, NodePermissions& perms); protected: QString _id; }; -const AgentPermissions DEFAULT_AGENT_PERMISSIONS; +const NodePermissions DEFAULT_AGENT_PERMISSIONS; -QDebug operator<<(QDebug debug, const AgentPermissions& perms); -QDebug operator<<(QDebug debug, const AgentPermissionsPointer& perms); -AgentPermissionsPointer& operator|=(AgentPermissionsPointer& lhs, const AgentPermissionsPointer& rhs); +QDebug operator<<(QDebug debug, const NodePermissions& perms); +QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms); +NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs); -#endif // hifi_AgentPermissions_h +#endif // hifi_NodePermissions_h From 80eeff5a64f80170e363143daf223b771907fb2c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 4 Jun 2016 10:32:07 -0700 Subject: [PATCH 0367/1237] bump packet versions. change how domainObject[RESTRICTED_ACCESS_FLAG] is decided --- domain-server/src/DomainServer.cpp | 4 +++- libraries/networking/src/NodePermissions.cpp | 4 ++++ libraries/networking/src/NodePermissions.h | 1 + libraries/networking/src/udt/PacketHeaders.cpp | 5 ++++- libraries/networking/src/udt/PacketHeaders.h | 10 ++++++++++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 5ad6862b62..020983eb03 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1087,7 +1087,9 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings const QString RESTRICTED_ACCESS_FLAG = "restricted"; - domainObject[RESTRICTED_ACCESS_FLAG] = _settingsManager.getAllNames().length() > 0; + // consider the domain to have restricted access if "anonymous" connections can't connect to the domain. + NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); + domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain; // figure out the breakdown of currently connected interface clients int numConnectedUnassigned = 0; diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index 7edff601b1..5f6dfa6b2f 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -17,6 +17,10 @@ QString NodePermissions::standardNameLocalhost = QString("localhost"); QString NodePermissions::standardNameLoggedIn = QString("logged-in"); QString NodePermissions::standardNameAnonymous = QString("anonymous"); +QList NodePermissions::standardNames = QList() + << NodePermissions::standardNameLocalhost + << NodePermissions::standardNameLoggedIn + << NodePermissions::standardNameAnonymous; NodePermissions& NodePermissions::operator|=(const NodePermissions& rhs) { this->canConnectToDomain |= rhs.canConnectToDomain; diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 82584580cc..22e5950426 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -41,6 +41,7 @@ public: static QString standardNameLocalhost; static QString standardNameLoggedIn; static QString standardNameAnonymous; + static QList standardNames; // the initializations here should match the defaults in describe-settings.json bool canConnectToDomain { true }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index db743f81e4..6ca50420f3 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -45,7 +45,7 @@ const QSet RELIABLE_PACKETS = QSet(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return 18; + return static_cast(DomainListVersion::PermissionsGrid); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: @@ -69,6 +69,9 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::DomainConnectRequest: return static_cast(DomainConnectRequestVersion::HasProtocolVersions); + case PacketType::DomainServerAddedNode: + return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); + default: return 17; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 320635379d..ae54450fee 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -199,4 +199,14 @@ enum class DomainConnectionDeniedVersion : PacketVersion { IncludesReasonCode }; +enum class DomainServerAddedNodeVersion : PacketVersion { + PrePermissionsGrid = 17, + PermissionsGrid +}; + +enum class DomainListVersion : PacketVersion { + PrePermissionsGrid = 18, + PermissionsGrid +}; + #endif // hifi_PacketHeaders_h From be403865fc207f67942da8a3d54ab54a0acdd067 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 4 Jun 2016 10:44:59 -0700 Subject: [PATCH 0368/1237] don't request public keys for the standard psuedo-account-names --- domain-server/src/DomainGatekeeper.cpp | 5 +++++ libraries/networking/src/NodePermissions.cpp | 2 +- libraries/networking/src/NodePermissions.h | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 98ac30b8b2..10e22c3293 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -404,6 +404,11 @@ void DomainGatekeeper::preloadAllowedUserPublicKeys() { } void DomainGatekeeper::requestUserPublicKey(const QString& username) { + // don't request public keys for the standard psuedo-account-names + if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { + return; + } + // even if we have a public key for them right now, request a new one in case it has just changed JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index 5f6dfa6b2f..fb74ccdc94 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -17,7 +17,7 @@ QString NodePermissions::standardNameLocalhost = QString("localhost"); QString NodePermissions::standardNameLoggedIn = QString("logged-in"); QString NodePermissions::standardNameAnonymous = QString("anonymous"); -QList NodePermissions::standardNames = QList() +QStringList NodePermissions::standardNames = QList() << NodePermissions::standardNameLocalhost << NodePermissions::standardNameLoggedIn << NodePermissions::standardNameAnonymous; diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 22e5950426..adc13ba375 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -41,7 +41,7 @@ public: static QString standardNameLocalhost; static QString standardNameLoggedIn; static QString standardNameAnonymous; - static QList standardNames; + static QStringList standardNames; // the initializations here should match the defaults in describe-settings.json bool canConnectToDomain { true }; From dd009bf8b9d08ad31a2189dd03c16ece05dc41de Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 4 Jun 2016 11:06:37 -0700 Subject: [PATCH 0369/1237] add some debugging prints. make blacklisting work --- domain-server/src/DomainGatekeeper.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 10e22c3293..c58ccc4036 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -190,6 +190,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); if (isLocalUser) { userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLocalhost); + qDebug() << "user-permissions: is local user, so:" << userPerms; } if (!username.isEmpty() && usernameSignature.isEmpty()) { @@ -197,7 +198,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); // ask for their public key right now to make sure we have it requestUserPublicKey(username); - if (!isLocalUser) { + if (!userPerms.canConnectToDomain) { return SharedNodePointer(); } } @@ -205,24 +206,31 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (username.isEmpty()) { // they didn't tell us who they are userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); + qDebug() << "user-permissions: no username, so:" << userPerms; } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they are sent us a username and the signature verifies it if (_server->_settingsManager.havePermissionsForName(username)) { // we have specific permissions for this user. userPerms |= _server->_settingsManager.getPermissionsForName(username); + qDebug() << "user-permissions: specific user matches, so:" << userPerms; } else { // they are logged into metaverse, but we don't have specific permissions for them. userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLoggedIn); + qDebug() << "user-permissions: user is logged in, so:" << userPerms; } } else { // they sent us a username, but it didn't check out requestUserPublicKey(username); - if (!isLocalUser) { + if (!userPerms.canConnectToDomain) { return SharedNodePointer(); } } + qDebug() << "user-permissions: final:" << userPerms; + if (!userPerms.canConnectToDomain) { + sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers); return SharedNodePointer(); } @@ -330,7 +338,7 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, rsaPublicKey); if (decryptResult == 1) { - qDebug() << "Username signature matches for" << username << "- allowing connection."; + qDebug() << "Username signature matches for" << username; // free up the public key and remove connection token before we return RSA_free(rsaPublicKey); From b6cb5617d3b1f98b746377690324b59eb4ea3d16 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 5 Jun 2016 08:55:27 +1200 Subject: [PATCH 0370/1237] Save toolbar position when moving it only when stopped moving it --- scripts/system/libraries/toolBars.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index 9efe533457..5a84bf9027 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -256,7 +256,6 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit y: y - ToolBar.SPACING }); } - this.save(); } this.setAlpha = function(alpha, tool) { @@ -421,6 +420,9 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit for (var tool in that.tools) { that.tools[tool].buttonDown(false); } + if (that.mightBeDragging) { + that.save(); + } } this.mouseMove = function (event) { if (!that.mightBeDragging || !event.isLeftButton) { From 39dcd1f9bd602bce050816a50a263b8540bbea64 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 4 Jun 2016 17:55:30 -0700 Subject: [PATCH 0371/1237] Eliminate file IO contentions for the settings --- interface/src/avatar/MyAvatar.cpp | 2 +- libraries/shared/src/SettingHandle.cpp | 65 +++++++++++++++++++ libraries/shared/src/SettingHandle.h | 32 +++++++-- libraries/shared/src/SettingInterface.cpp | 63 +++++++++++------- libraries/shared/src/SettingInterface.h | 10 ++- libraries/shared/src/SettingManager.cpp | 41 +++++++++--- libraries/shared/src/SettingManager.h | 16 +++-- .../shared/src/shared/ReadWriteLockable.h | 2 + 8 files changed, 186 insertions(+), 45 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6fdcd8f797..f48104235d 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -739,7 +739,7 @@ void MyAvatar::saveData() { settings.endGroup(); } -float loadSetting(QSettings& settings, const char* name, float defaultValue) { +float loadSetting(Settings& settings, const QString& name, float defaultValue) { float value = settings.value(name, defaultValue).toFloat(); if (glm::isnan(value)) { value = defaultValue; diff --git a/libraries/shared/src/SettingHandle.cpp b/libraries/shared/src/SettingHandle.cpp index d2e84d8b75..cad2a0286f 100644 --- a/libraries/shared/src/SettingHandle.cpp +++ b/libraries/shared/src/SettingHandle.cpp @@ -10,11 +10,76 @@ // #include "SettingHandle.h" +#include "SettingManager.h" #include + + const QString Settings::firstRun { "firstRun" }; +Settings::Settings() : + _manager(DependencyManager::get()), + _locker(&(_manager->getLock())) +{ +} + +Settings::~Settings() { +} + +void Settings::remove(const QString& key) { + _manager->remove(key); +} + +QStringList Settings::childGroups() const { + return _manager->childGroups(); +} + +QStringList Settings::childKeys() const { + return _manager->childKeys(); +} + +QStringList Settings::allKeys() const { + return _manager->allKeys(); +} + +bool Settings::contains(const QString& key) const { + return _manager->contains(key); +} + +int Settings::beginReadArray(const QString & prefix) { + return _manager->beginReadArray(prefix); +} + +void Settings::beginWriteArray(const QString& prefix, int size) { + _manager->beginWriteArray(prefix, size); +} + +void Settings::endArray() { + _manager->endArray(); +} + +void Settings::setArrayIndex(int i) { + _manager->setArrayIndex(i); +} + +void Settings::beginGroup(const QString& prefix) { + _manager->beginGroup(prefix); +} + +void Settings::endGroup() { + _manager->endGroup(); +} + +void Settings::setValue(const QString& name, const QVariant& value) { + _manager->setValue(name, value); +} + +QVariant Settings::value(const QString& name, const QVariant& defaultValue) const { + return _manager->value(name, defaultValue); +} + + void Settings::getFloatValueIfValid(const QString& name, float& floatValue) { const QVariant badDefaultValue = NAN; bool ok = true; diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index bef2daf84c..e83c563036 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -14,19 +14,40 @@ #include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include "SettingInterface.h" + // TODO: remove -class Settings : public QSettings { +class Settings { public: static const QString firstRun; + Settings(); + ~Settings(); + + void remove(const QString& key); + QStringList childGroups() const; + QStringList childKeys() const; + QStringList allKeys() const; + bool contains(const QString& key) const; + int beginReadArray(const QString & prefix); + void beginWriteArray(const QString& prefix, int size = -1); + void endArray(); + void setArrayIndex(int i); + + void beginGroup(const QString& prefix); + void endGroup(); + + void setValue(const QString& name, const QVariant& value); + QVariant value(const QString& name, const QVariant& defaultValue = QVariant()) const; void getFloatValueIfValid(const QString& name, float& floatValue); void getBoolValue(const QString& name, bool& boolValue); @@ -36,6 +57,9 @@ public: void setQuatValue(const QString& name, const glm::quat& quatValue); void getQuatValueIfValid(const QString& name, glm::quat& quatValue); + + QSharedPointer _manager; + QWriteLocker _locker; }; namespace Setting { diff --git a/libraries/shared/src/SettingInterface.cpp b/libraries/shared/src/SettingInterface.cpp index a931875771..630d52bf7d 100644 --- a/libraries/shared/src/SettingInterface.cpp +++ b/libraries/shared/src/SettingInterface.cpp @@ -9,27 +9,33 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "SettingInterface.h" + #include #include #include #include #include "PathUtils.h" -#include "SettingInterface.h" #include "SettingManager.h" #include "SharedLogging.h" namespace Setting { - static Manager* privateInstance = nullptr; + static QSharedPointer globalManager; + const QString Interface::FIRST_RUN { "firstRun" }; + // cleans up the settings private instance. Should only be run once at closing down. void cleanupPrivateInstance() { // grab the thread before we nuke the instance - QThread* settingsManagerThread = privateInstance->thread(); - + QThread* settingsManagerThread = DependencyManager::get()->thread(); + // tell the private instance to clean itself up on its thread - privateInstance->deleteLater(); - privateInstance = NULL; + DependencyManager::destroy(); + + // + globalManager->deleteLater(); + globalManager.reset(); // quit the settings manager thread and wait on it to make sure it's gone settingsManagerThread->quit(); @@ -63,14 +69,13 @@ namespace Setting { QThread* thread = new QThread(); Q_CHECK_PTR(thread); thread->setObjectName("Settings Thread"); - - privateInstance = new Manager(); - Q_CHECK_PTR(privateInstance); - QObject::connect(privateInstance, SIGNAL(destroyed()), thread, SLOT(quit())); - QObject::connect(thread, SIGNAL(started()), privateInstance, SLOT(startTimer())); + globalManager = DependencyManager::set(); + + QObject::connect(globalManager.data(), SIGNAL(destroyed()), thread, SLOT(quit())); + QObject::connect(thread, SIGNAL(started()), globalManager.data(), SLOT(startTimer())); QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); - privateInstance->moveToThread(thread); + globalManager->moveToThread(thread); thread->start(); qCDebug(shared) << "Settings thread started."; @@ -79,7 +84,7 @@ namespace Setting { } void Interface::init() { - if (!privateInstance) { + if (!DependencyManager::isSet()) { // WARNING: As long as we are using QSettings this should always be triggered for each Setting::Handle // in an assignment-client - the QSettings backing we use for this means persistence of these // settings from an AC (when there can be multiple terminating at same time on one machine) @@ -87,9 +92,13 @@ namespace Setting { qWarning() << "Setting::Interface::init() for key" << _key << "- Manager not yet created." << "Settings persistence disabled."; } else { - // Register Handle - privateInstance->registerHandle(this); - _isInitialized = true; + _manager = DependencyManager::get(); + auto manager = _manager.lock(); + if (manager) { + // Register Handle + manager->registerHandle(this); + _isInitialized = true; + } // Load value from disk load(); @@ -97,11 +106,13 @@ namespace Setting { } void Interface::deinit() { - if (_isInitialized && privateInstance) { - // Save value to disk - save(); - - privateInstance->removeHandle(_key); + if (_isInitialized && _manager) { + auto manager = _manager.lock(); + if (manager) { + // Save value to disk + save(); + manager->removeHandle(_key); + } } } @@ -113,14 +124,16 @@ namespace Setting { } void Interface::save() { - if (privateInstance) { - privateInstance->saveSetting(this); + auto manager = _manager.lock(); + if (manager) { + manager->saveSetting(this); } } void Interface::load() { - if (privateInstance) { - privateInstance->loadSetting(this); + auto manager = _manager.lock(); + if (manager) { + manager->loadSetting(this); } } } diff --git a/libraries/shared/src/SettingInterface.h b/libraries/shared/src/SettingInterface.h index 3aad048dbe..8a868c5900 100644 --- a/libraries/shared/src/SettingInterface.h +++ b/libraries/shared/src/SettingInterface.h @@ -12,8 +12,10 @@ #ifndef hifi_SettingInterface_h #define hifi_SettingInterface_h -#include -#include +#include +#include +#include +#include namespace Setting { void preInit(); @@ -22,6 +24,8 @@ namespace Setting { class Interface { public: + static const QString FIRST_RUN; + QString getKey() const { return _key; } bool isSet() const { return _isSet; } @@ -44,6 +48,8 @@ namespace Setting { const QString _key; friend class Manager; + + QWeakPointer _manager; }; } diff --git a/libraries/shared/src/SettingManager.cpp b/libraries/shared/src/SettingManager.cpp index 7c0051c809..abe6d50f2e 100644 --- a/libraries/shared/src/SettingManager.cpp +++ b/libraries/shared/src/SettingManager.cpp @@ -9,13 +9,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include -#include +#include +#include +#include #include "SettingInterface.h" #include "SettingManager.h" namespace Setting { + Manager::~Manager() { // Cleanup timer stopTimer(); @@ -27,6 +29,10 @@ namespace Setting { // sync will be called in the QSettings destructor } + // Custom deleter does nothing, because we need to shutdown later than the dependency manager + void Manager::customDeleter() { } + + void Manager::registerHandle(Setting::Interface* handle) { QString key = handle->getKey(); withWriteLock([&] { @@ -47,12 +53,17 @@ namespace Setting { handle->setVariant(value(handle->getKey())); } + void Manager::saveSetting(Interface* handle) { + auto key = handle->getKey(); + QVariant handleValue = UNSET_VALUE; if (handle->isSet()) { - setValue(handle->getKey(), handle->getVariant()); - } else { - remove(handle->getKey()); + handleValue = handle->getVariant(); } + + withWriteLock([&] { + _pendingChanges[key] = handleValue; + }); } static const int SAVE_INTERVAL_MSEC = 5 * 1000; // 5 sec @@ -74,12 +85,24 @@ namespace Setting { } void Manager::saveAll() { - withReadLock([&] { - for (auto handle : _handles) { - saveSetting(handle); - } + QHash newPendingChanges; + withWriteLock([&] { + newPendingChanges.swap(_pendingChanges); }); + for (auto key : newPendingChanges.keys()) { + auto newValue = newPendingChanges[key]; + auto savedValue = value(key, UNSET_VALUE); + if (newValue == savedValue) { + continue; + } + if (newValue == UNSET_VALUE) { + remove(key); + } else { + setValue(key, newValue); + } + } + // Restart timer if (_saveTimer) { _saveTimer->start(); diff --git a/libraries/shared/src/SettingManager.h b/libraries/shared/src/SettingManager.h index e4981f1bce..1f309c966f 100644 --- a/libraries/shared/src/SettingManager.h +++ b/libraries/shared/src/SettingManager.h @@ -12,17 +12,23 @@ #ifndef hifi_SettingManager_h #define hifi_SettingManager_h -#include -#include -#include +#include +#include +#include +#include +#include "DependencyManager.h" #include "shared/ReadWriteLockable.h" namespace Setting { class Interface; - class Manager : public QSettings, public ReadWriteLockable { + class Manager : public QSettings, public ReadWriteLockable, public Dependency { Q_OBJECT + + public: + void customDeleter() override; + protected: ~Manager(); void registerHandle(Interface* handle); @@ -40,6 +46,8 @@ namespace Setting { private: QHash _handles; QPointer _saveTimer = nullptr; + const QVariant UNSET_VALUE { QUuid::createUuid().variant() }; + QHash _pendingChanges; friend class Interface; friend void cleanupPrivateInstance(); diff --git a/libraries/shared/src/shared/ReadWriteLockable.h b/libraries/shared/src/shared/ReadWriteLockable.h index 07b46bb92a..539678a73d 100644 --- a/libraries/shared/src/shared/ReadWriteLockable.h +++ b/libraries/shared/src/shared/ReadWriteLockable.h @@ -45,6 +45,8 @@ public: template bool withTryReadLock(F&& f, int timeout) const; + QReadWriteLock& getLock() const { return _lock; } + private: mutable QReadWriteLock _lock { QReadWriteLock::Recursive }; }; From 65d8f65ed7be48098dabcc76b3b63a89038cb0ba Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 4 Jun 2016 18:39:46 -0700 Subject: [PATCH 0372/1237] Fix unix builds, make settings ACID --- libraries/shared/src/SettingInterface.h | 4 ++- libraries/shared/src/SettingManager.cpp | 41 +++++++++++++++---------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/libraries/shared/src/SettingInterface.h b/libraries/shared/src/SettingInterface.h index 8a868c5900..2b32e8a3b4 100644 --- a/libraries/shared/src/SettingInterface.h +++ b/libraries/shared/src/SettingInterface.h @@ -18,6 +18,8 @@ #include namespace Setting { + class Manager; + void preInit(); void init(); void cleanupSettings(); @@ -26,7 +28,7 @@ namespace Setting { public: static const QString FIRST_RUN; - QString getKey() const { return _key; } + const QString& getKey() const { return _key; } bool isSet() const { return _isSet; } virtual void setVariant(const QVariant& variant) = 0; diff --git a/libraries/shared/src/SettingManager.cpp b/libraries/shared/src/SettingManager.cpp index abe6d50f2e..bacec2ee0c 100644 --- a/libraries/shared/src/SettingManager.cpp +++ b/libraries/shared/src/SettingManager.cpp @@ -50,12 +50,21 @@ namespace Setting { } void Manager::loadSetting(Interface* handle) { - handle->setVariant(value(handle->getKey())); + const auto& key = handle->getKey(); + withWriteLock([&] { + QVariant loadedValue; + if (_pendingChanges.contains(key)) { + loadedValue = _pendingChanges[key]; + } else { + loadedValue = value(key); + } + handle->setVariant(loadedValue); + }); } void Manager::saveSetting(Interface* handle) { - auto key = handle->getKey(); + const auto& key = handle->getKey(); QVariant handleValue = UNSET_VALUE; if (handle->isSet()) { handleValue = handle->getVariant(); @@ -85,24 +94,22 @@ namespace Setting { } void Manager::saveAll() { - QHash newPendingChanges; withWriteLock([&] { - newPendingChanges.swap(_pendingChanges); + for (auto key : _pendingChanges.keys()) { + auto newValue = _pendingChanges[key]; + auto savedValue = value(key, UNSET_VALUE); + if (newValue == savedValue) { + continue; + } + if (newValue == UNSET_VALUE) { + remove(key); + } else { + setValue(key, newValue); + } + } + _pendingChanges.clear(); }); - for (auto key : newPendingChanges.keys()) { - auto newValue = newPendingChanges[key]; - auto savedValue = value(key, UNSET_VALUE); - if (newValue == savedValue) { - continue; - } - if (newValue == UNSET_VALUE) { - remove(key); - } else { - setValue(key, newValue); - } - } - // Restart timer if (_saveTimer) { _saveTimer->start(); From bc81f00dc76e45a29f79800a5949a5fe7c0515ca Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 4 Jun 2016 20:47:10 -0700 Subject: [PATCH 0373/1237] Eliminate extraneous writes for unchanged values --- libraries/shared/src/SettingHandle.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/shared/src/SettingHandle.cpp b/libraries/shared/src/SettingHandle.cpp index cad2a0286f..b2f23f5a04 100644 --- a/libraries/shared/src/SettingHandle.cpp +++ b/libraries/shared/src/SettingHandle.cpp @@ -28,7 +28,9 @@ Settings::~Settings() { } void Settings::remove(const QString& key) { - _manager->remove(key); + if (key == "" || _manager->contains(key)) { + _manager->remove(key); + } } QStringList Settings::childGroups() const { @@ -72,7 +74,9 @@ void Settings::endGroup() { } void Settings::setValue(const QString& name, const QVariant& value) { - _manager->setValue(name, value); + if (_manager->value(name) != value) { + _manager->setValue(name, value); + } } QVariant Settings::value(const QString& name, const QVariant& defaultValue) const { From 062711b202612731adf78168c348729924e1fc0e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 4 Jun 2016 20:51:21 -0700 Subject: [PATCH 0374/1237] Revert "Update the build docs for a better OpenSSL security posture." --- BUILD.md | 4 ++-- BUILD_WIN.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index e89933fa7d..c1ccd3193e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,8 +2,8 @@ * [cmake](http://www.cmake.org/cmake/resources/software.html) ~> 3.3.2 * [Qt](http://www.qt.io/download-open-source) ~> 5.5.1 -* [OpenSSL](https://www.openssl.org/community/binaries.html) ~> 1.0.1 (highest letter) - * IMPORTANT: Using the recommended version of OpenSSL is critical to avoid security vulnerabilities. Make sure to check regularly to see if there is a new version. +* [OpenSSL](https://www.openssl.org/community/binaries.html) ~> 1.0.1m + * IMPORTANT: Using the recommended version of OpenSSL is critical to avoid security vulnerabilities. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) ####CMake External Project Dependencies diff --git a/BUILD_WIN.md b/BUILD_WIN.md index a80c279798..25e20952ca 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -73,8 +73,8 @@ Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll QSslSocket: cannot resolve SSL_get0_next_proto_negotiated To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](http://slproweb.com/products/Win32OpenSSL.html): -* Win32 OpenSSL v1.0.1 (highest letter) -* Win64 OpenSSL v1.0.1 (highest letter) +* Win32 OpenSSL v1.0.1q +* Win64 OpenSSL v1.0.1q Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. From 47a84fe91d4e92233dfce7f3e9a86e10eae5aa5a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Sat, 4 Jun 2016 20:51:46 -0700 Subject: [PATCH 0375/1237] make shelf stuff wearable --- .../Home/kineticObjects/stuff_on_shelves.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json b/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json index cecd02197a..ee5d44d49c 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json @@ -35,7 +35,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T23:06:57Z", @@ -72,7 +72,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T22:45:06Z", @@ -109,7 +109,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T23:06:57Z", @@ -146,7 +146,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T22:47:17Z", @@ -183,7 +183,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T22:49:28Z", @@ -220,7 +220,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }], "Version": 57 } \ No newline at end of file From 49c835b6fc72707cedc9c8a66708daaeabe28a95 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 4 Jun 2016 20:53:30 -0700 Subject: [PATCH 0376/1237] use camera twist for walk steering --- interface/src/avatar/MyAvatar.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6fdcd8f797..ca9bbec6ff 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1214,7 +1214,10 @@ void MyAvatar::updateMotors() { if (_characterController.getState() == CharacterController::State::Hover) { motorRotation = getHead()->getCameraOrientation(); } else { - motorRotation = getOrientation(); + // non-hovering = walking: follow camera twist about vertical but not lift + // so we decompose camera's rotation and store the twist part in motorRotation + glm::quat liftRotation; + swingTwistDecomposition(getHead()->getCameraOrientation(), getHead()->getUpDirection(), liftRotation, motorRotation); } const float DEFAULT_MOTOR_TIMESCALE = 0.2f; const float INVALID_MOTOR_TIMESCALE = 1.0e6f; From e6844e246884a5181e016746b7df2c41771e71f8 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 4 Jun 2016 21:11:31 -0700 Subject: [PATCH 0377/1237] use world-UP rather than head-UP --- interface/src/avatar/MyAvatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ca9bbec6ff..074cef865e 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1217,7 +1217,7 @@ void MyAvatar::updateMotors() { // non-hovering = walking: follow camera twist about vertical but not lift // so we decompose camera's rotation and store the twist part in motorRotation glm::quat liftRotation; - swingTwistDecomposition(getHead()->getCameraOrientation(), getHead()->getUpDirection(), liftRotation, motorRotation); + swingTwistDecomposition(getHead()->getCameraOrientation(), _worldUpDirection, liftRotation, motorRotation); } const float DEFAULT_MOTOR_TIMESCALE = 0.2f; const float INVALID_MOTOR_TIMESCALE = 1.0e6f; From eb3cafb4f93f2ef64c584cfc18ed71119f91ed02 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 6 Jun 2016 08:59:41 -0700 Subject: [PATCH 0378/1237] shelf things --- .../kineticObjects/dressingRoomBricabrac.json | 26 +++++++++---------- .../Home/kineticObjects/stuff_on_shelves.json | 12 ++++----- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json index fdfb505846..c87fd7bc98 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/dressingRoomBricabrac.json @@ -36,7 +36,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, @@ -74,7 +74,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, @@ -112,7 +112,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", @@ -152,7 +152,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", @@ -192,7 +192,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, @@ -231,7 +231,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", @@ -271,7 +271,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", @@ -311,7 +311,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", @@ -351,7 +351,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, @@ -389,7 +389,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collisionsWillMove": 1, @@ -428,7 +428,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", @@ -468,7 +468,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }, { "name": "home_model_dressing_room_bricabrac", "collidesWith": "static,dynamic,kinematic,otherAvatar,", @@ -548,7 +548,7 @@ }, "shapeType": "compound", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" }], "Version": 57 } \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json b/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json index ee5d44d49c..cecd02197a 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/stuff_on_shelves.json @@ -35,7 +35,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T23:06:57Z", @@ -72,7 +72,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T22:45:06Z", @@ -109,7 +109,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T23:06:57Z", @@ -146,7 +146,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T22:47:17Z", @@ -183,7 +183,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }, { "collisionsWillMove": 1, "created": "2016-03-31T22:49:28Z", @@ -220,7 +220,7 @@ }, "shapeType": "box", "type": "Model", - "userData": "{\"hifiHomeKey\":{\"reset\":true},\"wearable\":{\"joints\":{\"head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"Head\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"hair\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}],\"neck\":[{\"x\":0,\"y\":0,\"z\":0},{\"w\":0,\"x\":0,\"y\":0,\"z\":0}]}}}" + "userData": "{\"hifiHomeKey\":{\"reset\":true}}" }], "Version": 57 } \ No newline at end of file From 3603c8cf2ec2d0067dc22112203563e88d7ae983 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 6 Jun 2016 09:12:07 -0700 Subject: [PATCH 0379/1237] add hover game scripts --- .../DomainContent/Home/hoverGame/hoverBall.js | 60 +++++++++++++++ .../Home/hoverGame/hoverContainer.js | 28 +++++++ .../DomainContent/Home/hoverGame/wrapper.js | 77 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 unpublishedScripts/DomainContent/Home/hoverGame/hoverBall.js create mode 100644 unpublishedScripts/DomainContent/Home/hoverGame/hoverContainer.js create mode 100644 unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js diff --git a/unpublishedScripts/DomainContent/Home/hoverGame/hoverBall.js b/unpublishedScripts/DomainContent/Home/hoverGame/hoverBall.js new file mode 100644 index 0000000000..b4072d4b69 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/hoverGame/hoverBall.js @@ -0,0 +1,60 @@ +(function() { + var _this; + HoverBall = function() { + _this = this; + } + + var MIN_DISTANCE_THRESHOLD = 0.075; + + var CENTER_POINT_LOCATION = { + x: 0, + y: 0, + z: 0 + }; + + HoverBall.prototype = { + preload: function(entityID) { + this.entityID = entityID; + + }, + unload: function() { + + }, + startDistanceGrab: function() { + + }, + continueDistantGrab: function() { + var position = Entities.getEntityProperties(_this.entityID).position; + var distanceFromCenterPoint = Vec3.distance(position, CENTER_POINT_LOCATION); + if (distanceFromCenterPoint < MIN_DISTANCE_THRESHOLD) { + + _this.turnOnGlow(); + } else { + _this.turnOffGlow(); + } + }, + releaseGrab: function() { + _this.turnOffGlow(); + }, + turnOnGlow: function() { + + }, + turnOffGlow: function() { + + }, + findHoverContainer: function() { + var position = Entities.getEntityProperties(_this.entityID).position; + var results = Entities.findEntities(position, 3); + results.forEach(function(item) { + var props = Entities.getEntityProperties(item); + if (props.name.indexOf('hoverGame_container') > -1) { + return item + } + }) + }, + + } + + return new HoverBall(); + +}) \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/hoverGame/hoverContainer.js b/unpublishedScripts/DomainContent/Home/hoverGame/hoverContainer.js new file mode 100644 index 0000000000..3391879793 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/hoverGame/hoverContainer.js @@ -0,0 +1,28 @@ +(function() { + + HoverContainer = function() { + + } + + HoverContainer.prototype = { + preload: function(entityID) { + this.entityID = entityID; + + var data = { + action: 'add', + id: this.entityID + }; + Messages.sendLocalMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)) + }, + unload: function() { + var data = { + action: 'remove', + id: this.entityID + }; + Messages.sendLocalMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)) + } + } + + return new HoverContainer(); + +}) \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js b/unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js new file mode 100644 index 0000000000..d38d4d47c3 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js @@ -0,0 +1,77 @@ +// createPingPongGun.js +// +// Script Type: Entity Spawner +// Created by James B. Pollack on 9/30/2015 +// Copyright 2015 High Fidelity, Inc. +// +// This script creates a gun that shoots ping pong balls when you pull the trigger on a hand controller. +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +HoverGame = function(spawnPosition, spawnRotation) { + + var scriptURL = "atp:/hoverGame/hoverInner.js"; + + var hoverContainerProps = { + type: 'Model', + modelURL: 'atp:/hoverGame/hover.fbx', + name: 'home_model_hoverGame_container', + dimensions: { + x: 0.2543, + y: 0.3269, + z: 0.4154 + }, + compoundShapeURL: 'atp:/hoverGame/hoverHull.obj', + rotation: spawnRotation, + script: scriptURL, + userData: JSON.stringify({ + "grabbableKey": { + "grabbable":false + }, + 'hifiHomeKey': { + 'reset': true + } + }), + dynamic: true, + position: spawnPosition + }; + + var hoverBall = { + type: 'Sphere', + name: 'home_model_hoverGame_sphere', + dimensions: { + x: 0.25, + y: 0.25, + z: 0.25, + }, + compoundShapeURL: 'atp:/hoverGame/hoverHull.obj', + rotation: spawnRotation, + script: scriptURL, + userData: JSON.stringify({ + 'hifiHomeKey': { + 'reset': true + } + }), + dynamic: true, + gravity:{ + x:0, + y:-9.8, + z:0 + }, + position: spawnPosition + }; + + var hoverContainer = Entities.addEntity(hoverContainerProps); + + function cleanup() { + print('HOVER GAME CLEANUP!') + Entities.deleteEntity(hoverInner); + } + + this.cleanup = cleanup; + + print('HOME CREATED HOVER GAME') + +} \ No newline at end of file From 8d90570f7299b1db738f96925a21b51c34e2d1d7 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 6 Jun 2016 09:29:50 -0700 Subject: [PATCH 0380/1237] Correcting the Z sign --- .../render-utils/src/DeferredTransform.slh | 3 +- .../render-utils/src/SurfaceGeometryPass.cpp | 58 ++++++++-- .../render-utils/src/SurfaceGeometryPass.h | 3 + .../src/surfaceGeometry_makeCurvature.slf | 108 +++++++++++------- .../src/surfaceGeometry_makeLinearDepth.slf | 25 ++++ 5 files changed, 145 insertions(+), 52 deletions(-) create mode 100644 libraries/render-utils/src/surfaceGeometry_makeLinearDepth.slf diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 25a62fca3d..7c3e7bf4da 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -19,7 +19,7 @@ struct DeferredFrameTransform { vec4 _depthInfo; vec4 _stereoInfo; mat4 _projection[2]; - mat4 _invView; + mat4 _viewInverse; }; uniform deferredFrameTransformBuffer { @@ -91,5 +91,4 @@ ivec2 getPixelPosNclipPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out ve <@endfunc@> - <@endif@> \ No newline at end of file diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index ac3f04383b..6e4bc466bb 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -18,6 +18,9 @@ const int SurfaceGeometryPass_FrameTransformSlot = 0; const int SurfaceGeometryPass_ParamsSlot = 1; const int SurfaceGeometryPass_DepthMapSlot = 0; +const int SurfaceGeometryPass_NormalMapSlot = 1; + +#include "surfaceGeometry_makeLinearDepth_frag.h" #include "surfaceGeometry_makeCurvature_frag.h" @@ -36,8 +39,10 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto framebufferCache = DependencyManager::get(); auto depthBuffer = framebufferCache->getPrimaryDepthTexture(); - // auto normalBuffer = framebufferCache->getDeferredNormalTexture(); - // auto pyramidFBO = framebufferCache->getDepthPyramidFramebuffer(); + auto normalTexture = framebufferCache->getDeferredNormalTexture(); + auto pyramidFBO = framebufferCache->getDepthPyramidFramebuffer(); + + auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); QSize framebufferSize = framebufferCache->getFrameBufferSize(); @@ -47,13 +52,12 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c float tHeight = args->_viewport.w / (float)framebufferSize.height(); + auto linearDepthPipeline = getLinearDepthPipeline(); auto curvaturePipeline = getCurvaturePipeline(); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); - // _gpuTimer.begin(batch); - batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); batch.setViewTransform(Transform()); @@ -64,20 +68,55 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setModelTransform(model); batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); - // batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); - - + // Pyramid pass - batch.setFramebuffer(curvatureFBO); + batch.setFramebuffer(pyramidFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(args->getViewFrustum().getFarClip(), 0.0f, 0.0f, 0.0f)); - batch.setPipeline(curvaturePipeline); + batch.setPipeline(linearDepthPipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); + // Pyramid pass + batch.setFramebuffer(curvatureFBO); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + batch.setPipeline(curvaturePipeline); + batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, pyramidTexture); + batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, normalTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); + batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, nullptr); + }); } +const gpu::PipelinePointer& SurfaceGeometryPass::getLinearDepthPipeline() { + if (!_linearDepthPipeline) { + auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(surfaceGeometry_makeLinearDepth_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SurfaceGeometryPass_FrameTransformSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), SurfaceGeometryPass_DepthMapSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + state->setColorWriteMask(true, false, false, false); + + // Good to go add the brand new pipeline + _linearDepthPipeline = gpu::Pipeline::create(program, state); + } + + return _linearDepthPipeline; +} + const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { if (!_curvaturePipeline) { auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); @@ -88,6 +127,7 @@ const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SurfaceGeometryPass_FrameTransformSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("ambientOcclusionParamsBuffer"), SurfaceGeometryPass_ParamsSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), SurfaceGeometryPass_DepthMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), SurfaceGeometryPass_NormalMapSlot)); gpu::Shader::makeProgram(*program, slotBindings); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 5501f43659..13ca7fa199 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -65,8 +65,11 @@ private: }; gpu::BufferView _parametersBuffer; + const gpu::PipelinePointer& getLinearDepthPipeline(); const gpu::PipelinePointer& getCurvaturePipeline(); + + gpu::PipelinePointer _linearDepthPipeline; gpu::PipelinePointer _curvaturePipeline; gpu::RangeTimer _gpuTimer; diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index ca221ae84a..d0281a2949 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -13,16 +13,58 @@ <$declareDeferredFrameTransform()$> -uniform sampler2D depthMap; +uniform sampler2D linearDepthMap; +float getZEye(ivec2 pixel) { + return -texelFetch(linearDepthMap, pixel, 0).x; +} +float getZEyeLinear(vec2 texcoord) { + return -texture(linearDepthMap, texcoord).x; +} + +vec2 signNotZero(vec2 v) { + return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); +} + +vec3 oct_to_float32x3(in vec2 e) { + vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); + if (v.z < 0) { + v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); + } + return normalize(v); +} + +vec2 unorm8x3_to_snorm12x2(vec3 u) { + u *= 255.0; + u.y *= (1.0 / 16.0); + vec2 s = vec2( u.x * 16.0 + floor(u.y), + fract(u.y) * (16.0 * 256.0) + u.z); + return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); +} +vec3 unpackNormal(in vec3 p) { + return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); +} + +uniform sampler2D normalMap; +vec3 getWorldNormal(vec2 texcoord) { + vec3 rawNormal = texture(normalMap, texcoord).xyz; + return unpackNormal(rawNormal); +} + +vec3 getWorldNormalDiff(vec2 texcoord, vec2 delta) { + vec3 normal0 = getWorldNormal(texcoord - delta); + vec3 normal1 = getWorldNormal(texcoord + delta); + return normal1 - normal0; +} + +float getEyeDepthDiff(vec2 texcoord, vec2 delta) { + vec3 normal0 = getWorldNormal(texcoord - delta); + vec3 normal1 = getWorldNormal(texcoord + delta); + return getZEyeLinear(texcoord + delta) - getZEyeLinear(texcoord - delta); +} + + out vec4 outFragColor; -/* -void main(void) { - float Zdb = texelFetch(depthMap, ivec2(gl_FragCoord.xy), 0).x; - float Zeye = -evalZeyeFromZdb(Zdb); - outFragColor = vec4(Zeye, 0.0, 0.0, 1.0); -} -*/ void main(void) { // Pixel being shaded @@ -32,42 +74,20 @@ void main(void) { ivec2 framePixelPos = getPixelPosNclipPosAndSide(gl_FragCoord.xy, pixelPos, nclipPos, stereoSide); // Fetch the z under the pixel (stereo or not) - float Zdb = texelFetch(depthMap, pixelPos, 0).x; - float Zeye = -evalZeyeFromZdb(Zdb); + float Zeye = getZEye(framePixelPos); - - // The position and normal of the pixel fragment in Eye space + // The position of the pixel fragment in Eye space then in world space vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, nclipPos); - - vec3 worldPos = (frameTransform._invView * vec4(eyePos, 1.0)).xyz; + vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; vec3 moduloPos = fract(worldPos); outFragColor = vec4(moduloPos, 1.0); - /* - return; - - // Choose the screen-space sample radius - // proportional to the projected area of the sphere - float ssDiskRadius = -getProjScale() * getRadius() / Cp.z; - - - vec2 texUV = gl_FragCoord.xy * getInvWidthHeight(); - float Zdb = texelFetch(depthMap, ivec2(gl_FragCoord.xy), 0).x; - float Zeye = -evalZeyeFromZdb(Zdb); - - ivec3 stereoInfo = getStereoSideInfo(gl_FragCoord.x, 0); - - // World Pos - vec4 samplePos = evalEyePositionFromZeye(stereoInfo.x, ) - + // Calculate the width scale. - ./ Choose the screen-space sample radius - // proportional to the projected area of the sphere - // float ssDiskRadius = -getProjScale() * getRadius() / Cp.z; - + // float distanceToProjectionWindow = 1.0f / tan(0.5f * radians(fov)); - float scale = getProjScaleEye() / Zeye; + float scale = -getProjScaleEye() / Zeye; vec2 viewportScale = scale * getInvWidthHeight(); @@ -75,11 +95,17 @@ void main(void) { vec2 du = vec2( 1.0f, 0.0f ) * viewportScale.x; vec2 dv = vec2( 0.0f, 1.0f ) * viewportScale.y; - vec4 dFdu = texture(depthMap, texUV + du.xy) - texture(depthMap, texUV - du.xy); - vec4 dFdv = texture(depthMap, texUV + dv.xy) - texture(depthMap, texUV - dv.xy); - dFdu *= step(abs(dFdu.w), 0.1f); dFdv *= step(abs(dFdv.w), 0.1f); - - // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) + + outFragColor = vec4(du.x, dv.y, scale, 1.0); + + vec4 dFdu = vec4(getWorldNormalDiff(nclipPos, du), getEyeDepthDiff(nclipPos, du)); + vec4 dFdv = vec4(getWorldNormalDiff(nclipPos, dv), getEyeDepthDiff(nclipPos, dv)); + dFdu *= step(abs(dFdu.w), 0.1f); dFdv *= step(abs(dFdv.w), 0.1f); + + outFragColor = vec4(dFdu.xyz, 1.0); + + /* + // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) float dist = 1.0f; samplePos.w = 1.0f; vec2 centerOffset = ((input.texUV - 0.5f) * 2.0f); vec4 px = mul( samplePos + vec4( dist, 0.0f, 0.0f, 0.0f ), matViewProj ); diff --git a/libraries/render-utils/src/surfaceGeometry_makeLinearDepth.slf b/libraries/render-utils/src/surfaceGeometry_makeLinearDepth.slf new file mode 100644 index 0000000000..d512f613bc --- /dev/null +++ b/libraries/render-utils/src/surfaceGeometry_makeLinearDepth.slf @@ -0,0 +1,25 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/3/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include DeferredTransform.slh@> +<$declareDeferredFrameTransform()$> + + +uniform sampler2D depthMap; + +out vec4 outFragColor; + +void main(void) { + float Zdb = texelFetch(depthMap, ivec2(gl_FragCoord.xy), 0).x; + float Zeye = -evalZeyeFromZdb(Zdb); + outFragColor = vec4(Zeye, 0.0, 0.0, 1.0); +} + From f51cb7ce0c8b847e27e7d017fb1dd5e392fe5abf Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 10:34:32 -0700 Subject: [PATCH 0381/1237] trying to add duration support for vive --- plugins/oculus/src/OculusControllerManager.h | 2 - plugins/openvr/src/ViveControllerManager.cpp | 46 +++++++++++++++++--- plugins/openvr/src/ViveControllerManager.h | 17 +++++++- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index 20e5726dff..e62c5f45ea 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -72,8 +72,6 @@ private: private: void stopHapticPulse(bool leftHand); - - private: void handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose); int _trackedControllers { 0 }; friend class OculusControllerManager; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 6b19646512..7d66429ed6 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -126,6 +126,11 @@ bool ViveControllerManager::activate() { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; + _leftHapticTimer.setSingleShot(true); + _rightHapticTimer.setSingleShot(true); + connect(&_leftHapticTimer, SIGNAL(timeout()), this, SLOT(hapticPulseHelper(true))); + connect(&_rightHapticTimer, SIGNAL(timeout()), this, SLOT(hapticPulseHelper(false))); + return true; } @@ -442,7 +447,20 @@ void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = avatarPose.transform(controllerToAvatar); } -// Vive Controllers do not support duration +void ViveControllerManager::hapticPulseHelper(bool leftHand) { + if (_inputDevice) { + _inputDevice->hapticPulseHelper(leftHand); + } +} + +void ViveControllerManager::InputDevice::hapticPulseHelper(bool leftHand) { + if (leftHand) { + triggerHapticPulse(prevLeftHapticStrength, prevLeftHapticDuration, leftHand); + } else { + triggerHapticPulse(prevRightHapticStrength, prevRightHapticDuration, leftHand); + } +} + bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, bool leftHand) { auto handRole = leftHand ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand; auto deviceIndex = _system->GetTrackedDeviceIndexForControllerRole(handRole); @@ -450,11 +468,29 @@ bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, floa if (_system->IsTrackedDeviceConnected(deviceIndex) && _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && _trackedDevicePose[deviceIndex].bPoseIsValid) { - // the documentation says the third argument to TriggerHapticPulse is duration - // but it seems to instead be strength, and is between 0 and 3999 + // Vive Controllers only support duration up to 4 ms, which is short enough that any variation feels more like strength + const float MAX_HAPTIC_TIME = 3999.0f; // in microseconds + float hapticTime = strength*MAX_HAPTIC_TIME; + if (hapticTime < duration*1000.0f) { + _system->TriggerHapticPulse(deviceIndex, 0, hapticTime); + } + + // Must wait 5 ms before triggering another pulse on this controller // https://github.com/ValveSoftware/openvr/wiki/IVRSystem::TriggerHapticPulse - const float MAX_HAPTIC_STRENGTH = 3999.0f; - _system->TriggerHapticPulse(deviceIndex, 0, strength*MAX_HAPTIC_STRENGTH); + const float HAPTIC_RESET_TIME = 5.0f; + float remainingHapticTime = duration - (hapticTime / 1000.0f + HAPTIC_RESET_TIME); // in milliseconds + if (remainingHapticTime > 0.0f) { + if (leftHand) { + prevLeftHapticStrength = strength; + prevLeftHapticDuration = remainingHapticTime; + _parent._leftHapticTimer.start(remainingHapticTime); + } + else { + prevRightHapticStrength = strength; + prevRightHapticDuration = remainingHapticTime; + _parent._rightHapticTimer.start(remainingHapticTime); + } + } return true; } return false; diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index c108d3087f..67fe43c15a 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -13,6 +13,7 @@ #define hifi__ViveControllerManager #include +#include #include #include @@ -45,10 +46,13 @@ public: void setRenderControllers(bool renderControllers) { _renderControllers = renderControllers; } +private slots: + void hapticPulseHelper(bool leftHand); + private: class InputDevice : public controller::InputDevice { public: - InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) {} + InputDevice(ViveControllerManager& parent, vr::IVRSystem*& system) : controller::InputDevice("Vive"), _parent(parent), _system(system) {} private: // Device functions controller::Input::NamedVector getAvailableInputs() const override; @@ -56,6 +60,7 @@ private: void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; + void hapticPulseHelper(bool leftHand); bool triggerHapticPulse(float strength, float duration, bool leftHand) override; void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); @@ -94,6 +99,11 @@ private: int _trackedControllers { 0 }; vr::IVRSystem*& _system; + ViveControllerManager& _parent; + float prevLeftHapticStrength; + float prevLeftHapticDuration; + float prevRightHapticStrength; + float prevRightHapticDuration; friend class ViveControllerManager; }; @@ -109,7 +119,10 @@ private: bool _renderControllers { false }; vr::IVRSystem* _system { nullptr }; - std::shared_ptr _inputDevice { std::make_shared(_system) }; + std::shared_ptr _inputDevice { std::make_shared(*this, _system) }; + + QTimer _leftHapticTimer; + QTimer _rightHapticTimer; static const QString NAME; }; From a4e4093a18c058d14c4b893653514e9d636195c7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 6 Jun 2016 10:39:47 -0700 Subject: [PATCH 0382/1237] split permissions into two sets, one for 'standard' ones and one for specific users --- .../resources/describe-settings.json | 73 ++++++++++- .../resources/web/settings/js/settings.js | 6 +- domain-server/src/DomainGatekeeper.cpp | 2 +- .../src/DomainServerSettingsManager.cpp | 115 +++++++++++++----- .../src/DomainServerSettingsManager.h | 8 +- 5 files changed, 165 insertions(+), 39 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index a4d6424997..26a368fd04 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -56,6 +56,7 @@ "label": "Paths", "help": "Clients can enter a path to reach an exact viewpoint in your domain.
Add rows to the table below to map a path to a viewpoint.
The index path ( / ) is where clients will enter if they do not enter an explicit path.", "type": "table", + "can_add_new_rows": true, "key": { "name": "path", "label": "Path", @@ -96,10 +97,11 @@ "advanced": false }, { - "name": "permissions", + "name": "standard_permissions", "type": "table", - "label": "Domain-Wide User Permissions", - "help": "Indicate which users or groups can have which domain-wide permissions.", + "label": "Domain-Wide Permissions", + "help": "Standard Permissions:", + "can_add_new_rows": false, "columns": [ { @@ -152,6 +154,61 @@ "non-deletable-row-key": "permissions_id", "non-deletable-row-values": ["localhost", "anonymous", "logged-in"] + }, + { + "name": "permissions", + "type": "table", + "help": "Permissions for Specific Users:", + "can_add_new_rows": true, + + "columns": [ + { + "name": "permissions_id", + "label": "User/Group" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": true + }, + { + "name": "id_can_adjust_locks", + "label": "Lock/Unlock", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez", + "label": "Rez", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp", + "label": "Rez Temp", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_write_to_asset_server", + "label": "Write Assets", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_connect_past_max_capacity", + "label": "Ignore Max Capacity", + "type": "checkbox", + "editable": true, + "default": false + } + ] } ] }, @@ -164,6 +221,8 @@ "type": "table", "label": "Persistent Scripts", "help": "Add the URLs for scripts that you would like to ensure are always running in your domain.", + "can_add_new_rows": true, + "columns": [ { "name": "url", @@ -248,6 +307,8 @@ "label": "Zones", "help": "In this table you can define a set of zones in which you can specify various audio properties.", "numbered": false, + "can_add_new_rows": true, + "key": { "name": "name", "label": "Name", @@ -299,6 +360,8 @@ "help": "In this table you can set custom attenuation coefficients between audio zones", "numbered": true, "can_order": true, + "can_add_new_rows": true, + "columns": [ { "name": "source", @@ -326,6 +389,8 @@ "label": "Reverb Settings", "help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.", "numbered": true, + "can_add_new_rows": true, + "columns": [ { "name": "zone", @@ -447,6 +512,8 @@ "label": "Backup Rules", "help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.", "numbered": false, + "can_add_new_rows": true, + "default": [ {"Name":"Half Hourly Rolling","backupInterval":1800,"format":".backup.halfhourly.%N","maxBackupVersions":5}, {"Name":"Daily Rolling","backupInterval":86400,"format":".backup.daily.%N","maxBackupVersions":7}, diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 43a3c89534..970e7551c4 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -1015,7 +1015,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { } // populate inputs in the table for new values - if (!isLocked && !setting.read_only) { + if (!isLocked && !setting.read_only && setting.can_add_new_rows) { html += makeTableInputs(setting) } html += "
" @@ -1052,8 +1052,8 @@ function makeTableInputs(setting) { if (setting.can_order) { html += "" } - html += "
" + html += "" html += "" return html diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index c58ccc4036..adc95f3c38 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -189,7 +189,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); if (isLocalUser) { - userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLocalhost); + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); qDebug() << "user-permissions: is local user, so:" << userPerms; } diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 9faf00ab56..1197bf023b 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -206,18 +206,18 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList(); bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool(); - _agentPermissions[NodePermissions::standardNameLocalhost].reset( + _standardAgentPermissions[NodePermissions::standardNameLocalhost].reset( new NodePermissions(NodePermissions::standardNameLocalhost)); - _agentPermissions[NodePermissions::standardNameLocalhost]->setAll(true); - _agentPermissions[NodePermissions::standardNameAnonymous].reset( + _standardAgentPermissions[NodePermissions::standardNameLocalhost]->setAll(true); + _standardAgentPermissions[NodePermissions::standardNameAnonymous].reset( new NodePermissions(NodePermissions::standardNameAnonymous)); - _agentPermissions[NodePermissions::standardNameLoggedIn].reset( + _standardAgentPermissions[NodePermissions::standardNameLoggedIn].reset( new NodePermissions(NodePermissions::standardNameLoggedIn)); if (isRestrictedAccess) { // only users in allow-users list can connect - _agentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false; - _agentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false; + _standardAgentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false; + _standardAgentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false; } // else anonymous and logged-in retain default of canConnectToDomain = true foreach (QString allowedUser, allowedUsers) { @@ -237,14 +237,20 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _agentPermissions[allowedEditor]->canAdjustLocks = true; } - foreach (QString userName, _agentPermissions.keys()) { - if (onlyEditorsAreRezzers) { - _agentPermissions[userName]->canRezPermanentEntities = _agentPermissions[userName]->canAdjustLocks; - } else { - _agentPermissions[userName]->canRezPermanentEntities = true; + QList> permissionsSets; + permissionsSets << _standardAgentPermissions << _agentPermissions; + foreach (auto permissionsSet, permissionsSets) { + foreach (QString userName, permissionsSet.keys()) { + if (onlyEditorsAreRezzers) { + permissionsSet[userName]->canRezPermanentEntities = permissionsSet[userName]->canAdjustLocks; + } else { + permissionsSet[userName]->canRezPermanentEntities = true; + } } } + packPermissions(argumentList); + _standardAgentPermissions.clear(); _agentPermissions.clear(); } } @@ -255,22 +261,36 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion); } -void DomainServerSettingsManager::packPermissions(const QStringList& argumentList) { - // transfer details from _agentPermissions to _configMap +void DomainServerSettingsManager::packPermissionsForMap(const QStringList& argumentList, + QString mapName, + QHash agentPermissions, + QString keyPath) { QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security"); - QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + + // save settings for anonymous / logged-in / localhost + QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath); if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { QVariantMap securityMap = security->toMap(); QVariantList userList; - securityMap["permissions"] = userList; + securityMap[mapName] = userList; _configMap.getUserConfig()["security"] = securityMap; - permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath); } QVariantList* permissionsList = reinterpret_cast(permissions); - foreach (QString userName, _agentPermissions.keys()) { - *permissionsList += _agentPermissions[userName]->toVariant(); + (*permissionsList).clear(); + foreach (QString userName, agentPermissions.keys()) { + *permissionsList += agentPermissions[userName]->toVariant(); } +} + +void DomainServerSettingsManager::packPermissions(const QStringList& argumentList) { + // transfer details from _agentPermissions to _configMap + packPermissionsForMap(argumentList, "standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH); + + // save settings for specific users + packPermissionsForMap(argumentList, "permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH); + persistToFile(); _configMap.loadMasterAndUserConfig(argumentList); } @@ -281,25 +301,43 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL bool foundLocalhost = false; bool foundAnonymous = false; bool foundLoggedIn = false; + bool needPack = false; + QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH); + if (!standardPermissions || !standardPermissions->canConvert(QMetaType::QVariantList)) { + qDebug() << "failed to extract standard permissions from settings."; + return; + } QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { qDebug() << "failed to extract permissions from settings."; return; } - QList permissionsList = permissions->toList(); - // QVariantList* permissionsList = reinterpret_cast(permissions); - - foreach (QVariant permsHash, permissionsList) { + QList standardPermissionsList = standardPermissions->toList(); + foreach (QVariant permsHash, standardPermissionsList) { NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; QString id = perms->getID(); foundLocalhost |= (id == NodePermissions::standardNameLocalhost); foundAnonymous |= (id == NodePermissions::standardNameAnonymous); foundLoggedIn |= (id == NodePermissions::standardNameLoggedIn); + if (_standardAgentPermissions.contains(id)) { + qDebug() << "duplicate name in standard permissions table: " << id; + _standardAgentPermissions[id] |= perms; + needPack = true; + } else { + _standardAgentPermissions[id] = perms; + } + } + + QList permissionsList = permissions->toList(); + foreach (QVariant permsHash, permissionsList) { + NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; + QString id = perms->getID(); if (_agentPermissions.contains(id)) { qDebug() << "duplicate name in permissions table: " << id; _agentPermissions[id] |= perms; + needPack = true; } else { _agentPermissions[id] = perms; } @@ -309,31 +347,46 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL if (!foundLocalhost) { NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLocalhost) }; perms->setAll(true); - _agentPermissions[perms->getID()] = perms; + _standardAgentPermissions[perms->getID()] = perms; } if (!foundAnonymous) { NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameAnonymous) }; - _agentPermissions[perms->getID()] = perms; + needPack = true; } if (!foundLoggedIn) { NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLoggedIn) }; - _agentPermissions[perms->getID()] = perms; + needPack = true; } - if (!foundLocalhost || !foundAnonymous || !foundLoggedIn) { + + if (needPack) { packPermissions(argumentList); } + #ifdef WANT_DEBUG qDebug() << "--------------- permissions ---------------------"; - QHashIterator i(_agentPermissions); - while (i.hasNext()) { - i.next(); - NodePermissionsPointer perms = i.value(); - qDebug() << i.key() << perms; + QList> permissionsSets; + permissionsSets << _standardAgentPermissions << _agentPermissions; + foreach (auto permissionSet, permissionsSets) { + QHashIterator i(permissionSet); + while (i.hasNext()) { + i.next(); + NodePermissionsPointer perms = i.value(); + qDebug() << i.key() << perms; + } } #endif } +NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const QString& name) const { + if (_standardAgentPermissions.contains(name)) { + return *(_standardAgentPermissions[name].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { if (_agentPermissions.contains(name)) { return *(_agentPermissions[name].get()); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index d82f5e588c..51ee657961 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -25,6 +25,7 @@ const QString SETTINGS_PATHS_KEY = "paths"; const QString SETTINGS_PATH = "/settings"; const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; +const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; class DomainServerSettingsManager : public QObject { @@ -40,7 +41,9 @@ public: QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } + bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); } bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); } + NodePermissions getStandardPermissionsForName(const QString& name) const; NodePermissions getPermissionsForName(const QString& name) const; QStringList getAllNames() { return _agentPermissions.keys(); } @@ -62,9 +65,12 @@ private: friend class DomainServer; + void packPermissionsForMap(const QStringList& argumentList, QString mapName, + QHash agentPermissions, QString keyPath); void packPermissions(const QStringList& argumentList); void unpackPermissions(const QStringList& argumentList); - QHash _agentPermissions; + QHash _standardAgentPermissions; // anonymous, logged-in, localhost + QHash _agentPermissions; // specific account-names }; #endif // hifi_DomainServerSettingsManager_h From 34f46b860b26ee9c284647a188d48e0b721c4afc Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Mon, 6 Jun 2016 11:54:18 -0700 Subject: [PATCH 0383/1237] AVX2 optimized audio resampler --- libraries/audio/src/AudioSRC.cpp | 195 ++++++++++++++++++++++--------- libraries/audio/src/AudioSRC.h | 28 ++++- 2 files changed, 163 insertions(+), 60 deletions(-) diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index c187d381a4..6c594db234 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -544,22 +544,9 @@ static const float prototypeFilter[PROTOTYPE_TAPS * PROTOTYPE_PHASES] = { 8.99882100e-06f, 7.61267073e-06f, 6.57702907e-06f, 5.59829210e-06f, 4.27698546e-06f, 1.03248674e-05f, }; -// -// polyphase filter -// -static const int SRC_PHASEBITS = 8; -static const int SRC_PHASES = (1 << SRC_PHASEBITS); -static const int SRC_FRACBITS = 32 - SRC_PHASEBITS; -static const uint32_t SRC_FRACMASK = (1 << SRC_FRACBITS) - 1; - -static const float QFRAC_TO_FLOAT = 1.0f / (1 << SRC_FRACBITS); -static const float Q32_TO_FLOAT = 1.0f / (1ULL << 32); - -// blocking size in frames, chosen so block processing fits in L1 cache -static const int SRC_BLOCK = 1024; - -#define lo32(a) ((uint32_t)(a)) -#define hi32(a) ((int32_t)((a) >> 32)) +// high/low part of int64_t +#define LO32(a) ((uint32_t)(a)) +#define HI32(a) ((int32_t)((a) >> 32)) // // Portable aligned malloc/free @@ -610,8 +597,8 @@ static void cubicInterpolation(const float* input, float* output, int inputSize, // Lagrange interpolation using Farrow structure for (int j = 0; j < outputSize; j++) { - int32_t i = hi32(offset); - uint32_t f = lo32(offset); + int32_t i = HI32(offset); + uint32_t f = LO32(offset); // values outside the window are zero float x0 = (i - 1 < 0) ? 0.0f : input[i - 1]; @@ -649,7 +636,7 @@ int AudioSRC::createRationalFilter(int upFactor, int downFactor, float gain) { numTaps = (numCoefs + upFactor - 1) / upFactor; gain *= (float)oldCoefs / numCoefs; } - numTaps = (numTaps + 3) & ~3; // SIMD4 + numTaps = (numTaps + 7) & ~7; // SIMD8 // interpolate the coefficients of the prototype filter float* tempFilter = new float[numTaps * numPhases]; @@ -658,7 +645,7 @@ int AudioSRC::createRationalFilter(int upFactor, int downFactor, float gain) { cubicInterpolation(prototypeFilter, tempFilter, prototypeCoefs, numCoefs, gain); // create the polyphase filter - _polyphaseFilter = (float*)aligned_malloc(numTaps * numPhases * sizeof(float), 16); // SIMD4 + _polyphaseFilter = (float*)aligned_malloc(numTaps * numPhases * sizeof(float), 32); // SIMD8 // rearrange into polyphase form, ordered by use for (int i = 0; i < numPhases; i++) { @@ -699,7 +686,7 @@ int AudioSRC::createIrrationalFilter(int upFactor, int downFactor, float gain) { numTaps = (numCoefs + upFactor - 1) / upFactor; gain *= (float)oldCoefs / numCoefs; } - numTaps = (numTaps + 3) & ~3; // SIMD4 + numTaps = (numTaps + 7) & ~7; // SIMD8 // interpolate the coefficients of the prototype filter float* tempFilter = new float[numTaps * numPhases]; @@ -708,7 +695,7 @@ int AudioSRC::createIrrationalFilter(int upFactor, int downFactor, float gain) { cubicInterpolation(prototypeFilter, tempFilter, prototypeCoefs, numCoefs, gain); // create the polyphase filter, with extra phase at the end to simplify coef interpolation - _polyphaseFilter = (float*)aligned_malloc(numTaps * (numPhases + 1) * sizeof(float), 16); // SIMD4 + _polyphaseFilter = (float*)aligned_malloc(numTaps * (numPhases + 1) * sizeof(float), 32); // SIMD8 // rearrange into polyphase form, ordered by fractional delay for (int phase = 0; phase < numPhases; phase++) { @@ -741,14 +728,14 @@ int AudioSRC::createIrrationalFilter(int upFactor, int downFactor, float gain) { #include -int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFrames) { +int AudioSRC::multirateFilter1_SSE(const float* input0, float* output0, int inputFrames) { int outputFrames = 0; - assert((_numTaps & 0x3) == 0); // SIMD4 + assert(_numTaps % 4 == 0); // SIMD4 if (_step == 0) { // rational - int32_t i = hi32(_offset); + int32_t i = HI32(_offset); while (i < inputFrames) { @@ -761,7 +748,7 @@ int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFra //float coef = c0[j]; __m128 coef0 = _mm_loadu_ps(&c0[j]); - //acc0 += input0[i + j] * coef; + //acc += input[i + j] * coef; acc0 = _mm_add_ps(_mm_mul_ps(_mm_loadu_ps(&input0[i + j]), coef0), acc0); } @@ -781,10 +768,10 @@ int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFra } else { // irrational - while (hi32(_offset) < inputFrames) { + while (HI32(_offset) < inputFrames) { - int32_t i = hi32(_offset); - uint32_t f = lo32(_offset); + int32_t i = HI32(_offset); + uint32_t f = LO32(_offset); uint32_t phase = f >> SRC_FRACBITS; __m128 frac = _mm_set1_ps((f & SRC_FRACMASK) * QFRAC_TO_FLOAT); @@ -802,7 +789,7 @@ int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFra coef1 = _mm_sub_ps(coef1, coef0); coef0 = _mm_add_ps(_mm_mul_ps(coef1, frac), coef0); - //acc0 += input0[i + j] * coef; + //acc += input[i + j] * coef; acc0 = _mm_add_ps(_mm_mul_ps(_mm_loadu_ps(&input0[i + j]), coef0), acc0); } @@ -821,14 +808,14 @@ int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFra return outputFrames; } -int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* output0, float* output1, int inputFrames) { +int AudioSRC::multirateFilter2_SSE(const float* input0, const float* input1, float* output0, float* output1, int inputFrames) { int outputFrames = 0; - assert((_numTaps & 0x3) == 0); // SIMD4 + assert(_numTaps % 4 == 0); // SIMD4 if (_step == 0) { // rational - int32_t i = hi32(_offset); + int32_t i = HI32(_offset); while (i < inputFrames) { @@ -842,7 +829,7 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* //float coef = c0[j]; __m128 coef0 = _mm_loadu_ps(&c0[j]); - //acc0 += input0[i + j] * coef; + //acc += input[i + j] * coef; acc0 = _mm_add_ps(_mm_mul_ps(_mm_loadu_ps(&input0[i + j]), coef0), acc0); acc1 = _mm_add_ps(_mm_mul_ps(_mm_loadu_ps(&input1[i + j]), coef0), acc1); } @@ -866,10 +853,10 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* } else { // irrational - while (hi32(_offset) < inputFrames) { + while (HI32(_offset) < inputFrames) { - int32_t i = hi32(_offset); - uint32_t f = lo32(_offset); + int32_t i = HI32(_offset); + uint32_t f = LO32(_offset); uint32_t phase = f >> SRC_FRACBITS; __m128 frac = _mm_set1_ps((f & SRC_FRACMASK) * QFRAC_TO_FLOAT); @@ -888,7 +875,7 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* coef1 = _mm_sub_ps(coef1, coef0); coef0 = _mm_add_ps(_mm_mul_ps(coef1, frac), coef0); - //acc0 += input0[i + j] * coef; + //acc += input[i + j] * coef; acc0 = _mm_add_ps(_mm_mul_ps(_mm_loadu_ps(&input0[i + j]), coef0), acc0); acc1 = _mm_add_ps(_mm_mul_ps(_mm_loadu_ps(&input1[i + j]), coef0), acc1); } @@ -911,6 +898,107 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* return outputFrames; } +// +// Detect AVX/AVX2 support +// + +#if defined(_MSC_VER) + +#include + +static bool cpuSupportsAVX() { + int info[4]; + int mask = (1 << 27) | (1 << 28); // OSXSAVE and AVX + + __cpuidex(info, 0x1, 0); + + bool result = false; + if ((info[2] & mask) == mask) { + + if ((_xgetbv(_XCR_XFEATURE_ENABLED_MASK) & 0x6) == 0x6) { + result = true; + } + } + return result; +} + +static bool cpuSupportsAVX2() { + int info[4]; + int mask = (1 << 5); // AVX2 + + bool result = false; + if (cpuSupportsAVX()) { + + __cpuidex(info, 0x7, 0); + + if ((info[1] & mask) == mask) { + result = true; + } + } + return result; +} + +#elif defined(__GNUC__) + +#include + +static bool cpuSupportsAVX() { + unsigned int eax, ebx, ecx, edx; + unsigned int mask = (1 << 27) | (1 << 28); // OSXSAVE and AVX + + bool result = false; + if (__get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & mask) == mask)) { + + __asm__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(0)); + if ((eax & 0x6) == 0x6) { + result = true; + } + } + return result; +} + +static bool cpuSupportsAVX2() { + unsigned int eax, ebx, ecx, edx; + unsigned mask = (1 << 5); // AVX2 + + bool result = false; + if (cpuSupportsAVX()) { + + if (__get_cpuid(0x7, &eax, &ebx, &ecx, &edx) && ((ebx & mask) == mask)) { + result = true; + } + } + return result; +} + +#else + +static bool cpuSupportsAVX() { + return false; +} + +static bool cpuSupportsAVX2() { + return false; +} + +#endif + +// +// Runtime CPU dispatch +// + +int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFrames) { + + static auto f = cpuSupportsAVX2() ? &AudioSRC::multirateFilter1_AVX2 : &AudioSRC::multirateFilter1_SSE; + return (this->*f)(input0, output0, inputFrames); // dispatch +} + +int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* output0, float* output1, int inputFrames) { + + static auto f = cpuSupportsAVX2() ? &AudioSRC::multirateFilter2_AVX2 : &AudioSRC::multirateFilter2_SSE; + return (this->*f)(input0, input1, output0, output1, inputFrames); // dispatch +} + // convert int16_t to float, deinterleave stereo void AudioSRC::convertInputFromInt16(const int16_t* input, float** outputs, int numFrames) { __m128 scale = _mm_set1_ps(1/32768.0f); @@ -1069,7 +1157,7 @@ int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFra if (_step == 0) { // rational - int32_t i = hi32(_offset); + int32_t i = HI32(_offset); while (i < inputFrames) { @@ -1096,10 +1184,10 @@ int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFra } else { // irrational - while (hi32(_offset) < inputFrames) { + while (HI32(_offset) < inputFrames) { - int32_t i = hi32(_offset); - uint32_t f = lo32(_offset); + int32_t i = HI32(_offset); + uint32_t f = LO32(_offset); uint32_t phase = f >> SRC_FRACBITS; float frac = (f & SRC_FRACMASK) * QFRAC_TO_FLOAT; @@ -1132,7 +1220,7 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* if (_step == 0) { // rational - int32_t i = hi32(_offset); + int32_t i = HI32(_offset); while (i < inputFrames) { @@ -1162,10 +1250,10 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* } else { // irrational - while (hi32(_offset) < inputFrames) { + while (HI32(_offset) < inputFrames) { - int32_t i = hi32(_offset); - uint32_t f = lo32(_offset); + int32_t i = HI32(_offset); + uint32_t f = LO32(_offset); uint32_t phase = f >> SRC_FRACBITS; float frac = (f & SRC_FRACMASK) * QFRAC_TO_FLOAT; @@ -1320,7 +1408,7 @@ AudioSRC::AudioSRC(int inputSampleRate, int outputSampleRate, int numChannels) { assert(inputSampleRate > 0); assert(outputSampleRate > 0); assert(numChannels > 0); - assert(numChannels <= MAX_CHANNELS); + assert(numChannels <= SRC_MAX_CHANNELS); _inputSampleRate = inputSampleRate; _outputSampleRate = outputSampleRate; @@ -1349,7 +1437,7 @@ AudioSRC::AudioSRC(int inputSampleRate, int outputSampleRate, int numChannels) { _numTaps = createIrrationalFilter(_upFactor, _downFactor, 1.0f); } - //printf("up=%d down=%.3f taps=%d\n", _upFactor, _downFactor + (lo32(_step)< +static const int SRC_MAX_CHANNELS = 2; + +// polyphase filter +static const int SRC_PHASEBITS = 8; +static const int SRC_PHASES = (1 << SRC_PHASEBITS); +static const int SRC_FRACBITS = 32 - SRC_PHASEBITS; +static const uint32_t SRC_FRACMASK = (1 << SRC_FRACBITS) - 1; + +static const float QFRAC_TO_FLOAT = 1.0f / (1 << SRC_FRACBITS); +static const float Q32_TO_FLOAT = 1.0f / (1ULL << 32); + +// blocking size in frames, chosen so block processing fits in L1 cache +static const int SRC_BLOCK = 256; + class AudioSRC { public: - static const int MAX_CHANNELS = 2; - AudioSRC(int inputSampleRate, int outputSampleRate, int numChannels); ~AudioSRC(); @@ -33,9 +45,9 @@ private: float* _polyphaseFilter; int* _stepTable; - float* _history[MAX_CHANNELS]; - float* _inputs[MAX_CHANNELS]; - float* _outputs[MAX_CHANNELS]; + float* _history[SRC_MAX_CHANNELS]; + float* _inputs[SRC_MAX_CHANNELS]; + float* _outputs[SRC_MAX_CHANNELS]; int _inputSampleRate; int _outputSampleRate; @@ -57,6 +69,12 @@ private: int multirateFilter1(const float* input0, float* output0, int inputFrames); int multirateFilter2(const float* input0, const float* input1, float* output0, float* output1, int inputFrames); + int multirateFilter1_SSE(const float* input0, float* output0, int inputFrames); + int multirateFilter2_SSE(const float* input0, const float* input1, float* output0, float* output1, int inputFrames); + + int multirateFilter1_AVX2(const float* input0, float* output0, int inputFrames); + int multirateFilter2_AVX2(const float* input0, const float* input1, float* output0, float* output1, int inputFrames); + void convertInputFromInt16(const int16_t* input, float** outputs, int numFrames); void convertOutputToInt16(float** inputs, int16_t* output, int numFrames); From 4f35d3df02745fae23da01527aa19faea02f0cbc Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Mon, 6 Jun 2016 11:57:17 -0700 Subject: [PATCH 0384/1237] AVX2 code must be in /avx for CMAKE to compile it properly --- libraries/audio/src/avx/AudioSRC_avx.cpp | 201 +++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 libraries/audio/src/avx/AudioSRC_avx.cpp diff --git a/libraries/audio/src/avx/AudioSRC_avx.cpp b/libraries/audio/src/avx/AudioSRC_avx.cpp new file mode 100644 index 0000000000..dcc56b94ad --- /dev/null +++ b/libraries/audio/src/avx/AudioSRC_avx.cpp @@ -0,0 +1,201 @@ +// +// AudioSRC_avx.cpp +// libraries/audio/src +// +// Created by Ken Cooke on 6/5/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__x86_64__) + +#include +#include + +#include "../AudioSRC.h" + +#ifndef __AVX__ +#error Must be compiled with /arch:AVX or -mavx. +#endif + +// high/low part of int64_t +#define LO32(a) ((uint32_t)(a)) +#define HI32(a) ((int32_t)((a) >> 32)) + +int AudioSRC::multirateFilter1_AVX2(const float* input0, float* output0, int inputFrames) { + int outputFrames = 0; + + assert(_numTaps % 8 == 0); // SIMD8 + + if (_step == 0) { // rational + + int32_t i = HI32(_offset); + + while (i < inputFrames) { + + const float* c0 = &_polyphaseFilter[_numTaps * _phase]; + + __m256 acc0 = _mm256_setzero_ps(); + + for (int j = 0; j < _numTaps; j += 8) { + + //float coef = c0[j]; + __m256 coef0 = _mm256_loadu_ps(&c0[j]); + + //acc += input[i + j] * coef; + acc0 = _mm256_fmadd_ps(_mm256_loadu_ps(&input0[i + j]), coef0, acc0); + } + + // horizontal sum + acc0 = _mm256_hadd_ps(acc0, acc0); + __m128 t0 = _mm_add_ps(_mm256_castps256_ps128(acc0), _mm256_extractf128_ps(acc0, 1)); + t0 = _mm_add_ps(t0, _mm_movehdup_ps(t0)); + + _mm_store_ss(&output0[outputFrames], t0); + outputFrames += 1; + + i += _stepTable[_phase]; + if (++_phase == _upFactor) { + _phase = 0; + } + } + _offset = (int64_t)(i - inputFrames) << 32; + + } else { // irrational + + while (HI32(_offset) < inputFrames) { + + int32_t i = HI32(_offset); + uint32_t f = LO32(_offset); + + uint32_t phase = f >> SRC_FRACBITS; + float ftmp = (f & SRC_FRACMASK) * QFRAC_TO_FLOAT; + + const float* c0 = &_polyphaseFilter[_numTaps * (phase + 0)]; + const float* c1 = &_polyphaseFilter[_numTaps * (phase + 1)]; + + __m256 acc0 = _mm256_setzero_ps(); + __m256 frac = _mm256_broadcast_ss(&ftmp); + + for (int j = 0; j < _numTaps; j += 8) { + + //float coef = c0[j] + frac * (c1[j] - c0[j]); + __m256 coef0 = _mm256_loadu_ps(&c0[j]); + __m256 coef1 = _mm256_loadu_ps(&c1[j]); + coef1 = _mm256_sub_ps(coef1, coef0); + coef0 = _mm256_fmadd_ps(coef1, frac, coef0); + + //acc += input[i + j] * coef; + acc0 = _mm256_fmadd_ps(_mm256_loadu_ps(&input0[i + j]), coef0, acc0); + } + + // horizontal sum + acc0 = _mm256_hadd_ps(acc0, acc0); + __m128 t0 = _mm_add_ps(_mm256_castps256_ps128(acc0), _mm256_extractf128_ps(acc0, 1)); + t0 = _mm_add_ps(t0, _mm_movehdup_ps(t0)); + + _mm_store_ss(&output0[outputFrames], t0); + outputFrames += 1; + + _offset += _step; + } + _offset -= (int64_t)inputFrames << 32; + } + _mm256_zeroupper(); + + return outputFrames; +} + +int AudioSRC::multirateFilter2_AVX2(const float* input0, const float* input1, float* output0, float* output1, int inputFrames) { + int outputFrames = 0; + + assert(_numTaps % 8 == 0); // SIMD8 + + if (_step == 0) { // rational + + int32_t i = HI32(_offset); + + while (i < inputFrames) { + + const float* c0 = &_polyphaseFilter[_numTaps * _phase]; + + __m256 acc0 = _mm256_setzero_ps(); + __m256 acc1 = _mm256_setzero_ps(); + + for (int j = 0; j < _numTaps; j += 8) { + + //float coef = c0[j]; + __m256 coef0 = _mm256_loadu_ps(&c0[j]); + + //acc += input[i + j] * coef; + acc0 = _mm256_fmadd_ps(_mm256_loadu_ps(&input0[i + j]), coef0, acc0); + acc1 = _mm256_fmadd_ps(_mm256_loadu_ps(&input1[i + j]), coef0, acc1); + } + + // horizontal sum + acc0 = _mm256_hadd_ps(acc0, acc1); + __m128 t0 = _mm_add_ps(_mm256_castps256_ps128(acc0), _mm256_extractf128_ps(acc0, 1)); + t0 = _mm_add_ps(t0, _mm_movehdup_ps(t0)); + + _mm_store_ss(&output0[outputFrames], t0); + _mm_store_ss(&output1[outputFrames], _mm_shuffle_ps(t0, t0, _MM_SHUFFLE(0,0,0,2))); + outputFrames += 1; + + i += _stepTable[_phase]; + if (++_phase == _upFactor) { + _phase = 0; + } + } + _offset = (int64_t)(i - inputFrames) << 32; + + } else { // irrational + + while (HI32(_offset) < inputFrames) { + + int32_t i = HI32(_offset); + uint32_t f = LO32(_offset); + + uint32_t phase = f >> SRC_FRACBITS; + float ftmp = (f & SRC_FRACMASK) * QFRAC_TO_FLOAT; + + const float* c0 = &_polyphaseFilter[_numTaps * (phase + 0)]; + const float* c1 = &_polyphaseFilter[_numTaps * (phase + 1)]; + + __m256 acc0 = _mm256_setzero_ps(); + __m256 acc1 = _mm256_setzero_ps(); + __m256 frac = _mm256_broadcast_ss(&ftmp); + + for (int j = 0; j < _numTaps; j += 8) { + + //float coef = c0[j] + frac * (c1[j] - c0[j]); + __m256 coef0 = _mm256_loadu_ps(&c0[j]); + __m256 coef1 = _mm256_loadu_ps(&c1[j]); + coef1 = _mm256_sub_ps(coef1, coef0); + coef0 = _mm256_fmadd_ps(coef1, frac, coef0); + + //acc += input[i + j] * coef; + acc0 = _mm256_fmadd_ps(_mm256_loadu_ps(&input0[i + j]), coef0, acc0); + acc1 = _mm256_fmadd_ps(_mm256_loadu_ps(&input1[i + j]), coef0, acc1); + } + + // horizontal sum + acc0 = _mm256_hadd_ps(acc0, acc1); + __m128 t0 = _mm_add_ps(_mm256_castps256_ps128(acc0), _mm256_extractf128_ps(acc0, 1)); + t0 = _mm_add_ps(t0, _mm_movehdup_ps(t0)); + + _mm_store_ss(&output0[outputFrames], t0); + _mm_store_ss(&output1[outputFrames], _mm_shuffle_ps(t0, t0, _MM_SHUFFLE(0,0,0,2))); + outputFrames += 1; + + _offset += _step; + } + _offset -= (int64_t)inputFrames << 32; + } + _mm256_zeroupper(); + + return outputFrames; +} + +#endif From d202a2bf1170dfd378401a04e1ee84203e13e3e7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 6 Jun 2016 11:59:39 -0700 Subject: [PATCH 0385/1237] better handling of missing settings keys --- .../src/DomainServerSettingsManager.cpp | 21 ++++++++++++------- libraries/shared/src/HifiConfigVariantMap.cpp | 8 ++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 1197bf023b..d52d926816 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -266,15 +266,16 @@ void DomainServerSettingsManager::packPermissionsForMap(const QStringList& argum QHash agentPermissions, QString keyPath) { QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security"); + if (!security || !security->canConvert(QMetaType::QVariantMap)) { + security = valueForKeyPath(_configMap.getUserConfig(), "security", true); + (*security) = QVariantMap(); + } // save settings for anonymous / logged-in / localhost QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath); if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { - QVariantMap securityMap = security->toMap(); - QVariantList userList; - securityMap[mapName] = userList; - _configMap.getUserConfig()["security"] = securityMap; - permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath); + permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath, true); + (*permissions) = QVariantList(); } QVariantList* permissionsList = reinterpret_cast(permissions); @@ -306,12 +307,14 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH); if (!standardPermissions || !standardPermissions->canConvert(QMetaType::QVariantList)) { qDebug() << "failed to extract standard permissions from settings."; - return; + standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH, true); + (*standardPermissions) = QVariantList(); } QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { qDebug() << "failed to extract permissions from settings."; - return; + permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH, true); + (*permissions) = QVariantList(); } QList standardPermissionsList = standardPermissions->toList(); @@ -348,13 +351,16 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLocalhost) }; perms->setAll(true); _standardAgentPermissions[perms->getID()] = perms; + needPack = true; } if (!foundAnonymous) { NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameAnonymous) }; + _standardAgentPermissions[perms->getID()] = perms; needPack = true; } if (!foundLoggedIn) { NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLoggedIn) }; + _standardAgentPermissions[perms->getID()] = perms; needPack = true; } @@ -362,7 +368,6 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL packPermissions(argumentList); } - #ifdef WANT_DEBUG qDebug() << "--------------- permissions ---------------------"; QList> permissionsSets; diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index b3920e70bc..5ae5ff740d 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -213,10 +213,12 @@ QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool if (shouldCreateIfMissing || variantMap.contains(firstKey)) { if (dotIndex == -1) { return &variantMap[firstKey]; - } else if (variantMap[firstKey].canConvert(QMetaType::QVariantMap)) { - return valueForKeyPath(*static_cast(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1), - shouldCreateIfMissing); } + if (!variantMap[firstKey].canConvert(QMetaType::QVariantMap)) { + variantMap[firstKey] = QVariantMap(); + } + return valueForKeyPath(*static_cast(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1), + shouldCreateIfMissing); } return NULL; From 8faaa36913b219e9e235135a9d456ba9e3d592ab Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Mon, 6 Jun 2016 12:05:48 -0700 Subject: [PATCH 0386/1237] moved table of precomputed data into AudioSRCData.h --- libraries/audio/src/AudioSRC.cpp | 530 +--------------------------- libraries/audio/src/AudioSRCData.h | 548 +++++++++++++++++++++++++++++ 2 files changed, 549 insertions(+), 529 deletions(-) create mode 100644 libraries/audio/src/AudioSRCData.h diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index 6c594db234..fbe109803e 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -14,535 +14,7 @@ #include #include "AudioSRC.h" - -// -// prototype lowpass filter -// -// Minimum-phase equiripple FIR -// taps = 96, oversampling = 32 -// -// passband = 0.918 -// stopband = 1.010 -// passband ripple = +-0.01dB -// stopband attn = -125dB (-70dB at 1.000) -// -static const int PROTOTYPE_TAPS = 96; // filter taps per phase -static const int PROTOTYPE_PHASES = 32; // oversampling factor - -static const float prototypeFilter[PROTOTYPE_TAPS * PROTOTYPE_PHASES] = { - 0.00000000e+00f, 1.55021703e-05f, 1.46054865e-05f, 2.07057160e-05f, 2.91335519e-05f, 4.00091078e-05f, - 5.33544450e-05f, 7.03618468e-05f, 9.10821639e-05f, 1.16484613e-04f, 1.47165999e-04f, 1.84168304e-04f, - 2.28429617e-04f, 2.80913884e-04f, 3.42940399e-04f, 4.15773039e-04f, 5.01023255e-04f, 6.00234953e-04f, - 7.15133271e-04f, 8.47838855e-04f, 1.00032516e-03f, 1.17508881e-03f, 1.37452550e-03f, 1.60147614e-03f, - 1.85886458e-03f, 2.14985024e-03f, 2.47783071e-03f, 2.84666764e-03f, 3.26016878e-03f, 3.72252797e-03f, - 4.23825900e-03f, 4.81207874e-03f, 5.44904143e-03f, 6.15447208e-03f, 6.93399929e-03f, 7.79337059e-03f, - 8.73903392e-03f, 9.77729117e-03f, 1.09149561e-02f, 1.21591316e-02f, 1.35171164e-02f, 1.49965439e-02f, - 1.66053136e-02f, 1.83515384e-02f, 2.02435362e-02f, 2.22899141e-02f, 2.44995340e-02f, 2.68813362e-02f, - 2.94443254e-02f, 3.21979928e-02f, 3.51514690e-02f, 3.83143719e-02f, 4.16960560e-02f, 4.53060504e-02f, - 4.91538115e-02f, 5.32486197e-02f, 5.75998650e-02f, 6.22164253e-02f, 6.71072811e-02f, 7.22809789e-02f, - 7.77457552e-02f, 8.35095233e-02f, 8.95796944e-02f, 9.59631768e-02f, 1.02666457e-01f, 1.09695215e-01f, - 1.17054591e-01f, 1.24748885e-01f, 1.32781656e-01f, 1.41155521e-01f, 1.49872243e-01f, 1.58932534e-01f, - 1.68335961e-01f, 1.78081143e-01f, 1.88165339e-01f, 1.98584621e-01f, 2.09333789e-01f, 2.20406193e-01f, - 2.31793899e-01f, 2.43487398e-01f, 2.55475740e-01f, 2.67746404e-01f, 2.80285305e-01f, 2.93076743e-01f, - 3.06103423e-01f, 3.19346351e-01f, 3.32784916e-01f, 3.46396772e-01f, 3.60158039e-01f, 3.74043042e-01f, - 3.88024564e-01f, 4.02073759e-01f, 4.16160177e-01f, 4.30251886e-01f, 4.44315429e-01f, 4.58315954e-01f, - 4.72217175e-01f, 4.85981675e-01f, 4.99570709e-01f, 5.12944586e-01f, 5.26062401e-01f, 5.38882630e-01f, - 5.51362766e-01f, 5.63459860e-01f, 5.75130384e-01f, 5.86330458e-01f, 5.97016050e-01f, 6.07143161e-01f, - 6.16667840e-01f, 6.25546499e-01f, 6.33735979e-01f, 6.41193959e-01f, 6.47878856e-01f, 6.53750084e-01f, - 6.58768549e-01f, 6.62896349e-01f, 6.66097381e-01f, 6.68337353e-01f, 6.69583869e-01f, 6.69807061e-01f, - 6.68979117e-01f, 6.67075139e-01f, 6.64072812e-01f, 6.59952827e-01f, 6.54699116e-01f, 6.48298688e-01f, - 6.40742160e-01f, 6.32023668e-01f, 6.22141039e-01f, 6.11095903e-01f, 5.98893921e-01f, 5.85544600e-01f, - 5.71061707e-01f, 5.55463040e-01f, 5.38770639e-01f, 5.21010762e-01f, 5.02213839e-01f, 4.82414572e-01f, - 4.61651859e-01f, 4.39968628e-01f, 4.17412000e-01f, 3.94032951e-01f, 3.69886464e-01f, 3.45031084e-01f, - 3.19529091e-01f, 2.93446187e-01f, 2.66851164e-01f, 2.39815999e-01f, 2.12415399e-01f, 1.84726660e-01f, - 1.56829293e-01f, 1.28804933e-01f, 1.00736965e-01f, 7.27100355e-02f, 4.48100810e-02f, 1.71237415e-02f, - -1.02620228e-02f, -3.72599591e-02f, -6.37832871e-02f, -8.97457733e-02f, -1.15062201e-01f, -1.39648782e-01f, - -1.63423488e-01f, -1.86306368e-01f, -2.08220103e-01f, -2.29090072e-01f, -2.48845046e-01f, -2.67417270e-01f, - -2.84742946e-01f, -3.00762597e-01f, -3.15421127e-01f, -3.28668542e-01f, -3.40459849e-01f, -3.50755400e-01f, - -3.59521402e-01f, -3.66729768e-01f, -3.72358475e-01f, -3.76391839e-01f, -3.78820421e-01f, -3.79641287e-01f, - -3.78858203e-01f, -3.76481336e-01f, -3.72527677e-01f, -3.67020780e-01f, -3.59990760e-01f, -3.51474372e-01f, - -3.41514630e-01f, -3.30160971e-01f, -3.17468898e-01f, -3.03499788e-01f, -2.88320749e-01f, -2.72004315e-01f, - -2.54628056e-01f, -2.36274454e-01f, -2.17030464e-01f, -1.96986952e-01f, -1.76238733e-01f, -1.54883647e-01f, - -1.33022496e-01f, -1.10758449e-01f, -8.81964466e-02f, -6.54430504e-02f, -4.26055475e-02f, -1.97916415e-02f, - 2.89108184e-03f, 2.53355868e-02f, 4.74362201e-02f, 6.90887518e-02f, 9.01914308e-02f, 1.10644978e-01f, - 1.30353494e-01f, 1.49224772e-01f, 1.67170735e-01f, 1.84107975e-01f, 1.99958067e-01f, 2.14648181e-01f, - 2.28111323e-01f, 2.40286622e-01f, 2.51119890e-01f, 2.60563701e-01f, 2.68577740e-01f, 2.75129027e-01f, - 2.80192144e-01f, 2.83749177e-01f, 2.85790223e-01f, 2.86312986e-01f, 2.85323221e-01f, 2.82834421e-01f, - 2.78867915e-01f, 2.73452721e-01f, 2.66625431e-01f, 2.58429983e-01f, 2.48917457e-01f, 2.38145826e-01f, - 2.26179680e-01f, 2.13089734e-01f, 1.98952740e-01f, 1.83850758e-01f, 1.67870897e-01f, 1.51104879e-01f, - 1.33648388e-01f, 1.15600665e-01f, 9.70639763e-02f, 7.81429119e-02f, 5.89439889e-02f, 3.95749746e-02f, - 2.01442353e-02f, 7.60241152e-04f, -1.84690990e-02f, -3.74370397e-02f, -5.60385970e-02f, -7.41711039e-02f, - -9.17348686e-02f, -1.08633632e-01f, -1.24775254e-01f, -1.40071993e-01f, -1.54441372e-01f, -1.67806284e-01f, - -1.80095654e-01f, -1.91244732e-01f, -2.01195605e-01f, -2.09897310e-01f, -2.17306320e-01f, -2.23386736e-01f, - -2.28110407e-01f, -2.31457193e-01f, -2.33415044e-01f, -2.33980051e-01f, -2.33156463e-01f, -2.30956673e-01f, - -2.27401097e-01f, -2.22518148e-01f, -2.16343899e-01f, -2.08921985e-01f, -2.00303365e-01f, -1.90545790e-01f, - -1.79713804e-01f, -1.67877977e-01f, -1.55114789e-01f, -1.41505907e-01f, -1.27137921e-01f, -1.12101628e-01f, - -9.64915640e-02f, -8.04054232e-02f, -6.39434707e-02f, -4.72078814e-02f, -3.03021635e-02f, -1.33305082e-02f, - 3.60284977e-03f, 2.03942507e-02f, 3.69413014e-02f, 5.31433810e-02f, 6.89024656e-02f, 8.41234679e-02f, - 9.87150268e-02f, 1.12589969e-01f, 1.25665865e-01f, 1.37865538e-01f, 1.49117506e-01f, 1.59356490e-01f, - 1.68523664e-01f, 1.76567229e-01f, 1.83442499e-01f, 1.89112308e-01f, 1.93547212e-01f, 1.96725586e-01f, - 1.98633878e-01f, 1.99266486e-01f, 1.98625999e-01f, 1.96723008e-01f, 1.93576075e-01f, 1.89211557e-01f, - 1.83663562e-01f, 1.76973516e-01f, 1.69190033e-01f, 1.60368490e-01f, 1.50570805e-01f, 1.39864815e-01f, - 1.28324021e-01f, 1.16026978e-01f, 1.03056879e-01f, 8.95008829e-02f, 7.54496798e-02f, 6.09968238e-02f, - 4.62380664e-02f, 3.12708901e-02f, 1.61936956e-02f, 1.10531988e-03f, -1.38957653e-02f, -2.87119784e-02f, - -4.32472742e-02f, -5.74078385e-02f, -7.11026311e-02f, -8.42439713e-02f, -9.67481917e-02f, -1.08536049e-01f, - -1.19533350e-01f, -1.29671345e-01f, -1.38887238e-01f, -1.47124498e-01f, -1.54333373e-01f, -1.60470968e-01f, - -1.65501755e-01f, -1.69397631e-01f, -1.72138140e-01f, -1.73710602e-01f, -1.74110159e-01f, -1.73339798e-01f, - -1.71410274e-01f, -1.68340111e-01f, -1.64155335e-01f, -1.58889414e-01f, -1.52582850e-01f, -1.45283122e-01f, - -1.37044042e-01f, -1.27925722e-01f, -1.17993860e-01f, -1.07319421e-01f, -9.59781808e-02f, -8.40500777e-02f, - -7.16188049e-02f, -5.87710561e-02f, -4.55961475e-02f, -3.21851919e-02f, -1.86306406e-02f, -5.02554942e-03f, - 8.53698384e-03f, 2.19645467e-02f, 3.51659468e-02f, 4.80518693e-02f, 6.05355056e-02f, 7.25330700e-02f, - 8.39645094e-02f, 9.47537898e-02f, 1.04829753e-01f, 1.14126254e-01f, 1.22582788e-01f, 1.30144907e-01f, - 1.36764459e-01f, 1.42400029e-01f, 1.47017076e-01f, 1.50588312e-01f, 1.53093700e-01f, 1.54520736e-01f, - 1.54864367e-01f, 1.54127119e-01f, 1.52318991e-01f, 1.49457408e-01f, 1.45567062e-01f, 1.40679709e-01f, - 1.34833933e-01f, 1.28074855e-01f, 1.20453893e-01f, 1.12028129e-01f, 1.02860307e-01f, 9.30178765e-02f, - 8.25730032e-02f, 7.16016450e-02f, 6.01833134e-02f, 4.84002546e-02f, 3.63370724e-02f, 2.40800037e-02f, - 1.17163168e-02f, -6.66217400e-04f, -1.29801121e-02f, -2.51385315e-02f, -3.70562030e-02f, -4.86497748e-02f, - -5.98384928e-02f, -7.05447859e-02f, -8.06947592e-02f, -9.02187441e-02f, -9.90517313e-02f, -1.07133911e-01f, - -1.14410951e-01f, -1.20834483e-01f, -1.26362422e-01f, -1.30959116e-01f, -1.34595787e-01f, -1.37250547e-01f, - -1.38908600e-01f, -1.39562374e-01f, -1.39211442e-01f, -1.37862602e-01f, -1.35529795e-01f, -1.32233909e-01f, - -1.28002721e-01f, -1.22870611e-01f, -1.16878278e-01f, -1.10072477e-01f, -1.02505698e-01f, -9.42356124e-02f, - -8.53248753e-02f, -7.58404912e-02f, -6.58532924e-02f, -5.54376360e-02f, -4.46705953e-02f, -3.36315414e-02f, - -2.24015972e-02f, -1.10628991e-02f, 3.01894735e-04f, 1.16101918e-02f, 2.27801642e-02f, 3.37311642e-02f, - 4.43845430e-02f, 5.46640016e-02f, 6.44962637e-02f, 7.38115400e-02f, 8.25440784e-02f, 9.06325572e-02f, - 9.80206066e-02f, 1.04657146e-01f, 1.10496723e-01f, 1.15499920e-01f, 1.19633523e-01f, 1.22870824e-01f, - 1.25191729e-01f, 1.26582959e-01f, 1.27038061e-01f, 1.26557494e-01f, 1.25148528e-01f, 1.22825305e-01f, - 1.19608512e-01f, 1.15525479e-01f, 1.10609643e-01f, 1.04900592e-01f, 9.84435537e-02f, 9.12890948e-02f, - 8.34927732e-02f, 7.51146973e-02f, 6.62190194e-02f, 5.68735547e-02f, 4.71491262e-02f, 3.71191855e-02f, - 2.68591932e-02f, 1.64459573e-02f, 5.95731808e-03f, -4.52874940e-03f, -1.49344723e-02f, -2.51829130e-02f, - -3.51986373e-02f, -4.49081427e-02f, -5.42404654e-02f, -6.31276969e-02f, -7.15054163e-02f, -7.93132713e-02f, - -8.64953327e-02f, -9.30005042e-02f, -9.87829011e-02f, -1.03802223e-01f, -1.08023943e-01f, -1.11419636e-01f, - -1.13967111e-01f, -1.15650603e-01f, -1.16460855e-01f, -1.16395152e-01f, -1.15457368e-01f, -1.13657871e-01f, - -1.11013433e-01f, -1.07547117e-01f, -1.03288073e-01f, -9.82712708e-02f, -9.25372646e-02f, -8.61318657e-02f, - -7.91057486e-02f, -7.15141053e-02f, -6.34161588e-02f, -5.48747791e-02f, -4.59559696e-02f, -3.67282941e-02f, - -2.72624874e-02f, -1.76307914e-02f, -7.90648674e-03f, 1.83670340e-03f, 1.15251424e-02f, 2.10858716e-02f, - 3.04471304e-02f, 3.95388944e-02f, 4.82933904e-02f, 5.66456655e-02f, 6.45340054e-02f, 7.19003487e-02f, - 7.86908695e-02f, 8.48562395e-02f, 9.03519908e-02f, 9.51389501e-02f, 9.91834077e-02f, 1.02457361e-01f, - 1.04938834e-01f, 1.06611872e-01f, 1.07466724e-01f, 1.07499917e-01f, 1.06714213e-01f, 1.05118588e-01f, - 1.02728167e-01f, 9.95640680e-02f, 9.56532488e-02f, 9.10282406e-02f, 8.57269309e-02f, 7.97922261e-02f, - 7.32717395e-02f, 6.62174249e-02f, 5.86850536e-02f, 5.07339959e-02f, 4.24265058e-02f, 3.38274345e-02f, - 2.50036502e-02f, 1.60234844e-02f, 6.95628026e-03f, -2.12820655e-03f, -1.11602438e-02f, -2.00708281e-02f, - -2.87920337e-02f, -3.72576320e-02f, -4.54035426e-02f, -5.31684173e-02f, -6.04938939e-02f, -6.73253212e-02f, - -7.36119310e-02f, -7.93072981e-02f, -8.43697556e-02f, -8.87625537e-02f, -9.24542939e-02f, -9.54189981e-02f, - -9.76364402e-02f, -9.90921435e-02f, -9.97776003e-02f, -9.96902366e-02f, -9.88334463e-02f, -9.72165780e-02f, - -9.48547668e-02f, -9.17688999e-02f, -8.79853312e-02f, -8.35357688e-02f, -7.84569594e-02f, -7.27903677e-02f, - -6.65818940e-02f, -5.98814932e-02f, -5.27427333e-02f, -4.52224733e-02f, -3.73802459e-02f, -2.92780037e-02f, - -2.09794209e-02f, -1.25495498e-02f, -4.05425988e-03f, 4.44034349e-03f, 1.28682571e-02f, 2.11643361e-02f, - 2.92645357e-02f, 3.71066200e-02f, 4.46305203e-02f, 5.17788267e-02f, 5.84972389e-02f, 6.47349496e-02f, - 7.04450836e-02f, 7.55849928e-02f, 8.01165748e-02f, 8.40066506e-02f, 8.72270848e-02f, 8.97550618e-02f, - 9.15732179e-02f, 9.26698315e-02f, 9.30387881e-02f, 9.26796720e-02f, 9.15978025e-02f, 8.98040443e-02f, - 8.73148489e-02f, 8.41520461e-02f, 8.03426093e-02f, 7.59185468e-02f, 7.09165136e-02f, 6.53776255e-02f, - 5.93470480e-02f, 5.28736293e-02f, 4.60095655e-02f, 3.88099545e-02f, 3.13323302e-02f, 2.36362162e-02f, - 1.57827398e-02f, 7.83395091e-03f, -1.47413782e-04f, -8.09864153e-03f, -1.59574406e-02f, -2.36623595e-02f, - -3.11534717e-02f, -3.83725840e-02f, -4.52638947e-02f, -5.17743411e-02f, -5.78539729e-02f, -6.34564348e-02f, - -6.85392092e-02f, -7.30640654e-02f, -7.69971954e-02f, -8.03096220e-02f, -8.29772975e-02f, -8.49813524e-02f, - -8.63081836e-02f, -8.69495746e-02f, -8.69027157e-02f, -8.61702687e-02f, -8.47602668e-02f, -8.26860569e-02f, - -7.99661981e-02f, -7.66242997e-02f, -7.26887788e-02f, -6.81926752e-02f, -6.31733712e-02f, -5.76722279e-02f, - -5.17343061e-02f, -4.54080069e-02f, -3.87446321e-02f, -3.17980032e-02f, -2.46239897e-02f, -1.72801497e-02f, - -9.82518156e-03f, -2.31845300e-03f, 5.18037510e-03f, 1.26119044e-02f, 1.99174857e-02f, 2.70395921e-02f, - 3.39223499e-02f, 4.05119404e-02f, 4.67570465e-02f, 5.26092142e-02f, 5.80232695e-02f, 6.29576539e-02f, - 6.73747113e-02f, 7.12410320e-02f, 7.45276905e-02f, 7.72104218e-02f, 7.92698394e-02f, 8.06915952e-02f, - 8.14664004e-02f, 8.15901977e-02f, 8.10640907e-02f, 7.98943315e-02f, 7.80922975e-02f, 7.56743792e-02f, - 7.26617861e-02f, 6.90804346e-02f, 6.49606433e-02f, 6.03370049e-02f, 5.52479503e-02f, 4.97355660e-02f, - 4.38451300e-02f, 3.76248662e-02f, 3.11254263e-02f, 2.43995757e-02f, 1.75017105e-02f, 1.04874823e-02f, - 3.41321948e-03f, -3.66433362e-03f, -1.06886566e-02f, -1.76037566e-02f, -2.43547422e-02f, -3.08881238e-02f, - -3.71523818e-02f, -4.30982377e-02f, -4.86791529e-02f, -5.38515978e-02f, -5.85754991e-02f, -6.28144137e-02f, - -6.65359631e-02f, -6.97119559e-02f, -7.23186409e-02f, -7.43369897e-02f, -7.57526047e-02f, -7.65560812e-02f, - -7.67428560e-02f, -7.63134051e-02f, -7.52730583e-02f, -7.36321241e-02f, -7.14055927e-02f, -6.86132027e-02f, - -6.52791213e-02f, -6.14318004e-02f, -5.71037475e-02f, -5.23312158e-02f, -4.71539306e-02f, -4.16147519e-02f, - -3.57593331e-02f, -2.96357023e-02f, -2.32939478e-02f, -1.67857228e-02f, -1.01639251e-02f, -3.48213128e-03f, - 3.20566951e-03f, 9.84566549e-03f, 1.63845318e-02f, 2.27699627e-02f, 2.89509937e-02f, 3.48784838e-02f, - 4.05054571e-02f, 4.57875191e-02f, 5.06831561e-02f, 5.51541055e-02f, 5.91656321e-02f, 6.26867948e-02f, - 6.56907214e-02f, 6.81547545e-02f, 7.00607045e-02f, 7.13948753e-02f, 7.21482790e-02f, 7.23165894e-02f, - 7.19002973e-02f, 7.09044846e-02f, 6.93390331e-02f, 6.72183039e-02f, 6.45611568e-02f, 6.13906537e-02f, - 5.77340810e-02f, 5.36223917e-02f, 4.90902973e-02f, 4.41756853e-02f, 3.89195025e-02f, 3.33653266e-02f, - 2.75589553e-02f, 2.15482187e-02f, 1.53823433e-02f, 9.11173206e-03f, 2.78750380e-03f, -3.53899736e-03f, - -9.81648845e-03f, -1.59942887e-02f, -2.20226002e-02f, -2.78530676e-02f, -3.34389835e-02f, -3.87358558e-02f, - -4.37015752e-02f, -4.82968641e-02f, -5.24856104e-02f, -5.62350079e-02f, -5.95160314e-02f, -6.23034090e-02f, - -6.45760369e-02f, -6.63170246e-02f, -6.75138263e-02f, -6.81583864e-02f, -6.82471093e-02f, -6.77809819e-02f, - -6.67654439e-02f, -6.52104027e-02f, -6.31301405e-02f, -6.05431381e-02f, -5.74719510e-02f, -5.39430121e-02f, - -4.99864152e-02f, -4.56356108e-02f, -4.09271785e-02f, -3.59005358e-02f, -3.05975021e-02f, -2.50620982e-02f, - -1.93400931e-02f, -1.34786109e-02f, -7.52582921e-03f, -1.53047296e-03f, 4.45846396e-03f, 1.03922252e-02f, - 1.62226043e-02f, 2.19024111e-02f, 2.73857927e-02f, 3.26286453e-02f, 3.75889120e-02f, 4.22270162e-02f, - 4.65060678e-02f, 5.03922602e-02f, 5.38550360e-02f, 5.68673912e-02f, 5.94061299e-02f, 6.14518959e-02f, - 6.29894927e-02f, 6.40078422e-02f, 6.45002081e-02f, 6.44641312e-02f, 6.39014463e-02f, 6.28183549e-02f, - 6.12252434e-02f, 5.91366226e-02f, 5.65710713e-02f, 5.35509478e-02f, 5.01023211e-02f, 4.62546289e-02f, - 4.20405644e-02f, 3.74956324e-02f, 3.26580309e-02f, 2.75681921e-02f, 2.22685138e-02f, 1.68029869e-02f, - 1.12168479e-02f, 5.55616360e-03f, -1.32475496e-04f, -5.80242145e-03f, -1.14072870e-02f, -1.69013632e-02f, - -2.22399629e-02f, -2.73798231e-02f, -3.22793559e-02f, -3.68992177e-02f, -4.12022700e-02f, -4.51542301e-02f, - -4.87237130e-02f, -5.18825743e-02f, -5.46061242e-02f, -5.68733215e-02f, -5.86668721e-02f, -5.99735198e-02f, - -6.07838952e-02f, -6.10928895e-02f, -6.08993923e-02f, -6.02064781e-02f, -5.90213291e-02f, -5.73550887e-02f, - -5.52228853e-02f, -5.26435817e-02f, -4.96396897e-02f, -4.62371294e-02f, -4.24650256e-02f, -3.83554628e-02f, - -3.39432096e-02f, -2.92654225e-02f, -2.43613233e-02f, -1.92718970e-02f, -1.40395616e-02f, -8.70771728e-03f, - -3.32056777e-03f, 2.07744785e-03f, 7.44190391e-03f, 1.27287222e-02f, 1.78946228e-02f, 2.28975002e-02f, - 2.76965843e-02f, 3.22530140e-02f, 3.65299534e-02f, 4.04930363e-02f, 4.41105069e-02f, 4.73536159e-02f, - 5.01967201e-02f, 5.26175750e-02f, 5.45974724e-02f, 5.61213729e-02f, 5.71780843e-02f, 5.77601946e-02f, - 5.78643759e-02f, 5.74910914e-02f, 5.66448597e-02f, 5.53340158e-02f, 5.35707338e-02f, 5.13708843e-02f, - 4.87538683e-02f, 4.57425137e-02f, 4.23627999e-02f, 3.86437075e-02f, 3.46169024e-02f, 3.03165387e-02f, - 2.57788894e-02f, 2.10421222e-02f, 1.61459251e-02f, 1.11311994e-02f, 6.03970466e-03f, 9.13695817e-04f, - -4.20433431e-03f, -9.27218149e-03f, -1.42480682e-02f, -1.90911878e-02f, -2.37618648e-02f, -2.82220093e-02f, - -3.24353766e-02f, -3.63678336e-02f, -3.99876924e-02f, -4.32659237e-02f, -4.61764207e-02f, -4.86961602e-02f, - -5.08054551e-02f, -5.24880386e-02f, -5.37312181e-02f, -5.45260166e-02f, -5.48671104e-02f, -5.47530531e-02f, - -5.41860463e-02f, -5.31721475e-02f, -5.17210363e-02f, -4.98459868e-02f, -4.75637647e-02f, -4.48944406e-02f, - -4.18612746e-02f, -3.84904206e-02f, -3.48107925e-02f, -3.08537797e-02f, -2.66529685e-02f, -2.22438695e-02f, - -1.76636682e-02f, -1.29507560e-02f, -8.14466071e-03f, -3.28544776e-03f, 1.58643018e-03f, 6.43050440e-03f, - 1.12067405e-02f, 1.58756642e-02f, 2.03989020e-02f, 2.47393345e-02f, 2.88614617e-02f, 3.27317634e-02f, - 3.63187992e-02f, 3.95936470e-02f, 4.25300387e-02f, 4.51045672e-02f, 4.72968940e-02f, 4.90899703e-02f, - 5.04700047e-02f, 5.14267809e-02f, 5.19535643e-02f, 5.20472034e-02f, 5.17082287e-02f, 5.09406434e-02f, - 4.97521048e-02f, 4.81537188e-02f, 4.61599131e-02f, 4.37884262e-02f, 4.10600706e-02f, 3.79985488e-02f, - 3.46302622e-02f, 3.09841217e-02f, 2.70912412e-02f, 2.29847199e-02f, 1.86992847e-02f, 1.42711599e-02f, - 9.73752669e-03f, 5.13643650e-03f, 5.06379454e-04f, -4.11408166e-03f, -8.68649476e-03f, -1.31729621e-02f, - -1.75363807e-02f, -2.17408089e-02f, -2.57516979e-02f, -2.95362143e-02f, -3.30635093e-02f, -3.63049622e-02f, - -3.92344048e-02f, -4.18283298e-02f, -4.40661418e-02f, -4.59301913e-02f, -4.74060505e-02f, -4.84825511e-02f, - -4.91518827e-02f, -4.94096235e-02f, -4.92548579e-02f, -4.86900251e-02f, -4.77210458e-02f, -4.63571741e-02f, - -4.46108878e-02f, -4.24979107e-02f, -4.00368564e-02f, -3.72492987e-02f, -3.41594108e-02f, -3.07938448e-02f, - -2.71814552e-02f, -2.33531198e-02f, -1.93413598e-02f, -1.51802063e-02f, -1.09048013e-02f, -6.55114338e-03f, - -2.15581014e-03f, 2.24443555e-03f, 6.61280814e-03f, 1.09129453e-02f, 1.51091980e-02f, 1.91667630e-02f, - 2.30522168e-02f, 2.67335907e-02f, 3.01807365e-02f, 3.33655579e-02f, 3.62622051e-02f, 3.88473226e-02f, - 4.11002204e-02f, 4.30030300e-02f, 4.45408790e-02f, 4.57019705e-02f, 4.64777109e-02f, 4.68627135e-02f, - 4.68549093e-02f, 4.64554958e-02f, 4.56689373e-02f, 4.45029599e-02f, 4.29683919e-02f, 4.10791386e-02f, - 3.88520159e-02f, 3.63066475e-02f, 3.34652385e-02f, 3.03523892e-02f, 2.69949681e-02f, 2.34217263e-02f, - 1.96632025e-02f, 1.57513974e-02f, 1.17194459e-02f, 7.60145677e-03f, 3.43215481e-03f, -7.53454950e-04f, - -4.92025229e-03f, -9.03345904e-03f, -1.30587503e-02f, -1.69627406e-02f, -2.07130441e-02f, -2.42787472e-02f, - -2.76304969e-02f, -3.07408842e-02f, -3.35845310e-02f, -3.61384026e-02f, -3.83819804e-02f, -4.02973364e-02f, - -4.18693911e-02f, -4.30859849e-02f, -4.39379525e-02f, -4.44192202e-02f, -4.45268207e-02f, -4.42609489e-02f, - -4.36249417e-02f, -4.26251693e-02f, -4.12710965e-02f, -3.95751119e-02f, -3.75524034e-02f, -3.52209020e-02f, - -3.26010732e-02f, -2.97156826e-02f, -2.65897306e-02f, -2.32501339e-02f, -1.97255230e-02f, -1.60459906e-02f, - -1.22428645e-02f, -8.34840613e-03f, -4.39555788e-03f, -4.17641093e-04f, 3.55186529e-03f, 7.47969548e-03f, - 1.13330289e-02f, 1.50796895e-02f, 1.86886063e-02f, 2.21298440e-02f, 2.53750227e-02f, 2.83974776e-02f, - 3.11724713e-02f, 3.36774564e-02f, 3.58921485e-02f, 3.77988281e-02f, 3.93823848e-02f, 4.06304645e-02f, - 4.15335460e-02f, 4.20850895e-02f, 4.22814530e-02f, 4.21220657e-02f, 4.16092724e-02f, 4.07484568e-02f, - 3.95478256e-02f, 3.80185099e-02f, 3.61742882e-02f, 3.40316228e-02f, 3.16093467e-02f, 2.89286854e-02f, - 2.60129143e-02f, 2.28872072e-02f, 1.95785162e-02f, 1.61151429e-02f, 1.25266872e-02f, 8.84367289e-03f, - 5.09737541e-03f, 1.31946573e-03f, -2.45819207e-03f, -6.20382907e-03f, -9.88599514e-03f, -1.34739714e-02f, - -1.69377975e-02f, -2.02487225e-02f, -2.33793144e-02f, -2.63038233e-02f, -2.89981802e-02f, -3.14404213e-02f, - -3.36107546e-02f, -3.54916723e-02f, -3.70682427e-02f, -3.83280672e-02f, -3.92614736e-02f, -3.98615776e-02f, - -4.01243243e-02f, -4.00484517e-02f, -3.96356708e-02f, -3.88903731e-02f, -3.78198781e-02f, -3.64341365e-02f, - -3.47457457e-02f, -3.27698392e-02f, -3.05238882e-02f, -2.80276282e-02f, -2.53028218e-02f, -2.23730957e-02f, - -1.92637467e-02f, -1.60015029e-02f, -1.26142882e-02f, -9.13104283e-03f, -5.58138981e-03f, -1.99542434e-03f, - 1.59649307e-03f, 5.16408174e-03f, 8.67737144e-03f, 1.21068581e-02f, 1.54239205e-02f, 1.86009100e-02f, - 2.16114772e-02f, 2.44306994e-02f, 2.70354163e-02f, 2.94042665e-02f, 3.15179985e-02f, 3.33595356e-02f, - 3.49141593e-02f, 3.61696229e-02f, 3.71161871e-02f, 3.77468512e-02f, 3.80571878e-02f, 3.80455485e-02f, - 3.77129900e-02f, 3.70632810e-02f, 3.61028508e-02f, 3.48407199e-02f, 3.32884428e-02f, 3.14600053e-02f, - 2.93716228e-02f, 2.70417408e-02f, 2.44907277e-02f, 2.17407576e-02f, 1.88156734e-02f, 1.57406803e-02f, - 1.25421761e-02f, 9.24754692e-03f, 5.88488640e-03f, 2.48280587e-03f, -9.29864758e-04f, -4.32426314e-03f, - -7.67179184e-03f, -1.09442952e-02f, -1.41143886e-02f, -1.71555974e-02f, -2.00425787e-02f, -2.27514891e-02f, - -2.52599054e-02f, -2.75472706e-02f, -2.95949315e-02f, -3.13863062e-02f, -3.29069832e-02f, -3.41450096e-02f, - -3.50907101e-02f, -3.57369992e-02f, -3.60793163e-02f, -3.61156751e-02f, -3.58467080e-02f, -3.52755740e-02f, - -3.44080617e-02f, -3.32523628e-02f, -3.18191314e-02f, -3.01213186e-02f, -2.81740846e-02f, -2.59946393e-02f, - -2.36021125e-02f, -2.10173975e-02f, -1.82629132e-02f, -1.53624700e-02f, -1.23410560e-02f, -9.22456599e-03f, - -6.03967755e-03f, -2.81350877e-03f, 4.26514319e-04f, 3.65292660e-03f, 6.83848944e-03f, 9.95638508e-03f, - 1.29804234e-02f, 1.58853076e-02f, 1.86468203e-02f, 2.12420277e-02f, 2.36494909e-02f, 2.58493792e-02f, - 2.78237450e-02f, 2.95565060e-02f, 3.10338053e-02f, 3.22438572e-02f, 3.31772716e-02f, 3.38269627e-02f, - 3.41883176e-02f, 3.42591610e-02f, 3.40397435e-02f, 3.35328606e-02f, 3.27436351e-02f, 3.16796573e-02f, - 3.03507246e-02f, 2.87689689e-02f, 2.69484839e-02f, 2.49054827e-02f, 2.26579086e-02f, 2.02254442e-02f, - 1.76292617e-02f, 1.48918382e-02f, 1.20368159e-02f, 9.08872468e-03f, 6.07283273e-03f, 3.01489838e-03f, - -5.90212194e-05f, -3.12287666e-03f, -6.15069532e-03f, -9.11695091e-03f, -1.19967033e-02f, -1.47657868e-02f, - -1.74011004e-02f, -1.98807214e-02f, -2.21841025e-02f, -2.42922632e-02f, -2.61879368e-02f, -2.78557311e-02f, - -2.92821801e-02f, -3.04559562e-02f, -3.13678907e-02f, -3.20110632e-02f, -3.23808087e-02f, -3.24749193e-02f, - -3.22933847e-02f, -3.18386269e-02f, -3.11153366e-02f, -3.01304804e-02f, -2.88932552e-02f, -2.74148734e-02f, - -2.57086673e-02f, -2.37898314e-02f, -2.16752343e-02f, -1.93835013e-02f, -1.69345799e-02f, -1.43497284e-02f, - -1.16513243e-02f, -8.86259097e-03f, -6.00748525e-03f, -3.11044903e-03f, -1.96143386e-04f, 2.71056658e-03f, - 5.58512222e-03f, 8.40318833e-03f, 1.11410160e-02f, 1.37756382e-02f, 1.62850338e-02f, 1.86482666e-02f, - 2.08457445e-02f, 2.28593437e-02f, 2.46725329e-02f, 2.62705694e-02f, 2.76405329e-02f, 2.87715470e-02f, - 2.96547092e-02f, 3.02833419e-02f, 3.06529059e-02f, 3.07610441e-02f, 3.06076742e-02f, 3.01949567e-02f, - 2.95271502e-02f, 2.86107876e-02f, 2.74543883e-02f, 2.60685701e-02f, 2.44657863e-02f, 2.26603655e-02f, - 2.06682557e-02f, 1.85070033e-02f, 1.61954603e-02f, 1.37537720e-02f, 1.12030588e-02f, 8.56537064e-03f, - 5.86336215e-03f, 3.12021752e-03f, 3.59345288e-04f, -2.39571357e-03f, -5.12158252e-03f, -7.79518527e-03f, - -1.03939536e-02f, -1.28961026e-02f, -1.52805838e-02f, -1.75275761e-02f, -1.96183935e-02f, -2.15357712e-02f, - -2.32639542e-02f, -2.47888545e-02f, -2.60981899e-02f, -2.71814567e-02f, -2.80302370e-02f, -2.86380088e-02f, - -2.90003996e-02f, -2.91151172e-02f, -2.89819544e-02f, -2.86028697e-02f, -2.79818317e-02f, -2.71249297e-02f, - -2.60401957e-02f, -2.47375751e-02f, -2.32288414e-02f, -2.15275091e-02f, -1.96486443e-02f, -1.76087964e-02f, - -1.54258426e-02f, -1.31187994e-02f, -1.07076937e-02f, -8.21335282e-03f, -5.65730582e-03f, -3.06143405e-03f, - -4.47990175e-04f, 2.16074548e-03f, 4.74260737e-03f, 7.27569124e-03f, 9.73864733e-03f, 1.21106824e-02f, - 1.43719841e-02f, 1.65036001e-02f, 1.84878471e-02f, 2.03083286e-02f, 2.19500531e-02f, 2.33996493e-02f, - 2.46453861e-02f, 2.56773512e-02f, 2.64874345e-02f, 2.70694463e-02f, 2.74192279e-02f, 2.75344951e-02f, - 2.74150667e-02f, 2.70627089e-02f, 2.64811913e-02f, 2.56761950e-02f, 2.46553112e-02f, 2.34279326e-02f, - 2.20051823e-02f, 2.03998041e-02f, 1.86260730e-02f, 1.66996483e-02f, 1.46373888e-02f, 1.24573628e-02f, - 1.01784699e-02f, 7.82046099e-03f, 5.40366356e-03f, 2.94886537e-03f, 4.77074685e-04f, -1.99056008e-03f, - -4.43309957e-03f, -6.82975366e-03f, -9.16032780e-03f, -1.14051392e-02f, -1.35453571e-02f, -1.55631186e-02f, - -1.74416221e-02f, -1.91653203e-02f, -2.07200521e-02f, -2.20931290e-02f, -2.32734389e-02f, -2.42515770e-02f, - -2.50198790e-02f, -2.55724740e-02f, -2.59053977e-02f, -2.60165073e-02f, -2.59056121e-02f, -2.55744100e-02f, - -2.50263861e-02f, -2.42670139e-02f, -2.33034172e-02f, -2.21444752e-02f, -2.08007704e-02f, -1.92843016e-02f, - -1.76086143e-02f, -1.57885066e-02f, -1.38399632e-02f, -1.17800468e-02f, -9.62665505e-03f, -7.39846180e-03f, - -5.11473979e-03f, -2.79509520e-03f, -4.59475153e-04f, 1.87219411e-03f, 4.18004886e-03f, 6.44446028e-03f, - 8.64630036e-03f, 1.07670050e-02f, 1.27887263e-02f, 1.46946183e-02f, 1.64687696e-02f, 1.80965074e-02f, - 1.95644657e-02f, 2.08606409e-02f, 2.19745569e-02f, 2.28973400e-02f, 2.36217678e-02f, 2.41423032e-02f, - 2.44552329e-02f, 2.45585559e-02f, 2.44521268e-02f, 2.41375247e-02f, 2.36181843e-02f, 2.28991883e-02f, - 2.19873596e-02f, 2.08911372e-02f, 1.96204854e-02f, 1.81868423e-02f, 1.66029686e-02f, 1.48829260e-02f, - 1.30418196e-02f, 1.10957823e-02f, 9.06176569e-03f, 6.95742371e-03f, 4.80095797e-03f, 2.61094572e-03f, - 4.06163422e-04f, -1.79448120e-03f, -3.97227507e-03f, -6.10867089e-03f, -8.18559133e-03f, -1.01855447e-02f, - -1.20916775e-02f, -1.38880736e-02f, -1.55597947e-02f, -1.70929424e-02f, -1.84749792e-02f, -1.96945768e-02f, - -2.07419008e-02f, -2.16086011e-02f, -2.22879060e-02f, -2.27746496e-02f, -2.30653527e-02f, -2.31582122e-02f, - -2.30530853e-02f, -2.27516002e-02f, -2.22569518e-02f, -2.15740851e-02f, -2.07094459e-02f, -1.96710504e-02f, - -1.84683607e-02f, -1.71122258e-02f, -1.56147530e-02f, -1.39891960e-02f, -1.22499260e-02f, -1.04121226e-02f, - -8.49187069e-03f, -6.50583812e-03f, -4.47121574e-03f, -2.40553061e-03f, -3.26560349e-04f, 1.74792849e-03f, - 3.80020986e-03f, 5.81284812e-03f, 7.76878436e-03f, 9.65152189e-03f, 1.14452321e-02f, 1.31348903e-02f, - 1.47064602e-02f, 1.61469015e-02f, 1.74443880e-02f, 1.85883329e-02f, 1.95694960e-02f, 2.03800747e-02f, - 2.10137416e-02f, 2.14657028e-02f, 2.17327470e-02f, 2.18132189e-02f, 2.17071096e-02f, 2.14159688e-02f, - 2.09429396e-02f, 2.02927056e-02f, 1.94714591e-02f, 1.84867806e-02f, 1.73476996e-02f, 1.60644888e-02f, - 1.46486021e-02f, 1.31126305e-02f, 1.14700918e-02f, 9.73543186e-03f, 7.92379251e-03f, 6.05090462e-03f, - 4.13301608e-03f, 2.18669055e-03f, 2.28581333e-04f, -1.72441072e-03f, -3.65572200e-03f, -5.54887990e-03f, - -7.38782061e-03f, -9.15706782e-03f, -1.08417082e-02f, -1.24276657e-02f, -1.39017311e-02f, -1.52516970e-02f, - -1.64664949e-02f, -1.75361817e-02f, -1.84521823e-02f, -1.92071599e-02f, -1.97953056e-02f, -2.02121243e-02f, - -2.04547147e-02f, -2.05216098e-02f, -2.04128534e-02f, -2.01300439e-02f, -1.96761990e-02f, -1.90558123e-02f, - -1.82748056e-02f, -1.73404276e-02f, -1.62612067e-02f, -1.50469098e-02f, -1.37084115e-02f, -1.22575769e-02f, - -1.07072432e-02f, -9.07102930e-03f, -7.36320826e-03f, -5.59869147e-03f, -3.79270806e-03f, -1.96092013e-03f, - -1.19027325e-04f, 1.71713152e-03f, 3.53191747e-03f, 5.30986343e-03f, 7.03590331e-03f, 8.69547560e-03f, - 1.02746006e-02f, 1.17601122e-02f, 1.31396009e-02f, 1.44016653e-02f, 1.55359973e-02f, 1.65332483e-02f, - 1.73855033e-02f, 1.80859434e-02f, 1.86291305e-02f, 1.90110277e-02f, 1.92289384e-02f, 1.92815880e-02f, - 1.91691688e-02f, 1.88932135e-02f, 1.84567183e-02f, 1.78639790e-02f, 1.71206377e-02f, 1.62336473e-02f, - 1.52110920e-02f, 1.40622274e-02f, 1.27973510e-02f, 1.14277163e-02f, 9.96541843e-03f, 8.42333112e-03f, - 6.81491991e-03f, 5.15420944e-03f, 3.45559138e-03f, 1.73374462e-03f, 3.49154958e-06f, -1.72033182e-03f, - -3.42300908e-03f, -5.09002877e-03f, -6.70728983e-03f, -8.26110592e-03f, -9.73843101e-03f, -1.11269177e-02f, - -1.24149972e-02f, -1.35920411e-02f, -1.46483675e-02f, -1.55754162e-02f, -1.63657097e-02f, -1.70130158e-02f, - -1.75123254e-02f, -1.78599156e-02f, -1.80533642e-02f, -1.80916471e-02f, -1.79749596e-02f, -1.77049199e-02f, - -1.72844059e-02f, -1.67175734e-02f, -1.60098348e-02f, -1.51677846e-02f, -1.41991369e-02f, -1.31126308e-02f, - -1.19180614e-02f, -1.06260158e-02f, -9.24795820e-03f, -7.79599691e-03f, -6.28282689e-03f, -4.72166017e-03f, - -3.12602130e-03f, -1.50971188e-03f, 1.13358008e-04f, 1.72924640e-03f, 3.32419869e-03f, 4.88457483e-03f, - 6.39719332e-03f, 7.84928507e-03f, 9.22860374e-03f, 1.05236737e-02f, 1.17237027e-02f, 1.28187631e-02f, - 1.37999219e-02f, 1.46591627e-02f, 1.53896448e-02f, 1.59855771e-02f, 1.64423748e-02f, 1.67566705e-02f, - 1.69263151e-02f, 1.69504088e-02f, 1.68293192e-02f, 1.65646048e-02f, 1.61591292e-02f, 1.56168830e-02f, - 1.49430466e-02f, 1.41438870e-02f, 1.32267343e-02f, 1.21999194e-02f, 1.10726150e-02f, 9.85491162e-03f, - 8.55755480e-03f, 7.19198626e-03f, 5.77013714e-03f, 4.30443841e-03f, 2.80758857e-03f, 1.29252809e-03f, - -2.27683018e-04f, -1.74000213e-03f, -3.23153173e-03f, -4.68956247e-03f, -6.10171563e-03f, -7.45612506e-03f, - -8.74136426e-03f, -9.94672023e-03f, -1.10621909e-02f, -1.20785406e-02f, -1.29874795e-02f, -1.37816456e-02f, - -1.44546479e-02f, -1.50012468e-02f, -1.54172106e-02f, -1.56995155e-02f, -1.58462779e-02f, -1.58567437e-02f, - -1.57313825e-02f, -1.54717967e-02f, -1.50807184e-02f, -1.45620705e-02f, -1.39207297e-02f, -1.31627253e-02f, - -1.22950111e-02f, -1.13254027e-02f, -1.02626834e-02f, -9.11627932e-03f, -7.89634415e-03f, -6.61364765e-03f, - -5.27939952e-03f, -3.90525708e-03f, -2.50314317e-03f, -1.08517576e-03f, 3.36418391e-04f, 1.74945190e-03f, - 3.14186033e-03f, 4.50178261e-03f, 5.81769448e-03f, 7.07851939e-03f, 8.27365386e-03f, 9.39310326e-03f, - 1.04276320e-02f, 1.13686527e-02f, 1.22085379e-02f, 1.29404450e-02f, 1.35585678e-02f, 1.40580446e-02f, - 1.44350939e-02f, 1.46869568e-02f, 1.48120098e-02f, 1.48096348e-02f, 1.46804295e-02f, 1.44259781e-02f, - 1.40489668e-02f, 1.35531325e-02f, 1.29432014e-02f, 1.22248563e-02f, 1.14046959e-02f, 1.04901687e-02f, - 9.48948107e-03f, 8.41156632e-03f, 7.26596347e-03f, 6.06280447e-03f, 4.81257444e-03f, 3.52622627e-03f, - 2.21492506e-03f, 8.89983592e-04f, -4.37153812e-04f, -1.75513167e-03f, -3.05265494e-03f, -4.31872834e-03f, - -5.54261874e-03f, -6.71396264e-03f, -7.82302244e-03f, -8.86045250e-03f, -9.81773278e-03f, -1.06869351e-02f, - -1.14610023e-02f, -1.21336754e-02f, -1.26995953e-02f, -1.31543908e-02f, -1.34945718e-02f, -1.37177266e-02f, - -1.38224110e-02f, -1.38082286e-02f, -1.36757739e-02f, -1.34266887e-02f, -1.30635886e-02f, -1.25900369e-02f, - -1.20105709e-02f, -1.13305978e-02f, -1.05563538e-02f, -9.69485926e-03f, -8.75389081e-03f, -7.74181164e-03f, - -6.66761679e-03f, -5.54076187e-03f, -4.37111830e-03f, -3.16893052e-03f, -1.94457115e-03f, -7.08705149e-04f, - 5.28079290e-04f, 1.75515870e-03f, 2.96204304e-03f, 4.13848585e-03f, 5.27451557e-03f, 6.36060039e-03f, - 7.38755863e-03f, 8.34692530e-03f, 9.23070802e-03f, 1.00316534e-02f, 1.07432528e-02f, 1.13597680e-02f, - 1.18763350e-02f, 1.22889283e-02f, 1.25944631e-02f, 1.27907515e-02f, 1.28765994e-02f, 1.28517102e-02f, - 1.27167966e-02f, 1.24734480e-02f, 1.21242371e-02f, 1.16725839e-02f, 1.11228281e-02f, 1.04800592e-02f, - 9.75022575e-03f, 8.93990424e-03f, 8.05644990e-03f, 7.10768601e-03f, 6.10205625e-03f, 5.04843878e-03f, - 3.95605458e-03f, 2.83441418e-03f, 1.69331277e-03f, 5.42568186e-04f, -6.07877124e-04f, -1.74818575e-03f, - -2.86860405e-03f, -3.95962685e-03f, -5.01201657e-03f, -6.01690058e-03f, -6.96589716e-03f, -7.85110424e-03f, - -8.66518231e-03f, -9.40145619e-03f, -1.00540095e-02f, -1.06175123e-02f, -1.10876024e-02f, -1.14606062e-02f, - -1.17337519e-02f, -1.19051415e-02f, -1.19737311e-02f, -1.19393909e-02f, -1.18028751e-02f, -1.15657387e-02f, - -1.12305357e-02f, -1.08005049e-02f, -1.02797519e-02f, -9.67318729e-03f, -8.98632838e-03f, -8.22543877e-03f, - -7.39737215e-03f, -6.50950785e-03f, -5.56975395e-03f, -4.58632875e-03f, -3.56792674e-03f, -2.52340823e-03f, - -1.46183597e-03f, -3.92391156e-04f, 6.75701684e-04f, 1.73331709e-03f, 2.77141530e-03f, 3.78118353e-03f, - 4.75407672e-03f, 5.68193005e-03f, 6.55698994e-03f, 7.37195674e-03f, 8.12013345e-03f, 8.79539509e-03f, - 9.39225030e-03f, 9.90597190e-03f, 1.03324819e-02f, 1.06685242e-02f, 1.09116177e-02f, 1.10600973e-02f, - 1.11130936e-02f, 1.10705983e-02f, 1.09333788e-02f, 1.07030445e-02f, 1.03819949e-02f, 9.97335332e-03f, - 9.48107464e-03f, 8.90968434e-03f, 8.26449756e-03f, 7.55132972e-03f, 6.77664458e-03f, 5.94731079e-03f, - 5.07073939e-03f, 4.15462520e-03f, 3.20700306e-03f, 2.23616222e-03f, 1.25050340e-03f, 2.58592562e-04f, - -7.31105992e-04f, -1.71003848e-03f, -2.66991104e-03f, -3.60254805e-03f, -4.50009626e-03f, -5.35500152e-03f, - -6.16013372e-03f, -6.90880302e-03f, -7.59484887e-03f, -8.21267759e-03f, -8.75730297e-03f, -9.22437062e-03f, - -9.61022818e-03f, -9.91196266e-03f, -1.01273334e-02f, -1.02549146e-02f, -1.02939949e-02f, -1.02446487e-02f, - -1.01077102e-02f, -9.88473930e-03f, -9.57804506e-03f, -9.19065219e-03f, -8.72623997e-03f, -8.18914967e-03f, - -7.58431711e-03f, -6.91725624e-03f, -6.19393169e-03f, -5.42085678e-03f, -4.60486090e-03f, -3.75314479e-03f, - -2.87318400e-03f, -1.97263669e-03f, -1.05936420e-03f, -1.41184633e-04f, 7.73935206e-04f, 1.67818033e-03f, - 2.56387121e-03f, 3.42348245e-03f, 4.24972968e-03f, 5.03575853e-03f, 5.77493594e-03f, 6.46117800e-03f, - 7.08885263e-03f, 7.65282423e-03f, 8.14856911e-03f, 8.57214716e-03f, 8.92027019e-03f, 9.19029194e-03f, - 9.38027470e-03f, 9.48895025e-03f, 9.51578399e-03f, 9.46091429e-03f, 9.32518284e-03f, 9.11016180e-03f, - 8.81806173e-03f, 8.45171440e-03f, 8.01466407e-03f, 7.51094572e-03f, 6.94521826e-03f, 6.32261691e-03f, - 5.64875255e-03f, 4.92963671e-03f, 4.17165548e-03f, 3.38149573e-03f, 2.56610069e-03f, 1.73253154e-03f, - 8.88083719e-04f, 4.00140997e-05f, -8.04377007e-04f, -1.63786496e-03f, -2.45336348e-03f, -3.24394120e-03f, - -4.00297149e-03f, -4.72406012e-03f, -5.40122825e-03f, -6.02886353e-03f, -6.60184564e-03f, -7.11547043e-03f, - -7.56567204e-03f, -7.94886879e-03f, -8.26207948e-03f, -8.50298133e-03f, -8.66984745e-03f, -8.76158174e-03f, - -8.77778600e-03f, -8.71866903e-03f, -8.58510255e-03f, -8.37858953e-03f, -8.10125332e-03f, -7.75580633e-03f, - -7.34555568e-03f, -6.87431135e-03f, -6.34642360e-03f, -5.76669768e-03f, -5.14031767e-03f, -4.47294897e-03f, - -3.77043291e-03f, -3.03903272e-03f, -2.28511456e-03f, -1.51527024e-03f, -7.36178447e-04f, 4.54225562e-05f, - 8.22859022e-04f, 1.58943109e-03f, 2.33866278e-03f, 3.06420334e-03f, 3.75990680e-03f, 4.42002538e-03f, - 5.03901750e-03f, 5.61180111e-03f, 6.13366220e-03f, 6.60043272e-03f, 7.00831931e-03f, 7.35414500e-03f, - 7.63524392e-03f, 7.84953557e-03f, 7.99547645e-03f, 8.07218955e-03f, 8.07933095e-03f, 8.01721906e-03f, - 7.88666864e-03f, 7.68919343e-03f, 7.42679720e-03f, 7.10202788e-03f, 6.71802523e-03f, 6.27832934e-03f, - 5.78702253e-03f, 5.24853339e-03f, 4.66776048e-03f, 4.04985033e-03f, 3.40032055e-03f, 2.72486114e-03f, - 2.02943382e-03f, 1.32005555e-03f, 6.02922229e-04f, -1.15810889e-04f, -8.29962401e-04f, -1.53344695e-03f, - -2.22024937e-03f, -2.88460828e-03f, -3.52090915e-03f, -4.12386103e-03f, -4.68844782e-03f, -5.21000854e-03f, - -5.68433641e-03f, -6.10753890e-03f, -6.47629357e-03f, -6.78770430e-03f, -7.03936807e-03f, -7.22944790e-03f, - -7.35662441e-03f, -7.42012069e-03f, -7.41971164e-03f, -7.35573757e-03f, -7.22905724e-03f, -7.04107429e-03f, - -6.79370122e-03f, -6.48940038e-03f, -6.13102314e-03f, -5.72192873e-03f, -5.26590521e-03f, -4.76707464e-03f, - -4.22993214e-03f, -3.65930825e-03f, -3.06022345e-03f, -2.43797793e-03f, -1.79803310e-03f, -1.14594988e-03f, - -4.87389180e-04f, 1.71985886e-04f, 8.26505744e-04f, 1.47057292e-03f, 2.09875564e-03f, 2.70572827e-03f, - 3.28638788e-03f, 3.83592350e-03f, 4.34975506e-03f, 4.82368759e-03f, 5.25383132e-03f, 5.63677359e-03f, - 5.96942535e-03f, 6.24924092e-03f, 6.47405650e-03f, 6.64226721e-03f, 6.75269253e-03f, 6.80469430e-03f, - 6.79815717e-03f, 6.73340631e-03f, 6.61130455e-03f, 6.43322863e-03f, 6.20094526e-03f, 5.91677710e-03f, - 5.58340169e-03f, 5.20393196e-03f, 4.78187614e-03f, 4.32106320e-03f, 3.82565711e-03f, 3.30005613e-03f, - 2.74895362e-03f, 2.17719303e-03f, 1.58978015e-03f, 9.91844057e-04f, 3.88540330e-04f, -2.14916878e-04f, - -8.13361192e-04f, -1.40168257e-03f, -1.97489740e-03f, -2.52818059e-03f, -3.05688539e-03f, -3.55662656e-03f, - -4.02326574e-03f, -4.45296958e-03f, -4.84228652e-03f, -5.18803438e-03f, -5.48755315e-03f, -5.73848611e-03f, - -5.93891991e-03f, -6.08745626e-03f, -6.18305471e-03f, -6.22520840e-03f, -6.21382472e-03f, -6.14928419e-03f, - -6.03244633e-03f, -5.86455879e-03f, -5.64736180e-03f, -5.38296537e-03f, -5.07389363e-03f, -4.72301916e-03f, - -4.33361321e-03f, -3.90915761e-03f, -3.45353173e-03f, -2.97077347e-03f, -2.46516689e-03f, -1.94119584e-03f, - -1.40340595e-03f, -8.56512644e-04f, -3.05232133e-04f, 2.45691031e-04f, 7.91538060e-04f, 1.32763724e-03f, - 1.84949345e-03f, 2.35267547e-03f, 2.83299113e-03f, 3.28645035e-03f, 3.70931698e-03f, 4.09812665e-03f, - 4.44973511e-03f, 4.76135341e-03f, 5.03050354e-03f, 5.25513155e-03f, 5.43353323e-03f, 5.56447821e-03f, - 5.64705544e-03f, 5.68083601e-03f, 5.66583437e-03f, 5.60238431e-03f, 5.49135375e-03f, 5.33391723e-03f, - 5.13169207e-03f, 4.88664671e-03f, 4.60113202e-03f, 4.27780860e-03f, 3.91964875e-03f, 3.52989866e-03f, - 3.11212090e-03f, 2.66999053e-03f, 2.20744344e-03f, 1.72859110e-03f, 1.23756351e-03f, 7.38678150e-04f, - 2.36236760e-04f, -2.65462378e-04f, -7.62072815e-04f, -1.24943395e-03f, -1.72337956e-03f, -2.17993754e-03f, - -2.61530935e-03f, -3.02588421e-03f, -3.40825196e-03f, -3.75935360e-03f, -4.07630652e-03f, -4.35660760e-03f, - -4.59808398e-03f, -4.79883718e-03f, -4.95743843e-03f, -5.07271280e-03f, -5.14393833e-03f, -5.17077608e-03f, - -5.15318763e-03f, -5.09164480e-03f, -4.98686807e-03f, -4.84002285e-03f, -4.65260103e-03f, -4.42642977e-03f, - -4.16366446e-03f, -3.86678300e-03f, -3.53847751e-03f, -3.18177292e-03f, -2.79986847e-03f, -2.39618401e-03f, - -1.97429017e-03f, -1.53788782e-03f, -1.09083664e-03f, -6.36973406e-04f, -1.80264329e-04f, 2.75399352e-04f, - 7.26104424e-04f, 1.16802598e-03f, 1.59744046e-03f, 2.01073128e-03f, 2.40446819e-03f, 2.77538562e-03f, - 3.12044615e-03f, 3.43683203e-03f, 3.72202393e-03f, 3.97374850e-03f, 4.19002854e-03f, 4.36925418e-03f, - 4.51006070e-03f, 4.61152219e-03f, 4.67293053e-03f, 4.69404975e-03f, 4.67490366e-03f, 4.61589307e-03f, - 4.51775252e-03f, 4.38154991e-03f, 4.20868532e-03f, 4.00082377e-03f, 3.75997274e-03f, 3.48836415e-03f, - 3.18851504e-03f, 2.86314343e-03f, 2.51519536e-03f, 2.14776743e-03f, 1.76411750e-03f, 1.36763070e-03f, - 9.61751835e-04f, 5.50052405e-04f, 1.36015058e-04f, -2.76720943e-04f, -6.84698152e-04f, -1.08442387e-03f, - -1.47253691e-03f, -1.84578853e-03f, -2.20105818e-03f, -2.53544188e-03f, -2.84616998e-03f, -3.13076058e-03f, - -3.38689733e-03f, -3.61260297e-03f, -3.80606518e-03f, -3.96589267e-03f, -4.09087232e-03f, -4.18013173e-03f, - -4.23315965e-03f, -4.24970953e-03f, -4.22981560e-03f, -4.17392494e-03f, -4.08267808e-03f, -3.95709577e-03f, - -3.79845153e-03f, -3.60829670e-03f, -3.38844338e-03f, -3.14094669e-03f, -2.86809742e-03f, -2.57237442e-03f, - -2.25643831e-03f, -1.92312165e-03f, -1.57535841e-03f, -1.21624129e-03f, -8.48868370e-04f, -4.76457354e-04f, - -1.02227062e-04f, 2.70659894e-04f, 6.38948957e-04f, 9.99596773e-04f, 1.34950884e-03f, 1.68579412e-03f, - 2.00565112e-03f, 2.30644176e-03f, 2.58570970e-03f, 2.84121989e-03f, 3.07087670e-03f, 3.27296771e-03f, - 3.44584695e-03f, 3.58825627e-03f, 3.69915439e-03f, 3.77779535e-03f, 3.82369144e-03f, 3.83666312e-03f, - 3.81678507e-03f, 3.76444486e-03f, 3.68027755e-03f, 3.56519883e-03f, 3.42038694e-03f, 3.24725992e-03f, - 3.04745181e-03f, 2.82287635e-03f, 2.57555610e-03f, 2.30778342e-03f, 2.02193938e-03f, 1.72060684e-03f, - 1.40642226e-03f, 1.08218540e-03f, 7.50708128e-04f, 4.14852040e-04f, 7.75468400e-05f, -2.58336678e-04f, - -5.89954675e-04f, -9.14464553e-04f, -1.22917409e-03f, -1.53142096e-03f, -1.81874942e-03f, -2.08875765e-03f, - -2.33925204e-03f, -2.56824046e-03f, -2.77387464e-03f, -2.95457151e-03f, -3.10891286e-03f, -3.23576957e-03f, - -3.33422309e-03f, -3.40361730e-03f, -3.44352432e-03f, -3.45380945e-03f, -3.43454926e-03f, -3.38612359e-03f, - -3.30910238e-03f, -3.20434413e-03f, -3.07289782e-03f, -2.91605448e-03f, -2.73534798e-03f, -2.53242439e-03f, - -2.30918427e-03f, -2.06766744e-03f, -1.81002532e-03f, -1.53857461e-03f, -1.25572213e-03f, -9.63956082e-04f, - -6.65804929e-04f, -3.63875198e-04f, -6.07622519e-05f, 2.40955893e-04f, 5.38685581e-04f, 8.29936911e-04f, - 1.11224977e-03f, 1.38328230e-03f, 1.64080028e-03f, 1.88265574e-03f, 2.10694670e-03f, 2.31181334e-03f, - 2.49567938e-03f, 2.65707799e-03f, 2.79477329e-03f, 2.90778929e-03f, 2.99526804e-03f, 3.05666792e-03f, - 3.09159989e-03f, 3.09996074e-03f, 3.08183486e-03f, 3.03757314e-03f, 2.96768997e-03f, 2.87296391e-03f, - 2.75438271e-03f, 2.61305979e-03f, 2.45041225e-03f, 2.26792371e-03f, 2.06728115e-03f, 1.85034398e-03f, - 1.61901728e-03f, 1.37543970e-03f, 1.12168235e-03f, 8.60048928e-04f, 5.92781787e-04f, 3.22217129e-04f, - 5.06437951e-05f, -2.19547817e-04f, -4.86132510e-04f, -7.46817210e-04f, -9.99443627e-04f, -1.24188233e-03f, - -1.47217245e-03f, -1.68839648e-03f, -1.88883105e-03f, -2.07184785e-03f, -2.23601745e-03f, -2.38006048e-03f, - -2.50288118e-03f, -2.60358292e-03f, -2.68144174e-03f, -2.73595307e-03f, -2.76679595e-03f, -2.77388624e-03f, - -2.75729794e-03f, -2.71735188e-03f, -2.65451985e-03f, -2.56952130e-03f, -2.46319204e-03f, -2.33660956e-03f, - -2.19096493e-03f, -2.02765268e-03f, -1.84815939e-03f, -1.65412932e-03f, -1.44731483e-03f, -1.22956426e-03f, - -1.00280075e-03f, -7.69022668e-04f, -5.30268510e-04f, -2.88586883e-04f, -4.60956253e-05f, 1.95186584e-04f, - 4.33161045e-04f, 6.65873263e-04f, 8.91328897e-04f, 1.10770620e-03f, 1.31316296e-03f, 1.50610067e-03f, - 1.68489795e-03f, 1.84814923e-03f, 1.99458512e-03f, 2.12304250e-03f, 2.23258384e-03f, 2.32237953e-03f, - 2.39181962e-03f, 2.44043032e-03f, 2.46796938e-03f, 2.47430968e-03f, 2.45957831e-03f, 2.42401283e-03f, - 2.36808884e-03f, 2.29238471e-03f, 2.19773378e-03f, 2.08501666e-03f, 1.95534528e-03f, 1.80993801e-03f, - 1.65014053e-03f, 1.47739854e-03f, 1.29329221e-03f, 1.09944593e-03f, 8.97596290e-04f, 6.89486470e-04f, - 4.76967544e-04f, 2.61847472e-04f, 4.59979030e-05f, -1.68770369e-04f, -3.80612759e-04f, -5.87744421e-04f, - -7.88452414e-04f, -9.81081718e-04f, -1.16402219e-03f, -1.33580811e-03f, -1.49504859e-03f, -1.64047131e-03f, - -1.77095587e-03f, -1.88548340e-03f, -1.98318254e-03f, -2.06335667e-03f, -2.12544333e-03f, -2.16903096e-03f, - -2.19389731e-03f, -2.19994674e-03f, -2.18726700e-03f, -2.15609170e-03f, -2.10683457e-03f, -2.04002290e-03f, - -1.95633800e-03f, -1.85665258e-03f, -1.74189023e-03f, -1.61313165e-03f, -1.47159921e-03f, -1.31856217e-03f, - -1.15541374e-03f, -9.83590913e-04f, -8.04645529e-04f, -6.20138811e-04f, -4.31664744e-04f, -2.40859759e-04f, - -4.93718861e-05f, 1.41183920e-04f, 3.29184443e-04f, 5.13049545e-04f, 6.91252710e-04f, 8.62329668e-04f, - 1.02486089e-03f, 1.17753306e-03f, 1.31912530e-03f, 1.44851584e-03f, 1.56468190e-03f, 1.66675270e-03f, - 1.75393226e-03f, 1.82562545e-03f, 1.88129935e-03f, 1.92062935e-03f, 1.94336360e-03f, 1.94946381e-03f, - 1.93898469e-03f, 1.91211060e-03f, 1.86925265e-03f, 1.81081128e-03f, 1.73745800e-03f, 1.64989979e-03f, - 1.54896085e-03f, 1.43565148e-03f, 1.31095906e-03f, 1.17607031e-03f, 1.03219054e-03f, 8.80596006e-04f, - 7.22634695e-04f, 5.59715925e-04f, 3.93223384e-04f, 2.24602808e-04f, 5.53223372e-05f, -1.13204206e-04f, - -2.79527886e-04f, -4.42273875e-04f, -6.00090187e-04f, -7.51646708e-04f, -8.95738714e-04f, -1.03117771e-03f, - -1.15687770e-03f, -1.27187587e-03f, -1.37523688e-03f, -1.46618576e-03f, -1.54403989e-03f, -1.60825931e-03f, - -1.65836399e-03f, -1.69405240e-03f, -1.71514183e-03f, -1.72154028e-03f, -1.71331327e-03f, -1.69063272e-03f, - -1.65381037e-03f, -1.60326168e-03f, -1.53948863e-03f, -1.46318779e-03f, -1.37503217e-03f, -1.27591969e-03f, - -1.16672308e-03f, -1.04846883e-03f, -9.22232848e-04f, -7.89108246e-04f, -6.50329911e-04f, -5.07057241e-04f, - -3.60579584e-04f, -2.12138548e-04f, -6.30166060e-05f, 8.55107333e-05f, 2.32212191e-04f, 3.75851456e-04f, - 5.15213418e-04f, 6.49182851e-04f, 7.76642588e-04f, 8.96585347e-04f, 1.00803198e-03f, 1.11010987e-03f, - 1.20203475e-03f, 1.28308439e-03f, 1.35268783e-03f, 1.41030687e-03f, 1.45558664e-03f, 1.48819124e-03f, - 1.50798717e-03f, 1.51486502e-03f, 1.50888467e-03f, 1.49022209e-03f, 1.45906012e-03f, 1.41583581e-03f, - 1.36095722e-03f, 1.29499749e-03f, 1.21859138e-03f, 1.13249419e-03f, 1.03745344e-03f, 9.34384957e-04f, - 8.24209226e-04f, 7.07921644e-04f, 5.86535461e-04f, 4.61118668e-04f, 3.32797940e-04f, 2.02615430e-04f, - 7.17560319e-05f, -5.87215139e-05f, -1.87700771e-04f, -3.14093799e-04f, -4.36855019e-04f, -5.54982470e-04f, - -6.67514567e-04f, -7.73539543e-04f, -8.72216549e-04f, -9.62754726e-04f, -1.04446836e-03f, -1.11673823e-03f, - -1.17901020e-03f, -1.23084835e-03f, -1.27191263e-03f, -1.30189831e-03f, -1.32066941e-03f, -1.32816613e-03f, - -1.32437715e-03f, -1.30944714e-03f, -1.28360668e-03f, -1.24710492e-03f, -1.20038313e-03f, -1.14391116e-03f, - -1.07822250e-03f, -1.00394823e-03f, -9.21799577e-04f, -8.32520513e-04f, -7.36916195e-04f, -6.35853312e-04f, - -5.30218398e-04f, -4.20950684e-04f, -3.08981087e-04f, -1.95310152e-04f, -8.08721649e-05f, 3.33481785e-05f, - 1.46369769e-04f, 2.57271691e-04f, 3.65123878e-04f, 4.69053422e-04f, 5.68205019e-04f, 6.61777482e-04f, - 7.49035427e-04f, 8.29295760e-04f, 9.01919035e-04f, 9.66370937e-04f, 1.02218113e-03f, 1.06892877e-03f, - 1.10630552e-03f, 1.13406370e-03f, 1.15204451e-03f, 1.16019052e-03f, 1.15848806e-03f, 1.14706630e-03f, - 1.12606449e-03f, 1.09574589e-03f, 1.05645362e-03f, 1.00859266e-03f, 9.52601766e-04f, 8.89057609e-04f, - 8.18535938e-04f, 7.41697389e-04f, 6.59241262e-04f, 5.71884368e-04f, 4.80414698e-04f, 3.85677252e-04f, - 2.88406796e-04f, 1.89536836e-04f, 8.98491837e-05f, -9.79888746e-06f, -1.08531507e-04f, -2.05575498e-04f, - -3.00092231e-04f, -3.91327952e-04f, -4.78537671e-04f, -5.61003964e-04f, -6.38090388e-04f, -7.09209697e-04f, - -7.73747838e-04f, -8.31297964e-04f, -8.81364804e-04f, -9.23641236e-04f, -9.57793553e-04f, -9.83624619e-04f, - -1.00098424e-03f, -1.00979404e-03f, -1.01003977e-03f, -1.00180772e-03f, -9.85219816e-04f, -9.60506778e-04f, - -9.27905874e-04f, -8.87790902e-04f, -8.40553609e-04f, -7.86632276e-04f, -7.26559669e-04f, -6.60872173e-04f, - -5.90177860e-04f, -5.15099219e-04f, -4.36341554e-04f, -3.54526447e-04f, -2.70436804e-04f, -1.84757234e-04f, - -9.82406108e-05f, -1.16228429e-05f, 7.44116225e-05f, 1.59099493e-04f, 2.41739119e-04f, 3.21707034e-04f, - 3.98276352e-04f, 4.70887555e-04f, 5.38973046e-04f, 6.01940918e-04f, 6.59368174e-04f, 7.10783030e-04f, - 7.55802336e-04f, 7.94127086e-04f, 8.25478803e-04f, 8.49639386e-04f, 8.66487952e-04f, 8.75935969e-04f, - 8.77948893e-04f, 8.72611584e-04f, 8.59994515e-04f, 8.40271458e-04f, 8.13696181e-04f, 7.80491851e-04f, - 7.41053306e-04f, 6.95727202e-04f, 6.44936090e-04f, 5.89181503e-04f, 5.28946796e-04f, 4.64790448e-04f, - 3.97272420e-04f, 3.27000597e-04f, 2.54559578e-04f, 1.80597276e-04f, 1.05760446e-04f, 3.06209047e-05f, - -4.41172003e-05f, -1.17884760e-04f, -1.90032814e-04f, -2.60000039e-04f, -3.27213235e-04f, -3.91110007e-04f, - -4.51226928e-04f, -5.07042112e-04f, -5.58194586e-04f, -6.04189222e-04f, -6.44816381e-04f, -6.79653847e-04f, - -7.08557315e-04f, -7.31282579e-04f, -7.47702169e-04f, -7.57731688e-04f, -7.61359812e-04f, -7.58589885e-04f, - -7.49503361e-04f, -7.34226582e-04f, -7.12935677e-04f, -6.85882645e-04f, -6.53307567e-04f, -6.15569562e-04f, - -5.72978650e-04f, -5.25977418e-04f, -4.74963705e-04f, -4.20426590e-04f, -3.62819514e-04f, -3.02647353e-04f, - -2.40497241e-04f, -1.76810216e-04f, -1.12210871e-04f, -4.71976690e-05f, 1.76624641e-05f, 8.18440593e-05f, - 1.44804207e-04f, 2.06021410e-04f, 2.65025446e-04f, 3.21327783e-04f, 3.74487008e-04f, 4.24062432e-04f, - 4.69715655e-04f, 5.11042943e-04f, 5.47794530e-04f, 5.79655168e-04f, 6.06446384e-04f, 6.27934546e-04f, - 6.44010762e-04f, 6.54614698e-04f, 6.59636425e-04f, 6.59157826e-04f, 6.53158826e-04f, 6.41794049e-04f, - 6.25154916e-04f, 6.03470855e-04f, 5.76917242e-04f, 5.45789736e-04f, 5.10368292e-04f, 4.70998661e-04f, - 4.28021656e-04f, 3.81834126e-04f, 3.32863326e-04f, 2.81489629e-04f, 2.28231239e-04f, 1.73484261e-04f, - 1.17756607e-04f, 6.14881351e-05f, 5.17778269e-06f, -5.07352374e-05f, -1.05745987e-04f, -1.59454662e-04f, - -2.11394268e-04f, -2.61151905e-04f, -3.08351703e-04f, -3.52598590e-04f, -3.93545002e-04f, -4.30916147e-04f, - -4.64387406e-04f, -4.93756593e-04f, -5.18755281e-04f, -5.39265493e-04f, -5.55137934e-04f, -5.66259303e-04f, - -5.72606783e-04f, -5.74140344e-04f, -5.70903292e-04f, -5.62934741e-04f, -5.50388898e-04f, -5.33351962e-04f, - -5.12028510e-04f, -4.86612455e-04f, -4.57392981e-04f, -4.24578939e-04f, -3.88503808e-04f, -3.49487518e-04f, - -3.07895836e-04f, -2.64036522e-04f, -2.18356445e-04f, -1.71198300e-04f, -1.22998901e-04f, -7.41392080e-05f, - -2.50280393e-05f, 2.38852047e-05f, 7.22663332e-05f, 1.19659647e-04f, 1.65718806e-04f, 2.10055385e-04f, - 2.52324173e-04f, 2.92190427e-04f, 3.29337577e-04f, 3.63510150e-04f, 3.94385715e-04f, 4.21803288e-04f, - 4.45519433e-04f, 4.65391876e-04f, 4.81270460e-04f, 4.93057625e-04f, 5.00688030e-04f, 5.04121708e-04f, - 5.03379627e-04f, 4.98485604e-04f, 4.89499566e-04f, 4.76539317e-04f, 4.59760023e-04f, 4.39274612e-04f, - 4.15334876e-04f, 3.88103885e-04f, 3.57902146e-04f, 3.24908089e-04f, 2.89490480e-04f, 2.51922687e-04f, - 2.12512220e-04f, 1.71637404e-04f, 1.29609890e-04f, 8.67866183e-05f, 4.35312276e-05f, 1.98808307e-07f, - -4.28589070e-05f, -8.52865394e-05f, -1.26765698e-04f, -1.66922292e-04f, -2.05456466e-04f, -2.42095652e-04f, - -2.76487494e-04f, -3.08425602e-04f, -3.37638832e-04f, -3.63923042e-04f, -3.87022898e-04f, -4.06875144e-04f, - -4.23245129e-04f, -4.36071615e-04f, -4.45236993e-04f, -4.50724682e-04f, -4.52491230e-04f, -4.50548104e-04f, - -4.44936790e-04f, -4.35725612e-04f, -4.22987381e-04f, -4.06882738e-04f, -3.87548587e-04f, -3.65123104e-04f, - -3.39860288e-04f, -3.11947486e-04f, -2.81618569e-04f, -2.49166817e-04f, -2.14824344e-04f, -1.78876370e-04f, - -1.41684861e-04f, -1.03466427e-04f, -6.45996088e-05f, -2.53738050e-05f, 1.39035721e-05f, 5.28977578e-05f, - 9.13010773e-05f, 1.28809554e-04f, 1.65139924e-04f, 2.00005346e-04f, 2.33095696e-04f, 2.64232233e-04f, - 2.93070034e-04f, 3.19508024e-04f, 3.43252648e-04f, 3.64165224e-04f, 3.82074036e-04f, 3.96868082e-04f, - 4.08408250e-04f, 4.16671952e-04f, 4.21556517e-04f, 4.23035822e-04f, 4.21172111e-04f, 4.15928838e-04f, - 4.07377025e-04f, 3.95568598e-04f, 3.80628038e-04f, 3.62729177e-04f, 3.41921136e-04f, 3.18489958e-04f, - 2.92497406e-04f, 2.64266550e-04f, 2.33955571e-04f, 2.01809261e-04f, 1.68092145e-04f, 1.33141461e-04f, - 9.71043460e-05f, 6.03452880e-05f, 2.31264055e-05f, -1.43105089e-05f, -5.15607083e-05f, -8.84833364e-05f, - -1.24679461e-04f, -1.59910519e-04f, -1.93952723e-04f, -2.26496145e-04f, -2.57307566e-04f, -2.86175538e-04f, - -3.12853472e-04f, -3.37140613e-04f, -3.58914997e-04f, -3.77932329e-04f, -3.94117065e-04f, -4.07317063e-04f, - -4.17422308e-04f, -4.24419479e-04f, -4.28161231e-04f, -4.28700484e-04f, -4.26016659e-04f, -4.20088126e-04f, - -4.11009185e-04f, -3.98835037e-04f, -3.83585114e-04f, -3.65493072e-04f, -3.44616197e-04f, -3.21064387e-04f, - -2.95119418e-04f, -2.66863117e-04f, -2.36549174e-04f, -2.04391686e-04f, -1.70585806e-04f, -1.35432614e-04f, - -9.91006984e-05f, -6.19152828e-05f, -2.41012311e-05f, 1.40621144e-05f, 5.22867497e-05f, 9.03199843e-05f, - 1.27917614e-04f, 1.64740292e-04f, 2.00634478e-04f, 2.35261402e-04f, 2.68377430e-04f, 2.99818019e-04f, - 3.29273634e-04f, 3.56562766e-04f, 3.81532332e-04f, 4.03948113e-04f, 4.23655375e-04f, 4.40488930e-04f, - 4.54376777e-04f, 4.65137195e-04f, 4.72679704e-04f, 4.77014073e-04f, 4.77982201e-04f, 4.75625277e-04f, - 4.69878507e-04f, 4.60802987e-04f, 4.48367418e-04f, 4.32641679e-04f, 4.13709630e-04f, 3.91634147e-04f, - 3.66512902e-04f, 3.38481392e-04f, 3.07634938e-04f, 2.74189182e-04f, 2.38229594e-04f, 1.99985879e-04f, - 1.59632210e-04f, 1.17351364e-04f, 7.33404728e-05f, 2.78844831e-05f, -1.89099461e-05f, -6.67343638e-05f, - -1.15367449e-04f, -1.64649983e-04f, -2.14224348e-04f, -2.64019844e-04f, -3.13654244e-04f, -3.62990333e-04f, - -4.11800705e-04f, -4.59821928e-04f, -5.06946486e-04f, -5.52847863e-04f, -5.97397068e-04f, -6.40454770e-04f, - -6.81765968e-04f, -7.21210131e-04f, -7.58634477e-04f, -7.93939572e-04f, -8.26964876e-04f, -8.57585335e-04f, - -8.85733438e-04f, -9.11351007e-04f, -9.34300512e-04f, -9.54617442e-04f, -9.72159416e-04f, -9.87012089e-04f, - -9.99095133e-04f, -1.00846242e-03f, -1.01506022e-03f, -1.01897105e-03f, -1.02021427e-03f, -1.01887259e-03f, - -1.01497557e-03f, -1.00861358e-03f, -9.99877741e-04f, -9.88823136e-04f, -9.75617693e-04f, -9.60303769e-04f, - -9.43035535e-04f, -9.23922797e-04f, -9.03105429e-04f, -8.80708716e-04f, -8.56853281e-04f, -8.31685264e-04f, - -8.05348207e-04f, -7.77961627e-04f, -7.49713086e-04f, -7.20674604e-04f, -6.91032783e-04f, -6.60888020e-04f, - -6.30372917e-04f, -5.99673349e-04f, -5.68830563e-04f, -5.38013304e-04f, -5.07353303e-04f, -4.76915043e-04f, - -4.46832926e-04f, -4.17179291e-04f, -3.88083307e-04f, -3.59575024e-04f, -3.31820735e-04f, -3.04804303e-04f, - -2.78616041e-04f, -2.53335964e-04f, -2.28986996e-04f, -2.05619529e-04f, -1.83318449e-04f, -1.61979425e-04f, - -1.41791423e-04f, -1.22648816e-04f, -1.04625498e-04f, -8.77122910e-05f, -7.18653457e-05f, -5.71787106e-05f, - -4.34807639e-05f, -3.09618857e-05f, -1.94074401e-05f, -8.88017971e-06f, 6.09625220e-07f, 9.14020334e-06f, - 1.67805558e-05f, 2.35369965e-05f, 2.94278194e-05f, 3.45049751e-05f, 3.88373828e-05f, 4.24291966e-05f, - 4.53445665e-05f, 4.76965834e-05f, 4.93395567e-05f, 5.05392111e-05f, 5.12257065e-05f, 5.14579340e-05f, - 5.12651750e-05f, 5.07312551e-05f, 4.98486765e-05f, 4.87082573e-05f, 4.73439631e-05f, 4.56740817e-05f, - 4.38653618e-05f, 4.19399075e-05f, 3.99125668e-05f, 3.77616021e-05f, 3.56135997e-05f, 3.33554815e-05f, - 3.11656899e-05f, 2.89038150e-05f, 2.67281634e-05f, 2.46192762e-05f, 2.24899205e-05f, 2.04698700e-05f, - 1.84927655e-05f, 1.66762886e-05f, 1.49393771e-05f, 1.32258081e-05f, 1.16985586e-05f, 1.01874391e-05f, - 8.99882100e-06f, 7.61267073e-06f, 6.57702907e-06f, 5.59829210e-06f, 4.27698546e-06f, 1.03248674e-05f, -}; +#include "AudioSRCData.h" // high/low part of int64_t #define LO32(a) ((uint32_t)(a)) diff --git a/libraries/audio/src/AudioSRCData.h b/libraries/audio/src/AudioSRCData.h new file mode 100644 index 0000000000..8ab868e898 --- /dev/null +++ b/libraries/audio/src/AudioSRCData.h @@ -0,0 +1,548 @@ +// +// AudioSRCData.h +// libraries/audio/src +// +// Created by Ken Cooke on 6/6/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// +// Prototype filter coefficients for audio sampling rate conversion. +// +// See "Design of Optimal Minimum Phase Digital FIR Filters Using Discrete Hilbert Transforms" +// IEEE TRANSACTIONS ON SIGNAL PROCESSING, May 2000 +// +// Minimum-phase equiripple FIR lowpass +// taps = 96, phases = 32 +// +// passband = 0.918 (20.2khz @ 44.1khz sampling rate) +// stopband = 1.010 (22.2khz @ 44.1khz sampling rate) +// passband ripple = +-0.01dB +// stopband attn = -125dB (-70dB at 1.000) +// +// Resampling algorithm: +// One of two methods is used, depending on whether the conversion is reducible to a ratio of small integers L/M. +// For rational ratio, the prototype is upsampled to L/M polyphase filter using 3rd-order Lagrange interpolation. +// For irrational ratio, the prototype is upsampled to 256/M polyphase filter, followed by linear interpolation. +// For both cases, oversampling at each stage ensures the original passband and stopband specifications are met. +// +static const int PROTOTYPE_TAPS = 96; // filter taps per phase +static const int PROTOTYPE_PHASES = 32; // oversampling factor + +static const float prototypeFilter[PROTOTYPE_TAPS * PROTOTYPE_PHASES] = { + 0.00000000e+00f, 1.55021703e-05f, 1.46054865e-05f, 2.07057160e-05f, 2.91335519e-05f, 4.00091078e-05f, + 5.33544450e-05f, 7.03618468e-05f, 9.10821639e-05f, 1.16484613e-04f, 1.47165999e-04f, 1.84168304e-04f, + 2.28429617e-04f, 2.80913884e-04f, 3.42940399e-04f, 4.15773039e-04f, 5.01023255e-04f, 6.00234953e-04f, + 7.15133271e-04f, 8.47838855e-04f, 1.00032516e-03f, 1.17508881e-03f, 1.37452550e-03f, 1.60147614e-03f, + 1.85886458e-03f, 2.14985024e-03f, 2.47783071e-03f, 2.84666764e-03f, 3.26016878e-03f, 3.72252797e-03f, + 4.23825900e-03f, 4.81207874e-03f, 5.44904143e-03f, 6.15447208e-03f, 6.93399929e-03f, 7.79337059e-03f, + 8.73903392e-03f, 9.77729117e-03f, 1.09149561e-02f, 1.21591316e-02f, 1.35171164e-02f, 1.49965439e-02f, + 1.66053136e-02f, 1.83515384e-02f, 2.02435362e-02f, 2.22899141e-02f, 2.44995340e-02f, 2.68813362e-02f, + 2.94443254e-02f, 3.21979928e-02f, 3.51514690e-02f, 3.83143719e-02f, 4.16960560e-02f, 4.53060504e-02f, + 4.91538115e-02f, 5.32486197e-02f, 5.75998650e-02f, 6.22164253e-02f, 6.71072811e-02f, 7.22809789e-02f, + 7.77457552e-02f, 8.35095233e-02f, 8.95796944e-02f, 9.59631768e-02f, 1.02666457e-01f, 1.09695215e-01f, + 1.17054591e-01f, 1.24748885e-01f, 1.32781656e-01f, 1.41155521e-01f, 1.49872243e-01f, 1.58932534e-01f, + 1.68335961e-01f, 1.78081143e-01f, 1.88165339e-01f, 1.98584621e-01f, 2.09333789e-01f, 2.20406193e-01f, + 2.31793899e-01f, 2.43487398e-01f, 2.55475740e-01f, 2.67746404e-01f, 2.80285305e-01f, 2.93076743e-01f, + 3.06103423e-01f, 3.19346351e-01f, 3.32784916e-01f, 3.46396772e-01f, 3.60158039e-01f, 3.74043042e-01f, + 3.88024564e-01f, 4.02073759e-01f, 4.16160177e-01f, 4.30251886e-01f, 4.44315429e-01f, 4.58315954e-01f, + 4.72217175e-01f, 4.85981675e-01f, 4.99570709e-01f, 5.12944586e-01f, 5.26062401e-01f, 5.38882630e-01f, + 5.51362766e-01f, 5.63459860e-01f, 5.75130384e-01f, 5.86330458e-01f, 5.97016050e-01f, 6.07143161e-01f, + 6.16667840e-01f, 6.25546499e-01f, 6.33735979e-01f, 6.41193959e-01f, 6.47878856e-01f, 6.53750084e-01f, + 6.58768549e-01f, 6.62896349e-01f, 6.66097381e-01f, 6.68337353e-01f, 6.69583869e-01f, 6.69807061e-01f, + 6.68979117e-01f, 6.67075139e-01f, 6.64072812e-01f, 6.59952827e-01f, 6.54699116e-01f, 6.48298688e-01f, + 6.40742160e-01f, 6.32023668e-01f, 6.22141039e-01f, 6.11095903e-01f, 5.98893921e-01f, 5.85544600e-01f, + 5.71061707e-01f, 5.55463040e-01f, 5.38770639e-01f, 5.21010762e-01f, 5.02213839e-01f, 4.82414572e-01f, + 4.61651859e-01f, 4.39968628e-01f, 4.17412000e-01f, 3.94032951e-01f, 3.69886464e-01f, 3.45031084e-01f, + 3.19529091e-01f, 2.93446187e-01f, 2.66851164e-01f, 2.39815999e-01f, 2.12415399e-01f, 1.84726660e-01f, + 1.56829293e-01f, 1.28804933e-01f, 1.00736965e-01f, 7.27100355e-02f, 4.48100810e-02f, 1.71237415e-02f, + -1.02620228e-02f, -3.72599591e-02f, -6.37832871e-02f, -8.97457733e-02f, -1.15062201e-01f, -1.39648782e-01f, + -1.63423488e-01f, -1.86306368e-01f, -2.08220103e-01f, -2.29090072e-01f, -2.48845046e-01f, -2.67417270e-01f, + -2.84742946e-01f, -3.00762597e-01f, -3.15421127e-01f, -3.28668542e-01f, -3.40459849e-01f, -3.50755400e-01f, + -3.59521402e-01f, -3.66729768e-01f, -3.72358475e-01f, -3.76391839e-01f, -3.78820421e-01f, -3.79641287e-01f, + -3.78858203e-01f, -3.76481336e-01f, -3.72527677e-01f, -3.67020780e-01f, -3.59990760e-01f, -3.51474372e-01f, + -3.41514630e-01f, -3.30160971e-01f, -3.17468898e-01f, -3.03499788e-01f, -2.88320749e-01f, -2.72004315e-01f, + -2.54628056e-01f, -2.36274454e-01f, -2.17030464e-01f, -1.96986952e-01f, -1.76238733e-01f, -1.54883647e-01f, + -1.33022496e-01f, -1.10758449e-01f, -8.81964466e-02f, -6.54430504e-02f, -4.26055475e-02f, -1.97916415e-02f, + 2.89108184e-03f, 2.53355868e-02f, 4.74362201e-02f, 6.90887518e-02f, 9.01914308e-02f, 1.10644978e-01f, + 1.30353494e-01f, 1.49224772e-01f, 1.67170735e-01f, 1.84107975e-01f, 1.99958067e-01f, 2.14648181e-01f, + 2.28111323e-01f, 2.40286622e-01f, 2.51119890e-01f, 2.60563701e-01f, 2.68577740e-01f, 2.75129027e-01f, + 2.80192144e-01f, 2.83749177e-01f, 2.85790223e-01f, 2.86312986e-01f, 2.85323221e-01f, 2.82834421e-01f, + 2.78867915e-01f, 2.73452721e-01f, 2.66625431e-01f, 2.58429983e-01f, 2.48917457e-01f, 2.38145826e-01f, + 2.26179680e-01f, 2.13089734e-01f, 1.98952740e-01f, 1.83850758e-01f, 1.67870897e-01f, 1.51104879e-01f, + 1.33648388e-01f, 1.15600665e-01f, 9.70639763e-02f, 7.81429119e-02f, 5.89439889e-02f, 3.95749746e-02f, + 2.01442353e-02f, 7.60241152e-04f, -1.84690990e-02f, -3.74370397e-02f, -5.60385970e-02f, -7.41711039e-02f, + -9.17348686e-02f, -1.08633632e-01f, -1.24775254e-01f, -1.40071993e-01f, -1.54441372e-01f, -1.67806284e-01f, + -1.80095654e-01f, -1.91244732e-01f, -2.01195605e-01f, -2.09897310e-01f, -2.17306320e-01f, -2.23386736e-01f, + -2.28110407e-01f, -2.31457193e-01f, -2.33415044e-01f, -2.33980051e-01f, -2.33156463e-01f, -2.30956673e-01f, + -2.27401097e-01f, -2.22518148e-01f, -2.16343899e-01f, -2.08921985e-01f, -2.00303365e-01f, -1.90545790e-01f, + -1.79713804e-01f, -1.67877977e-01f, -1.55114789e-01f, -1.41505907e-01f, -1.27137921e-01f, -1.12101628e-01f, + -9.64915640e-02f, -8.04054232e-02f, -6.39434707e-02f, -4.72078814e-02f, -3.03021635e-02f, -1.33305082e-02f, + 3.60284977e-03f, 2.03942507e-02f, 3.69413014e-02f, 5.31433810e-02f, 6.89024656e-02f, 8.41234679e-02f, + 9.87150268e-02f, 1.12589969e-01f, 1.25665865e-01f, 1.37865538e-01f, 1.49117506e-01f, 1.59356490e-01f, + 1.68523664e-01f, 1.76567229e-01f, 1.83442499e-01f, 1.89112308e-01f, 1.93547212e-01f, 1.96725586e-01f, + 1.98633878e-01f, 1.99266486e-01f, 1.98625999e-01f, 1.96723008e-01f, 1.93576075e-01f, 1.89211557e-01f, + 1.83663562e-01f, 1.76973516e-01f, 1.69190033e-01f, 1.60368490e-01f, 1.50570805e-01f, 1.39864815e-01f, + 1.28324021e-01f, 1.16026978e-01f, 1.03056879e-01f, 8.95008829e-02f, 7.54496798e-02f, 6.09968238e-02f, + 4.62380664e-02f, 3.12708901e-02f, 1.61936956e-02f, 1.10531988e-03f, -1.38957653e-02f, -2.87119784e-02f, + -4.32472742e-02f, -5.74078385e-02f, -7.11026311e-02f, -8.42439713e-02f, -9.67481917e-02f, -1.08536049e-01f, + -1.19533350e-01f, -1.29671345e-01f, -1.38887238e-01f, -1.47124498e-01f, -1.54333373e-01f, -1.60470968e-01f, + -1.65501755e-01f, -1.69397631e-01f, -1.72138140e-01f, -1.73710602e-01f, -1.74110159e-01f, -1.73339798e-01f, + -1.71410274e-01f, -1.68340111e-01f, -1.64155335e-01f, -1.58889414e-01f, -1.52582850e-01f, -1.45283122e-01f, + -1.37044042e-01f, -1.27925722e-01f, -1.17993860e-01f, -1.07319421e-01f, -9.59781808e-02f, -8.40500777e-02f, + -7.16188049e-02f, -5.87710561e-02f, -4.55961475e-02f, -3.21851919e-02f, -1.86306406e-02f, -5.02554942e-03f, + 8.53698384e-03f, 2.19645467e-02f, 3.51659468e-02f, 4.80518693e-02f, 6.05355056e-02f, 7.25330700e-02f, + 8.39645094e-02f, 9.47537898e-02f, 1.04829753e-01f, 1.14126254e-01f, 1.22582788e-01f, 1.30144907e-01f, + 1.36764459e-01f, 1.42400029e-01f, 1.47017076e-01f, 1.50588312e-01f, 1.53093700e-01f, 1.54520736e-01f, + 1.54864367e-01f, 1.54127119e-01f, 1.52318991e-01f, 1.49457408e-01f, 1.45567062e-01f, 1.40679709e-01f, + 1.34833933e-01f, 1.28074855e-01f, 1.20453893e-01f, 1.12028129e-01f, 1.02860307e-01f, 9.30178765e-02f, + 8.25730032e-02f, 7.16016450e-02f, 6.01833134e-02f, 4.84002546e-02f, 3.63370724e-02f, 2.40800037e-02f, + 1.17163168e-02f, -6.66217400e-04f, -1.29801121e-02f, -2.51385315e-02f, -3.70562030e-02f, -4.86497748e-02f, + -5.98384928e-02f, -7.05447859e-02f, -8.06947592e-02f, -9.02187441e-02f, -9.90517313e-02f, -1.07133911e-01f, + -1.14410951e-01f, -1.20834483e-01f, -1.26362422e-01f, -1.30959116e-01f, -1.34595787e-01f, -1.37250547e-01f, + -1.38908600e-01f, -1.39562374e-01f, -1.39211442e-01f, -1.37862602e-01f, -1.35529795e-01f, -1.32233909e-01f, + -1.28002721e-01f, -1.22870611e-01f, -1.16878278e-01f, -1.10072477e-01f, -1.02505698e-01f, -9.42356124e-02f, + -8.53248753e-02f, -7.58404912e-02f, -6.58532924e-02f, -5.54376360e-02f, -4.46705953e-02f, -3.36315414e-02f, + -2.24015972e-02f, -1.10628991e-02f, 3.01894735e-04f, 1.16101918e-02f, 2.27801642e-02f, 3.37311642e-02f, + 4.43845430e-02f, 5.46640016e-02f, 6.44962637e-02f, 7.38115400e-02f, 8.25440784e-02f, 9.06325572e-02f, + 9.80206066e-02f, 1.04657146e-01f, 1.10496723e-01f, 1.15499920e-01f, 1.19633523e-01f, 1.22870824e-01f, + 1.25191729e-01f, 1.26582959e-01f, 1.27038061e-01f, 1.26557494e-01f, 1.25148528e-01f, 1.22825305e-01f, + 1.19608512e-01f, 1.15525479e-01f, 1.10609643e-01f, 1.04900592e-01f, 9.84435537e-02f, 9.12890948e-02f, + 8.34927732e-02f, 7.51146973e-02f, 6.62190194e-02f, 5.68735547e-02f, 4.71491262e-02f, 3.71191855e-02f, + 2.68591932e-02f, 1.64459573e-02f, 5.95731808e-03f, -4.52874940e-03f, -1.49344723e-02f, -2.51829130e-02f, + -3.51986373e-02f, -4.49081427e-02f, -5.42404654e-02f, -6.31276969e-02f, -7.15054163e-02f, -7.93132713e-02f, + -8.64953327e-02f, -9.30005042e-02f, -9.87829011e-02f, -1.03802223e-01f, -1.08023943e-01f, -1.11419636e-01f, + -1.13967111e-01f, -1.15650603e-01f, -1.16460855e-01f, -1.16395152e-01f, -1.15457368e-01f, -1.13657871e-01f, + -1.11013433e-01f, -1.07547117e-01f, -1.03288073e-01f, -9.82712708e-02f, -9.25372646e-02f, -8.61318657e-02f, + -7.91057486e-02f, -7.15141053e-02f, -6.34161588e-02f, -5.48747791e-02f, -4.59559696e-02f, -3.67282941e-02f, + -2.72624874e-02f, -1.76307914e-02f, -7.90648674e-03f, 1.83670340e-03f, 1.15251424e-02f, 2.10858716e-02f, + 3.04471304e-02f, 3.95388944e-02f, 4.82933904e-02f, 5.66456655e-02f, 6.45340054e-02f, 7.19003487e-02f, + 7.86908695e-02f, 8.48562395e-02f, 9.03519908e-02f, 9.51389501e-02f, 9.91834077e-02f, 1.02457361e-01f, + 1.04938834e-01f, 1.06611872e-01f, 1.07466724e-01f, 1.07499917e-01f, 1.06714213e-01f, 1.05118588e-01f, + 1.02728167e-01f, 9.95640680e-02f, 9.56532488e-02f, 9.10282406e-02f, 8.57269309e-02f, 7.97922261e-02f, + 7.32717395e-02f, 6.62174249e-02f, 5.86850536e-02f, 5.07339959e-02f, 4.24265058e-02f, 3.38274345e-02f, + 2.50036502e-02f, 1.60234844e-02f, 6.95628026e-03f, -2.12820655e-03f, -1.11602438e-02f, -2.00708281e-02f, + -2.87920337e-02f, -3.72576320e-02f, -4.54035426e-02f, -5.31684173e-02f, -6.04938939e-02f, -6.73253212e-02f, + -7.36119310e-02f, -7.93072981e-02f, -8.43697556e-02f, -8.87625537e-02f, -9.24542939e-02f, -9.54189981e-02f, + -9.76364402e-02f, -9.90921435e-02f, -9.97776003e-02f, -9.96902366e-02f, -9.88334463e-02f, -9.72165780e-02f, + -9.48547668e-02f, -9.17688999e-02f, -8.79853312e-02f, -8.35357688e-02f, -7.84569594e-02f, -7.27903677e-02f, + -6.65818940e-02f, -5.98814932e-02f, -5.27427333e-02f, -4.52224733e-02f, -3.73802459e-02f, -2.92780037e-02f, + -2.09794209e-02f, -1.25495498e-02f, -4.05425988e-03f, 4.44034349e-03f, 1.28682571e-02f, 2.11643361e-02f, + 2.92645357e-02f, 3.71066200e-02f, 4.46305203e-02f, 5.17788267e-02f, 5.84972389e-02f, 6.47349496e-02f, + 7.04450836e-02f, 7.55849928e-02f, 8.01165748e-02f, 8.40066506e-02f, 8.72270848e-02f, 8.97550618e-02f, + 9.15732179e-02f, 9.26698315e-02f, 9.30387881e-02f, 9.26796720e-02f, 9.15978025e-02f, 8.98040443e-02f, + 8.73148489e-02f, 8.41520461e-02f, 8.03426093e-02f, 7.59185468e-02f, 7.09165136e-02f, 6.53776255e-02f, + 5.93470480e-02f, 5.28736293e-02f, 4.60095655e-02f, 3.88099545e-02f, 3.13323302e-02f, 2.36362162e-02f, + 1.57827398e-02f, 7.83395091e-03f, -1.47413782e-04f, -8.09864153e-03f, -1.59574406e-02f, -2.36623595e-02f, + -3.11534717e-02f, -3.83725840e-02f, -4.52638947e-02f, -5.17743411e-02f, -5.78539729e-02f, -6.34564348e-02f, + -6.85392092e-02f, -7.30640654e-02f, -7.69971954e-02f, -8.03096220e-02f, -8.29772975e-02f, -8.49813524e-02f, + -8.63081836e-02f, -8.69495746e-02f, -8.69027157e-02f, -8.61702687e-02f, -8.47602668e-02f, -8.26860569e-02f, + -7.99661981e-02f, -7.66242997e-02f, -7.26887788e-02f, -6.81926752e-02f, -6.31733712e-02f, -5.76722279e-02f, + -5.17343061e-02f, -4.54080069e-02f, -3.87446321e-02f, -3.17980032e-02f, -2.46239897e-02f, -1.72801497e-02f, + -9.82518156e-03f, -2.31845300e-03f, 5.18037510e-03f, 1.26119044e-02f, 1.99174857e-02f, 2.70395921e-02f, + 3.39223499e-02f, 4.05119404e-02f, 4.67570465e-02f, 5.26092142e-02f, 5.80232695e-02f, 6.29576539e-02f, + 6.73747113e-02f, 7.12410320e-02f, 7.45276905e-02f, 7.72104218e-02f, 7.92698394e-02f, 8.06915952e-02f, + 8.14664004e-02f, 8.15901977e-02f, 8.10640907e-02f, 7.98943315e-02f, 7.80922975e-02f, 7.56743792e-02f, + 7.26617861e-02f, 6.90804346e-02f, 6.49606433e-02f, 6.03370049e-02f, 5.52479503e-02f, 4.97355660e-02f, + 4.38451300e-02f, 3.76248662e-02f, 3.11254263e-02f, 2.43995757e-02f, 1.75017105e-02f, 1.04874823e-02f, + 3.41321948e-03f, -3.66433362e-03f, -1.06886566e-02f, -1.76037566e-02f, -2.43547422e-02f, -3.08881238e-02f, + -3.71523818e-02f, -4.30982377e-02f, -4.86791529e-02f, -5.38515978e-02f, -5.85754991e-02f, -6.28144137e-02f, + -6.65359631e-02f, -6.97119559e-02f, -7.23186409e-02f, -7.43369897e-02f, -7.57526047e-02f, -7.65560812e-02f, + -7.67428560e-02f, -7.63134051e-02f, -7.52730583e-02f, -7.36321241e-02f, -7.14055927e-02f, -6.86132027e-02f, + -6.52791213e-02f, -6.14318004e-02f, -5.71037475e-02f, -5.23312158e-02f, -4.71539306e-02f, -4.16147519e-02f, + -3.57593331e-02f, -2.96357023e-02f, -2.32939478e-02f, -1.67857228e-02f, -1.01639251e-02f, -3.48213128e-03f, + 3.20566951e-03f, 9.84566549e-03f, 1.63845318e-02f, 2.27699627e-02f, 2.89509937e-02f, 3.48784838e-02f, + 4.05054571e-02f, 4.57875191e-02f, 5.06831561e-02f, 5.51541055e-02f, 5.91656321e-02f, 6.26867948e-02f, + 6.56907214e-02f, 6.81547545e-02f, 7.00607045e-02f, 7.13948753e-02f, 7.21482790e-02f, 7.23165894e-02f, + 7.19002973e-02f, 7.09044846e-02f, 6.93390331e-02f, 6.72183039e-02f, 6.45611568e-02f, 6.13906537e-02f, + 5.77340810e-02f, 5.36223917e-02f, 4.90902973e-02f, 4.41756853e-02f, 3.89195025e-02f, 3.33653266e-02f, + 2.75589553e-02f, 2.15482187e-02f, 1.53823433e-02f, 9.11173206e-03f, 2.78750380e-03f, -3.53899736e-03f, + -9.81648845e-03f, -1.59942887e-02f, -2.20226002e-02f, -2.78530676e-02f, -3.34389835e-02f, -3.87358558e-02f, + -4.37015752e-02f, -4.82968641e-02f, -5.24856104e-02f, -5.62350079e-02f, -5.95160314e-02f, -6.23034090e-02f, + -6.45760369e-02f, -6.63170246e-02f, -6.75138263e-02f, -6.81583864e-02f, -6.82471093e-02f, -6.77809819e-02f, + -6.67654439e-02f, -6.52104027e-02f, -6.31301405e-02f, -6.05431381e-02f, -5.74719510e-02f, -5.39430121e-02f, + -4.99864152e-02f, -4.56356108e-02f, -4.09271785e-02f, -3.59005358e-02f, -3.05975021e-02f, -2.50620982e-02f, + -1.93400931e-02f, -1.34786109e-02f, -7.52582921e-03f, -1.53047296e-03f, 4.45846396e-03f, 1.03922252e-02f, + 1.62226043e-02f, 2.19024111e-02f, 2.73857927e-02f, 3.26286453e-02f, 3.75889120e-02f, 4.22270162e-02f, + 4.65060678e-02f, 5.03922602e-02f, 5.38550360e-02f, 5.68673912e-02f, 5.94061299e-02f, 6.14518959e-02f, + 6.29894927e-02f, 6.40078422e-02f, 6.45002081e-02f, 6.44641312e-02f, 6.39014463e-02f, 6.28183549e-02f, + 6.12252434e-02f, 5.91366226e-02f, 5.65710713e-02f, 5.35509478e-02f, 5.01023211e-02f, 4.62546289e-02f, + 4.20405644e-02f, 3.74956324e-02f, 3.26580309e-02f, 2.75681921e-02f, 2.22685138e-02f, 1.68029869e-02f, + 1.12168479e-02f, 5.55616360e-03f, -1.32475496e-04f, -5.80242145e-03f, -1.14072870e-02f, -1.69013632e-02f, + -2.22399629e-02f, -2.73798231e-02f, -3.22793559e-02f, -3.68992177e-02f, -4.12022700e-02f, -4.51542301e-02f, + -4.87237130e-02f, -5.18825743e-02f, -5.46061242e-02f, -5.68733215e-02f, -5.86668721e-02f, -5.99735198e-02f, + -6.07838952e-02f, -6.10928895e-02f, -6.08993923e-02f, -6.02064781e-02f, -5.90213291e-02f, -5.73550887e-02f, + -5.52228853e-02f, -5.26435817e-02f, -4.96396897e-02f, -4.62371294e-02f, -4.24650256e-02f, -3.83554628e-02f, + -3.39432096e-02f, -2.92654225e-02f, -2.43613233e-02f, -1.92718970e-02f, -1.40395616e-02f, -8.70771728e-03f, + -3.32056777e-03f, 2.07744785e-03f, 7.44190391e-03f, 1.27287222e-02f, 1.78946228e-02f, 2.28975002e-02f, + 2.76965843e-02f, 3.22530140e-02f, 3.65299534e-02f, 4.04930363e-02f, 4.41105069e-02f, 4.73536159e-02f, + 5.01967201e-02f, 5.26175750e-02f, 5.45974724e-02f, 5.61213729e-02f, 5.71780843e-02f, 5.77601946e-02f, + 5.78643759e-02f, 5.74910914e-02f, 5.66448597e-02f, 5.53340158e-02f, 5.35707338e-02f, 5.13708843e-02f, + 4.87538683e-02f, 4.57425137e-02f, 4.23627999e-02f, 3.86437075e-02f, 3.46169024e-02f, 3.03165387e-02f, + 2.57788894e-02f, 2.10421222e-02f, 1.61459251e-02f, 1.11311994e-02f, 6.03970466e-03f, 9.13695817e-04f, + -4.20433431e-03f, -9.27218149e-03f, -1.42480682e-02f, -1.90911878e-02f, -2.37618648e-02f, -2.82220093e-02f, + -3.24353766e-02f, -3.63678336e-02f, -3.99876924e-02f, -4.32659237e-02f, -4.61764207e-02f, -4.86961602e-02f, + -5.08054551e-02f, -5.24880386e-02f, -5.37312181e-02f, -5.45260166e-02f, -5.48671104e-02f, -5.47530531e-02f, + -5.41860463e-02f, -5.31721475e-02f, -5.17210363e-02f, -4.98459868e-02f, -4.75637647e-02f, -4.48944406e-02f, + -4.18612746e-02f, -3.84904206e-02f, -3.48107925e-02f, -3.08537797e-02f, -2.66529685e-02f, -2.22438695e-02f, + -1.76636682e-02f, -1.29507560e-02f, -8.14466071e-03f, -3.28544776e-03f, 1.58643018e-03f, 6.43050440e-03f, + 1.12067405e-02f, 1.58756642e-02f, 2.03989020e-02f, 2.47393345e-02f, 2.88614617e-02f, 3.27317634e-02f, + 3.63187992e-02f, 3.95936470e-02f, 4.25300387e-02f, 4.51045672e-02f, 4.72968940e-02f, 4.90899703e-02f, + 5.04700047e-02f, 5.14267809e-02f, 5.19535643e-02f, 5.20472034e-02f, 5.17082287e-02f, 5.09406434e-02f, + 4.97521048e-02f, 4.81537188e-02f, 4.61599131e-02f, 4.37884262e-02f, 4.10600706e-02f, 3.79985488e-02f, + 3.46302622e-02f, 3.09841217e-02f, 2.70912412e-02f, 2.29847199e-02f, 1.86992847e-02f, 1.42711599e-02f, + 9.73752669e-03f, 5.13643650e-03f, 5.06379454e-04f, -4.11408166e-03f, -8.68649476e-03f, -1.31729621e-02f, + -1.75363807e-02f, -2.17408089e-02f, -2.57516979e-02f, -2.95362143e-02f, -3.30635093e-02f, -3.63049622e-02f, + -3.92344048e-02f, -4.18283298e-02f, -4.40661418e-02f, -4.59301913e-02f, -4.74060505e-02f, -4.84825511e-02f, + -4.91518827e-02f, -4.94096235e-02f, -4.92548579e-02f, -4.86900251e-02f, -4.77210458e-02f, -4.63571741e-02f, + -4.46108878e-02f, -4.24979107e-02f, -4.00368564e-02f, -3.72492987e-02f, -3.41594108e-02f, -3.07938448e-02f, + -2.71814552e-02f, -2.33531198e-02f, -1.93413598e-02f, -1.51802063e-02f, -1.09048013e-02f, -6.55114338e-03f, + -2.15581014e-03f, 2.24443555e-03f, 6.61280814e-03f, 1.09129453e-02f, 1.51091980e-02f, 1.91667630e-02f, + 2.30522168e-02f, 2.67335907e-02f, 3.01807365e-02f, 3.33655579e-02f, 3.62622051e-02f, 3.88473226e-02f, + 4.11002204e-02f, 4.30030300e-02f, 4.45408790e-02f, 4.57019705e-02f, 4.64777109e-02f, 4.68627135e-02f, + 4.68549093e-02f, 4.64554958e-02f, 4.56689373e-02f, 4.45029599e-02f, 4.29683919e-02f, 4.10791386e-02f, + 3.88520159e-02f, 3.63066475e-02f, 3.34652385e-02f, 3.03523892e-02f, 2.69949681e-02f, 2.34217263e-02f, + 1.96632025e-02f, 1.57513974e-02f, 1.17194459e-02f, 7.60145677e-03f, 3.43215481e-03f, -7.53454950e-04f, + -4.92025229e-03f, -9.03345904e-03f, -1.30587503e-02f, -1.69627406e-02f, -2.07130441e-02f, -2.42787472e-02f, + -2.76304969e-02f, -3.07408842e-02f, -3.35845310e-02f, -3.61384026e-02f, -3.83819804e-02f, -4.02973364e-02f, + -4.18693911e-02f, -4.30859849e-02f, -4.39379525e-02f, -4.44192202e-02f, -4.45268207e-02f, -4.42609489e-02f, + -4.36249417e-02f, -4.26251693e-02f, -4.12710965e-02f, -3.95751119e-02f, -3.75524034e-02f, -3.52209020e-02f, + -3.26010732e-02f, -2.97156826e-02f, -2.65897306e-02f, -2.32501339e-02f, -1.97255230e-02f, -1.60459906e-02f, + -1.22428645e-02f, -8.34840613e-03f, -4.39555788e-03f, -4.17641093e-04f, 3.55186529e-03f, 7.47969548e-03f, + 1.13330289e-02f, 1.50796895e-02f, 1.86886063e-02f, 2.21298440e-02f, 2.53750227e-02f, 2.83974776e-02f, + 3.11724713e-02f, 3.36774564e-02f, 3.58921485e-02f, 3.77988281e-02f, 3.93823848e-02f, 4.06304645e-02f, + 4.15335460e-02f, 4.20850895e-02f, 4.22814530e-02f, 4.21220657e-02f, 4.16092724e-02f, 4.07484568e-02f, + 3.95478256e-02f, 3.80185099e-02f, 3.61742882e-02f, 3.40316228e-02f, 3.16093467e-02f, 2.89286854e-02f, + 2.60129143e-02f, 2.28872072e-02f, 1.95785162e-02f, 1.61151429e-02f, 1.25266872e-02f, 8.84367289e-03f, + 5.09737541e-03f, 1.31946573e-03f, -2.45819207e-03f, -6.20382907e-03f, -9.88599514e-03f, -1.34739714e-02f, + -1.69377975e-02f, -2.02487225e-02f, -2.33793144e-02f, -2.63038233e-02f, -2.89981802e-02f, -3.14404213e-02f, + -3.36107546e-02f, -3.54916723e-02f, -3.70682427e-02f, -3.83280672e-02f, -3.92614736e-02f, -3.98615776e-02f, + -4.01243243e-02f, -4.00484517e-02f, -3.96356708e-02f, -3.88903731e-02f, -3.78198781e-02f, -3.64341365e-02f, + -3.47457457e-02f, -3.27698392e-02f, -3.05238882e-02f, -2.80276282e-02f, -2.53028218e-02f, -2.23730957e-02f, + -1.92637467e-02f, -1.60015029e-02f, -1.26142882e-02f, -9.13104283e-03f, -5.58138981e-03f, -1.99542434e-03f, + 1.59649307e-03f, 5.16408174e-03f, 8.67737144e-03f, 1.21068581e-02f, 1.54239205e-02f, 1.86009100e-02f, + 2.16114772e-02f, 2.44306994e-02f, 2.70354163e-02f, 2.94042665e-02f, 3.15179985e-02f, 3.33595356e-02f, + 3.49141593e-02f, 3.61696229e-02f, 3.71161871e-02f, 3.77468512e-02f, 3.80571878e-02f, 3.80455485e-02f, + 3.77129900e-02f, 3.70632810e-02f, 3.61028508e-02f, 3.48407199e-02f, 3.32884428e-02f, 3.14600053e-02f, + 2.93716228e-02f, 2.70417408e-02f, 2.44907277e-02f, 2.17407576e-02f, 1.88156734e-02f, 1.57406803e-02f, + 1.25421761e-02f, 9.24754692e-03f, 5.88488640e-03f, 2.48280587e-03f, -9.29864758e-04f, -4.32426314e-03f, + -7.67179184e-03f, -1.09442952e-02f, -1.41143886e-02f, -1.71555974e-02f, -2.00425787e-02f, -2.27514891e-02f, + -2.52599054e-02f, -2.75472706e-02f, -2.95949315e-02f, -3.13863062e-02f, -3.29069832e-02f, -3.41450096e-02f, + -3.50907101e-02f, -3.57369992e-02f, -3.60793163e-02f, -3.61156751e-02f, -3.58467080e-02f, -3.52755740e-02f, + -3.44080617e-02f, -3.32523628e-02f, -3.18191314e-02f, -3.01213186e-02f, -2.81740846e-02f, -2.59946393e-02f, + -2.36021125e-02f, -2.10173975e-02f, -1.82629132e-02f, -1.53624700e-02f, -1.23410560e-02f, -9.22456599e-03f, + -6.03967755e-03f, -2.81350877e-03f, 4.26514319e-04f, 3.65292660e-03f, 6.83848944e-03f, 9.95638508e-03f, + 1.29804234e-02f, 1.58853076e-02f, 1.86468203e-02f, 2.12420277e-02f, 2.36494909e-02f, 2.58493792e-02f, + 2.78237450e-02f, 2.95565060e-02f, 3.10338053e-02f, 3.22438572e-02f, 3.31772716e-02f, 3.38269627e-02f, + 3.41883176e-02f, 3.42591610e-02f, 3.40397435e-02f, 3.35328606e-02f, 3.27436351e-02f, 3.16796573e-02f, + 3.03507246e-02f, 2.87689689e-02f, 2.69484839e-02f, 2.49054827e-02f, 2.26579086e-02f, 2.02254442e-02f, + 1.76292617e-02f, 1.48918382e-02f, 1.20368159e-02f, 9.08872468e-03f, 6.07283273e-03f, 3.01489838e-03f, + -5.90212194e-05f, -3.12287666e-03f, -6.15069532e-03f, -9.11695091e-03f, -1.19967033e-02f, -1.47657868e-02f, + -1.74011004e-02f, -1.98807214e-02f, -2.21841025e-02f, -2.42922632e-02f, -2.61879368e-02f, -2.78557311e-02f, + -2.92821801e-02f, -3.04559562e-02f, -3.13678907e-02f, -3.20110632e-02f, -3.23808087e-02f, -3.24749193e-02f, + -3.22933847e-02f, -3.18386269e-02f, -3.11153366e-02f, -3.01304804e-02f, -2.88932552e-02f, -2.74148734e-02f, + -2.57086673e-02f, -2.37898314e-02f, -2.16752343e-02f, -1.93835013e-02f, -1.69345799e-02f, -1.43497284e-02f, + -1.16513243e-02f, -8.86259097e-03f, -6.00748525e-03f, -3.11044903e-03f, -1.96143386e-04f, 2.71056658e-03f, + 5.58512222e-03f, 8.40318833e-03f, 1.11410160e-02f, 1.37756382e-02f, 1.62850338e-02f, 1.86482666e-02f, + 2.08457445e-02f, 2.28593437e-02f, 2.46725329e-02f, 2.62705694e-02f, 2.76405329e-02f, 2.87715470e-02f, + 2.96547092e-02f, 3.02833419e-02f, 3.06529059e-02f, 3.07610441e-02f, 3.06076742e-02f, 3.01949567e-02f, + 2.95271502e-02f, 2.86107876e-02f, 2.74543883e-02f, 2.60685701e-02f, 2.44657863e-02f, 2.26603655e-02f, + 2.06682557e-02f, 1.85070033e-02f, 1.61954603e-02f, 1.37537720e-02f, 1.12030588e-02f, 8.56537064e-03f, + 5.86336215e-03f, 3.12021752e-03f, 3.59345288e-04f, -2.39571357e-03f, -5.12158252e-03f, -7.79518527e-03f, + -1.03939536e-02f, -1.28961026e-02f, -1.52805838e-02f, -1.75275761e-02f, -1.96183935e-02f, -2.15357712e-02f, + -2.32639542e-02f, -2.47888545e-02f, -2.60981899e-02f, -2.71814567e-02f, -2.80302370e-02f, -2.86380088e-02f, + -2.90003996e-02f, -2.91151172e-02f, -2.89819544e-02f, -2.86028697e-02f, -2.79818317e-02f, -2.71249297e-02f, + -2.60401957e-02f, -2.47375751e-02f, -2.32288414e-02f, -2.15275091e-02f, -1.96486443e-02f, -1.76087964e-02f, + -1.54258426e-02f, -1.31187994e-02f, -1.07076937e-02f, -8.21335282e-03f, -5.65730582e-03f, -3.06143405e-03f, + -4.47990175e-04f, 2.16074548e-03f, 4.74260737e-03f, 7.27569124e-03f, 9.73864733e-03f, 1.21106824e-02f, + 1.43719841e-02f, 1.65036001e-02f, 1.84878471e-02f, 2.03083286e-02f, 2.19500531e-02f, 2.33996493e-02f, + 2.46453861e-02f, 2.56773512e-02f, 2.64874345e-02f, 2.70694463e-02f, 2.74192279e-02f, 2.75344951e-02f, + 2.74150667e-02f, 2.70627089e-02f, 2.64811913e-02f, 2.56761950e-02f, 2.46553112e-02f, 2.34279326e-02f, + 2.20051823e-02f, 2.03998041e-02f, 1.86260730e-02f, 1.66996483e-02f, 1.46373888e-02f, 1.24573628e-02f, + 1.01784699e-02f, 7.82046099e-03f, 5.40366356e-03f, 2.94886537e-03f, 4.77074685e-04f, -1.99056008e-03f, + -4.43309957e-03f, -6.82975366e-03f, -9.16032780e-03f, -1.14051392e-02f, -1.35453571e-02f, -1.55631186e-02f, + -1.74416221e-02f, -1.91653203e-02f, -2.07200521e-02f, -2.20931290e-02f, -2.32734389e-02f, -2.42515770e-02f, + -2.50198790e-02f, -2.55724740e-02f, -2.59053977e-02f, -2.60165073e-02f, -2.59056121e-02f, -2.55744100e-02f, + -2.50263861e-02f, -2.42670139e-02f, -2.33034172e-02f, -2.21444752e-02f, -2.08007704e-02f, -1.92843016e-02f, + -1.76086143e-02f, -1.57885066e-02f, -1.38399632e-02f, -1.17800468e-02f, -9.62665505e-03f, -7.39846180e-03f, + -5.11473979e-03f, -2.79509520e-03f, -4.59475153e-04f, 1.87219411e-03f, 4.18004886e-03f, 6.44446028e-03f, + 8.64630036e-03f, 1.07670050e-02f, 1.27887263e-02f, 1.46946183e-02f, 1.64687696e-02f, 1.80965074e-02f, + 1.95644657e-02f, 2.08606409e-02f, 2.19745569e-02f, 2.28973400e-02f, 2.36217678e-02f, 2.41423032e-02f, + 2.44552329e-02f, 2.45585559e-02f, 2.44521268e-02f, 2.41375247e-02f, 2.36181843e-02f, 2.28991883e-02f, + 2.19873596e-02f, 2.08911372e-02f, 1.96204854e-02f, 1.81868423e-02f, 1.66029686e-02f, 1.48829260e-02f, + 1.30418196e-02f, 1.10957823e-02f, 9.06176569e-03f, 6.95742371e-03f, 4.80095797e-03f, 2.61094572e-03f, + 4.06163422e-04f, -1.79448120e-03f, -3.97227507e-03f, -6.10867089e-03f, -8.18559133e-03f, -1.01855447e-02f, + -1.20916775e-02f, -1.38880736e-02f, -1.55597947e-02f, -1.70929424e-02f, -1.84749792e-02f, -1.96945768e-02f, + -2.07419008e-02f, -2.16086011e-02f, -2.22879060e-02f, -2.27746496e-02f, -2.30653527e-02f, -2.31582122e-02f, + -2.30530853e-02f, -2.27516002e-02f, -2.22569518e-02f, -2.15740851e-02f, -2.07094459e-02f, -1.96710504e-02f, + -1.84683607e-02f, -1.71122258e-02f, -1.56147530e-02f, -1.39891960e-02f, -1.22499260e-02f, -1.04121226e-02f, + -8.49187069e-03f, -6.50583812e-03f, -4.47121574e-03f, -2.40553061e-03f, -3.26560349e-04f, 1.74792849e-03f, + 3.80020986e-03f, 5.81284812e-03f, 7.76878436e-03f, 9.65152189e-03f, 1.14452321e-02f, 1.31348903e-02f, + 1.47064602e-02f, 1.61469015e-02f, 1.74443880e-02f, 1.85883329e-02f, 1.95694960e-02f, 2.03800747e-02f, + 2.10137416e-02f, 2.14657028e-02f, 2.17327470e-02f, 2.18132189e-02f, 2.17071096e-02f, 2.14159688e-02f, + 2.09429396e-02f, 2.02927056e-02f, 1.94714591e-02f, 1.84867806e-02f, 1.73476996e-02f, 1.60644888e-02f, + 1.46486021e-02f, 1.31126305e-02f, 1.14700918e-02f, 9.73543186e-03f, 7.92379251e-03f, 6.05090462e-03f, + 4.13301608e-03f, 2.18669055e-03f, 2.28581333e-04f, -1.72441072e-03f, -3.65572200e-03f, -5.54887990e-03f, + -7.38782061e-03f, -9.15706782e-03f, -1.08417082e-02f, -1.24276657e-02f, -1.39017311e-02f, -1.52516970e-02f, + -1.64664949e-02f, -1.75361817e-02f, -1.84521823e-02f, -1.92071599e-02f, -1.97953056e-02f, -2.02121243e-02f, + -2.04547147e-02f, -2.05216098e-02f, -2.04128534e-02f, -2.01300439e-02f, -1.96761990e-02f, -1.90558123e-02f, + -1.82748056e-02f, -1.73404276e-02f, -1.62612067e-02f, -1.50469098e-02f, -1.37084115e-02f, -1.22575769e-02f, + -1.07072432e-02f, -9.07102930e-03f, -7.36320826e-03f, -5.59869147e-03f, -3.79270806e-03f, -1.96092013e-03f, + -1.19027325e-04f, 1.71713152e-03f, 3.53191747e-03f, 5.30986343e-03f, 7.03590331e-03f, 8.69547560e-03f, + 1.02746006e-02f, 1.17601122e-02f, 1.31396009e-02f, 1.44016653e-02f, 1.55359973e-02f, 1.65332483e-02f, + 1.73855033e-02f, 1.80859434e-02f, 1.86291305e-02f, 1.90110277e-02f, 1.92289384e-02f, 1.92815880e-02f, + 1.91691688e-02f, 1.88932135e-02f, 1.84567183e-02f, 1.78639790e-02f, 1.71206377e-02f, 1.62336473e-02f, + 1.52110920e-02f, 1.40622274e-02f, 1.27973510e-02f, 1.14277163e-02f, 9.96541843e-03f, 8.42333112e-03f, + 6.81491991e-03f, 5.15420944e-03f, 3.45559138e-03f, 1.73374462e-03f, 3.49154958e-06f, -1.72033182e-03f, + -3.42300908e-03f, -5.09002877e-03f, -6.70728983e-03f, -8.26110592e-03f, -9.73843101e-03f, -1.11269177e-02f, + -1.24149972e-02f, -1.35920411e-02f, -1.46483675e-02f, -1.55754162e-02f, -1.63657097e-02f, -1.70130158e-02f, + -1.75123254e-02f, -1.78599156e-02f, -1.80533642e-02f, -1.80916471e-02f, -1.79749596e-02f, -1.77049199e-02f, + -1.72844059e-02f, -1.67175734e-02f, -1.60098348e-02f, -1.51677846e-02f, -1.41991369e-02f, -1.31126308e-02f, + -1.19180614e-02f, -1.06260158e-02f, -9.24795820e-03f, -7.79599691e-03f, -6.28282689e-03f, -4.72166017e-03f, + -3.12602130e-03f, -1.50971188e-03f, 1.13358008e-04f, 1.72924640e-03f, 3.32419869e-03f, 4.88457483e-03f, + 6.39719332e-03f, 7.84928507e-03f, 9.22860374e-03f, 1.05236737e-02f, 1.17237027e-02f, 1.28187631e-02f, + 1.37999219e-02f, 1.46591627e-02f, 1.53896448e-02f, 1.59855771e-02f, 1.64423748e-02f, 1.67566705e-02f, + 1.69263151e-02f, 1.69504088e-02f, 1.68293192e-02f, 1.65646048e-02f, 1.61591292e-02f, 1.56168830e-02f, + 1.49430466e-02f, 1.41438870e-02f, 1.32267343e-02f, 1.21999194e-02f, 1.10726150e-02f, 9.85491162e-03f, + 8.55755480e-03f, 7.19198626e-03f, 5.77013714e-03f, 4.30443841e-03f, 2.80758857e-03f, 1.29252809e-03f, + -2.27683018e-04f, -1.74000213e-03f, -3.23153173e-03f, -4.68956247e-03f, -6.10171563e-03f, -7.45612506e-03f, + -8.74136426e-03f, -9.94672023e-03f, -1.10621909e-02f, -1.20785406e-02f, -1.29874795e-02f, -1.37816456e-02f, + -1.44546479e-02f, -1.50012468e-02f, -1.54172106e-02f, -1.56995155e-02f, -1.58462779e-02f, -1.58567437e-02f, + -1.57313825e-02f, -1.54717967e-02f, -1.50807184e-02f, -1.45620705e-02f, -1.39207297e-02f, -1.31627253e-02f, + -1.22950111e-02f, -1.13254027e-02f, -1.02626834e-02f, -9.11627932e-03f, -7.89634415e-03f, -6.61364765e-03f, + -5.27939952e-03f, -3.90525708e-03f, -2.50314317e-03f, -1.08517576e-03f, 3.36418391e-04f, 1.74945190e-03f, + 3.14186033e-03f, 4.50178261e-03f, 5.81769448e-03f, 7.07851939e-03f, 8.27365386e-03f, 9.39310326e-03f, + 1.04276320e-02f, 1.13686527e-02f, 1.22085379e-02f, 1.29404450e-02f, 1.35585678e-02f, 1.40580446e-02f, + 1.44350939e-02f, 1.46869568e-02f, 1.48120098e-02f, 1.48096348e-02f, 1.46804295e-02f, 1.44259781e-02f, + 1.40489668e-02f, 1.35531325e-02f, 1.29432014e-02f, 1.22248563e-02f, 1.14046959e-02f, 1.04901687e-02f, + 9.48948107e-03f, 8.41156632e-03f, 7.26596347e-03f, 6.06280447e-03f, 4.81257444e-03f, 3.52622627e-03f, + 2.21492506e-03f, 8.89983592e-04f, -4.37153812e-04f, -1.75513167e-03f, -3.05265494e-03f, -4.31872834e-03f, + -5.54261874e-03f, -6.71396264e-03f, -7.82302244e-03f, -8.86045250e-03f, -9.81773278e-03f, -1.06869351e-02f, + -1.14610023e-02f, -1.21336754e-02f, -1.26995953e-02f, -1.31543908e-02f, -1.34945718e-02f, -1.37177266e-02f, + -1.38224110e-02f, -1.38082286e-02f, -1.36757739e-02f, -1.34266887e-02f, -1.30635886e-02f, -1.25900369e-02f, + -1.20105709e-02f, -1.13305978e-02f, -1.05563538e-02f, -9.69485926e-03f, -8.75389081e-03f, -7.74181164e-03f, + -6.66761679e-03f, -5.54076187e-03f, -4.37111830e-03f, -3.16893052e-03f, -1.94457115e-03f, -7.08705149e-04f, + 5.28079290e-04f, 1.75515870e-03f, 2.96204304e-03f, 4.13848585e-03f, 5.27451557e-03f, 6.36060039e-03f, + 7.38755863e-03f, 8.34692530e-03f, 9.23070802e-03f, 1.00316534e-02f, 1.07432528e-02f, 1.13597680e-02f, + 1.18763350e-02f, 1.22889283e-02f, 1.25944631e-02f, 1.27907515e-02f, 1.28765994e-02f, 1.28517102e-02f, + 1.27167966e-02f, 1.24734480e-02f, 1.21242371e-02f, 1.16725839e-02f, 1.11228281e-02f, 1.04800592e-02f, + 9.75022575e-03f, 8.93990424e-03f, 8.05644990e-03f, 7.10768601e-03f, 6.10205625e-03f, 5.04843878e-03f, + 3.95605458e-03f, 2.83441418e-03f, 1.69331277e-03f, 5.42568186e-04f, -6.07877124e-04f, -1.74818575e-03f, + -2.86860405e-03f, -3.95962685e-03f, -5.01201657e-03f, -6.01690058e-03f, -6.96589716e-03f, -7.85110424e-03f, + -8.66518231e-03f, -9.40145619e-03f, -1.00540095e-02f, -1.06175123e-02f, -1.10876024e-02f, -1.14606062e-02f, + -1.17337519e-02f, -1.19051415e-02f, -1.19737311e-02f, -1.19393909e-02f, -1.18028751e-02f, -1.15657387e-02f, + -1.12305357e-02f, -1.08005049e-02f, -1.02797519e-02f, -9.67318729e-03f, -8.98632838e-03f, -8.22543877e-03f, + -7.39737215e-03f, -6.50950785e-03f, -5.56975395e-03f, -4.58632875e-03f, -3.56792674e-03f, -2.52340823e-03f, + -1.46183597e-03f, -3.92391156e-04f, 6.75701684e-04f, 1.73331709e-03f, 2.77141530e-03f, 3.78118353e-03f, + 4.75407672e-03f, 5.68193005e-03f, 6.55698994e-03f, 7.37195674e-03f, 8.12013345e-03f, 8.79539509e-03f, + 9.39225030e-03f, 9.90597190e-03f, 1.03324819e-02f, 1.06685242e-02f, 1.09116177e-02f, 1.10600973e-02f, + 1.11130936e-02f, 1.10705983e-02f, 1.09333788e-02f, 1.07030445e-02f, 1.03819949e-02f, 9.97335332e-03f, + 9.48107464e-03f, 8.90968434e-03f, 8.26449756e-03f, 7.55132972e-03f, 6.77664458e-03f, 5.94731079e-03f, + 5.07073939e-03f, 4.15462520e-03f, 3.20700306e-03f, 2.23616222e-03f, 1.25050340e-03f, 2.58592562e-04f, + -7.31105992e-04f, -1.71003848e-03f, -2.66991104e-03f, -3.60254805e-03f, -4.50009626e-03f, -5.35500152e-03f, + -6.16013372e-03f, -6.90880302e-03f, -7.59484887e-03f, -8.21267759e-03f, -8.75730297e-03f, -9.22437062e-03f, + -9.61022818e-03f, -9.91196266e-03f, -1.01273334e-02f, -1.02549146e-02f, -1.02939949e-02f, -1.02446487e-02f, + -1.01077102e-02f, -9.88473930e-03f, -9.57804506e-03f, -9.19065219e-03f, -8.72623997e-03f, -8.18914967e-03f, + -7.58431711e-03f, -6.91725624e-03f, -6.19393169e-03f, -5.42085678e-03f, -4.60486090e-03f, -3.75314479e-03f, + -2.87318400e-03f, -1.97263669e-03f, -1.05936420e-03f, -1.41184633e-04f, 7.73935206e-04f, 1.67818033e-03f, + 2.56387121e-03f, 3.42348245e-03f, 4.24972968e-03f, 5.03575853e-03f, 5.77493594e-03f, 6.46117800e-03f, + 7.08885263e-03f, 7.65282423e-03f, 8.14856911e-03f, 8.57214716e-03f, 8.92027019e-03f, 9.19029194e-03f, + 9.38027470e-03f, 9.48895025e-03f, 9.51578399e-03f, 9.46091429e-03f, 9.32518284e-03f, 9.11016180e-03f, + 8.81806173e-03f, 8.45171440e-03f, 8.01466407e-03f, 7.51094572e-03f, 6.94521826e-03f, 6.32261691e-03f, + 5.64875255e-03f, 4.92963671e-03f, 4.17165548e-03f, 3.38149573e-03f, 2.56610069e-03f, 1.73253154e-03f, + 8.88083719e-04f, 4.00140997e-05f, -8.04377007e-04f, -1.63786496e-03f, -2.45336348e-03f, -3.24394120e-03f, + -4.00297149e-03f, -4.72406012e-03f, -5.40122825e-03f, -6.02886353e-03f, -6.60184564e-03f, -7.11547043e-03f, + -7.56567204e-03f, -7.94886879e-03f, -8.26207948e-03f, -8.50298133e-03f, -8.66984745e-03f, -8.76158174e-03f, + -8.77778600e-03f, -8.71866903e-03f, -8.58510255e-03f, -8.37858953e-03f, -8.10125332e-03f, -7.75580633e-03f, + -7.34555568e-03f, -6.87431135e-03f, -6.34642360e-03f, -5.76669768e-03f, -5.14031767e-03f, -4.47294897e-03f, + -3.77043291e-03f, -3.03903272e-03f, -2.28511456e-03f, -1.51527024e-03f, -7.36178447e-04f, 4.54225562e-05f, + 8.22859022e-04f, 1.58943109e-03f, 2.33866278e-03f, 3.06420334e-03f, 3.75990680e-03f, 4.42002538e-03f, + 5.03901750e-03f, 5.61180111e-03f, 6.13366220e-03f, 6.60043272e-03f, 7.00831931e-03f, 7.35414500e-03f, + 7.63524392e-03f, 7.84953557e-03f, 7.99547645e-03f, 8.07218955e-03f, 8.07933095e-03f, 8.01721906e-03f, + 7.88666864e-03f, 7.68919343e-03f, 7.42679720e-03f, 7.10202788e-03f, 6.71802523e-03f, 6.27832934e-03f, + 5.78702253e-03f, 5.24853339e-03f, 4.66776048e-03f, 4.04985033e-03f, 3.40032055e-03f, 2.72486114e-03f, + 2.02943382e-03f, 1.32005555e-03f, 6.02922229e-04f, -1.15810889e-04f, -8.29962401e-04f, -1.53344695e-03f, + -2.22024937e-03f, -2.88460828e-03f, -3.52090915e-03f, -4.12386103e-03f, -4.68844782e-03f, -5.21000854e-03f, + -5.68433641e-03f, -6.10753890e-03f, -6.47629357e-03f, -6.78770430e-03f, -7.03936807e-03f, -7.22944790e-03f, + -7.35662441e-03f, -7.42012069e-03f, -7.41971164e-03f, -7.35573757e-03f, -7.22905724e-03f, -7.04107429e-03f, + -6.79370122e-03f, -6.48940038e-03f, -6.13102314e-03f, -5.72192873e-03f, -5.26590521e-03f, -4.76707464e-03f, + -4.22993214e-03f, -3.65930825e-03f, -3.06022345e-03f, -2.43797793e-03f, -1.79803310e-03f, -1.14594988e-03f, + -4.87389180e-04f, 1.71985886e-04f, 8.26505744e-04f, 1.47057292e-03f, 2.09875564e-03f, 2.70572827e-03f, + 3.28638788e-03f, 3.83592350e-03f, 4.34975506e-03f, 4.82368759e-03f, 5.25383132e-03f, 5.63677359e-03f, + 5.96942535e-03f, 6.24924092e-03f, 6.47405650e-03f, 6.64226721e-03f, 6.75269253e-03f, 6.80469430e-03f, + 6.79815717e-03f, 6.73340631e-03f, 6.61130455e-03f, 6.43322863e-03f, 6.20094526e-03f, 5.91677710e-03f, + 5.58340169e-03f, 5.20393196e-03f, 4.78187614e-03f, 4.32106320e-03f, 3.82565711e-03f, 3.30005613e-03f, + 2.74895362e-03f, 2.17719303e-03f, 1.58978015e-03f, 9.91844057e-04f, 3.88540330e-04f, -2.14916878e-04f, + -8.13361192e-04f, -1.40168257e-03f, -1.97489740e-03f, -2.52818059e-03f, -3.05688539e-03f, -3.55662656e-03f, + -4.02326574e-03f, -4.45296958e-03f, -4.84228652e-03f, -5.18803438e-03f, -5.48755315e-03f, -5.73848611e-03f, + -5.93891991e-03f, -6.08745626e-03f, -6.18305471e-03f, -6.22520840e-03f, -6.21382472e-03f, -6.14928419e-03f, + -6.03244633e-03f, -5.86455879e-03f, -5.64736180e-03f, -5.38296537e-03f, -5.07389363e-03f, -4.72301916e-03f, + -4.33361321e-03f, -3.90915761e-03f, -3.45353173e-03f, -2.97077347e-03f, -2.46516689e-03f, -1.94119584e-03f, + -1.40340595e-03f, -8.56512644e-04f, -3.05232133e-04f, 2.45691031e-04f, 7.91538060e-04f, 1.32763724e-03f, + 1.84949345e-03f, 2.35267547e-03f, 2.83299113e-03f, 3.28645035e-03f, 3.70931698e-03f, 4.09812665e-03f, + 4.44973511e-03f, 4.76135341e-03f, 5.03050354e-03f, 5.25513155e-03f, 5.43353323e-03f, 5.56447821e-03f, + 5.64705544e-03f, 5.68083601e-03f, 5.66583437e-03f, 5.60238431e-03f, 5.49135375e-03f, 5.33391723e-03f, + 5.13169207e-03f, 4.88664671e-03f, 4.60113202e-03f, 4.27780860e-03f, 3.91964875e-03f, 3.52989866e-03f, + 3.11212090e-03f, 2.66999053e-03f, 2.20744344e-03f, 1.72859110e-03f, 1.23756351e-03f, 7.38678150e-04f, + 2.36236760e-04f, -2.65462378e-04f, -7.62072815e-04f, -1.24943395e-03f, -1.72337956e-03f, -2.17993754e-03f, + -2.61530935e-03f, -3.02588421e-03f, -3.40825196e-03f, -3.75935360e-03f, -4.07630652e-03f, -4.35660760e-03f, + -4.59808398e-03f, -4.79883718e-03f, -4.95743843e-03f, -5.07271280e-03f, -5.14393833e-03f, -5.17077608e-03f, + -5.15318763e-03f, -5.09164480e-03f, -4.98686807e-03f, -4.84002285e-03f, -4.65260103e-03f, -4.42642977e-03f, + -4.16366446e-03f, -3.86678300e-03f, -3.53847751e-03f, -3.18177292e-03f, -2.79986847e-03f, -2.39618401e-03f, + -1.97429017e-03f, -1.53788782e-03f, -1.09083664e-03f, -6.36973406e-04f, -1.80264329e-04f, 2.75399352e-04f, + 7.26104424e-04f, 1.16802598e-03f, 1.59744046e-03f, 2.01073128e-03f, 2.40446819e-03f, 2.77538562e-03f, + 3.12044615e-03f, 3.43683203e-03f, 3.72202393e-03f, 3.97374850e-03f, 4.19002854e-03f, 4.36925418e-03f, + 4.51006070e-03f, 4.61152219e-03f, 4.67293053e-03f, 4.69404975e-03f, 4.67490366e-03f, 4.61589307e-03f, + 4.51775252e-03f, 4.38154991e-03f, 4.20868532e-03f, 4.00082377e-03f, 3.75997274e-03f, 3.48836415e-03f, + 3.18851504e-03f, 2.86314343e-03f, 2.51519536e-03f, 2.14776743e-03f, 1.76411750e-03f, 1.36763070e-03f, + 9.61751835e-04f, 5.50052405e-04f, 1.36015058e-04f, -2.76720943e-04f, -6.84698152e-04f, -1.08442387e-03f, + -1.47253691e-03f, -1.84578853e-03f, -2.20105818e-03f, -2.53544188e-03f, -2.84616998e-03f, -3.13076058e-03f, + -3.38689733e-03f, -3.61260297e-03f, -3.80606518e-03f, -3.96589267e-03f, -4.09087232e-03f, -4.18013173e-03f, + -4.23315965e-03f, -4.24970953e-03f, -4.22981560e-03f, -4.17392494e-03f, -4.08267808e-03f, -3.95709577e-03f, + -3.79845153e-03f, -3.60829670e-03f, -3.38844338e-03f, -3.14094669e-03f, -2.86809742e-03f, -2.57237442e-03f, + -2.25643831e-03f, -1.92312165e-03f, -1.57535841e-03f, -1.21624129e-03f, -8.48868370e-04f, -4.76457354e-04f, + -1.02227062e-04f, 2.70659894e-04f, 6.38948957e-04f, 9.99596773e-04f, 1.34950884e-03f, 1.68579412e-03f, + 2.00565112e-03f, 2.30644176e-03f, 2.58570970e-03f, 2.84121989e-03f, 3.07087670e-03f, 3.27296771e-03f, + 3.44584695e-03f, 3.58825627e-03f, 3.69915439e-03f, 3.77779535e-03f, 3.82369144e-03f, 3.83666312e-03f, + 3.81678507e-03f, 3.76444486e-03f, 3.68027755e-03f, 3.56519883e-03f, 3.42038694e-03f, 3.24725992e-03f, + 3.04745181e-03f, 2.82287635e-03f, 2.57555610e-03f, 2.30778342e-03f, 2.02193938e-03f, 1.72060684e-03f, + 1.40642226e-03f, 1.08218540e-03f, 7.50708128e-04f, 4.14852040e-04f, 7.75468400e-05f, -2.58336678e-04f, + -5.89954675e-04f, -9.14464553e-04f, -1.22917409e-03f, -1.53142096e-03f, -1.81874942e-03f, -2.08875765e-03f, + -2.33925204e-03f, -2.56824046e-03f, -2.77387464e-03f, -2.95457151e-03f, -3.10891286e-03f, -3.23576957e-03f, + -3.33422309e-03f, -3.40361730e-03f, -3.44352432e-03f, -3.45380945e-03f, -3.43454926e-03f, -3.38612359e-03f, + -3.30910238e-03f, -3.20434413e-03f, -3.07289782e-03f, -2.91605448e-03f, -2.73534798e-03f, -2.53242439e-03f, + -2.30918427e-03f, -2.06766744e-03f, -1.81002532e-03f, -1.53857461e-03f, -1.25572213e-03f, -9.63956082e-04f, + -6.65804929e-04f, -3.63875198e-04f, -6.07622519e-05f, 2.40955893e-04f, 5.38685581e-04f, 8.29936911e-04f, + 1.11224977e-03f, 1.38328230e-03f, 1.64080028e-03f, 1.88265574e-03f, 2.10694670e-03f, 2.31181334e-03f, + 2.49567938e-03f, 2.65707799e-03f, 2.79477329e-03f, 2.90778929e-03f, 2.99526804e-03f, 3.05666792e-03f, + 3.09159989e-03f, 3.09996074e-03f, 3.08183486e-03f, 3.03757314e-03f, 2.96768997e-03f, 2.87296391e-03f, + 2.75438271e-03f, 2.61305979e-03f, 2.45041225e-03f, 2.26792371e-03f, 2.06728115e-03f, 1.85034398e-03f, + 1.61901728e-03f, 1.37543970e-03f, 1.12168235e-03f, 8.60048928e-04f, 5.92781787e-04f, 3.22217129e-04f, + 5.06437951e-05f, -2.19547817e-04f, -4.86132510e-04f, -7.46817210e-04f, -9.99443627e-04f, -1.24188233e-03f, + -1.47217245e-03f, -1.68839648e-03f, -1.88883105e-03f, -2.07184785e-03f, -2.23601745e-03f, -2.38006048e-03f, + -2.50288118e-03f, -2.60358292e-03f, -2.68144174e-03f, -2.73595307e-03f, -2.76679595e-03f, -2.77388624e-03f, + -2.75729794e-03f, -2.71735188e-03f, -2.65451985e-03f, -2.56952130e-03f, -2.46319204e-03f, -2.33660956e-03f, + -2.19096493e-03f, -2.02765268e-03f, -1.84815939e-03f, -1.65412932e-03f, -1.44731483e-03f, -1.22956426e-03f, + -1.00280075e-03f, -7.69022668e-04f, -5.30268510e-04f, -2.88586883e-04f, -4.60956253e-05f, 1.95186584e-04f, + 4.33161045e-04f, 6.65873263e-04f, 8.91328897e-04f, 1.10770620e-03f, 1.31316296e-03f, 1.50610067e-03f, + 1.68489795e-03f, 1.84814923e-03f, 1.99458512e-03f, 2.12304250e-03f, 2.23258384e-03f, 2.32237953e-03f, + 2.39181962e-03f, 2.44043032e-03f, 2.46796938e-03f, 2.47430968e-03f, 2.45957831e-03f, 2.42401283e-03f, + 2.36808884e-03f, 2.29238471e-03f, 2.19773378e-03f, 2.08501666e-03f, 1.95534528e-03f, 1.80993801e-03f, + 1.65014053e-03f, 1.47739854e-03f, 1.29329221e-03f, 1.09944593e-03f, 8.97596290e-04f, 6.89486470e-04f, + 4.76967544e-04f, 2.61847472e-04f, 4.59979030e-05f, -1.68770369e-04f, -3.80612759e-04f, -5.87744421e-04f, + -7.88452414e-04f, -9.81081718e-04f, -1.16402219e-03f, -1.33580811e-03f, -1.49504859e-03f, -1.64047131e-03f, + -1.77095587e-03f, -1.88548340e-03f, -1.98318254e-03f, -2.06335667e-03f, -2.12544333e-03f, -2.16903096e-03f, + -2.19389731e-03f, -2.19994674e-03f, -2.18726700e-03f, -2.15609170e-03f, -2.10683457e-03f, -2.04002290e-03f, + -1.95633800e-03f, -1.85665258e-03f, -1.74189023e-03f, -1.61313165e-03f, -1.47159921e-03f, -1.31856217e-03f, + -1.15541374e-03f, -9.83590913e-04f, -8.04645529e-04f, -6.20138811e-04f, -4.31664744e-04f, -2.40859759e-04f, + -4.93718861e-05f, 1.41183920e-04f, 3.29184443e-04f, 5.13049545e-04f, 6.91252710e-04f, 8.62329668e-04f, + 1.02486089e-03f, 1.17753306e-03f, 1.31912530e-03f, 1.44851584e-03f, 1.56468190e-03f, 1.66675270e-03f, + 1.75393226e-03f, 1.82562545e-03f, 1.88129935e-03f, 1.92062935e-03f, 1.94336360e-03f, 1.94946381e-03f, + 1.93898469e-03f, 1.91211060e-03f, 1.86925265e-03f, 1.81081128e-03f, 1.73745800e-03f, 1.64989979e-03f, + 1.54896085e-03f, 1.43565148e-03f, 1.31095906e-03f, 1.17607031e-03f, 1.03219054e-03f, 8.80596006e-04f, + 7.22634695e-04f, 5.59715925e-04f, 3.93223384e-04f, 2.24602808e-04f, 5.53223372e-05f, -1.13204206e-04f, + -2.79527886e-04f, -4.42273875e-04f, -6.00090187e-04f, -7.51646708e-04f, -8.95738714e-04f, -1.03117771e-03f, + -1.15687770e-03f, -1.27187587e-03f, -1.37523688e-03f, -1.46618576e-03f, -1.54403989e-03f, -1.60825931e-03f, + -1.65836399e-03f, -1.69405240e-03f, -1.71514183e-03f, -1.72154028e-03f, -1.71331327e-03f, -1.69063272e-03f, + -1.65381037e-03f, -1.60326168e-03f, -1.53948863e-03f, -1.46318779e-03f, -1.37503217e-03f, -1.27591969e-03f, + -1.16672308e-03f, -1.04846883e-03f, -9.22232848e-04f, -7.89108246e-04f, -6.50329911e-04f, -5.07057241e-04f, + -3.60579584e-04f, -2.12138548e-04f, -6.30166060e-05f, 8.55107333e-05f, 2.32212191e-04f, 3.75851456e-04f, + 5.15213418e-04f, 6.49182851e-04f, 7.76642588e-04f, 8.96585347e-04f, 1.00803198e-03f, 1.11010987e-03f, + 1.20203475e-03f, 1.28308439e-03f, 1.35268783e-03f, 1.41030687e-03f, 1.45558664e-03f, 1.48819124e-03f, + 1.50798717e-03f, 1.51486502e-03f, 1.50888467e-03f, 1.49022209e-03f, 1.45906012e-03f, 1.41583581e-03f, + 1.36095722e-03f, 1.29499749e-03f, 1.21859138e-03f, 1.13249419e-03f, 1.03745344e-03f, 9.34384957e-04f, + 8.24209226e-04f, 7.07921644e-04f, 5.86535461e-04f, 4.61118668e-04f, 3.32797940e-04f, 2.02615430e-04f, + 7.17560319e-05f, -5.87215139e-05f, -1.87700771e-04f, -3.14093799e-04f, -4.36855019e-04f, -5.54982470e-04f, + -6.67514567e-04f, -7.73539543e-04f, -8.72216549e-04f, -9.62754726e-04f, -1.04446836e-03f, -1.11673823e-03f, + -1.17901020e-03f, -1.23084835e-03f, -1.27191263e-03f, -1.30189831e-03f, -1.32066941e-03f, -1.32816613e-03f, + -1.32437715e-03f, -1.30944714e-03f, -1.28360668e-03f, -1.24710492e-03f, -1.20038313e-03f, -1.14391116e-03f, + -1.07822250e-03f, -1.00394823e-03f, -9.21799577e-04f, -8.32520513e-04f, -7.36916195e-04f, -6.35853312e-04f, + -5.30218398e-04f, -4.20950684e-04f, -3.08981087e-04f, -1.95310152e-04f, -8.08721649e-05f, 3.33481785e-05f, + 1.46369769e-04f, 2.57271691e-04f, 3.65123878e-04f, 4.69053422e-04f, 5.68205019e-04f, 6.61777482e-04f, + 7.49035427e-04f, 8.29295760e-04f, 9.01919035e-04f, 9.66370937e-04f, 1.02218113e-03f, 1.06892877e-03f, + 1.10630552e-03f, 1.13406370e-03f, 1.15204451e-03f, 1.16019052e-03f, 1.15848806e-03f, 1.14706630e-03f, + 1.12606449e-03f, 1.09574589e-03f, 1.05645362e-03f, 1.00859266e-03f, 9.52601766e-04f, 8.89057609e-04f, + 8.18535938e-04f, 7.41697389e-04f, 6.59241262e-04f, 5.71884368e-04f, 4.80414698e-04f, 3.85677252e-04f, + 2.88406796e-04f, 1.89536836e-04f, 8.98491837e-05f, -9.79888746e-06f, -1.08531507e-04f, -2.05575498e-04f, + -3.00092231e-04f, -3.91327952e-04f, -4.78537671e-04f, -5.61003964e-04f, -6.38090388e-04f, -7.09209697e-04f, + -7.73747838e-04f, -8.31297964e-04f, -8.81364804e-04f, -9.23641236e-04f, -9.57793553e-04f, -9.83624619e-04f, + -1.00098424e-03f, -1.00979404e-03f, -1.01003977e-03f, -1.00180772e-03f, -9.85219816e-04f, -9.60506778e-04f, + -9.27905874e-04f, -8.87790902e-04f, -8.40553609e-04f, -7.86632276e-04f, -7.26559669e-04f, -6.60872173e-04f, + -5.90177860e-04f, -5.15099219e-04f, -4.36341554e-04f, -3.54526447e-04f, -2.70436804e-04f, -1.84757234e-04f, + -9.82406108e-05f, -1.16228429e-05f, 7.44116225e-05f, 1.59099493e-04f, 2.41739119e-04f, 3.21707034e-04f, + 3.98276352e-04f, 4.70887555e-04f, 5.38973046e-04f, 6.01940918e-04f, 6.59368174e-04f, 7.10783030e-04f, + 7.55802336e-04f, 7.94127086e-04f, 8.25478803e-04f, 8.49639386e-04f, 8.66487952e-04f, 8.75935969e-04f, + 8.77948893e-04f, 8.72611584e-04f, 8.59994515e-04f, 8.40271458e-04f, 8.13696181e-04f, 7.80491851e-04f, + 7.41053306e-04f, 6.95727202e-04f, 6.44936090e-04f, 5.89181503e-04f, 5.28946796e-04f, 4.64790448e-04f, + 3.97272420e-04f, 3.27000597e-04f, 2.54559578e-04f, 1.80597276e-04f, 1.05760446e-04f, 3.06209047e-05f, + -4.41172003e-05f, -1.17884760e-04f, -1.90032814e-04f, -2.60000039e-04f, -3.27213235e-04f, -3.91110007e-04f, + -4.51226928e-04f, -5.07042112e-04f, -5.58194586e-04f, -6.04189222e-04f, -6.44816381e-04f, -6.79653847e-04f, + -7.08557315e-04f, -7.31282579e-04f, -7.47702169e-04f, -7.57731688e-04f, -7.61359812e-04f, -7.58589885e-04f, + -7.49503361e-04f, -7.34226582e-04f, -7.12935677e-04f, -6.85882645e-04f, -6.53307567e-04f, -6.15569562e-04f, + -5.72978650e-04f, -5.25977418e-04f, -4.74963705e-04f, -4.20426590e-04f, -3.62819514e-04f, -3.02647353e-04f, + -2.40497241e-04f, -1.76810216e-04f, -1.12210871e-04f, -4.71976690e-05f, 1.76624641e-05f, 8.18440593e-05f, + 1.44804207e-04f, 2.06021410e-04f, 2.65025446e-04f, 3.21327783e-04f, 3.74487008e-04f, 4.24062432e-04f, + 4.69715655e-04f, 5.11042943e-04f, 5.47794530e-04f, 5.79655168e-04f, 6.06446384e-04f, 6.27934546e-04f, + 6.44010762e-04f, 6.54614698e-04f, 6.59636425e-04f, 6.59157826e-04f, 6.53158826e-04f, 6.41794049e-04f, + 6.25154916e-04f, 6.03470855e-04f, 5.76917242e-04f, 5.45789736e-04f, 5.10368292e-04f, 4.70998661e-04f, + 4.28021656e-04f, 3.81834126e-04f, 3.32863326e-04f, 2.81489629e-04f, 2.28231239e-04f, 1.73484261e-04f, + 1.17756607e-04f, 6.14881351e-05f, 5.17778269e-06f, -5.07352374e-05f, -1.05745987e-04f, -1.59454662e-04f, + -2.11394268e-04f, -2.61151905e-04f, -3.08351703e-04f, -3.52598590e-04f, -3.93545002e-04f, -4.30916147e-04f, + -4.64387406e-04f, -4.93756593e-04f, -5.18755281e-04f, -5.39265493e-04f, -5.55137934e-04f, -5.66259303e-04f, + -5.72606783e-04f, -5.74140344e-04f, -5.70903292e-04f, -5.62934741e-04f, -5.50388898e-04f, -5.33351962e-04f, + -5.12028510e-04f, -4.86612455e-04f, -4.57392981e-04f, -4.24578939e-04f, -3.88503808e-04f, -3.49487518e-04f, + -3.07895836e-04f, -2.64036522e-04f, -2.18356445e-04f, -1.71198300e-04f, -1.22998901e-04f, -7.41392080e-05f, + -2.50280393e-05f, 2.38852047e-05f, 7.22663332e-05f, 1.19659647e-04f, 1.65718806e-04f, 2.10055385e-04f, + 2.52324173e-04f, 2.92190427e-04f, 3.29337577e-04f, 3.63510150e-04f, 3.94385715e-04f, 4.21803288e-04f, + 4.45519433e-04f, 4.65391876e-04f, 4.81270460e-04f, 4.93057625e-04f, 5.00688030e-04f, 5.04121708e-04f, + 5.03379627e-04f, 4.98485604e-04f, 4.89499566e-04f, 4.76539317e-04f, 4.59760023e-04f, 4.39274612e-04f, + 4.15334876e-04f, 3.88103885e-04f, 3.57902146e-04f, 3.24908089e-04f, 2.89490480e-04f, 2.51922687e-04f, + 2.12512220e-04f, 1.71637404e-04f, 1.29609890e-04f, 8.67866183e-05f, 4.35312276e-05f, 1.98808307e-07f, + -4.28589070e-05f, -8.52865394e-05f, -1.26765698e-04f, -1.66922292e-04f, -2.05456466e-04f, -2.42095652e-04f, + -2.76487494e-04f, -3.08425602e-04f, -3.37638832e-04f, -3.63923042e-04f, -3.87022898e-04f, -4.06875144e-04f, + -4.23245129e-04f, -4.36071615e-04f, -4.45236993e-04f, -4.50724682e-04f, -4.52491230e-04f, -4.50548104e-04f, + -4.44936790e-04f, -4.35725612e-04f, -4.22987381e-04f, -4.06882738e-04f, -3.87548587e-04f, -3.65123104e-04f, + -3.39860288e-04f, -3.11947486e-04f, -2.81618569e-04f, -2.49166817e-04f, -2.14824344e-04f, -1.78876370e-04f, + -1.41684861e-04f, -1.03466427e-04f, -6.45996088e-05f, -2.53738050e-05f, 1.39035721e-05f, 5.28977578e-05f, + 9.13010773e-05f, 1.28809554e-04f, 1.65139924e-04f, 2.00005346e-04f, 2.33095696e-04f, 2.64232233e-04f, + 2.93070034e-04f, 3.19508024e-04f, 3.43252648e-04f, 3.64165224e-04f, 3.82074036e-04f, 3.96868082e-04f, + 4.08408250e-04f, 4.16671952e-04f, 4.21556517e-04f, 4.23035822e-04f, 4.21172111e-04f, 4.15928838e-04f, + 4.07377025e-04f, 3.95568598e-04f, 3.80628038e-04f, 3.62729177e-04f, 3.41921136e-04f, 3.18489958e-04f, + 2.92497406e-04f, 2.64266550e-04f, 2.33955571e-04f, 2.01809261e-04f, 1.68092145e-04f, 1.33141461e-04f, + 9.71043460e-05f, 6.03452880e-05f, 2.31264055e-05f, -1.43105089e-05f, -5.15607083e-05f, -8.84833364e-05f, + -1.24679461e-04f, -1.59910519e-04f, -1.93952723e-04f, -2.26496145e-04f, -2.57307566e-04f, -2.86175538e-04f, + -3.12853472e-04f, -3.37140613e-04f, -3.58914997e-04f, -3.77932329e-04f, -3.94117065e-04f, -4.07317063e-04f, + -4.17422308e-04f, -4.24419479e-04f, -4.28161231e-04f, -4.28700484e-04f, -4.26016659e-04f, -4.20088126e-04f, + -4.11009185e-04f, -3.98835037e-04f, -3.83585114e-04f, -3.65493072e-04f, -3.44616197e-04f, -3.21064387e-04f, + -2.95119418e-04f, -2.66863117e-04f, -2.36549174e-04f, -2.04391686e-04f, -1.70585806e-04f, -1.35432614e-04f, + -9.91006984e-05f, -6.19152828e-05f, -2.41012311e-05f, 1.40621144e-05f, 5.22867497e-05f, 9.03199843e-05f, + 1.27917614e-04f, 1.64740292e-04f, 2.00634478e-04f, 2.35261402e-04f, 2.68377430e-04f, 2.99818019e-04f, + 3.29273634e-04f, 3.56562766e-04f, 3.81532332e-04f, 4.03948113e-04f, 4.23655375e-04f, 4.40488930e-04f, + 4.54376777e-04f, 4.65137195e-04f, 4.72679704e-04f, 4.77014073e-04f, 4.77982201e-04f, 4.75625277e-04f, + 4.69878507e-04f, 4.60802987e-04f, 4.48367418e-04f, 4.32641679e-04f, 4.13709630e-04f, 3.91634147e-04f, + 3.66512902e-04f, 3.38481392e-04f, 3.07634938e-04f, 2.74189182e-04f, 2.38229594e-04f, 1.99985879e-04f, + 1.59632210e-04f, 1.17351364e-04f, 7.33404728e-05f, 2.78844831e-05f, -1.89099461e-05f, -6.67343638e-05f, + -1.15367449e-04f, -1.64649983e-04f, -2.14224348e-04f, -2.64019844e-04f, -3.13654244e-04f, -3.62990333e-04f, + -4.11800705e-04f, -4.59821928e-04f, -5.06946486e-04f, -5.52847863e-04f, -5.97397068e-04f, -6.40454770e-04f, + -6.81765968e-04f, -7.21210131e-04f, -7.58634477e-04f, -7.93939572e-04f, -8.26964876e-04f, -8.57585335e-04f, + -8.85733438e-04f, -9.11351007e-04f, -9.34300512e-04f, -9.54617442e-04f, -9.72159416e-04f, -9.87012089e-04f, + -9.99095133e-04f, -1.00846242e-03f, -1.01506022e-03f, -1.01897105e-03f, -1.02021427e-03f, -1.01887259e-03f, + -1.01497557e-03f, -1.00861358e-03f, -9.99877741e-04f, -9.88823136e-04f, -9.75617693e-04f, -9.60303769e-04f, + -9.43035535e-04f, -9.23922797e-04f, -9.03105429e-04f, -8.80708716e-04f, -8.56853281e-04f, -8.31685264e-04f, + -8.05348207e-04f, -7.77961627e-04f, -7.49713086e-04f, -7.20674604e-04f, -6.91032783e-04f, -6.60888020e-04f, + -6.30372917e-04f, -5.99673349e-04f, -5.68830563e-04f, -5.38013304e-04f, -5.07353303e-04f, -4.76915043e-04f, + -4.46832926e-04f, -4.17179291e-04f, -3.88083307e-04f, -3.59575024e-04f, -3.31820735e-04f, -3.04804303e-04f, + -2.78616041e-04f, -2.53335964e-04f, -2.28986996e-04f, -2.05619529e-04f, -1.83318449e-04f, -1.61979425e-04f, + -1.41791423e-04f, -1.22648816e-04f, -1.04625498e-04f, -8.77122910e-05f, -7.18653457e-05f, -5.71787106e-05f, + -4.34807639e-05f, -3.09618857e-05f, -1.94074401e-05f, -8.88017971e-06f, 6.09625220e-07f, 9.14020334e-06f, + 1.67805558e-05f, 2.35369965e-05f, 2.94278194e-05f, 3.45049751e-05f, 3.88373828e-05f, 4.24291966e-05f, + 4.53445665e-05f, 4.76965834e-05f, 4.93395567e-05f, 5.05392111e-05f, 5.12257065e-05f, 5.14579340e-05f, + 5.12651750e-05f, 5.07312551e-05f, 4.98486765e-05f, 4.87082573e-05f, 4.73439631e-05f, 4.56740817e-05f, + 4.38653618e-05f, 4.19399075e-05f, 3.99125668e-05f, 3.77616021e-05f, 3.56135997e-05f, 3.33554815e-05f, + 3.11656899e-05f, 2.89038150e-05f, 2.67281634e-05f, 2.46192762e-05f, 2.24899205e-05f, 2.04698700e-05f, + 1.84927655e-05f, 1.66762886e-05f, 1.49393771e-05f, 1.32258081e-05f, 1.16985586e-05f, 1.01874391e-05f, + 8.99882100e-06f, 7.61267073e-06f, 6.57702907e-06f, 5.59829210e-06f, 4.27698546e-06f, 1.03248674e-05f, +}; From afca0ec2c9542cc39bfd6f141525fb625e3bfa2e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 13:25:05 -0700 Subject: [PATCH 0387/1237] moved vive pulses to correct thread, works with duration --- plugins/openvr/src/ViveControllerManager.cpp | 65 ++++++++++---------- plugins/openvr/src/ViveControllerManager.h | 29 +++++---- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 7d66429ed6..39b5e620ad 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -125,12 +125,6 @@ bool ViveControllerManager::activate() { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; - - _leftHapticTimer.setSingleShot(true); - _rightHapticTimer.setSingleShot(true); - connect(&_leftHapticTimer, SIGNAL(timeout()), this, SLOT(hapticPulseHelper(true))); - connect(&_rightHapticTimer, SIGNAL(timeout()), this, SLOT(hapticPulseHelper(false))); - return true; } @@ -250,6 +244,17 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle handleHandController(deltaTime, leftHandDeviceIndex, inputCalibrationData, true); handleHandController(deltaTime, rightHandDeviceIndex, inputCalibrationData, false); + // handle haptics + { + Locker locker(_lock); + if (_leftHapticDuration > 0.0f) { + hapticsHelper(deltaTime, true); + } + if (_rightHapticDuration > 0.0f) { + hapticsHelper(deltaTime, false); + } + } + int numTrackedControllers = 0; if (leftHandDeviceIndex != vr::k_unTrackedDeviceIndexInvalid) { numTrackedControllers++; @@ -447,27 +452,28 @@ void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = avatarPose.transform(controllerToAvatar); } -void ViveControllerManager::hapticPulseHelper(bool leftHand) { - if (_inputDevice) { - _inputDevice->hapticPulseHelper(leftHand); - } -} - -void ViveControllerManager::InputDevice::hapticPulseHelper(bool leftHand) { - if (leftHand) { - triggerHapticPulse(prevLeftHapticStrength, prevLeftHapticDuration, leftHand); - } else { - triggerHapticPulse(prevRightHapticStrength, prevRightHapticDuration, leftHand); - } -} - bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, bool leftHand) { + Locker locker(_lock); + if (leftHand) { + _leftHapticStrength = strength; + _leftHapticDuration = duration; + } else { + _rightHapticStrength = strength; + _rightHapticDuration = duration; + } + return true; +} + +void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool leftHand) { auto handRole = leftHand ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand; auto deviceIndex = _system->GetTrackedDeviceIndexForControllerRole(handRole); if (_system->IsTrackedDeviceConnected(deviceIndex) && _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && _trackedDevicePose[deviceIndex].bPoseIsValid) { + float strength = leftHand ? _leftHapticStrength : _rightHapticStrength; + float duration = leftHand ? _leftHapticDuration : _rightHapticDuration; + // Vive Controllers only support duration up to 4 ms, which is short enough that any variation feels more like strength const float MAX_HAPTIC_TIME = 3999.0f; // in microseconds float hapticTime = strength*MAX_HAPTIC_TIME; @@ -478,22 +484,13 @@ bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, floa // Must wait 5 ms before triggering another pulse on this controller // https://github.com/ValveSoftware/openvr/wiki/IVRSystem::TriggerHapticPulse const float HAPTIC_RESET_TIME = 5.0f; - float remainingHapticTime = duration - (hapticTime / 1000.0f + HAPTIC_RESET_TIME); // in milliseconds - if (remainingHapticTime > 0.0f) { - if (leftHand) { - prevLeftHapticStrength = strength; - prevLeftHapticDuration = remainingHapticTime; - _parent._leftHapticTimer.start(remainingHapticTime); - } - else { - prevRightHapticStrength = strength; - prevRightHapticDuration = remainingHapticTime; - _parent._rightHapticTimer.start(remainingHapticTime); - } + float remainingHapticTime = duration - (hapticTime / 1000.0f + deltaTime * 1000.0f); // in milliseconds + if (leftHand) { + _leftHapticDuration = remainingHapticTime; + } else { + _rightHapticDuration = remainingHapticTime; } - return true; } - return false; } controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableInputs() const { diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 67fe43c15a..9bdbd0370e 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -13,7 +13,6 @@ #define hifi__ViveControllerManager #include -#include #include #include @@ -46,13 +45,10 @@ public: void setRenderControllers(bool renderControllers) { _renderControllers = renderControllers; } -private slots: - void hapticPulseHelper(bool leftHand); - private: class InputDevice : public controller::InputDevice { public: - InputDevice(ViveControllerManager& parent, vr::IVRSystem*& system) : controller::InputDevice("Vive"), _parent(parent), _system(system) {} + InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system), _leftHapticDuration(0.0f), _rightHapticDuration(0.0f) {} private: // Device functions controller::Input::NamedVector getAvailableInputs() const override; @@ -60,8 +56,8 @@ private: void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; - void hapticPulseHelper(bool leftHand); bool triggerHapticPulse(float strength, float duration, bool leftHand) override; + void hapticsHelper(float deltaTime, bool leftHand); void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); void handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand); @@ -97,13 +93,19 @@ private: FilteredStick _filteredLeftStick; FilteredStick _filteredRightStick; + // perform an action when the InputDevice mutex is acquired. + using Locker = std::unique_lock; + template + void withLock(F&& f) { Locker locker(_lock); f(); } + int _trackedControllers { 0 }; vr::IVRSystem*& _system; - ViveControllerManager& _parent; - float prevLeftHapticStrength; - float prevLeftHapticDuration; - float prevRightHapticStrength; - float prevRightHapticDuration; + float _leftHapticStrength; + float _leftHapticDuration; + float _rightHapticStrength; + float _rightHapticDuration; + mutable std::recursive_mutex _lock; + friend class ViveControllerManager; }; @@ -119,10 +121,7 @@ private: bool _renderControllers { false }; vr::IVRSystem* _system { nullptr }; - std::shared_ptr _inputDevice { std::make_shared(*this, _system) }; - - QTimer _leftHapticTimer; - QTimer _rightHapticTimer; + std::shared_ptr _inputDevice { std::make_shared(_system) }; static const QString NAME; }; From 3dadb515f37615803b21543add98784c78d23d59 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 1 Jun 2016 20:30:43 -0700 Subject: [PATCH 0388/1237] Add plugins name to their metadata file --- plugins/hifiNeuron/src/plugin.json | 2 +- plugins/hifiSdl2/src/plugin.json | 2 +- plugins/hifiSixense/src/plugin.json | 2 +- plugins/hifiSpacemouse/src/plugin.json | 2 +- plugins/oculus/src/oculus.json | 2 +- plugins/oculusLegacy/src/oculus.json | 2 +- plugins/openvr/src/plugin.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/hifiNeuron/src/plugin.json b/plugins/hifiNeuron/src/plugin.json index 0967ef424b..d153b5cebd 100644 --- a/plugins/hifiNeuron/src/plugin.json +++ b/plugins/hifiNeuron/src/plugin.json @@ -1 +1 @@ -{} +{"name":"Neuron"} diff --git a/plugins/hifiSdl2/src/plugin.json b/plugins/hifiSdl2/src/plugin.json index 0967ef424b..a65846ecab 100644 --- a/plugins/hifiSdl2/src/plugin.json +++ b/plugins/hifiSdl2/src/plugin.json @@ -1 +1 @@ -{} +{"name":"SDL2"} diff --git a/plugins/hifiSixense/src/plugin.json b/plugins/hifiSixense/src/plugin.json index 0967ef424b..9e6e15a354 100644 --- a/plugins/hifiSixense/src/plugin.json +++ b/plugins/hifiSixense/src/plugin.json @@ -1 +1 @@ -{} +{"name":"Sixense"} diff --git a/plugins/hifiSpacemouse/src/plugin.json b/plugins/hifiSpacemouse/src/plugin.json index 0967ef424b..294f436039 100644 --- a/plugins/hifiSpacemouse/src/plugin.json +++ b/plugins/hifiSpacemouse/src/plugin.json @@ -1 +1 @@ -{} +{"name":"Spacemouse"} diff --git a/plugins/oculus/src/oculus.json b/plugins/oculus/src/oculus.json index 0967ef424b..86546c8dd5 100644 --- a/plugins/oculus/src/oculus.json +++ b/plugins/oculus/src/oculus.json @@ -1 +1 @@ -{} +{"name":"Oculus Rift"} diff --git a/plugins/oculusLegacy/src/oculus.json b/plugins/oculusLegacy/src/oculus.json index 0967ef424b..4cd9a136b3 100644 --- a/plugins/oculusLegacy/src/oculus.json +++ b/plugins/oculusLegacy/src/oculus.json @@ -1 +1 @@ -{} +{"name":"Oculus Rift (0.5) (Legacy)"} diff --git a/plugins/openvr/src/plugin.json b/plugins/openvr/src/plugin.json index 0967ef424b..d68c8e68d3 100644 --- a/plugins/openvr/src/plugin.json +++ b/plugins/openvr/src/plugin.json @@ -1 +1 @@ -{} +{"name":"OpenVR (Vive)"} From d8493f960a32d0d6d0db83cbfe8c38dfedd4a27b Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 1 Jun 2016 20:50:24 -0700 Subject: [PATCH 0389/1237] Add command line option to enable/disable plugins --- interface/src/Application.cpp | 29 +++++++- interface/src/Application.h | 2 +- interface/src/main.cpp | 28 ++++---- .../plugins/src/plugins/PluginManager.cpp | 66 ++++++++++++++++++- libraries/plugins/src/plugins/PluginManager.h | 15 +++-- 5 files changed, 117 insertions(+), 23 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e789b7c508..62f3a3a9c5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -4936,7 +4937,33 @@ void Application::postLambdaEvent(std::function f) { } } -void Application::initPlugins() { +void Application::initPlugins(const QStringList& arguments) { + QCommandLineOption display("display", "Default display", "display"); + QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "display"); + QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "input"); + + QCommandLineParser parser; + parser.addOption(display); + parser.addOption(disableDisplays); + parser.addOption(disableInputs); + parser.parse(arguments); + + if (parser.isSet(display)) { + auto defaultDisplay = parser.value(display); + qInfo() << "Setting prefered display plugin:" << defaultDisplay; + } + + if (parser.isSet(disableDisplays)) { + auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following display plugins:" << disabledDisplays; + PluginManager::getInstance()->disableDisplays(disabledDisplays); + } + + if (parser.isSet(disableInputs)) { + auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following input plugins:" << disabledInputs; + PluginManager::getInstance()->disableInputs(disabledInputs); + } } void Application::shutdownPlugins() { diff --git a/interface/src/Application.h b/interface/src/Application.h index a17250a58e..ed7b582bfc 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -101,7 +101,7 @@ public: }; // FIXME? Empty methods, do we still need them? - static void initPlugins(); + static void initPlugins(const QStringList& arguments); static void shutdownPlugins(); Application(int& argc, char** argv, QElapsedTimer& startup_time); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 13f9470fda..6866d5637c 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -8,6 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include #include @@ -17,6 +19,12 @@ #include #include +#ifdef HAS_BUGSPLAT +#include +#include +#include +#endif + #include #include @@ -25,13 +33,7 @@ #include "InterfaceLogging.h" #include "UserActivityLogger.h" #include "MainWindow.h" -#include -#ifdef HAS_BUGSPLAT -#include -#include -#include -#endif int main(int argc, const char* argv[]) { #if HAS_BUGSPLAT @@ -46,6 +48,12 @@ int main(int argc, const char* argv[]) { bool instanceMightBeRunning = true; + QStringList arguments; + for (int i = 0; i < argc; ++i) { + arguments << argv[i]; + } + + #ifdef Q_OS_WIN // Try to create a shared memory block - if it can't be created, there is an instance of // interface already running. We only do this on Windows for now because of the potential @@ -64,12 +72,6 @@ int main(int argc, const char* argv[]) { // Try to connect - if we can't connect, interface has probably just gone down if (socket.waitForConnected(LOCAL_SERVER_TIMEOUT_MS)) { - - QStringList arguments; - for (int i = 0; i < argc; ++i) { - arguments << argv[i]; - } - QCommandLineParser parser; QCommandLineOption urlOption("url", "", "value"); parser.addOption(urlOption); @@ -135,7 +137,7 @@ int main(int argc, const char* argv[]) { // Oculus initialization MUST PRECEDE OpenGL context creation. // The nature of the Application constructor means this has to be either here, // or in the main window ctor, before GL startup. - Application::initPlugins(); + Application::initPlugins(arguments); int exitCode; { diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index eb6465aab2..9047b1c271 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -25,6 +25,49 @@ PluginManager* PluginManager::getInstance() { return &_manager; } +QString getPluginNameFromMetaData(QJsonObject object) { + static const char* METADATA_KEY = "MetaData"; + static const char* NAME_KEY = "name"; + + if (!object.contains(METADATA_KEY) || !object[METADATA_KEY].isObject()) { + return QString(); + } + + auto metaDataObject = object[METADATA_KEY].toObject(); + + if (!metaDataObject.contains(NAME_KEY) || !metaDataObject[NAME_KEY].isString()) { + return QString(); + } + + return metaDataObject[NAME_KEY].toString(); +} + +QString getPluginIIDFromMetaData(QJsonObject object) { + static const char* IID_KEY = "IID"; + + if (!object.contains(IID_KEY) || !object[IID_KEY].isString()) { + return QString(); + } + + return object[IID_KEY].toString(); +} + +QStringList disabledDisplays; +QStringList disabledInputs; + +bool isDisabled(QJsonObject metaData) { + auto name = getPluginNameFromMetaData(metaData); + auto iid = getPluginIIDFromMetaData(metaData); + + if (iid == DisplayProvider_iid) { + return disabledDisplays.contains(name); + } else if (iid == InputProvider_iid) { + return disabledInputs.contains(name); + } + + return false; +} + using Loader = QSharedPointer; using LoaderList = QList; @@ -43,11 +86,21 @@ const LoaderList& getLoadedPlugins() { qDebug() << "Loading runtime plugins from " << pluginPath; auto candidates = pluginDir.entryList(); for (auto plugin : candidates) { - qDebug() << "Attempting plugins " << plugin; + qDebug() << "Attempting plugin" << qPrintable(plugin); QSharedPointer loader(new QPluginLoader(pluginPath + plugin)); + + if (isDisabled(loader->metaData())) { + qWarning() << "Plugin" << qPrintable(plugin) << "is disabled"; + // Skip this one, it's disabled + continue; + } + if (loader->load()) { - qDebug() << "Plugins " << plugin << " success"; + qDebug() << "Plugin" << qPrintable(plugin) << "loaded successfully"; loadedPlugins.push_back(loader); + } else { + qDebug() << "Plugin" << qPrintable(plugin) << "failed to load:"; + qDebug() << " " << qPrintable(loader->errorString()); } } } @@ -124,6 +177,15 @@ const InputPluginList& PluginManager::getInputPlugins() { return inputPlugins; } + +void PluginManager::disableDisplays(const QStringList& displays) { + disabledDisplays << displays; +} + +void PluginManager::disableInputs(const QStringList& inputs) { + disabledInputs << inputs; +} + void PluginManager::saveSettings() { saveInputPluginSettings(getInputPlugins()); } diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index cf0b8efe64..351087dce8 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -13,11 +13,14 @@ class PluginManager : public QObject { public: - static PluginManager* getInstance(); - PluginManager(); + static PluginManager* getInstance(); + PluginManager(); - const DisplayPluginList& getDisplayPlugins(); - void disableDisplayPlugin(const QString& name); - const InputPluginList& getInputPlugins(); - void saveSettings(); + const DisplayPluginList& getDisplayPlugins(); + const InputPluginList& getInputPlugins(); + + void disableDisplayPlugin(const QString& name); + void disableDisplays(const QStringList& displays); + void disableInputs(const QStringList& inputs); + void saveSettings(); }; From 932838b1e377efc97d4d965de63b3fd1388b9ca5 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 1 Jun 2016 22:16:16 -0700 Subject: [PATCH 0390/1237] Setup preferred display plugin --- interface/src/Application.cpp | 14 ++++++++--- .../plugins/src/plugins/PluginManager.cpp | 24 +++++++++++++++++++ libraries/plugins/src/plugins/PluginManager.h | 3 +++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 62f3a3a9c5..9463b41404 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2957,6 +2957,13 @@ void Application::loadSettings() { //DependencyManager::get()->setAutomaticLODAdjust(false); Menu::getInstance()->loadSettings(); + + // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. + if (auto plugin = PluginManager::getInstance()->getPreferredDisplayPlugin()) { + Q_ASSERT(plugin == getActiveDisplayPlugin()); + Menu::getInstance()->setIsOptionChecked(plugin->getName(), true); + } + getMyAvatar()->loadData(); _settingsLoaded = true; @@ -4938,7 +4945,7 @@ void Application::postLambdaEvent(std::function f) { } void Application::initPlugins(const QStringList& arguments) { - QCommandLineOption display("display", "Default display", "display"); + QCommandLineOption display("display", "Preferred display", "display"); QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "display"); QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "input"); @@ -4949,8 +4956,9 @@ void Application::initPlugins(const QStringList& arguments) { parser.parse(arguments); if (parser.isSet(display)) { - auto defaultDisplay = parser.value(display); - qInfo() << "Setting prefered display plugin:" << defaultDisplay; + auto preferredDisplay = parser.value(display); + qInfo() << "Setting prefered display plugin:" << preferredDisplay; + PluginManager::getInstance()->setPreferredDisplayPlugin(preferredDisplay); } if (parser.isSet(disableDisplays)) { diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 9047b1c271..0c9b58cffa 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -52,6 +52,7 @@ QString getPluginIIDFromMetaData(QJsonObject object) { return object[IID_KEY].toString(); } +QString preferredDisplayPluginName; QStringList disabledDisplays; QStringList disabledInputs; @@ -177,6 +178,29 @@ const InputPluginList& PluginManager::getInputPlugins() { return inputPlugins; } +void PluginManager::setPreferredDisplayPlugin(const QString& display) { + preferredDisplayPluginName = display; +} + +DisplayPluginPointer PluginManager::getPreferredDisplayPlugin() { + static DisplayPluginPointer displayPlugin; + + static std::once_flag once; + std::call_once(once, [&] { + // Grab the built in plugins + auto plugins = getDisplayPlugins(); + + auto it = std::find_if(plugins.begin(), plugins.end(), [](DisplayPluginPointer plugin) { + return plugin->getName() == preferredDisplayPluginName; + }); + if (it != plugins.end()) { + displayPlugin = *it; + } + }); + + return displayPlugin; +} + void PluginManager::disableDisplays(const QStringList& displays) { disabledDisplays << displays; diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 351087dce8..d44ca2c28e 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -19,6 +19,9 @@ public: const DisplayPluginList& getDisplayPlugins(); const InputPluginList& getInputPlugins(); + DisplayPluginPointer getPreferredDisplayPlugin(); + + void setPreferredDisplayPlugin(const QString& display); void disableDisplayPlugin(const QString& name); void disableDisplays(const QStringList& displays); void disableInputs(const QStringList& inputs); From 992fa639326ea4b74d3c554a6104b9b28c9bcef4 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 2 Jun 2016 14:37:35 -0700 Subject: [PATCH 0391/1237] Allow ordered list of preferred plugins --- interface/src/Application.cpp | 24 ++++++++++++------- .../plugins/src/plugins/PluginManager.cpp | 24 ++++++++++--------- libraries/plugins/src/plugins/PluginManager.h | 4 ++-- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9463b41404..52d9fdf64a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2959,9 +2959,15 @@ void Application::loadSettings() { Menu::getInstance()->loadSettings(); // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. - if (auto plugin = PluginManager::getInstance()->getPreferredDisplayPlugin()) { - Q_ASSERT(plugin == getActiveDisplayPlugin()); - Menu::getInstance()->setIsOptionChecked(plugin->getName(), true); + auto plugins = PluginManager::getInstance()->getPreferredDisplayPlugins(); + for (auto plugin : plugins) { + auto menu = Menu::getInstance(); + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + // Find and activat5ed highest priority plugin, bail for the rest + break; + } } getMyAvatar()->loadData(); @@ -4945,9 +4951,9 @@ void Application::postLambdaEvent(std::function f) { } void Application::initPlugins(const QStringList& arguments) { - QCommandLineOption display("display", "Preferred display", "display"); - QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "display"); - QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "input"); + QCommandLineOption display("display", "Preferred displays", "displays"); + QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); + QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); QCommandLineParser parser; parser.addOption(display); @@ -4956,9 +4962,9 @@ void Application::initPlugins(const QStringList& arguments) { parser.parse(arguments); if (parser.isSet(display)) { - auto preferredDisplay = parser.value(display); - qInfo() << "Setting prefered display plugin:" << preferredDisplay; - PluginManager::getInstance()->setPreferredDisplayPlugin(preferredDisplay); + auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); + qInfo() << "Setting prefered display plugins:" << preferredDisplays; + PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); } if (parser.isSet(disableDisplays)) { diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 0c9b58cffa..ee5fb509b2 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -52,7 +52,7 @@ QString getPluginIIDFromMetaData(QJsonObject object) { return object[IID_KEY].toString(); } -QString preferredDisplayPluginName; +QStringList preferredDisplayPlugins; QStringList disabledDisplays; QStringList disabledInputs; @@ -178,27 +178,29 @@ const InputPluginList& PluginManager::getInputPlugins() { return inputPlugins; } -void PluginManager::setPreferredDisplayPlugin(const QString& display) { - preferredDisplayPluginName = display; +void PluginManager::setPreferredDisplayPlugins(const QStringList& displays) { + preferredDisplayPlugins = displays; } -DisplayPluginPointer PluginManager::getPreferredDisplayPlugin() { - static DisplayPluginPointer displayPlugin; +DisplayPluginList PluginManager::getPreferredDisplayPlugins() { + static DisplayPluginList displayPlugins; static std::once_flag once; std::call_once(once, [&] { // Grab the built in plugins auto plugins = getDisplayPlugins(); - auto it = std::find_if(plugins.begin(), plugins.end(), [](DisplayPluginPointer plugin) { - return plugin->getName() == preferredDisplayPluginName; - }); - if (it != plugins.end()) { - displayPlugin = *it; + for (auto pluginName : preferredDisplayPlugins) { + auto it = std::find_if(plugins.begin(), plugins.end(), [&](DisplayPluginPointer plugin) { + return plugin->getName() == pluginName; + }); + if (it != plugins.end()) { + displayPlugins.push_back(*it); + } } }); - return displayPlugin; + return displayPlugins; } diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index d44ca2c28e..2a94e6490b 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -19,9 +19,9 @@ public: const DisplayPluginList& getDisplayPlugins(); const InputPluginList& getInputPlugins(); - DisplayPluginPointer getPreferredDisplayPlugin(); + DisplayPluginList getPreferredDisplayPlugins(); + void setPreferredDisplayPlugins(const QStringList& displays); - void setPreferredDisplayPlugin(const QString& display); void disableDisplayPlugin(const QString& name); void disableDisplays(const QStringList& displays); void disableInputs(const QStringList& inputs); From ee62a211ac230c93b12047b4e29b5ab75836a8c8 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 2 Jun 2016 15:36:11 -0700 Subject: [PATCH 0392/1237] Remove "Input Devices" menu --- interface/src/Application.cpp | 76 ----------------------------------- interface/src/Application.h | 1 - interface/src/Menu.cpp | 6 --- interface/src/Menu.h | 1 - 4 files changed, 84 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 52d9fdf64a..d41c971896 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1477,7 +1477,6 @@ void Application::initializeUi() { } } _window->setMenuBar(new Menu()); - updateInputModes(); auto compositorHelper = DependencyManager::get(); connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, [=] { @@ -5247,81 +5246,6 @@ void Application::updateDisplayMode() { Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } -static void addInputPluginToMenu(InputPluginPointer inputPlugin) { - auto menu = Menu::getInstance(); - QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); - Q_ASSERT(!menu->menuItemExists(MenuOption::InputMenu, name)); - - static QActionGroup* inputPluginGroup = nullptr; - if (!inputPluginGroup) { - inputPluginGroup = new QActionGroup(menu); - inputPluginGroup->setExclusive(false); - } - - auto parent = menu->getMenu(MenuOption::InputMenu); - auto action = menu->addCheckableActionToQMenuAndActionHash(parent, - name, 0, true, qApp, - SLOT(updateInputModes())); - - inputPluginGroup->addAction(action); - Q_ASSERT(menu->menuItemExists(MenuOption::InputMenu, name)); -} - - -void Application::updateInputModes() { - auto menu = Menu::getInstance(); - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - static std::once_flag once; - std::call_once(once, [&] { - foreach(auto inputPlugin, inputPlugins) { - addInputPluginToMenu(inputPlugin); - } - }); - auto offscreenUi = DependencyManager::get(); - - InputPluginList newInputPlugins; - InputPluginList removedInputPlugins; - foreach(auto inputPlugin, inputPlugins) { - QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); - QAction* action = menu->getActionForOption(name); - - auto it = std::find(std::begin(_activeInputPlugins), std::end(_activeInputPlugins), inputPlugin); - if (action->isChecked() && it == std::end(_activeInputPlugins)) { - _activeInputPlugins.push_back(inputPlugin); - newInputPlugins.push_back(inputPlugin); - } else if (!action->isChecked() && it != std::end(_activeInputPlugins)) { - _activeInputPlugins.erase(it); - removedInputPlugins.push_back(inputPlugin); - } - } - - // A plugin was checked - if (newInputPlugins.size() > 0) { - foreach(auto newInputPlugin, newInputPlugins) { - newInputPlugin->activate(); - //newInputPlugin->installEventFilter(qApp); - //newInputPlugin->installEventFilter(offscreenUi.data()); - } - } - if (removedInputPlugins.size() > 0) { // A plugin was unchecked - foreach(auto removedInputPlugin, removedInputPlugins) { - removedInputPlugin->deactivate(); - //removedInputPlugin->removeEventFilter(qApp); - //removedInputPlugin->removeEventFilter(offscreenUi.data()); - } - } - - //if (newInputPlugins.size() > 0 || removedInputPlugins.size() > 0) { - // if (!_currentInputPluginActions.isEmpty()) { - // auto menu = Menu::getInstance(); - // foreach(auto itemInfo, _currentInputPluginActions) { - // menu->removeMenuItem(itemInfo.first, itemInfo.second); - // } - // _currentInputPluginActions.clear(); - // } - //} -} - mat4 Application::getEyeProjection(int eye) const { QMutexLocker viewLocker(&_viewMutex); if (isHMDMode()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index ed7b582bfc..f93434f581 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -327,7 +327,6 @@ private slots: void nodeKilled(SharedNodePointer node); static void packetSent(quint64 length); void updateDisplayMode(); - void updateInputModes(); void domainConnectionRefused(const QString& reasonMessage, int reason); private: diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a21aa71753..031564fa7a 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -403,12 +403,6 @@ Menu::Menu() { // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); - // Settings > Input Devices - MenuWrapper* inputModeMenu = addMenu(MenuOption::InputMenu, "Advanced"); - QActionGroup* inputModeGroup = new QActionGroup(inputModeMenu); - inputModeGroup->setExclusive(false); - - // Developer > Avatar > Face Tracking MenuWrapper* faceTrackingMenu = avatarDebugMenu->addMenu("Face Tracking"); { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index fcaf8e6caa..8081e27eb8 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -113,7 +113,6 @@ namespace MenuOption { const QString Help = "Help..."; const QString IncreaseAvatarSize = "Increase Avatar Size"; const QString IndependentMode = "Independent Mode"; - const QString InputMenu = "Developer>Avatar>Input Devices"; const QString ActionMotorControl = "Enable Default Motor Control"; const QString LeapMotionOnHMD = "Leap Motion on HMD"; const QString LoadScript = "Open and Run Script File..."; From b635e32933835c497b98eefbff3e0bb3243be5fc Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 2 Jun 2016 17:44:39 -0700 Subject: [PATCH 0393/1237] Rename Mac only Oculus display plugin --- plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp | 2 +- plugins/oculusLegacy/src/oculus.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index f89e71b829..8e044fbc16 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -31,7 +31,7 @@ using namespace oglplus; -const QString OculusLegacyDisplayPlugin::NAME("Oculus Rift (0.5) (Legacy)"); +const QString OculusLegacyDisplayPlugin::NAME("Oculus Rift"); OculusLegacyDisplayPlugin::OculusLegacyDisplayPlugin() { } diff --git a/plugins/oculusLegacy/src/oculus.json b/plugins/oculusLegacy/src/oculus.json index 4cd9a136b3..86546c8dd5 100644 --- a/plugins/oculusLegacy/src/oculus.json +++ b/plugins/oculusLegacy/src/oculus.json @@ -1 +1 @@ -{"name":"Oculus Rift (0.5) (Legacy)"} +{"name":"Oculus Rift"} From 6f75fab2980958d78718f090f356864d91341933 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 2 Jun 2016 18:02:30 -0700 Subject: [PATCH 0394/1237] Revert header changes --- interface/src/main.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 6866d5637c..8fc0384aee 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -8,8 +8,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include - #include #include #include @@ -19,12 +17,6 @@ #include #include -#ifdef HAS_BUGSPLAT -#include -#include -#include -#endif - #include #include @@ -33,7 +25,13 @@ #include "InterfaceLogging.h" #include "UserActivityLogger.h" #include "MainWindow.h" +#include +#ifdef HAS_BUGSPLAT +#include +#include +#include +#endif int main(int argc, const char* argv[]) { #if HAS_BUGSPLAT From 24bbb8db3f663f73b8659a3a317bc24fcfa84c94 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 3 Jun 2016 11:51:35 -0700 Subject: [PATCH 0395/1237] Remove input plugin menu dependency --- interface/src/Application.cpp | 33 ++++++++----------- .../src/input-plugins/InputPlugin.cpp | 1 - .../plugins/src/plugins/PluginManager.cpp | 5 ++- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d41c971896..6dcb64a07d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -198,7 +198,6 @@ static const float PHYSICS_READY_RANGE = 3.0f; // how far from avatar to check f static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); -static const QString INPUT_DEVICE_MENU_PREFIX = "Device: "; Setting::Handle maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS); const QHash Application::_acceptedExtensions { @@ -1000,7 +999,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : RenderableWebEntityItem* webEntity = dynamic_cast(entity.get()); if (webEntity) { webEntity->setProxyWindow(_window->windowHandle()); - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->pluginFocusOutEvent(); } _keyboardFocusedItem = entityItemID; @@ -1153,9 +1152,7 @@ void Application::aboutToQuit() { emit beforeAboutToQuit(); foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); - QAction* action = Menu::getInstance()->getActionForOption(name); - if (action->isChecked()) { + if (inputPlugin->isActive()) { inputPlugin->deactivate(); } } @@ -2024,7 +2021,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } if (hasFocus()) { - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyPressEvent(event); } @@ -2358,7 +2355,7 @@ void Application::keyReleaseEvent(QKeyEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyReleaseEvent(event); } @@ -2390,9 +2387,7 @@ void Application::keyReleaseEvent(QKeyEvent* event) { void Application::focusOutEvent(QFocusEvent* event) { auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); foreach(auto inputPlugin, inputPlugins) { - QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); - QAction* action = Menu::getInstance()->getActionForOption(name); - if (action && action->isChecked()) { + if (inputPlugin->isActive()) { inputPlugin->pluginFocusOutEvent(); } } @@ -2477,7 +2472,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mouseMoveEvent(event); } @@ -2514,7 +2509,7 @@ void Application::mousePressEvent(QMouseEvent* event) { if (hasFocus()) { - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mousePressEvent(event); } @@ -2559,7 +2554,7 @@ void Application::mouseReleaseEvent(QMouseEvent* event) { } if (hasFocus()) { - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mouseReleaseEvent(event); } @@ -2586,7 +2581,7 @@ void Application::touchUpdateEvent(QTouchEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchUpdateEvent(event); } } @@ -2604,7 +2599,7 @@ void Application::touchBeginEvent(QTouchEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchBeginEvent(event); } @@ -2621,7 +2616,7 @@ void Application::touchEndEvent(QTouchEvent* event) { return; } - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchEndEvent(event); } @@ -2637,7 +2632,7 @@ void Application::wheelEvent(QWheelEvent* event) const { return; } - if (Menu::getInstance()->isOptionChecked(INPUT_DEVICE_MENU_PREFIX + KeyboardMouseDevice::NAME)) { + if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->wheelEvent(event); } } @@ -2770,9 +2765,7 @@ void Application::idle(float nsecsElapsed) { getActiveDisplayPlugin()->idle(); auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); foreach(auto inputPlugin, inputPlugins) { - QString name = INPUT_DEVICE_MENU_PREFIX + inputPlugin->getName(); - QAction* action = Menu::getInstance()->getActionForOption(name); - if (action && action->isChecked()) { + if (inputPlugin->isActive()) { inputPlugin->idle(); } } diff --git a/libraries/input-plugins/src/input-plugins/InputPlugin.cpp b/libraries/input-plugins/src/input-plugins/InputPlugin.cpp index 4d59adb602..32c28af2ef 100644 --- a/libraries/input-plugins/src/input-plugins/InputPlugin.cpp +++ b/libraries/input-plugins/src/input-plugins/InputPlugin.cpp @@ -25,7 +25,6 @@ InputPluginList getInputPlugins() { for (int i = 0; PLUGIN_POOL[i]; ++i) { InputPlugin* plugin = PLUGIN_POOL[i]; if (plugin->isSupported()) { - plugin->init(); result.push_back(InputPluginPointer(plugin)); } } diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index ee5fb509b2..6bff5a36f3 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -164,7 +164,9 @@ const InputPluginList& PluginManager::getInputPlugins() { InputProvider* inputProvider = qobject_cast(loader->instance()); if (inputProvider) { for (auto inputPlugin : inputProvider->getInputPlugins()) { - inputPlugins.push_back(inputPlugin); + if (inputPlugin->isSupported()) { + inputPlugins.push_back(inputPlugin); + } } } } @@ -173,6 +175,7 @@ const InputPluginList& PluginManager::getInputPlugins() { for (auto plugin : inputPlugins) { plugin->setContainer(&container); plugin->init(); + plugin->activate(); } }); return inputPlugins; From 26acc6fd8b8b99e02a01e34f39c38ab4261f0228 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 3 Jun 2016 12:45:27 -0700 Subject: [PATCH 0396/1237] Typo --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6dcb64a07d..ed212dade5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2957,7 +2957,7 @@ void Application::loadSettings() { if (auto action = menu->getActionForOption(plugin->getName())) { action->setChecked(true); action->trigger(); - // Find and activat5ed highest priority plugin, bail for the rest + // Find and activated highest priority plugin, bail for the rest break; } } From 399517fcbd54ab7a6c6825bd9360528421b32023 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 6 Jun 2016 13:27:58 -0700 Subject: [PATCH 0397/1237] Activate Inputs after menus are setup --- interface/src/Application.cpp | 10 +++++++++- libraries/plugins/src/plugins/PluginManager.cpp | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ed212dade5..68e916c29e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2951,7 +2951,8 @@ void Application::loadSettings() { Menu::getInstance()->loadSettings(); // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. - auto plugins = PluginManager::getInstance()->getPreferredDisplayPlugins(); + auto pluginManager = PluginManager::getInstance(); + auto plugins = pluginManager->getPreferredDisplayPlugins(); for (auto plugin : plugins) { auto menu = Menu::getInstance(); if (auto action = menu->getActionForOption(plugin->getName())) { @@ -2962,6 +2963,13 @@ void Application::loadSettings() { } } + auto inputs = pluginManager->getInputPlugins(); + for (auto plugin : inputs) { + if (!plugin->isActive()) { + plugin->activate(); + } + } + getMyAvatar()->loadData(); _settingsLoaded = true; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 6bff5a36f3..7161132c5e 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -175,7 +175,6 @@ const InputPluginList& PluginManager::getInputPlugins() { for (auto plugin : inputPlugins) { plugin->setContainer(&container); plugin->init(); - plugin->activate(); } }); return inputPlugins; From c9f440a49ddc59b357b807af6e81addd1255196a Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 1 Jun 2016 09:53:06 -0700 Subject: [PATCH 0398/1237] handControllerGrab: initial rendering of equip hot-spots * When trigger is depressed, any entities that are marked with a wearable equip-point will become highlighted by a wireframe box (temporary art). * The grab state machine is now defined by the CONTROLLER_STATE_MACHINE object. this includes each state's name and updateMethod name. * Support was added for entry and exit methods when changing states, this functionality is used to draw and delete the hot spots when entering and exiting the searching state. --- .../system/controllers/handControllerGrab.js | 280 +++++++++++------- 1 file changed, 181 insertions(+), 99 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index be4bffe58c..0561e73e1f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -92,6 +92,7 @@ var EQUIP_SPRING_TIMEFRAME = 0.4; // how quickly objects move to their new posit // other constants // +var HOTSPOT_DRAW_DISTANCE = 10; var RIGHT_HAND = 1; var LEFT_HAND = 0; @@ -179,47 +180,85 @@ var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic"; var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; -function stateToName(state) { - switch (state) { - case STATE_OFF: - return "off"; - case STATE_SEARCHING: - return "searching"; - case STATE_HOLD_SEARCHING: - return "hold_searching"; - case STATE_DISTANCE_HOLDING: - return "distance_holding"; - case STATE_CONTINUE_DISTANCE_HOLDING: - return "continue_distance_holding"; - case STATE_NEAR_GRABBING: - return "near_grabbing"; - case STATE_CONTINUE_NEAR_GRABBING: - return "continue_near_grabbing"; - case STATE_NEAR_TRIGGER: - return "near_trigger"; - case STATE_CONTINUE_NEAR_TRIGGER: - return "continue_near_trigger"; - case STATE_FAR_TRIGGER: - return "far_trigger"; - case STATE_CONTINUE_FAR_TRIGGER: - return "continue_far_trigger"; - case STATE_RELEASE: - return "release"; - case STATE_EQUIP: - return "equip"; - case STATE_HOLD: - return "hold"; - case STATE_CONTINUE_HOLD: - return "continue_hold"; - case STATE_CONTINUE_EQUIP: - return "continue_equip"; - case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE: - return "waiting_for_equip_thumb_release"; - case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE: - return "waiting_for_release_thumb_release"; - } +var CONTROLLER_STATE_MACHINE = {}; - return "unknown"; +CONTROLLER_STATE_MACHINE[STATE_OFF] = { + name: "off", + updateMethod: "off" +}; +CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { + name: "searching", + updateMethod: "search", + enterMethod: "searchEnter", + exitMethod: "searchExit" +}; +CONTROLLER_STATE_MACHINE[STATE_HOLD_SEARCHING] = { + name: "hold_searching", + updateMethod: "search" +}; +CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { + name: "distance_holding", + updateMethod: "distanceHolding" +}; +CONTROLLER_STATE_MACHINE[STATE_CONTINUE_DISTANCE_HOLDING] = { + name: "continue_distance_holding", + updateMethod: "continueDistanceHolding" +}; +CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { + name: "near_grabbing", + updateMethod: "nearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_EQUIP] = { + name: "equip", + updateMethod: "nearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_HOLD] = { + name: "hold", + updateMethod: "nearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_CONTINUE_NEAR_GRABBING] = { + name: "continue_near_grabbing", + updateMethod: "continueNearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_CONTINUE_HOLD] = { + name: "continue_hold", + updateMethod: "continueNearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_CONTINUE_EQUIP] = { + name: "continue_equip", + updateMethod: "continueNearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { + name: "near_trigger", + updateMethod: "nearTrigger" +}; +CONTROLLER_STATE_MACHINE[STATE_CONTINUE_NEAR_TRIGGER] = { + name: "continue_near_trigger", + updateMethod: "continueNearTrigger" +}; +CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { + name: "far_trigger", + updateMethod: "farTrigger" +}; +CONTROLLER_STATE_MACHINE[STATE_CONTINUE_FAR_TRIGGER] = { + name: "continue_far_trigger", + updateMethod: "continueFarTrigger" +}; +CONTROLLER_STATE_MACHINE[STATE_RELEASE] = { + name: "release", + updateMethod: "release" +}; +CONTROLLER_STATE_MACHINE[STATE_WAITING_FOR_EQUIP_THUMB_RELEASE] = { + name: "waiting_for_equip_thumb_release", + updateMethod: "waitingForEquipThumbRelease" +}; +CONTROLLER_STATE_MACHINE[STATE_WAITING_FOR_RELEASE_THUMB_RELEASE] = { + name: "waiting_for_release_thumb_release", + updateMethod: "waitingForReleaseThumbRelease" +}; + +function stateToName(state) { + return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; } function getTag() { @@ -249,6 +288,14 @@ function entityIsGrabbedByOther(entityID) { return false; } +function propsArePhysical(props) { + if (!props.dynamic) { + return false; + } + var isPhysical = (props.shapeType && props.shapeType != 'none'); + return isPhysical; +} + // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; @@ -314,55 +361,22 @@ function MyController(hand) { this.update = function() { this.updateSmoothedTrigger(); + if (isIn2DMode()) { _this.turnOffVisualizations(); return; } - switch (this.state) { - case STATE_OFF: - this.off(); - break; - case STATE_SEARCHING: - case STATE_HOLD_SEARCHING: - this.search(); - break; - case STATE_DISTANCE_HOLDING: - this.distanceHolding(); - break; - case STATE_CONTINUE_DISTANCE_HOLDING: - this.continueDistanceHolding(); - break; - case STATE_NEAR_GRABBING: - case STATE_EQUIP: - case STATE_HOLD: - this.nearGrabbing(); - break; - case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE: - this.waitingForEquipThumbRelease(); - break; - case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE: - this.waitingForReleaseThumbRelease(); - break; - case STATE_CONTINUE_NEAR_GRABBING: - case STATE_CONTINUE_HOLD: - case STATE_CONTINUE_EQUIP: - this.continueNearGrabbing(); - break; - case STATE_NEAR_TRIGGER: - this.nearTrigger(); - break; - case STATE_CONTINUE_NEAR_TRIGGER: - this.continueNearTrigger(); - break; - case STATE_FAR_TRIGGER: - this.farTrigger(); - break; - case STATE_CONTINUE_FAR_TRIGGER: - this.continueFarTrigger(); - break; - case STATE_RELEASE: - this.release(); - break; + + if (CONTROLLER_STATE_MACHINE[this.state]) { + var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; + var updateMethod = this[updateMethodName]; + if (updateMethod) { + updateMethod.call(this); + } else { + print("WARNING: could not find updateMethod for state " + stateToName(this.state)); + } + } else { + print("WARNING: could not find state " + this.state + " " + CONTROLLER_STATE_MACHINE[this.state]); } }; @@ -374,9 +388,33 @@ function MyController(hand) { this.setState = function(newState) { this.grabSphereOff(); if (WANT_DEBUG || WANT_DEBUG_STATE) { - print("STATE (" + this.hand + "): " + stateToName(this.state) + " --> " + - stateToName(newState) + ", hand: " + this.hand); + var oldStateName = stateToName(this.state); + var newStateName = stateToName(newState); + print("STATE (" + this.hand + "): " + oldStateName + " --> " + newStateName); } + + // exit the old state + if (CONTROLLER_STATE_MACHINE[this.state]) { + var exitMethodName = CONTROLLER_STATE_MACHINE[this.state].exitMethod; + var exitMethod = this[exitMethodName]; + if (exitMethod) { + exitMethod.call(this); + } + } else { + print("WARNING: could not find this.state " + this.state); + } + + // enter the new state + if (CONTROLLER_STATE_MACHINE[newState]) { + var enterMethodName = CONTROLLER_STATE_MACHINE[newState].enterMethod; + var enterMethod = this[enterMethodName]; + if (enterMethod) { + enterMethod.call(this); + } + } else { + print("WARNING: could not find newState " + newState); + } + this.state = newState; }; @@ -759,14 +797,6 @@ function MyController(hand) { } }; - this.propsArePhysical = function(props) { - if (!props.dynamic) { - return false; - } - var isPhysical = (props.shapeType && props.shapeType != 'none'); - return isPhysical; - } - this.turnOffVisualizations = function() { if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); @@ -852,6 +882,58 @@ function MyController(hand) { } }; + this.searchEnter = function() { + this.equipHotspotOverlays = []; + + // find entities near the avatar that might be equipable. + var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); + var i, l = entities.length; + for (i = 0; i < l; i++) { + + // is this entity equipable? + var grabData = getEntityCustomData(GRABBABLE_DATA_KEY, entities[i], undefined); + var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); + if (grabData) { + + var hotspotPos = grabProps.position; + + // does this entity have an equip point? + var wearableData = getEntityCustomData("wearable", entities[i], undefined); + if (wearableData) { + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (wearableData[handJointName]) { + // draw the hotspot around the equip point. + hotspotPos = wearableData[handJointName][0]; + } + } + + // draw a hotspot! + this.equipHotspotOverlays.push(Overlays.addOverlay("cube", { + position: hotspotPos, + size: 0.1, + color: { red: 90, green: 255, blue: 90 }, + alpha: 1, + solid: false, + visible: true, + dashed: false, + lineWidth: 2.0, + ignoreRayIntersection: true, // this never ray intersects + drawInFront: true + })); + } + } + }; + + this.searchExit = function() { + + // delete all equip hotspots + var i, l = this.equipHotspotOverlays.length; + for (i = 0; i < l; i++) { + Overlays.deleteOverlay(this.equipHotspotOverlays[i]); + } + this.equipHotspotOverlays = []; + }; + this.search = function() { this.grabbedEntity = null; this.isInitialGrab = false; @@ -951,7 +1033,7 @@ function MyController(hand) { var propsForCandidate = Entities.getEntityProperties(candidateEntities[i], GRABBABLE_PROPERTIES); var near = (nearPickedCandidateEntities.indexOf(candidateEntities[i]) >= 0); - var isPhysical = this.propsArePhysical(propsForCandidate); + var isPhysical = propsArePhysical(propsForCandidate); var grabbable; if (isPhysical) { // physical things default to grabbable @@ -1030,7 +1112,7 @@ function MyController(hand) { if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.bumperSqueezed())) { // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. var near = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; - var isPhysical = this.propsArePhysical(props); + var isPhysical = propsArePhysical(props); // near or far trigger if (grabbableData.wantsTrigger) { @@ -1462,7 +1544,7 @@ function MyController(hand) { } } - var isPhysical = this.propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); + var isPhysical = propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); if (isPhysical && this.state == STATE_NEAR_GRABBING) { // grab entity via action if (!this.setupHoldAction()) { @@ -1879,7 +1961,7 @@ function MyController(hand) { var forceVelocity = false; var doSetVelocity = false; - if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && this.propsArePhysical(props)) { + if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && propsArePhysical(props)) { // TODO: EntityScriptingInterface::convertLocationToScriptSemantics should be setting up // props.velocity to be a world-frame velocity and localVelocity to be vs parent. Until that // is done, we use a measured velocity here so that things held via a bumper-grab / parenting-grab From 6d462a477dd3620ddc81efb6355af7df6dd1522e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 1 Jun 2016 16:54:14 -0700 Subject: [PATCH 0399/1237] Change equip-hotspot to a green sphere. --- .../system/controllers/handControllerGrab.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 0561e73e1f..4163b07c6d 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -897,28 +897,26 @@ function MyController(hand) { var hotspotPos = grabProps.position; - // does this entity have an equip point? + // does this entity have an attach point? var wearableData = getEntityCustomData("wearable", entities[i], undefined); if (wearableData) { var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; if (wearableData[handJointName]) { - // draw the hotspot around the equip point. + // draw the hotspot around the attach point. hotspotPos = wearableData[handJointName][0]; } } // draw a hotspot! - this.equipHotspotOverlays.push(Overlays.addOverlay("cube", { + this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { position: hotspotPos, - size: 0.1, + size: 0.2, color: { red: 90, green: 255, blue: 90 }, - alpha: 1, - solid: false, + alpha: 0.7, + solid: true, visible: true, - dashed: false, - lineWidth: 2.0, - ignoreRayIntersection: true, // this never ray intersects - drawInFront: true + ignoreRayIntersection: false, + drawInFront: false })); } } From b836a580ffda8b2625e64ec404d3520cfcf13ed5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 6 Jun 2016 14:39:14 -0700 Subject: [PATCH 0400/1237] Updated some debug print information --- scripts/system/controllers/handControllerGrab.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4163b07c6d..8a96e9d80c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -376,7 +376,7 @@ function MyController(hand) { print("WARNING: could not find updateMethod for state " + stateToName(this.state)); } } else { - print("WARNING: could not find state " + this.state + " " + CONTROLLER_STATE_MACHINE[this.state]); + print("WARNING: could not find state " + this.state + " in state machine"); } }; @@ -390,7 +390,7 @@ function MyController(hand) { if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); - print("STATE (" + this.hand + "): " + oldStateName + " --> " + newStateName); + print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName); } // exit the old state @@ -401,7 +401,7 @@ function MyController(hand) { exitMethod.call(this); } } else { - print("WARNING: could not find this.state " + this.state); + print("WARNING: could not find state " + this.state + " in state machine"); } // enter the new state @@ -412,7 +412,7 @@ function MyController(hand) { enterMethod.call(this); } } else { - print("WARNING: could not find newState " + newState); + print("WARNING: could not find newState " + newState + " in state machine"); } this.state = newState; @@ -943,6 +943,7 @@ function MyController(hand) { this.setState(STATE_RELEASE); return; } + if (this.state == STATE_HOLD_SEARCHING && this.bumperReleased()) { this.setState(STATE_RELEASE); return; From b60b9bb312a4528b620dc384cdfd9b5101f5d117 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Mon, 6 Jun 2016 14:54:15 -0700 Subject: [PATCH 0401/1237] compiler fixes for GCC/clang --- cmake/macros/SetupHifiLibrary.cmake | 10 ++++++++++ .../{avx/AudioSRC_avx.cpp => avx2/AudioSRC_avx2.cpp} | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) rename libraries/audio/src/{avx/AudioSRC_avx.cpp => avx2/AudioSRC_avx2.cpp} (98%) diff --git a/cmake/macros/SetupHifiLibrary.cmake b/cmake/macros/SetupHifiLibrary.cmake index 628f65b278..26c769c6e6 100644 --- a/cmake/macros/SetupHifiLibrary.cmake +++ b/cmake/macros/SetupHifiLibrary.cmake @@ -24,6 +24,16 @@ macro(SETUP_HIFI_LIBRARY) set_source_files_properties(${SRC} PROPERTIES COMPILE_FLAGS -mavx) endif() endforeach() + + # add compiler flags to AVX2 source files + file(GLOB_RECURSE AVX2_SRCS "src/avx2/*.cpp" "src/avx2/*.c") + foreach(SRC ${AVX2_SRCS}) + if (WIN32) + set_source_files_properties(${SRC} PROPERTIES COMPILE_FLAGS /arch:AVX2) + elseif (APPLE OR UNIX) + set_source_files_properties(${SRC} PROPERTIES COMPILE_FLAGS "-mavx2 -mfma") + endif() + endforeach() setup_memory_debugger() diff --git a/libraries/audio/src/avx/AudioSRC_avx.cpp b/libraries/audio/src/avx2/AudioSRC_avx2.cpp similarity index 98% rename from libraries/audio/src/avx/AudioSRC_avx.cpp rename to libraries/audio/src/avx2/AudioSRC_avx2.cpp index dcc56b94ad..e634554bfb 100644 --- a/libraries/audio/src/avx/AudioSRC_avx.cpp +++ b/libraries/audio/src/avx2/AudioSRC_avx2.cpp @@ -1,5 +1,5 @@ // -// AudioSRC_avx.cpp +// AudioSRC_avx2.cpp // libraries/audio/src // // Created by Ken Cooke on 6/5/16. @@ -16,8 +16,8 @@ #include "../AudioSRC.h" -#ifndef __AVX__ -#error Must be compiled with /arch:AVX or -mavx. +#ifndef __AVX2__ +#error Must be compiled with /arch:AVX2 or -mavx2 -mfma. #endif // high/low part of int64_t From e7743cd8e2603927da7099ea6e82e696e237fd1c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 15:03:08 -0700 Subject: [PATCH 0402/1237] added options for both hands (default), all devices, short pulse, and fixed touch timing mechanism --- .../controllers/src/controllers/InputDevice.h | 8 +++- .../src/controllers/ScriptingInterface.cpp | 18 +++++++- .../src/controllers/ScriptingInterface.h | 5 ++- .../src/controllers/UserInputMapper.cpp | 25 ++++++++++- .../src/controllers/UserInputMapper.h | 4 +- plugins/hifiSdl2/src/Joystick.cpp | 2 +- plugins/hifiSdl2/src/Joystick.h | 2 +- .../oculus/src/OculusControllerManager.cpp | 45 ++++++++++++------- plugins/oculus/src/OculusControllerManager.h | 15 +++++-- plugins/openvr/src/ViveControllerManager.cpp | 10 ++--- plugins/openvr/src/ViveControllerManager.h | 12 ++--- 11 files changed, 105 insertions(+), 41 deletions(-) diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index cc158497e0..10e1a104f4 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -31,6 +31,12 @@ namespace controller { class Endpoint; using EndpointPointer = std::shared_ptr; +enum Hand { + LEFT = 0, + RIGHT, + BOTH +}; + // NOTE: If something inherits from both InputDevice and InputPlugin, InputPlugin must go first. // e.g. class Example : public InputPlugin, public InputDevice // instead of class Example : public InputDevice, public InputPlugin @@ -56,7 +62,7 @@ public: const QString& getName() const { return _name; } // By default, Input Devices do not support haptics - virtual bool triggerHapticPulse(float strength, float duration, bool leftHand) { return false; } + virtual bool triggerHapticPulse(float strength, float duration, controller::Hand hand) { return false; } // Update call MUST be called once per simulation loop // It takes care of updating the action states and deltas diff --git a/libraries/controllers/src/controllers/ScriptingInterface.cpp b/libraries/controllers/src/controllers/ScriptingInterface.cpp index 97b7a9ddd9..78e9378c18 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.cpp +++ b/libraries/controllers/src/controllers/ScriptingInterface.cpp @@ -140,8 +140,22 @@ namespace controller { return DependencyManager::get()->getActionNames(); } - bool ScriptingInterface::triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand) const { - return DependencyManager::get()->triggerHapticPulse(device, strength, duration, leftHand); + bool ScriptingInterface::triggerHapticPulse(float strength, float duration, controller::Hand hand) const { + return DependencyManager::get()->triggerHapticPulse(strength, duration, hand); + } + + bool ScriptingInterface::triggerShortHapticPulse(float strength, controller::Hand hand) const { + const float SHORT_HAPTIC_DURATION_MS = 250.0f; + return DependencyManager::get()->triggerHapticPulse(strength, SHORT_HAPTIC_DURATION_MS, hand); + } + + bool ScriptingInterface::triggerHapticPulseOnDevice(unsigned int device, float strength, float duration, controller::Hand hand) const { + return DependencyManager::get()->triggerHapticPulseOnDevice(device, strength, duration, hand); + } + + bool ScriptingInterface::triggerShortHapticPulseOnDevice(unsigned int device, float strength, controller::Hand hand) const { + const float SHORT_HAPTIC_DURATION_MS = 250.0f; + return DependencyManager::get()->triggerHapticPulseOnDevice(device, strength, SHORT_HAPTIC_DURATION_MS, hand); } void ScriptingInterface::updateMaps() { diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index 015ec25454..713e864561 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -84,7 +84,10 @@ namespace controller { Q_INVOKABLE Pose getPoseValue(const int& source) const; Q_INVOKABLE Pose getPoseValue(StandardPoseChannel source, uint16_t device = 0) const; - Q_INVOKABLE bool triggerHapticPulse(unsigned int device, float strength, float duration, bool leftHand = true) const; + Q_INVOKABLE bool triggerHapticPulse(float strength, float duration, controller::Hand hand = BOTH) const; + Q_INVOKABLE bool triggerShortHapticPulse(float strength, controller::Hand hand = BOTH) const; + Q_INVOKABLE bool triggerHapticPulseOnDevice(unsigned int device, float strength, float duration, controller::Hand hand = BOTH) const; + Q_INVOKABLE bool triggerShortHapticPulseOnDevice(unsigned int device, float strength, controller::Hand hand = BOTH) const; Q_INVOKABLE QObject* newMapping(const QString& mappingName = QUuid::createUuid().toString()); Q_INVOKABLE void enableMapping(const QString& mappingName, bool enable = true); diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index c7c62ad7d9..921f78c613 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -336,10 +336,19 @@ QVector UserInputMapper::getActionNames() const { return result; } -bool UserInputMapper::triggerHapticPulse(uint16 deviceID, float strength, float duration, bool leftHand) { +bool UserInputMapper::triggerHapticPulse(float strength, float duration, controller::Hand hand) { + Locker locker(_lock); + bool toReturn = false; + for (auto device : _registeredDevices) { + toReturn = toReturn || device.second->triggerHapticPulse(strength, duration, hand); + } + return toReturn; +} + +bool UserInputMapper::triggerHapticPulseOnDevice(uint16 deviceID, float strength, float duration, controller::Hand hand) { Locker locker(_lock); if (_registeredDevices.find(deviceID) != _registeredDevices.end()) { - return _registeredDevices[deviceID]->triggerHapticPulse(strength, duration, leftHand); + return _registeredDevices[deviceID]->triggerHapticPulse(strength, duration, hand); } return false; } @@ -348,6 +357,7 @@ int actionMetaTypeId = qRegisterMetaType(); int inputMetaTypeId = qRegisterMetaType(); int inputPairMetaTypeId = qRegisterMetaType(); int poseMetaTypeId = qRegisterMetaType("Pose"); +int handMetaTypeId = qRegisterMetaType(); QScriptValue inputToScriptValue(QScriptEngine* engine, const Input& input); void inputFromScriptValue(const QScriptValue& object, Input& input); @@ -355,6 +365,8 @@ QScriptValue actionToScriptValue(QScriptEngine* engine, const Action& action); void actionFromScriptValue(const QScriptValue& object, Action& action); QScriptValue inputPairToScriptValue(QScriptEngine* engine, const Input::NamedPair& inputPair); void inputPairFromScriptValue(const QScriptValue& object, Input::NamedPair& inputPair); +QScriptValue handToScriptValue(QScriptEngine* engine, const controller::Hand& hand); +void handFromScriptValue(const QScriptValue& object, controller::Hand& hand); QScriptValue inputToScriptValue(QScriptEngine* engine, const Input& input) { QScriptValue obj = engine->newObject(); @@ -393,12 +405,21 @@ void inputPairFromScriptValue(const QScriptValue& object, Input::NamedPair& inpu inputPair.second = QString(object.property("inputName").toVariant().toString()); } +QScriptValue handToScriptValue(QScriptEngine* engine, const controller::Hand& hand) { + return engine->newVariant((int)hand); +} + +void handFromScriptValue(const QScriptValue& object, controller::Hand& hand) { + hand = Hand(object.toVariant().toInt()); +} + void UserInputMapper::registerControllerTypes(QScriptEngine* engine) { qScriptRegisterSequenceMetaType >(engine); qScriptRegisterSequenceMetaType(engine); qScriptRegisterMetaType(engine, actionToScriptValue, actionFromScriptValue); qScriptRegisterMetaType(engine, inputToScriptValue, inputFromScriptValue); qScriptRegisterMetaType(engine, inputPairToScriptValue, inputPairFromScriptValue); + qScriptRegisterMetaType(engine, handToScriptValue, handFromScriptValue); qScriptRegisterMetaType(engine, Pose::toScriptValue, Pose::fromScriptValue); } diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index b23657fff7..811b621b09 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -89,7 +89,8 @@ namespace controller { void setActionState(Action action, float value) { _actionStates[toInt(action)] = value; } void deltaActionState(Action action, float delta) { _actionStates[toInt(action)] += delta; } void setActionState(Action action, const Pose& value) { _poseStates[toInt(action)] = value; } - bool triggerHapticPulse(uint16 deviceID, float strength, float duration, bool leftHand); + bool triggerHapticPulse(float strength, float duration, controller::Hand hand); + bool triggerHapticPulseOnDevice(uint16 deviceID, float strength, float duration, controller::Hand hand); static Input makeStandardInput(controller::StandardButtonChannel button); static Input makeStandardInput(controller::StandardAxisChannel axis); @@ -200,6 +201,7 @@ Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(controller::Input) Q_DECLARE_METATYPE(controller::Action) Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(controller::Hand) // Cheating. using UserInputMapper = controller::UserInputMapper; diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index cf7dde371b..c20670d4bb 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -67,7 +67,7 @@ void Joystick::handleButtonEvent(const SDL_ControllerButtonEvent& event) { } } -bool Joystick::triggerHapticPulse(float strength, float duration, bool leftHand) { +bool Joystick::triggerHapticPulse(float strength, float duration, controller::Hand hand) { if (SDL_HapticRumblePlay(_sdlHaptic, strength, duration) != 0) { return false; } diff --git a/plugins/hifiSdl2/src/Joystick.h b/plugins/hifiSdl2/src/Joystick.h index 4f785f85ce..b37d22de39 100644 --- a/plugins/hifiSdl2/src/Joystick.h +++ b/plugins/hifiSdl2/src/Joystick.h @@ -37,7 +37,7 @@ public: virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void focusOutEvent() override; - bool triggerHapticPulse(float strength, float duration, bool leftHand) override; + bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override; Joystick() : InputDevice("GamePad") {} ~Joystick(); diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 3151db1d90..2e7044bd69 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -55,11 +55,6 @@ bool OculusControllerManager::activate() { userInputMapper->registerDevice(_touch); } - _leftHapticTimer.setSingleShot(true); - _rightHapticTimer.setSingleShot(true); - connect(&_leftHapticTimer, SIGNAL(timeout()), this, SLOT(stopHapticPulse(true))); - connect(&_rightHapticTimer, SIGNAL(timeout()), this, SLOT(stopHapticPulse(false))); - return true; } @@ -221,6 +216,21 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control _buttonPressedMap.insert(pair.second); } } + + // Haptics + { + Locker locker(_lock); + if (_leftHapticDuration > 0.0f) { + _leftHapticDuration -= deltaTime; + } else { + stopHapticPulse(true); + } + if (_rightHapticDuration > 0.0f) { + _rightHapticDuration -= deltaTime; + } else { + stopHapticPulse(false); + } + } } void OculusControllerManager::TouchDevice::focusOutEvent() { @@ -239,19 +249,22 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, pose.velocity = toGlm(handPose.LinearVelocity); } -bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, float duration, bool leftHand) { - auto handType = (leftHand ? ovrControllerType_LTouch : ovrControllerType_RTouch); - if (ovr_SetControllerVibration(_parent._session, handType, 1.0f, strength) != ovrSuccess) { - return false; +bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { + Locker locker(_lock); + bool toReturn = true; + if (hand == controller::BOTH || hand == controller::LEFT) { + if (ovr_SetControllerVibration(_parent._session, ovrControllerType_LTouch, 1.0f, strength) != ovrSuccess) { + toReturn = false; + } + _leftHapticDuration = duration; } - - if (leftHand) { - _parent._leftHapticTimer.start(duration); - } else { - _parent._rightHapticTimer.start(duration); + if (hand == controller::BOTH || hand == controller::RIGHT) { + if (ovr_SetControllerVibration(_parent._session, ovrControllerType_RTouch, 1.0f, strength) != ovrSuccess) { + toReturn = false; + } + _rightHapticDuration = duration; } - - return true; + return toReturn; } void OculusControllerManager::TouchDevice::stopHapticPulse(bool leftHand) { diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index e62c5f45ea..244fe41f67 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -10,7 +10,6 @@ #define hifi__OculusControllerManager #include -#include #include #include @@ -68,20 +67,28 @@ private: void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; - bool triggerHapticPulse(float strength, float duration, bool leftHand) override; + bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override; private: void stopHapticPulse(bool leftHand); void handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose); int _trackedControllers { 0 }; + + // perform an action when the TouchDevice mutex is acquired. + using Locker = std::unique_lock; + template + void withLock(F&& f) { Locker locker(_lock); f(); } + + float _leftHapticDuration { 0.0f }; + float _rightHapticDuration { 0.0f }; + mutable std::recursive_mutex _lock; + friend class OculusControllerManager; }; ovrSession _session { nullptr }; ovrInputState _inputState {}; RemoteDevice::Pointer _remote; - QTimer _leftHapticTimer; - QTimer _rightHapticTimer; TouchDevice::Pointer _touch; static const QString NAME; }; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 39b5e620ad..27b4887edd 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -452,12 +452,13 @@ void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = avatarPose.transform(controllerToAvatar); } -bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, bool leftHand) { +bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { Locker locker(_lock); - if (leftHand) { + if (hand == controller::BOTH || hand == controller::LEFT) { _leftHapticStrength = strength; _leftHapticDuration = duration; - } else { + } + if (hand == controller::BOTH || hand == controller::RIGHT) { _rightHapticStrength = strength; _rightHapticDuration = duration; } @@ -481,9 +482,6 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef _system->TriggerHapticPulse(deviceIndex, 0, hapticTime); } - // Must wait 5 ms before triggering another pulse on this controller - // https://github.com/ValveSoftware/openvr/wiki/IVRSystem::TriggerHapticPulse - const float HAPTIC_RESET_TIME = 5.0f; float remainingHapticTime = duration - (hapticTime / 1000.0f + deltaTime * 1000.0f); // in milliseconds if (leftHand) { _leftHapticDuration = remainingHapticTime; diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 9bdbd0370e..3a2ef1573f 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -48,7 +48,7 @@ public: private: class InputDevice : public controller::InputDevice { public: - InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system), _leftHapticDuration(0.0f), _rightHapticDuration(0.0f) {} + InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) {} private: // Device functions controller::Input::NamedVector getAvailableInputs() const override; @@ -56,7 +56,7 @@ private: void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; - bool triggerHapticPulse(float strength, float duration, bool leftHand) override; + bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override; void hapticsHelper(float deltaTime, bool leftHand); void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); @@ -100,10 +100,10 @@ private: int _trackedControllers { 0 }; vr::IVRSystem*& _system; - float _leftHapticStrength; - float _leftHapticDuration; - float _rightHapticStrength; - float _rightHapticDuration; + float _leftHapticStrength { 0.0f }; + float _leftHapticDuration { 0.0f }; + float _rightHapticStrength { 0.0f }; + float _rightHapticDuration { 0.0f }; mutable std::recursive_mutex _lock; friend class ViveControllerManager; From cea0d74c358cbe61ecd6ad19d53ddc6f08e34b5e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 15:20:10 -0700 Subject: [PATCH 0403/1237] fix spaces and touch duration --- plugins/oculus/src/OculusControllerManager.cpp | 4 ++-- plugins/openvr/src/ViveControllerManager.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 2e7044bd69..21d97adbda 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -221,12 +221,12 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control { Locker locker(_lock); if (_leftHapticDuration > 0.0f) { - _leftHapticDuration -= deltaTime; + _leftHapticDuration -= deltaTime * 1000.0f; // milliseconds } else { stopHapticPulse(true); } if (_rightHapticDuration > 0.0f) { - _rightHapticDuration -= deltaTime; + _rightHapticDuration -= deltaTime * 1000.0f; // milliseconds } else { stopHapticPulse(false); } diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 27b4887edd..8e4b277bd7 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -477,8 +477,8 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef // Vive Controllers only support duration up to 4 ms, which is short enough that any variation feels more like strength const float MAX_HAPTIC_TIME = 3999.0f; // in microseconds - float hapticTime = strength*MAX_HAPTIC_TIME; - if (hapticTime < duration*1000.0f) { + float hapticTime = strength * MAX_HAPTIC_TIME; + if (hapticTime < duration * 1000.0f) { _system->TriggerHapticPulse(deviceIndex, 0, hapticTime); } From b59d597780fd4f3ea7789551e0634395a34aa6c1 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 15:35:12 -0700 Subject: [PATCH 0404/1237] if overlapping calls, haptics take on strength and duration of call that will finish last --- plugins/oculus/src/OculusControllerManager.cpp | 10 ++++++---- plugins/oculus/src/OculusControllerManager.h | 2 ++ plugins/openvr/src/ViveControllerManager.cpp | 8 ++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 21d97adbda..02ac44d77c 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -253,16 +253,18 @@ bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, fl Locker locker(_lock); bool toReturn = true; if (hand == controller::BOTH || hand == controller::LEFT) { - if (ovr_SetControllerVibration(_parent._session, ovrControllerType_LTouch, 1.0f, strength) != ovrSuccess) { + _leftHapticStrength = (duration > _leftHapticDuration) ? strength : _leftHapticStrength; + if (ovr_SetControllerVibration(_parent._session, ovrControllerType_LTouch, 1.0f, _leftHapticStrength) != ovrSuccess) { toReturn = false; } - _leftHapticDuration = duration; + _leftHapticDuration = std::max(duration, _leftHapticDuration); } if (hand == controller::BOTH || hand == controller::RIGHT) { - if (ovr_SetControllerVibration(_parent._session, ovrControllerType_RTouch, 1.0f, strength) != ovrSuccess) { + _rightHapticStrength = (duration > _rightHapticDuration) ? strength : _rightHapticStrength; + if (ovr_SetControllerVibration(_parent._session, ovrControllerType_RTouch, 1.0f, _rightHapticStrength) != ovrSuccess) { toReturn = false; } - _rightHapticDuration = duration; + _rightHapticDuration = std::max(duration, _rightHapticDuration); } return toReturn; } diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index 244fe41f67..3c5cdeb7c6 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -80,7 +80,9 @@ private: void withLock(F&& f) { Locker locker(_lock); f(); } float _leftHapticDuration { 0.0f }; + float _leftHapticStrength { 0.0f }; float _rightHapticDuration { 0.0f }; + float _rightHapticStrength { 0.0f }; mutable std::recursive_mutex _lock; friend class OculusControllerManager; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 8e4b277bd7..7254117922 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -455,12 +455,12 @@ void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { Locker locker(_lock); if (hand == controller::BOTH || hand == controller::LEFT) { - _leftHapticStrength = strength; - _leftHapticDuration = duration; + _leftHapticStrength = (duration > _leftHapticDuration) ? strength : _leftHapticStrength; + _leftHapticDuration = std::max(duration, _leftHapticDuration); } if (hand == controller::BOTH || hand == controller::RIGHT) { - _rightHapticStrength = strength; - _rightHapticDuration = duration; + _rightHapticStrength = (duration > _rightHapticDuration) ? strength : _rightHapticStrength; + _rightHapticDuration = std::max(duration, _rightHapticDuration); } return true; } From 1994eabc981ca3b212d0d9298b5b2ee9ef91b96f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 16:22:02 -0700 Subject: [PATCH 0405/1237] grip buttons on vive and touch will make you active in away.js --- scripts/system/away.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system/away.js b/scripts/system/away.js index 2b2ea8a42b..38b0f13c00 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -265,9 +265,11 @@ eventMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(goActive); eventMapping.from(Controller.Standard.LT).peek().to(goActive); eventMapping.from(Controller.Standard.LB).peek().to(goActive); eventMapping.from(Controller.Standard.LS).peek().to(goActive); +eventMapping.from(Controller.Standard.LeftGrip).peek().to(goActive); eventMapping.from(Controller.Standard.RT).peek().to(goActive); eventMapping.from(Controller.Standard.RB).peek().to(goActive); eventMapping.from(Controller.Standard.RS).peek().to(goActive); +eventMapping.from(Controller.Standard.RightGrip).peek().to(goActive); eventMapping.from(Controller.Standard.Back).peek().to(goActive); eventMapping.from(Controller.Standard.Start).peek().to(goActive); Controller.enableMapping(eventMappingName); From b6af46cac9ab316187eec143610dd333b4f43e7b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 7 Jun 2016 11:30:07 +1200 Subject: [PATCH 0406/1237] Add "Load Defaults" button to Running Scripts dialog And rename "Stop All" to "Remove All". --- .../qml/hifi/dialogs/RunningScripts.qml | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 31bb553809..cbbbec5bff 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -23,9 +23,9 @@ Window { title: "Running Scripts" resizable: true destroyOnInvisible: true - implicitWidth: 400 + implicitWidth: 424 implicitHeight: isHMD ? 695 : 728 - minSize: Qt.vector2d(200, 300) + minSize: Qt.vector2d(424, 300) HifiConstants { id: hifi } @@ -83,6 +83,11 @@ Window { scripts.reloadAllScripts(); } + function loadDefaults() { + console.log("Load default scripts"); + scripts.loadOneScript(scripts.defaultScriptsPath + "/defaultScripts.js"); + } + function stopAll() { console.log("Stop all scripts"); scripts.stopAllScripts(); @@ -101,13 +106,13 @@ Window { spacing: hifi.dimensions.contentSpacing.x HifiControls.Button { - text: "Reload all" + text: "Reload All" color: hifi.buttons.black onClicked: reloadAll() } HifiControls.Button { - text: "Stop all" + text: "Remove All" color: hifi.buttons.red onClicked: stopAll() } @@ -215,7 +220,6 @@ Window { Row { spacing: hifi.dimensions.contentSpacing.x - anchors.right: parent.right HifiControls.Button { text: "from URL" @@ -253,6 +257,12 @@ Window { onTriggered: ApplicationInterface.loadDialog(); } } + + HifiControls.Button { + text: "Load Defaults" + color: hifi.buttons.black + onClicked: loadDefaults() + } } HifiControls.VerticalSpacer {} From 0c18df62782aac62ef325ecebfbc7fc0b0aab95c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 6 Jun 2016 16:32:18 -0700 Subject: [PATCH 0407/1237] don't restart domain-server if the only settings changes where permissions --- domain-server/src/DomainGatekeeper.cpp | 61 ++++++++++++++++++- domain-server/src/DomainGatekeeper.h | 6 +- domain-server/src/DomainServer.cpp | 54 ++++++++++------ domain-server/src/DomainServer.h | 2 + .../src/DomainServerSettingsManager.cpp | 54 ++++++++++------ .../src/DomainServerSettingsManager.h | 15 +++-- libraries/networking/src/NodePermissions.h | 7 +++ 7 files changed, 152 insertions(+), 47 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index adc95f3c38..0abae339c6 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -123,6 +123,61 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer nodesToKill; + + auto limitedNodeList = DependencyManager::get(); + limitedNodeList->eachNodeBreakable([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){ + QString username = node->getPermissions().getUserName(); + NodePermissions userPerms(username); + + if (node->getPermissions().isAssignment) { + // this node is an assignment-client + userPerms.isAssignment = true; + userPerms.canAdjustLocks = true; + userPerms.canRezPermanentEntities = true; + } else { + // this node is an agent + userPerms.setAll(false); + + const QHostAddress& addr = node->getLocalSocket().getAddress(); + bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() || + addr == QHostAddress::LocalHost); + if (isLocalUser) { + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); + } + + if (username.isEmpty()) { + userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); + } else { + if (_server->_settingsManager.havePermissionsForName(username)) { + userPerms = _server->_settingsManager.getPermissionsForName(username); + } else { + userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLoggedIn); + } + } + } + + node->setPermissions(userPerms); + + if (!userPerms.canConnectToDomain) { + qDebug() << "node" << node->getUUID() << "no longer has permission to connect."; + // hang up on this node + nodesToKill << node; + } + + return true; + }); + + foreach (auto node, nodesToKill) { + emit killNode(node); + } +} + SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeConnectionData& nodeConnection, const PendingAssignedNodeData& pendingAssignment) { @@ -166,6 +221,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo // always allow assignment clients to create and destroy entities NodePermissions userPerms; + userPerms.isAssignment = true; userPerms.canAdjustLocks = true; userPerms.canRezPermanentEntities = true; newNode->setPermissions(userPerms); @@ -184,7 +240,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect NodePermissions userPerms(username); userPerms.setAll(false); - // check if this user is on our local machine - if this is true they are always allowed to connect + // check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress(); bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); @@ -209,9 +265,10 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect qDebug() << "user-permissions: no username, so:" << userPerms; } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they are sent us a username and the signature verifies it + userPerms.setUserName(username); if (_server->_settingsManager.havePermissionsForName(username)) { // we have specific permissions for this user. - userPerms |= _server->_settingsManager.getPermissionsForName(username); + userPerms = _server->_settingsManager.getPermissionsForName(username); qDebug() << "user-permissions: specific user matches, so:" << userPerms; } else { // they are logged into metaverse, but we don't have specific permissions for them. diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 352f84385e..bac50af2ec 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -51,8 +51,12 @@ public slots: void publicKeyJSONCallback(QNetworkReply& requestReply); signals: + void killNode(SharedNodePointer node); void connectedNode(SharedNodePointer node); - + +public slots: + void updateNodePermissions(); + private slots: void handlePeerPingTimeout(); private: diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 020983eb03..9abc605912 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -97,6 +97,13 @@ DomainServer::DomainServer(int argc, char* argv[]) : // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); + // if a connected node loses connection privileges, hang up on it + connect(&_gatekeeper, &DomainGatekeeper::killNode, this, &DomainServer::handleKillNode); + + // if permissions are updated, relay the changes to the Node datastructures + connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions, + &_gatekeeper, &DomainGatekeeper::updateNodePermissions); + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -2113,35 +2120,42 @@ void DomainServer::processPathQueryPacket(QSharedPointer messag void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer message) { // This packet has been matched to a source node and they're asking not to be in the domain anymore auto limitedNodeList = DependencyManager::get(); - + const QUuid& nodeUUID = message->getSourceID(); - + qDebug() << "Received a disconnect request from node with UUID" << nodeUUID; - + // we want to check what type this node was before going to kill it so that we can avoid sending the RemovedNode // packet to nodes that don't care about this type auto nodeToKill = limitedNodeList->nodeWithUUID(nodeUUID); - + if (nodeToKill) { - auto nodeType = nodeToKill->getType(); - limitedNodeList->killNodeWithUUID(nodeUUID); - - static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID); - - removedNodePacket->reset(); - removedNodePacket->write(nodeUUID.toRfc4122()); - - // broadcast out the DomainServerRemovedNode message - limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool { - // only send the removed node packet to nodes that care about the type of node this was - auto nodeLinkedData = dynamic_cast(otherNode->getLinkedData()); - return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType); - }, [&limitedNodeList](const SharedNodePointer& otherNode){ - limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode); - }); + handleKillNode(nodeToKill); } } +void DomainServer::handleKillNode(SharedNodePointer nodeToKill) { + auto nodeType = nodeToKill->getType(); + auto limitedNodeList = DependencyManager::get(); + const QUuid& nodeUUID = nodeToKill->getUUID(); + + limitedNodeList->killNodeWithUUID(nodeUUID); + + static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID); + + removedNodePacket->reset(); + removedNodePacket->write(nodeUUID.toRfc4122()); + + // broadcast out the DomainServerRemovedNode message + limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool { + // only send the removed node packet to nodes that care about the type of node this was + auto nodeLinkedData = dynamic_cast(otherNode->getLinkedData()); + return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType); + }, [&limitedNodeList](const SharedNodePointer& otherNode){ + limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode); + }); +} + void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer message) { static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index c39e405380..fb8a7d4e1a 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -111,6 +111,8 @@ private: unsigned int countConnectedUsers(); + void handleKillNode(SharedNodePointer nodeToKill); + void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr); QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index d52d926816..68e3ddcc2b 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -92,7 +92,8 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer agentPermissions, QString keyPath) { QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security"); @@ -285,20 +285,23 @@ void DomainServerSettingsManager::packPermissionsForMap(const QStringList& argum } } -void DomainServerSettingsManager::packPermissions(const QStringList& argumentList) { +void DomainServerSettingsManager::packPermissions() { // transfer details from _agentPermissions to _configMap - packPermissionsForMap(argumentList, "standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH); + packPermissionsForMap("standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH); // save settings for specific users - packPermissionsForMap(argumentList, "permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH); + packPermissionsForMap("permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH); persistToFile(); - _configMap.loadMasterAndUserConfig(argumentList); + _configMap.loadMasterAndUserConfig(_argumentList); } -void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentList) { +void DomainServerSettingsManager::unpackPermissions() { // transfer details from _configMap to _agentPermissions; + _standardAgentPermissions.clear(); + _agentPermissions.clear(); + bool foundLocalhost = false; bool foundAnonymous = false; bool foundLoggedIn = false; @@ -365,7 +368,7 @@ void DomainServerSettingsManager::unpackPermissions(const QStringList& argumentL } if (needPack) { - packPermissions(argumentList); + packPermissions(); } #ifdef WANT_DEBUG @@ -463,7 +466,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection qDebug() << "DomainServerSettingsManager postedObject -" << postedObject; // we recurse one level deep below each group for the appropriate setting - recurseJSONObjectAndOverwriteSettings(postedObject); + bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject); // store whatever the current _settingsMap is to file persistToFile(); @@ -473,8 +476,13 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); // defer a restart to the domain-server, this gives our HTTPConnection enough time to respond - const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; - QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); + if (restartRequired) { + const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; + QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); + } else { + unpackPermissions(); + emit updateNodePermissions(); + } return true; } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) { @@ -677,9 +685,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson return QJsonObject(); } -void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) { +bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) { auto& settingsVariant = _configMap.getUserConfig(); - + bool needRestart = false; + // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { QJsonValue rootValue = postedObject[rootKey]; @@ -727,6 +736,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject); + if (rootKey != "security") { + needRestart = true; + } } else { qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting."; } @@ -740,6 +752,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { QJsonValue settingValue = rootValue.toObject()[settingKey]; updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject); + if (rootKey != "security") { + needRestart = true; + } } else { qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey << "- cannot update setting."; @@ -755,6 +770,7 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ // re-merge the user and master configs after a settings change _configMap.mergeMasterAndUserConfigs(); + return needRestart; } void DomainServerSettingsManager::persistToFile() { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 51ee657961..f6a5ef6f89 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -47,12 +47,18 @@ public: NodePermissions getPermissionsForName(const QString& name) const; QStringList getAllNames() { return _agentPermissions.keys(); } +signals: + void updateNodePermissions(); + + private slots: void processSettingsRequestPacket(QSharedPointer message); private: + QStringList _argumentList; + QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false); - void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject); + bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject); void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); @@ -65,10 +71,9 @@ private: friend class DomainServer; - void packPermissionsForMap(const QStringList& argumentList, QString mapName, - QHash agentPermissions, QString keyPath); - void packPermissions(const QStringList& argumentList); - void unpackPermissions(const QStringList& argumentList); + void packPermissionsForMap(QString mapName, QHash agentPermissions, QString keyPath); + void packPermissions(); + void unpackPermissions(); QHash _standardAgentPermissions; // anonymous, logged-in, localhost QHash _agentPermissions; // specific account-names }; diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index adc13ba375..c153878a7e 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -37,6 +37,12 @@ public: QString getID() const { return _id; } + // the _id member isn't authenticated and _username is. + void setUserName(QString userName) { _userName = userName; } + QString getUserName() { return _userName; } + + bool isAssignment { false }; + // these 3 names have special meaning. static QString standardNameLocalhost; static QString standardNameLoggedIn; @@ -79,6 +85,7 @@ public: protected: QString _id; + QString _userName; }; const NodePermissions DEFAULT_AGENT_PERMISSIONS; From 8fa52fa15987067d67243e7f8d80583e6666aa6b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 16:48:21 -0700 Subject: [PATCH 0408/1237] trying to fix warning in joystick.cpp --- plugins/hifiSdl2/src/Joystick.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index c20670d4bb..69c83e5d23 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -25,7 +25,7 @@ Joystick::Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameControl _instanceId(instanceId) { if (!_sdlHaptic) { - qDebug(SDL_GetError()); + qDebug() << SDL_GetError(); } SDL_HapticRumbleInit(_sdlHaptic); } From 7a3cba8580ab1fcfd1bcb04b18007a69b9785a4d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 17:06:59 -0700 Subject: [PATCH 0409/1237] warning be gone --- plugins/hifiSdl2/src/Joystick.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index 69c83e5d23..aa6b358d38 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -25,7 +25,7 @@ Joystick::Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameControl _instanceId(instanceId) { if (!_sdlHaptic) { - qDebug() << SDL_GetError(); + qDebug() << QString(SDL_GetError()); } SDL_HapticRumbleInit(_sdlHaptic); } From 509f889c0d33394b58e24ba416639e946081d9fb Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 6 Jun 2016 18:40:12 -0700 Subject: [PATCH 0410/1237] fixed constant turn of 22.5 degrees --- interface/resources/controllers/standard.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index f25e0cc3c4..5c0ea09939 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -9,6 +9,8 @@ "to": "Actions.StepYaw", "filters": [ + { "type": "deadZone", "min": 0.15 }, + "constrainToInteger", { "type": "pulse", "interval": 0.5 }, { "type": "scale", "scale": 22.5 } ] From 577142a4d1a3e4d693d9499abe493c5797922b5b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 6 Jun 2016 18:41:14 -0700 Subject: [PATCH 0411/1237] add ping pong target and milk pail game --- .../DomainContent/Home/hoverGame/wrapper.js | 19 ++-- .../DomainContent/Home/pingPongGun/target.js | 78 +++++++++++++++ .../DomainContent/Home/reset.js | 95 +++++++++++++++++++ 3 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 unpublishedScripts/DomainContent/Home/pingPongGun/target.js diff --git a/unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js b/unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js index d38d4d47c3..a176e359fa 100644 --- a/unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js +++ b/unpublishedScripts/DomainContent/Home/hoverGame/wrapper.js @@ -9,7 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - +//var position = {x:1098.4813,y:461.6781,z:-71.3820} +// var dimensions = {x:1.0233,y:3.1541,z:0.8684} HoverGame = function(spawnPosition, spawnRotation) { var scriptURL = "atp:/hoverGame/hoverInner.js"; @@ -18,11 +19,7 @@ HoverGame = function(spawnPosition, spawnRotation) { type: 'Model', modelURL: 'atp:/hoverGame/hover.fbx', name: 'home_model_hoverGame_container', - dimensions: { - x: 0.2543, - y: 0.3269, - z: 0.4154 - }, + dimensions: {x:1.0233,y:3.1541,z:0.8684}, compoundShapeURL: 'atp:/hoverGame/hoverHull.obj', rotation: spawnRotation, script: scriptURL, @@ -34,7 +31,7 @@ HoverGame = function(spawnPosition, spawnRotation) { 'reset': true } }), - dynamic: true, + dynamic: false, position: spawnPosition }; @@ -60,10 +57,16 @@ HoverGame = function(spawnPosition, spawnRotation) { y:-9.8, z:0 }, - position: spawnPosition + position: spawnPosition, + userData:JSON.stringify({ + grabKey:{ + shouldCollideWith:'static' + } + }) }; var hoverContainer = Entities.addEntity(hoverContainerProps); + var hoverBall = Entities.addEntity(hoverContainerProps); function cleanup() { print('HOVER GAME CLEANUP!') diff --git a/unpublishedScripts/DomainContent/Home/pingPongGun/target.js b/unpublishedScripts/DomainContent/Home/pingPongGun/target.js new file mode 100644 index 0000000000..4898973f97 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/pingPongGun/target.js @@ -0,0 +1,78 @@ +// +// Copyright 2016 High Fidelity, Inc. +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +(function() { + + var _this = this; + _this.COLLISION_COOLDOWN_TIME = 5000; + + var startPosition = { + x: 1100.6343, + y: 460.5366, + z: -65.2142 + }; + + var startRotation = Quat.fromPitchYawRollDegrees(3.1471, -170.4121, -0.0060) + + _this.preload = function(entityID) { + + //set our id so other methods can get it. + _this.entityID = entityID; + + //variables we will use to keep track of when to reset the cow + _this.timeSinceLastCollision = 0; + _this.shouldUntip = true; + } + + _this.collisionWithEntity = function(myID, otherID, collisionInfo) { + //we dont actually use any of the parameters above, since we don't really care what we collided with, or the details of the collision. + print('JBP TARGET COLLISION') + //5 seconds after a collision, upright the target. protect from multiple collisions in a short timespan with the 'shouldUntip' variable + if (_this.shouldUntip) { + //in Hifi, preface setTimeout with Script.setTimeout + Script.setTimeout(function() { + _this.untip(); + _this.shouldUntip = true; + }, _this.COLLISION_COOLDOWN_TIME); + } + + _this.shouldUntip = false; + + } + + _this.untip = function() { + print('JBP SHOULD UNTIP') + var props = Entities.getEntityProperties(this.entityID); + var rotation = Quat.safeEulerAngles(props.rotation) + if (rotation.x > 3 || rotation.x < -3 || rotation.z > 3 || rotation.z < -3) { + print('too much pitch or roll, fix it'); + + //we zero out the velocity and angular velocity + Entities.editEntity(_this.entityID, { + position: startPosition, + rotation: startRotation, + velocity: { + x: 0, + y: 0, + z: 0 + }, + angularVelocity: { + x: 0, + y: 0, + z: 0 + } + }); + } + + + + } + + +}); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index 9b00bc647a..65ec95cdac 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -202,6 +202,8 @@ _this.createKineticEntities(); _this.createScriptedEntities(); _this.setupDressingRoom(); + _this.createMilkPailBalls(); + _this.createTarget(); }, 750); } }, @@ -526,6 +528,99 @@ var dais = Entities.addEntity(daisProperties); }, + createTarget: function() { + var targetProperties = { + type: 'Model', + modelURL: 'atp:/pingPongGun/Target.fbx', + shapeType: 'Compound', + compoundShapeURL: 'atp:/pingPongGun/Target.obj', + dimensions: { + x: 0.4937, + y: 0.6816, + z: 0.0778 + }, + rotation: Quat.fromPitchYawRollDegrees(3.1471, -170.4121, -0.0060), + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + velocity: { + x: 0, + y: -0.1, + z: 0 + }, + position: { + x: 1100.6343, + y: 460.5366, + z: -65.2142 + }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true + } + }), + density:100, + dynamic: true + } + var target = Entities.addEntity(targetProperties); + }, + + createMilkPailBalls: function() { + var locations = [{ + x: 1099.0795, + y: 459.4186, + z: -70.8603 + }, { + x: 1099.2826, + y: 459.4186, + z: -70.9094 + }, { + x: 1099.5012, + y: 459.4186, + z: -71.1000 + }]; + + var ballProperties = { + type: 'Model', + modelURL: 'atp:/static_objects/StarBall.fbx', + shapeType: 'Sphere', + dimensions: { + x: 0.1646, + y: 0.1646, + z: 0.1646 + }, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + velocity: { + x: 0, + y: -0.1, + z: 0 + }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true + }, + hifiHomeKey: { + reset: true + } + }), + dynamic: true + }; + + locations.forEach(function(location) { + ballProperties.position = location; + var ball = Entities.addEntity(ballProperties); + }); + print('HOME made milk pail balls') + }, + createTransformers: function() { var firstDollPosition = { x: 1107.6, From a4be536a82ad5c8222ed784ea7928a0d79c8778f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 7 Jun 2016 12:54:01 +1200 Subject: [PATCH 0412/1237] Change table switches to checkboxes; vertically align table rows --- domain-server/resources/web/css/style.css | 4 ++++ domain-server/resources/web/settings/js/settings.js | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index efb9e907c5..4bf9c81b39 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -20,6 +20,10 @@ body { top: 40px; } +.table .value-row td, .table .inputs td { + vertical-align: middle; +} + .glyphicon-remove { font-size: 24px; } diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 970e7551c4..a38d544502 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -233,7 +233,7 @@ $(document).ready(function(){ }); // Bootstrap switch in table - $('#' + Settings.FORM_ID).on('switchChange.bootstrapSwitch', 'input.table-checkbox', function () { + $('#' + Settings.FORM_ID).on('change', 'input.table-checkbox', function () { // Bootstrap switches in table: set the changed data attribute for all rows in table. var row = $(this).closest('tr'); if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. @@ -851,7 +851,6 @@ function reloadSettings(callback) { // setup any bootstrap switches $('.toggle-checkbox').bootstrapSwitch(); - $('.table-checkbox').bootstrapSwitch(); // add tooltip to locked settings $('label.locked').tooltip({ @@ -1176,7 +1175,7 @@ function addTableRow(add_glyphicon) { var input = $(element).find("input") var isCheckbox = false; if (input.hasClass("table-checkbox")) { - input = $(input).parent().parent(); + input = $(input).parent(); isCheckbox = true; } From 0119af11d6d02201c2d55f07fdd666e05db1301e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 7 Jun 2016 13:58:13 +1200 Subject: [PATCH 0413/1237] Finesse security table text and general layout --- .../resources/describe-settings.json | 19 ++++++++++--------- domain-server/resources/web/css/style.css | 10 ++++++++++ .../resources/web/settings/js/settings.js | 4 ++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 26a368fd04..2314a0551c 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -99,14 +99,15 @@ { "name": "standard_permissions", "type": "table", - "label": "Domain-Wide Permissions", - "help": "Standard Permissions:", + "label": "Domain-Wide User Permissions", + "help": "Indicate which users or groups can have which domain-wide permissions.", + "caption": "Standard Permissions", "can_add_new_rows": false, "columns": [ { "name": "permissions_id", - "label": "User/Group" + "label": "User / Group" }, { "name": "id_can_connect", @@ -117,7 +118,7 @@ }, { "name": "id_can_adjust_locks", - "label": "Lock/Unlock", + "label": "Lock / Unlock", "type": "checkbox", "editable": true, "default": false @@ -131,7 +132,7 @@ }, { "name": "id_can_rez_tmp", - "label": "Rez Temp", + "label": "Rez Temporary", "type": "checkbox", "editable": true, "default": false @@ -158,13 +159,13 @@ { "name": "permissions", "type": "table", - "help": "Permissions for Specific Users:", + "caption": "Permissions for Specific Users", "can_add_new_rows": true, "columns": [ { "name": "permissions_id", - "label": "User/Group" + "label": "User / Group" }, { "name": "id_can_connect", @@ -175,7 +176,7 @@ }, { "name": "id_can_adjust_locks", - "label": "Lock/Unlock", + "label": "Lock / Unlock", "type": "checkbox", "editable": true, "default": false @@ -189,7 +190,7 @@ }, { "name": "id_can_rez_tmp", - "label": "Rez Temp", + "label": "Rez Temporary", "type": "checkbox", "editable": true, "default": false diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 4bf9c81b39..35cb6ad492 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -111,6 +111,16 @@ table { word-wrap: break-word; } +caption { + color: #333; + font-weight: 700; + padding-top: 0; +} + +table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td { + text-align: center; +} + #xs-advanced-container { margin-bottom: 20px; } diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index a38d544502..71abae76f8 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -927,6 +927,10 @@ function makeTable(setting, keypath, setting_value, isLocked) { + "' name='" + keypath + "' id='" + (typeof setting.html_id !== 'undefined' ? setting.html_id : keypath) + "' data-setting-type='" + (isArray ? 'array' : 'hash') + "'>"; + if (setting.caption) { + html += "" + setting.caption + "" + } + // Column names html += "" From f2d81f1338ab7634f16e86ff57f7246ee4e3e87a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 7 Jun 2016 16:42:23 +1200 Subject: [PATCH 0414/1237] Add security table heading groups --- .../resources/describe-settings.json | 26 +++++++++++++++++-- .../resources/web/settings/js/settings.js | 16 ++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 2314a0551c..397c7fe6d0 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -104,10 +104,21 @@ "caption": "Standard Permissions", "can_add_new_rows": false, + "groups": [ + { + "label": "User / Group", + "span": 1 + }, + { + "label": "Permissions", + "span": 6 + } + ], + "columns": [ { "name": "permissions_id", - "label": "User / Group" + "label": "" }, { "name": "id_can_connect", @@ -162,10 +173,21 @@ "caption": "Permissions for Specific Users", "can_add_new_rows": true, + "groups": [ + { + "label": "User / Group", + "span": 1 + }, + { + "label": "Permissions", + "span": 6 + } + ], + "columns": [ { "name": "permissions_id", - "label": "User / Group" + "label": "" }, { "name": "id_can_connect", diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 71abae76f8..97da975748 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -931,6 +931,22 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" + setting.caption + "" } + // Column groups + if (setting.groups) { + html += "" + _.each(setting.groups, function (group) { + html += "" + group.label + "" + }) + if (!isLocked && !setting.read_only) { + if (setting.can_order) { + html += ""; + } + html += "" + } + html += "" + } + // Column names html += "" From 14b51d6615fec72c8f60fe55537b3220520bcc1a Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 7 Jun 2016 06:50:38 -0700 Subject: [PATCH 0415/1237] Use common code for AVX detection --- libraries/audio/src/AudioSRC.cpp | 87 +------------------------------- 1 file changed, 2 insertions(+), 85 deletions(-) diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index fbe109803e..98de36e655 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -370,95 +370,12 @@ int AudioSRC::multirateFilter2_SSE(const float* input0, const float* input1, flo return outputFrames; } -// -// Detect AVX/AVX2 support -// - -#if defined(_MSC_VER) - -#include - -static bool cpuSupportsAVX() { - int info[4]; - int mask = (1 << 27) | (1 << 28); // OSXSAVE and AVX - - __cpuidex(info, 0x1, 0); - - bool result = false; - if ((info[2] & mask) == mask) { - - if ((_xgetbv(_XCR_XFEATURE_ENABLED_MASK) & 0x6) == 0x6) { - result = true; - } - } - return result; -} - -static bool cpuSupportsAVX2() { - int info[4]; - int mask = (1 << 5); // AVX2 - - bool result = false; - if (cpuSupportsAVX()) { - - __cpuidex(info, 0x7, 0); - - if ((info[1] & mask) == mask) { - result = true; - } - } - return result; -} - -#elif defined(__GNUC__) - -#include - -static bool cpuSupportsAVX() { - unsigned int eax, ebx, ecx, edx; - unsigned int mask = (1 << 27) | (1 << 28); // OSXSAVE and AVX - - bool result = false; - if (__get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & mask) == mask)) { - - __asm__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(0)); - if ((eax & 0x6) == 0x6) { - result = true; - } - } - return result; -} - -static bool cpuSupportsAVX2() { - unsigned int eax, ebx, ecx, edx; - unsigned mask = (1 << 5); // AVX2 - - bool result = false; - if (cpuSupportsAVX()) { - - if (__get_cpuid(0x7, &eax, &ebx, &ecx, &edx) && ((ebx & mask) == mask)) { - result = true; - } - } - return result; -} - -#else - -static bool cpuSupportsAVX() { - return false; -} - -static bool cpuSupportsAVX2() { - return false; -} - -#endif - // // Runtime CPU dispatch // +#include "CPUDetect.h" + int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFrames) { static auto f = cpuSupportsAVX2() ? &AudioSRC::multirateFilter1_AVX2 : &AudioSRC::multirateFilter1_SSE; From 27d39eb1b09eb3be43db30985c624669a7d329b0 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 7 Jun 2016 06:53:50 -0700 Subject: [PATCH 0416/1237] Lightweight functions to detect SSE3, SSSE3, SSE4.1, SSS4.2, AVX and AVX2. These are cross platform, and return false on unsupported platforms. --- libraries/shared/src/CPUDetect.h | 181 +++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 libraries/shared/src/CPUDetect.h diff --git a/libraries/shared/src/CPUDetect.h b/libraries/shared/src/CPUDetect.h new file mode 100644 index 0000000000..c9d2eb649b --- /dev/null +++ b/libraries/shared/src/CPUDetect.h @@ -0,0 +1,181 @@ +// +// CPUDetect.h +// libraries/shared/src +// +// Created by Ken Cooke on 6/6/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_CPUDetect_h +#define hifi_CPUDetect_h + +// +// Lightweight functions to detect SSE/AVX/AVX2 support +// + +#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__x86_64__) +#define ARCH_X86 +#endif + +#define MASK_SSE3 (1 << 0) // SSE3 +#define MASK_SSSE3 (1 << 9) // SSSE3 +#define MASK_SSE41 (1 << 19) // SSE4.1 +#define MASK_SSE42 ((1 << 20) | (1 << 23)) // SSE4.2 and POPCNT +#define MASK_AVX ((1 << 27) | (1 << 28)) // OSXSAVE and AVX +#define MASK_AVX2 (1 << 5) // AVX2 + +#if defined(ARCH_X86) && defined(_MSC_VER) + +#include + +static inline bool cpuSupportsSSE3() { + int info[4]; + + __cpuidex(info, 0x1, 0); + + return ((info[2] & MASK_SSE3) == MASK_SSE3); +} + +static inline bool cpuSupportsSSSE3() { + int info[4]; + + __cpuidex(info, 0x1, 0); + + return ((info[2] & MASK_SSSE3) == MASK_SSSE3); +} + +static inline bool cpuSupportsSSE41() { + int info[4]; + + __cpuidex(info, 0x1, 0); + + return ((info[2] & MASK_SSE41) == MASK_SSE41); +} + +static inline bool cpuSupportsSSE42() { + int info[4]; + + __cpuidex(info, 0x1, 0); + + return ((info[2] & MASK_SSE42) == MASK_SSE42); +} + +static inline bool cpuSupportsAVX() { + int info[4]; + + __cpuidex(info, 0x1, 0); + + bool result = false; + if ((info[2] & MASK_AVX) == MASK_AVX) { + + // verify OS support for YMM state + if ((_xgetbv(_XCR_XFEATURE_ENABLED_MASK) & 0x6) == 0x6) { + result = true; + } + } + return result; +} + +static inline bool cpuSupportsAVX2() { + int info[4]; + + bool result = false; + if (cpuSupportsAVX()) { + + __cpuidex(info, 0x7, 0); + + if ((info[1] & MASK_AVX2) == MASK_AVX2) { + result = true; + } + } + return result; +} + +#elif defined(ARCH_X86) && defined(__GNUC__) + +#include + +static inline bool cpuSupportsSSE3() { + unsigned int eax, ebx, ecx, edx; + + return __get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & MASK_SSE3) == MASK_SSE3); +} + +static inline bool cpuSupportsSSSE3() { + unsigned int eax, ebx, ecx, edx; + + return __get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & MASK_SSSE3) == MASK_SSSE3); +} + +static inline bool cpuSupportsSSE41() { + unsigned int eax, ebx, ecx, edx; + + return __get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & MASK_SSE41) == MASK_SSE41); +} + +static inline bool cpuSupportsSSE42() { + unsigned int eax, ebx, ecx, edx; + + return __get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & MASK_SSE42) == MASK_SSE42); +} + +static inline bool cpuSupportsAVX() { + unsigned int eax, ebx, ecx, edx; + + bool result = false; + if (__get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & MASK_AVX) == MASK_AVX)) { + + // verify OS support for YMM state + __asm__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(0)); + if ((eax & 0x6) == 0x6) { + result = true; + } + } + return result; +} + +static inline bool cpuSupportsAVX2() { + unsigned int eax, ebx, ecx, edx; + + bool result = false; + if (cpuSupportsAVX()) { + + if (__get_cpuid(0x7, &eax, &ebx, &ecx, &edx) && ((ebx & MASK_AVX2) == MASK_AVX2)) { + result = true; + } + } + return result; +} + +#else + +static inline bool cpuSupportsSSE3() { + return false; +} + +static inline bool cpuSupportsSSSE3() { + return false; +} + +static inline bool cpuSupportsSSE41() { + return false; +} + +static inline bool cpuSupportsSSE42() { + return false; +} + +static inline bool cpuSupportsAVX() { + return false; +} + +static inline bool cpuSupportsAVX2() { + return false; +} + +#endif + +#endif // hifi_CPUDetect_h From 74c5ddd49f751937b1814763a18ddc5b189667d1 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 7 Jun 2016 09:50:10 -0700 Subject: [PATCH 0417/1237] fix paused interactions with mouse --- scripts/system/away.js | 8 +++++--- scripts/system/controllers/handControllerPointer.js | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/system/away.js b/scripts/system/away.js index 2b2ea8a42b..399b3dc96c 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -176,9 +176,11 @@ function goAway() { // tell the Reticle, we want to stop capturing the mouse until we come back Reticle.allowMouseCapture = false; - if (HMD.active) { - Reticle.visible = false; - } + // Allow users to find their way to other applications, our menus, etc. + // For desktop, that means we want the reticle visible. + // For HMD, the hmd preview will show the system mouse because of allowMouseCapture, + // but we want to turn off our Reticle so that we don't get two in preview and a stuck one in headset. + Reticle.visible = !HMD.active; wasHmdMounted = safeGetHMDMounted(); // always remember the correct state avatarPosition = MyAvatar.position; diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index e70f704ce7..7b45babf4d 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -410,8 +410,8 @@ function update() { if (!Menu.isOptionChecked("First Person")) { return turnOffVisualization(); // What to do? menus can be behind hand! } - if (!Window.hasFocus()) { - return turnOffVisualization(); // Don't mess with other apps + if (!Window.hasFocus() || !Reticle.allowMouseCapture) { + return turnOffVisualization(); // Don't mess with other apps or paused mouse activity } leftTrigger.update(); rightTrigger.update(); From dd97f16728575b1c0b9c703536e6df7de756a635 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 7 Jun 2016 11:42:58 -0700 Subject: [PATCH 0418/1237] investigating xbox failure on restart (WIP) --- .../controllers/src/controllers/UserInputMapper.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index c1ee3ce36c..e3a520e6c4 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -77,7 +77,7 @@ void UserInputMapper::registerDevice(InputDevice::Pointer device) { } const auto& deviceID = device->_deviceID; - recordDeviceOfType(device->getName()); + //recordDeviceOfType(device->getName()); qCDebug(controllers) << "Registered input device <" << device->getName() << "> deviceID = " << deviceID; @@ -134,6 +134,15 @@ void UserInputMapper::removeDevice(int deviceID) { _mappingsByDevice.erase(mappingsEntry); } + for (const auto& inputMapping : device->getAvailableInputs()) { + const auto& input = inputMapping.first; + auto endpoint = _endpointsByInput.find(input); + if (endpoint != _endpointsByInput.end()) { + _inputsByEndpoint.erase((*endpoint).second); + _endpointsByInput.erase(input); + } + } + _registeredDevices.erase(proxyEntry); emit hardwareChanged(); From ff2405437bbbda73d4caaf6df2efca9c94709be1 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 7 Jun 2016 12:19:38 -0700 Subject: [PATCH 0419/1237] can set strength to 0 --- .../oculus/src/OculusControllerManager.cpp | 26 +++++++++++++------ plugins/openvr/src/ViveControllerManager.cpp | 18 ++++++++++--- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 02ac44d77c..f00b943d7c 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -253,18 +253,28 @@ bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, fl Locker locker(_lock); bool toReturn = true; if (hand == controller::BOTH || hand == controller::LEFT) { - _leftHapticStrength = (duration > _leftHapticDuration) ? strength : _leftHapticStrength; - if (ovr_SetControllerVibration(_parent._session, ovrControllerType_LTouch, 1.0f, _leftHapticStrength) != ovrSuccess) { - toReturn = false; + if (strength == 0.0f) { + _leftHapticStrength = 0.0f; + _leftHapticDuration = 0.0f; + } else { + _leftHapticStrength = (duration > _leftHapticDuration) ? strength : _leftHapticStrength; + if (ovr_SetControllerVibration(_parent._session, ovrControllerType_LTouch, 1.0f, _leftHapticStrength) != ovrSuccess) { + toReturn = false; + } + _leftHapticDuration = std::max(duration, _leftHapticDuration); } - _leftHapticDuration = std::max(duration, _leftHapticDuration); } if (hand == controller::BOTH || hand == controller::RIGHT) { - _rightHapticStrength = (duration > _rightHapticDuration) ? strength : _rightHapticStrength; - if (ovr_SetControllerVibration(_parent._session, ovrControllerType_RTouch, 1.0f, _rightHapticStrength) != ovrSuccess) { - toReturn = false; + if (strength == 0.0f) { + _rightHapticStrength = 0.0f; + _rightHapticDuration = 0.0f; + } else { + _rightHapticStrength = (duration > _rightHapticDuration) ? strength : _rightHapticStrength; + if (ovr_SetControllerVibration(_parent._session, ovrControllerType_RTouch, 1.0f, _rightHapticStrength) != ovrSuccess) { + toReturn = false; + } + _rightHapticDuration = std::max(duration, _rightHapticDuration); } - _rightHapticDuration = std::max(duration, _rightHapticDuration); } return toReturn; } diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 7254117922..74c232af33 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -455,12 +455,22 @@ void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { Locker locker(_lock); if (hand == controller::BOTH || hand == controller::LEFT) { - _leftHapticStrength = (duration > _leftHapticDuration) ? strength : _leftHapticStrength; - _leftHapticDuration = std::max(duration, _leftHapticDuration); + if (strength == 0.0f) { + _leftHapticStrength = 0.0f; + _leftHapticDuration = 0.0f; + } else { + _leftHapticStrength = (duration > _leftHapticDuration) ? strength : _leftHapticStrength; + _leftHapticDuration = std::max(duration, _leftHapticDuration); + } } if (hand == controller::BOTH || hand == controller::RIGHT) { - _rightHapticStrength = (duration > _rightHapticDuration) ? strength : _rightHapticStrength; - _rightHapticDuration = std::max(duration, _rightHapticDuration); + if (strength == 0.0f) { + _rightHapticStrength = 0.0f; + _rightHapticDuration = 0.0f; + } else { + _rightHapticStrength = (duration > _rightHapticDuration) ? strength : _rightHapticStrength; + _rightHapticDuration = std::max(duration, _rightHapticDuration); + } } return true; } From 2e71a635749350960c2c5d8b171b18a2391b77c0 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 7 Jun 2016 13:28:10 -0700 Subject: [PATCH 0420/1237] cr fixes --- plugins/hifiSdl2/src/Joystick.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index aa6b358d38..e88fe8b958 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -25,9 +25,12 @@ Joystick::Joystick(SDL_JoystickID instanceId, SDL_GameController* sdlGameControl _instanceId(instanceId) { if (!_sdlHaptic) { - qDebug() << QString(SDL_GetError()); + qDebug() << "SDL Haptic Open Failure: " << QString(SDL_GetError()); + } else { + if (SDL_HapticRumbleInit(_sdlHaptic) != 0) { + qDebug() << "SDL Haptic Rumble Init Failure: " << QString(SDL_GetError()); + } } - SDL_HapticRumbleInit(_sdlHaptic); } Joystick::~Joystick() { @@ -35,7 +38,9 @@ Joystick::~Joystick() { } void Joystick::closeJoystick() { - SDL_HapticClose(_sdlHaptic); + if (_sdlHaptic) { + SDL_HapticClose(_sdlHaptic); + } SDL_GameControllerClose(_sdlGameController); } From 9f4289ae1b164391e4a44b81e3d790fe2088bed9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 7 Jun 2016 13:31:42 -0700 Subject: [PATCH 0421/1237] add down velocity to shelfy things --- .../Home/kineticObjects/wrapper.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js b/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js index 5ddea20560..0dee32d05c 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js @@ -254,6 +254,7 @@ StuffOnShelves = function(spawnLocation, spawnRotation) { print('CREATE STUFF ON SHELVES'); var created = []; + function create() { var success = Clipboard.importEntities(STUFF_ON_SHELVES_URL); if (success === true) { @@ -268,6 +269,8 @@ StuffOnShelves = function(spawnLocation, spawnRotation) { }) }; + + create(); this.cleanup = cleanup; @@ -300,11 +303,26 @@ Bricabrac = function(spawnLocation, spawnRotation) { print('HOME CREATE BRICABRAC'); var created = []; + function addVelocityDown() { + print('HOME ADDING DOWN VELOCITY TO DRESSING ROOM ITEMS') + created.forEach(function(obj) { + Entities.editEntity(obj, { + velocity: { + x: 0, + y: -0.1, + z: 0 + } + }); + }) + } + function create() { var success = Clipboard.importEntities(BRICABRAC_URL); if (success === true) { created = Clipboard.pasteEntities(spawnLocation) print('created ' + created); + addVelocityDown(); + } }; From b9ee0c087e6f79b36850799bf476e80ab93c5938 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 7 Jun 2016 13:45:23 -0700 Subject: [PATCH 0422/1237] Add a constant hud toggle button to the UI --- interface/resources/icons/hud-01.svg | 105 ++++++++++++++++++++ interface/resources/qml/desktop/Desktop.qml | 2 +- interface/resources/qml/hifi/Desktop.qml | 21 ++++ tests/ui/qml/main.qml | 2 +- 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 interface/resources/icons/hud-01.svg diff --git a/interface/resources/icons/hud-01.svg b/interface/resources/icons/hud-01.svg new file mode 100644 index 0000000000..4929389268 --- /dev/null +++ b/interface/resources/icons/hud-01.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 241cd91611..e269a63cde 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -299,7 +299,7 @@ FocusScope { if (pinned) { // recalculate our non-pinned children hiddenChildren = d.findMatchingChildren(desktop, function(child){ - return !d.isTopLevelWindow(child) && child.visible; + return !d.isTopLevelWindow(child) && child.visible && !child.pinned; }); hiddenChildren.forEach(function(child){ diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 59278a17b4..f1c1aeb4d1 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -47,6 +47,27 @@ Desktop { } } + Item { + id: hudToggleButton + clip: true + width: 50 + height: 50 + anchors.bottom: parent.bottom + anchors.bottomMargin: 32 + anchors.horizontalCenter: parent.horizontalCenter + property bool pinned: true + Image { + y: desktop.pinned ? -50 : 0 + id: hudToggleImage + source: "../../icons/hud-01.svg" + } + MouseArea { + anchors.fill: parent + onClicked: desktop.togglePinned() + } + } + + } diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index d752734de4..33408fb821 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -51,7 +51,7 @@ ApplicationWindow { Button { text: "toggle desktop" - onClicked: desktop.toggleVisible() + onClicked: desktop.togglePinned() } // Error alerts From 801981816bd4fa9eafad10ab4c3bffe6ec242039 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 8 Jun 2016 09:11:37 +1200 Subject: [PATCH 0423/1237] Add permissions tool-tips --- domain-server/resources/describe-settings.json | 14 +++++++------- domain-server/resources/web/css/style.css | 5 +++++ .../resources/web/settings/js/settings.js | 2 ++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 397c7fe6d0..43eb5c7ee9 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -100,18 +100,18 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which users or groups can have which domain-wide permissions.", + "help": "Indicate which users or groups can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, "groups": [ { "label": "User / Group", - "span": 1 + "span": 1 }, { - "label": "Permissions", - "span": 6 + "label": "Permissions ?", + "span": 6 } ], @@ -176,11 +176,11 @@ "groups": [ { "label": "User / Group", - "span": 1 + "span": 1 }, { - "label": "Permissions", - "span": 6 + "label": "Permissions ?", + "span": 6 } ], diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 35cb6ad492..21cf753bfd 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -121,6 +121,11 @@ table[name="security.standard_permissions"] .headers td + td, table[name="securi text-align: center; } +#security .tooltip-inner { + max-width: 520px; + text-align: left; +} + #xs-advanced-container { margin-bottom: 20px; } diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 97da975748..9188772ec9 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -852,6 +852,8 @@ function reloadSettings(callback) { // setup any bootstrap switches $('.toggle-checkbox').bootstrapSwitch(); + $('[data-toggle="tooltip"]').tooltip(); + // add tooltip to locked settings $('label.locked').tooltip({ placement: 'right', From 6573b3c71e72da122c337a9ff6ce2353cfb8bac7 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 7 Jun 2016 14:37:19 -0700 Subject: [PATCH 0424/1237] remove bricabrac from dressing room shelves for now --- scripts/defaultScripts.js | 2 +- .../Home/kineticObjects/wrapper.js | 68 +++++++++++-------- .../DomainContent/Home/reset.js | 10 +-- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index f460ddf88f..4fd17a1c6f 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,4 +21,4 @@ Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); Script.load("system/dialTone.js"); -Script.load("system/attachedEntitiesManager.js"); \ No newline at end of file +// Script.load("system/attachedEntitiesManager.js"); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js b/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js index 0dee32d05c..d4512823db 100644 --- a/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js +++ b/unpublishedScripts/DomainContent/Home/kineticObjects/wrapper.js @@ -279,32 +279,8 @@ StuffOnShelves = function(spawnLocation, spawnRotation) { HomeJunk = function(spawnLocation, spawnRotation) { print('HOME CREATE JUNK'); var created = []; - - function create() { - var success = Clipboard.importEntities(JUNK_URL); - if (success === true) { - created = Clipboard.pasteEntities(spawnLocation) - print('created ' + created); - } - }; - - function cleanup() { - created.forEach(function(obj) { - Entities.deleteEntity(obj); - }) - }; - - create(); - - this.cleanup = cleanup; -} - -Bricabrac = function(spawnLocation, spawnRotation) { - print('HOME CREATE BRICABRAC'); - var created = []; - function addVelocityDown() { - print('HOME ADDING DOWN VELOCITY TO DRESSING ROOM ITEMS') + print('HOME ADDING DOWN VELOCITY TO SHELF ITEMS') created.forEach(function(obj) { Entities.editEntity(obj, { velocity: { @@ -315,14 +291,12 @@ Bricabrac = function(spawnLocation, spawnRotation) { }); }) } - function create() { - var success = Clipboard.importEntities(BRICABRAC_URL); + var success = Clipboard.importEntities(JUNK_URL); if (success === true) { created = Clipboard.pasteEntities(spawnLocation) print('created ' + created); addVelocityDown(); - } }; @@ -337,6 +311,44 @@ Bricabrac = function(spawnLocation, spawnRotation) { this.cleanup = cleanup; } +// Bricabrac = function(spawnLocation, spawnRotation) { +// print('HOME CREATE BRICABRAC'); +// var created = []; + +// function addVelocityDown() { +// print('HOME ADDING DOWN VELOCITY TO DRESSING ROOM ITEMS') +// created.forEach(function(obj) { +// Entities.editEntity(obj, { +// velocity: { +// x: 0, +// y: -0.1, +// z: 0 +// } +// }); +// }) +// } + +// function create() { +// var success = Clipboard.importEntities(BRICABRAC_URL); +// if (success === true) { +// created = Clipboard.pasteEntities(spawnLocation) +// print('created ' + created); +// addVelocityDown(); + +// } +// }; + +// function cleanup() { +// created.forEach(function(obj) { +// Entities.deleteEntity(obj); +// }) +// }; + +// create(); + +// this.cleanup = cleanup; +// } + Bench = function(spawnLocation, spawnRotation) { print('HOME CREATE BENCH'); var created = []; diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index 65ec95cdac..7e69e1782b 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -422,11 +422,11 @@ z: -73.3 }); - var dressingRoomBricabrac = new Bricabrac({ - x: 1106.8, - y: 460.3909, - z: -72.6 - }); + // var dressingRoomBricabrac = new Bricabrac({ + // x: 1106.8, + // y: 460.3909, + // z: -72.6 + // }); var bench = new Bench({ x: 1100.1210, From f3f04fba9754abfd647e265cb464df4c6b06e13f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 8 Jun 2016 09:41:31 +1200 Subject: [PATCH 0425/1237] Fix IE displaying checkboxes too large --- domain-server/resources/web/css/style.css | 7 +++++++ domain-server/resources/web/settings/js/settings.js | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 21cf753bfd..4c419a1baa 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -24,6 +24,13 @@ body { vertical-align: middle; } +.table .table-checkbox { + /* Fix IE sizing checkboxes to fill table cell */ + width: auto; + margin-left: auto; + margin-right: auto; +} + .glyphicon-remove { font-size: 24px; } diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 9188772ec9..aecc48b31f 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -1005,7 +1005,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { if (isArray && col.type === "checkbox" && col.editable) { html += "" - + ""; } else { // Use a hidden input so that the values are posted. @@ -1060,7 +1060,7 @@ function makeTableInputs(setting) { _.each(setting.columns, function(col) { if (col.type === "checkbox") { html += "" - + ""; } else { html += "\ From c545f53c75041280d40bd7a1f1e7007c2c7e1a29 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 7 Jun 2016 14:43:23 -0700 Subject: [PATCH 0426/1237] target script attached in reset spawner --- unpublishedScripts/DomainContent/Home/pingPongGun/target.js | 6 ++---- unpublishedScripts/DomainContent/Home/reset.js | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/unpublishedScripts/DomainContent/Home/pingPongGun/target.js b/unpublishedScripts/DomainContent/Home/pingPongGun/target.js index 4898973f97..1a8ea6a25b 100644 --- a/unpublishedScripts/DomainContent/Home/pingPongGun/target.js +++ b/unpublishedScripts/DomainContent/Home/pingPongGun/target.js @@ -32,8 +32,7 @@ _this.collisionWithEntity = function(myID, otherID, collisionInfo) { //we dont actually use any of the parameters above, since we don't really care what we collided with, or the details of the collision. - print('JBP TARGET COLLISION') - //5 seconds after a collision, upright the target. protect from multiple collisions in a short timespan with the 'shouldUntip' variable + //5 seconds after a collision, upright the target. protect from multiple collisions in a short timespan with the 'shouldUntip' variable if (_this.shouldUntip) { //in Hifi, preface setTimeout with Script.setTimeout Script.setTimeout(function() { @@ -47,11 +46,10 @@ } _this.untip = function() { - print('JBP SHOULD UNTIP') var props = Entities.getEntityProperties(this.entityID); var rotation = Quat.safeEulerAngles(props.rotation) if (rotation.x > 3 || rotation.x < -3 || rotation.z > 3 || rotation.z < -3) { - print('too much pitch or roll, fix it'); + print('home target - too much pitch or roll, fix it'); //we zero out the velocity and angular velocity Entities.editEntity(_this.entityID, { diff --git a/unpublishedScripts/DomainContent/Home/reset.js b/unpublishedScripts/DomainContent/Home/reset.js index 7e69e1782b..cd1dcea76e 100644 --- a/unpublishedScripts/DomainContent/Home/reset.js +++ b/unpublishedScripts/DomainContent/Home/reset.js @@ -563,7 +563,8 @@ reset: true } }), - density:100, + script: 'atp:/pingPongGun/target.js', + density: 100, dynamic: true } var target = Entities.addEntity(targetProperties); From 5daccba235814bdf3f61ebf5e76813330aa66b69 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 7 Jun 2016 15:14:08 -0700 Subject: [PATCH 0427/1237] xbox controller works on restart --- libraries/controllers/src/controllers/UserInputMapper.cpp | 6 +++++- libraries/controllers/src/controllers/UserInputMapper.h | 2 +- plugins/hifiSdl2/src/Joystick.cpp | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index e3a520e6c4..9132b9d3a5 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -77,7 +77,7 @@ void UserInputMapper::registerDevice(InputDevice::Pointer device) { } const auto& deviceID = device->_deviceID; - //recordDeviceOfType(device->getName()); + recordDeviceOfType(device->getName()); qCDebug(controllers) << "Registered input device <" << device->getName() << "> deviceID = " << deviceID; @@ -126,6 +126,10 @@ void UserInputMapper::removeDevice(int deviceID) { auto device = proxyEntry->second; qCDebug(controllers) << "Unregistering input device <" << device->getName() << "> deviceID = " << deviceID; + if (!_deviceCounts.contains(device->getName())) { + _deviceCounts[device->getName()] -= 1; + } + unloadMappings(device->getDefaultMappingConfigs()); auto mappingsEntry = _mappingsByDevice.find(deviceID); diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index 9c79415b6e..2c8aabc099 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -142,7 +142,7 @@ namespace controller { std::vector _lastStandardStates = std::vector(); int recordDeviceOfType(const QString& deviceName); - QHash _deviceCounts; + QHash _deviceCounts; static float getValue(const EndpointPointer& endpoint, bool peek = false); static Pose getPose(const EndpointPointer& endpoint, bool peek = false); diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index a109656489..614adff7d6 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -64,7 +64,7 @@ void Joystick::handleButtonEvent(const SDL_ControllerButtonEvent& event) { controller::Input::NamedVector Joystick::getAvailableInputs() const { using namespace controller; - static const Input::NamedVector availableInputs{ + const Input::NamedVector availableInputs{ makePair(A, "A"), makePair(B, "B"), makePair(X, "X"), From 545dda0a9887652942e6368da1be64a7ace3b441 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 7 Jun 2016 15:15:42 -0700 Subject: [PATCH 0428/1237] whoops --- libraries/controllers/src/controllers/UserInputMapper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 9132b9d3a5..9dbad89a9b 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -126,7 +126,7 @@ void UserInputMapper::removeDevice(int deviceID) { auto device = proxyEntry->second; qCDebug(controllers) << "Unregistering input device <" << device->getName() << "> deviceID = " << deviceID; - if (!_deviceCounts.contains(device->getName())) { + if (_deviceCounts.contains(device->getName())) { _deviceCounts[device->getName()] -= 1; } From ba61491ee605d44fd5c39babb134cf85f07e8331 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 7 Jun 2016 15:33:00 -0700 Subject: [PATCH 0429/1237] PR feedback --- interface/resources/qml/hifi/Desktop.qml | 22 +++--------- .../resources/qml/hifi/ToggleHudButton.qml | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 interface/resources/qml/hifi/ToggleHudButton.qml diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index f1c1aeb4d1..e127a235e6 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -4,11 +4,13 @@ import QtWebEngine 1.1; import "../desktop" import ".." +import "." Desktop { id: desktop MouseArea { + id: hoverWatch anchors.fill: parent hoverEnabled: true propagateComposedEvents: true @@ -47,28 +49,12 @@ Desktop { } } - Item { - id: hudToggleButton - clip: true - width: 50 - height: 50 + + ToggleHudButton { anchors.bottom: parent.bottom anchors.bottomMargin: 32 anchors.horizontalCenter: parent.horizontalCenter - property bool pinned: true - Image { - y: desktop.pinned ? -50 : 0 - id: hudToggleImage - source: "../../icons/hud-01.svg" - } - MouseArea { - anchors.fill: parent - onClicked: desktop.togglePinned() - } } - - } - diff --git a/interface/resources/qml/hifi/ToggleHudButton.qml b/interface/resources/qml/hifi/ToggleHudButton.qml new file mode 100644 index 0000000000..6454f42305 --- /dev/null +++ b/interface/resources/qml/hifi/ToggleHudButton.qml @@ -0,0 +1,36 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../windows" + +Window { + //frame: HiddenFrame {} + hideBackground: true + resizable: false + destroyOnCloseButton: false + destroyOnHidden: false + closable: false + shown: true + pinned: true + width: 50 + height: 50 + clip: true + visible: true + + Item { + width: 50 + height: 50 + Image { + y: desktop.pinned ? -50 : 0 + id: hudToggleImage + source: "../../icons/hud-01.svg" + } + MouseArea { + readonly property string overlayMenuItem: "Overlays" + anchors.fill: parent + onClicked: MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)) + } + } +} + + From a87483dfff3bdedb0b6c74e5e3aa27c20c437ba8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 8 Jun 2016 10:55:14 +1200 Subject: [PATCH 0430/1237] Fix colors of buttons in Asset Browser dialog --- interface/resources/qml/AssetServer.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 370bc92d81..6d2e8e7ba0 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -341,7 +341,7 @@ Window { HifiControls.GlyphButton { glyph: hifi.glyphs.reload - color: hifi.buttons.white + color: hifi.buttons.black colorScheme: root.colorScheme width: hifi.dimensions.controlLineHeight @@ -349,8 +349,8 @@ Window { } HifiControls.Button { - text: "ADD TO WORLD" - color: hifi.buttons.white + text: "Add To World" + color: hifi.buttons.black colorScheme: root.colorScheme width: 120 @@ -360,8 +360,8 @@ Window { } HifiControls.Button { - text: "RENAME" - color: hifi.buttons.white + text: "Rename" + color: hifi.buttons.black colorScheme: root.colorScheme width: 80 @@ -372,7 +372,7 @@ Window { HifiControls.Button { id: deleteButton - text: "DELETE" + text: "Delete" color: hifi.buttons.red colorScheme: root.colorScheme width: 80 From b2f63a2132fc0767fce87738cf62e475132da60b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 7 Jun 2016 16:17:58 -0700 Subject: [PATCH 0431/1237] hook up can-rez-tmp and can-write-to-asset-server --- assignment-client/src/assets/AssetServer.cpp | 8 ++-- domain-server/src/DomainGatekeeper.cpp | 2 + .../src/DomainServerSettingsManager.cpp | 2 + interface/src/Application.cpp | 4 +- interface/src/Menu.cpp | 4 +- .../entities/src/EntityScriptingInterface.cpp | 6 +++ .../entities/src/EntityScriptingInterface.h | 2 + libraries/entities/src/EntityTree.cpp | 45 ++++++++++++++++--- libraries/networking/src/LimitedNodeList.cpp | 21 ++++----- libraries/networking/src/LimitedNodeList.h | 4 ++ libraries/networking/src/Node.h | 2 + 11 files changed, 74 insertions(+), 26 deletions(-) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 1fb0674e7d..7f43b86328 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -235,7 +235,7 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN } void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { QString assetPath = message.readString(); auto assetHash = message.read(SHA256_HASH_LENGTH).toHex(); @@ -251,7 +251,7 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode } void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { int numberOfDeletedMappings { 0 }; message.readPrimitive(&numberOfDeletedMappings); @@ -272,7 +272,7 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared } void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { QString oldPath = message.readString(); QString newPath = message.readString(); @@ -337,7 +337,7 @@ void AssetServer::handleAssetGet(QSharedPointer message, Shared void AssetServer::handleAssetUpload(QSharedPointer message, SharedNodePointer senderNode) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID()); auto task = new UploadAssetTask(message, senderNode, _filesDirectory); diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 0abae339c6..af449eadec 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -140,6 +140,7 @@ void DomainGatekeeper::updateNodePermissions() { userPerms.isAssignment = true; userPerms.canAdjustLocks = true; userPerms.canRezPermanentEntities = true; + userPerms.canRezTemporaryEntities = true; } else { // this node is an agent userPerms.setAll(false); @@ -224,6 +225,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.isAssignment = true; userPerms.canAdjustLocks = true; userPerms.canRezPermanentEntities = true; + userPerms.canRezTemporaryEntities = true; newNode->setPermissions(userPerms); return newNode; } diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 68e3ddcc2b..64fce73191 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -244,8 +244,10 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList foreach (QString userName, permissionsSet.keys()) { if (onlyEditorsAreRezzers) { permissionsSet[userName]->canRezPermanentEntities = permissionsSet[userName]->canAdjustLocks; + permissionsSet[userName]->canRezTemporaryEntities = permissionsSet[userName]->canAdjustLocks; } else { permissionsSet[userName]->canRezPermanentEntities = true; + permissionsSet[userName]->canRezTemporaryEntities = true; } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e789b7c508..e102c13122 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4279,7 +4279,7 @@ void Application::nodeActivated(SharedNodePointer node) { if (assetDialog) { auto nodeList = DependencyManager::get(); - if (nodeList->getThisNodeCanRez()) { + if (nodeList->getThisNodeCanWriteAssets()) { // call reload on the shown asset browser dialog to get the mappings (if permissions allow) QMetaObject::invokeMethod(assetDialog, "reload"); } else { @@ -4786,7 +4786,7 @@ void Application::toggleRunningScriptsWidget() const { } void Application::toggleAssetServerWidget(QString filePath) { - if (!DependencyManager::get()->getThisNodeCanRez()) { + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { return; } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a21aa71753..7b8f835672 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -136,8 +136,8 @@ Menu::Menu() { Qt::CTRL | Qt::SHIFT | Qt::Key_A, qApp, SLOT(toggleAssetServerWidget())); auto nodeList = DependencyManager::get(); - QObject::connect(nodeList.data(), &NodeList::canRezChanged, assetServerAction, &QAction::setEnabled); - assetServerAction->setEnabled(nodeList->getThisNodeCanRez()); + QObject::connect(nodeList.data(), &NodeList::canWriteAssetsChanged, assetServerAction, &QAction::setEnabled); + assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets()); // Edit > Package Model... [advanced] addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index d09fc60d9b..e0863041a1 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -32,6 +32,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged); connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged); + connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged); } void EntityScriptingInterface::queueEntityMessage(PacketType packetType, @@ -49,6 +50,11 @@ bool EntityScriptingInterface::canRez() { return nodeList->getThisNodeCanRez(); } +bool EntityScriptingInterface::canRezTmp() { + auto nodeList = DependencyManager::get(); + return nodeList->getThisNodeCanRezTmp(); +} + void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) { if (_entityTree) { disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 8ae6a77dab..e9024eb721 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -80,6 +80,7 @@ public slots: // returns true if the DomainServer will allow this Node/Avatar to rez new entities Q_INVOKABLE bool canRez(); + Q_INVOKABLE bool canRezTmp(); /// adds a model with the specific properties Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false); @@ -179,6 +180,7 @@ signals: void canAdjustLocksChanged(bool canAdjustLocks); void canRezChanged(bool canRez); + void canRezTmpChanged(bool canRez); void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 581e0a9568..911ebd88b3 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -26,6 +26,8 @@ #include "LogHandler.h" static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; +static const float MAX_TMP_ENTITY_LIFETIME = 10 * 60; // 10 minutes + EntityTree::EntityTree(bool shouldReaverage) : Octree(shouldReaverage), @@ -128,13 +130,16 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityItemProperties properties = origProperties; bool allowLockChange; + bool canRezPermanentEntities; QUuid senderID; if (senderNode.isNull()) { auto nodeList = DependencyManager::get(); allowLockChange = nodeList->isAllowedEditor(); + canRezPermanentEntities = nodeList->getThisNodeCanRez(); senderID = nodeList->getSessionUUID(); } else { allowLockChange = senderNode->isAllowedEditor(); + canRezPermanentEntities = senderNode->getCanRez(); senderID = senderNode->getUUID(); } @@ -143,6 +148,12 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return false; } + if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) { + // we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones + qCDebug(entities) << "Refusing disallowed entity lifetime adjustment."; + return false; + } + // enforce support for locked entities. If an entity is currently locked, then the only // property we allow you to change is the locked property. if (entity->getLocked()) { @@ -308,17 +319,39 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return true; } +bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) { + float lifeTime = properties.getLifetime(); + + if (lifeTime == 0.0f || lifeTime > MAX_TMP_ENTITY_LIFETIME) { + // this is an attempt to rez a permanent entity. + if (!canRez) { + return false; + } + } else { + // this is an attempt to rez a temporary entity. + if (!canRezTmp) { + return false; + } + } + + return true; +} + EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer result = NULL; + auto nodeList = DependencyManager::get(); + if (!nodeList) { + qDebug() << "EntityTree::addEntity -- can't get NodeList"; + return nullptr; + } + bool clientOnly = properties.getClientOnly(); - if (!clientOnly && getIsClient()) { + if (!clientOnly && getIsClient() && + !permissionsAllowRez(properties, nodeList->getThisNodeCanRez(), nodeList->getThisNodeCanRezTmp())) { // if our Node isn't allowed to create entities in this domain, don't try. - auto nodeList = DependencyManager::get(); - if (nodeList && !nodeList->getThisNodeCanRez()) { - return NULL; - } + return nullptr; } bool recordCreationTime = false; @@ -920,7 +953,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endUpdate = usecTimestampNow(); _totalUpdates++; } else if (message.getType() == PacketType::EntityAdd) { - if (senderNode->getCanRez()) { + if (permissionsAllowRez(properties, senderNode->getCanRez(), senderNode->getCanRezTmp())) { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); startCreate = usecTimestampNow(); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a4daff5d35..d7a2d47fab 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -130,26 +130,23 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) { } } - void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { - bool emitIsAllowedEditorChanged { false }; - bool emitCanRezChanged { false }; - - if (_permissions.canAdjustLocks != newPermissions.canAdjustLocks) { - emitIsAllowedEditorChanged = true; - } - if (_permissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) { - emitCanRezChanged = true; - } + NodePermissions originalPermissions = _permissions; _permissions = newPermissions; - if (emitIsAllowedEditorChanged) { + if (originalPermissions.canAdjustLocks != newPermissions.canAdjustLocks) { emit isAllowedEditorChanged(_permissions.canAdjustLocks); } - if (emitCanRezChanged) { + if (originalPermissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) { emit canRezChanged(_permissions.canRezPermanentEntities); } + if (originalPermissions.canRezTemporaryEntities != newPermissions.canRezTemporaryEntities) { + emit canRezTmpChanged(_permissions.canRezTemporaryEntities); + } + if (originalPermissions.canWriteToAssetServer != newPermissions.canWriteToAssetServer) { + emit canWriteAssetsChanged(_permissions.canWriteToAssetServer); + } } QUdpSocket& LimitedNodeList::getDTLSSocket() { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index e4c3ae9dec..483aa0734c 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -107,6 +107,8 @@ public: void setPermissions(const NodePermissions& newPermissions); bool isAllowedEditor() const { return _permissions.canAdjustLocks; } bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; } + bool getThisNodeCanRezTmp() const { return _permissions.canRezTemporaryEntities; } + bool getThisNodeCanWriteAssets() const { return _permissions.canWriteToAssetServer; } quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); } QUdpSocket& getDTLSSocket(); @@ -252,6 +254,8 @@ signals: void isAllowedEditorChanged(bool isAllowedEditor); void canRezChanged(bool canRez); + void canRezTmpChanged(bool canRezTmp); + void canWriteAssetsChanged(bool canWriteAssets); protected slots: void connectedForLocalSocketTest(); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 367e17d345..b277ac0083 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -63,6 +63,8 @@ public: NodePermissions getPermissions() const { return _permissions; } bool isAllowedEditor() const { return _permissions.canAdjustLocks; } bool getCanRez() const { return _permissions.canRezPermanentEntities; } + bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; } + bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; } friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node); From 7696ead0126c9232a0e388c61fb13fe72352c8c4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 7 Jun 2016 16:38:52 -0700 Subject: [PATCH 0432/1237] look for standard permissions in standard table rather than in user table --- domain-server/src/DomainGatekeeper.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index af449eadec..6377a37f34 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -153,12 +153,12 @@ void DomainGatekeeper::updateNodePermissions() { } if (username.isEmpty()) { - userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); } else { if (_server->_settingsManager.havePermissionsForName(username)) { userPerms = _server->_settingsManager.getPermissionsForName(username); } else { - userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLoggedIn); + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); } } } @@ -263,7 +263,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (username.isEmpty()) { // they didn't tell us who they are - userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); qDebug() << "user-permissions: no username, so:" << userPerms; } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they are sent us a username and the signature verifies it @@ -274,7 +274,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect qDebug() << "user-permissions: specific user matches, so:" << userPerms; } else { // they are logged into metaverse, but we don't have specific permissions for them. - userPerms |= _server->_settingsManager.getPermissionsForName(NodePermissions::standardNameLoggedIn); + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); qDebug() << "user-permissions: user is logged in, so:" << userPerms; } } else { From 29d9f51b584e205d62bf944af6400cf0beebc1a3 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 7 Jun 2016 16:46:17 -0700 Subject: [PATCH 0433/1237] Adding missed file --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8f4d80a86f..c0a7857c95 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1418,7 +1418,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Overlays", &_overlays); rootContext->setContextProperty("Window", DependencyManager::get().data()); - rootContext->setContextProperty("Menu", MenuScriptingInterface::getInstance()); + rootContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); rootContext->setContextProperty("Stats", Stats::getInstance()); rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); rootContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); From 71fb0e665ffbb92e18dc092934b62d3dba1298a5 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 7 Jun 2016 16:46:08 -0700 Subject: [PATCH 0434/1237] Fix OSX warning --- libraries/entities/src/EntityTreeElement.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 37a0f36d2f..a7baeb361b 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -55,7 +55,7 @@ void EntityTreeElement::debugExtraEncodeData(EncodeBitstreamParams& params) cons if (extraEncodeData->contains(this)) { EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData - = static_cast(extraEncodeData->value(this)); + = static_cast((*extraEncodeData)[this]); qCDebug(entities) << " encode data:" << entityTreeElementExtraEncodeData; } else { qCDebug(entities) << " encode data: MISSING!!"; @@ -97,7 +97,7 @@ bool EntityTreeElement::shouldIncludeChildData(int childIndex, EncodeBitstreamPa if (extraEncodeData->contains(this)) { EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData - = static_cast(extraEncodeData->value(this)); + = static_cast((*extraEncodeData)[this]); bool childCompleted = entityTreeElementExtraEncodeData->childCompleted[childIndex]; @@ -126,7 +126,7 @@ bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const if (extraEncodeData->contains(this)) { EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData - = static_cast(extraEncodeData->value(this)); + = static_cast((*extraEncodeData)[this]); // If we know that ALL subtrees below us have already been recursed, then we don't // need to recurse this child. @@ -140,7 +140,7 @@ void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppen assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes if (extraEncodeData->contains(this)) { EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData - = static_cast(extraEncodeData->value(this)); + = static_cast((*extraEncodeData)[this]); if (childAppendState == OctreeElement::COMPLETED) { entityTreeElementExtraEncodeData->childCompleted[childIndex] = true; @@ -165,7 +165,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con assert(extraEncodeData->contains(this)); EntityTreeElementExtraEncodeData* thisExtraEncodeData - = static_cast(extraEncodeData->value(this)); + = static_cast((*extraEncodeData)[this]); // Note: this will be called when OUR element has finished running through encodeTreeBitstreamRecursion() // which means, it's possible that our parent element hasn't finished encoding OUR data... so @@ -241,7 +241,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData bool hadElementExtraData = false; if (extraEncodeData && extraEncodeData->contains(this)) { entityTreeElementExtraEncodeData = - static_cast(extraEncodeData->value(this)); + static_cast((*extraEncodeData)[this]); hadElementExtraData = true; } else { // if there wasn't one already, then create one @@ -268,7 +268,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData //assert(extraEncodeData); //assert(extraEncodeData->contains(this)); - //entityTreeElementExtraEncodeData = static_cast(extraEncodeData->value(this)); + //entityTreeElementExtraEncodeData = static_cast((*extraEncodeData)[this]); LevelDetails elementLevel = packetData->startLevel(); From 30d8ae36e8db11d17f7e4ea237ed89771630d221 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 7 Jun 2016 16:55:32 -0700 Subject: [PATCH 0435/1237] Added MyAvatar.characterControllerEnabled property --- interface/src/avatar/MyAvatar.cpp | 26 ++++++++++++++++++++------ interface/src/avatar/MyAvatar.h | 4 ++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0f723d29e3..3495a05962 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -234,7 +234,7 @@ QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) { void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "reset", Q_ARG(bool, andRecenter)); + QMetaObject::invokeMethod(this, "reset", Q_ARG(bool, andRecenter), Q_ARG(bool, andReload), Q_ARG(bool, andHead)); return; } @@ -1816,6 +1816,16 @@ void MyAvatar::updateMotionBehaviorFromMenu() { _motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; } + setCharacterControllerEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController)); +} + +void MyAvatar::setCharacterControllerEnabled(bool enabled) { + + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setCharacterControllerEnabled", Q_ARG(bool, enabled)); + return; + } + bool ghostingAllowed = true; EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); if (entityTreeRenderer) { @@ -1824,12 +1834,16 @@ void MyAvatar::updateMotionBehaviorFromMenu() { ghostingAllowed = zone->getGhostingAllowed(); } } - bool checked = menu->isOptionChecked(MenuOption::EnableCharacterController); - if (!ghostingAllowed) { - checked = true; - } + _characterController.setEnabled(ghostingAllowed ? enabled : true); +} - _characterController.setEnabled(checked); +bool MyAvatar::getCharacterControllerEnabled() { + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(this, "getCharacterControllerEnabled", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); + return result; + } + return _characterController.isEnabled(); } void MyAvatar::clearDriveKeys() { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a938aea675..05afe39a32 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -84,6 +84,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) + Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) public: explicit MyAvatar(RigPointer rig); @@ -265,6 +266,9 @@ public: controller::Pose getLeftHandControllerPoseInAvatarFrame() const; controller::Pose getRightHandControllerPoseInAvatarFrame() const; + Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); + Q_INVOKABLE bool getCharacterControllerEnabled(); + public slots: void increaseSize(); void decreaseSize(); From 2c1d20bd1af4abf59a30f86125f5b2c24210b769 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 7 Jun 2016 17:12:24 -0700 Subject: [PATCH 0436/1237] removed _deviceCounts, cache joystick available inputs, added deadzone to gamepad thumbsticks --- interface/resources/controllers/xbox.json | 8 +- .../src/controllers/UserInputMapper.cpp | 14 --- .../src/controllers/UserInputMapper.h | 3 - plugins/hifiSdl2/src/Joystick.cpp | 94 ++++++++++--------- plugins/hifiSdl2/src/Joystick.h | 2 + 5 files changed, 54 insertions(+), 67 deletions(-) diff --git a/interface/resources/controllers/xbox.json b/interface/resources/controllers/xbox.json index 8c341dff83..fdac70ff33 100644 --- a/interface/resources/controllers/xbox.json +++ b/interface/resources/controllers/xbox.json @@ -1,14 +1,14 @@ { "name": "XBox to Standard", "channels": [ - { "from": "GamePad.LY", "to": "Standard.LY" }, - { "from": "GamePad.LX", "to": "Standard.LX" }, + { "from": "GamePad.LY", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LY" }, + { "from": "GamePad.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LX" }, { "from": "GamePad.LT", "to": "Standard.LT" }, { "from": "GamePad.LB", "to": "Standard.LB" }, { "from": "GamePad.LS", "to": "Standard.LS" }, - { "from": "GamePad.RY", "to": "Standard.RY" }, - { "from": "GamePad.RX", "to": "Standard.RX" }, + { "from": "GamePad.RY", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RY" }, + { "from": "GamePad.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RX" }, { "from": "GamePad.RT", "to": "Standard.RT" }, { "from": "GamePad.RB", "to": "Standard.RB" }, { "from": "GamePad.RS", "to": "Standard.RS" }, diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 9dbad89a9b..4df460cbea 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -62,14 +62,6 @@ namespace controller { UserInputMapper::~UserInputMapper() { } -int UserInputMapper::recordDeviceOfType(const QString& deviceName) { - if (!_deviceCounts.contains(deviceName)) { - _deviceCounts[deviceName] = 0; - } - _deviceCounts[deviceName] += 1; - return _deviceCounts[deviceName]; -} - void UserInputMapper::registerDevice(InputDevice::Pointer device) { Locker locker(_lock); if (device->_deviceID == Input::INVALID_DEVICE) { @@ -77,8 +69,6 @@ void UserInputMapper::registerDevice(InputDevice::Pointer device) { } const auto& deviceID = device->_deviceID; - recordDeviceOfType(device->getName()); - qCDebug(controllers) << "Registered input device <" << device->getName() << "> deviceID = " << deviceID; for (const auto& inputMapping : device->getAvailableInputs()) { @@ -126,10 +116,6 @@ void UserInputMapper::removeDevice(int deviceID) { auto device = proxyEntry->second; qCDebug(controllers) << "Unregistering input device <" << device->getName() << "> deviceID = " << deviceID; - if (_deviceCounts.contains(device->getName())) { - _deviceCounts[device->getName()] -= 1; - } - unloadMappings(device->getDefaultMappingConfigs()); auto mappingsEntry = _mappingsByDevice.find(deviceID); diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index 2c8aabc099..f752b05b9f 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -141,9 +141,6 @@ namespace controller { std::vector _poseStates = std::vector(toInt(Action::NUM_ACTIONS)); std::vector _lastStandardStates = std::vector(); - int recordDeviceOfType(const QString& deviceName); - QHash _deviceCounts; - static float getValue(const EndpointPointer& endpoint, bool peek = false); static Pose getPose(const EndpointPointer& endpoint, bool peek = false); diff --git a/plugins/hifiSdl2/src/Joystick.cpp b/plugins/hifiSdl2/src/Joystick.cpp index 614adff7d6..7233bf3079 100644 --- a/plugins/hifiSdl2/src/Joystick.cpp +++ b/plugins/hifiSdl2/src/Joystick.cpp @@ -64,53 +64,55 @@ void Joystick::handleButtonEvent(const SDL_ControllerButtonEvent& event) { controller::Input::NamedVector Joystick::getAvailableInputs() const { using namespace controller; - const Input::NamedVector availableInputs{ - makePair(A, "A"), - makePair(B, "B"), - makePair(X, "X"), - makePair(Y, "Y"), - // DPad - makePair(DU, "DU"), - makePair(DD, "DD"), - makePair(DL, "DL"), - makePair(DR, "DR"), - // Bumpers - makePair(LB, "LB"), - makePair(RB, "RB"), - // Stick press - makePair(LS, "LS"), - makePair(RS, "RS"), - // Center buttons - makePair(START, "Start"), - makePair(BACK, "Back"), - // Analog sticks - makePair(LX, "LX"), - makePair(LY, "LY"), - makePair(RX, "RX"), - makePair(RY, "RY"), - - // Triggers - makePair(LT, "LT"), - makePair(RT, "RT"), + if (_availableInputs.length() == 0) { + _availableInputs = { + makePair(A, "A"), + makePair(B, "B"), + makePair(X, "X"), + makePair(Y, "Y"), + // DPad + makePair(DU, "DU"), + makePair(DD, "DD"), + makePair(DL, "DL"), + makePair(DR, "DR"), + // Bumpers + makePair(LB, "LB"), + makePair(RB, "RB"), + // Stick press + makePair(LS, "LS"), + makePair(RS, "RS"), + // Center buttons + makePair(START, "Start"), + makePair(BACK, "Back"), + // Analog sticks + makePair(LX, "LX"), + makePair(LY, "LY"), + makePair(RX, "RX"), + makePair(RY, "RY"), - // Aliases, PlayStation style names - makePair(LB, "L1"), - makePair(RB, "R1"), - makePair(LT, "L2"), - makePair(RT, "R2"), - makePair(LS, "L3"), - makePair(RS, "R3"), - makePair(BACK, "Select"), - makePair(A, "Cross"), - makePair(B, "Circle"), - makePair(X, "Square"), - makePair(Y, "Triangle"), - makePair(DU, "Up"), - makePair(DD, "Down"), - makePair(DL, "Left"), - makePair(DR, "Right"), - }; - return availableInputs; + // Triggers + makePair(LT, "LT"), + makePair(RT, "RT"), + + // Aliases, PlayStation style names + makePair(LB, "L1"), + makePair(RB, "R1"), + makePair(LT, "L2"), + makePair(RT, "R2"), + makePair(LS, "L3"), + makePair(RS, "R3"), + makePair(BACK, "Select"), + makePair(A, "Cross"), + makePair(B, "Circle"), + makePair(X, "Square"), + makePair(Y, "Triangle"), + makePair(DU, "Up"), + makePair(DD, "Down"), + makePair(DL, "Left"), + makePair(DR, "Right"), + }; + } + return _availableInputs; } QString Joystick::getDefaultMappingConfig() const { diff --git a/plugins/hifiSdl2/src/Joystick.h b/plugins/hifiSdl2/src/Joystick.h index e2eaeaef8b..fb4d205689 100644 --- a/plugins/hifiSdl2/src/Joystick.h +++ b/plugins/hifiSdl2/src/Joystick.h @@ -53,6 +53,8 @@ private: SDL_GameController* _sdlGameController; SDL_Joystick* _sdlJoystick; SDL_JoystickID _instanceId; + + mutable controller::Input::NamedVector _availableInputs; }; #endif // hifi_Joystick_h From 75a5f6bd89ef816ff920dbaa6e816806dc7b2d56 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 7 Jun 2016 18:13:40 -0700 Subject: [PATCH 0437/1237] Intoducing the bluring as a separate Job reusable and the first version of curvature --- .../gl/src/gl/QOpenGLDebugLoggerWrapper.cpp | 7 +- .../gl/src/gl/QOpenGLDebugLoggerWrapper.h | 6 + .../render-utils/src/DebugDeferredBuffer.cpp | 3 +- .../src/DeferredFrameTransform.cpp | 22 ++- .../render-utils/src/DeferredFrameTransform.h | 2 + .../render-utils/src/DeferredTransform.slh | 15 +- .../render-utils/src/FramebufferCache.cpp | 8 +- .../render-utils/src/RenderDeferredTask.cpp | 5 +- .../render-utils/src/SurfaceGeometry.slh | 46 ----- .../render-utils/src/SurfaceGeometryPass.cpp | 29 ++- .../render-utils/src/SurfaceGeometryPass.h | 20 +- .../src/surfaceGeometry_makeCurvature.slf | 116 ++++++++---- libraries/render/src/render/BlurTask.cpp | 177 ++++++++++++++++++ libraries/render/src/render/BlurTask.h | 91 +++++++++ libraries/render/src/render/BlurTask.slh | 65 +++++++ libraries/render/src/render/blurGaussianH.slf | 23 +++ libraries/render/src/render/blurGaussianV.slf | 22 +++ .../render/debugSurfaceGeometryPass.js | 20 ++ .../utilities/render/framebuffer.qml | 10 +- .../utilities/render/surfaceGeometryPass.qml | 33 ++++ 20 files changed, 606 insertions(+), 114 deletions(-) create mode 100644 libraries/render/src/render/BlurTask.cpp create mode 100644 libraries/render/src/render/BlurTask.h create mode 100644 libraries/render/src/render/BlurTask.slh create mode 100644 libraries/render/src/render/blurGaussianH.slf create mode 100644 libraries/render/src/render/blurGaussianV.slf create mode 100644 scripts/developer/utilities/render/debugSurfaceGeometryPass.js create mode 100644 scripts/developer/utilities/render/surfaceGeometryPass.qml diff --git a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp b/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp index bd185034f4..2a351ead7e 100644 --- a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp @@ -14,11 +14,16 @@ #include #include +void OpenGLDebug::log(const QOpenGLDebugMessage & debugMessage) { + qDebug() << debugMessage; +} + void setupDebugLogger(QObject* window) { QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(window); logger->initialize(); // initializes in the current context, i.e. ctx logger->enableMessages(); QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, window, [&](const QOpenGLDebugMessage & debugMessage) { - qDebug() << debugMessage; + OpenGLDebug::log(debugMessage); + }); } \ No newline at end of file diff --git a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h b/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h index e2b1c5d9d4..2a378a712a 100644 --- a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h +++ b/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h @@ -13,7 +13,13 @@ #define hifi_QOpenGLDebugLoggerWrapper_h class QObject; +class QOpenGLDebugMessage; void setupDebugLogger(QObject* window); +class OpenGLDebug { +public: + static void log(const QOpenGLDebugMessage & debugMessage); +}; + #endif // hifi_QOpenGLDebugLoggerWrapper_h \ No newline at end of file diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 5de61df423..0a3de15c36 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -141,7 +141,8 @@ static const std::string DEFAULT_PYRAMID_DEPTH_SHADER { static const std::string DEFAULT_CURVATURE_SHADER{ "vec4 getFragmentColor() {" - " return vec4(texture(curvatureMap, uv).xyz, 1.0);" + // " return vec4(pow(vec3(texture(curvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" + " return vec4(pow(vec3(texture(curvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" " }" }; diff --git a/libraries/render-utils/src/DeferredFrameTransform.cpp b/libraries/render-utils/src/DeferredFrameTransform.cpp index 1cb85058d8..c5345e24f9 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.cpp +++ b/libraries/render-utils/src/DeferredFrameTransform.cpp @@ -23,15 +23,18 @@ void DeferredFrameTransform::update(RenderArgs* args) { // Update the depth info with near and far (same for stereo) auto nearZ = args->getViewFrustum().getNearClip(); auto farZ = args->getViewFrustum().getFarClip(); - _frameTransformBuffer.edit().depthInfo = glm::vec4(nearZ*farZ, farZ - nearZ, -farZ, 0.0f); - _frameTransformBuffer.edit().pixelInfo = args->_viewport; + auto& frameTransformBuffer = _frameTransformBuffer.edit(); + frameTransformBuffer.depthInfo = glm::vec4(nearZ*farZ, farZ - nearZ, -farZ, 0.0f); + + frameTransformBuffer.pixelInfo = args->_viewport; //_parametersBuffer.edit()._ditheringInfo.y += 0.25f; Transform cameraTransform; args->getViewFrustum().evalViewTransform(cameraTransform); - cameraTransform.getMatrix(_frameTransformBuffer.edit().invView); + cameraTransform.getMatrix(frameTransformBuffer.invView); + cameraTransform.getInverseMatrix(frameTransformBuffer.view); // Running in stero ? bool isStereo = args->_context->isStereo(); @@ -39,10 +42,9 @@ void DeferredFrameTransform::update(RenderArgs* args) { // Eval the mono projection mat4 monoProjMat; args->getViewFrustum().evalProjectionMatrix(monoProjMat); - _frameTransformBuffer.edit().projection[0] = monoProjMat; - _frameTransformBuffer.edit().stereoInfo = glm::vec4(0.0f, (float)args->_viewport.z, 0.0f, 0.0f); - _frameTransformBuffer.edit().invpixelInfo = glm::vec4(1.0f / args->_viewport.z, 1.0f / args->_viewport.w, 0.0f, 0.0f); - + frameTransformBuffer.projection[0] = monoProjMat; + frameTransformBuffer.stereoInfo = glm::vec4(0.0f, (float)args->_viewport.z, 0.0f, 0.0f); + frameTransformBuffer.invpixelInfo = glm::vec4(1.0f / args->_viewport.z, 1.0f / args->_viewport.w, 0.0f, 0.0f); } else { mat4 projMats[2]; @@ -53,11 +55,11 @@ void DeferredFrameTransform::update(RenderArgs* args) { for (int i = 0; i < 2; i++) { // Compose the mono Eye space to Stereo clip space Projection Matrix auto sideViewMat = projMats[i] * eyeViews[i]; - _frameTransformBuffer.edit().projection[i] = sideViewMat; + frameTransformBuffer.projection[i] = sideViewMat; } - _frameTransformBuffer.edit().stereoInfo = glm::vec4(1.0f, (float)(args->_viewport.z >> 1), 0.0f, 1.0f); - _frameTransformBuffer.edit().invpixelInfo = glm::vec4(1.0f / (float)(args->_viewport.z >> 1), 1.0f / args->_viewport.w, 0.0f, 0.0f); + frameTransformBuffer.stereoInfo = glm::vec4(1.0f, (float)(args->_viewport.z >> 1), 0.0f, 1.0f); + frameTransformBuffer.invpixelInfo = glm::vec4(1.0f / (float)(args->_viewport.z >> 1), 1.0f / args->_viewport.w, 0.0f, 0.0f); } } diff --git a/libraries/render-utils/src/DeferredFrameTransform.h b/libraries/render-utils/src/DeferredFrameTransform.h index b6c3667c28..82bc989028 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.h +++ b/libraries/render-utils/src/DeferredFrameTransform.h @@ -47,6 +47,8 @@ protected: glm::mat4 projection[2]; // Inv View matrix from eye space (mono) to world space glm::mat4 invView; + // View matrix from world space to eye space (mono) + glm::mat4 view; FrameTransform() {} }; diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 7c3e7bf4da..647ac0a76c 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -20,6 +20,7 @@ struct DeferredFrameTransform { vec4 _stereoInfo; mat4 _projection[2]; mat4 _viewInverse; + mat4 _view; }; uniform deferredFrameTransformBuffer { @@ -45,6 +46,14 @@ mat4 getProjection(int side) { return frameTransform._projection[side]; } +mat4 getViewInverse() { + return frameTransform._viewInverse; +} + +mat4 getView() { + return frameTransform._view; +} + bool isStereo() { return frameTransform._stereoInfo.x > 0.0f; } @@ -53,9 +62,9 @@ float getStereoSideWidth(int resolutionLevel) { return float(int(frameTransform._stereoInfo.y) >> resolutionLevel); } -ivec3 getStereoSideInfo(int xPos, int resolutionLevel) { +ivec4 getStereoSideInfo(int xPos, int resolutionLevel) { int sideWidth = int(getStereoSideWidth(resolutionLevel)); - return ivec3(xPos < sideWidth ? ivec2(0, 0) : ivec2(1, sideWidth), sideWidth); + return ivec4(xPos < sideWidth ? ivec2(0, 0) : ivec2(1, sideWidth), sideWidth, isStereo()); } float evalZeyeFromZdb(float depth) { @@ -75,7 +84,7 @@ vec3 evalEyePositionFromZeye(int side, float Zeye, vec2 texcoord) { return vec3(Xe, Ye, Zeye); } -ivec2 getPixelPosNclipPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out vec2 nclipPos, out ivec3 stereoSide) { +ivec2 getPixelPosNclipPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out vec2 nclipPos, out ivec4 stereoSide) { ivec2 fragPos = ivec2(glFragCoord.xy); stereoSide = getStereoSideInfo(fragPos.x, 0); diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 63ae7e521e..79a8af8eb7 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -47,6 +47,8 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { _lightingFramebuffer.reset(); _depthPyramidFramebuffer.reset(); _depthPyramidTexture.reset(); + _curvatureFramebuffer.reset(); + _curvatureTexture.reset(); _occlusionFramebuffer.reset(); _occlusionTexture.reset(); _occlusionBlurredFramebuffer.reset(); @@ -109,15 +111,12 @@ void FramebufferCache::createPrimaryFramebuffer() { _depthPyramidFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _depthPyramidFramebuffer->setRenderBuffer(0, _depthPyramidTexture); _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - - - + _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - resizeAmbientOcclusionBuffers(); } @@ -266,7 +265,6 @@ gpu::TexturePointer FramebufferCache::getCurvatureTexture() { return _curvatureTexture; } - void FramebufferCache::setAmbientOcclusionResolutionLevel(int level) { const int MAX_AO_RESOLUTION_LEVEL = 4; level = std::max(0, std::min(level, MAX_AO_RESOLUTION_LEVEL)); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 6fa98089be..20b12c7516 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "DebugDeferredBuffer.h" #include "DeferredLightingEffect.h" @@ -109,7 +110,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DrawBackgroundDeferred", background); // Opaque all rendered, generate surface geometry buffers - addJob("SurfaceGeometry", deferredFrameTransform); + const auto curvatureFramebuffer = addJob("SurfaceGeometry", deferredFrameTransform); + + addJob("DiffuseCurvature", curvatureFramebuffer); // AO job addJob("AmbientOcclusion"); diff --git a/libraries/render-utils/src/SurfaceGeometry.slh b/libraries/render-utils/src/SurfaceGeometry.slh index cd9e4c8ac4..5dd5b8128e 100644 --- a/libraries/render-utils/src/SurfaceGeometry.slh +++ b/libraries/render-utils/src/SurfaceGeometry.slh @@ -13,50 +13,4 @@ <$declareDeferredFrameTransform()$> -uniform sampler2D depthMap; -out vec4 outFragColor; - -void main(void) { - // Fetch normal and depth of current pixel - float4 samplePos = sourceTexture.SampleLevel(pointSampler, input.texUV, 0.0f); - float4 sampleNormal = depthTexture.SampleLevel(pointSampler, input.texUV, 0.0f); - - // Calculate the width scale. - float distanceToProjectionWindow = 1.0f / tan(0.5f * radians(fov)); - float scale = distanceToProjectionWindow / sampleNormal.w; - - // Calculate dF/du and dF/dv - float2 du = float2( 1.0f, 0.0f ) * UVfactor.x * screenPixel * scale; - float2 dv = float2( 0.0f, 1.0f ) * UVfactor.x * screenPixel * scale; - float4 dFdu = depthTexture.SampleLevel(linearSampler, input.texUV + du.xy, 0.0f) - - depthTexture.SampleLevel(linearSampler, input.texUV - du.xy, 0.0f); - float4 dFdv = depthTexture.SampleLevel(linearSampler, input.texUV + dv.xy, 0.0f) - - depthTexture.SampleLevel(linearSampler, input.texUV - dv.xy, 0.0f); - dFdu *= step(abs(dFdu.w), 0.1f); dFdv *= step(abs(dFdv.w), 0.1f); - - // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) - float dist = 1.0f; samplePos.w = 1.0f; - float2 centerOffset = ((input.texUV - 0.5f) * 2.0f); - float4 px = mul( samplePos + float4( dist, 0.0f, 0.0f, 0.0f ), matViewProj ); - float4 py = mul( samplePos + float4( 0.0f, dist, 0.0f, 0.0f ), matViewProj ); - float4 pz = mul( samplePos + float4( 0.0f, 0.0f, dist, 0.0f ), matViewProj ); - #ifdef INVERT_TEXTURE_V - centerOffset.y = -centerOffset.y; - #endif - px.xy = ((px.xy / px.w) - centerOffset) / scale; - py.xy = ((py.xy / py.w) - centerOffset) / scale; - pz.xy = ((pz.xy / pz.w) - centerOffset) / scale; - #ifdef INVERT_TEXTURE_V - px.y = -px.y; py.y = -py.y; pz.y = -pz.y; - #endif - - // Calculate dF/dx, dF/dy and dF/dz using chain rule - float4 dFdx = dFdu * px.x + dFdv * px.y; - float4 dFdy = dFdu * py.x + dFdv * py.y; - float4 dFdz = dFdu * pz.x + dFdv * pz.y; - - // Calculate the mean curvature - float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333f) * 100.0f; - return (float4( sampleNormal.xyz, meanCurvature ) + 1.0f) * 0.5f; -} diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 6e4bc466bb..d5db4e74d8 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -24,13 +24,28 @@ const int SurfaceGeometryPass_NormalMapSlot = 1; #include "surfaceGeometry_makeCurvature_frag.h" + SurfaceGeometryPass::SurfaceGeometryPass() { + Parameters parameters; + _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); } void SurfaceGeometryPass::configure(const Config& config) { + + if (config.depthThreshold != getCurvatureDepthThreshold()) { + _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold; + } + + if (config.basisScale != getCurvatureBasisScale()) { + _parametersBuffer.edit().curvatureInfo.y = config.basisScale; + } + + if (config.curvatureScale != getCurvatureScale()) { + _parametersBuffer.edit().curvatureInfo.w = config.curvatureScale; + } } -void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { +void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -44,6 +59,9 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); + curvatureFramebuffer = curvatureFBO; + + auto curvatureTexture = framebufferCache->getCurvatureTexture(); QSize framebufferSize = framebufferCache->getFrameBufferSize(); float sMin = args->_viewport.x / (float)framebufferSize.width(); @@ -68,7 +86,8 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setModelTransform(model); batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); - + batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); + // Pyramid pass batch.setFramebuffer(pyramidFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(args->getViewFrustum().getFarClip(), 0.0f, 0.0f, 0.0f)); @@ -76,17 +95,15 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); - // Pyramid pass + // Curvature pass batch.setFramebuffer(curvatureFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); batch.setPipeline(curvaturePipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, pyramidTexture); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, normalTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); - batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, nullptr); - }); } @@ -125,7 +142,7 @@ const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SurfaceGeometryPass_FrameTransformSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("ambientOcclusionParamsBuffer"), SurfaceGeometryPass_ParamsSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("surfaceGeometryParamsBuffer"), SurfaceGeometryPass_ParamsSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), SurfaceGeometryPass_DepthMapSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), SurfaceGeometryPass_NormalMapSlot)); gpu::Shader::makeProgram(*program, slotBindings); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 13ca7fa199..04c0276181 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -19,10 +19,17 @@ class SurfaceGeometryPassConfig : public render::Job::Config { Q_OBJECT + Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) + Q_PROPERTY(float basisScale MEMBER basisScale NOTIFY dirty) + Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) Q_PROPERTY(double gpuTime READ getGpuTime) public: SurfaceGeometryPassConfig() : render::Job::Config(true) {} + float depthThreshold{ 0.1f }; + float basisScale{ 1.0f }; + float curvatureScale{ 100.0f }; + double getGpuTime() { return gpuTime; } double gpuTime{ 0.0 }; @@ -34,13 +41,17 @@ signals: class SurfaceGeometryPass { public: using Config = SurfaceGeometryPassConfig; - using JobModel = render::Job::ModelI; + using JobModel = render::Job::ModelIO; SurfaceGeometryPass(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer); + float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } + float getCurvatureBasisScale() const { return _parametersBuffer.get().curvatureInfo.y; } + float getCurvatureScale() const { return _parametersBuffer.get().curvatureInfo.w; } + private: typedef gpu::BufferView UniformBufferView; @@ -49,8 +60,8 @@ private: public: // Resolution info glm::vec4 resolutionInfo { -1.0f, 0.0f, 0.0f, 0.0f }; - // radius info is { R, R^2, 1 / R^6, ObscuranceScale} - glm::vec4 radiusInfo{ 0.5f, 0.5f * 0.5f, 1.0f / (0.25f * 0.25f * 0.25f), 1.0f }; + // Curvature algorithm + glm::vec4 curvatureInfo{ 0.0f }; // Dithering info glm::vec4 ditheringInfo { 0.0f, 0.0f, 0.01f, 1.0f }; // Sampling info @@ -68,7 +79,6 @@ private: const gpu::PipelinePointer& getLinearDepthPipeline(); const gpu::PipelinePointer& getCurvaturePipeline(); - gpu::PipelinePointer _linearDepthPipeline; gpu::PipelinePointer _curvaturePipeline; diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index d0281a2949..d441fcb8f6 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -12,6 +12,37 @@ <@include DeferredTransform.slh@> <$declareDeferredFrameTransform()$> +struct SurfaceGeometryParams { + // Resolution info + vec4 resolutionInfo; + // Curvature algorithm + vec4 curvatureInfo; + // Dithering info + vec4 ditheringInfo; + // Sampling info + vec4 sampleInfo; + // Blurring info + vec4 blurInfo; + // gaussian distribution coefficients first is the sampling radius (max is 6) + vec4 _gaussianCoefs[2]; +}; + +uniform surfaceGeometryParamsBuffer { + SurfaceGeometryParams params; +}; + +float getCurvatureDepthThreshold() { + return params.curvatureInfo.x; +} + +float getCurvatureBasisScale() { + return params.curvatureInfo.y; +} + +float getCurvatureScale() { + return params.curvatureInfo.w; +} + uniform sampler2D linearDepthMap; float getZEye(ivec2 pixel) { @@ -44,9 +75,18 @@ vec3 unpackNormal(in vec3 p) { return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); } +vec2 sideToFrameNclip(vec2 side, vec2 nclipPos) { + return vec2((nclipPos.x + side.x) * side.y, nclipPos.y); +} + uniform sampler2D normalMap; + +vec3 getRawNormal(vec2 texcoord) { + return texture(normalMap, texcoord).xyz; +} + vec3 getWorldNormal(vec2 texcoord) { - vec3 rawNormal = texture(normalMap, texcoord).xyz; + vec3 rawNormal = getRawNormal(texcoord); return unpackNormal(rawNormal); } @@ -70,56 +110,62 @@ void main(void) { // Pixel being shaded ivec2 pixelPos; vec2 nclipPos; - ivec3 stereoSide; + ivec4 stereoSide; ivec2 framePixelPos = getPixelPosNclipPosAndSide(gl_FragCoord.xy, pixelPos, nclipPos, stereoSide); + vec2 stereoSideClip = vec2(stereoSide.x, (isStereo() ? 0.5 : 1.0)); + vec2 frameNclipPos = sideToFrameNclip(stereoSideClip, nclipPos); // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); + vec3 worldNormal = getWorldNormal(frameNclipPos); + // The position of the pixel fragment in Eye space then in world space vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, nclipPos); vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; - vec3 moduloPos = fract(worldPos); + // Calculate the perspective scale. + float perspectiveScale =(-getProjScaleEye() / Zeye); + //outFragColor = vec4(vec3(perspectiveScale * 0.1), 1.0); - outFragColor = vec4(moduloPos, 1.0); - - // Calculate the width scale. - - // float distanceToProjectionWindow = 1.0f / tan(0.5f * radians(fov)); - float scale = -getProjScaleEye() / Zeye; - - vec2 viewportScale = scale * getInvWidthHeight(); + vec2 viewportScale = perspectiveScale * getInvWidthHeight(); // Calculate dF/du and dF/dv + float threshold = getCurvatureDepthThreshold(); vec2 du = vec2( 1.0f, 0.0f ) * viewportScale.x; vec2 dv = vec2( 0.0f, 1.0f ) * viewportScale.y; - - outFragColor = vec4(du.x, dv.y, scale, 1.0); - - vec4 dFdu = vec4(getWorldNormalDiff(nclipPos, du), getEyeDepthDiff(nclipPos, du)); - vec4 dFdv = vec4(getWorldNormalDiff(nclipPos, dv), getEyeDepthDiff(nclipPos, dv)); - dFdu *= step(abs(dFdu.w), 0.1f); dFdv *= step(abs(dFdv.w), 0.1f); + vec4 dFdu = vec4(getWorldNormalDiff(frameNclipPos, du), getEyeDepthDiff(frameNclipPos, du)); + vec4 dFdv = vec4(getWorldNormalDiff(frameNclipPos, dv), getEyeDepthDiff(frameNclipPos, dv)); + dFdu *= step(abs(dFdu.w), threshold); + dFdv *= step(abs(dFdv.w), threshold); outFragColor = vec4(dFdu.xyz, 1.0); - /* // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) - float dist = 1.0f; samplePos.w = 1.0f; - vec2 centerOffset = ((input.texUV - 0.5f) * 2.0f); - vec4 px = mul( samplePos + vec4( dist, 0.0f, 0.0f, 0.0f ), matViewProj ); - vec4 py = mul( samplePos + vec4( 0.0f, dist, 0.0f, 0.0f ), matViewProj ); - vec4 pz = mul( samplePos + vec4( 0.0f, 0.0f, dist, 0.0f ), matViewProj ); - #ifdef INVERT_TEXTURE_V - centerOffset.y = -centerOffset.y; - #endif - px.xy = ((px.xy / px.w) - centerOffset) / scale; - py.xy = ((py.xy / py.w) - centerOffset) / scale; - pz.xy = ((pz.xy / pz.w) - centerOffset) / scale; - #ifdef INVERT_TEXTURE_V - px.y = -px.y; py.y = -py.y; pz.y = -pz.y; - #endif + + // Eval px, py, pz world positions of the basis centered on the world pos of the fragment + float dist = getCurvatureBasisScale(); + vec4 px = vec4(worldPos, 1.0) + vec4(dist, 0.0f, 0.0f, 0.0f); + vec4 py = vec4(worldPos, 1.0) + vec4(0.0f, dist, 0.0f, 0.0f); + vec4 pz = vec4(worldPos, 1.0) + vec4(0.0f, 0.0f, dist, 0.0f); + + // Project px, py pz to homogeneous clip space + mat4 viewProj = getProjection(stereoSide.x) * frameTransform._view; + px = viewProj * px; + py = viewProj * py; + pz = viewProj * pz; + + // then to normalized clip space + px.xy /= px.w; + py.xy /= py.w; + pz.xy /= pz.w; + + vec2 hclipPos = (nclipPos * 2.0 - 1.0); + + px.xy = (px.xy - hclipPos) / perspectiveScale; + py.xy = (py.xy - hclipPos) / perspectiveScale; + pz.xy = (pz.xy - hclipPos) / perspectiveScale; // Calculate dF/dx, dF/dy and dF/dz using chain rule vec4 dFdx = dFdu * px.x + dFdv * px.y; @@ -127,7 +173,7 @@ void main(void) { vec4 dFdz = dFdu * pz.x + dFdv * pz.y; // Calculate the mean curvature - float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333) * 100.0; - outFragColor = vec4( (meanCurvature + 1.0) * 0.5); - */ + float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333) * params.curvatureInfo.w; + //outFragColor = vec4(vec3(worldNormal + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); + outFragColor = vec4((vec3(dFdx.x, dFdy.y, dFdz.z) * params.curvatureInfo.w + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); } diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp new file mode 100644 index 0000000000..01fb610ff5 --- /dev/null +++ b/libraries/render/src/render/BlurTask.cpp @@ -0,0 +1,177 @@ +// +// BlurTask.cpp +// render/src/render +// +// Created by Sam Gateau on 6/7/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "BlurTask.h" + +#include +#include + +#include "blurGaussianV_frag.h" +#include "blurGaussianH_frag.h" + +using namespace render; + +enum BlurShaderBufferSlots { + BlurTask_ParamsSlot = 0, +}; +enum BlurShaderMapSlots { + BlurTask_SourceSlot = 0, +}; + +const float BLUR_NUM_SAMPLES = 7.0f; + +BlurParams::BlurParams() { + Params params; + _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Params), (const gpu::Byte*) ¶ms)); +} + +void BlurParams::setWidthHeight(int width, int height) { + auto resolutionInfo = _parametersBuffer.get().resolutionInfo; + if (width != resolutionInfo.x || height != resolutionInfo.y) { + _parametersBuffer.edit().resolutionInfo = glm::vec4((float) width, (float) height, 1.0f / (float) width, 1.0f / (float) height); + } +} + +void BlurParams::setFilterRadiusScale(float scale) { + auto filterInfo = _parametersBuffer.get().filterInfo; + if (scale != filterInfo.x) { + _parametersBuffer.edit().filterInfo.x = scale; + _parametersBuffer.edit().filterInfo.y = scale / BLUR_NUM_SAMPLES; + } +} + +BlurGaussian::BlurGaussian() { + _parameters = std::make_shared(); +} + +gpu::PipelinePointer BlurGaussian::getBlurVPipeline() { + if (!_blurVPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(blurGaussianV_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + _blurVPipeline = gpu::Pipeline::create(program, state); + } + + return _blurVPipeline; +} + +gpu::PipelinePointer BlurGaussian::getBlurHPipeline() { + if (!_blurHPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(blurGaussianH_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + _blurHPipeline = gpu::Pipeline::create(program, state); + } + + return _blurHPipeline; +} + +bool BlurGaussian::updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources) { + if (!sourceFramebuffer) { + return false; + } + + if (!_blurredFramebuffer) { + _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + + // attach depthStencil if present in source + if (sourceFramebuffer->hasDepthStencil()) { + _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + _blurredFramebuffer->setRenderBuffer(0, blurringTarget); + } + else { + // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so + if ((_blurredFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_blurredFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { + _blurredFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); + if (sourceFramebuffer->hasDepthStencil()) { + _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + } + } + + blurringResources.sourceTexture = sourceFramebuffer->getRenderBuffer(0); + blurringResources.blurringFramebuffer = _blurredFramebuffer; + blurringResources.blurringTexture = _blurredFramebuffer->getRenderBuffer(0); + blurringResources.finalFramebuffer = sourceFramebuffer; + + return true; +} + +void BlurGaussian::configure(const Config& config) { + _parameters->setFilterRadiusScale(config.filterScale); +} + + +void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + + RenderArgs* args = renderContext->args; + + + BlurringResources blurringResources; + if (!updateBlurringResources(sourceFramebuffer, blurringResources)) { + // early exit if no valid blurring resources + return; + } + + auto blurVPipeline = getBlurVPipeline(); + auto blurHPipeline = getBlurHPipeline(); + + _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setViewportTransform(args->_viewport); + + batch.setUniformBuffer(BlurTask_ParamsSlot, _parameters->_parametersBuffer); + + batch.setFramebuffer(blurringResources.blurringFramebuffer); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + + batch.setPipeline(blurVPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.sourceTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setFramebuffer(blurringResources.finalFramebuffer); + batch.setPipeline(blurHPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.blurringTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setResourceTexture(BlurTask_SourceSlot, nullptr); + batch.setUniformBuffer(BlurTask_ParamsSlot, nullptr); + }); +} + diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h new file mode 100644 index 0000000000..50031dcfea --- /dev/null +++ b/libraries/render/src/render/BlurTask.h @@ -0,0 +1,91 @@ +// +// BlurTask.h +// render/src/render +// +// Created by Sam Gateau on 6/7/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_render_BlurTask_h +#define hifi_render_BlurTask_h + +#include "Engine.h" + +namespace render { + + +class BlurParams { +public: + + void setWidthHeight(int width, int height); + + void setFilterRadiusScale(float scale); + + // Class describing the uniform buffer with all the parameters common to the blur shaders + class Params { + public: + // Resolution info (width, height, inverse of width, inverse of height) + glm::vec4 resolutionInfo{ 0.0f, 0.0f, 0.0f, 0.0f }; + + // Filter info (radius scale + glm::vec4 filterInfo{ 1.0f, 0.0f, 0.0f, 0.0f }; + + Params() {} + }; + gpu::BufferView _parametersBuffer; + + BlurParams(); +}; +using BlurParamsPointer = std::shared_ptr; + +class BlurGaussianConfig : public Job::Config { + Q_OBJECT + Q_PROPERTY(bool enabled MEMBER enabled NOTIFY dirty) // expose enabled flag + Q_PROPERTY(float filterScale MEMBER filterScale NOTIFY dirty) // expose enabled flag +public: + + float filterScale{ 2.0f }; +signals : + void dirty(); + +protected: +}; + + +class BlurGaussian { +public: + using Config = BlurGaussianConfig; + using JobModel = Job::ModelI; + + BlurGaussian(); + + void configure(const Config& config); + void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer); + +protected: + + BlurParamsPointer _parameters; + + gpu::PipelinePointer _blurVPipeline; + gpu::PipelinePointer _blurHPipeline; + + gpu::PipelinePointer getBlurVPipeline(); + gpu::PipelinePointer getBlurHPipeline(); + + gpu::FramebufferPointer _blurredFramebuffer; + + struct BlurringResources { + gpu::TexturePointer sourceTexture; + gpu::FramebufferPointer blurringFramebuffer; + gpu::TexturePointer blurringTexture; + gpu::FramebufferPointer finalFramebuffer; + }; + bool updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources); +}; + +} + +#endif // hifi_render_DrawTask_h diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh new file mode 100644 index 0000000000..6073e641e9 --- /dev/null +++ b/libraries/render/src/render/BlurTask.slh @@ -0,0 +1,65 @@ +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/7/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@func declareBlurUniforms()@> + +#define NUM_TAPS 7 +#define NUM_TAPS_OFFSET 3.0f + +float uniformFilterWidth = 0.05f; + +const float gaussianDistributionCurve[NUM_TAPS] = float[]( + 0.383f, 0.006f, 0.061f, 0.242f, 0.242f, 0.061f, 0.006f +); +const float gaussianDistributionOffset[NUM_TAPS] = float[]( + 0.0f, -3.0f, -2.0f, -1.0f, 1.0f, 2.0f, 3.0f +); + +struct BlurParameters { + vec4 resolutionInfo; + vec4 filterInfo; +}; + +uniform blurParamsBuffer { + BlurParameters parameters; +}; + +vec2 getViewportInvWidthHeight() { + return parameters.resolutionInfo.zw; +} + +<@endfunc@> + + +<@func declareBlurGaussian()@> + +<$declareBlurUniforms()$> + +uniform sampler2D sourceMap; + +vec4 pixelShaderGaussian(vec2 texcoord, vec2 direction, vec2 pixelStep) { + + vec4 sampleCenter = texture(sourceMap, texcoord); + + vec2 finalStep = parameters.filterInfo.x * direction * pixelStep; + vec4 srcBlurred = vec4(0.0); + + for(int i = 0; i < NUM_TAPS; i++) { + // Fetch color and depth for current sample. + vec2 sampleCoord = texcoord + (gaussianDistributionOffset[i] * finalStep); + vec4 srcSample = texture(sourceMap, sampleCoord); + // Accumulate. + srcBlurred += gaussianDistributionCurve[i] * srcSample; + } + + return srcBlurred; +} + +<@endfunc@> + diff --git a/libraries/render/src/render/blurGaussianH.slf b/libraries/render/src/render/blurGaussianH.slf new file mode 100644 index 0000000000..02cc73fe13 --- /dev/null +++ b/libraries/render/src/render/blurGaussianH.slf @@ -0,0 +1,23 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/7/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include BlurTask.slh@> +<$declareBlurGaussian()$> + + +in vec2 varTexCoord0; + +out vec4 outFragColor; + +void main(void) { + outFragColor = pixelShaderGaussian(varTexCoord0, vec2(1.0, 0.0), getViewportInvWidthHeight()); +} + diff --git a/libraries/render/src/render/blurGaussianV.slf b/libraries/render/src/render/blurGaussianV.slf new file mode 100644 index 0000000000..99beab6275 --- /dev/null +++ b/libraries/render/src/render/blurGaussianV.slf @@ -0,0 +1,22 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/7/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +<@include BlurTask.slh@> +<$declareBlurGaussian()$> + + +in vec2 varTexCoord0; + +out vec4 outFragColor; + +void main(void) { + outFragColor = pixelShaderGaussian(varTexCoord0, vec2(0.0, 1.0), getViewportInvWidthHeight()); +} + diff --git a/scripts/developer/utilities/render/debugSurfaceGeometryPass.js b/scripts/developer/utilities/render/debugSurfaceGeometryPass.js new file mode 100644 index 0000000000..fed9ce4ef3 --- /dev/null +++ b/scripts/developer/utilities/render/debugSurfaceGeometryPass.js @@ -0,0 +1,20 @@ +// +// debugSurfaceGeometryPass.js +// +// Created by Sam Gateau on 6/6/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +// Set up the qml ui +var qml = Script.resolvePath('surfaceGeometryPass.qml'); +var window = new OverlayWindow({ + title: 'Surface Geometry Pass', + source: qml, + width: 400, height: 400, +}); +window.setPosition(250, 500); +window.closed.connect(function() { Script.stop(); }); + diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index e8122db8c9..a7f164e78b 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -22,7 +22,15 @@ Column { debug.config.mode = mode; } - Label { text: qsTr("Debug Buffer") } + function setLayout(layout) { + debug.config.size = { x: -1, y: -1, z: 1, w: 1 }; + } + + Button { + text: "Fullscreen" + onClicked: { debug.setLayout(1); } + } + ExclusiveGroup { id: bufferGroup } Repeater { model: [ diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml new file mode 100644 index 0000000000..ab3e64a33a --- /dev/null +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -0,0 +1,33 @@ +// +// surfaceGeometryPass.qml +// +// Created by Sam Gateau on 6/6/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "configSlider" + +Column { + spacing: 8 + Column { + id: surfaceGeometry + + Column{ + Repeater { + model: [ "Depth Threshold:depthThreshold:1.0", "Basis Scale:basisScale:1.0", "Curvature Scale:curvatureScale:200.0" ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: false + config: Render.getConfig("SurfaceGeometry") + property: modelData.split(":")[1] + max: modelData.split(":")[2] + min: 0.0 + } + } + } + } +} From f9d6a7ba8d57cccb8e0feb16ecc9f4c344dbcdb5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 8 Jun 2016 14:02:27 +1200 Subject: [PATCH 0438/1237] Fix image2d overlay color property --- interface/resources/qml/hifi/overlays/ImageOverlay.qml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/interface/resources/qml/hifi/overlays/ImageOverlay.qml b/interface/resources/qml/hifi/overlays/ImageOverlay.qml index b10b66f07c..b509f0ce3a 100644 --- a/interface/resources/qml/hifi/overlays/ImageOverlay.qml +++ b/interface/resources/qml/hifi/overlays/ImageOverlay.qml @@ -1,5 +1,6 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 +import QtGraphicalEffects 1.0 import "." @@ -44,6 +45,12 @@ Overlay { } } + ColorOverlay { + id: color + anchors.fill: image + source: image + } + function updateSubImage(subImage) { var keys = Object.keys(subImage); for (var i = 0; i < keys.length; ++i) { @@ -70,6 +77,7 @@ Overlay { case "alpha": root.opacity = value; break; case "imageURL": image.source = value; break; case "subImage": updateSubImage(value); break; + case "color": color.color = Qt.rgba(value.red / 255, value.green / 255, value.blue / 255, root.opacity); break; default: console.log("OVERLAY Unhandled image property " + key); } } From eab611acc072d443ab7fccb2dfaed6b4b370046c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 18 May 2016 21:46:30 -0700 Subject: [PATCH 0439/1237] Enable Steam VR text input --- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 41 ++++++++++++++++++++ libraries/gl/src/gl/OffscreenQmlSurface.h | 8 +++- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 3 ++ plugins/openvr/src/ViveControllerManager.cpp | 34 +++++++++++++++- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 388ca26482..fa9cb62b7c 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -396,6 +396,8 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _renderer->_renderControl->_renderWindow = _proxyWindow; + connect(_renderer->_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged); + // Create a QML engine. _qmlEngine = new QQmlEngine; if (!_qmlEngine->incubationController()) { @@ -742,3 +744,42 @@ QVariant OffscreenQmlSurface::returnFromUiThread(std::function funct return function(); } + +void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) { + if (!object) { + setFocusText(false); + return; + } + + QVariant result; +#if 1 + auto invokeResult = QMetaObject::invokeMethod(object, "inputMethodQuery", Q_RETURN_ARG(QVariant, result), + Q_ARG(Qt::InputMethodQuery, Qt::ImEnabled), + Q_ARG(QVariant, QVariant())); +#else + + //static const char* INPUT_METHOD_QUERY_METHOD_NAME = "inputMethodQuery(Qt::InputMethodQuery, QVariant)"; + static const char* INPUT_METHOD_QUERY_METHOD_NAME = "inputMethodQuery"; + auto meta = object->metaObject(); + qDebug() << "new focus " << object; + auto index = meta->indexOfMethod(INPUT_METHOD_QUERY_METHOD_NAME); + if (index < 0 || index >= meta->methodCount()) { + setFocusText(false); + return; + } + + auto method = meta->method(index); + auto invokeResult = method.invoke(object, + Q_RETURN_ARG(QVariant, result), + Q_ARG(Qt::InputMethodQuery, Qt::ImEnabled), + Q_ARG(QVariant, QVariant())); +#endif + setFocusText(invokeResult && result.toBool()); +} + +void OffscreenQmlSurface::setFocusText(bool newFocusText) { + if (newFocusText != _focusText) { + _focusText = newFocusText; + emit focusTextChanged(_focusText); + } +} diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 22a1b99fe6..1ce7276877 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -30,7 +30,7 @@ class OffscreenQmlRenderThread; class OffscreenQmlSurface : public QObject { Q_OBJECT - + Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) public: OffscreenQmlSurface(); virtual ~OffscreenQmlSurface(); @@ -55,6 +55,7 @@ public: _mouseTranslator = mouseTranslator; } + bool isFocusText() const { return _focusText; } void pause(); void resume(); bool isPaused() const; @@ -70,6 +71,8 @@ public: signals: void textureUpdated(unsigned int texture); + void focusObjectChanged(QObject* newFocus); + void focusTextChanged(bool focusText); public slots: void requestUpdate(); @@ -78,6 +81,7 @@ public slots: protected: bool filterEnabled(QObject* originalDestination, QEvent* event) const; + void setFocusText(bool newFocusText); private: QObject* finishQmlLoad(std::function f); @@ -85,6 +89,7 @@ private: private slots: void updateQuick(); + void onFocusObjectChanged(QObject* newFocus); private: friend class OffscreenQmlRenderThread; @@ -97,6 +102,7 @@ private: bool _render{ false }; bool _polish{ true }; bool _paused{ true }; + bool _focusText { false }; uint8_t _maxFps{ 60 }; MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p.toPoint(); } }; QWindow* _proxyWindow { nullptr }; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index fe406cc29a..fbade9fd68 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -36,12 +36,14 @@ vec3 _trackedDeviceLinearVelocities[vr::k_unMaxTrackedDeviceCount]; vec3 _trackedDeviceAngularVelocities[vr::k_unMaxTrackedDeviceCount]; static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; +bool _openVrDisplayActive { false }; bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } bool OpenVrDisplayPlugin::internalActivate() { + _openVrDisplayActive = true; _container->setIsOptionChecked(StandingHMDSensorMode, true); if (!_system) { @@ -94,6 +96,7 @@ bool OpenVrDisplayPlugin::internalActivate() { void OpenVrDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); + _openVrDisplayActive = false; _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { // Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 739d3cde10..8d6c661ae6 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -12,6 +12,7 @@ #include "ViveControllerManager.h" #include +#include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include @@ -55,6 +57,9 @@ bool ViveControllerManager::isSupported() const { return openVrSupported(); } +QMetaObject::Connection _focusConnection; +extern bool _openVrDisplayActive; + bool ViveControllerManager::activate() { InputPlugin::activate(); @@ -67,7 +72,20 @@ bool ViveControllerManager::activate() { _system = acquireOpenVrSystem(); } Q_ASSERT(_system); - + auto offscreenUi = DependencyManager::get(); + _focusConnection = connect(offscreenUi.data(), &OffscreenUi::focusTextChanged, [this](bool focusText) { + if (_openVrDisplayActive) { + auto overlay = vr::VROverlay(); + if (overlay) { + if (focusText) { + //virtual EVROverlayError ShowKeyboard( eInputMode, EGamepadTextInputLineMode eLineInputMode, const char *pchDescription, uint32_t unCharMax, const char *pchExistingText, bool bUseMinimalMode, uint64_t uUserValue) = 0; + overlay->ShowKeyboard(vr::EGamepadTextInputMode::k_EGamepadTextInputModeNormal, vr::k_EGamepadTextInputLineModeSingleLine, "Test", 1024, "", false, 0); + } else { + overlay->HideKeyboard(); + } + } + } + }); // OpenVR provides 3d mesh representations of the controllers // Disabled controller rendering code /* @@ -132,6 +150,8 @@ bool ViveControllerManager::activate() { void ViveControllerManager::deactivate() { InputPlugin::deactivate(); + disconnect(_focusConnection); + _container->removeMenuItem(MENU_NAME, RENDER_CONTROLLERS); _container->removeMenu(MENU_PATH); @@ -220,6 +240,18 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu return; } + vr::VREvent_t vrEvent; + static char textArray[8192]; + while (vr::VRSystem()->PollNextEvent(&vrEvent, sizeof(vrEvent))) { + if (vrEvent.eventType == vr::VREvent_KeyboardDone) { + auto chars = vr::VROverlay()->GetKeyboardText(textArray, 8192); + QInputMethodEvent* event = new QInputMethodEvent(); + event->setCommitString(QString(QByteArray(textArray, chars)), 0, 0); + auto focusObject = DependencyManager::get()->getWindow()->focusObject(); + qApp->postEvent(focusObject, event); + } + } + // because update mutates the internal state we need to lock userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); From c2aa9e7f61f71fd3fd53acf3f9ecdec94d34ce06 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 6 Jun 2016 15:14:10 -0700 Subject: [PATCH 0440/1237] Update SteamVR keyboard behavior --- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 27 +------ plugins/openvr/src/OpenVrHelpers.cpp | 85 ++++++++++++++++++++ plugins/openvr/src/OpenVrHelpers.h | 3 + plugins/openvr/src/ViveControllerManager.cpp | 37 +-------- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index fa9cb62b7c..14518ac37a 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -751,30 +751,9 @@ void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) { return; } - QVariant result; -#if 1 - auto invokeResult = QMetaObject::invokeMethod(object, "inputMethodQuery", Q_RETURN_ARG(QVariant, result), - Q_ARG(Qt::InputMethodQuery, Qt::ImEnabled), - Q_ARG(QVariant, QVariant())); -#else - - //static const char* INPUT_METHOD_QUERY_METHOD_NAME = "inputMethodQuery(Qt::InputMethodQuery, QVariant)"; - static const char* INPUT_METHOD_QUERY_METHOD_NAME = "inputMethodQuery"; - auto meta = object->metaObject(); - qDebug() << "new focus " << object; - auto index = meta->indexOfMethod(INPUT_METHOD_QUERY_METHOD_NAME); - if (index < 0 || index >= meta->methodCount()) { - setFocusText(false); - return; - } - - auto method = meta->method(index); - auto invokeResult = method.invoke(object, - Q_RETURN_ARG(QVariant, result), - Q_ARG(Qt::InputMethodQuery, Qt::ImEnabled), - Q_ARG(QVariant, QVariant())); -#endif - setFocusText(invokeResult && result.toBool()); + QInputMethodQueryEvent query(Qt::ImEnabled); + qApp->sendEvent(object, &query); + setFocusText(query.value(Qt::ImEnabled).toBool()); } void OffscreenQmlSurface::setFocusText(bool newFocusText) { diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 155bc9f079..b13a60d388 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -14,9 +14,13 @@ #include #include #include +#include +#include #include +#include + Q_DECLARE_LOGGING_CATEGORY(displayplugins) Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") @@ -90,6 +94,82 @@ void releaseOpenVrSystem() { } } +static char textArray[8192]; + +static QMetaObject::Connection _focusConnection, _focusTextConnection; +extern bool _openVrDisplayActive; +static vr::IVROverlay* _overlay { nullptr }; +static QObject* _focusObject { nullptr }; +static QString _existingText; +static Qt::InputMethodHints _currentHints; + +void showOpenVrKeyboard(bool show = true) { + if (_overlay) { + if (show) { + auto offscreenUi = DependencyManager::get(); + _focusObject = offscreenUi->getWindow()->focusObject(); + + QInputMethodQueryEvent query(Qt::ImQueryInput | Qt::ImHints); + qApp->sendEvent(_focusObject, &query); + _currentHints = Qt::InputMethodHints(query.value(Qt::ImHints).toUInt()); + vr::EGamepadTextInputMode inputMode = vr::k_EGamepadTextInputModeNormal; + if (_currentHints & Qt::ImhHiddenText) { + inputMode = vr::k_EGamepadTextInputModePassword; + } + vr::EGamepadTextInputLineMode lineMode = vr::k_EGamepadTextInputLineModeSingleLine; + if (_currentHints & Qt::ImhMultiLine) { + lineMode = vr::k_EGamepadTextInputLineModeMultipleLines; + } + _existingText = query.value(Qt::ImSurroundingText).toString(); + _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, _existingText.toLocal8Bit().toStdString().c_str(), false, (uint64_t)(void*)_focusObject); + } else { + _focusObject = nullptr; + _overlay->HideKeyboard(); + } + } +} + +void finishOpenVrKeyboardInput() { + auto offscreenUi = DependencyManager::get(); + auto chars = _overlay->GetKeyboardText(textArray, 8192); + auto newText = QString(QByteArray(textArray, chars)); + // TODO modify the new text to match the possible input hints: + // ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly + // ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly + QInputMethodEvent event(_existingText, QList()); + event.setCommitString(newText, 0, _existingText.size()); + qApp->sendEvent(_focusObject, &event); + // Simulate an enter press on the top level window to trigger the action + if (0 == (_currentHints & Qt::ImhMultiLine)) { + qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n"))); + qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyRelease, Qt::Key_Return, Qt::KeyboardModifiers())); + } +} + +void enableOpenVrKeyboard() { + auto offscreenUi = DependencyManager::get(); + _overlay = vr::VROverlay(); + + _focusConnection = QObject::connect(offscreenUi->getWindow(), &QQuickWindow::focusObjectChanged, [](QObject* object) { + if (object != _focusObject && _overlay) { + showOpenVrKeyboard(false); + } + }); + + _focusTextConnection = QObject::connect(offscreenUi.data(), &OffscreenUi::focusTextChanged, [](bool focusText) { + if (_openVrDisplayActive) { + showOpenVrKeyboard(focusText); + } + }); +} + + +void disableOpenVrKeyboard() { + QObject::disconnect(_focusTextConnection); + QObject::disconnect(_focusConnection); +} + + void handleOpenVrEvents() { if (!activeHmd) { return; @@ -107,6 +187,10 @@ void handleOpenVrEvents() { activeHmd->AcknowledgeQuit_Exiting(); break; + case vr::VREvent_KeyboardDone: + finishOpenVrKeyboardInput(); + break; + default: break; } @@ -114,3 +198,4 @@ void handleOpenVrEvents() { } } + diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 1e5914844c..41e7dcb27d 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -18,6 +18,9 @@ vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); void handleOpenVrEvents(); bool openVrQuitRequested(); +void enableOpenVrKeyboard(); +void disableOpenVrKeyboard(); + template void openvr_for_each_eye(F f) { diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 8d6c661ae6..6d3ce46e82 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -11,9 +11,6 @@ #include "ViveControllerManager.h" -#include -#include - #include #include #include @@ -57,9 +54,6 @@ bool ViveControllerManager::isSupported() const { return openVrSupported(); } -QMetaObject::Connection _focusConnection; -extern bool _openVrDisplayActive; - bool ViveControllerManager::activate() { InputPlugin::activate(); @@ -72,20 +66,9 @@ bool ViveControllerManager::activate() { _system = acquireOpenVrSystem(); } Q_ASSERT(_system); - auto offscreenUi = DependencyManager::get(); - _focusConnection = connect(offscreenUi.data(), &OffscreenUi::focusTextChanged, [this](bool focusText) { - if (_openVrDisplayActive) { - auto overlay = vr::VROverlay(); - if (overlay) { - if (focusText) { - //virtual EVROverlayError ShowKeyboard( eInputMode, EGamepadTextInputLineMode eLineInputMode, const char *pchDescription, uint32_t unCharMax, const char *pchExistingText, bool bUseMinimalMode, uint64_t uUserValue) = 0; - overlay->ShowKeyboard(vr::EGamepadTextInputMode::k_EGamepadTextInputModeNormal, vr::k_EGamepadTextInputLineModeSingleLine, "Test", 1024, "", false, 0); - } else { - overlay->HideKeyboard(); - } - } - } - }); + + enableOpenVrKeyboard(); + // OpenVR provides 3d mesh representations of the controllers // Disabled controller rendering code /* @@ -150,7 +133,7 @@ bool ViveControllerManager::activate() { void ViveControllerManager::deactivate() { InputPlugin::deactivate(); - disconnect(_focusConnection); + disableOpenVrKeyboard(); _container->removeMenuItem(MENU_NAME, RENDER_CONTROLLERS); _container->removeMenu(MENU_PATH); @@ -240,18 +223,6 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu return; } - vr::VREvent_t vrEvent; - static char textArray[8192]; - while (vr::VRSystem()->PollNextEvent(&vrEvent, sizeof(vrEvent))) { - if (vrEvent.eventType == vr::VREvent_KeyboardDone) { - auto chars = vr::VROverlay()->GetKeyboardText(textArray, 8192); - QInputMethodEvent* event = new QInputMethodEvent(); - event->setCommitString(QString(QByteArray(textArray, chars)), 0, 0); - auto focusObject = DependencyManager::get()->getWindow()->focusObject(); - qApp->postEvent(focusObject, event); - } - } - // because update mutates the internal state we need to lock userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); From 79c68b2ecb203cb09453e53179eb22e7689d00df Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 7 Jun 2016 21:49:29 -0700 Subject: [PATCH 0441/1237] Working on positioning of the keyboard --- plugins/openvr/src/OpenVrHelpers.cpp | 11 ++++++++++- plugins/openvr/src/OpenVrHelpers.h | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index b13a60d388..6533371db8 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -102,6 +102,7 @@ static vr::IVROverlay* _overlay { nullptr }; static QObject* _focusObject { nullptr }; static QString _existingText; static Qt::InputMethodHints _currentHints; +extern vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; void showOpenVrKeyboard(bool show = true) { if (_overlay) { @@ -121,7 +122,15 @@ void showOpenVrKeyboard(bool show = true) { lineMode = vr::k_EGamepadTextInputLineModeMultipleLines; } _existingText = query.value(Qt::ImSurroundingText).toString(); - _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, _existingText.toLocal8Bit().toStdString().c_str(), false, (uint64_t)(void*)_focusObject); + + auto showKeyboardResult = _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, + _existingText.toLocal8Bit().toStdString().c_str(), false, 0); + + mat4 headPose = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); + keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); + auto keyboardTransformVr = toOpenVr(keyboardTransform); + _overlay->SetKeyboardTransformAbsolute(vr::ETrackingUniverseOrigin::TrackingUniverseStanding, &keyboardTransformVr); } else { _focusObject = nullptr; _overlay->HideKeyboard(); diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 41e7dcb27d..426178cd65 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -44,3 +44,13 @@ inline mat4 toGlm(const vr::HmdMatrix34_t& m) { m.m[0][3], m.m[1][3], m.m[2][3], 1.0f); return result; } + +inline vr::HmdMatrix34_t toOpenVr(const mat4& m) { + vr::HmdMatrix34_t result; + for (uint8_t i = 0; i < 3; ++i) { + for (uint8_t j = 0; j < 4; ++j) { + result.m[i][j] = m[j][i]; + } + } + return result; +} From 8085266c1d6e4b4020e270351d3ce08745ed47a9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 10:36:44 -0700 Subject: [PATCH 0442/1237] cause names in permissions tables to be alpha-sorted --- .../src/DomainServerSettingsManager.cpp | 37 ++++++++++++++++++- .../src/DomainServerSettingsManager.h | 1 + 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 64fce73191..fe569d54b9 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include #include @@ -498,7 +500,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true); rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object(); - connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json"); } @@ -674,6 +675,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV // TODO: we still need to recurse here with the description in case values in the array have special types settingMap[key] = newValue.toArray().toVariantList(); } + + sortPermissions(); } QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) { @@ -772,10 +775,42 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ // re-merge the user and master configs after a settings change _configMap.mergeMasterAndUserConfigs(); + return needRestart; } +// Compare two members of a permissions list +bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) { + if (!v1.canConvert(QMetaType::QVariantMap) || + !v2.canConvert(QMetaType::QVariantMap)) { + return v1.toString() < v2.toString(); + } + QVariantMap m1 = v1.toMap(); + QVariantMap m2 = v2.toMap(); + + if (!m1.contains("permissions_id") || + !m2.contains("permissions_id")) { + return v1.toString() < v2.toString(); + } + return m1["permissions_id"].toString() < m2["permissions_id"].toString(); +} + +void DomainServerSettingsManager::sortPermissions() { + // sort the permission-names + QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH); + if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) { + QList* standardPermissionsList = reinterpret_cast(standardPermissions); + std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan); + } + QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + if (permissions && permissions->canConvert(QMetaType::QVariantList)) { + QList* permissionsList = reinterpret_cast(permissions); + std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); + } +} + void DomainServerSettingsManager::persistToFile() { + sortPermissions(); // make sure we have the dir the settings file is supposed to live in QFileInfo settingsFileInfo(_configMap.getUserConfigFilename()); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index f6a5ef6f89..446e9a2eed 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -63,6 +63,7 @@ private: void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName); + void sortPermissions(); void persistToFile(); double _descriptionVersion; From 40778d7f299777d5c395e0e742532f214ee5c799 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 8 Jun 2016 10:41:19 -0700 Subject: [PATCH 0443/1237] Allow env disabling of the keyboard. Show keyboard after short delay to avoid flickers --- plugins/openvr/src/OpenVrHelpers.cpp | 64 ++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 6533371db8..1ff1c65ef8 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -99,19 +99,33 @@ static char textArray[8192]; static QMetaObject::Connection _focusConnection, _focusTextConnection; extern bool _openVrDisplayActive; static vr::IVROverlay* _overlay { nullptr }; -static QObject* _focusObject { nullptr }; +static QObject* _keyboardFocusObject { nullptr }; static QString _existingText; static Qt::InputMethodHints _currentHints; extern vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; +static bool _keyboardShown { false }; +static const uint32_t SHOW_KEYBOARD_DELAY_MS = 100; void showOpenVrKeyboard(bool show = true) { - if (_overlay) { - if (show) { - auto offscreenUi = DependencyManager::get(); - _focusObject = offscreenUi->getWindow()->focusObject(); + if (!_overlay) { + return; + } - QInputMethodQueryEvent query(Qt::ImQueryInput | Qt::ImHints); - qApp->sendEvent(_focusObject, &query); + if (show) { + // To avoid flickering the keyboard when a text element is only briefly selected, + // show the keyboard asynchrnously after a very short delay, but only after we check + // that the current focus object is still one that is text enabled + QTimer::singleShot(SHOW_KEYBOARD_DELAY_MS, [] { + auto offscreenUi = DependencyManager::get(); + auto currentFocus = offscreenUi->getWindow()->focusObject(); + QInputMethodQueryEvent query(Qt::ImEnabled | Qt::ImQueryInput | Qt::ImHints); + qApp->sendEvent(currentFocus, &query); + // Current focus isn't text enabled, bail early. + if (!query.value(Qt::ImEnabled).toBool()) { + return; + } + // We're going to show the keyboard now... + _keyboardFocusObject = currentFocus; _currentHints = Qt::InputMethodHints(query.value(Qt::ImHints).toUInt()); vr::EGamepadTextInputMode inputMode = vr::k_EGamepadTextInputModeNormal; if (_currentHints & Qt::ImhHiddenText) { @@ -123,17 +137,24 @@ void showOpenVrKeyboard(bool show = true) { } _existingText = query.value(Qt::ImSurroundingText).toString(); - auto showKeyboardResult = _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, + auto showKeyboardResult = _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, _existingText.toLocal8Bit().toStdString().c_str(), false, 0); - mat4 headPose = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); - mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); - keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); - auto keyboardTransformVr = toOpenVr(keyboardTransform); - _overlay->SetKeyboardTransformAbsolute(vr::ETrackingUniverseOrigin::TrackingUniverseStanding, &keyboardTransformVr); - } else { - _focusObject = nullptr; + if (vr::VROverlayError_None == showKeyboardResult) { + _keyboardShown = true; + // Try to position the keyboard slightly below where the user is looking. + mat4 headPose = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); + keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); + auto keyboardTransformVr = toOpenVr(keyboardTransform); + _overlay->SetKeyboardTransformAbsolute(vr::ETrackingUniverseOrigin::TrackingUniverseStanding, &keyboardTransformVr); + } + }); + } else { + _keyboardFocusObject = nullptr; + if (_keyboardShown) { _overlay->HideKeyboard(); + _keyboardShown = false; } } } @@ -147,7 +168,7 @@ void finishOpenVrKeyboardInput() { // ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly QInputMethodEvent event(_existingText, QList()); event.setCommitString(newText, 0, _existingText.size()); - qApp->sendEvent(_focusObject, &event); + qApp->sendEvent(_keyboardFocusObject, &event); // Simulate an enter press on the top level window to trigger the action if (0 == (_currentHints & Qt::ImhMultiLine)) { qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n"))); @@ -155,12 +176,18 @@ void finishOpenVrKeyboardInput() { } } +static const QString DEBUG_FLAG("HIFI_DISABLE_STEAM_VR_KEYBOARD"); +bool disableSteamVrKeyboard = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + void enableOpenVrKeyboard() { + if (disableSteamVrKeyboard) { + return; + } auto offscreenUi = DependencyManager::get(); _overlay = vr::VROverlay(); _focusConnection = QObject::connect(offscreenUi->getWindow(), &QQuickWindow::focusObjectChanged, [](QObject* object) { - if (object != _focusObject && _overlay) { + if (object != _keyboardFocusObject) { showOpenVrKeyboard(false); } }); @@ -174,6 +201,9 @@ void enableOpenVrKeyboard() { void disableOpenVrKeyboard() { + if (disableSteamVrKeyboard) { + return; + } QObject::disconnect(_focusTextConnection); QObject::disconnect(_focusConnection); } From f8353fb082c875ec56cb094853cba9730a005610 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 8 Jun 2016 11:11:18 -0700 Subject: [PATCH 0444/1237] New button content and initial positions. --- .../assets/images/tools/hmd-switch-01.svg | 198 ++++++++++-------- scripts/system/edit.js | 2 +- scripts/system/examples.js | 4 +- scripts/system/goto.js | 2 +- scripts/system/hmd.js | 2 +- 5 files changed, 115 insertions(+), 93 deletions(-) diff --git a/scripts/system/assets/images/tools/hmd-switch-01.svg b/scripts/system/assets/images/tools/hmd-switch-01.svg index 15ecb02b6b..31389d355c 100644 --- a/scripts/system/assets/images/tools/hmd-switch-01.svg +++ b/scripts/system/assets/images/tools/hmd-switch-01.svg @@ -4,99 +4,121 @@ viewBox="0 0 50 150" style="enable-background:new 0 0 50 150;" xml:space="preserve"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + diff --git a/scripts/system/edit.js b/scripts/system/edit.js index afbc679ec4..38d596f83e 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -181,7 +181,7 @@ var toolBar = (function() { function initialize() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.edit.toolbar", function(windowDimensions, toolbar) { return { - x: windowDimensions.x / 2, + x: (windowDimensions.x / 2) + (Tool.IMAGE_WIDTH * 2), y: windowDimensions.y }; }, { diff --git a/scripts/system/examples.js b/scripts/system/examples.js index 9d33e473af..6f4268182c 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -60,7 +60,7 @@ var toolBar = (function() { function initialize() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.examples.toolbar", function(windowDimensions, toolbar) { return { - x: windowDimensions.x / 2, + x: (windowDimensions.x / 2) + (Tool.IMAGE_WIDTH * 2), y: windowDimensions.y }; }, { @@ -135,4 +135,4 @@ var toolBar = (function() { }()); Controller.mousePressEvent.connect(toolBar.mousePressEvent) -Script.scriptEnding.connect(toolBar.cleanup); \ No newline at end of file +Script.scriptEnding.connect(toolBar.cleanup); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 75d9829905..00b5e912c0 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -13,7 +13,7 @@ Script.include("libraries/toolBars.js"); function initialPosition(windowDimensions, toolbar) { return { - x: windowDimensions.x / 2 - Tool.IMAGE_WIDTH, + x: (windowDimensions.x / 2) - (Tool.IMAGE_WIDTH * 1), y: windowDimensions.y }; } diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 277af68315..8b91e45676 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -22,7 +22,7 @@ var desktopMenuItemName = "Desktop"; function initialPosition(windowDimensions, toolbar) { return { - x: windowDimensions.x / 2 + (2 * Tool.IMAGE_WIDTH), + x: (windowDimensions.x / 2) - (Tool.IMAGE_WIDTH * 2.5), y: windowDimensions.y }; } From 1b4b38c85bb3d6aaa410ad29e834a7b40ad50310 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 11:28:49 -0700 Subject: [PATCH 0445/1237] when parent is an avatar, set velocity to zero (still incorrect, but avoids server-side problems) rather than the world-frame velocity --- libraries/shared/src/SpatiallyNestable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 2a3cb4af47..ddc82e2169 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -422,7 +422,7 @@ void SpatiallyNestable::setVelocity(const glm::vec3& velocity, bool& success) { // causes EntityItem::stepKinematicMotion to have an effect on the equipped entity, // which causes it to drift from the hand. if (hasAncestorOfType(NestableType::Avatar)) { - _velocity = velocity; + _velocity = glm::vec3(0.0f); } else { // TODO: take parent angularVelocity into account. _velocity = glm::inverse(parentTransform.getRotation()) * (velocity - parentVelocity); From 5cbaf05e45cb708402adc1bf1a7c44c0f5cadc7e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 11:55:17 -0700 Subject: [PATCH 0446/1237] also zero angular velocity for child of avatar --- libraries/shared/src/SpatiallyNestable.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index ddc82e2169..02a8d60fba 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -479,7 +479,12 @@ void SpatiallyNestable::setAngularVelocity(const glm::vec3& angularVelocity, boo glm::vec3 parentAngularVelocity = getParentAngularVelocity(success); Transform parentTransform = getParentTransform(success); _angularVelocityLock.withWriteLock([&] { - _angularVelocity = glm::inverse(parentTransform.getRotation()) * (angularVelocity - parentAngularVelocity); + if (hasAncestorOfType(NestableType::Avatar)) { + // TODO: this is done to keep entity-server from doing extrapolation, and isn't right. + _angularVelocity = glm::vec3(0.0f); + } else { + _angularVelocity = glm::inverse(parentTransform.getRotation()) * (angularVelocity - parentAngularVelocity); + } }); } From 201d7a7d3b616d26e0f5b4a6f23c2a3ad5ef520d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 12:11:08 -0700 Subject: [PATCH 0447/1237] when parent is an avatar, set velocity to zero (still incorrect, but avoids server-side problems) rather than the world-frame velocity --- libraries/shared/src/SpatiallyNestable.cpp | 32 ++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 02a8d60fba..6872b1d197 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -415,12 +415,8 @@ void SpatiallyNestable::setVelocity(const glm::vec3& velocity, bool& success) { glm::vec3 parentVelocity = getParentVelocity(success); Transform parentTransform = getParentTransform(success); _velocityLock.withWriteLock([&] { - // HACK: until we are treating _velocity the same way we treat _position (meaning, - // _velocity is a vs parent value and any request for a world-frame velocity must - // be computed), do this to avoid equipped (parenting-grabbed) things from drifting. - // turning a zero velocity into a non-zero _velocity (because the avatar is moving) - // causes EntityItem::stepKinematicMotion to have an effect on the equipped entity, - // which causes it to drift from the hand. + // HACK: The entity-server doesn't know where avatars are. In order to avoid having the server + // try to do simple extrapolation, set relative velocities to zero if this is a child of an avatar. if (hasAncestorOfType(NestableType::Avatar)) { _velocity = glm::vec3(0.0f); } else { @@ -898,13 +894,21 @@ void SpatiallyNestable::setLocalTransformAndVelocities( _transformLock.withWriteLock([&] { _transform = localTransform; }); - // linear velocity - _velocityLock.withWriteLock([&] { - _velocity = localVelocity; - }); - // angular velocity - _angularVelocityLock.withWriteLock([&] { - _angularVelocity = localAngularVelocity; - }); + // linear and angular velocity + if (hasAncestorOfType(NestableType::Avatar)) { + _velocityLock.withWriteLock([&] { + _velocity = glm::vec3(0.0f); + }); + _angularVelocityLock.withWriteLock([&] { + _angularVelocity = glm::vec3(0.0f); + }); + } else { + _velocityLock.withWriteLock([&] { + _velocity = localVelocity; + }); + _angularVelocityLock.withWriteLock([&] { + _angularVelocity = localAngularVelocity; + }); + } locationChanged(false); } From a1766539f402a249758f87640163e6ea2edded26 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 12:17:07 -0700 Subject: [PATCH 0448/1237] don't do simple simulation on children of avatars --- libraries/entities/src/EntitySimulation.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index 893ae83cc5..b99ab76b7a 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -261,7 +261,11 @@ void EntitySimulation::moveSimpleKinematics(const quint64& now) { SetOfEntities::iterator itemItr = _simpleKinematicEntities.begin(); while (itemItr != _simpleKinematicEntities.end()) { EntityItemPointer entity = *itemItr; - if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo()) { + if (entity->isMovingRelativeToParent() && + !entity->getPhysicsInfo() && + // The entity-server doesn't know where avatars are, so don't attempt to do simple extrapolation for + // children of avatars. + !entity->hasAncestorOfType(NestableType::Avatar)) { entity->simulate(now); _entitiesToSort.insert(entity); ++itemItr; From 65e2cdbbf366a75b5cc70f36014e314392585947 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 8 Jun 2016 13:44:53 -0700 Subject: [PATCH 0449/1237] clear dead code --- scripts/defaultScripts.js | 1 - scripts/system/attachedEntitiesManager.js | 20 -------------------- 2 files changed, 21 deletions(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 4fd17a1c6f..2a050d183e 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,4 +21,3 @@ Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); Script.load("system/dialTone.js"); -// Script.load("system/attachedEntitiesManager.js"); \ No newline at end of file diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index fcd48b664c..ee852d8d65 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -86,10 +86,6 @@ function AttachedEntitiesManager() { if (channel !== 'Hifi-Object-Manipulation') { return; } - // if (sender !== MyAvatar.sessionUUID) { - // print('wearablesManager got message from wrong sender'); - // return; - // } var parsedMessage = null; @@ -116,11 +112,6 @@ function AttachedEntitiesManager() { this.handleEntityRelease = function(grabbedEntity, releasedFromJoint) { // if this is still equipped, just rewrite the position information. var grabData = getEntityCustomData('grabKey', grabbedEntity, {}); - // if ("refCount" in grabData && grabData.refCount > 0) { - // // for adjusting things in your other hand - // manager.updateRelativeOffsets(grabbedEntity); - // return; - // } var allowedJoints = getEntityCustomData('wearable', grabbedEntity, DEFAULT_WEARABLE_DATA).joints; @@ -221,17 +212,6 @@ function AttachedEntitiesManager() { return false; } - // this.toggleLocked = function() { - // print("toggleLocked"); - // if (clothingLocked) { - // clothingLocked = false; - // toolBar.setImageURL(Script.resolvePath("assets/images/unlock.svg"), lockButton); - // } else { - // clothingLocked = true; - // toolBar.setImageURL(Script.resolvePath("assets/images/lock.svg"), lockButton); - // } - // } - // this.saveAttachedEntities = function() { // print("--- saving attached entities ---"); // saveData = []; From d7fc789ea705df7de5c601e48a2c041e9705445d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 13:47:52 -0700 Subject: [PATCH 0450/1237] try harder to not do simple-simulation on things that have an avatar ancestor --- libraries/entities/src/EntitySimulation.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index b99ab76b7a..8419e4ac6a 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -261,11 +261,14 @@ void EntitySimulation::moveSimpleKinematics(const quint64& now) { SetOfEntities::iterator itemItr = _simpleKinematicEntities.begin(); while (itemItr != _simpleKinematicEntities.end()) { EntityItemPointer entity = *itemItr; - if (entity->isMovingRelativeToParent() && - !entity->getPhysicsInfo() && - // The entity-server doesn't know where avatars are, so don't attempt to do simple extrapolation for - // children of avatars. - !entity->hasAncestorOfType(NestableType::Avatar)) { + + // The entity-server doesn't know where avatars are, so don't attempt to do simple extrapolation for + // children of avatars. + bool ancestryIsKnown; + entity->getMaximumAACube(ancestryIsKnown); + bool hasAvatarAncestor = entity->hasAncestorOfType(NestableType::Avatar); + + if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo() && ancestryIsKnown && !hasAvatarAncestor) { entity->simulate(now); _entitiesToSort.insert(entity); ++itemItr; From a312218722c6be133d303421c917f82dfa92a77d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 14:07:16 -0700 Subject: [PATCH 0451/1237] back out some changes --- libraries/shared/src/SpatiallyNestable.cpp | 41 +++++++++------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 6872b1d197..2a3cb4af47 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -415,10 +415,14 @@ void SpatiallyNestable::setVelocity(const glm::vec3& velocity, bool& success) { glm::vec3 parentVelocity = getParentVelocity(success); Transform parentTransform = getParentTransform(success); _velocityLock.withWriteLock([&] { - // HACK: The entity-server doesn't know where avatars are. In order to avoid having the server - // try to do simple extrapolation, set relative velocities to zero if this is a child of an avatar. + // HACK: until we are treating _velocity the same way we treat _position (meaning, + // _velocity is a vs parent value and any request for a world-frame velocity must + // be computed), do this to avoid equipped (parenting-grabbed) things from drifting. + // turning a zero velocity into a non-zero _velocity (because the avatar is moving) + // causes EntityItem::stepKinematicMotion to have an effect on the equipped entity, + // which causes it to drift from the hand. if (hasAncestorOfType(NestableType::Avatar)) { - _velocity = glm::vec3(0.0f); + _velocity = velocity; } else { // TODO: take parent angularVelocity into account. _velocity = glm::inverse(parentTransform.getRotation()) * (velocity - parentVelocity); @@ -475,12 +479,7 @@ void SpatiallyNestable::setAngularVelocity(const glm::vec3& angularVelocity, boo glm::vec3 parentAngularVelocity = getParentAngularVelocity(success); Transform parentTransform = getParentTransform(success); _angularVelocityLock.withWriteLock([&] { - if (hasAncestorOfType(NestableType::Avatar)) { - // TODO: this is done to keep entity-server from doing extrapolation, and isn't right. - _angularVelocity = glm::vec3(0.0f); - } else { - _angularVelocity = glm::inverse(parentTransform.getRotation()) * (angularVelocity - parentAngularVelocity); - } + _angularVelocity = glm::inverse(parentTransform.getRotation()) * (angularVelocity - parentAngularVelocity); }); } @@ -894,21 +893,13 @@ void SpatiallyNestable::setLocalTransformAndVelocities( _transformLock.withWriteLock([&] { _transform = localTransform; }); - // linear and angular velocity - if (hasAncestorOfType(NestableType::Avatar)) { - _velocityLock.withWriteLock([&] { - _velocity = glm::vec3(0.0f); - }); - _angularVelocityLock.withWriteLock([&] { - _angularVelocity = glm::vec3(0.0f); - }); - } else { - _velocityLock.withWriteLock([&] { - _velocity = localVelocity; - }); - _angularVelocityLock.withWriteLock([&] { - _angularVelocity = localAngularVelocity; - }); - } + // linear velocity + _velocityLock.withWriteLock([&] { + _velocity = localVelocity; + }); + // angular velocity + _angularVelocityLock.withWriteLock([&] { + _angularVelocity = localAngularVelocity; + }); locationChanged(false); } From 42068cf2dd04bb7e4f4b3e8827f4bafbb4b0c82b Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 8 Jun 2016 15:01:53 -0700 Subject: [PATCH 0452/1237] Only render equip-hotspots for objects with attach points. --- .../system/controllers/handControllerGrab.js | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 8a96e9d80c..140aa54144 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -894,30 +894,24 @@ function MyController(hand) { var grabData = getEntityCustomData(GRABBABLE_DATA_KEY, entities[i], undefined); var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); if (grabData) { - - var hotspotPos = grabProps.position; - // does this entity have an attach point? var wearableData = getEntityCustomData("wearable", entities[i], undefined); - if (wearableData) { + if (wearableData && wearableData.joints) { var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (wearableData[handJointName]) { - // draw the hotspot around the attach point. - hotspotPos = wearableData[handJointName][0]; + if (wearableData.joints[handJointName]) { + // draw the hotspot + this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { + position: grabProps.position, + size: 0.2, + color: { red: 90, green: 255, blue: 90 }, + alpha: 0.7, + solid: true, + visible: true, + ignoreRayIntersection: false, + drawInFront: false + })); } } - - // draw a hotspot! - this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { - position: hotspotPos, - size: 0.2, - color: { red: 90, green: 255, blue: 90 }, - alpha: 0.7, - solid: true, - visible: true, - ignoreRayIntersection: false, - drawInFront: false - })); } } }; From f070708b4a49c299967c78b7881df36bfa909cdc Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 8 Jun 2016 15:53:54 -0700 Subject: [PATCH 0453/1237] _avatarEntityData is accessed by more than one thread. --- interface/src/avatar/Avatar.cpp | 10 ++-- interface/src/avatar/MyAvatar.cpp | 14 ++--- libraries/avatars/src/AvatarData.cpp | 79 ++++++++++++++++++---------- libraries/avatars/src/AvatarData.h | 1 + 4 files changed, 65 insertions(+), 39 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 3e22448386..b5ac8ba77d 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -240,11 +240,13 @@ void Avatar::updateAvatarEntities() { } AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); - foreach (auto entityID, recentlyDettachedAvatarEntities) { - if (!_avatarEntityData.contains(entityID)) { - entityTree->deleteEntity(entityID, true, true); + _avatarEntitiesLock.withReadLock([&] { + foreach (auto entityID, recentlyDettachedAvatarEntities) { + if (!_avatarEntityData.contains(entityID)) { + entityTree->deleteEntity(entityID, true, true); + } } - } + }); }); if (success) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 62772c6933..324ff1534f 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -723,12 +723,14 @@ void MyAvatar::saveData() { settings.beginWriteArray("avatarEntityData"); int avatarEntityIndex = 0; - for (auto entityID : _avatarEntityData.keys()) { - settings.setArrayIndex(avatarEntityIndex); - settings.setValue("id", entityID); - settings.setValue("properties", _avatarEntityData.value(entityID)); - avatarEntityIndex++; - } + _avatarEntitiesLock.withReadLock([&] { + for (auto entityID : _avatarEntityData.keys()) { + settings.setArrayIndex(avatarEntityIndex); + settings.setValue("id", entityID); + settings.setValue("properties", _avatarEntityData.value(entityID)); + avatarEntityIndex++; + } + }); settings.endArray(); settings.setValue("displayName", _displayName); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index ff7438bb17..6d99d6ad81 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -900,7 +900,11 @@ bool AvatarData::processAvatarIdentity(const Identity& identity) { hasIdentityChanged = true; } - if (identity.avatarEntityData != _avatarEntityData) { + bool avatarEntityDataChanged = false; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityDataChanged = (identity.avatarEntityData != _avatarEntityData); + }); + if (avatarEntityDataChanged) { setAvatarEntityData(identity.avatarEntityData); hasIdentityChanged = true; } @@ -914,7 +918,9 @@ QByteArray AvatarData::identityByteArray() { QUrl emptyURL(""); const QUrl& urlToSend = _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; - identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName << _avatarEntityData; + _avatarEntitiesLock.withReadLock([&] { + identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName << _avatarEntityData; + }); return identityData; } @@ -1306,16 +1312,18 @@ QJsonObject AvatarData::toJson() const { root[JSON_AVATAR_ATTACHEMENTS] = attachmentsJson; } - if (!_avatarEntityData.empty()) { - QJsonArray avatarEntityJson; - for (auto entityID : _avatarEntityData.keys()) { - QVariantMap entityData; - entityData.insert("id", entityID); - entityData.insert("properties", _avatarEntityData.value(entityID)); - avatarEntityJson.push_back(QVariant(entityData).toJsonObject()); + _avatarEntitiesLock.withReadLock([&] { + if (!_avatarEntityData.empty()) { + QJsonArray avatarEntityJson; + for (auto entityID : _avatarEntityData.keys()) { + QVariantMap entityData; + entityData.insert("id", entityID); + entityData.insert("properties", _avatarEntityData.value(entityID)); + avatarEntityJson.push_back(QVariant(entityData).toJsonObject()); + } + root[JSON_AVATAR_ENTITIES] = avatarEntityJson; } - root[JSON_AVATAR_ENTITIES] = avatarEntityJson; - } + }); auto recordingBasis = getRecordingBasis(); bool success; @@ -1604,8 +1612,10 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent QMetaObject::invokeMethod(this, "updateAvatarEntity", Q_ARG(const QUuid&, entityID), Q_ARG(QByteArray, entityData)); return; } - _avatarEntityData.insert(entityID, entityData); - _avatarEntityDataLocallyEdited = true; + _avatarEntitiesLock.withWriteLock([&] { + _avatarEntityData.insert(entityID, entityData); + _avatarEntityDataLocallyEdited = true; + }); } void AvatarData::clearAvatarEntity(const QUuid& entityID) { @@ -1613,18 +1623,25 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID) { QMetaObject::invokeMethod(this, "clearAvatarEntity", Q_ARG(const QUuid&, entityID)); return; } - _avatarEntityData.remove(entityID); - _avatarEntityDataLocallyEdited = true; + + _avatarEntitiesLock.withWriteLock([&] { + _avatarEntityData.remove(entityID); + _avatarEntityDataLocallyEdited = true; + }); } AvatarEntityMap AvatarData::getAvatarEntityData() const { + AvatarEntityMap result; if (QThread::currentThread() != thread()) { - AvatarEntityMap result; QMetaObject::invokeMethod(const_cast(this), "getAvatarEntityData", Qt::BlockingQueuedConnection, Q_RETURN_ARG(AvatarEntityMap, result)); return result; } - return _avatarEntityData; + + _avatarEntitiesLock.withReadLock([&] { + result = _avatarEntityData; + }); + return result; } void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { @@ -1632,29 +1649,33 @@ void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { QMetaObject::invokeMethod(this, "setAvatarEntityData", Q_ARG(const AvatarEntityMap&, avatarEntityData)); return; } - if (_avatarEntityData != avatarEntityData) { - // keep track of entities that were attached to this avatar but no longer are - AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); + _avatarEntitiesLock.withWriteLock([&] { + if (_avatarEntityData != avatarEntityData) { + // keep track of entities that were attached to this avatar but no longer are + AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); - _avatarEntityData = avatarEntityData; - setAvatarEntityDataChanged(true); + _avatarEntityData = avatarEntityData; + setAvatarEntityDataChanged(true); - foreach (auto entityID, previousAvatarEntityIDs) { - if (!_avatarEntityData.contains(entityID)) { - _avatarEntityDetached.insert(entityID); + foreach (auto entityID, previousAvatarEntityIDs) { + if (!_avatarEntityData.contains(entityID)) { + _avatarEntityDetached.insert(entityID); + } } } - } + }); } AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { + AvatarEntityIDs result; if (QThread::currentThread() != thread()) { - AvatarEntityIDs result; QMetaObject::invokeMethod(const_cast(this), "getRecentlyDetachedIDs", Qt::BlockingQueuedConnection, Q_RETURN_ARG(AvatarEntityIDs, result)); return result; } - AvatarEntityIDs result = _avatarEntityDetached; - _avatarEntityDetached.clear(); + _avatarEntitiesLock.withWriteLock([&] { + result = _avatarEntityDetached; + _avatarEntityDetached.clear(); + }); return result; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 61ee649273..2dd1079b49 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -418,6 +418,7 @@ protected: // updates about one avatar to another. glm::vec3 _globalPosition; + mutable ReadWriteLockable _avatarEntitiesLock; AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar AvatarEntityMap _avatarEntityData; bool _avatarEntityDataLocallyEdited { false }; From 9926c809179c8d98efdbce5afd497bd59b2ee63c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 8 Jun 2016 18:15:34 -0500 Subject: [PATCH 0454/1237] Only check user metadata before sending --- domain-server/src/DomainMetadata.cpp | 14 +++++++++++--- domain-server/src/DomainMetadata.h | 10 ++++++++++ domain-server/src/DomainServer.cpp | 5 +++++ domain-server/src/DomainServer.h | 1 + 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 8fdbc2bbd1..26d2bb87ce 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -79,11 +79,11 @@ void DomainMetadata::setDescriptors(QVariantMap& settings) { _metadata[DESCRIPTORS] = descriptors; #if DEV_BUILD || PR_BUILD - qDebug() << "Regenerated domain metadata - descriptors:" << descriptors; + qDebug() << "Domain metadata descriptors set:" << descriptors; #endif } -void DomainMetadata::usersChanged() { +void DomainMetadata::updateUsers() { static const QString DEFAULT_HOSTNAME = "*"; auto nodeList = DependencyManager::get(); @@ -119,6 +119,14 @@ void DomainMetadata::usersChanged() { _metadata[USERS] = users; #if DEV_BUILD || PR_BUILD - qDebug() << "Regenerated domain metadata - users:" << users; + qDebug() << "Domain metadata users updated:" << users; +#endif +} + +void DomainMetadata::usersChanged() { + ++_tic; + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata users change detected"; #endif } diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index e2f4674afc..7d58d43182 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -11,6 +11,8 @@ #ifndef hifi_DomainMetadata_h #define hifi_DomainMetadata_h +#include + #include #include @@ -39,17 +41,25 @@ Q_OBJECT public: DomainMetadata(); + // Returns the last set metadata + // If connected users have changed, metadata may need to be updated + // this should be checked by storing tic = getTic() between calls + // and testing it for equality before the next get (tic == getTic()) QJsonObject get() { return QJsonObject::fromVariantMap(_metadata); } QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS].toMap()); } QJsonObject getDescriptors() { return QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); } + uint32_t getTic() { return _tic; } + void setDescriptors(QVariantMap& settings); + void updateUsers(); public slots: void usersChanged(); protected: QVariantMap _metadata; + uint32_t _tic{ 0 }; }; #endif // hifi_DomainMetadata_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 88c4e215b2..0f5498a575 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1101,6 +1101,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // Add the metadata to the heartbeat static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + auto tic = _metadata.getTic(); + if (_metadataTic != tic) { + _metadataTic = tic; + _metadata.updateUsers(); + } domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.getUsers(); QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index acda550ce5..8b8409ff0a 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -171,6 +171,7 @@ private: DomainServerSettingsManager _settingsManager; DomainMetadata _metadata; + uint32_t _metadataTic{ 0 }; HifiSockAddr _iceServerSocket; std::unique_ptr _iceServerHeartbeatPacket; From 24e5000aebee67e7ccd00a7fe8773c2362615cbe Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 8 Jun 2016 18:26:54 -0700 Subject: [PATCH 0455/1237] exposed orientation and eye position to procedural entity shaders --- .../src/RenderableProceduralItemShader.h | 2 ++ .../src/RenderableShapeEntityItem.cpp | 2 +- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 17 +++++++++++++++++ libraries/gpu-gl/src/gpu/gl/GLBackend.h | 1 + libraries/gpu/src/gpu/Batch.cpp | 10 ++++++++++ libraries/gpu/src/gpu/Batch.h | 6 ++++++ .../procedural/src/procedural/Procedural.cpp | 18 +++++++++++++++--- .../procedural/src/procedural/Procedural.h | 6 +++++- .../src/procedural/ProceduralShaders.h | 2 ++ .../src/procedural/ProceduralSkybox.cpp | 2 +- 10 files changed, 60 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableProceduralItemShader.h b/libraries/entities-renderer/src/RenderableProceduralItemShader.h index 4762f619bf..d7df43de14 100644 --- a/libraries/entities-renderer/src/RenderableProceduralItemShader.h +++ b/libraries/entities-renderer/src/RenderableProceduralItemShader.h @@ -314,6 +314,8 @@ in vec4 _position; // TODO add more uniforms uniform float iGlobalTime; // shader playback time (in seconds) uniform vec3 iWorldScale; // the dimensions of the object being rendered +uniform mat3 iWorldOrientation; // the orientation of the object being rendered +uniform vec3 iWorldEyePosition; // the world position of the eye // TODO add support for textures // TODO document available inputs other than the uniforms diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index c93ae252e3..2b04478831 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -98,7 +98,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { } batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation if (_procedural->ready()) { - _procedural->prepare(batch, getPosition(), getDimensions()); + _procedural->prepare(batch, getPosition(), getDimensions(), getOrientation(), args->getViewFrustum().getPosition()); auto outColor = _procedural->getColor(color); batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); DependencyManager::get()->renderShape(batch, MAPPING[_shape]); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index ece1ee730c..e18b784018 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -114,6 +114,7 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::gl::GLBackend::do_glUniform3fv), (&::gpu::gl::GLBackend::do_glUniform4fv), (&::gpu::gl::GLBackend::do_glUniform4iv), + (&::gpu::gl::GLBackend::do_glUniformMatrix3fv), (&::gpu::gl::GLBackend::do_glUniformMatrix4fv), (&::gpu::gl::GLBackend::do_glColor4f), @@ -515,6 +516,22 @@ void GLBackend::do_glUniform4iv(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } +void GLBackend::do_glUniformMatrix3fv(Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + + glUniformMatrix3fv( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), + batch._params[paramOffset + 2]._uint, + batch._params[paramOffset + 1]._uint, + (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); + (void)CHECK_GL_ERROR(); +} + void GLBackend::do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 610672f44f..5255a6cb25 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -136,6 +136,7 @@ public: virtual void do_glUniform3fv(Batch& batch, size_t paramOffset) final; virtual void do_glUniform4fv(Batch& batch, size_t paramOffset) final; virtual void do_glUniform4iv(Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix3fv(Batch& batch, size_t paramOffset) final; virtual void do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) final; virtual void do_glColor4f(Batch& batch, size_t paramOffset) final; diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 9126f11acf..6dc1d63ca8 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -567,6 +567,16 @@ void Batch::_glUniform4iv(int32 location, int count, const int32* value) { _params.push_back(location); } +void Batch::_glUniformMatrix3fv(int32 location, int count, uint8 transpose, const float* value) { + ADD_COMMAND(glUniformMatrix3fv); + + const int MATRIX3_SIZE = 9 * sizeof(float); + _params.push_back(cacheData(count * MATRIX3_SIZE, value)); + _params.push_back(transpose); + _params.push_back(count); + _params.push_back(location); +} + void Batch::_glUniformMatrix4fv(int32 location, int count, uint8 transpose, const float* value) { ADD_COMMAND(glUniformMatrix4fv); diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index a2ee57759f..f8b447975c 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -269,6 +269,7 @@ public: void _glUniform3fv(int location, int count, const float* value); void _glUniform4fv(int location, int count, const float* value); void _glUniform4iv(int location, int count, const int* value); + void _glUniformMatrix3fv(int location, int count, unsigned char transpose, const float* value); void _glUniformMatrix4fv(int location, int count, unsigned char transpose, const float* value); void _glUniform(int location, int v0) { @@ -291,6 +292,10 @@ public: _glUniform4f(location, v.x, v.y, v.z, v.w); } + void _glUniform(int location, const glm::quat& v) { + _glUniformMatrix3fv(location, 1, false, reinterpret_cast< const float* >(&glm::mat3_cast(v))); + } + void _glColor4f(float red, float green, float blue, float alpha); enum Command { @@ -348,6 +353,7 @@ public: COMMAND_glUniform3fv, COMMAND_glUniform4fv, COMMAND_glUniform4iv, + COMMAND_glUniformMatrix3fv, COMMAND_glUniformMatrix4fv, COMMAND_glColor4f, diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 781b508bc7..4f308e0812 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -39,6 +39,8 @@ static const std::string STANDARD_UNIFORM_NAMES[Procedural::NUM_STANDARD_UNIFORM "iFrameCount", "iWorldScale", "iWorldPosition", + "iWorldOrientation", + "iWorldEyePosition", "iChannelResolution" }; @@ -202,9 +204,11 @@ bool Procedural::ready() { return true; } -void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size) { +void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const glm::vec3& eyePos) { _entityDimensions = size; _entityPosition = position; + _entityOrientation = orientation; + _eyePos = eyePos; if (_shaderUrl.isLocalFile()) { auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { @@ -404,10 +408,10 @@ void Procedural::setupUniforms() { }); } - if (gpu::Shader::INVALID_LOCATION != _standardUniformSlots[SCALE]) { + if (gpu::Shader::INVALID_LOCATION != _standardUniformSlots[ORIENTATION]) { // FIXME move into the 'set once' section, since this doesn't change over time _uniforms.push_back([=](gpu::Batch& batch) { - batch._glUniform(_standardUniformSlots[SCALE], _entityDimensions); + batch._glUniform(_standardUniformSlots[ORIENTATION], _entityOrientation); }); } @@ -417,6 +421,14 @@ void Procedural::setupUniforms() { batch._glUniform(_standardUniformSlots[POSITION], _entityPosition); }); } + + if (gpu::Shader::INVALID_LOCATION != _standardUniformSlots[EYE_POSITION]) { + // FIXME move into the 'set once' section, since this doesn't change over time + _uniforms.push_back([=](gpu::Batch& batch) { + batch._glUniform(_standardUniformSlots[EYE_POSITION], _eyePos); + }); + } + } void Procedural::setupChannels(bool shouldCreate) { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index f928d0e594..e27f8ade44 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -38,7 +38,7 @@ public: void parse(const QString& userDataJson); bool ready(); - void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size); + void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const glm::vec3& eyePos); const gpu::ShaderPointer& getShader() const { return _shader; } glm::vec4 getColor(const glm::vec4& entityColor); @@ -56,6 +56,8 @@ public: FRAME_COUNT, SCALE, POSITION, + ORIENTATION, + EYE_POSITION, CHANNEL_RESOLUTION, NUM_STANDARD_UNIFORMS }; @@ -93,6 +95,8 @@ protected: // Entity metadata glm::vec3 _entityDimensions; glm::vec3 _entityPosition; + glm::quat _entityOrientation; + glm::vec3 _eyePos; private: // This should only be called from the render thread, as it shares data with Procedural::prepare diff --git a/libraries/procedural/src/procedural/ProceduralShaders.h b/libraries/procedural/src/procedural/ProceduralShaders.h index eddf53cb09..00571c3a94 100644 --- a/libraries/procedural/src/procedural/ProceduralShaders.h +++ b/libraries/procedural/src/procedural/ProceduralShaders.h @@ -289,6 +289,8 @@ const vec4 iChannelTime = vec4(0.0); uniform vec4 iDate; uniform int iFrameCount; uniform vec3 iWorldPosition; +uniform mat3 iWorldOrientation; +uniform vec3 iWorldEyePosition; uniform vec3 iChannelResolution[4]; uniform sampler2D iChannel0; uniform sampler2D iChannel1; diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 1aa59c90d7..5905bfb017 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -52,7 +52,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setModelTransform(Transform()); // only for Mac auto& procedural = skybox._procedural; - procedural.prepare(batch, glm::vec3(0), glm::vec3(1)); + procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(1, 0, 0, 0), glm::vec3(0)); auto textureSlot = procedural.getShader()->getTextures().findLocation("cubeMap"); auto bufferSlot = procedural.getShader()->getBuffers().findLocation("skyboxBuffer"); skybox.prepare(batch, textureSlot, bufferSlot); From 5ef6847dc35b047ffbced088d07de9bcc79bda8e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 8 Jun 2016 18:36:36 -0700 Subject: [PATCH 0456/1237] HMD re-centering while driving improvements. Previously the HUD fading in/out would also recenter the hmd sensor and the avatar, which caused many problems including: * The user's view could shift vertically. * Your avatar would briefly go into t-pose * other users would see your avatar go into t-pose. Now we now move the UI sphere instead, which results in a much smoother experience. MyAvatar: added hasDriveInput method. OverlayConductor: * removed avatar and sensor reset, instead the overlay's modelTransform is changed. * revived STANDING mode, which is active if myAvatar->getClearOverlayWhenDriving() is true and you are wearing an HMD. * SITTING & FLAT mode should be unchanged. * Instead of using avatar velocity to fade out/fade in the hud, We use the presense or absanse of avatar drive input. * Additionally, we check distance to the UI sphere, and quickly recenter the hud if the users head is too close to the actual hud sphere. CompositorHelper: * Bug fixes for ray picks not using the modelTransform. HmdDisplayPlugin: * Bug fixes for rendering not using the modelTransform. --- interface/src/avatar/MyAvatar.cpp | 7 +- interface/src/avatar/MyAvatar.h | 2 + interface/src/ui/OverlayConductor.cpp | 113 ++++++++---------- interface/src/ui/OverlayConductor.h | 1 + .../src/display-plugins/CompositorHelper.cpp | 4 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 3 +- 6 files changed, 66 insertions(+), 64 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 62772c6933..76e48d34d6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1261,8 +1261,7 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setPositionAndOrientation(getPosition(), getOrientation()); if (qApp->isHMDMode()) { - bool hasDriveInput = fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; - _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput); + _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } else { _follow.deactivate(); } @@ -2135,3 +2134,7 @@ bool MyAvatar::didTeleport() { lastPosition = pos; return (changeInPosition.length() > MAX_AVATAR_MOVEMENT_PER_FRAME); } + +bool MyAvatar::hasDriveInput() const { + return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d3da32e0ed..381d658046 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -264,6 +264,8 @@ public: controller::Pose getLeftHandControllerPoseInAvatarFrame() const; controller::Pose getRightHandControllerPoseInAvatarFrame() const; + bool hasDriveInput() const; + public slots: void increaseSize(); void decreaseSize(); diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 54dba229e3..c4fd938e97 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -22,10 +22,31 @@ OverlayConductor::OverlayConductor() { OverlayConductor::~OverlayConductor() { } +bool OverlayConductor::shouldCenterUI() const { + + glm::mat4 hmdMat = qApp->getHMDSensorPose(); + glm::vec3 hmdPos = extractTranslation(hmdMat); + glm::vec3 hmdForward = transformVectorFast(hmdMat, glm::vec3(0.0f, 0.0f, -1.0f)); + + Transform uiTransform = qApp->getApplicationCompositor().getModelTransform(); + glm::vec3 uiPos = uiTransform.getTranslation(); + glm::vec3 uiForward = uiTransform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); + + const float MAX_COMPOSITOR_DISTANCE = 0.6f; + const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled + if (glm::distance(uiPos, hmdPos) > MAX_COMPOSITOR_DISTANCE || + glm::dot(uiForward, hmdForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE))) { + return true; + } + return false; +} + void OverlayConductor::update(float dt) { updateMode(); + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + switch (_mode) { case SITTING: { // when sitting, the overlay is at the origin, facing down the -z axis. @@ -36,27 +57,30 @@ void OverlayConductor::update(float dt) { break; } case STANDING: { - // when standing, the overlay is at a reference position, which is set when the overlay is - // enabled. The camera is taken directly from the HMD, but in world space. - // So the sensorToWorldMatrix must be applied. - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - Transform t; - t.evalFromRawMatrix(myAvatar->getSensorToWorldMatrix()); - qApp->getApplicationCompositor().setCameraBaseTransform(t); - // detect when head moves out side of sweet spot, or looks away. - mat4 headMat = myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose(); - vec3 headWorldPos = extractTranslation(headMat); - vec3 headForward = glm::quat_cast(headMat) * glm::vec3(0.0f, 0.0f, -1.0f); - Transform modelXform = qApp->getApplicationCompositor().getModelTransform(); - vec3 compositorWorldPos = modelXform.getTranslation(); - vec3 compositorForward = modelXform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - const float MAX_COMPOSITOR_DISTANCE = 0.6f; - const float MAX_COMPOSITOR_ANGLE = 110.0f; - if (_enabled && (glm::distance(headWorldPos, compositorWorldPos) > MAX_COMPOSITOR_DISTANCE || - glm::dot(headForward, compositorForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE)))) { - // fade out the overlay - setEnabled(false); + const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; + const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; + + // fade in or out the overlay, based on driving. + bool nowDriving = myAvatar->hasDriveInput(); + // Check that we're in this new mode for long enough to really trigger a transition. + if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. + _timeInPotentialMode = 0; + } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. + _timeInPotentialMode = usecTimestampNow(); + } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { + _timeInPotentialMode = 0; // a real transition + bool wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); + if (wantsOverlays) { + setEnabled(!nowDriving); + } + _driving = nowDriving; + } + + // center the UI + if (shouldCenterUI()) { + Transform hmdTransform(cancelOutRollAndPitch(qApp->getHMDSensorPose())); + qApp->getApplicationCompositor().setModelTransform(hmdTransform); } break; } @@ -68,43 +92,14 @@ void OverlayConductor::update(float dt) { void OverlayConductor::updateMode() { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - if (myAvatar->getClearOverlayWhenDriving()) { - float speed = glm::length(myAvatar->getVelocity()); - const float MIN_DRIVING = 0.2f; - const float MAX_NOT_DRIVING = 0.01f; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; - bool nowDriving = _driving; // Assume current _driving mode unless... - if (speed > MIN_DRIVING) { // ... we're definitely moving... - nowDriving = true; - } else if (speed < MAX_NOT_DRIVING) { // ... or definitely not. - nowDriving = false; - } - // Check that we're in this new mode for long enough to really trigger a transition. - if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. - _timeInPotentialMode = 0; - } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. - _timeInPotentialMode = usecTimestampNow(); - } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { - _timeInPotentialMode = 0; // a real transition - if (nowDriving) { - _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); - } else { // reset when coming out of driving - _mode = FLAT; // Seems appropriate to let things reset, below, after the following. - // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. - qApp->getActiveDisplayPlugin()->resetSensors(); - myAvatar->reset(true, false, false); - } - if (_wantsOverlays) { - setEnabled(!nowDriving); - } - _driving = nowDriving; - } // Else haven't accumulated enough time in new mode, but keep timing. - } Mode newMode; if (qApp->isHMDMode()) { - newMode = SITTING; + if (myAvatar->getClearOverlayWhenDriving()) { + newMode = STANDING; + } else { + newMode = SITTING; + } } else { newMode = FLAT; } @@ -117,11 +112,10 @@ void OverlayConductor::updateMode() { qApp->getApplicationCompositor().setModelTransform(Transform()); break; } - case STANDING: { // STANDING mode is not currently used. + case STANDING: { // enter the STANDING state - // place the overlay at the current hmd position in world space - auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); + Transform hmdTransform(cancelOutRollAndPitch(qApp->getHMDSensorPose())); + qApp->getApplicationCompositor().setModelTransform(hmdTransform); break; } @@ -132,7 +126,6 @@ void OverlayConductor::updateMode() { } _mode = newMode; - } void OverlayConductor::setEnabled(bool enabled) { @@ -149,7 +142,7 @@ void OverlayConductor::setEnabled(bool enabled) { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); - } + } } bool OverlayConductor::getEnabled() const { diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 1ec66663a4..4ecbac5bcf 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -19,6 +19,7 @@ public: void update(float dt); void setEnabled(bool enable); bool getEnabled() const; + bool shouldCenterUI() const; private: void updateMode(); diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index d4fff1b976..2d3c79071f 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -336,7 +336,9 @@ void CompositorHelper::computeHmdPickRay(const glm::vec2& cursorPos, glm::vec3& } glm::mat4 CompositorHelper::getUiTransform() const { - return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose()); + glm::mat4 modelMat; + _modelTransform.getMatrix(modelMat); + return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose()) * modelMat; } //Finds the collision point of a world space ray diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 1616dcdb77..b9f6ad1a98 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -253,12 +253,13 @@ void HmdDisplayPlugin::compositeScene() { void HmdDisplayPlugin::compositeOverlay() { using namespace oglplus; auto compositorHelper = DependencyManager::get(); + glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); useProgram(_program); _sphereSection->Use(); for_each_eye([&](Eye eye) { eyeViewport(eye); - auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)); + auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; auto mvp = _eyeProjections[eye] * modelView; Uniform(*_program, _mvpUniform).Set(mvp); _sphereSection->Draw(); From e69022285e31976f2219058de2fba7ee1cbf776c Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 8 Jun 2016 18:58:50 -0700 Subject: [PATCH 0457/1237] A lot more stuff to try to see the skin scattering --- .../render-utils/src/DebugDeferredBuffer.cpp | 4 +- .../render-utils/src/DeferredTransform.slh | 4 +- .../render-utils/src/RenderDeferredTask.cpp | 3 + .../render-utils/src/SubsurfaceScattering.cpp | 257 ++++++++++++++++++ .../render-utils/src/SubsurfaceScattering.h | 71 +++++ .../src/surfaceGeometry_makeCurvature.slf | 36 +-- libraries/render/src/render/BlurTask.cpp | 144 +++++++++- libraries/render/src/render/BlurTask.h | 47 +++- libraries/render/src/render/BlurTask.slh | 1 + .../src/render/blurGaussianDepthAwareH.slf | 23 ++ .../src/render/blurGaussianDepthAwareV.slf | 23 ++ 11 files changed, 588 insertions(+), 25 deletions(-) create mode 100644 libraries/render-utils/src/SubsurfaceScattering.cpp create mode 100644 libraries/render-utils/src/SubsurfaceScattering.h create mode 100644 libraries/render/src/render/blurGaussianDepthAwareH.slf create mode 100644 libraries/render/src/render/blurGaussianDepthAwareV.slf diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 0a3de15c36..2fbdc92b22 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -141,8 +141,8 @@ static const std::string DEFAULT_PYRAMID_DEPTH_SHADER { static const std::string DEFAULT_CURVATURE_SHADER{ "vec4 getFragmentColor() {" - // " return vec4(pow(vec3(texture(curvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" - " return vec4(pow(vec3(texture(curvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + " return vec4(pow(vec3(texture(curvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" + // " return vec4(pow(vec3(texture(curvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" " }" }; diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 647ac0a76c..536d77d7c1 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -84,7 +84,7 @@ vec3 evalEyePositionFromZeye(int side, float Zeye, vec2 texcoord) { return vec3(Xe, Ye, Zeye); } -ivec2 getPixelPosNclipPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out vec2 nclipPos, out ivec4 stereoSide) { +ivec2 getPixelPosTexcoordPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out vec2 texcoordPos, out ivec4 stereoSide) { ivec2 fragPos = ivec2(glFragCoord.xy); stereoSide = getStereoSideInfo(fragPos.x, 0); @@ -92,7 +92,7 @@ ivec2 getPixelPosNclipPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out ve pixelPos = fragPos; pixelPos.x -= stereoSide.y; - nclipPos = (vec2(pixelPos) + 0.5) * getInvWidthHeight(); + texcoordPos = (vec2(pixelPos) + 0.5) * getInvWidthHeight(); return fragPos; } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 20b12c7516..0aac515fbd 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -35,6 +35,7 @@ #include "AmbientOcclusionEffect.h" #include "AntialiasingEffect.h" #include "ToneMappingEffect.h" +#include "SubsurfaceScattering.h" using namespace render; @@ -114,6 +115,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DiffuseCurvature", curvatureFramebuffer); + // AO job addJob("AmbientOcclusion"); @@ -136,6 +138,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DrawOverlay3DOpaque", overlayOpaques, true); addJob("DrawOverlay3DTransparent", overlayTransparents, false); + addJob("Scattering", deferredFrameTransform); // Debugging stages { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp new file mode 100644 index 0000000000..cfcf444a2f --- /dev/null +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -0,0 +1,257 @@ +// +// SubsurfaceScattering.cpp +// libraries/render-utils/src/ +// +// Created by Sam Gateau 6/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "SubsurfaceScattering.h" + +#include +#include + +#include "FramebufferCache.h" + +const int SubsurfaceScattering_FrameTransformSlot = 0; +const int SubsurfaceScattering_ParamsSlot = 1; +const int SubsurfaceScattering_DepthMapSlot = 0; +const int SubsurfaceScattering_NormalMapSlot = 1; + +SubsurfaceScattering::SubsurfaceScattering() { + Parameters parameters; + _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); +} + +void SubsurfaceScattering::configure(const Config& config) { + + if (config.depthThreshold != getCurvatureDepthThreshold()) { + _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold; + } +} + + + +gpu::PipelinePointer SubsurfaceScattering::getScatteringPipeline() { + if (!_scatteringPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + // slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); + // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + _scatteringPipeline = gpu::Pipeline::create(program, state); + } + + return _scatteringPipeline; +} + + +void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + + RenderArgs* args = renderContext->args; + + if (!_scatteringTable) { + _scatteringTable = SubsurfaceScattering::generatePreIntegratedScattering(); + } + + auto& pipeline = getScatteringPipeline(); + + + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + + batch.setViewportTransform(args->_viewport >> 1); + /* batch.setProjectionTransform(glm::mat4()); + batch.setViewTransform(Transform()); + + Transform model; + model.setTranslation(glm::vec3(sMin, tMin, 0.0f)); + model.setScale(glm::vec3(sWidth, tHeight, 1.0f)); + batch.setModelTransform(model);*/ + + batch.setPipeline(pipeline); + batch.setResourceTexture(0, _scatteringTable); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + +} + + + + + +// Reference: http://www.altdevblogaday.com/2011/12/31/skin-shading-in-unity3d/ + +#include +#include +#include + +#define _PI 3.14159265358979523846 +#define HEIGHT 512 +#define WIDTH 512 + +using namespace std; + + +double gaussian(float v, float r) { + double g = (1.0 / sqrt(2.0 * _PI * v)) * exp(-(r*r) / (2.0 * v)); + return g; +} + +vec3 scatter(double r) { + // Values from GPU Gems 3 "Advanced Skin Rendering". + // Originally taken from real life samples. + static const double profile[][4] = { + { 0.0064, 0.233, 0.455, 0.649 }, + { 0.0484, 0.100, 0.336, 0.344 }, + { 0.1870, 0.118, 0.198, 0.000 }, + { 0.5670, 0.113, 0.007, 0.007 }, + { 1.9900, 0.358, 0.004, 0.000 }, + { 7.4100, 0.078, 0.000, 0.000 } + }; + static const int profileNum = 6; + vec3 ret; ret.x = 0.0; ret.y = 0.0; ret.z = 0.0; + for (int i = 0; i < profileNum; i++) { + double g = gaussian(profile[i][0] * 1.414f, r); + ret.x += g * profile[i][1]; + ret.y += g * profile[i][2]; + ret.z += g * profile[i][3]; + } + + return ret; +} + +vec3 integrate(double cosTheta, double skinRadius) { + // Angle from lighting direction. + double theta = acos(cosTheta); + vec3 totalWeights; totalWeights.x = 0.0; totalWeights.y = 0.0; totalWeights.z = 0.0; + vec3 totalLight; totalLight.x = 0.0; totalLight.y = 0.0; totalLight.z = 0.0; + vec3 skinColour; skinColour.x = 1.0; skinColour.y = 1.0; skinColour.z = 1.0; + + double a = -(_PI); + + //const double inc = 0.001; + const double inc = 0.01; + + while (a <= (_PI)) { + double sampleAngle = theta + a; + double diffuse = cos(sampleAngle); + if (diffuse < 0.0) diffuse = 0.0; + if (diffuse > 1.0) diffuse = 1.0; + + // Distance. + double sampleDist = abs(2.0 * skinRadius * sin(a * 0.5)); + + // Profile Weight. + vec3 weights = scatter(sampleDist); + + totalWeights.x += weights.x; + totalWeights.y += weights.y; + totalWeights.z += weights.z; + totalLight.x += diffuse * weights.x * (skinColour.x * skinColour.x); + totalLight.y += diffuse * weights.y * (skinColour.y * skinColour.y); + totalLight.z += diffuse * weights.z * (skinColour.z * skinColour.z); + a += inc; + } + + vec3 result; + result.x = totalLight.x / totalWeights.x; + result.y = totalLight.y / totalWeights.y; + result.z = totalLight.z / totalWeights.z; + + return result; +} + +void diffuseScatter(gpu::TexturePointer& lut) { + for (int j = 0; j < HEIGHT; j++) { + for (int i = 0; i < WIDTH; i++) { + // Lookup by: x: NDotL y: 1 / r + float y = 2.0 * 1.0 / ((j + 1.0) / (double)HEIGHT); + float x = ((i / (double)WIDTH) * 2.0) - 1.0; + vec3 val = integrate(x, y); + + // Convert to linear + val.x = sqrt(val.x); + val.y = sqrt(val.y); + val.z = sqrt(val.z); + + // Convert to 24-bit image. + unsigned char valI[3]; + if (val.x > 1.0) val.x = 1.0; + if (val.y > 1.0) val.y = 1.0; + if (val.z > 1.0) val.z = 1.0; + valI[0] = (unsigned char)(val.x * 256.0); + valI[1] = (unsigned char)(val.y * 256.0); + valI[2] = (unsigned char)(val.z * 256.0); + //printf("%u %u %u\n", valI[0], valI[1], valI[2]); + + // Write to file. + // fwrite(valI, sizeof(unsigned char), 3, output_file); + } + + printf("%.2lf%% Done...\n", (j / (float)HEIGHT) * 100.0f); + } +} + +void diffuseProfile(gpu::TexturePointer& profile) { + std::vector bytes(3 * HEIGHT * WIDTH); + int size = sizeof(unsigned char) * bytes.size(); + + int index = 0; + for (int j = 0; j < HEIGHT; j++) { + for (int i = 0; i < WIDTH; i++) { + float y = (double)(j + 1.0) / (double)HEIGHT; + vec3 val = scatter(y * 2.0f); + + // Convert to 24-bit image. + unsigned char valI[3]; + if (val.x > 1.0) val.x = 1.0; + if (val.y > 1.0) val.y = 1.0; + if (val.z > 1.0) val.z = 1.0; + valI[0] = (unsigned char)(val.x * 255.0); + valI[1] = (unsigned char)(val.y * 255.0); + valI[2] = (unsigned char)(val.z * 255.0); + + bytes[3 * index] = valI[0]; + bytes[3 * index + 1] = valI[1]; + bytes[3 * index + 2] = valI[2]; + + // Write to file. + // fwrite(valI, sizeof(unsigned char), 3, output_file); + + index++; + } + } + + + profile->assignStoredMip(0, profile->getTexelFormat(), size, bytes.data()); +} + +/*int main() { + diffuseScatter(); + //diffuseProfile(); +}*/ + + + +gpu::TexturePointer SubsurfaceScattering::generatePreIntegratedScattering() { + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT)); + //diffuseScatter(scatteringLUT); + diffuseProfile(scatteringLUT); + return scatteringLUT; +} + diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h new file mode 100644 index 0000000000..593acd0d51 --- /dev/null +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -0,0 +1,71 @@ +// +// SubsurfaceScattering.h +// libraries/render-utils/src/ +// +// Created by Sam Gateau 6/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SubsurfaceScattering_h +#define hifi_SubsurfaceScattering_h + +#include + +#include "render/DrawTask.h" +#include "DeferredFrameTransform.h" + +class SubsurfaceScatteringConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) +public: + SubsurfaceScatteringConfig() : render::Job::Config(true) {} + + float depthThreshold{ 0.1f }; + +signals: + void dirty(); +}; + +class SubsurfaceScattering { +public: + using Config = SubsurfaceScatteringConfig; + using JobModel = render::Job::ModelIO; + + SubsurfaceScattering(); + + void configure(const Config& config); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer); + + float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } + + + static gpu::TexturePointer generatePreIntegratedScattering(); + +private: + typedef gpu::BufferView UniformBufferView; + + // Class describing the uniform buffer with all the parameters common to the AO shaders + class Parameters { + public: + // Resolution info + glm::vec4 resolutionInfo { -1.0f, 0.0f, 0.0f, 0.0f }; + // Curvature algorithm + glm::vec4 curvatureInfo{ 0.0f }; + + Parameters() {} + }; + gpu::BufferView _parametersBuffer; + + + gpu::TexturePointer _scatteringTable; + + + gpu::PipelinePointer _scatteringPipeline; + + gpu::PipelinePointer getScatteringPipeline(); +}; + +#endif // hifi_SubsurfaceScattering_h diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index d441fcb8f6..cb98819c0d 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -75,8 +75,8 @@ vec3 unpackNormal(in vec3 p) { return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); } -vec2 sideToFrameNclip(vec2 side, vec2 nclipPos) { - return vec2((nclipPos.x + side.x) * side.y, nclipPos.y); +vec2 sideToFrameTexcoord(vec2 side, vec2 texcoordPos) { + return vec2((texcoordPos.x + side.x) * side.y, texcoordPos.y); } uniform sampler2D normalMap; @@ -109,24 +109,24 @@ out vec4 outFragColor; void main(void) { // Pixel being shaded ivec2 pixelPos; - vec2 nclipPos; + vec2 texcoordPos; ivec4 stereoSide; - ivec2 framePixelPos = getPixelPosNclipPosAndSide(gl_FragCoord.xy, pixelPos, nclipPos, stereoSide); + ivec2 framePixelPos = getPixelPosTexcoordPosAndSide(gl_FragCoord.xy, pixelPos, texcoordPos, stereoSide); vec2 stereoSideClip = vec2(stereoSide.x, (isStereo() ? 0.5 : 1.0)); - vec2 frameNclipPos = sideToFrameNclip(stereoSideClip, nclipPos); + vec2 frameTexcoordPos = sideToFrameTexcoord(stereoSideClip, texcoordPos); // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); - vec3 worldNormal = getWorldNormal(frameNclipPos); + vec3 worldNormal = getWorldNormal(frameTexcoordPos); // The position of the pixel fragment in Eye space then in world space - vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, nclipPos); + vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; // Calculate the perspective scale. - float perspectiveScale =(-getProjScaleEye() / Zeye); - //outFragColor = vec4(vec3(perspectiveScale * 0.1), 1.0); + float perspectiveScale = (-getProjScaleEye() / Zeye); + float pixPerspectiveScaleInv = 1.0 / (perspectiveScale); vec2 viewportScale = perspectiveScale * getInvWidthHeight(); @@ -135,12 +135,12 @@ void main(void) { vec2 du = vec2( 1.0f, 0.0f ) * viewportScale.x; vec2 dv = vec2( 0.0f, 1.0f ) * viewportScale.y; - vec4 dFdu = vec4(getWorldNormalDiff(frameNclipPos, du), getEyeDepthDiff(frameNclipPos, du)); - vec4 dFdv = vec4(getWorldNormalDiff(frameNclipPos, dv), getEyeDepthDiff(frameNclipPos, dv)); + vec4 dFdu = vec4(getWorldNormalDiff(frameTexcoordPos, du), getEyeDepthDiff(frameTexcoordPos, du)); + vec4 dFdv = vec4(getWorldNormalDiff(frameTexcoordPos, dv), getEyeDepthDiff(frameTexcoordPos, dv)); dFdu *= step(abs(dFdu.w), threshold); dFdv *= step(abs(dFdv.w), threshold); - outFragColor = vec4(dFdu.xyz, 1.0); + // outFragColor = vec4(dFdu.xyz, 1.0); // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) @@ -161,11 +161,11 @@ void main(void) { py.xy /= py.w; pz.xy /= pz.w; - vec2 hclipPos = (nclipPos * 2.0 - 1.0); + vec2 nclipPos = (texcoordPos - 0.5) * 2.0; - px.xy = (px.xy - hclipPos) / perspectiveScale; - py.xy = (py.xy - hclipPos) / perspectiveScale; - pz.xy = (pz.xy - hclipPos) / perspectiveScale; + px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv; + py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv; + pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv; // Calculate dF/dx, dF/dy and dF/dz using chain rule vec4 dFdx = dFdu * px.x + dFdv * px.y; @@ -174,6 +174,6 @@ void main(void) { // Calculate the mean curvature float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333) * params.curvatureInfo.w; - //outFragColor = vec4(vec3(worldNormal + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); - outFragColor = vec4((vec3(dFdx.x, dFdy.y, dFdz.z) * params.curvatureInfo.w + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); + outFragColor = vec4(vec3(worldNormal + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); + // outFragColor = vec4((vec3(dFdx.x, dFdy.y, dFdz.z) * params.curvatureInfo.w + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); } diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 01fb610ff5..a5a02ff78c 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -16,6 +16,9 @@ #include "blurGaussianV_frag.h" #include "blurGaussianH_frag.h" +#include "blurGaussianDepthAwareV_frag.h" +#include "blurGaussianDepthAwareH_frag.h" + using namespace render; enum BlurShaderBufferSlots { @@ -32,11 +35,18 @@ BlurParams::BlurParams() { _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Params), (const gpu::Byte*) ¶ms)); } -void BlurParams::setWidthHeight(int width, int height) { +void BlurParams::setWidthHeight(int width, int height, bool isStereo) { auto resolutionInfo = _parametersBuffer.get().resolutionInfo; + bool resChanged = false; if (width != resolutionInfo.x || height != resolutionInfo.y) { + resChanged = true; _parametersBuffer.edit().resolutionInfo = glm::vec4((float) width, (float) height, 1.0f / (float) width, 1.0f / (float) height); } + + auto stereoInfo = _parametersBuffer.get().stereoInfo; + if (isStereo || resChanged) { + _parametersBuffer.edit().stereoInfo = glm::vec4((float)width, (float)height, 1.0f / (float)width, 1.0f / (float)height); + } } void BlurParams::setFilterRadiusScale(float scale) { @@ -150,7 +160,7 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont auto blurVPipeline = getBlurVPipeline(); auto blurHPipeline = getBlurHPipeline(); - _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w); + _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); @@ -175,3 +185,133 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont }); } + + +BlurGaussianDepthAware::BlurGaussianDepthAware() { + _parameters = std::make_shared(); +} + +gpu::PipelinePointer BlurGaussianDepthAware::getBlurVPipeline() { + if (!_blurVPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(blurGaussianDepthAwareV_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + _blurVPipeline = gpu::Pipeline::create(program, state); + } + + return _blurVPipeline; +} + +gpu::PipelinePointer BlurGaussianDepthAware::getBlurHPipeline() { + if (!_blurHPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(blurGaussianDepthAwareH_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + _blurHPipeline = gpu::Pipeline::create(program, state); + } + + return _blurHPipeline; +} + +bool BlurGaussianDepthAware::updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources) { + if (!sourceFramebuffer) { + return false; + } + + if (!_blurredFramebuffer) { + _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + + // attach depthStencil if present in source + if (sourceFramebuffer->hasDepthStencil()) { + _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + _blurredFramebuffer->setRenderBuffer(0, blurringTarget); + } else { + // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so + if ((_blurredFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_blurredFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { + _blurredFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); + if (sourceFramebuffer->hasDepthStencil()) { + _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + } + } + + blurringResources.sourceTexture = sourceFramebuffer->getRenderBuffer(0); + blurringResources.blurringFramebuffer = _blurredFramebuffer; + blurringResources.blurringTexture = _blurredFramebuffer->getRenderBuffer(0); + blurringResources.finalFramebuffer = sourceFramebuffer; + + return true; +} + +void BlurGaussianDepthAware::configure(const Config& config) { + _parameters->setFilterRadiusScale(config.filterScale); +} + + +void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const InputPair& SourceAndDepth) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + + RenderArgs* args = renderContext->args; + + auto& sourceFramebuffer = SourceAndDepth.first.template get(); + auto& depthTexture = SourceAndDepth.first.template get(); + + BlurringResources blurringResources; + if (!updateBlurringResources(sourceFramebuffer, blurringResources)) { + // early exit if no valid blurring resources + return; + } + + auto blurVPipeline = getBlurVPipeline(); + auto blurHPipeline = getBlurHPipeline(); + + _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setViewportTransform(args->_viewport); + + batch.setUniformBuffer(BlurTask_ParamsSlot, _parameters->_parametersBuffer); + + batch.setFramebuffer(blurringResources.blurringFramebuffer); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + + batch.setPipeline(blurVPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.sourceTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setFramebuffer(blurringResources.finalFramebuffer); + batch.setPipeline(blurHPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.blurringTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setResourceTexture(BlurTask_SourceSlot, nullptr); + batch.setUniformBuffer(BlurTask_ParamsSlot, nullptr); + }); +} diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 50031dcfea..e5aea599bc 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -20,7 +20,7 @@ namespace render { class BlurParams { public: - void setWidthHeight(int width, int height); + void setWidthHeight(int width, int height, bool isStereo); void setFilterRadiusScale(float scale); @@ -33,6 +33,9 @@ public: // Filter info (radius scale glm::vec4 filterInfo{ 1.0f, 0.0f, 0.0f, 0.0f }; + // stereo info if blurring a stereo render + glm::vec4 stereoInfo{ 0.0f }; + Params() {} }; gpu::BufferView _parametersBuffer; @@ -86,6 +89,48 @@ protected: bool updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources); }; + +template < class T0, class T1 > +class VaryingPair : public std::pair { +public: + using Parent = std::pair; + + VaryingPair() : Parent(Varying(T0()), T1()) {} +}; + +class BlurGaussianDepthAware { +public: + using InputPair = VaryingPair; + using Config = BlurGaussianConfig; + using JobModel = Job::ModelI; + + BlurGaussianDepthAware(); + + void configure(const Config& config); + void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const InputPair& SourceAndDepth); + +protected: + + BlurParamsPointer _parameters; + + gpu::PipelinePointer _blurVPipeline; + gpu::PipelinePointer _blurHPipeline; + + gpu::PipelinePointer getBlurVPipeline(); + gpu::PipelinePointer getBlurHPipeline(); + + gpu::FramebufferPointer _blurredFramebuffer; + + struct BlurringResources { + gpu::TexturePointer sourceTexture; + gpu::FramebufferPointer blurringFramebuffer; + gpu::TexturePointer blurringTexture; + gpu::FramebufferPointer finalFramebuffer; + }; + bool updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources); +}; + + } #endif // hifi_render_DrawTask_h diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index 6073e641e9..686ab35ba1 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -24,6 +24,7 @@ const float gaussianDistributionOffset[NUM_TAPS] = float[]( struct BlurParameters { vec4 resolutionInfo; vec4 filterInfo; + vec4 stereoInfo; }; uniform blurParamsBuffer { diff --git a/libraries/render/src/render/blurGaussianDepthAwareH.slf b/libraries/render/src/render/blurGaussianDepthAwareH.slf new file mode 100644 index 0000000000..828a934776 --- /dev/null +++ b/libraries/render/src/render/blurGaussianDepthAwareH.slf @@ -0,0 +1,23 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/7/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include BlurTask.slh@> +<$declareBlurGaussian()$> + + +in vec2 varTexCoord0; + +out vec4 outFragColor; + +void main(void) { + outFragColor = pixelShaderGaussianDepthAware(varTexCoord0, vec2(1.0, 0.0), getViewportInvWidthHeight()); +} + diff --git a/libraries/render/src/render/blurGaussianDepthAwareV.slf b/libraries/render/src/render/blurGaussianDepthAwareV.slf new file mode 100644 index 0000000000..9c6467d61d --- /dev/null +++ b/libraries/render/src/render/blurGaussianDepthAwareV.slf @@ -0,0 +1,23 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/7/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include BlurTask.slh@> +<$declareBlurGaussian()$> + + +in vec2 varTexCoord0; + +out vec4 outFragColor; + +void main(void) { + outFragColor = pixelShaderGaussianDepthAware(varTexCoord0, vec2(0.0, 1.0), getViewportInvWidthHeight()); +} + From 358355cfbf1597e97f84d576a06724b0bec57173 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 14:55:03 +1200 Subject: [PATCH 0458/1237] Clear file selection and disabled Open button when change directory --- interface/resources/qml/dialogs/FileDialog.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 015a192185..fb50279ed6 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -215,6 +215,10 @@ ModalWindow { openButton.text = root.selectDirectory && row === -1 ? "Choose" : "Open" if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } return; } From 4ae018400d45283a9c1b27e6554277a13d888827 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 14:55:30 +1200 Subject: [PATCH 0459/1237] Give table focus after using dropdown --- interface/resources/qml/dialogs/FileDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index fb50279ed6..89c788a57c 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -187,6 +187,7 @@ ModalWindow { if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { fileTableModel.folder = folder; + fileTableView.forceActiveFocus(); } } } From 0ef13e6c01280b9e16d486b889330e272ac9c3fd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 14:56:11 +1200 Subject: [PATCH 0460/1237] Fix action button label to be "Save" when saving a file --- interface/resources/qml/dialogs/FileDialog.qml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 89c788a57c..b4558c306b 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -213,8 +213,6 @@ ModalWindow { function update() { var row = fileTableView.currentRow; - openButton.text = root.selectDirectory && row === -1 ? "Choose" : "Open" - if (row === -1) { if (!root.selectDirectory) { currentSelection.text = ""; @@ -657,7 +655,7 @@ ModalWindow { Action { id: okAction - text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open") + text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false onTriggered: { if (!root.selectDirectory && !d.currentSelectionIsFolder @@ -681,7 +679,6 @@ ModalWindow { return; } - // Handle the ambiguity between different cases // * typed name (with or without extension) // * full path vs relative vs filename only From 3780b18cc07efc1dc2696c9a64ce7999024abcef Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 15:14:14 +1200 Subject: [PATCH 0461/1237] Fix a QML error --- interface/resources/qml/controls-uit/ComboBox.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index cd6dc8ede0..df3210a20d 100755 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -199,7 +199,7 @@ FocusScope { anchors.leftMargin: hifi.dimensions.textPadding anchors.verticalCenter: parent.verticalCenter id: popupText - text: listView.model[index] + text: listView.model[index] ? listView.model[index] : "" size: hifi.fontSizes.textFieldInput color: hifi.colors.baseGray } From 0fe93a1e89fdd1638549f699ed23fc41a167f675 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 16:12:11 +1200 Subject: [PATCH 0462/1237] Fix capitalization of import and export dialog titles --- scripts/system/edit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index afbc679ec4..1232c8d94d 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1222,7 +1222,7 @@ function handeMenuEvent(menuItem) { Window.alert("No entities have been selected."); } else { var filename = "entities__" + Window.location.hostname + ".svo.json"; - filename = Window.save("Select where to save", filename, "*.json") + filename = Window.save("Select Where to Save", filename, "*.json") if (filename) { var success = Clipboard.exportEntities(filename, selectionManager.selections); if (!success) { @@ -1234,7 +1234,7 @@ function handeMenuEvent(menuItem) { var importURL = null; if (menuItem == "Import Entities") { - var fullPath = Window.browse("Select models to import", "", "*.json"); + var fullPath = Window.browse("Select Model to Import", "", "*.json"); if (fullPath) { importURL = "file:///" + fullPath; } From b4543cc579001968114f4d1bddec712452737853 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 16:29:15 +1200 Subject: [PATCH 0463/1237] Unhighlight directory in table when type filename to save --- interface/resources/qml/dialogs/FileDialog.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index b4558c306b..dc1d7ab340 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -610,6 +610,12 @@ ModalWindow { readOnly: !root.saveDialog activeFocusOnTab: !readOnly onActiveFocusChanged: if (activeFocus) { selectAll(); } + onTextChanged: { + if (root.saveDialog && text !== "") { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + } + } onAccepted: okAction.trigger(); } From d346d4c7310fb3dbdc1587f72adb7165e09d5e82 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 16:41:51 +1200 Subject: [PATCH 0464/1237] When choosing a directory make the dropdown update the path value --- interface/resources/qml/dialogs/FileDialog.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index dc1d7ab340..eee52161b6 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -186,6 +186,9 @@ ModalWindow { } if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + if (root.selectDirectory) { + currentSelection.text = currentText !== "This PC" ? currentText : ""; + } fileTableModel.folder = folder; fileTableView.forceActiveFocus(); } From 4176940e248bab158d2585d370d8d67b7d518521 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 17:02:43 +1200 Subject: [PATCH 0465/1237] Don't highlight first drive in table when choose "This PC" from dropdown --- interface/resources/qml/dialogs/FileDialog.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index eee52161b6..5f8d8c7bbb 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -451,12 +451,6 @@ ModalWindow { onSortIndicatorOrderChanged: { updateSort(); } - onActiveFocusChanged: { - if (activeFocus && currentRow == -1) { - fileTableView.selection.select(0) - } - } - itemDelegate: Item { clip: true From cf6945b40363a1879c0e27b6cf34eb968fbda1fb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 17:11:00 +1200 Subject: [PATCH 0466/1237] Fix selecting directory directly from dropdown --- interface/resources/qml/dialogs/FileDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 5f8d8c7bbb..93ccbc0b8c 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -188,6 +188,7 @@ ModalWindow { if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { if (root.selectDirectory) { currentSelection.text = currentText !== "This PC" ? currentText : ""; + d.currentSelectionUrl = helper.pathToUrl(currentText); } fileTableModel.folder = folder; fileTableView.forceActiveFocus(); From 5352bd4b76bc711d999897a47589632147d868a5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 9 Jun 2016 17:15:17 +1200 Subject: [PATCH 0467/1237] Fix height of Load Defaults button --- interface/resources/qml/hifi/dialogs/RunningScripts.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index cbbbec5bff..94b8c1905f 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -261,6 +261,7 @@ Window { HifiControls.Button { text: "Load Defaults" color: hifi.buttons.black + height: 26 onClicked: loadDefaults() } } From ff8d4883b10eafd167d96f6c63b9817bc848a96e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 8 Jun 2016 14:56:18 -0700 Subject: [PATCH 0468/1237] Attempt to get better logging from pure virtual call crashes --- interface/src/CrashReporter.cpp | 44 +++++++++++++++++++++-- interface/src/FileLogger.cpp | 4 +++ interface/src/FileLogger.h | 1 + libraries/shared/src/GenericQueueThread.h | 27 ++++++++++++-- 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/interface/src/CrashReporter.cpp b/interface/src/CrashReporter.cpp index 1fd534ffac..d683c5e4df 100644 --- a/interface/src/CrashReporter.cpp +++ b/interface/src/CrashReporter.cpp @@ -10,15 +10,20 @@ // -#ifdef HAS_BUGSPLAT #include "CrashReporter.h" +#ifdef _WIN32 +#include #include #include +#include #include - #include +#include "Application.h" + + +#pragma comment(lib, "Dbghelp.lib") // SetUnhandledExceptionFilter can be overridden by the CRT at the point that an error occurs. More information // can be found here: http://www.codeproject.com/Articles/154686/SetUnhandledExceptionFilter-and-the-C-C-Runtime-Li @@ -77,13 +82,43 @@ BOOL redirectLibraryFunctionToFunction(char* library, char* function, void* fn) return bRet; } +void printStackTrace(ULONG framesToSkip = 1) { + QString result; + unsigned int i; + void * stack[100]; + unsigned short frames; + SYMBOL_INFO * symbol; + HANDLE process; + + process = GetCurrentProcess(); + SymInitialize(process, NULL, TRUE); + frames = CaptureStackBackTrace(framesToSkip, 100, stack, NULL); + symbol = (SYMBOL_INFO *)calloc(sizeof(SYMBOL_INFO) + 256 * sizeof(char), 1); + symbol->MaxNameLen = 255; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + + for (i = 0; i < frames; i++) { + SymFromAddr(process, (DWORD64)(stack[i]), 0, symbol); + qWarning() << QString("%1: %2 - 0x%0X").arg(QString::number(frames - i - 1), QString(symbol->Name), QString::number(symbol->Address, 16)); + } + + free(symbol); + + // Try to force the log to sync to the filesystem + auto app = qApp; + if (app && app->getLogger()) { + app->getLogger()->sync(); + } +} void handleSignal(int signal) { // Throw so BugSplat can handle throw(signal); } -void handlePureVirtualCall() { +void __cdecl handlePureVirtualCall() { + qWarning() << "Pure virtual function call detected"; + printStackTrace(2); // Throw so BugSplat can handle throw("ERROR: Pure virtual call"); } @@ -107,6 +142,8 @@ _purecall_handler __cdecl noop_set_purecall_handler(_purecall_handler pNew) { return nullptr; } +#ifdef HAS_BUGSPLAT + static const DWORD BUG_SPLAT_FLAGS = MDSF_PREVENTHIJACKING | MDSF_USEGUARDMEMORY; CrashReporter::CrashReporter(QString bugSplatDatabase, QString bugSplatApplicationName, QString version) @@ -133,3 +170,4 @@ CrashReporter::CrashReporter(QString bugSplatDatabase, QString bugSplatApplicati } } #endif +#endif \ No newline at end of file diff --git a/interface/src/FileLogger.cpp b/interface/src/FileLogger.cpp index f5f7ce6ebe..50f7d15d6b 100644 --- a/interface/src/FileLogger.cpp +++ b/interface/src/FileLogger.cpp @@ -115,3 +115,7 @@ QString FileLogger::getLogData() { } return result; } + +void FileLogger::sync() { + _persistThreadInstance->waitIdle(); +} diff --git a/interface/src/FileLogger.h b/interface/src/FileLogger.h index e9bae63a73..28ac6fba40 100644 --- a/interface/src/FileLogger.h +++ b/interface/src/FileLogger.h @@ -28,6 +28,7 @@ public: virtual void addMessage(const QString&) override; virtual QString getLogData() override; virtual void locateLog() override; + void sync(); signals: void rollingLogFile(QString newFilename); diff --git a/libraries/shared/src/GenericQueueThread.h b/libraries/shared/src/GenericQueueThread.h index 28fcdb0ed6..7c1bdccaa7 100644 --- a/libraries/shared/src/GenericQueueThread.h +++ b/libraries/shared/src/GenericQueueThread.h @@ -12,9 +12,10 @@ #include -#include -#include -#include +#include +#include +#include +#include #include "GenericThread.h" #include "NumericalConstants.h" @@ -35,6 +36,25 @@ public: _hasItems.wakeAll(); } + void waitIdle(uint32_t maxWaitMs = UINT32_MAX) { + QElapsedTimer timer; + timer.start(); + + // FIXME this will work as long as the thread doing the wait + // is the only thread which can add work to the queue. + // It would be better if instead we had a queue empty condition to wait on + // that would ensure that we get woken as soon as we're idle the very + // first time the queue was empty. + while (timer.elapsed() < maxWaitMs) { + lock(); + if (!_items.size()) { + unlock(); + return; + } + unlock(); + } + } + protected: virtual void queueItemInternal(const T& t) { _items.push_back(t); @@ -44,6 +64,7 @@ protected: return MSECS_PER_SECOND; } + virtual bool process() { lock(); if (!_items.size()) { From 7bd686167e2981613b5c6ff98c48018f6b94ffee Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 9 Jun 2016 09:33:03 -0700 Subject: [PATCH 0469/1237] Adding the generation of the Scattering on GPU --- .../render-utils/src/SubsurfaceScattering.cpp | 138 ++++++++++++------ .../render-utils/src/SubsurfaceScattering.h | 2 +- .../src/subsurfaceScattering_makeLUT.slf | 30 ++++ 3 files changed, 127 insertions(+), 43 deletions(-) create mode 100644 libraries/render-utils/src/subsurfaceScattering_makeLUT.slf diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index cfcf444a2f..89cb80b285 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -15,6 +15,8 @@ #include "FramebufferCache.h" +#include "subsurfaceScattering_makeLUT_frag.h" + const int SubsurfaceScattering_FrameTransformSlot = 0; const int SubsurfaceScattering_ParamsSlot = 1; const int SubsurfaceScattering_DepthMapSlot = 0; @@ -64,10 +66,10 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, RenderArgs* args = renderContext->args; if (!_scatteringTable) { - _scatteringTable = SubsurfaceScattering::generatePreIntegratedScattering(); + _scatteringTable = SubsurfaceScattering::generatePreIntegratedScattering(args); } - auto& pipeline = getScatteringPipeline(); + auto pipeline = getScatteringPipeline(); @@ -101,8 +103,6 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, #include #define _PI 3.14159265358979523846 -#define HEIGHT 512 -#define WIDTH 512 using namespace std; @@ -124,7 +124,7 @@ vec3 scatter(double r) { { 7.4100, 0.078, 0.000, 0.000 } }; static const int profileNum = 6; - vec3 ret; ret.x = 0.0; ret.y = 0.0; ret.z = 0.0; + vec3 ret(0.0); for (int i = 0; i < profileNum; i++) { double g = gaussian(profile[i][0] * 1.414f, r); ret.x += g * profile[i][1]; @@ -138,14 +138,15 @@ vec3 scatter(double r) { vec3 integrate(double cosTheta, double skinRadius) { // Angle from lighting direction. double theta = acos(cosTheta); - vec3 totalWeights; totalWeights.x = 0.0; totalWeights.y = 0.0; totalWeights.z = 0.0; - vec3 totalLight; totalLight.x = 0.0; totalLight.y = 0.0; totalLight.z = 0.0; - vec3 skinColour; skinColour.x = 1.0; skinColour.y = 1.0; skinColour.z = 1.0; + vec3 totalWeights(0.0); + vec3 totalLight(0.0); + vec3 skinColour(1.0); double a = -(_PI); - //const double inc = 0.001; - const double inc = 0.01; + double inc = 0.001; + if (cosTheta > 0) + inc = 0.01; while (a <= (_PI)) { double sampleAngle = theta + a; @@ -159,9 +160,7 @@ vec3 integrate(double cosTheta, double skinRadius) { // Profile Weight. vec3 weights = scatter(sampleDist); - totalWeights.x += weights.x; - totalWeights.y += weights.y; - totalWeights.z += weights.z; + totalWeights += weights; totalLight.x += diffuse * weights.x * (skinColour.x * skinColour.x); totalLight.y += diffuse * weights.y * (skinColour.y * skinColour.y); totalLight.z += diffuse * weights.z * (skinColour.z * skinColour.z); @@ -177,11 +176,18 @@ vec3 integrate(double cosTheta, double skinRadius) { } void diffuseScatter(gpu::TexturePointer& lut) { - for (int j = 0; j < HEIGHT; j++) { - for (int i = 0; i < WIDTH; i++) { + int width = lut->getWidth(); + int height = lut->getHeight(); + + const int COMPONENT_COUNT = 4; + std::vector bytes(COMPONENT_COUNT * height * width); + + int index = 0; + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { // Lookup by: x: NDotL y: 1 / r - float y = 2.0 * 1.0 / ((j + 1.0) / (double)HEIGHT); - float x = ((i / (double)WIDTH) * 2.0) - 1.0; + float y = 2.0 * 1.0 / ((j + 1.0) / (double)height); + float x = ((i / (double)width) * 2.0) - 1.0; vec3 val = integrate(x, y); // Convert to linear @@ -197,24 +203,73 @@ void diffuseScatter(gpu::TexturePointer& lut) { valI[0] = (unsigned char)(val.x * 256.0); valI[1] = (unsigned char)(val.y * 256.0); valI[2] = (unsigned char)(val.z * 256.0); - //printf("%u %u %u\n", valI[0], valI[1], valI[2]); - - // Write to file. - // fwrite(valI, sizeof(unsigned char), 3, output_file); + + bytes[COMPONENT_COUNT * index] = valI[0]; + bytes[COMPONENT_COUNT * index + 1] = valI[1]; + bytes[COMPONENT_COUNT * index + 2] = valI[2]; + bytes[COMPONENT_COUNT * index + 3] = 255.0; + + index++; } - - printf("%.2lf%% Done...\n", (j / (float)HEIGHT) * 100.0f); } + + lut->assignStoredMip(0, gpu::Element::COLOR_RGBA_32, bytes.size(), bytes.data()); +} + + +void diffuseScatterGPU(gpu::TexturePointer& profileMap, gpu::TexturePointer& lut, RenderArgs* args) { + int width = lut->getWidth(); + int height = lut->getHeight(); + + gpu::PipelinePointer makePipeline; + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_makeLUT_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("profileMap"), 0)); + // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + makePipeline = gpu::Pipeline::create(program, state); + } + + auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + makeFramebuffer->setRenderBuffer(0, lut); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + + batch.setViewportTransform(glm::ivec4(0, 0, width, height)); + + batch.setFramebuffer(makeFramebuffer); + batch.setPipeline(makePipeline); + batch.setResourceTexture(0, profileMap); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(0, nullptr); + batch.setPipeline(nullptr); + batch.setFramebuffer(nullptr); + + }); } void diffuseProfile(gpu::TexturePointer& profile) { - std::vector bytes(3 * HEIGHT * WIDTH); - int size = sizeof(unsigned char) * bytes.size(); + int width = profile->getWidth(); + int height = profile->getHeight(); + + const int COMPONENT_COUNT = 4; + std::vector bytes(COMPONENT_COUNT * height * width); int index = 0; - for (int j = 0; j < HEIGHT; j++) { - for (int i = 0; i < WIDTH; i++) { - float y = (double)(j + 1.0) / (double)HEIGHT; + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + float y = (double)(i + 1.0) / (double)width; vec3 val = scatter(y * 2.0f); // Convert to 24-bit image. @@ -226,32 +281,31 @@ void diffuseProfile(gpu::TexturePointer& profile) { valI[1] = (unsigned char)(val.y * 255.0); valI[2] = (unsigned char)(val.z * 255.0); - bytes[3 * index] = valI[0]; - bytes[3 * index + 1] = valI[1]; - bytes[3 * index + 2] = valI[2]; - - // Write to file. - // fwrite(valI, sizeof(unsigned char), 3, output_file); + bytes[COMPONENT_COUNT * index] = valI[0]; + bytes[COMPONENT_COUNT * index + 1] = valI[1]; + bytes[COMPONENT_COUNT * index + 2] = valI[2]; + bytes[COMPONENT_COUNT * index + 3] = 255.0; index++; } } - profile->assignStoredMip(0, profile->getTexelFormat(), size, bytes.data()); + profile->assignStoredMip(0, gpu::Element::COLOR_RGBA_32, bytes.size(), bytes.data()); } -/*int main() { - diffuseScatter(); - //diffuseProfile(); -}*/ +gpu::TexturePointer SubsurfaceScattering::generatePreIntegratedScattering(RenderArgs* args) { + + auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 128, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + diffuseProfile(profileMap); -gpu::TexturePointer SubsurfaceScattering::generatePreIntegratedScattering() { + const int WIDTH = 128; + const int HEIGHT = 128; auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT)); - //diffuseScatter(scatteringLUT); - diffuseProfile(scatteringLUT); + // diffuseScatter(scatteringLUT); + diffuseScatterGPU(profileMap, scatteringLUT, args); return scatteringLUT; } diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 593acd0d51..85e57a4328 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -42,7 +42,7 @@ public: float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } - static gpu::TexturePointer generatePreIntegratedScattering(); + static gpu::TexturePointer generatePreIntegratedScattering(RenderArgs* args); private: typedef gpu::BufferView UniformBufferView; diff --git a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf new file mode 100644 index 0000000000..577fffba9a --- /dev/null +++ b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf @@ -0,0 +1,30 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/8/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#define _PI 3.14159265358979523846 + + +uniform sampler2D profileMap; + +vec3 scatter(float r) { + return texture(profileMap, vec2(r * 0.5, 0.5)).xyz; +} + + +in vec2 varTexCoord0; +out vec4 outFragColor; + +void main(void) { + + outFragColor = vec4(scatter(varTexCoord0.x * 2.0), 1.0); +} + From 1aae22f5a59246e795801aa09ecf07bd93ee50af Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 9 Jun 2016 09:35:19 -0700 Subject: [PATCH 0470/1237] Optimized MyAvatar.getCharacterControllerEnabled() Instead of doing a blocking queued invokeMethod, it just calls into CharacterController.isEnabled() which is now thread-safe. --- interface/src/avatar/MyAvatar.cpp | 11 +++-------- libraries/physics/src/CharacterController.h | 6 ++++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3495a05962..d5c481164c 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1253,13 +1253,13 @@ void MyAvatar::prepareForPhysicsSimulation() { void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaTime) { glm::vec3 position = getPosition(); glm::quat orientation = getOrientation(); - if (_characterController.isEnabled()) { + if (_characterController.isEnabledAndReady()) { _characterController.getPositionAndOrientation(position, orientation); } nextAttitude(position, orientation); _bodySensorMatrix = _follow.postPhysicsUpdate(*this, _bodySensorMatrix); - if (_characterController.isEnabled()) { + if (_characterController.isEnabledAndReady()) { setVelocity(_characterController.getLinearVelocity() + _characterController.getFollowVelocity()); } else { setVelocity(getVelocity() + _characterController.getFollowVelocity()); @@ -1642,7 +1642,7 @@ void MyAvatar::updatePosition(float deltaTime) { vec3 velocity = getVelocity(); const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s - if (!_characterController.isEnabled()) { + if (!_characterController.isEnabledAndReady()) { // _characterController is not in physics simulation but it can still compute its target velocity updateMotors(); _characterController.computeNewVelocity(deltaTime, velocity); @@ -1838,11 +1838,6 @@ void MyAvatar::setCharacterControllerEnabled(bool enabled) { } bool MyAvatar::getCharacterControllerEnabled() { - if (QThread::currentThread() != thread()) { - bool result; - QMetaObject::invokeMethod(this, "getCharacterControllerEnabled", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); - return result; - } return _characterController.isEnabled(); } diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index 2191f46d55..586ea175e6 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -105,8 +106,9 @@ public: void setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale); + bool isEnabled() const { return _enabled; } // thread-safe void setEnabled(bool enabled); - bool isEnabled() const { return _enabled && _dynamicsWorld; } + bool isEnabledAndReady() const { return _enabled && _dynamicsWorld; } bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); @@ -167,7 +169,7 @@ protected: btQuaternion _followAngularDisplacement; btVector3 _linearAcceleration; - bool _enabled; + std::atomic_bool _enabled; State _state; bool _isPushingUp; From eee1ba09062216b0032110f5b1e6583a3ee4510c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 9 Jun 2016 10:19:38 -0700 Subject: [PATCH 0471/1237] trying to get buildability --- interface/src/CrashReporter.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/interface/src/CrashReporter.cpp b/interface/src/CrashReporter.cpp index d683c5e4df..fce472e311 100644 --- a/interface/src/CrashReporter.cpp +++ b/interface/src/CrashReporter.cpp @@ -10,17 +10,16 @@ // +#include "Application.h" #include "CrashReporter.h" #ifdef _WIN32 -#include #include #include #include #include #include -#include "Application.h" #pragma comment(lib, "Dbghelp.lib") @@ -170,4 +169,4 @@ CrashReporter::CrashReporter(QString bugSplatDatabase, QString bugSplatApplicati } } #endif -#endif \ No newline at end of file +#endif From bade215907c4f01e5334e8c885cd245c94f43660 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 9 Jun 2016 10:41:30 -0700 Subject: [PATCH 0472/1237] maximum 'tmp' entity lifetime is now a domain-server setting, defaults to 1 hour --- assignment-client/src/entities/EntityServer.cpp | 8 ++++++++ domain-server/resources/describe-settings.json | 8 ++++++++ libraries/entities/src/EntityTree.cpp | 6 +++--- libraries/entities/src/EntityTree.h | 8 ++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 0555f95c65..7594d5dd2c 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -268,6 +268,14 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio qDebug("wantTerseEditLogging=%s", debug::valueOf(wantTerseEditLogging)); EntityTreePointer tree = std::static_pointer_cast(_tree); + + int maxTmpEntityLifetime; + if (readOptionInt("maxTmpLifetime", settingsSectionObject, maxTmpEntityLifetime)) { + tree->setEntityMaxTmpLifetime(maxTmpEntityLifetime); + } else { + tree->setEntityMaxTmpLifetime(EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME); + } + tree->setWantEditLogging(wantEditLogging); tree->setWantTerseEditLogging(wantTerseEditLogging); } diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 43eb5c7ee9..6d15cf75ed 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -513,6 +513,14 @@ "label": "Entity Server Settings", "assignment-types": [6], "settings": [ + { + "name": "maxTmpLifetime", + "label": "Maximum Lifetime of Temporary Entities", + "help": "The maximum number of seconds for the lifetime of an entity which will be considered \"temporary\".", + "placeholder": "3600", + "default": "3600", + "advanced": true + }, { "name": "persistFilePath", "label": "Entities File Path", diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 911ebd88b3..5892ac0e54 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -26,7 +26,7 @@ #include "LogHandler.h" static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; -static const float MAX_TMP_ENTITY_LIFETIME = 10 * 60; // 10 minutes +const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour EntityTree::EntityTree(bool shouldReaverage) : @@ -319,10 +319,10 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return true; } -bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) { +bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) { float lifeTime = properties.getLifetime(); - if (lifeTime == 0.0f || lifeTime > MAX_TMP_ENTITY_LIFETIME) { + if (lifeTime == 0.0f || lifeTime > _maxTmpEntityLifetime) { // this is an attempt to rez a permanent entity. if (!canRez) { return false; diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index a85624c9ae..8afb8d878f 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -62,6 +62,10 @@ public: void createRootElement(); + + void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; } + bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp); + /// Implements our type specific root element factory virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override; @@ -252,6 +256,8 @@ public: void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID); + static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; + public slots: void callLoader(EntityItemID entityID); @@ -331,6 +337,8 @@ protected: // we maintain a list of avatarIDs to notice when an entity is a child of one. QSet _avatarIDs; // IDs of avatars connected to entity server QHash> _childrenOfAvatars; // which entities are children of which avatars + + float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME }; }; #endif // hifi_EntityTree_h From 6e3057479ffacde155330039d249b242210adb92 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 9 Jun 2016 11:27:02 -0700 Subject: [PATCH 0473/1237] fixes for warnings Also overlays menu option should work better in conjunction with ui-center/fading --- interface/src/ui/OverlayConductor.cpp | 4 +--- interface/src/ui/OverlayConductor.h | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index c4fd938e97..9e2e8cb0db 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -71,9 +71,7 @@ void OverlayConductor::update(float dt) { } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { _timeInPotentialMode = 0; // a real transition bool wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); - if (wantsOverlays) { - setEnabled(!nowDriving); - } + setEnabled(!nowDriving && wantsOverlays); _driving = nowDriving; } diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 4ecbac5bcf..2e425454c7 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -34,7 +34,6 @@ private: bool _enabled { false }; bool _driving { false }; quint64 _timeInPotentialMode { 0 }; - bool _wantsOverlays { true }; }; #endif From cd7c6be5906019c64a25876b33fd460f7e9eeefa Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 9 Jun 2016 12:25:21 -0700 Subject: [PATCH 0474/1237] Fix formatting --- interface/src/CrashReporter.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/interface/src/CrashReporter.cpp b/interface/src/CrashReporter.cpp index fce472e311..596c34ca92 100644 --- a/interface/src/CrashReporter.cpp +++ b/interface/src/CrashReporter.cpp @@ -82,21 +82,15 @@ BOOL redirectLibraryFunctionToFunction(char* library, char* function, void* fn) } void printStackTrace(ULONG framesToSkip = 1) { - QString result; - unsigned int i; - void * stack[100]; - unsigned short frames; - SYMBOL_INFO * symbol; - HANDLE process; - - process = GetCurrentProcess(); + HANDLE process = GetCurrentProcess(); SymInitialize(process, NULL, TRUE); - frames = CaptureStackBackTrace(framesToSkip, 100, stack, NULL); - symbol = (SYMBOL_INFO *)calloc(sizeof(SYMBOL_INFO) + 256 * sizeof(char), 1); + void* stack[100]; + uint16_t frames = CaptureStackBackTrace(framesToSkip, 100, stack, NULL); + SYMBOL_INFO* symbol = (SYMBOL_INFO *)calloc(sizeof(SYMBOL_INFO) + 256 * sizeof(char), 1); symbol->MaxNameLen = 255; symbol->SizeOfStruct = sizeof(SYMBOL_INFO); - for (i = 0; i < frames; i++) { + for (uint16_t i = 0; i < frames; ++i) { SymFromAddr(process, (DWORD64)(stack[i]), 0, symbol); qWarning() << QString("%1: %2 - 0x%0X").arg(QString::number(frames - i - 1), QString(symbol->Name), QString::number(symbol->Address, 16)); } From 786c74d7f94834ea5b6fcb4fdacec2bc3c8d7001 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 10 Jun 2016 08:57:52 +1200 Subject: [PATCH 0475/1237] Vertically center table headings, reduce font size in subheadings --- domain-server/resources/web/css/style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 4c419a1baa..177fa42fef 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -124,6 +124,15 @@ caption { padding-top: 0; } +table > tbody > .headers > td { + vertical-align: middle; +} + +table .headers + .headers td { + font-size: 13px; + color: #222; +} + table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td { text-align: center; } From 5650ef9d52b46d486639731747eb2acb154dcfba Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 9 Jun 2016 14:35:17 -0700 Subject: [PATCH 0476/1237] have code where physics guesses at server values also avoid doing simple simulation of children of avatars --- libraries/entities/src/EntitySimulation.cpp | 2 +- libraries/physics/src/EntityMotionState.cpp | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index 8419e4ac6a..a29ea8e2c8 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -263,7 +263,7 @@ void EntitySimulation::moveSimpleKinematics(const quint64& now) { EntityItemPointer entity = *itemItr; // The entity-server doesn't know where avatars are, so don't attempt to do simple extrapolation for - // children of avatars. + // children of avatars. See related code in EntityMotionState::remoteSimulationOutOfSync. bool ancestryIsKnown; entity->getMaximumAACube(ancestryIsKnown); bool hasAvatarAncestor = entity->hasAncestorOfType(NestableType::Avatar); diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 053bfcbd85..be7862ade3 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -317,9 +317,19 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { if (glm::length2(_serverVelocity) > 0.0f) { _serverVelocity += _serverAcceleration * dt; _serverVelocity *= powf(1.0f - _body->getLinearDamping(), dt); - // NOTE: we ignore the second-order acceleration term when integrating - // the position forward because Bullet also does this. - _serverPosition += dt * _serverVelocity; + + // the entity-server doesn't know where avatars are, so it doesn't do simple extrapolation for children of + // avatars. We are trying to guess what values the entity server has, so we don't do it here, either. See + // related code in EntitySimulation::moveSimpleKinematics. + bool ancestryIsKnown; + _entity->getMaximumAACube(ancestryIsKnown); + bool hasAvatarAncestor = _entity->hasAncestorOfType(NestableType::Avatar); + + if (ancestryIsKnown && !hasAvatarAncestor) { + // NOTE: we ignore the second-order acceleration term when integrating + // the position forward because Bullet also does this. + _serverPosition += dt * _serverVelocity; + } } if (_entity->actionDataNeedsTransmit()) { From 0b873365edad54c9dd0c94673f1d5a8eb09208c1 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 9 Jun 2016 14:35:44 -0700 Subject: [PATCH 0477/1237] save current state --- .../render-utils/src/DebugDeferredBuffer.cpp | 1 + .../render-utils/src/SubsurfaceScattering.cpp | 6 ++-- .../src/subsurfaceScattering_makeLUT.slf | 33 ++++++++++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 2fbdc92b22..09b0e04f4c 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -301,6 +301,7 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); + batch.setViewportTransform(args->_viewport); const auto geometryBuffer = DependencyManager::get(); const auto framebufferCache = DependencyManager::get(); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 89cb80b285..b0d0e9a38e 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -144,9 +144,7 @@ vec3 integrate(double cosTheta, double skinRadius) { double a = -(_PI); - double inc = 0.001; - if (cosTheta > 0) - inc = 0.01; + double inc = 0.1; while (a <= (_PI)) { double sampleAngle = theta + a; @@ -304,7 +302,7 @@ gpu::TexturePointer SubsurfaceScattering::generatePreIntegratedScattering(Render const int WIDTH = 128; const int HEIGHT = 128; auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT)); - // diffuseScatter(scatteringLUT); + // diffuseScatter(scatteringLUT); diffuseScatterGPU(profileMap, scatteringLUT, args); return scatteringLUT; } diff --git a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf index 577fffba9a..e008c52385 100644 --- a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf +++ b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf @@ -20,11 +20,42 @@ vec3 scatter(float r) { } +vec3 integrate(float cosTheta, float skinRadius) { + // Angle from lighting direction. + float theta = acos(cosTheta); + vec3 totalWeights = vec3(0.0); + vec3 totalLight= vec3(0.0); + vec3 skinColour = vec3(1.0); + + float a = -(_PI); + + float inc = 0.1; + + while (a <= (_PI)) { + float sampleAngle = theta + a; + float diffuse = clamp(cos(sampleAngle), 0.0, 1.0); + + // Distance. + float sampleDist = abs(2.0 * skinRadius * sin(a * 0.5)); + + // Profile Weight. + vec3 weights = scatter(sampleDist); + + totalWeights += weights; + totalLight += diffuse * weights /** (skinColour * skinColour)*/; + a += inc; + } + + vec3 result = sqrt(totalLight / totalWeights); + + return min(result, vec3(1.0)); +} + in vec2 varTexCoord0; out vec4 outFragColor; void main(void) { - outFragColor = vec4(scatter(varTexCoord0.x * 2.0), 1.0); + outFragColor = vec4(integrate(varTexCoord0.x * 2.0 - 1, 2.0 * varTexCoord0.y), 1.0); } From d7079fce8c78dc51af64f9f6646b7d2473cef00f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 9 Jun 2016 15:07:24 -0700 Subject: [PATCH 0478/1237] Use shared CPUDetect.h for CPU detection --- libraries/audio/src/AudioHRTF.cpp | 53 ++----------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index 7fadf073a1..89c929011a 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -119,61 +119,12 @@ static void FIR_1x4_SSE(float* src, float* dst0, float* dst1, float* dst2, float } } -// -// Detect AVX/AVX2 support -// - -#if defined(_MSC_VER) - -#include - -static bool cpuSupportsAVX() { - int info[4]; - int mask = (1 << 27) | (1 << 28); // OSXSAVE and AVX - - __cpuidex(info, 0x1, 0); - - bool result = false; - if ((info[2] & mask) == mask) { - - if ((_xgetbv(_XCR_XFEATURE_ENABLED_MASK) & 0x6) == 0x6) { - result = true; - } - } - return result; -} - -#elif defined(__GNUC__) - -#include - -static bool cpuSupportsAVX() { - unsigned int eax, ebx, ecx, edx; - unsigned int mask = (1 << 27) | (1 << 28); // OSXSAVE and AVX - - bool result = false; - if (__get_cpuid(0x1, &eax, &ebx, &ecx, &edx) && ((ecx & mask) == mask)) { - - __asm__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(0)); - if ((eax & 0x6) == 0x6) { - result = true; - } - } - return result; -} - -#else - -static bool cpuSupportsAVX() { - return false; -} - -#endif - // // Runtime CPU dispatch // +#include "CPUDetect.h" + typedef void FIR_1x4_t(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames); FIR_1x4_t FIR_1x4_AVX; // separate compilation with VEX-encoding enabled From d54247da8d2f8a28cfd05b6a8b30f70e4a1e186c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 10 Jun 2016 10:13:54 +1200 Subject: [PATCH 0479/1237] Format hover popups --- .../resources/describe-settings.json | 6 ++-- domain-server/resources/web/css/style.css | 30 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 1c55ea3859..bad24dd3a1 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -170,7 +170,7 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which users or groups can have which domain-wide permissions.", + "help": "Indicate which users or groups can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, @@ -180,7 +180,7 @@ "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 6 } ], @@ -249,7 +249,7 @@ "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 6 } ], diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 177fa42fef..2862feed87 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -137,9 +137,37 @@ table[name="security.standard_permissions"] .headers td + td, table[name="securi text-align: center; } +.tooltip.top .tooltip-arrow { + border-top-color: #fff; + border-width: 10px 10px 0; + margin-bottom: -5px; +} + +.tooltip-inner { + padding: 20px 20px 10px 20px; + font-size: 14px; + text-align: left; + color: #333; + background-color: #fff; + box-shadow: 0 3px 8px 8px #e8e8e8; +} + +.tooltip.in { + opacity: 1; +} + +.tooltip-inner ul { + padding-left: 0; + margin-bottom: 15px; +} + +.tooltip-inner li { + list-style-type: none; + margin-bottom: 5px; +} + #security .tooltip-inner { max-width: 520px; - text-align: left; } #xs-advanced-container { From 1e9e2641247bd4d3da1639ec99662a1627e615fd Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 6 Jun 2016 10:50:36 -0700 Subject: [PATCH 0480/1237] Revert "Revert "refresh API info during re-connect - case 570"" This reverts commit b8c80e222286a16ef56a5ecfcf7c95aa04a9918e. --- libraries/networking/src/AddressManager.cpp | 44 ++++++++++++++++++--- libraries/networking/src/AddressManager.h | 9 ++++- libraries/networking/src/DomainHandler.cpp | 18 +++------ libraries/networking/src/DomainHandler.h | 10 ++--- libraries/networking/src/NodeList.cpp | 14 +++++-- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 1b7ed11cce..9a3d147ead 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -144,12 +144,21 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // 4. domain network address (IP or dns resolvable hostname) // use our regex'ed helpers to figure out what we're supposed to do with this - if (!handleUsername(lookupUrl.authority())) { + if (handleUsername(lookupUrl.authority())) { + // handled a username for lookup + + // in case we're failing to connect to where we thought this user was + // store their username as previous lookup so we can refresh their location via API + _previousLookup = lookupUrl; + } else { // we're assuming this is either a network address or global place name // check if it is a network address first bool hostChanged; if (handleNetworkAddress(lookupUrl.host() - + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + // a network address lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); // If the host changed then we have already saved to history if (hostChanged) { @@ -165,10 +174,16 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info + _previousLookup = lookupUrl; + // no place name - this is probably a domain ID // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + // store this place name as the previous lookup in case we fail to connect and want to refresh API info + _previousLookup = lookupUrl; + // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger); @@ -180,9 +195,13 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); + // a path lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); + // if this is a relative path then handle it as a relative viewpoint handlePath(lookupUrl.path(), trigger, true); emit lookupResultsFinished(); + return true; } @@ -276,7 +295,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; - emit possibleDomainChangeRequired(domainHostname, domainPort); + emit possibleDomainChangeRequired(domainHostname, domainPort, domainID); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); @@ -315,7 +334,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty()) { - handlePath(overridePath, trigger); + // make sure we don't re-handle an overriden path if this was a refresh of info from API + if (trigger != LookupTrigger::AttemptedRefresh) { + handlePath(overridePath, trigger); + } } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; @@ -598,7 +620,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress); - emit possibleDomainChangeRequired(hostname, port); + emit possibleDomainChangeRequired(hostname, port, QUuid()); return hostChanged; } @@ -618,6 +640,13 @@ void AddressManager::goToUser(const QString& username) { QByteArray(), nullptr, requestParams); } +void AddressManager::refreshPreviousLookup() { + // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history) + if (!_previousLookup.isEmpty()) { + handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh); + } +} + void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } @@ -629,7 +658,10 @@ void AddressManager::copyPath() { void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { // if we're cold starting and this is called for the first address (from settings) we don't do anything - if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) { + if (trigger != LookupTrigger::StartupFromSettings + && trigger != LookupTrigger::DomainPathResponse + && trigger != LookupTrigger::AttemptedRefresh) { + if (trigger == LookupTrigger::Back) { // we're about to push to the forward stack // if it's currently empty emit our signal to say that going forward is now possible diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 643924ff5c..a3aaee3ba2 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -48,7 +48,8 @@ public: Forward, StartupFromSettings, DomainPathResponse, - Internal + Internal, + AttemptedRefresh }; bool isConnected(); @@ -89,6 +90,8 @@ public slots: void goToUser(const QString& username); + void refreshPreviousLookup(); + void storeCurrentAddress(); void copyAddress(); @@ -99,7 +102,7 @@ signals: void lookupResultIsOffline(); void lookupResultIsNotFound(); - void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort); + void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID); void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID); void locationChangeRequired(const glm::vec3& newPosition, @@ -152,6 +155,8 @@ private: quint64 _lastBackPush = 0; QString _newHostLookupPath; + + QUrl _previousLookup; }; #endif // hifi_AddressManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 4f85296f03..1efcfc7f27 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -28,16 +28,8 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), - _uuid(), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), - _assignmentUUID(), - _connectionToken(), - _iceDomainID(), - _iceClientID(), - _iceServerSockAddr(), _icePeer(this), - _isConnected(false), - _settingsObject(), _settingsTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -105,7 +97,7 @@ void DomainHandler::hardReset() { softReset(); qCDebug(networking) << "Hard reset in NodeList DomainHandler."; - _iceDomainID = QUuid(); + _pendingDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); _sockAddr.clear(); @@ -139,7 +131,7 @@ void DomainHandler::setUUID(const QUuid& uuid) { } } -void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { +void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded @@ -171,6 +163,8 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { // grab the port by reading the string after the colon _sockAddr.setPort(port); } + + _pendingDomainID = domainID; } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { @@ -181,7 +175,7 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - _iceDomainID = id; + _pendingDomainID = id; HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); @@ -343,7 +337,7 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation); - if (_icePeer.getUUID() != _iceDomainID) { + if (_icePeer.getUUID() != _pendingDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; _icePeer.reset(); } else { diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index bcee7668d1..226186f1d0 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -58,8 +58,8 @@ public: const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - - const QUuid& getICEDomainID() const { return _iceDomainID; } + + const QUuid& getPendingDomainID() const { return _pendingDomainID; } const QUuid& getICEClientID() const { return _iceClientID; } @@ -94,7 +94,7 @@ public: }; public slots: - void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); + void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid()); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); void processSettingsPacketList(QSharedPointer packetList); @@ -136,11 +136,11 @@ private: HifiSockAddr _sockAddr; QUuid _assignmentUUID; QUuid _connectionToken; - QUuid _iceDomainID; + QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection QUuid _iceClientID; HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; - bool _isConnected; + bool _isConnected { false }; QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 16a4083b08..082200fccc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, - &_domainHandler, &DomainHandler::setHostnameAndPort); + &_domainHandler, &DomainHandler::setSocketAndID); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); @@ -355,6 +355,14 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } + + if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { + // if we aren't connected to the domain-server, and we have an ID + // (that we presume belongs to a domain in the HF Metaverse) + // we request connection information for the domain every so often to make sure what we have is up to date + + DependencyManager::get()->refreshPreviousLookup(); + } } void NodeList::handleDSPathQuery(const QString& newPath) { @@ -462,7 +470,7 @@ void NodeList::handleICEConnectionToDomainServer() { LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), - _domainHandler.getICEDomainID()); + _domainHandler.getPendingDomainID()); } } @@ -475,7 +483,7 @@ void NodeList::pingPunchForDomainServer() { if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" - << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); + << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID()); } else { if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat From 8318e9ccc8526c8170da07b13f261097d1a96e2a Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 6 Jun 2016 10:50:49 -0700 Subject: [PATCH 0481/1237] Revert "additional revert related change" This reverts commit 3119b654dc9b0053c5c1bdee7668246ded0fcb87. --- libraries/networking/src/AddressManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 9a3d147ead..80989acd2c 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -384,7 +384,7 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) { if (errorReply.error() == QNetworkReply::ContentNotFoundError) { // if this is a lookup that has no result, don't keep re-trying it - //_previousLookup.clear(); + _previousLookup.clear(); emit lookupResultIsNotFound(); } From f59335ceff908d6a2db5f26227840ddc172c1661 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 6 Jun 2016 11:49:01 -0700 Subject: [PATCH 0482/1237] don't thrash API re-check when connection denied --- libraries/networking/src/DomainHandler.cpp | 6 ++---- libraries/networking/src/DomainHandler.h | 3 ++- libraries/networking/src/NodeList.cpp | 9 ++++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 1efcfc7f27..9b94c07ed2 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -85,6 +85,7 @@ void DomainHandler::softReset() { clearSettings(); + _domainConnectionRefusals.clear(); _connectionDenialsSinceKeypairRegen = 0; // cancel the failure timeout for any pending requests for settings @@ -141,9 +142,6 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const // set the new hostname _hostname = hostname; - // FIXME - is this the right place??? - _domainConnectionRefusals.clear(); - qCDebug(networking) << "Updated domain hostname to" << _hostname; // re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname @@ -168,7 +166,7 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { - if (id != _uuid) { + if (_iceServerSockAddr.getAddress().toString() != iceServerHostname && id != _pendingDomainID) { // re-set the domain info to connect to new domain hardReset(); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 226186f1d0..c33a946d98 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -72,10 +72,11 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); + bool wasConnectionRefused() const { return !_domainConnectionRefusals.isEmpty(); } + bool hasSettings() const { return !_settingsObject.isEmpty(); } void requestDomainSettings(); const QJsonObject& getSettingsObject() const { return _settingsObject; } - void setPendingPath(const QString& pendingPath) { _pendingPath = pendingPath; } const QString& getPendingPath() { return _pendingPath; } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 082200fccc..41cfe2cfd7 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -356,7 +356,14 @@ void NodeList::sendDomainServerCheckIn() { _numNoReplyDomainCheckIns++; } - if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { + const int NUM_NO_REPLY_CHECKINS_BEFORE_API_REFRESH = 2; + + if (!_publicSockAddr.isNull() + && !_domainHandler.isConnected() + && _numNoReplyDomainCheckIns > NUM_NO_REPLY_CHECKINS_BEFORE_API_REFRESH + && !_domainHandler.getPendingDomainID().isNull() + && !_domainHandler.wasConnectionRefused()) { + // if we aren't connected to the domain-server, and we have an ID // (that we presume belongs to a domain in the HF Metaverse) // we request connection information for the domain every so often to make sure what we have is up to date From e39c708bf6a2ba8b7f506c4085f6b70cc2cd907e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 7 Jun 2016 11:10:48 -0700 Subject: [PATCH 0483/1237] require two triggers for API refresh, fix check for ICE domain --- libraries/networking/src/DomainHandler.cpp | 3 ++- libraries/networking/src/NodeList.cpp | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 9b94c07ed2..74e38a5003 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -166,7 +166,8 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { - if (_iceServerSockAddr.getAddress().toString() != iceServerHostname && id != _pendingDomainID) { + + if (_iceServerSockAddr.getAddress().toString() != iceServerHostname || id != _pendingDomainID) { // re-set the domain info to connect to new domain hardReset(); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 41cfe2cfd7..adb6c8e13e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -356,19 +356,22 @@ void NodeList::sendDomainServerCheckIn() { _numNoReplyDomainCheckIns++; } - const int NUM_NO_REPLY_CHECKINS_BEFORE_API_REFRESH = 2; + static int numTriggersSinceAPIRefresh = 0; if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() - && _numNoReplyDomainCheckIns > NUM_NO_REPLY_CHECKINS_BEFORE_API_REFRESH && !_domainHandler.getPendingDomainID().isNull() - && !_domainHandler.wasConnectionRefused()) { + && !_domainHandler.wasConnectionRefused() + && ++numTriggersSinceAPIRefresh > 1) { // if we aren't connected to the domain-server, and we have an ID // (that we presume belongs to a domain in the HF Metaverse) - // we request connection information for the domain every so often to make sure what we have is up to date + // we re-request connection information from the AdressManager + // every 2 failing check in attempts to make sure what we have is up to date DependencyManager::get()->refreshPreviousLookup(); + numTriggersSinceAPIRefresh = 0; + } } From c5a4a72a84ce6909338f7e88ec7f2db8e5732f42 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 7 Jun 2016 12:13:59 -0700 Subject: [PATCH 0484/1237] trigger domain API refresh from DomainHandler --- libraries/networking/src/DomainHandler.cpp | 33 +++++++++++++++++++--- libraries/networking/src/DomainHandler.h | 4 +-- libraries/networking/src/NodeList.cpp | 18 ------------ libraries/networking/src/NodeList.h | 1 + 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 74e38a5003..6880b7a329 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -14,6 +14,7 @@ #include #include +#include "AddressManager.h" #include "Assignment.h" #include "HifiSockAddr.h" #include "NodeList.h" @@ -30,7 +31,8 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), _icePeer(this), - _settingsTimer(this) + _settingsTimer(this), + _apiRefreshTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -41,6 +43,16 @@ DomainHandler::DomainHandler(QObject* parent) : static const int DOMAIN_SETTINGS_TIMEOUT_MS = 5000; _settingsTimer.setInterval(DOMAIN_SETTINGS_TIMEOUT_MS); connect(&_settingsTimer, &QTimer::timeout, this, &DomainHandler::settingsReceiveFail); + + // setup the API refresh timer for auto connection information refresh from API when failing to connect + const int API_REFRESH_TIMEOUT_MSEC = 2500; + _apiRefreshTimer.setInterval(API_REFRESH_TIMEOUT_MSEC); + + auto addressManager = DependencyManager::get(); + connect(&_apiRefreshTimer, &QTimer::timeout, addressManager.data(), &AddressManager::refreshPreviousLookup); + + // stop the refresh timer if we connect to a domain + connect(this, &DomainHandler::connectedToDomain, &_apiRefreshTimer, &QTimer::stop); } void DomainHandler::disconnect() { @@ -90,6 +102,9 @@ void DomainHandler::softReset() { // cancel the failure timeout for any pending requests for settings QMetaObject::invokeMethod(&_settingsTimer, "stop"); + + // restart the API refresh timer in case we fail to connect and need to refresh information + QMetaObject::invokeMethod(&_apiRefreshTimer, "start"); } void DomainHandler::hardReset() { @@ -134,6 +149,8 @@ void DomainHandler::setUUID(const QUuid& uuid) { void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { + _pendingDomainID = domainID; + if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded hardReset(); @@ -161,8 +178,6 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const // grab the port by reading the string after the colon _sockAddr.setPort(port); } - - _pendingDomainID = domainID; } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { @@ -248,6 +263,7 @@ void DomainHandler::setIsConnected(bool isConnected) { // we've connected to new domain - time to ask it for global settings requestDomainSettings(); + } else { emit disconnectedFromDomain(); } @@ -298,6 +314,9 @@ void DomainHandler::processICEPingReplyPacket(QSharedPointer me qCDebug(networking) << "Received reply from domain-server on" << senderSockAddr; if (getIP().isNull()) { + // we're hearing back from this domain-server, no need to refresh API information + _apiRefreshTimer.stop(); + // for now we're unsafely assuming this came back from the domain if (senderSockAddr == _icePeer.getLocalSocket()) { qCDebug(networking) << "Connecting to domain using local socket"; @@ -326,10 +345,13 @@ void DomainHandler::processDTLSRequirementPacket(QSharedPointer void DomainHandler::processICEResponsePacket(QSharedPointer message) { if (_icePeer.hasSockets()) { qDebug() << "Received an ICE peer packet for domain-server but we already have sockets. Not processing."; - // bail on processing this packet if our ice peer doesn't have sockets + // bail on processing this packet if our ice peer already has sockets return; } + // start or restart the API refresh timer now that we have new information + _apiRefreshTimer.start(); + QDataStream iceResponseStream(message->getMessage()); iceResponseStream >> _icePeer; @@ -366,6 +388,9 @@ bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) { } void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer message) { + // we're hearing from this domain-server, don't need to refresh API info + _apiRefreshTimer.stop(); + // Read deny reason from packet uint8_t reasonCodeWire; diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index c33a946d98..1328174e87 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -72,8 +72,6 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); - bool wasConnectionRefused() const { return !_domainConnectionRefusals.isEmpty(); } - bool hasSettings() const { return !_settingsObject.isEmpty(); } void requestDomainSettings(); const QJsonObject& getSettingsObject() const { return _settingsObject; } @@ -149,6 +147,8 @@ private: QStringList _domainConnectionRefusals; bool _hasCheckedForAccessToken { false }; int _connectionDenialsSinceKeypairRegen { 0 }; + + QTimer _apiRefreshTimer; }; #endif // hifi_DomainHandler_h diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index adb6c8e13e..60d4a30d2b 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -355,24 +355,6 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } - - static int numTriggersSinceAPIRefresh = 0; - - if (!_publicSockAddr.isNull() - && !_domainHandler.isConnected() - && !_domainHandler.getPendingDomainID().isNull() - && !_domainHandler.wasConnectionRefused() - && ++numTriggersSinceAPIRefresh > 1) { - - // if we aren't connected to the domain-server, and we have an ID - // (that we presume belongs to a domain in the HF Metaverse) - // we re-request connection information from the AdressManager - // every 2 failing check in attempts to make sure what we have is up to date - - DependencyManager::get()->refreshPreviousLookup(); - numTriggersSinceAPIRefresh = 0; - - } } void NodeList::handleDSPathQuery(const QString& newPath) { diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index b269554e77..e6521acbe5 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -105,6 +105,7 @@ private slots: void pingPunchForDomainServer(); void sendKeepAlivePings(); + private: NodeList() : LimitedNodeList(0, 0) { assert(false); } // Not implemented, needed for DependencyManager templates compile NodeList(char ownerType, unsigned short socketListenPort = 0, unsigned short dtlsListenPort = 0); From 4b5a5541226b89ae311f825df6211bf70ce1ceb3 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 8 Jun 2016 11:26:54 -0700 Subject: [PATCH 0485/1237] remove check in version downgrading from NodeList --- domain-server/src/DomainServer.cpp | 2 +- interface/src/Application.cpp | 13 ------------- interface/src/Application.h | 1 - libraries/networking/src/NodeList.cpp | 18 +++++------------- libraries/networking/src/NodeList.h | 8 -------- 5 files changed, 6 insertions(+), 36 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 0f5498a575..c73c55579b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -324,7 +324,7 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { // don't understand, so we can send them an empty EntityData with our latest version and they will // warn the user that the protocol is not compatible if (headerType == PacketType::DomainConnectRequest && - headerVersion < static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { + headerVersion (DomainConnectRequestVersion::HasProtocolVersions)) { auto packetWithBadVersion = NLPacket::create(PacketType::EntityData); nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr()); return false; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 68e916c29e..0184fc4990 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -630,7 +630,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); - connect(&domainHandler, &DomainHandler::resetting, nodeList.data(), &NodeList::resetDomainServerCheckInVersion); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one @@ -654,7 +653,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); - connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, this, &Application::limitOfSilentDomainCheckInsReached); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); // connect to appropriate slots on AccountManager @@ -4619,17 +4617,6 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const { Physics::setSessionUUID(sessionUUID); } - -// If we're not getting anything back from the domain server checkin, it might be that the domain speaks an -// older version of the DomainConnectRequest protocol. We will attempt to send and older version of DomainConnectRequest. -// We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we -// need to be downgraded to talk to it). -void Application::limitOfSilentDomainCheckInsReached() { - auto nodeList = DependencyManager::get(); - nodeList->downgradeDomainServerCheckInVersion(); // attempt to use an older domain checkin version - nodeList->reset(); -} - bool Application::askToSetAvatarUrl(const QString& url) { QUrl realUrl(url); if (realUrl.isLocalFile()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index f93434f581..6b6148be32 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -318,7 +318,6 @@ private slots: bool displayAvatarAttachmentConfirmationDialog(const QString& name) const; void setSessionUUID(const QUuid& sessionUUID) const; - void limitOfSilentDomainCheckInsReached(); void domainChanged(const QString& domainHostname); void updateWindowTitle() const; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 60d4a30d2b..8a6e96f414 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -250,7 +250,6 @@ void NodeList::sendDomainServerCheckIn() { qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in."; handleICEConnectionToDomainServer(); } else if (!_domainHandler.getIP().isNull()) { - bool isUsingDTLS = false; PacketType domainPacketType = !_domainHandler.isConnected() ? PacketType::DomainConnectRequest : PacketType::DomainListRequest; @@ -292,8 +291,7 @@ void NodeList::sendDomainServerCheckIn() { return; } - auto packetVersion = (domainPacketType == PacketType::DomainConnectRequest) ? _domainConnectRequestVersion : 0; - auto domainPacket = NLPacket::create(domainPacketType, -1, false, false, packetVersion); + auto domainPacket = NLPacket::create(domainPacketType); QDataStream packetStream(domainPacket.get()); @@ -315,18 +313,14 @@ void NodeList::sendDomainServerCheckIn() { packetStream << connectUUID; // include the protocol version signature in our connect request - if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { - QByteArray protocolVersionSig = protocolVersionsSignature(); - packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); - } + QByteArray protocolVersionSig = protocolVersionsSignature(); + packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); } // pack our data to send to the domain-server including // the hostname information (so the domain-server can see which place name we came in on) packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); - if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasHostname)) { - packetStream << DependencyManager::get()->getPlaceName(); - } + packetStream << DependencyManager::get()->getPlaceName(); if (!_domainHandler.isConnected()) { DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); @@ -341,9 +335,7 @@ void NodeList::sendDomainServerCheckIn() { flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendDSCheckIn); - if (!isUsingDTLS) { - sendPacket(std::move(domainPacket), _domainHandler.getSockAddr()); - } + sendPacket(std::move(domainPacket), _domainHandler.getSockAddr()); if (_numNoReplyDomainCheckIns >= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index e6521acbe5..51eee744c0 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -68,9 +68,6 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } - /// downgrades the DomainConnnectRequest PacketVersion to attempt to probe for older domain servers - void downgradeDomainServerCheckInVersion() { _domainConnectRequestVersion--; } - public slots: void reset(); void sendDomainServerCheckIn(); @@ -88,9 +85,6 @@ public slots: void processICEPingPacket(QSharedPointer message); - void resetDomainServerCheckInVersion() - { _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); } - signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); @@ -130,8 +124,6 @@ private: HifiSockAddr _assignmentServerSocket; bool _isShuttingDown { false }; QTimer _keepAlivePingTimer; - - PacketVersion _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); }; #endif // hifi_NodeList_h From f963adb5dd74a26a4cf22d46c338a978f50109b2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 8 Jun 2016 11:47:58 -0700 Subject: [PATCH 0486/1237] send connection denial for connect version mismatch --- domain-server/src/DomainGatekeeper.cpp | 16 ++++++++++------ domain-server/src/DomainGatekeeper.h | 6 ++++-- domain-server/src/DomainServer.cpp | 15 +++++---------- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index b940d46849..680b5c277b 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -62,10 +62,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSenderSockAddr(), - DomainHandler::ConnectionRefusedReason::ProtocolMismatch); + sendProtocolMismatchConnectionDenial(message->getSenderSockAddr()); return; } @@ -123,6 +120,13 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer 0) { diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 09e3b04ed7..237f8d3185 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -42,6 +42,8 @@ public: void preloadAllowedUserPublicKeys(); void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); } + + static void sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr); public slots: void processConnectRequestPacket(QSharedPointer message); void processICEPingPacket(QSharedPointer message); @@ -76,8 +78,8 @@ private: const HifiSockAddr& senderSockAddr); void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr); - void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, - DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown); + static void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, + DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown); void pingPunchForConnectingPeer(const SharedNetworkPeer& peer); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c73c55579b..7c20817353 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -318,16 +318,11 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { auto nodeList = DependencyManager::get(); - // This implements a special case that handles OLD clients which don't know how to negotiate matching - // protocol versions. We know these clients will sent DomainConnectRequest with older versions. We also - // know these clients will show a warning dialog if they get an EntityData with a protocol version they - // don't understand, so we can send them an empty EntityData with our latest version and they will - // warn the user that the protocol is not compatible - if (headerType == PacketType::DomainConnectRequest && - headerVersion (DomainConnectRequestVersion::HasProtocolVersions)) { - auto packetWithBadVersion = NLPacket::create(PacketType::EntityData); - nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr()); - return false; + // if this is a mismatching connect packet, we can't simply drop it on the floor + // send back a packet to the interface that tells them we refuse connection for a mismatch + if (headerType == PacketType::DomainConnectRequest + && headerVersion != versionForPacketType(PacketType::DomainConnectRequest)) { + DomainGatekeeper::sendProtocolMismatchConnectionDenial(packet.getSenderSockAddr()); } // let the normal nodeList implementation handle all other packets. diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index db743f81e4..a891b1e422 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -67,7 +67,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectionDeniedVersion::IncludesReasonCode); case PacketType::DomainConnectRequest: - return static_cast(DomainConnectRequestVersion::HasProtocolVersions); + return static_cast(DomainConnectRequestVersion::HasHostname); default: return 17; From 12e5851ba4376a7a8e42633cd783f5856cce4e74 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 8 Jun 2016 11:54:31 -0700 Subject: [PATCH 0487/1237] fix connection denial message, put version back --- domain-server/src/DomainGatekeeper.cpp | 16 +++++++++------- interface/src/Application.cpp | 2 -- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 680b5c277b..ec913b27f5 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -120,13 +120,6 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer(reasonCode)) { case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: - notifyPacketVersionMismatch(); - break; case DomainHandler::ConnectionRefusedReason::TooManyUsers: case DomainHandler::ConnectionRefusedReason::Unknown: { QString message = "Unable to connect to the location you are visiting.\n"; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index a891b1e422..db743f81e4 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -67,7 +67,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectionDeniedVersion::IncludesReasonCode); case PacketType::DomainConnectRequest: - return static_cast(DomainConnectRequestVersion::HasHostname); + return static_cast(DomainConnectRequestVersion::HasProtocolVersions); default: return 17; From e4ebafa0d8a463f9f78a4527d1580da4622ccc6d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 8 Jun 2016 13:38:07 -0700 Subject: [PATCH 0488/1237] add a menu option to force the wrong DS connect version --- interface/src/Menu.cpp | 3 +++ interface/src/Menu.h | 1 + libraries/networking/src/NLPacket.cpp | 5 +++++ libraries/networking/src/NLPacket.h | 1 + libraries/networking/src/NodeList.cpp | 7 +++++++ libraries/networking/src/NodeList.h | 8 ++++++++ 6 files changed, 25 insertions(+) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 031564fa7a..4fd5569ebd 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -542,6 +542,9 @@ Menu::Menu() { #if (PR_BUILD || DEV_BUILD) addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false, qApp, SLOT(sendWrongProtocolVersionsSignature(bool))); + + addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongDSConnectVersion, 0, false, + nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool))); #endif diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8081e27eb8..503cbf51fa 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -166,6 +166,7 @@ namespace MenuOption { const QString RunTimingTests = "Run Timing Tests"; const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; + const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; const QString SetHomeLocation = "Set Home Location"; const QString ShowDSConnectTable = "Show Domain Connection Timing"; diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 34a159ae6c..a11dd69753 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -184,6 +184,11 @@ void NLPacket::setType(PacketType type) { writeTypeAndVersion(); } +void NLPacket::setVersion(PacketVersion version) { + _version = version; + writeTypeAndVersion(); +} + void NLPacket::readType() { _type = NLPacket::typeInHeader(*this); } diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index f49f8498a5..33de262dfb 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -65,6 +65,7 @@ public: void setType(PacketType type); PacketVersion getVersion() const { return _version; } + void setVersion(PacketVersion version); const QUuid& getSourceID() const { return _sourceID; } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 8a6e96f414..8375cd0b1e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -296,6 +296,13 @@ void NodeList::sendDomainServerCheckIn() { QDataStream packetStream(domainPacket.get()); if (domainPacketType == PacketType::DomainConnectRequest) { + +#if (PR_BUILD || DEV_BUILD) + if (_shouldSendNewerVersion) { + domainPacket->setVersion(versionForPacketType(domainPacketType) + 1); + } +#endif + QUuid connectUUID; if (!_domainHandler.getAssignmentUUID().isNull()) { diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 51eee744c0..3fbc86c736 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -85,6 +85,10 @@ public slots: void processICEPingPacket(QSharedPointer message); +#if (PR_BUILD || DEV_BUILD) + void toggleSendNewerDSConnectVersion(bool shouldSendNewerVersion) { _shouldSendNewerVersion = shouldSendNewerVersion; } +#endif + signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); @@ -124,6 +128,10 @@ private: HifiSockAddr _assignmentServerSocket; bool _isShuttingDown { false }; QTimer _keepAlivePingTimer; + +#if (PR_BUILD || DEV_BUILD) + bool _shouldSendNewerVersion { false }; +#endif }; #endif // hifi_NodeList_h From 8cccd5416a3f6a66933979dcc24330fd77fd0f2f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 9 Jun 2016 16:10:27 -0700 Subject: [PATCH 0489/1237] try to fix mac errors --- libraries/gpu/src/gpu/Batch.h | 5 +++-- libraries/procedural/src/procedural/Procedural.cpp | 2 +- libraries/procedural/src/procedural/Procedural.h | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index f8b447975c..e0a0e057f7 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -292,8 +293,8 @@ public: _glUniform4f(location, v.x, v.y, v.z, v.w); } - void _glUniform(int location, const glm::quat& v) { - _glUniformMatrix3fv(location, 1, false, reinterpret_cast< const float* >(&glm::mat3_cast(v))); + void _glUniform(int location, const glm::mat3& v) { + _glUniformMatrix3fv(location, 1, false, glm::value_ptr(v)); } void _glColor4f(float red, float green, float blue, float alpha); diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 4f308e0812..ed3d5712e3 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -207,7 +207,7 @@ bool Procedural::ready() { void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const glm::vec3& eyePos) { _entityDimensions = size; _entityPosition = position; - _entityOrientation = orientation; + _entityOrientation = glm::mat3_cast(orientation); _eyePos = eyePos; if (_shaderUrl.isLocalFile()) { auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index e27f8ade44..45dcfd7bf6 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -95,7 +95,7 @@ protected: // Entity metadata glm::vec3 _entityDimensions; glm::vec3 _entityPosition; - glm::quat _entityOrientation; + glm::mat3 _entityOrientation; glm::vec3 _eyePos; private: From dd0d594524975d11ba6dac6152a4adf35dcdfcb8 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 9 Jun 2016 16:31:09 -0700 Subject: [PATCH 0490/1237] cleanup --- libraries/audio/src/AudioHRTF.cpp | 8 +++----- libraries/audio/src/AudioHRTF.h | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index 89c929011a..e7cf4436c6 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -10,7 +10,6 @@ // #include -#include #include #include @@ -125,13 +124,12 @@ static void FIR_1x4_SSE(float* src, float* dst0, float* dst1, float* dst2, float #include "CPUDetect.h" -typedef void FIR_1x4_t(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames); -FIR_1x4_t FIR_1x4_AVX; // separate compilation with VEX-encoding enabled +void FIR_1x4_AVX(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames); static void FIR_1x4(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames) { - static FIR_1x4_t* f = cpuSupportsAVX() ? FIR_1x4_AVX : FIR_1x4_SSE; // init on first call - (*f)(src, dst0, dst1, dst2, dst3, coef, numFrames); // dispatch + static auto f = cpuSupportsAVX() ? FIR_1x4_AVX : FIR_1x4_SSE; + (*f)(src, dst0, dst1, dst2, dst3, coef, numFrames); // dispatch } // 4 channel planar to interleaved diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index f92f9c1602..d3b6237d0c 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -32,7 +32,7 @@ public: // input: mono source // output: interleaved stereo mix buffer (accumulates into existing output) // index: HRTF subject index - // azimuth: clockwise panning angle [0, 360] in degrees + // azimuth: clockwise panning angle in radians // gain: gain factor for distance attenuation // numFrames: must be HRTF_BLOCK in this version // From 2953ca08cbe82e4134f9e0b449f8d1aef977e968 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 10 Jun 2016 12:04:41 +1200 Subject: [PATCH 0491/1237] Fix import of invalid JSON file failing silently --- libraries/entities/src/EntityTree.cpp | 9 ++++++++- libraries/octree/src/Octree.cpp | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 581e0a9568..27a708cf07 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1430,6 +1430,12 @@ bool EntityTree::readFromMap(QVariantMap& map) { QVariantList entitiesQList = map["Entities"].toList(); QScriptEngine scriptEngine; + if (entitiesQList.length() == 0) { + // Empty map or invalidly formed file. + return false; + } + + bool success = true; foreach (QVariant entityVariant, entitiesQList) { // QVariantMap --> QScriptValue --> EntityItemProperties --> Entity QVariantMap entityMap = entityVariant.toMap(); @@ -1447,9 +1453,10 @@ bool EntityTree::readFromMap(QVariantMap& map) { EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType(); + success = false; } } - return true; + return success; } void EntityTree::resetClientEditStats() { diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 39be760944..475beef03c 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1863,9 +1863,9 @@ bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputSt QJsonDocument asDocument = QJsonDocument::fromJson(jsonBuffer); QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); - readFromMap(asMap); + bool success = readFromMap(asMap); delete[] rawData; - return true; + return success; } void Octree::writeToFile(const char* fileName, OctreeElementPointer element, QString persistAsFileType) { From 685710d0ec088b60b13fe7aef0ba3054cb67155e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 9 Jun 2016 18:09:04 -0700 Subject: [PATCH 0492/1237] OverlayConductor improvments * the HUD will fade/in when driving, even in desktop mode. * the HUD no longer pops when you lean outside of the UI sphere, instead it should smoothly fade out and fade back in. * the overlay toggle button should override fading while driving, as expected. * removed any notion of SITTING, STANDING or FLAT mode from overlay. --- interface/src/ui/OverlayConductor.cpp | 139 ++++++++++++-------------- interface/src/ui/OverlayConductor.h | 22 ++-- 2 files changed, 76 insertions(+), 85 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 9e2e8cb0db..790652aa0f 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -22,7 +22,7 @@ OverlayConductor::OverlayConductor() { OverlayConductor::~OverlayConductor() { } -bool OverlayConductor::shouldCenterUI() const { +bool OverlayConductor::headOutsideOverlay() const { glm::mat4 hmdMat = qApp->getHMDSensorPose(); glm::vec3 hmdPos = extractTranslation(hmdMat); @@ -41,89 +41,82 @@ bool OverlayConductor::shouldCenterUI() const { return false; } -void OverlayConductor::update(float dt) { - - updateMode(); - +bool OverlayConductor::avatarHasDriveInput() const { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - switch (_mode) { - case SITTING: { - // when sitting, the overlay is at the origin, facing down the -z axis. - // the camera is taken directly from the HMD. - Transform identity; - qApp->getApplicationCompositor().setModelTransform(identity); - qApp->getApplicationCompositor().setCameraBaseTransform(identity); - break; - } - case STANDING: { + const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000; // 200 ms + const quint64 DRIVE_DISABLE_TIME_USECS = 1000 * 1000; // 1 s - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; - - // fade in or out the overlay, based on driving. - bool nowDriving = myAvatar->hasDriveInput(); - // Check that we're in this new mode for long enough to really trigger a transition. - if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. - _timeInPotentialMode = 0; - } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. - _timeInPotentialMode = usecTimestampNow(); - } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { - _timeInPotentialMode = 0; // a real transition - bool wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); - setEnabled(!nowDriving && wantsOverlays); - _driving = nowDriving; - } - - // center the UI - if (shouldCenterUI()) { - Transform hmdTransform(cancelOutRollAndPitch(qApp->getHMDSensorPose())); - qApp->getApplicationCompositor().setModelTransform(hmdTransform); - } - break; + bool desiredDriving = myAvatar->hasDriveInput(); + if (desiredDriving != _desiredDriving) { + // start timer + _desiredDrivingTimer = usecTimestampNow() + (desiredDriving ? DRIVE_ENABLE_TIME_USECS : DRIVE_DISABLE_TIME_USECS); } - case FLAT: - // do nothing - break; + + _desiredDriving = desiredDriving; + + if (_desiredDrivingTimer != 0 && usecTimestampNow() > _desiredDrivingTimer) { + // timer expired + // change state! + _currentDriving = _desiredDriving; + // disable timer + _desiredDrivingTimer = 0; } + + return _currentDriving; } -void OverlayConductor::updateMode() { +bool OverlayConductor::shouldShowOverlay() const { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - Mode newMode; - if (qApp->isHMDMode()) { - if (myAvatar->getClearOverlayWhenDriving()) { - newMode = STANDING; - } else { - newMode = SITTING; - } - } else { - newMode = FLAT; +#ifdef WANT_DEBUG + qDebug() << "AJT: wantsOverlays =" << Menu::getInstance()->isOptionChecked(MenuOption::Overlays) << ", clearOverlayWhenDriving =" << myAvatar->getClearOverlayWhenDriving() << + ", headOutsideOverlay =" << headOutsideOverlay() << ", hasDriveInput =" << avatarHasDriveInput(); +#endif + + return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (!myAvatar->getClearOverlayWhenDriving() || (!headOutsideOverlay() && !avatarHasDriveInput())); +} + +bool OverlayConductor::shouldRecenterOnFadeOut() const { + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && myAvatar->getClearOverlayWhenDriving() && headOutsideOverlay(); +} + +void OverlayConductor::centerUI() { + // place the overlay at the current hmd position in sensor space + auto camMat = cancelOutRollAndPitch(qApp->getHMDSensorPose()); + qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); +} + +void OverlayConductor::update(float dt) { + + // centerUI if hmd mode changes + if (qApp->isHMDMode() && !_hmdMode) { + centerUI(); + } + _hmdMode = qApp->isHMDMode(); + + // centerUI if timer expires + if (_fadeOutTime != 0 && usecTimestampNow() > _fadeOutTime) { + // fade out timer expired + _fadeOutTime = 0; + centerUI(); } - if (newMode != _mode) { - switch (newMode) { - case SITTING: { - // enter the SITTING state - // place the overlay at origin - qApp->getApplicationCompositor().setModelTransform(Transform()); - break; - } - case STANDING: { - // enter the STANDING state - Transform hmdTransform(cancelOutRollAndPitch(qApp->getHMDSensorPose())); - qApp->getApplicationCompositor().setModelTransform(hmdTransform); - break; - } + bool showOverlay = shouldShowOverlay(); - case FLAT: - // do nothing - break; + if (showOverlay != getEnabled()) { + if (showOverlay) { + // disable fadeOut timer + _fadeOutTime = 0; + } else if (shouldRecenterOnFadeOut()) { + // start fadeOut timer + const quint64 FADE_OUT_TIME_USECS = 300 * 1000; // 300 ms + _fadeOutTime = usecTimestampNow() + FADE_OUT_TIME_USECS; } } - _mode = newMode; + setEnabled(showOverlay); } void OverlayConductor::setEnabled(bool enabled) { @@ -135,11 +128,9 @@ void OverlayConductor::setEnabled(bool enabled) { auto offscreenUi = DependencyManager::get(); offscreenUi->setPinned(!_enabled); // if the new state is visible/enabled... - if (_enabled && _mode == STANDING) { - // place the overlay at the current hmd position in world space - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + if (_enabled && myAvatar->getClearOverlayWhenDriving() && qApp->isHMDMode()) { + centerUI(); } } diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 2e425454c7..40ccb4b91f 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -19,21 +19,21 @@ public: void update(float dt); void setEnabled(bool enable); bool getEnabled() const; - bool shouldCenterUI() const; private: - void updateMode(); + bool headOutsideOverlay() const; + bool avatarHasDriveInput() const; + bool shouldShowOverlay() const; + bool shouldRecenterOnFadeOut() const; + void centerUI(); - enum Mode { - FLAT, - SITTING, - STANDING - }; - - Mode _mode { FLAT }; + quint64 _fadeOutTime { 0 }; bool _enabled { false }; - bool _driving { false }; - quint64 _timeInPotentialMode { 0 }; + bool _hmdMode { false }; + + mutable quint64 _desiredDrivingTimer { 0 }; + mutable bool _desiredDriving { false }; + mutable bool _currentDriving { false }; }; #endif From 6831710efb0dc29947470a6d89edb95691dc339c Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 9 Jun 2016 19:13:28 -0700 Subject: [PATCH 0493/1237] Adding the deawSCattering pass --- .../render-utils/src/SubsurfaceScattering.cpp | 35 +++--- .../subsurfaceScattering_drawScattering.slf | 114 ++++++++++++++++++ .../src/subsurfaceScattering_makeLUT.slf | 17 +-- 3 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 libraries/render-utils/src/subsurfaceScattering_drawScattering.slf diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index b0d0e9a38e..d6543bfc1d 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -16,11 +16,13 @@ #include "FramebufferCache.h" #include "subsurfaceScattering_makeLUT_frag.h" +#include "subsurfaceScattering_drawScattering_frag.h" const int SubsurfaceScattering_FrameTransformSlot = 0; const int SubsurfaceScattering_ParamsSlot = 1; -const int SubsurfaceScattering_DepthMapSlot = 0; +const int SubsurfaceScattering_CurvatureMapSlot = 0; const int SubsurfaceScattering_NormalMapSlot = 1; +const int SubsurfaceScattering_ScatteringTableSlot = 2; SubsurfaceScattering::SubsurfaceScattering() { Parameters parameters; @@ -39,12 +41,17 @@ void SubsurfaceScattering::configure(const Config& config) { gpu::PipelinePointer SubsurfaceScattering::getScatteringPipeline() { if (!_scatteringPipeline) { auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); + // auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); + auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_drawScattering_frag)); gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; - // slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); - // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SubsurfaceScattering_FrameTransformSlot)); + // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + + slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), SubsurfaceScattering_CurvatureMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), SubsurfaceScattering_NormalMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), SubsurfaceScattering_ScatteringTableSlot)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -71,25 +78,23 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, auto pipeline = getScatteringPipeline(); + auto framebufferCache = DependencyManager::get(); +// if (curvatureFramebuffer->getRenderBuffer(0)) gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); batch.setViewportTransform(args->_viewport >> 1); - /* batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); - Transform model; - model.setTranslation(glm::vec3(sMin, tMin, 0.0f)); - model.setScale(glm::vec3(sWidth, tHeight, 1.0f)); - batch.setModelTransform(model);*/ + batch.setUniformBuffer(SubsurfaceScattering_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); batch.setPipeline(pipeline); - batch.setResourceTexture(0, _scatteringTable); + batch.setResourceTexture(SubsurfaceScattering_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); + batch.setResourceTexture(SubsurfaceScattering_CurvatureMapSlot, framebufferCache->getCurvatureTexture()); + batch.setResourceTexture(SubsurfaceScattering_ScatteringTableSlot, _scatteringTable); batch.draw(gpu::TRIANGLE_STRIP, 4); }); - } @@ -144,7 +149,7 @@ vec3 integrate(double cosTheta, double skinRadius) { double a = -(_PI); - double inc = 0.1; + double inc = 0.01; while (a <= (_PI)) { double sampleAngle = theta + a; @@ -302,8 +307,8 @@ gpu::TexturePointer SubsurfaceScattering::generatePreIntegratedScattering(Render const int WIDTH = 128; const int HEIGHT = 128; auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT)); - // diffuseScatter(scatteringLUT); - diffuseScatterGPU(profileMap, scatteringLUT, args); + diffuseScatter(scatteringLUT); + //diffuseScatterGPU(profileMap, scatteringLUT, args); return scatteringLUT; } diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf new file mode 100644 index 0000000000..28d7867e51 --- /dev/null +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -0,0 +1,114 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/8/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +<@include DeferredTransform.slh@> +<$declareDeferredFrameTransform()$> + + + + +vec2 signNotZero(vec2 v) { + return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); +} + +vec3 oct_to_float32x3(in vec2 e) { + vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); + if (v.z < 0) { + v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); + } + return normalize(v); +} + +vec2 unorm8x3_to_snorm12x2(vec3 u) { + u *= 255.0; + u.y *= (1.0 / 16.0); + vec2 s = vec2( u.x * 16.0 + floor(u.y), + fract(u.y) * (16.0 * 256.0) + u.z); + return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); +} +vec3 unpackNormal(in vec3 p) { + return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); +} + +vec2 sideToFrameTexcoord(vec2 side, vec2 texcoordPos) { + return vec2((texcoordPos.x + side.x) * side.y, texcoordPos.y); +} + +uniform sampler2D normalMap; + +vec3 getRawNormal(vec2 texcoord) { + return texture(normalMap, texcoord).xyz; +} + +vec3 getWorldNormal(vec2 texcoord) { + vec3 rawNormal = getRawNormal(texcoord); + return unpackNormal(rawNormal); +} + + +// the curvature texture +uniform sampler2D curvatureMap; + +vec4 fetchCurvature(vec2 texcoord) { + return texture(curvatureMap, texcoord); +} + + +uniform sampler2D scatteringLUT; + +vec3 fetchBRDF(float curvature, float LdotN) { + return texture(scatteringLUT, vec2(curvature, LdotN)).xyz; +} + +// Scattering parameters +float normalBendFactor = 1.0f; +float normalBendR = 1.5f; +float normalBendG = 0.8f; +float normalBendB = 0.3f; +float scatterBase = 0.012f; +float scatterCurve = 0.25f; + +in vec2 varTexCoord0; +out vec4 _fragColor; + +void main(void) { + // DeferredTransform deferredTransform = getDeferredTransform(); + // DeferredFragment frag = unpackDeferredFragment(deferredTransform, varTexCoord0); + + vec3 normal = getWorldNormal(varTexCoord0); + vec4 diffusedCurvature = fetchCurvature(varTexCoord0); + + // --> Calculate bent normals. + vec3 bentNormalN = normal; + vec3 bentNormalR = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); + float curvature = abs(diffusedCurvature.w * 2 - 1) * 0.5f * scatterCurve + scatterBase; + + // --> Calculate the light vector. + vec3 lightVector = normalize(vec3(-1.0f, -1.0f, -1.0f)); //normalize(lightPos - sourcePos.xyz); + + + // --> Optimise for skin diffusion profile. + float diffuseBlendedR = dot(normalize(mix( bentNormalN.xyz, bentNormalN, normalBendR * normalBendFactor)), lightVector); + float diffuseBlendedG = dot(normalize(mix(normal.xyz, bentNormalN, normalBendG * normalBendFactor)), lightVector); + float diffuseBlendedB = dot(normalize(mix(normal.xyz, bentNormalN, normalBendB * normalBendFactor)), lightVector); + + + // --> Look up the pre-integrated curvature-dependent BDRF textures + vec3 bdrfR = fetchBRDF(diffuseBlendedR, curvature); + vec3 bdrfG = fetchBRDF(diffuseBlendedG, curvature); + vec3 bdrfB = fetchBRDF(diffuseBlendedB, curvature); + vec3 bdrf = vec3( bdrfR.x, bdrfG.y, bdrfB.z); + bdrf *= bdrf; + _fragColor = vec4(vec3(bdrf.xyz), 1.0); +} + + diff --git a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf index e008c52385..90c7bfbf4e 100644 --- a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf +++ b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf @@ -10,7 +10,7 @@ // -#define _PI 3.14159265358979523846 +const float _PI = 3.14159265358979523846; uniform sampler2D profileMap; @@ -25,7 +25,6 @@ vec3 integrate(float cosTheta, float skinRadius) { float theta = acos(cosTheta); vec3 totalWeights = vec3(0.0); vec3 totalLight= vec3(0.0); - vec3 skinColour = vec3(1.0); float a = -(_PI); @@ -33,7 +32,9 @@ vec3 integrate(float cosTheta, float skinRadius) { while (a <= (_PI)) { float sampleAngle = theta + a; - float diffuse = clamp(cos(sampleAngle), 0.0, 1.0); + float diffuse = cos(sampleAngle); + if (diffuse < 0.0) diffuse = 0.0; + if (diffuse > 1.0) diffuse = 1.0; // Distance. float sampleDist = abs(2.0 * skinRadius * sin(a * 0.5)); @@ -42,13 +43,15 @@ vec3 integrate(float cosTheta, float skinRadius) { vec3 weights = scatter(sampleDist); totalWeights += weights; - totalLight += diffuse * weights /** (skinColour * skinColour)*/; + totalLight += diffuse * weights; a += inc; } - vec3 result = sqrt(totalLight / totalWeights); + vec3 result = (totalLight / totalWeights); - return min(result, vec3(1.0)); + return min(sqrt(result), vec3(1.0)); + + return scatter(skinRadius); } in vec2 varTexCoord0; @@ -56,6 +59,6 @@ out vec4 outFragColor; void main(void) { - outFragColor = vec4(integrate(varTexCoord0.x * 2.0 - 1, 2.0 * varTexCoord0.y), 1.0); + outFragColor = vec4(integrate(varTexCoord0.x * 2.0 - 1.0, 2.0 / varTexCoord0.y), 1.0); } From 3c4f336e728a637778dca5e1434393360bd99f56 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 10 Jun 2016 09:11:25 -0700 Subject: [PATCH 0494/1237] re-instate NodeList reset on limit of check ins reached --- interface/src/Application.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 90d8b677fe..504d573678 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -655,6 +655,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + // you might think we could just do this in NodeList but we only want this connection for Interface + connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); + // connect to appropriate slots on AccountManager auto accountManager = DependencyManager::get(); From e1ae2a193f0e3c510dcaa101ea5a21a984615437 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 10 Jun 2016 11:49:15 -0700 Subject: [PATCH 0495/1237] EntityMotionState now uses parent-relative position and rotation and velocity when deciding if it needs to send an edit packet to the entity-server --- libraries/physics/src/EntityMotionState.cpp | 41 ++++++------ libraries/physics/src/PhysicsEngine.cpp | 5 ++ libraries/shared/src/SpatiallyNestable.cpp | 69 +++++++++++++++++++++ libraries/shared/src/SpatiallyNestable.h | 22 ++++--- 4 files changed, 110 insertions(+), 27 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index be7862ade3..28f7756d9a 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -85,10 +85,10 @@ void EntityMotionState::updateServerPhysicsVariables() { return; } - _serverPosition = _entity->getPosition(); - _serverRotation = _entity->getRotation(); - _serverVelocity = _entity->getVelocity(); - _serverAngularVelocity = _entity->getAngularVelocity(); + Transform localTransform; + _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); + _serverPosition = localTransform.getTranslation(); + _serverRotation = localTransform.getRotation(); _serverAcceleration = _entity->getAcceleration(); _serverActionData = _entity->getActionData(); } @@ -274,11 +274,11 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { // if we've never checked before, our _lastStep will be 0, and we need to initialize our state if (_lastStep == 0) { btTransform xform = _body->getWorldTransform(); - _serverPosition = bulletToGLM(xform.getOrigin()); - _serverRotation = bulletToGLM(xform.getRotation()); - _serverVelocity = getBodyLinearVelocityGTSigma(); + _serverPosition = _entity->worldPositionToParent(bulletToGLM(xform.getOrigin())); + _serverRotation = _entity->worldRotationToParent(bulletToGLM(xform.getRotation())); + _serverVelocity = _entity->worldVelocityToParent(getBodyLinearVelocityGTSigma()); _serverAcceleration = Vectors::ZERO; - _serverAngularVelocity = bulletToGLM(_body->getAngularVelocity()); + _serverAngularVelocity = _entity->worldVelocityToParent(bulletToGLM(_body->getAngularVelocity())); _lastStep = simulationStep; _serverActionData = _entity->getActionData(); _numInactiveUpdates = 1; @@ -315,9 +315,6 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { _lastStep = simulationStep; if (glm::length2(_serverVelocity) > 0.0f) { - _serverVelocity += _serverAcceleration * dt; - _serverVelocity *= powf(1.0f - _body->getLinearDamping(), dt); - // the entity-server doesn't know where avatars are, so it doesn't do simple extrapolation for children of // avatars. We are trying to guess what values the entity server has, so we don't do it here, either. See // related code in EntitySimulation::moveSimpleKinematics. @@ -326,6 +323,9 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { bool hasAvatarAncestor = _entity->hasAncestorOfType(NestableType::Avatar); if (ancestryIsKnown && !hasAvatarAncestor) { + _serverVelocity += _serverAcceleration * dt; + _serverVelocity *= powf(1.0f - _body->getLinearDamping(), dt); + // NOTE: we ignore the second-order acceleration term when integrating // the position forward because Bullet also does this. _serverPosition += dt * _serverVelocity; @@ -351,7 +351,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { // compute position error btTransform worldTrans = _body->getWorldTransform(); - glm::vec3 position = bulletToGLM(worldTrans.getOrigin()); + glm::vec3 position = _entity->worldPositionToParent(bulletToGLM(worldTrans.getOrigin())); float dx2 = glm::distance2(position, _serverPosition); const float MAX_POSITION_ERROR_SQUARED = 0.000004f; // corresponds to 2mm @@ -386,7 +386,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { } } const float MIN_ROTATION_DOT = 0.99999f; // This corresponds to about 0.5 degrees of rotation - glm::quat actualRotation = bulletToGLM(worldTrans.getRotation()); + glm::quat actualRotation = _entity->worldRotationToParent(bulletToGLM(worldTrans.getRotation())); #ifdef WANT_DEBUG if ((fabsf(glm::dot(actualRotation, _serverRotation)) < MIN_ROTATION_DOT)) { @@ -491,11 +491,11 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ } // remember properties for local server prediction - _serverPosition = _entity->getPosition(); - _serverRotation = _entity->getRotation(); - _serverVelocity = _entity->getVelocity(); + Transform localTransform; + _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); + _serverPosition = localTransform.getTranslation(); + _serverRotation = localTransform.getRotation(); _serverAcceleration = _entity->getAcceleration(); - _serverAngularVelocity = _entity->getAngularVelocity(); _serverActionData = _entity->getActionData(); EntityItemProperties properties; @@ -600,7 +600,7 @@ uint32_t EntityMotionState::getIncomingDirtyFlags() { if (_body && _entity) { dirtyFlags = _entity->getDirtyFlags(); - if (dirtyFlags | Simulation::DIRTY_SIMULATOR_ID) { + if (dirtyFlags & Simulation::DIRTY_SIMULATOR_ID) { // when SIMULATOR_ID changes we must check for reinterpretation of asymmetric collision mask // bits for the avatar groups (e.g. MY_AVATAR vs OTHER_AVATAR) uint8_t entityCollisionMask = _entity->getCollisionless() ? 0 : _entity->getCollisionMask(); @@ -613,8 +613,9 @@ uint32_t EntityMotionState::getIncomingDirtyFlags() { // we add DIRTY_MOTION_TYPE if the body's motion type disagrees with entity velocity settings int bodyFlags = _body->getCollisionFlags(); bool isMoving = _entity->isMovingRelativeToParent(); - if (((bodyFlags & btCollisionObject::CF_STATIC_OBJECT) && isMoving) || - (bodyFlags & btCollisionObject::CF_KINEMATIC_OBJECT && !isMoving)) { + if (((bodyFlags & btCollisionObject::CF_STATIC_OBJECT) && isMoving) // || + // (bodyFlags & btCollisionObject::CF_KINEMATIC_OBJECT && !isMoving) + ) { dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } } diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index d3247ec62c..6f8acfa6a7 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -75,6 +75,7 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { motionState->setMotionType(motionType); switch(motionType) { case MOTION_TYPE_KINEMATIC: { + qDebug() << " --> KINEMATIC"; if (!body) { btCollisionShape* shape = motionState->getShape(); assert(shape); @@ -91,6 +92,8 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { break; } case MOTION_TYPE_DYNAMIC: { + qDebug() << " --> DYNAMIC"; + mass = motionState->getMass(); btCollisionShape* shape = motionState->getShape(); assert(shape); @@ -117,6 +120,8 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { } case MOTION_TYPE_STATIC: default: { + qDebug() << " --> STATIC"; + if (!body) { assert(motionState->getShape()); body = new btRigidBody(mass, motionState, motionState->getShape(), inertia); diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 2a3cb4af47..6edf80ab98 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -174,6 +174,66 @@ glm::vec3 SpatiallyNestable::worldToLocal(const glm::vec3& position, return result.getTranslation(); } +glm::vec3 SpatiallyNestable::worldPositionToParent(const glm::vec3& position) { + bool success; + glm::vec3 result = SpatiallyNestable::worldToLocal(position, getParentID(), getParentJointIndex(), success); + if (!success) { + qDebug() << "Warning -- worldToLocal failed" << getID(); + } + return result; +} + +glm::vec3 SpatiallyNestable::worldVelocityToLocal(const glm::vec3& velocity, // can be linear or angular + const QUuid& parentID, int parentJointIndex, + bool& success) { + Transform result; + QSharedPointer parentFinder = DependencyManager::get(); + if (!parentFinder) { + success = false; + return glm::vec3(); + } + + Transform parentTransform; + auto parentWP = parentFinder->find(parentID, success); + if (!success) { + return glm::vec3(); + } + + auto parent = parentWP.lock(); + if (!parentID.isNull() && !parent) { + success = false; + return glm::vec3(); + } + + if (parent) { + parentTransform = parent->getTransform(parentJointIndex, success); + if (!success) { + return glm::vec3(); + } + parentTransform.setScale(1.0f); // TODO: scale + } + success = true; + + parentTransform.setTranslation(glm::vec3(0.0f)); + + Transform velocityTransform; + velocityTransform.setTranslation(velocity); + Transform myWorldTransform; + Transform::mult(myWorldTransform, parentTransform, velocityTransform); + myWorldTransform.setTranslation(velocity); + Transform::inverseMult(result, parentTransform, myWorldTransform); + return result.getTranslation(); +} + +glm::vec3 SpatiallyNestable::worldVelocityToParent(const glm::vec3& velocity) { + bool success; + glm::vec3 result = SpatiallyNestable::worldVelocityToLocal(velocity, getParentID(), getParentJointIndex(), success); + if (!success) { + qDebug() << "Warning -- worldVelocityToLocal failed" << getID(); + } + return result; +} + glm::quat SpatiallyNestable::worldToLocal(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success) { @@ -214,6 +274,15 @@ glm::quat SpatiallyNestable::worldToLocal(const glm::quat& orientation, return result.getRotation(); } +glm::quat SpatiallyNestable::worldRotationToParent(const glm::quat& orientation) { + bool success; + glm::quat result = SpatiallyNestable::worldToLocal(orientation, getParentID(), getParentJointIndex(), success); + if (!success) { + qDebug() << "Warning -- worldToLocal failed" << getID(); + } + return result; +} + glm::vec3 SpatiallyNestable::localToWorld(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success) { diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 04bb57a688..ffb00ac040 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -46,11 +46,17 @@ public: virtual void setParentJointIndex(quint16 parentJointIndex); static glm::vec3 worldToLocal(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success); + static glm::vec3 worldVelocityToLocal(const glm::vec3& position, const QUuid& parentID, + int parentJointIndex, bool& success); static glm::quat worldToLocal(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success); static glm::vec3 localToWorld(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success); static glm::quat localToWorld(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success); + glm::vec3 worldPositionToParent(const glm::vec3& position); + glm::vec3 worldVelocityToParent(const glm::vec3& velocity); + glm::quat worldRotationToParent(const glm::quat& orientation); + // world frame virtual const Transform getTransform(bool& success, int depth = 0) const; virtual void setTransform(const Transform& transform, bool& success); @@ -144,6 +150,15 @@ public: bool hasAncestorOfType(NestableType nestableType); + void getLocalTransformAndVelocities(Transform& localTransform, + glm::vec3& localVelocity, + glm::vec3& localAngularVelocity) const; + + void setLocalTransformAndVelocities( + const Transform& localTransform, + const glm::vec3& localVelocity, + const glm::vec3& localAngularVelocity); + protected: const NestableType _nestableType; // EntityItem or an AvatarData QUuid _id; @@ -151,13 +166,6 @@ protected: quint16 _parentJointIndex { 0 }; // which joint of the parent is this relative to? SpatiallyNestablePointer getParentPointer(bool& success) const; - void getLocalTransformAndVelocities(Transform& localTransform, glm::vec3& localVelocity, glm::vec3& localAngularVelocity) const; - - void setLocalTransformAndVelocities( - const Transform& localTransform, - const glm::vec3& localVelocity, - const glm::vec3& localAngularVelocity); - mutable SpatiallyNestableWeakPointer _parent; virtual void beParentOfChild(SpatiallyNestablePointer newChild) const; From 41c399897a2eec5d3ad3ed43d59aa12c1879f0ed Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 10 Jun 2016 12:08:13 -0700 Subject: [PATCH 0496/1237] remove some debug prints --- libraries/physics/src/EntityMotionState.cpp | 2 ++ libraries/physics/src/PhysicsEngine.cpp | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 28f7756d9a..e5957ce7b2 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -613,6 +613,8 @@ uint32_t EntityMotionState::getIncomingDirtyFlags() { // we add DIRTY_MOTION_TYPE if the body's motion type disagrees with entity velocity settings int bodyFlags = _body->getCollisionFlags(); bool isMoving = _entity->isMovingRelativeToParent(); + + // XXX what's right, here? if (((bodyFlags & btCollisionObject::CF_STATIC_OBJECT) && isMoving) // || // (bodyFlags & btCollisionObject::CF_KINEMATIC_OBJECT && !isMoving) ) { diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 6f8acfa6a7..d3247ec62c 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -75,7 +75,6 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { motionState->setMotionType(motionType); switch(motionType) { case MOTION_TYPE_KINEMATIC: { - qDebug() << " --> KINEMATIC"; if (!body) { btCollisionShape* shape = motionState->getShape(); assert(shape); @@ -92,8 +91,6 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { break; } case MOTION_TYPE_DYNAMIC: { - qDebug() << " --> DYNAMIC"; - mass = motionState->getMass(); btCollisionShape* shape = motionState->getShape(); assert(shape); @@ -120,8 +117,6 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { } case MOTION_TYPE_STATIC: default: { - qDebug() << " --> STATIC"; - if (!body) { assert(motionState->getShape()); body = new btRigidBody(mass, motionState, motionState->getShape(), inertia); From da98ee0916b904cd963f05c67c2a7c8264d0baee Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 10 Jun 2016 13:42:19 -0700 Subject: [PATCH 0497/1237] reorganized procedural shader code, added getWorldEyeWorldPos(), removed iWorldEyePosition --- .../src/RenderableProceduralItemShader.h | 382 ------------------ libraries/gpu/src/gpu/Transform.slh | 4 + .../procedural/src/procedural/Procedural.cpp | 13 +- .../procedural/src/procedural/Procedural.h | 1 - ...oceduralShaders.h => ProceduralCommon.slf} | 22 +- .../src/procedural/ProceduralSkybox.cpp | 2 +- 6 files changed, 16 insertions(+), 408 deletions(-) delete mode 100644 libraries/entities-renderer/src/RenderableProceduralItemShader.h rename libraries/procedural/src/procedural/{ProceduralShaders.h => ProceduralCommon.slf} (94%) diff --git a/libraries/entities-renderer/src/RenderableProceduralItemShader.h b/libraries/entities-renderer/src/RenderableProceduralItemShader.h deleted file mode 100644 index d7df43de14..0000000000 --- a/libraries/entities-renderer/src/RenderableProceduralItemShader.h +++ /dev/null @@ -1,382 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015/09/05 -// Copyright 2013-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 -// - -// Shader includes portions of webgl-noise: -// Description : Array and textureless GLSL 2D/3D/4D simplex -// noise functions. -// Author : Ian McEwan, Ashima Arts. -// Maintainer : ijm -// Lastmod : 20110822 (ijm) -// License : Copyright (C) 2011 Ashima Arts. All rights reserved. -// Distributed under the MIT License. See LICENSE file. -// https://github.com/ashima/webgl-noise -// - - -const QString SHADER_COMMON = R"SHADER( -layout(location = 0) out vec4 _fragColor0; -layout(location = 1) out vec4 _fragColor1; -layout(location = 2) out vec4 _fragColor2; - -// the alpha threshold -uniform float alphaThreshold; - -vec2 signNotZero(vec2 v) { - return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); -} - -vec2 float32x3_to_oct(in vec3 v) { - vec2 p = v.xy * (1.0 / (abs(v.x) + abs(v.y) + abs(v.z))); - return ((v.z <= 0.0) ? ((1.0 - abs(p.yx)) * signNotZero(p)) : p); -} - - -vec3 oct_to_float32x3(in vec2 e) { - vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); - if (v.z < 0) { - v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); - } - return normalize(v); -} - -vec3 snorm12x2_to_unorm8x3(vec2 f) { - vec2 u = vec2(round(clamp(f, -1.0, 1.0) * 2047.0 + 2047.0)); - float t = floor(u.y / 256.0); - - return floor(vec3( - u.x / 16.0, - fract(u.x / 16.0) * 256.0 + t, - u.y - t * 256.0 - )) / 255.0; -} - -vec2 unorm8x3_to_snorm12x2(vec3 u) { - u *= 255.0; - u.y *= (1.0 / 16.0); - vec2 s = vec2( u.x * 16.0 + floor(u.y), - fract(u.y) * (16.0 * 256.0) + u.z); - return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); -} - -float mod289(float x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec2 mod289(vec2 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec3 mod289(vec3 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 mod289(vec4 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -float permute(float x) { - return mod289(((x*34.0)+1.0)*x); -} - -vec3 permute(vec3 x) { - return mod289(((x*34.0)+1.0)*x); -} - -vec4 permute(vec4 x) { - return mod289(((x*34.0)+1.0)*x); -} - -float taylorInvSqrt(float r) { - return 1.79284291400159 - 0.85373472095314 * r; -} - -vec4 taylorInvSqrt(vec4 r) { - return 1.79284291400159 - 0.85373472095314 * r; -} - -vec4 grad4(float j, vec4 ip) { - const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0); - vec4 p, s; - - p.xyz = floor(fract(vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0; - p.w = 1.5 - dot(abs(p.xyz), ones.xyz); - s = vec4(lessThan(p, vec4(0.0))); - p.xyz = p.xyz + (s.xyz * 2.0 - 1.0) * s.www; - - return p; -} - -// (sqrt(5) - 1)/4 = F4, used once below -#define F4 0.309016994374947451 - -float snoise(vec4 v) { - const vec4 C = vec4(0.138196601125011, // (5 - sqrt(5))/20 G4 - 0.276393202250021, // 2 * G4 - 0.414589803375032, // 3 * G4 - -0.447213595499958); // -1 + 4 * G4 - - // First corner - vec4 i = floor(v + dot(v, vec4(F4))); - vec4 x0 = v - i + dot(i, C.xxxx); - - // Other corners - - // Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI) - vec4 i0; - vec3 isX = step(x0.yzw, x0.xxx); - vec3 isYZ = step(x0.zww, x0.yyz); - i0.x = isX.x + isX.y + isX.z; - i0.yzw = 1.0 - isX; - i0.y += isYZ.x + isYZ.y; - i0.zw += 1.0 - isYZ.xy; - i0.z += isYZ.z; - i0.w += 1.0 - isYZ.z; - - // i0 now contains the unique values 0,1,2,3 in each channel - vec4 i3 = clamp(i0, 0.0, 1.0); - vec4 i2 = clamp(i0 - 1.0, 0.0, 1.0); - vec4 i1 = clamp(i0 - 2.0, 0.0, 1.0); - - vec4 x1 = x0 - i1 + C.xxxx; - vec4 x2 = x0 - i2 + C.yyyy; - vec4 x3 = x0 - i3 + C.zzzz; - vec4 x4 = x0 + C.wwww; - - // Permutations - i = mod289(i); - float j0 = permute(permute(permute(permute(i.w) + i.z) + i.y) + i.x); - vec4 j1 = permute( - permute( - permute( - permute(i.w + vec4(i1.w, i2.w, i3.w, 1.0)) + i.z - + vec4(i1.z, i2.z, i3.z, 1.0)) + i.y - + vec4(i1.y, i2.y, i3.y, 1.0)) + i.x - + vec4(i1.x, i2.x, i3.x, 1.0)); - - // Gradients: 7x7x6 points over a cube, mapped onto a 4-cross polytope - // 7*7*6 = 294, which is close to the ring size 17*17 = 289. - vec4 ip = vec4(1.0 / 294.0, 1.0 / 49.0, 1.0 / 7.0, 0.0); - - vec4 p0 = grad4(j0, ip); - vec4 p1 = grad4(j1.x, ip); - vec4 p2 = grad4(j1.y, ip); - vec4 p3 = grad4(j1.z, ip); - vec4 p4 = grad4(j1.w, ip); - - // Normalise gradients - vec4 norm = taylorInvSqrt( - vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); - p0 *= norm.x; - p1 *= norm.y; - p2 *= norm.z; - p3 *= norm.w; - p4 *= taylorInvSqrt(dot(p4, p4)); - - // Mix contributions from the five corners - vec3 m0 = max(0.6 - vec3(dot(x0, x0), dot(x1, x1), dot(x2, x2)), 0.0); - vec2 m1 = max(0.6 - vec2(dot(x3, x3), dot(x4, x4)), 0.0); - m0 = m0 * m0; - m1 = m1 * m1; - return 49.0 - * (dot(m0 * m0, vec3(dot(p0, x0), dot(p1, x1), dot(p2, x2))) - + dot(m1 * m1, vec2(dot(p3, x3), dot(p4, x4)))); - -} - -float snoise(vec3 v) { - const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); - const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); - - // First corner - vec3 i = floor(v + dot(v, C.yyy)); - vec3 x0 = v - i + dot(i, C.xxx); - - // Other corners - vec3 g = step(x0.yzx, x0.xyz); - vec3 l = 1.0 - g; - vec3 i1 = min(g.xyz, l.zxy); - vec3 i2 = max(g.xyz, l.zxy); - - vec3 x1 = x0 - i1 + C.xxx; - vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y - vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y - - // Permutations - i = mod289(i); - vec4 p = permute( - permute( - permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y - + vec4(0.0, i1.y, i2.y, 1.0)) + i.x - + vec4(0.0, i1.x, i2.x, 1.0)); - - // Gradients: 7x7 points over a square, mapped onto an octahedron. - // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) - float n_ = 0.142857142857; // 1.0/7.0 - vec3 ns = n_ * D.wyz - D.xzx; - - vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) - - vec4 x_ = floor(j * ns.z); - vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) - - vec4 x = x_ * ns.x + ns.yyyy; - vec4 y = y_ * ns.x + ns.yyyy; - vec4 h = 1.0 - abs(x) - abs(y); - - vec4 b0 = vec4(x.xy, y.xy); - vec4 b1 = vec4(x.zw, y.zw); - - //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; - //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; - vec4 s0 = floor(b0) * 2.0 + 1.0; - vec4 s1 = floor(b1) * 2.0 + 1.0; - vec4 sh = -step(h, vec4(0.0)); - - vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; - vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; - - vec3 p0 = vec3(a0.xy, h.x); - vec3 p1 = vec3(a0.zw, h.y); - vec3 p2 = vec3(a1.xy, h.z); - vec3 p3 = vec3(a1.zw, h.w); - - //Normalise gradients - vec4 norm = taylorInvSqrt( - vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); - p0 *= norm.x; - p1 *= norm.y; - p2 *= norm.z; - p3 *= norm.w; - - // Mix final noise value - vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), - 0.0); - m = m * m; - return 42.0 - * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); -} - -float snoise(vec2 v) { - const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0 - 0.366025403784439, // 0.5*(sqrt(3.0)-1.0) - -0.577350269189626, // -1.0 + 2.0 * C.x - 0.024390243902439); // 1.0 / 41.0 - // First corner - vec2 i = floor(v + dot(v, C.yy)); - vec2 x0 = v - i + dot(i, C.xx); - - // Other corners - vec2 i1; - i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); - vec4 x12 = x0.xyxy + C.xxzz; - x12.xy -= i1; - - // Permutations - i = mod289(i); // Avoid truncation effects in permutation - vec3 p = permute( - permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); - - vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), - 0.0); - m = m * m; - m = m * m; - - // Gradients: 41 points uniformly over a line, mapped onto a diamond. - // The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287) - - vec3 x = 2.0 * fract(p * C.www) - 1.0; - vec3 h = abs(x) - 0.5; - vec3 ox = floor(x + 0.5); - vec3 a0 = x - ox; - - // Normalise gradients implicitly by scaling m - // Approximation of: m *= inversesqrt( a0*a0 + h*h ); - m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); - - // Compute final noise value at P - vec3 g; - g.x = a0.x * x0.x + h.x * x0.y; - g.yz = a0.yz * x12.xz + h.yz * x12.yw; - return 130.0 * dot(m, g); -} - -// the interpolated normal -in vec3 _normal; -in vec3 _color; -in vec2 _texCoord0; -in vec4 _position; - -// TODO add more uniforms -uniform float iGlobalTime; // shader playback time (in seconds) -uniform vec3 iWorldScale; // the dimensions of the object being rendered -uniform mat3 iWorldOrientation; // the orientation of the object being rendered -uniform vec3 iWorldEyePosition; // the world position of the eye - -// TODO add support for textures -// TODO document available inputs other than the uniforms -// TODO provide world scale in addition to the untransformed position - -const vec3 DEFAULT_SPECULAR = vec3(0.1); -const float DEFAULT_SHININESS = 10; - -)SHADER"; - -// V1 shaders, only support emissive -// vec4 getProceduralColor() -const QString SHADER_TEMPLATE_V1 = SHADER_COMMON + R"SCRIBE( - -#line 1001 -%1 -#line 317 - -void main(void) { - vec4 emissive = getProceduralColor(); - - float alpha = emissive.a; - if (alpha != 1.0) { - discard; - } - - vec4 diffuse = vec4(_color.rgb, alpha); - vec4 normal = vec4(packNormal(normalize(_normal)), 0.5); - - _fragColor0 = diffuse; - _fragColor1 = normal; - _fragColor2 = vec4(emissive.rgb, DEFAULT_SHININESS / 128.0); -} - -)SCRIBE"; - -// void getProceduralDiffuseAndEmissive(out vec4 diffuse, out vec4 emissive) -const QString SHADER_TEMPLATE_V2 = SHADER_COMMON + R"SCRIBE( -// FIXME should we be doing the swizzle here? -vec3 iResolution = iWorldScale.xzy; - -// FIXME Mouse X,Y coordinates, and Z,W are for the click position if clicked (not supported in High Fidelity at the moment) -vec4 iMouse = vec4(0); - -// FIXME We set the seconds (iDate.w) of iDate to iGlobalTime, which contains the current date in seconds -vec4 iDate = vec4(0, 0, 0, iGlobalTime); - - -#line 1001 -%1 -#line 351 - -void main(void) { - vec3 diffuse = _color.rgb; - vec3 specular = DEFAULT_SPECULAR; - float shininess = DEFAULT_SHININESS; - - float emissiveAmount = getProceduralColors(diffuse, specular, shininess); - - _fragColor0 = vec4(diffuse.rgb, 1.0); - _fragColor1 = vec4(packNormal(normalize(_normal.xyz)), 1.0 - (emissiveAmount / 2.0)); - _fragColor2 = vec4(specular, shininess / 128.0); -} -)SCRIBE"; diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index 246167e186..20629c5f32 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -27,6 +27,10 @@ layout(std140) uniform transformCameraBuffer { TransformCamera getTransformCamera() { return _camera; } + +vec3 getEyeWorldPos() { + return _camera._viewInverse[3].xyz; +} <@endfunc@> diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index ed3d5712e3..c9ed959087 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -19,7 +19,7 @@ #include #include -#include "ProceduralShaders.h" +#include "ProceduralCommon_frag.h" // Userdata parsing constants static const QString PROCEDURAL_USER_DATA_KEY = "ProceduralEntity"; @@ -40,7 +40,6 @@ static const std::string STANDARD_UNIFORM_NAMES[Procedural::NUM_STANDARD_UNIFORM "iWorldScale", "iWorldPosition", "iWorldOrientation", - "iWorldEyePosition", "iChannelResolution" }; @@ -231,7 +230,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm std::string fragmentShaderSource = _fragmentSource; size_t replaceIndex = fragmentShaderSource.find(PROCEDURAL_COMMON_BLOCK); if (replaceIndex != std::string::npos) { - fragmentShaderSource.replace(replaceIndex, PROCEDURAL_COMMON_BLOCK.size(), SHADER_COMMON); + fragmentShaderSource.replace(replaceIndex, PROCEDURAL_COMMON_BLOCK.size(), ProceduralCommon_frag); } replaceIndex = fragmentShaderSource.find(PROCEDURAL_VERSION); @@ -421,14 +420,6 @@ void Procedural::setupUniforms() { batch._glUniform(_standardUniformSlots[POSITION], _entityPosition); }); } - - if (gpu::Shader::INVALID_LOCATION != _standardUniformSlots[EYE_POSITION]) { - // FIXME move into the 'set once' section, since this doesn't change over time - _uniforms.push_back([=](gpu::Batch& batch) { - batch._glUniform(_standardUniformSlots[EYE_POSITION], _eyePos); - }); - } - } void Procedural::setupChannels(bool shouldCreate) { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 45dcfd7bf6..e551afe004 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -57,7 +57,6 @@ public: SCALE, POSITION, ORIENTATION, - EYE_POSITION, CHANNEL_RESOLUTION, NUM_STANDARD_UNIFORMS }; diff --git a/libraries/procedural/src/procedural/ProceduralShaders.h b/libraries/procedural/src/procedural/ProceduralCommon.slf similarity index 94% rename from libraries/procedural/src/procedural/ProceduralShaders.h rename to libraries/procedural/src/procedural/ProceduralCommon.slf index 00571c3a94..09fb1b0b04 100644 --- a/libraries/procedural/src/procedural/ProceduralShaders.h +++ b/libraries/procedural/src/procedural/ProceduralCommon.slf @@ -1,3 +1,5 @@ +<@include gpu/Config.slh@> +// Generated on <$_SCRIBE_DATE$> // // Created by Bradley Austin Davis on 2015/09/05 // Copyright 2013-2015 High Fidelity, Inc. @@ -17,8 +19,8 @@ // https://github.com/ashima/webgl-noise // - -const std::string SHADER_COMMON = R"SHADER( +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> float mod289(float x) { return x - floor(x * (1.0 / 289.0)) * 289.0; @@ -262,11 +264,6 @@ float snoise(vec2 v) { return 130.0 * dot(m, g); } -// shader playback time (in seconds) -uniform float iGlobalTime; -// the dimensions of the object being rendered -uniform vec3 iWorldScale; - #define PROCEDURAL 1 //PROCEDURAL_VERSION @@ -286,17 +283,16 @@ const float iSampleRate = 1.0; const vec4 iChannelTime = vec4(0.0); +uniform float iGlobalTime; // shader playback time (in seconds) uniform vec4 iDate; uniform int iFrameCount; -uniform vec3 iWorldPosition; -uniform mat3 iWorldOrientation; -uniform vec3 iWorldEyePosition; +uniform vec3 iWorldPosition; // the position of the object being rendered +uniform vec3 iWorldScale; // the dimensions of the object being rendered +uniform mat3 iWorldOrientation; // the orientation of the object being rendered uniform vec3 iChannelResolution[4]; uniform sampler2D iChannel0; uniform sampler2D iChannel1; uniform sampler2D iChannel2; uniform sampler2D iChannel3; -#endif - -)SHADER"; +#endif \ No newline at end of file diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 5905bfb017..7ba711bdb1 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -52,7 +52,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setModelTransform(Transform()); // only for Mac auto& procedural = skybox._procedural; - procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(1, 0, 0, 0), glm::vec3(0)); + procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(), glm::vec3(0)); auto textureSlot = procedural.getShader()->getTextures().findLocation("cubeMap"); auto bufferSlot = procedural.getShader()->getBuffers().findLocation("skyboxBuffer"); skybox.prepare(batch, textureSlot, bufferSlot); From f32e29ac2df13b536586932a2b8d1bf0671ab759 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 10 Jun 2016 13:46:02 -0700 Subject: [PATCH 0498/1237] small changes --- libraries/gpu/src/gpu/Batch.h | 2 +- libraries/gpu/src/gpu/Context.h | 2 +- libraries/procedural/src/procedural/ProceduralCommon.slf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index e0a0e057f7..4e51038368 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -453,7 +453,7 @@ public: Params _params; Bytes _data; - // SSBO class... layout MUST match the layout in TransformCamera.slh + // SSBO class... layout MUST match the layout in Transform.slh class TransformObject { public: Mat4 _model; diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index cead2c623d..652338f911 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -95,7 +95,7 @@ public: virtual void syncCache() = 0; virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) = 0; - // UBO class... layout MUST match the layout in TransformCamera.slh + // UBO class... layout MUST match the layout in Transform.slh class TransformCamera { public: mutable Mat4 _view; diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slf b/libraries/procedural/src/procedural/ProceduralCommon.slf index 09fb1b0b04..128723265f 100644 --- a/libraries/procedural/src/procedural/ProceduralCommon.slf +++ b/libraries/procedural/src/procedural/ProceduralCommon.slf @@ -23,7 +23,7 @@ <$declareStandardCameraTransform()$> float mod289(float x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; + return x - floor(x * (1.0 / 289.0)) * 289.0; } vec2 mod289(vec2 x) { From 53435fc7301b3ec0dea8779e60b784305858a515 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 10 Jun 2016 14:04:33 -0700 Subject: [PATCH 0499/1237] removed extra argument --- libraries/entities-renderer/src/RenderableShapeEntityItem.cpp | 2 +- libraries/procedural/src/procedural/Procedural.cpp | 3 +-- libraries/procedural/src/procedural/Procedural.h | 3 +-- libraries/procedural/src/procedural/ProceduralSkybox.cpp | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 2b04478831..ec07e10ccf 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -98,7 +98,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { } batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation if (_procedural->ready()) { - _procedural->prepare(batch, getPosition(), getDimensions(), getOrientation(), args->getViewFrustum().getPosition()); + _procedural->prepare(batch, getPosition(), getDimensions(), getOrientation()); auto outColor = _procedural->getColor(color); batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); DependencyManager::get()->renderShape(batch, MAPPING[_shape]); diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index c9ed959087..a9a297a2e1 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -203,11 +203,10 @@ bool Procedural::ready() { return true; } -void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const glm::vec3& eyePos) { +void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation) { _entityDimensions = size; _entityPosition = position; _entityOrientation = glm::mat3_cast(orientation); - _eyePos = eyePos; if (_shaderUrl.isLocalFile()) { auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index e551afe004..6991b47946 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -38,7 +38,7 @@ public: void parse(const QString& userDataJson); bool ready(); - void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const glm::vec3& eyePos); + void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation); const gpu::ShaderPointer& getShader() const { return _shader; } glm::vec4 getColor(const glm::vec4& entityColor); @@ -95,7 +95,6 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; - glm::vec3 _eyePos; private: // This should only be called from the render thread, as it shares data with Procedural::prepare diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 7ba711bdb1..9e9a26d902 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -52,7 +52,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setModelTransform(Transform()); // only for Mac auto& procedural = skybox._procedural; - procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(), glm::vec3(0)); + procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat()); auto textureSlot = procedural.getShader()->getTextures().findLocation("cubeMap"); auto bufferSlot = procedural.getShader()->getBuffers().findLocation("skyboxBuffer"); skybox.prepare(batch, textureSlot, bufferSlot); From 7b21d711804e45cdbb121ecca0f9e189b2665f8e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 10 Jun 2016 14:09:04 -0700 Subject: [PATCH 0500/1237] try to fix cmake error --- libraries/procedural/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt index 7145f7de5c..a2c1a019de 100644 --- a/libraries/procedural/CMakeLists.txt +++ b/libraries/procedural/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME procedural) -AUTOSCRIBE_SHADER_LIB(gpu model) +AUTOSCRIBE_SHADER_LIB(gpu model procedural) setup_hifi_library() link_hifi_libraries(shared gpu gpu-gl networking model model-networking) From 4344a35c60f421e99e48df11612ac75c1cd21f28 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 10 Jun 2016 16:00:16 -0700 Subject: [PATCH 0501/1237] Use the new system pointer functionality. --- .../controllers/handControllerPointer.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 7b45babf4d..1728647e5e 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -305,13 +305,19 @@ var leftTrigger = new Trigger(); var rightTrigger = new Trigger(); var activeTrigger = rightTrigger; var activeHand = Controller.Standard.RightHand; +var LEFT_HUD_LASER = 1; +var RIGHT_HUD_LASER = 2; +var BOTH_HUD_LASERS = LEFT_HUD_LASER + RIGHT_HUD_LASER; +var activeHudLaser = RIGHT_HUD_LASER; function toggleHand() { // unequivocally switch which hand controls mouse position if (activeHand === Controller.Standard.RightHand) { activeHand = Controller.Standard.LeftHand; activeTrigger = leftTrigger; + activeHudLaser = LEFT_HUD_LASER; } else { activeHand = Controller.Standard.RightHand; activeTrigger = rightTrigger; + activeHudLaser = RIGHT_HUD_LASER; } } function makeToggleAction(hand) { // return a function(0|1) that makes the specified hand control mouse when 1 @@ -342,6 +348,7 @@ clickMapping.enable(); // Same properties as handControllerGrab search sphere var BALL_SIZE = 0.011; var BALL_ALPHA = 0.5; +var LASER_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: BALL_ALPHA}; var fakeProjectionBall = Overlays.addOverlay("sphere", { size: 5 * BALL_SIZE, color: {red: 255, green: 10, blue: 10}, @@ -356,9 +363,24 @@ Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. +var systemLaserOn = false; +var SYSTEM_LASER_DIRECTION = Vec3.normalize({x: 0, y: -1, z: -1}); // Guessing 45 degrees. +function clearSystemLaser() { + if (!systemLaserOn) { + return; + } + print('FIXME remove: disableHandLasers', BOTH_HUD_LASERS); + HMD.disableHandLasers(BOTH_HUD_LASERS); + systemLaserOn = false; +} function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD if (!optionalEnableClicks) { expireMouseCursor(); + clearSystemLaser(); + } else if (!systemLaserOn) { + print('FIXME remove: setHandLasers', activeHudLaser, true, JSON.stringify(LASER_COLOR_XYZW), JSON.stringify(SYSTEM_LASER_DIRECTION)); + HMD.setHandLasers(activeHudLaser, true, LASER_COLOR_XYZW, SYSTEM_LASER_DIRECTION); + systemLaserOn = true; } if (!visualizationIsShowing) { return; @@ -371,6 +393,7 @@ function turnOffVisualization(optionalEnableClicks) { // because we're showing c var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { ignore(controllerPosition, controllerDirection, hudPosition2d); + clearSystemLaser(); // Show an indication of where the cursor will appear when crossing a HUD element, // and where in-world clicking will occur. // From 8a682450a9b88b0feb7e881bf2b238249d36ff0f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 10 Jun 2016 17:01:22 -0700 Subject: [PATCH 0502/1237] still trying to fix cmake errors --- libraries/entities-renderer/CMakeLists.txt | 2 +- libraries/gpu-gl/CMakeLists.txt | 1 - libraries/procedural/CMakeLists.txt | 2 +- libraries/render-utils/CMakeLists.txt | 2 +- libraries/render/CMakeLists.txt | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index bb90c04c95..0063f4a701 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME entities-renderer) -AUTOSCRIBE_SHADER_LIB(gpu model render render-utils) +AUTOSCRIBE_SHADER_LIB(gpu model procedural render render-utils) setup_hifi_library(Widgets Network Script) link_hifi_libraries(shared gpu procedural model model-networking script-engine render render-utils) diff --git a/libraries/gpu-gl/CMakeLists.txt b/libraries/gpu-gl/CMakeLists.txt index dfac6dd516..398fdd04d6 100644 --- a/libraries/gpu-gl/CMakeLists.txt +++ b/libraries/gpu-gl/CMakeLists.txt @@ -1,5 +1,4 @@ set(TARGET_NAME gpu-gl) -AUTOSCRIBE_SHADER_LIB(gpu) setup_hifi_library() link_hifi_libraries(shared gl gpu) GroupSources("src") diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt index a2c1a019de..7145f7de5c 100644 --- a/libraries/procedural/CMakeLists.txt +++ b/libraries/procedural/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME procedural) -AUTOSCRIBE_SHADER_LIB(gpu model procedural) +AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() link_hifi_libraries(shared gpu gpu-gl networking model model-networking) diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index c4f28aeffd..2a7d33e33a 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME render-utils) -AUTOSCRIBE_SHADER_LIB(gpu model render) +AUTOSCRIBE_SHADER_LIB(gpu model render procedural) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index 76fc8303ce..c5cfdf3668 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME render) -AUTOSCRIBE_SHADER_LIB(gpu model) +AUTOSCRIBE_SHADER_LIB(gpu model procedural) setup_hifi_library() link_hifi_libraries(shared gpu model) From 28886f6ec3fa3c2a26e41c85317a817fc25fb4d3 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 10 Jun 2016 17:18:12 -0700 Subject: [PATCH 0503/1237] Many improvments --- .../render-utils/src/DebugDeferredBuffer.cpp | 29 ++++++- .../render-utils/src/DebugDeferredBuffer.h | 6 +- libraries/render-utils/src/FramebufferCache.h | 3 + .../render-utils/src/RenderDeferredTask.cpp | 8 +- .../render-utils/src/SubsurfaceScattering.cpp | 85 +++++++++++++++++-- .../render-utils/src/SubsurfaceScattering.h | 13 ++- .../render-utils/src/SurfaceGeometryPass.h | 2 +- .../src/debug_deferred_buffer.slf | 1 + .../subsurfaceScattering_drawScattering.slf | 11 +-- .../src/surfaceGeometry_makeCurvature.slf | 28 +++++- libraries/render/src/render/BlurTask.cpp | 4 +- libraries/render/src/render/BlurTask.h | 9 -- libraries/render/src/render/CullTask.h | 10 --- libraries/render/src/render/Task.h | 25 ++++++ .../utilities/render/framebuffer.qml | 2 + .../utilities/render/surfaceGeometryPass.qml | 14 +++ 16 files changed, 206 insertions(+), 44 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index b67ed5b002..0e007b8314 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -48,6 +48,8 @@ enum Slot { Shadow, Pyramid, Curvature, + DiffusedCurvature, + Scattering, AmbientOcclusion, AmbientOcclusionBlurred }; @@ -147,6 +149,21 @@ static const std::string DEFAULT_CURVATURE_SHADER{ " }" }; +static const std::string DEFAULT_NORMAL_CURVATURE_SHADER{ + "vec4 getFragmentColor() {" + //" return vec4(pow(vec3(texture(curvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" + " return vec4(pow(vec3(texture(curvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" + " }" +}; + +static const std::string DEFAULT_SCATTERING_SHADER{ + "vec4 getFragmentColor() {" + // " return vec4(pow(vec3(texture(scatteringMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + " return vec4(vec3(texture(scatteringMap, uv).xyz), 1.0);" + " }" +}; + static const std::string DEFAULT_AMBIENT_OCCLUSION_SHADER{ "vec4 getFragmentColor() {" " return vec4(vec3(texture(obscuranceMap, uv).x), 1.0);" @@ -214,6 +231,10 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_PYRAMID_DEPTH_SHADER; case CurvatureMode: return DEFAULT_CURVATURE_SHADER; + case NormalCurvatureMode: + return DEFAULT_NORMAL_CURVATURE_SHADER; + case ScatteringMode: + return DEFAULT_SCATTERING_SHADER; case AmbientOcclusionMode: return DEFAULT_AMBIENT_OCCLUSION_SHADER; case AmbientOcclusionBlurredMode: @@ -269,6 +290,8 @@ const gpu::PipelinePointer& DebugDeferredBuffer::getPipeline(Mode mode, std::str slotBindings.insert(gpu::Shader::Binding("shadowMap", Shadow)); slotBindings.insert(gpu::Shader::Binding("pyramidMap", Pyramid)); slotBindings.insert(gpu::Shader::Binding("curvatureMap", Curvature)); + slotBindings.insert(gpu::Shader::Binding("diffusedCurvatureMap", DiffusedCurvature)); + slotBindings.insert(gpu::Shader::Binding("scatteringMap", Scattering)); slotBindings.insert(gpu::Shader::Binding("occlusionBlurredMap", AmbientOcclusionBlurred)); gpu::Shader::makeProgram(*program, slotBindings); @@ -294,11 +317,13 @@ void DebugDeferredBuffer::configure(const Config& config) { _size = config.size; } -void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { +void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputBuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; + auto& scatteringFramebuffer = inputBuffer; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); batch.setViewportTransform(args->_viewport); @@ -329,6 +354,8 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setResourceTexture(Shadow, lightStage.lights[0]->shadow.framebuffer->getDepthStencilBuffer()); batch.setResourceTexture(Pyramid, framebufferCache->getDepthPyramidTexture()); batch.setResourceTexture(Curvature, framebufferCache->getCurvatureTexture()); + //batch.setResourceTexture(DiffusedCurvature, diffusedCurvatureBuffer); + batch.setResourceTexture(Scattering, scatteringFramebuffer->getRenderBuffer(0)); if (DependencyManager::get()->isAmbientOcclusionEnabled()) { batch.setResourceTexture(AmbientOcclusion, framebufferCache->getOcclusionTexture()); } else { diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 0af6d589e9..fc99cae82c 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -35,12 +35,12 @@ signals: class DebugDeferredBuffer { public: using Config = DebugDeferredBufferConfig; - using JobModel = render::Job::Model; + using JobModel = render::Job::ModelI; DebugDeferredBuffer(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputBuffer); protected: friend class DebugDeferredBufferConfig; @@ -60,6 +60,8 @@ protected: ShadowMode, PyramidDepthMode, CurvatureMode, + NormalCurvatureMode, + ScatteringMode, AmbientOcclusionMode, AmbientOcclusionBlurredMode, CustomMode // Needs to stay last diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index 0fb9b9b2ee..ab1d0a555b 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -50,6 +50,9 @@ public: gpu::FramebufferPointer getCurvatureFramebuffer(); gpu::TexturePointer getCurvatureTexture(); + gpu::FramebufferPointer getScatteringFramebuffer(); + gpu::TexturePointer getScatteringTexture(); + void setAmbientOcclusionResolutionLevel(int level); gpu::FramebufferPointer getOcclusionFramebuffer(); gpu::TexturePointer getOcclusionTexture(); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 0aac515fbd..1226750e62 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -122,9 +122,12 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); + const auto scatteringFramebuffer = addJob("Scattering", deferredFrameTransform); + // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred"); + // AA job to be revisited addJob("Antialiasing"); @@ -138,12 +141,11 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DrawOverlay3DOpaque", overlayOpaques, true); addJob("DrawOverlay3DTransparent", overlayTransparents, false); - addJob("Scattering", deferredFrameTransform); - + // Debugging stages { // Debugging Deferred buffer job - addJob("DebugDeferredBuffer"); + addJob("DebugDeferredBuffer", scatteringFramebuffer); // Scene Octree Debuging job { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index d6543bfc1d..9700f4f651 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -34,6 +34,8 @@ void SubsurfaceScattering::configure(const Config& config) { if (config.depthThreshold != getCurvatureDepthThreshold()) { _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold; } + + _showLUT = config.showLUT; } @@ -66,7 +68,65 @@ gpu::PipelinePointer SubsurfaceScattering::getScatteringPipeline() { } -void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer) { +gpu::PipelinePointer _showLUTPipeline; +gpu::PipelinePointer getShowLUTPipeline(); +gpu::PipelinePointer SubsurfaceScattering::getShowLUTPipeline() { + if (!_showLUTPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + // slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SubsurfaceScattering_FrameTransformSlot)); + // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + _showLUTPipeline = gpu::Pipeline::create(program, state); + } + + return _showLUTPipeline; +} + +bool SubsurfaceScattering::updateScatteringFramebuffer(const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& scatteringFramebuffer) { + if (!sourceFramebuffer) { + return false; + } + + if (!_scatteringFramebuffer) { + _scatteringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + + // attach depthStencil if present in source + if (sourceFramebuffer->hasDepthStencil()) { + _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + _scatteringFramebuffer->setRenderBuffer(0, blurringTarget); + } else { + // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so + if ((_scatteringFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_scatteringFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { + _scatteringFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); + if (sourceFramebuffer->hasDepthStencil()) { + _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + } + } + + if (!scatteringFramebuffer) { + scatteringFramebuffer = _scatteringFramebuffer; + } + + return true; + +} + + +void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& scatteringFramebuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -76,16 +136,21 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, _scatteringTable = SubsurfaceScattering::generatePreIntegratedScattering(args); } + auto pipeline = getScatteringPipeline(); auto framebufferCache = DependencyManager::get(); - -// if (curvatureFramebuffer->getRenderBuffer(0)) + + if (!updateScatteringFramebuffer(framebufferCache->getCurvatureFramebuffer(), scatteringFramebuffer)) { + return; + } gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); - batch.setViewportTransform(args->_viewport >> 1); + batch.setViewportTransform(args->_viewport); + + batch.setFramebuffer(_scatteringFramebuffer); batch.setUniformBuffer(SubsurfaceScattering_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); @@ -94,6 +159,14 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setResourceTexture(SubsurfaceScattering_CurvatureMapSlot, framebufferCache->getCurvatureTexture()); batch.setResourceTexture(SubsurfaceScattering_ScatteringTableSlot, _scatteringTable); batch.draw(gpu::TRIANGLE_STRIP, 4); + + if (_showLUT) { + auto viewportSize = std::min(args->_viewport.z, args->_viewport.w) >> 1; + batch.setViewportTransform(glm::ivec4(0, 0, viewportSize, viewportSize)); + batch.setPipeline(getShowLUTPipeline()); + batch.setResourceTexture(0, _scatteringTable); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } }); } @@ -149,7 +222,7 @@ vec3 integrate(double cosTheta, double skinRadius) { double a = -(_PI); - double inc = 0.01; + double inc = 0.005; while (a <= (_PI)) { double sampleAngle = theta + a; @@ -306,7 +379,7 @@ gpu::TexturePointer SubsurfaceScattering::generatePreIntegratedScattering(Render const int WIDTH = 128; const int HEIGHT = 128; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT)); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); diffuseScatter(scatteringLUT); //diffuseScatterGPU(profileMap, scatteringLUT, args); return scatteringLUT; diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 85e57a4328..46fc238176 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -20,10 +20,12 @@ class SubsurfaceScatteringConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) + Q_PROPERTY(bool showLUT MEMBER showLUT NOTIFY dirty) public: SubsurfaceScatteringConfig() : render::Job::Config(true) {} float depthThreshold{ 0.1f }; + bool showLUT{ true }; signals: void dirty(); @@ -37,7 +39,7 @@ public: SubsurfaceScattering(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& scatteringFramebuffer); float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } @@ -63,9 +65,16 @@ private: gpu::TexturePointer _scatteringTable; - gpu::PipelinePointer _scatteringPipeline; + bool updateScatteringFramebuffer(const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& scatteringFramebuffer); + gpu::FramebufferPointer _scatteringFramebuffer; + + gpu::PipelinePointer _scatteringPipeline; gpu::PipelinePointer getScatteringPipeline(); + + gpu::PipelinePointer _showLUTPipeline; + gpu::PipelinePointer getShowLUTPipeline(); + bool _showLUT{ false }; }; #endif // hifi_SubsurfaceScattering_h diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 04c0276181..00d407d482 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -28,7 +28,7 @@ public: float depthThreshold{ 0.1f }; float basisScale{ 1.0f }; - float curvatureScale{ 100.0f }; + float curvatureScale{ 1.0f }; // Mean curvature value scaling (SI SI Dimension is [1/meters]) double getGpuTime() { return gpuTime; } diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index 4c045b7e99..a6028f3c95 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -18,6 +18,7 @@ uniform sampler2D pyramidMap; uniform sampler2D occlusionMap; uniform sampler2D occlusionBlurredMap; uniform sampler2D curvatureMap; +uniform sampler2D scatteringMap; in vec2 uv; out vec4 outFragColor; diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index 28d7867e51..8cadda10bc 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -65,8 +65,8 @@ vec4 fetchCurvature(vec2 texcoord) { uniform sampler2D scatteringLUT; -vec3 fetchBRDF(float curvature, float LdotN) { - return texture(scatteringLUT, vec2(curvature, LdotN)).xyz; +vec3 fetchBRDF(float LdotN, float curvature) { + return texture(scatteringLUT, vec2( LdotN * 0.5 + 0.5, curvature)).xyz; } // Scattering parameters @@ -93,9 +93,10 @@ void main(void) { float curvature = abs(diffusedCurvature.w * 2 - 1) * 0.5f * scatterCurve + scatterBase; // --> Calculate the light vector. - vec3 lightVector = normalize(vec3(-1.0f, -1.0f, -1.0f)); //normalize(lightPos - sourcePos.xyz); + vec3 lightVector = normalize(vec3(1.0f, 1.0f, 1.0f)); //normalize(lightPos - sourcePos.xyz); - + _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); +/* // --> Optimise for skin diffusion profile. float diffuseBlendedR = dot(normalize(mix( bentNormalN.xyz, bentNormalN, normalBendR * normalBendFactor)), lightVector); float diffuseBlendedG = dot(normalize(mix(normal.xyz, bentNormalN, normalBendG * normalBendFactor)), lightVector); @@ -108,7 +109,7 @@ void main(void) { vec3 bdrfB = fetchBRDF(diffuseBlendedB, curvature); vec3 bdrf = vec3( bdrfR.x, bdrfG.y, bdrfB.z); bdrf *= bdrf; - _fragColor = vec4(vec3(bdrf.xyz), 1.0); + _fragColor = vec4(vec3(bdrf.xyz), 1.0);*/ } diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index cb98819c0d..dc222dab4c 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -126,7 +126,6 @@ void main(void) { // Calculate the perspective scale. float perspectiveScale = (-getProjScaleEye() / Zeye); - float pixPerspectiveScaleInv = 1.0 / (perspectiveScale); vec2 viewportScale = perspectiveScale * getInvWidthHeight(); @@ -150,8 +149,21 @@ void main(void) { vec4 py = vec4(worldPos, 1.0) + vec4(0.0f, dist, 0.0f, 0.0f); vec4 pz = vec4(worldPos, 1.0) + vec4(0.0f, 0.0f, dist, 0.0f); + px = frameTransform._view * px; + py = frameTransform._view * py; + pz = frameTransform._view * pz; + + /* + if (texcoordPos.y > 0.5) { + outFragColor = vec4(fract(px.xyz), 1.0); + } else { + outFragColor = vec4(fract(eyePos.xyz), 1.0); + } + return; + */ + // Project px, py pz to homogeneous clip space - mat4 viewProj = getProjection(stereoSide.x) * frameTransform._view; + mat4 viewProj = getProjection(stereoSide.x); px = viewProj * px; py = viewProj * py; pz = viewProj * pz; @@ -162,7 +174,17 @@ void main(void) { pz.xy /= pz.w; vec2 nclipPos = (texcoordPos - 0.5) * 2.0; +/* + if (texcoordPos.y > 0.5) { + outFragColor = vec4(px.xy * 0.5 + 0.5, 0.0, 1.0); + } else { + outFragColor = vec4(nclipPos * 0.5 + 0.5, 0.0, 1.0); + } + return; +*/ + float pixPerspectiveScaleInv = 1.0 / (perspectiveScale); + //vec2 pixPerspectiveScaleInv = 1.0 / viewportScale; px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv; py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv; pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv; @@ -174,6 +196,6 @@ void main(void) { // Calculate the mean curvature float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333) * params.curvatureInfo.w; + outFragColor = vec4(vec3(worldNormal + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); - // outFragColor = vec4((vec3(dFdx.x, dFdy.y, dFdz.z) * params.curvatureInfo.w + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); } diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index a5a02ff78c..0511bc846c 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -279,8 +279,8 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const RenderArgs* args = renderContext->args; - auto& sourceFramebuffer = SourceAndDepth.first.template get(); - auto& depthTexture = SourceAndDepth.first.template get(); + auto& sourceFramebuffer = SourceAndDepth.getFirst(); + auto& depthTexture = SourceAndDepth.getSecond(); BlurringResources blurringResources; if (!updateBlurringResources(sourceFramebuffer, blurringResources)) { diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index e5aea599bc..9de555ff54 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -89,15 +89,6 @@ protected: bool updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources); }; - -template < class T0, class T1 > -class VaryingPair : public std::pair { -public: - using Parent = std::pair; - - VaryingPair() : Parent(Varying(T0()), T1()) {} -}; - class BlurGaussianDepthAware { public: using InputPair = VaryingPair; diff --git a/libraries/render/src/render/CullTask.h b/libraries/render/src/render/CullTask.h index e84f018e91..56729083dd 100644 --- a/libraries/render/src/render/CullTask.h +++ b/libraries/render/src/render/CullTask.h @@ -140,16 +140,6 @@ namespace render { int getNumItems() { return numItems; } }; - template < class T, int NUM > - class VaryingArray : public std::array { - public: - VaryingArray() { - for (size_t i = 0; i < NUM; i++) { - (*this)[i] = Varying(T()); - } - } - }; - template class MultiFilterItem { public: diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 300c0efd56..1efefa053a 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -57,6 +57,31 @@ protected: std::shared_ptr _concept; }; + +template < class T0, class T1 > +class VaryingPair : public std::pair { +public: + using Parent = std::pair; + + VaryingPair() : Parent(Varying(T0()), T1()) {} + + const T0& getFirst() const { return first.get(); } + T0& editFirst() { return first.edit(); } + + const T1& getSecond() const { return second.get(); } + T1& editSecond() { return second.edit(); } +}; + +template < class T, int NUM > +class VaryingArray : public std::array { +public: + VaryingArray() { + for (size_t i = 0; i < NUM; i++) { + (*this)[i] = Varying(T()); + } + } +}; + class Job; class Task; class JobNoIO {}; diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index 3207ec13ff..4ed0b7dcf0 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -48,6 +48,8 @@ Column { "Shadow", "Pyramid Depth", "Curvature", + "NormalCurvature", + "Scattering", "Ambient Occlusion", "Ambient Occlusion Blurred", "Custom Shader" diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index ab3e64a33a..ca954be40d 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -29,5 +29,19 @@ Column { } } } + + Column{ + Repeater { + model: [ "Blur Scale:filterScale:4.0" ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: false + config: Render.getConfig("DiffuseCurvature") + property: modelData.split(":")[1] + max: modelData.split(":")[2] + min: 0.0 + } + } + } } } From f6ed5a1dae57820a320fa53162960b600bebce47 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 10 Jun 2016 18:33:40 -0700 Subject: [PATCH 0504/1237] Bugfixes based on feedback * When the overlay is hidden because your head is too close to the sphere, instead of coming back immediately, it waits until the avatar's velocity is near zero for a period of time. * Hooked up jump and fly to MyAvatar::hasDriveInput() * Added an internal state machine to OverlayConductor to manage hiding/showing transitions. * The overlay menu state is now tied directly to the overlay, so it will change state as the overlay is dynamically hidden/shown from code. * Removed slot going directly from MenuOption::Overlays directly to OverlayConductor::setEnable(). --- interface/src/Menu.cpp | 3 +- interface/src/avatar/MyAvatar.cpp | 2 +- interface/src/ui/OverlayConductor.cpp | 139 +++++++++++++++++++------- interface/src/ui/OverlayConductor.h | 35 +++++-- 4 files changed, 131 insertions(+), 48 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 031564fa7a..0c1f2116d9 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -256,8 +256,7 @@ Menu::Menu() { UNSPECIFIED_POSITION, "Advanced"); // View > Overlays - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true, - qApp, SLOT(setOverlaysVisible(bool))); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true); // Navigate menu ---------------------------------- MenuWrapper* navigateMenu = addMenu("Navigate"); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 76e48d34d6..12fe7c4ac2 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2136,5 +2136,5 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; + return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; } diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 790652aa0f..01649a0b3a 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -17,13 +17,13 @@ #include "OverlayConductor.h" OverlayConductor::OverlayConductor() { + } OverlayConductor::~OverlayConductor() { } bool OverlayConductor::headOutsideOverlay() const { - glm::mat4 hmdMat = qApp->getHMDSensorPose(); glm::vec3 hmdPos = extractTranslation(hmdMat); glm::vec3 hmdForward = transformVectorFast(hmdMat, glm::vec3(0.0f, 0.0f, -1.0f)); @@ -41,7 +41,34 @@ bool OverlayConductor::headOutsideOverlay() const { return false; } -bool OverlayConductor::avatarHasDriveInput() const { +bool OverlayConductor::updateAvatarIsAtRest() { + + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + + const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s + const quint64 REST_DISABLE_TIME_USECS = 200 * 1000; // 200 ms + + const float AT_REST_THRESHOLD = 0.01f; + bool desiredAtRest = glm::length(myAvatar->getVelocity()) < AT_REST_THRESHOLD; + if (desiredAtRest != _desiredAtRest) { + // start timer + _desiredAtRestTimer = usecTimestampNow() + (desiredAtRest ? REST_ENABLE_TIME_USECS : REST_DISABLE_TIME_USECS); + } + + _desiredAtRest = desiredAtRest; + + if (_desiredAtRestTimer != 0 && usecTimestampNow() > _desiredAtRestTimer) { + // timer expired + // change state! + _currentAtRest = _desiredAtRest; + // disable timer + _desiredAtRestTimer = 0; + } + + return _currentAtRest; +} + +bool OverlayConductor::updateAvatarHasDriveInput() { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000; // 200 ms @@ -66,57 +93,88 @@ bool OverlayConductor::avatarHasDriveInput() const { return _currentDriving; } -bool OverlayConductor::shouldShowOverlay() const { - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - -#ifdef WANT_DEBUG - qDebug() << "AJT: wantsOverlays =" << Menu::getInstance()->isOptionChecked(MenuOption::Overlays) << ", clearOverlayWhenDriving =" << myAvatar->getClearOverlayWhenDriving() << - ", headOutsideOverlay =" << headOutsideOverlay() << ", hasDriveInput =" << avatarHasDriveInput(); -#endif - - return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (!myAvatar->getClearOverlayWhenDriving() || (!headOutsideOverlay() && !avatarHasDriveInput())); -} - -bool OverlayConductor::shouldRecenterOnFadeOut() const { - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && myAvatar->getClearOverlayWhenDriving() && headOutsideOverlay(); -} - void OverlayConductor::centerUI() { // place the overlay at the current hmd position in sensor space auto camMat = cancelOutRollAndPitch(qApp->getHMDSensorPose()); qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); } +bool OverlayConductor::userWishesToHide() const { + // user pressed toggle button. + return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) != _prevOverlayMenuChecked && Menu::getInstance()->isOptionChecked(MenuOption::Overlays); +} + +bool OverlayConductor::userWishesToShow() const { + // user pressed toggle button. + return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) != _prevOverlayMenuChecked && !Menu::getInstance()->isOptionChecked(MenuOption::Overlays); +} + +void OverlayConductor::setState(State state) { +#ifdef WANT_DEBUG + static QString stateToString[NumStates] = { "Enabled", "DisabledByDrive", "DisabledByHead", "DisabledByToggle" }; + qDebug() << "OverlayConductor " << stateToString[state] << "<--" << stateToString[_state]; +#endif + _state = state; +} + +OverlayConductor::State OverlayConductor::getState() const { + return _state; +} + void OverlayConductor::update(float dt) { - // centerUI if hmd mode changes + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + + // centerUI when hmd mode is first enabled if (qApp->isHMDMode() && !_hmdMode) { centerUI(); } _hmdMode = qApp->isHMDMode(); - // centerUI if timer expires - if (_fadeOutTime != 0 && usecTimestampNow() > _fadeOutTime) { - // fade out timer expired - _fadeOutTime = 0; - centerUI(); + bool prevDriving = _currentDriving; + bool isDriving = updateAvatarHasDriveInput(); + bool drivingChanged = prevDriving != isDriving; + + bool isAtRest = updateAvatarIsAtRest(); + + switch (getState()) { + case Enabled: + if (qApp->isHMDMode() && headOutsideOverlay()) { + setState(DisabledByHead); + setEnabled(false); + } + if (userWishesToHide()) { + setState(DisabledByToggle); + setEnabled(false); + } + if (drivingChanged && isDriving) { + setState(DisabledByDrive); + setEnabled(false); + } + break; + case DisabledByDrive: + if (!isDriving || userWishesToShow()) { + setState(Enabled); + setEnabled(true); + } + break; + case DisabledByHead: + if (isAtRest || userWishesToShow()) { + setState(Enabled); + setEnabled(true); + } + break; + case DisabledByToggle: + if (userWishesToShow()) { + setState(Enabled); + setEnabled(true); + } + break; + default: + break; } - bool showOverlay = shouldShowOverlay(); - - if (showOverlay != getEnabled()) { - if (showOverlay) { - // disable fadeOut timer - _fadeOutTime = 0; - } else if (shouldRecenterOnFadeOut()) { - // start fadeOut timer - const quint64 FADE_OUT_TIME_USECS = 300 * 1000; // 300 ms - _fadeOutTime = usecTimestampNow() + FADE_OUT_TIME_USECS; - } - } - - setEnabled(showOverlay); + _prevOverlayMenuChecked = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); } void OverlayConductor::setEnabled(bool enabled) { @@ -127,6 +185,11 @@ void OverlayConductor::setEnabled(bool enabled) { _enabled = enabled; // set the new value auto offscreenUi = DependencyManager::get(); offscreenUi->setPinned(!_enabled); + + // ensure that the the state of the menu item reflects the state of the overlay. + Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, _enabled); + _prevOverlayMenuChecked = _enabled; + // if the new state is visible/enabled... MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); if (_enabled && myAvatar->getClearOverlayWhenDriving() && qApp->isHMDMode()) { diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 40ccb4b91f..83d6957012 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -22,18 +22,39 @@ public: private: bool headOutsideOverlay() const; - bool avatarHasDriveInput() const; - bool shouldShowOverlay() const; - bool shouldRecenterOnFadeOut() const; + bool updateAvatarHasDriveInput(); + bool updateAvatarIsAtRest(); + bool userWishesToHide() const; + bool userWishesToShow() const; void centerUI(); - quint64 _fadeOutTime { 0 }; + enum State { + Enabled = 0, + DisabledByDrive, + DisabledByHead, + DisabledByToggle, + NumStates + }; + + void setState(State state); + State getState() const; + + State _state { DisabledByDrive }; + + bool _prevOverlayMenuChecked { true }; bool _enabled { false }; bool _hmdMode { false }; + bool _disabledFromHead { false }; - mutable quint64 _desiredDrivingTimer { 0 }; - mutable bool _desiredDriving { false }; - mutable bool _currentDriving { false }; + // used by updateAvatarHasDriveInput + quint64 _desiredDrivingTimer { 0 }; + bool _desiredDriving { false }; + bool _currentDriving { false }; + + // used by updateAvatarIsAtRest + quint64 _desiredAtRestTimer { 0 }; + bool _desiredAtRest { true }; + bool _currentAtRest { true }; }; #endif From 58f89c88a2af16b3d42e2f9357e4e5c3cc2a42a8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 11 Jun 2016 16:45:17 +1200 Subject: [PATCH 0505/1237] Remember last directory used for Window.browse() and Window.save() Use last directory if not specified in method call. Default to desktop. --- .../scripting/WindowScriptingInterface.cpp | 36 +++++++++++++++++-- .../src/scripting/WindowScriptingInterface.h | 4 +++ scripts/system/edit.js | 3 +- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0443c65453..6579366cf8 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include "Application.h" #include "DomainHandler.h" #include "MainWindow.h" @@ -23,6 +25,10 @@ #include "WindowScriptingInterface.h" +static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation"; + + WindowScriptingInterface::WindowScriptingInterface() { const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged); @@ -101,6 +107,14 @@ QString fixupPathForMac(const QString& directory) { return path; } +QString WindowScriptingInterface::getPreviousBrowseLocation() const { + return Setting::Handle(LAST_BROWSE_LOCATION_SETTING, DESKTOP_LOCATION).get(); +} + +void WindowScriptingInterface::setPreviousBrowseLocation(const QString& location) { + Setting::Handle(LAST_BROWSE_LOCATION_SETTING).set(location); +} + /// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window @@ -108,8 +122,17 @@ QString fixupPathForMac(const QString& directory) { /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { - QString path = fixupPathForMac(directory); + QString path = directory; + if (path == "") { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif QString result = OffscreenUi::getOpenFileName(nullptr, title, path, nameFilter); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } @@ -120,8 +143,17 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { - QString path = fixupPathForMac(directory); + QString path = directory; + if (path == "") { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif QString result = OffscreenUi::getSaveFileName(nullptr, title, path, nameFilter); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index dfe02a5064..b92114c1bf 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -49,6 +49,10 @@ signals: private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); + +private: + QString getPreviousBrowseLocation() const; + void setPreviousBrowseLocation(const QString& location); }; #endif // hifi_WindowScriptingInterface_h diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 1232c8d94d..42eddf11c3 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1221,8 +1221,7 @@ function handeMenuEvent(menuItem) { if (!selectionManager.hasSelection()) { Window.alert("No entities have been selected."); } else { - var filename = "entities__" + Window.location.hostname + ".svo.json"; - filename = Window.save("Select Where to Save", filename, "*.json") + var filename = Window.save("Select Where to Save", "", "*.json") if (filename) { var success = Clipboard.exportEntities(filename, selectionManager.selections); if (!success) { From e79767a121ee70bd30edea89de524ac80fc1c5b1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 11 Jun 2016 15:16:02 -0700 Subject: [PATCH 0506/1237] authed username wasn't being saved in permissions, so that interface would get briefly disconnected when their permissions changed. --- domain-server/src/DomainGatekeeper.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 342b8213d1..c4a7d1a425 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -128,7 +128,7 @@ void DomainGatekeeper::updateNodePermissions() { QList nodesToKill; auto limitedNodeList = DependencyManager::get(); - limitedNodeList->eachNodeBreakable([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){ + limitedNodeList->eachNode([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){ QString username = node->getPermissions().getUserName(); NodePermissions userPerms(username); @@ -167,8 +167,6 @@ void DomainGatekeeper::updateNodePermissions() { // hang up on this node nodesToKill << node; } - - return true; }); foreach (auto node, nodesToKill) { @@ -264,7 +262,6 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect qDebug() << "user-permissions: no username, so:" << userPerms; } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they are sent us a username and the signature verifies it - userPerms.setUserName(username); if (_server->_settingsManager.havePermissionsForName(username)) { // we have specific permissions for this user. userPerms = _server->_settingsManager.getPermissionsForName(username); @@ -274,6 +271,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); qDebug() << "user-permissions: user is logged in, so:" << userPerms; } + userPerms.setUserName(username); } else { // they sent us a username, but it didn't check out requestUserPublicKey(username); From 924c949af5789c147b77f952084e4374ae6e5241 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 11 Jun 2016 15:16:45 -0700 Subject: [PATCH 0507/1237] permissions for the local node were being updated without the various *changed* signals being called. this was keeping the asset-server menu item from being correct, among other things --- libraries/networking/src/NodeList.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 11b5f5469a..fd1442d639 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -543,7 +543,9 @@ void NodeList::processDomainServerList(QSharedPointer message) setSessionUUID(newUUID); // pull the permissions/right/privileges for this node out of the stream - packetStream >> _permissions; + NodePermissions newPermissions; + packetStream >> newPermissions; + setPermissions(newPermissions); // pull each node in the packet while (packetStream.device()->pos() < message->getSize()) { From c325cc50dd139ba3be82c202e4050d7de3611bea Mon Sep 17 00:00:00 2001 From: samcake Date: Sat, 11 Jun 2016 18:54:22 -0700 Subject: [PATCH 0508/1237] Adding the depth aware blur --- .../render-utils/src/RenderDeferredTask.cpp | 4 +- .../render-utils/src/SurfaceGeometryPass.cpp | 5 +- .../render-utils/src/SurfaceGeometryPass.h | 5 +- .../subsurfaceScattering_drawScattering.slf | 13 ++++- libraries/render/src/render/BlurTask.cpp | 22 +++++++ libraries/render/src/render/BlurTask.h | 19 +++++- libraries/render/src/render/BlurTask.slh | 58 +++++++++++++++++++ .../src/render/blurGaussianDepthAwareH.slf | 3 +- .../src/render/blurGaussianDepthAwareV.slf | 3 +- .../utilities/render/surfaceGeometryPass.qml | 2 +- 10 files changed, 120 insertions(+), 14 deletions(-) diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 1226750e62..c325a2fd9f 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -111,9 +111,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DrawBackgroundDeferred", background); // Opaque all rendered, generate surface geometry buffers - const auto curvatureFramebuffer = addJob("SurfaceGeometry", deferredFrameTransform); + const auto curvatureFramebufferAndDepth = addJob("SurfaceGeometry", deferredFrameTransform); - addJob("DiffuseCurvature", curvatureFramebuffer); + addJob("DiffuseCurvature", curvatureFramebufferAndDepth); // AO job diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index d5db4e74d8..70a33d93d8 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -45,7 +45,7 @@ void SurfaceGeometryPass::configure(const Config& config) { } } -void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer) { +void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, InputPair& curvatureAndDepth) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -59,7 +59,8 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); - curvatureFramebuffer = curvatureFBO; + curvatureAndDepth.editFirst() = curvatureFBO; + curvatureAndDepth.editSecond() = pyramidTexture; auto curvatureTexture = framebufferCache->getCurvatureTexture(); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 00d407d482..582e5b3e52 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -40,13 +40,14 @@ signals: class SurfaceGeometryPass { public: + using InputPair = render::VaryingPair; using Config = SurfaceGeometryPassConfig; - using JobModel = render::Job::ModelIO; + using JobModel = render::Job::ModelIO; SurfaceGeometryPass(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& curvatureFramebuffer); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, InputPair& curvatureAndDepth); float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } float getCurvatureBasisScale() const { return _parametersBuffer.get().curvatureInfo.y; } diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index 8cadda10bc..6b3c5580f4 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -80,6 +80,8 @@ float scatterCurve = 0.25f; in vec2 varTexCoord0; out vec4 _fragColor; +uniform vec3 uniformLightVector = vec3(1.0); + void main(void) { // DeferredTransform deferredTransform = getDeferredTransform(); // DeferredFragment frag = unpackDeferredFragment(deferredTransform, varTexCoord0); @@ -91,11 +93,18 @@ void main(void) { vec3 bentNormalN = normal; vec3 bentNormalR = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); float curvature = abs(diffusedCurvature.w * 2 - 1) * 0.5f * scatterCurve + scatterBase; + // _fragColor = vec4(vec3(diffusedCurvature.xyz), 1.0); // --> Calculate the light vector. - vec3 lightVector = normalize(vec3(1.0f, 1.0f, 1.0f)); //normalize(lightPos - sourcePos.xyz); + vec3 lightVector = normalize(uniformLightVector); //normalize(lightPos - sourcePos.xyz); - _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); + // _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); + + _fragColor = vec4(bentNormalR * lightVector, 1.0); + + //_fragColor = vec4(vec3(bentNormalR * 0.5 + 0.5), 1.0); + + /* // --> Optimise for skin diffusion profile. float diffuseBlendedR = dot(normalize(mix( bentNormalN.xyz, bentNormalN, normalBendR * normalBendFactor)), lightVector); diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 0511bc846c..e944dff393 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -26,6 +26,7 @@ enum BlurShaderBufferSlots { }; enum BlurShaderMapSlots { BlurTask_SourceSlot = 0, + BlurTask_DepthSlot, }; const float BLUR_NUM_SAMPLES = 7.0f; @@ -57,6 +58,20 @@ void BlurParams::setFilterRadiusScale(float scale) { } } +void BlurParams::setDepthPerspective(float oneOverTan2FOV) { + auto depthInfo = _parametersBuffer.get().depthInfo; + if (oneOverTan2FOV != depthInfo.w) { + _parametersBuffer.edit().depthInfo.w = oneOverTan2FOV; + } +} + +void BlurParams::setDepthThreshold(float threshold) { + auto depthInfo = _parametersBuffer.get().depthInfo; + if (threshold != depthInfo.x) { + _parametersBuffer.edit().depthInfo.x = threshold; + } +} + BlurGaussian::BlurGaussian() { _parameters = std::make_shared(); } @@ -200,6 +215,7 @@ gpu::PipelinePointer BlurGaussianDepthAware::getBlurVPipeline() { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), BlurTask_DepthSlot)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -222,6 +238,7 @@ gpu::PipelinePointer BlurGaussianDepthAware::getBlurHPipeline() { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("blurParamsBuffer"), BlurTask_ParamsSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), BlurTask_DepthSlot)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -270,6 +287,7 @@ bool BlurGaussianDepthAware::updateBlurringResources(const gpu::FramebufferPoint void BlurGaussianDepthAware::configure(const Config& config) { _parameters->setFilterRadiusScale(config.filterScale); + _parameters->setDepthThreshold(config.depthThreshold); } @@ -292,6 +310,7 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const auto blurHPipeline = getBlurHPipeline(); _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); + _parameters->setDepthPerspective(args->getViewFrustum().getProjection()[1][1]); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); @@ -299,6 +318,8 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const batch.setUniformBuffer(BlurTask_ParamsSlot, _parameters->_parametersBuffer); + batch.setResourceTexture(BlurTask_DepthSlot, depthTexture); + batch.setFramebuffer(blurringResources.blurringFramebuffer); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); @@ -312,6 +333,7 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const batch.draw(gpu::TRIANGLE_STRIP, 4); batch.setResourceTexture(BlurTask_SourceSlot, nullptr); + batch.setResourceTexture(BlurTask_DepthSlot, nullptr); batch.setUniformBuffer(BlurTask_ParamsSlot, nullptr); }); } diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 9de555ff54..796e03e2c9 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -24,6 +24,9 @@ public: void setFilterRadiusScale(float scale); + void setDepthPerspective(float oneOverTan2FOV); + void setDepthThreshold(float threshold); + // Class describing the uniform buffer with all the parameters common to the blur shaders class Params { public: @@ -33,6 +36,9 @@ public: // Filter info (radius scale glm::vec4 filterInfo{ 1.0f, 0.0f, 0.0f, 0.0f }; + // Depth info (radius scale + glm::vec4 depthInfo{ 1.0f, 0.0f, 0.0f, 0.0f }; + // stereo info if blurring a stereo render glm::vec4 stereoInfo{ 0.0f }; @@ -89,10 +95,21 @@ protected: bool updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources); }; +class BlurGaussianDepthAwareConfig : public BlurGaussianConfig { + Q_OBJECT + Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) // expose enabled flag +public: + + float depthThreshold{ 2.0f }; +signals: + void dirty(); +protected: +}; + class BlurGaussianDepthAware { public: using InputPair = VaryingPair; - using Config = BlurGaussianConfig; + using Config = BlurGaussianDepthAwareConfig; using JobModel = Job::ModelI; BlurGaussianDepthAware(); diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index 686ab35ba1..a8c96c12b2 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -24,6 +24,7 @@ const float gaussianDistributionOffset[NUM_TAPS] = float[]( struct BlurParameters { vec4 resolutionInfo; vec4 filterInfo; + vec4 depthInfo; vec4 stereoInfo; }; @@ -35,6 +36,19 @@ vec2 getViewportInvWidthHeight() { return parameters.resolutionInfo.zw; } +float getFilterScale() { + return parameters.filterInfo.x; +} + + +float getDepthThreshold() { + return parameters.depthInfo.x; +} + +float getDepthPerspective() { + return parameters.depthInfo.w; +} + <@endfunc@> @@ -64,3 +78,47 @@ vec4 pixelShaderGaussian(vec2 texcoord, vec2 direction, vec2 pixelStep) { <@endfunc@> +<@func declareBlurGaussianDepthAware()@> + +<$declareBlurUniforms()$> + +uniform sampler2D sourceMap; +uniform sampler2D depthMap; + +vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep) { + + float sampleDepth = texture(depthMap, texcoord).x; + vec4 sampleCenter = texture(sourceMap, texcoord); + + // Calculate the width scale. + float distanceToProjectionWindow = getDepthPerspective(); + + float depthThreshold = getDepthThreshold(); + + // Calculate the final step to fetch the surrounding pixels. + float filterScale = getFilterScale(); + float scale = distanceToProjectionWindow / sampleDepth; + + vec2 finalStep = filterScale * scale * direction * pixelStep; + + vec4 srcBlurred = vec4(0.0); + + for(int i = 1; i < NUM_TAPS; i++) { + // Fetch color and depth for current sample. + vec2 sampleCoord = texcoord + (gaussianDistributionOffset[i] * finalStep); + float srcDepth = texture(depthMap, sampleCoord).x; + vec4 srcSample = texture(sourceMap, sampleCoord); + + + // If the difference in depth is huge, we lerp color back. + float s = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); + srcSample = mix(srcSample, sampleCenter, s); + + // Accumulate. + srcBlurred += gaussianDistributionCurve[i] * srcSample; + } + + return srcBlurred; +} + +<@endfunc@> diff --git a/libraries/render/src/render/blurGaussianDepthAwareH.slf b/libraries/render/src/render/blurGaussianDepthAwareH.slf index 828a934776..aab1fe2b02 100644 --- a/libraries/render/src/render/blurGaussianDepthAwareH.slf +++ b/libraries/render/src/render/blurGaussianDepthAwareH.slf @@ -10,8 +10,7 @@ // <@include BlurTask.slh@> -<$declareBlurGaussian()$> - +<$declareBlurGaussianDepthAware()$> in vec2 varTexCoord0; diff --git a/libraries/render/src/render/blurGaussianDepthAwareV.slf b/libraries/render/src/render/blurGaussianDepthAwareV.slf index 9c6467d61d..35f0d3a381 100644 --- a/libraries/render/src/render/blurGaussianDepthAwareV.slf +++ b/libraries/render/src/render/blurGaussianDepthAwareV.slf @@ -10,8 +10,7 @@ // <@include BlurTask.slh@> -<$declareBlurGaussian()$> - +<$declareBlurGaussianDepthAware()$> in vec2 varTexCoord0; diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index ca954be40d..4ec397addd 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -32,7 +32,7 @@ Column { Column{ Repeater { - model: [ "Blur Scale:filterScale:4.0" ] + model: [ "Blur Scale:filterScale:2.0", "Blur Depth Threshold:depthThreshold:100.0" ] ConfigSlider { label: qsTr(modelData.split(":")[0]) integral: false From 1ec145298fa91d512a7ac1e07588a2952a4e37ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E9=B3=B3?= Date: Sun, 12 Jun 2016 21:07:49 +0800 Subject: [PATCH 0509/1237] Update BUILD_OSX.md Qt5.6 removed QTWebKit which hifi depends on. Before we complete the migration listed in https://wiki.qt.io/Porting_from_QtWebKit_to_QtWebEngine the build instruction need to tap into qt55 in historical brew versions. --- BUILD_OSX.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BUILD_OSX.md b/BUILD_OSX.md index c8f19710ca..44f27d3d02 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -3,9 +3,10 @@ Please read the [general build guide](BUILD.md) for information on dependencies ###Homebrew [Homebrew](http://brew.sh/) is an excellent package manager for OS X. It makes install of all High Fidelity dependencies very simple. - brew install cmake openssl qt5 + brew tap homebrew/versions + brew install cmake openssl qt55 -We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x and above provide a mechanism to disable the wireless scanning we previously had a custom patch for. +We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x provide a mechanism to disable the wireless scanning we previously had a custom patch for. ###OpenSSL and Qt From d74f1c8b5dc93ce543af94a55036317f0612d917 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 13 Jun 2016 14:51:14 +1200 Subject: [PATCH 0510/1237] Fix default directory for Settings directory preference fields --- .../qml/dialogs/preferences/BrowsablePreference.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml b/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml index 9a19889938..2cf50891c9 100644 --- a/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml +++ b/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml @@ -65,7 +65,10 @@ Preference { verticalCenter: dataTextField.verticalCenter } onClicked: { - var browser = fileBrowserBuilder.createObject(desktop, { selectDirectory: true, folder: fileDialogHelper.pathToUrl(preference.value) }); + var browser = fileBrowserBuilder.createObject(desktop, { + selectDirectory: true, + dir: fileDialogHelper.pathToUrl(preference.value) + }); browser.selectedFile.connect(function(fileUrl){ console.log(fileUrl); dataTextField.text = fileDialogHelper.urlToPath(fileUrl); From 546699da03c889d2ef7df821de6db770e60acc7a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 13 Jun 2016 15:06:10 +1200 Subject: [PATCH 0511/1237] When choosing a directory default selection to default directory --- interface/resources/qml/dialogs/FileDialog.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 93ccbc0b8c..f877aa448e 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -82,6 +82,12 @@ ModalWindow { // Clear selection when click on external frame. frameClicked.connect(function() { d.clearSelection(); }); + + if (selectDirectory) { + currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); + } + + fileTableView.forceActiveFocus(); } Item { From f7578f084f4142e5a6c2ade27975a4a20f926aca Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 13 Jun 2016 15:21:59 +1200 Subject: [PATCH 0512/1237] Fix buttons on warning message box when can't export a file --- interface/resources/qml/dialogs/FileDialog.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index f877aa448e..56f761e42d 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -709,7 +709,6 @@ ModalWindow { if (!helper.urlIsWritable(selection)) { desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, - buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, text: "Unable to write to location " + selection }) return; From 6a5dff06c4fff9d922999467ccadb1ef218584bb Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 13 Jun 2016 09:46:24 -0700 Subject: [PATCH 0513/1237] Pushing a shitty state where the pairs of Varyings don;t get copied correctly --- .../render-utils/src/RenderDeferredTask.cpp | 6 +- .../render-utils/src/SubsurfaceScattering.cpp | 12 ++- .../render-utils/src/SubsurfaceScattering.h | 6 +- .../render-utils/src/SurfaceGeometryPass.cpp | 8 +- .../render-utils/src/SurfaceGeometryPass.h | 6 +- .../subsurfaceScattering_drawScattering.slf | 5 +- libraries/render/src/render/BlurTask.cpp | 44 +++++++-- libraries/render/src/render/BlurTask.h | 13 ++- libraries/render/src/render/Task.cpp | 15 +++ libraries/render/src/render/Task.h | 93 ++++++++++++++++++- 10 files changed, 177 insertions(+), 31 deletions(-) diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index c325a2fd9f..de9ff40955 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -115,14 +115,16 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DiffuseCurvature", curvatureFramebufferAndDepth); - + const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth, true); + // AO job addJob("AmbientOcclusion"); // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - const auto scatteringFramebuffer = addJob("Scattering", deferredFrameTransform); + const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebufferAndDepth[0])); + const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred"); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 9700f4f651..1dcc9c7196 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -102,7 +102,7 @@ bool SubsurfaceScattering::updateScatteringFramebuffer(const gpu::FramebufferPoi // attach depthStencil if present in source if (sourceFramebuffer->hasDepthStencil()) { - _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + // _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); } auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); @@ -126,7 +126,7 @@ bool SubsurfaceScattering::updateScatteringFramebuffer(const gpu::FramebufferPoi } -void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& scatteringFramebuffer) { +void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, gpu::FramebufferPointer& scatteringFramebuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -138,10 +138,14 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, auto pipeline = getScatteringPipeline(); + + auto& frameTransform = inputs.first. template get();//getFirst(); + auto& curvatureFramebuffer = inputs.second. template get();//getSecond(); + auto framebufferCache = DependencyManager::get(); - if (!updateScatteringFramebuffer(framebufferCache->getCurvatureFramebuffer(), scatteringFramebuffer)) { + if (!updateScatteringFramebuffer(curvatureFramebuffer, scatteringFramebuffer)) { return; } @@ -156,7 +160,7 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setPipeline(pipeline); batch.setResourceTexture(SubsurfaceScattering_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); - batch.setResourceTexture(SubsurfaceScattering_CurvatureMapSlot, framebufferCache->getCurvatureTexture()); + batch.setResourceTexture(SubsurfaceScattering_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(SubsurfaceScattering_ScatteringTableSlot, _scatteringTable); batch.draw(gpu::TRIANGLE_STRIP, 4); diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 46fc238176..832b7c8683 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -33,13 +33,15 @@ signals: class SubsurfaceScattering { public: + using Inputs = render::VaryingPair;//; + //using Inputs = render::VaryingPairBase;//; using Config = SubsurfaceScatteringConfig; - using JobModel = render::Job::ModelIO; + using JobModel = render::Job::ModelIO; SubsurfaceScattering(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, gpu::FramebufferPointer& scatteringFramebuffer); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, gpu::FramebufferPointer& scatteringFramebuffer); float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 70a33d93d8..48188ee196 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -45,7 +45,7 @@ void SurfaceGeometryPass::configure(const Config& config) { } } -void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, InputPair& curvatureAndDepth) { +void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, Outputs& curvatureAndDepth) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -59,8 +59,8 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); - curvatureAndDepth.editFirst() = curvatureFBO; - curvatureAndDepth.editSecond() = pyramidTexture; + curvatureAndDepth.first. template edit() = curvatureFBO; + curvatureAndDepth.second. template edit() = pyramidTexture; auto curvatureTexture = framebufferCache->getCurvatureTexture(); @@ -154,8 +154,6 @@ const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { // Stencil test the curvature pass for objects pixels only, not the background state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - // state->setColorWriteMask(true, false, false, false); - // Good to go add the brand new pipeline _curvaturePipeline = gpu::Pipeline::create(program, state); } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 582e5b3e52..8e8486c499 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -40,14 +40,14 @@ signals: class SurfaceGeometryPass { public: - using InputPair = render::VaryingPair; + using Outputs = render::VaryingPair;//; using Config = SurfaceGeometryPassConfig; - using JobModel = render::Job::ModelIO; + using JobModel = render::Job::ModelIO; SurfaceGeometryPass(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, InputPair& curvatureAndDepth); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, Outputs& curvatureAndDepth); float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } float getCurvatureBasisScale() const { return _parametersBuffer.get().curvatureInfo.y; } diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index 6b3c5580f4..ae78ab4577 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -100,9 +100,10 @@ void main(void) { // _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); - _fragColor = vec4(bentNormalR * lightVector, 1.0); + // _fragColor = vec4(vec3(abs(dot(bentNormalR, lightVector))), 1.0); + _fragColor = vec4(vec3(varTexCoord0, 0.0), 1.0); - //_fragColor = vec4(vec3(bentNormalR * 0.5 + 0.5), 1.0); + // _fragColor = vec4(vec3(bentNormalR * 0.5 + 0.5), 1.0); /* diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index e944dff393..004ac079c0 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -202,7 +202,9 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont -BlurGaussianDepthAware::BlurGaussianDepthAware() { +BlurGaussianDepthAware::BlurGaussianDepthAware(bool generateOutputFramebuffer) : + _generateOutputFramebuffer(generateOutputFramebuffer) +{ _parameters = std::make_shared(); } @@ -276,12 +278,40 @@ bool BlurGaussianDepthAware::updateBlurringResources(const gpu::FramebufferPoint } } } - + blurringResources.sourceTexture = sourceFramebuffer->getRenderBuffer(0); blurringResources.blurringFramebuffer = _blurredFramebuffer; blurringResources.blurringTexture = _blurredFramebuffer->getRenderBuffer(0); - blurringResources.finalFramebuffer = sourceFramebuffer; + if (_generateOutputFramebuffer) { + // The job output the blur result in a new Framebuffer spawning here. + // Let s make sure it s ready for this + if (!_outputFramebuffer) { + _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + + // attach depthStencil if present in source + if (sourceFramebuffer->hasDepthStencil()) { + _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + _outputFramebuffer->setRenderBuffer(0, blurringTarget); + } else { + if ((_outputFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_outputFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { + _outputFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); + if (sourceFramebuffer->hasDepthStencil()) { + _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + } + } + + // Should be good to use the output Framebuffer as final + blurringResources.finalFramebuffer = _outputFramebuffer; + } else { + // Just the reuse the input as output to blur itself. + blurringResources.finalFramebuffer = sourceFramebuffer; + } + return true; } @@ -291,20 +321,22 @@ void BlurGaussianDepthAware::configure(const Config& config) { } -void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const InputPair& SourceAndDepth) { +void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& SourceAndDepth, gpu::FramebufferPointer& blurredFramebuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; - auto& sourceFramebuffer = SourceAndDepth.getFirst(); - auto& depthTexture = SourceAndDepth.getSecond(); + auto& sourceFramebuffer = SourceAndDepth.first. template get();//getFirst(); + auto& depthTexture = SourceAndDepth.second. template get();//getSecond(); BlurringResources blurringResources; if (!updateBlurringResources(sourceFramebuffer, blurringResources)) { // early exit if no valid blurring resources return; } + + blurredFramebuffer = blurringResources.finalFramebuffer; auto blurVPipeline = getBlurVPipeline(); auto blurHPipeline = getBlurHPipeline(); diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 796e03e2c9..df863e375b 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -108,14 +108,15 @@ protected: class BlurGaussianDepthAware { public: - using InputPair = VaryingPair; + using Inputs = VaryingPair;//; + // using InputPair = VaryingPairBase;//; using Config = BlurGaussianDepthAwareConfig; - using JobModel = Job::ModelI; + using JobModel = Job::ModelIO; - BlurGaussianDepthAware(); + BlurGaussianDepthAware(bool generateNewOutput = false); void configure(const Config& config); - void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const InputPair& SourceAndDepth); + void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& SourceAndDepth, gpu::FramebufferPointer& blurredFramebuffer); protected: @@ -129,6 +130,10 @@ protected: gpu::FramebufferPointer _blurredFramebuffer; + // the output framebuffer defined if the job needs to output the result in a new framebuffer and not in place in th einput buffer + gpu::FramebufferPointer _outputFramebuffer; + bool _generateOutputFramebuffer { false }; + struct BlurringResources { gpu::TexturePointer sourceTexture; gpu::FramebufferPointer blurringFramebuffer; diff --git a/libraries/render/src/render/Task.cpp b/libraries/render/src/render/Task.cpp index 0bf20c6042..663b1edc09 100644 --- a/libraries/render/src/render/Task.cpp +++ b/libraries/render/src/render/Task.cpp @@ -23,3 +23,18 @@ void TaskConfig::refresh() { _task->configure(*this); } + + +namespace render{ + + template <> void varyingGet(const VaryingPair& data, uint8_t index, Varying& var) { + if (index == 0) { + var = data.first; + } else { + var = data.second; + } + } + + template <> uint8_t varyingLength(const VaryingPair& data) { return 2; } + +} \ No newline at end of file diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 1efefa053a..85bc2c44e8 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -28,21 +28,38 @@ namespace render { +class Varying; + + template void varyingGet(const T& data, uint8_t index, Varying& var) {} +template uint8_t varyingLength(const T& data) { return 0; } + // A varying piece of data, to be used as Job/Task I/O // TODO: Task IO class Varying { public: Varying() {} Varying(const Varying& var) : _concept(var._concept) {} + Varying& operator=(const Varying& var) { + _concept = var._concept; + return (*this); + } template Varying(const T& data) : _concept(std::make_shared>(data)) {} template T& edit() { return std::static_pointer_cast>(_concept)->_data; } template const T& get() const { return std::static_pointer_cast>(_concept)->_data; } + + // access potential sub varyings contained in this one. + Varying operator[] (uint8_t index) const { return (*_concept)[index]; } + uint8_t length() const { return _concept->length(); } + protected: class Concept { public: virtual ~Concept() = default; + + virtual Varying operator[] (uint8_t index) const = 0; + virtual uint8_t length() const = 0; }; template class Model : public Concept { public: @@ -50,6 +67,14 @@ protected: Model(const Data& data) : _data(data) {} virtual ~Model() = default; + + + virtual Varying operator[] (uint8_t index) const { + Varying var; + varyingGet(_data, index, var); + return var; + } + virtual uint8_t length() const { return varyingLength(_data); } Data _data; }; @@ -58,20 +83,82 @@ protected: }; + + +using VaryingPair = std::pair; +/* +class VaryingPairBase { + public: + Varying first; + Varying second; + + + // template < class T0, class T1> VaryingPairBase() : Parent(Varying(T0()), Varying(T1())) {} + // VaryingPairBase(const VaryingPairBase& pair) : Parent(pair.first, pair.second) {} + VaryingPairBase(const Varying& _first, const Varying& _second) : first(_first), second(_second) {} + +}; + */ /* template < class T0, class T1 > class VaryingPair : public std::pair { public: using Parent = std::pair; - VaryingPair() : Parent(Varying(T0()), T1()) {} - + VaryingPair() : Parent(Varying(T0()), Varying(T1())) {} + VaryingPair(const VaryingPair& pair) : Parent(pair.first, pair.second) {} + VaryingPair(const Varying& first, const Varying& second) : Parent(first, second) {} + const T0& getFirst() const { return first.get(); } T0& editFirst() { return first.edit(); } const T1& getSecond() const { return second.get(); } T1& editSecond() { return second.edit(); } -}; +}; + */ + + template <> void varyingGet(const VaryingPair& data, uint8_t index, Varying& var); + template <> uint8_t varyingLength(const VaryingPair& data); + /* template Varying varyingGet(const T& data, uint8_t index) { + return Varying(T()); + }*/ + +//template Varying varyingGet(template VaryingPair& data, uint8_t index); +//template <> uint8_t varyingLength(template VaryingPair& data); + + +/* +template < class T0, class T1 > +class VaryingPair : Varying { +public: + using Parent = Varying; + using Pair = std::pair; + + VaryingPair() : Parent(Pair(Varying(T0()), Varying(T1()))) {} + VaryingPair(const Varying& first, const Varying& second) : Parent(Pair(first, second)) {} + + + Pair& editPair() { return edit(); } + const Pair& getPair() const { return get(); } + + const T0& getFirst() const { return getPair().first.template get(); } + T0& editFirst() { return editPair().first.template edit(); } + + const T1& getSecond() const { return getPair().second.template get(); } + T1& editSecond() { return editPair().second.template edit(); } + + // access potential sub varyings contained in this one. + virtual Varying operator[] (uint8_t index) const { + if (index == 0) { + return getPair().first; + } else { + return getPair().second; + } } + virtual uint8_t length() const { return 2; } + +}; + */ + template < class T, int NUM > class VaryingArray : public std::array { public: From f69c72f984ed025bd4ae60b6e8471e812a768b90 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 13 Jun 2016 10:44:31 -0700 Subject: [PATCH 0514/1237] Fixing the VaryingPair problem --- .../render-utils/src/RenderDeferredTask.cpp | 5 ++++- .../render-utils/src/SubsurfaceScattering.h | 3 +-- .../render-utils/src/SurfaceGeometryPass.cpp | 1 + .../render-utils/src/SurfaceGeometryPass.h | 2 +- libraries/render/src/render/BlurTask.h | 3 +-- libraries/render/src/render/Task.cpp | 4 ++-- libraries/render/src/render/Task.h | 19 +++++++++++-------- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index de9ff40955..b4b6a979da 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -123,7 +123,10 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebufferAndDepth[0])); + curvatureFramebufferAndDepth.get().first; + + // const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebufferAndDepth[0])); + const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebufferAndDepth.get().first)); const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); // DeferredBuffer is complete, now let's shade it into the LightingBuffer diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 832b7c8683..15369492a0 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -33,8 +33,7 @@ signals: class SubsurfaceScattering { public: - using Inputs = render::VaryingPair;//; - //using Inputs = render::VaryingPairBase;//; + using Inputs = render::VaryingPair; using Config = SubsurfaceScatteringConfig; using JobModel = render::Job::ModelIO; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 48188ee196..9e5d455725 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -59,6 +59,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); + curvatureAndDepth.first. template edit() = curvatureFBO; curvatureAndDepth.second. template edit() = pyramidTexture; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 8e8486c499..bfad811340 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -40,7 +40,7 @@ signals: class SurfaceGeometryPass { public: - using Outputs = render::VaryingPair;//; + using Outputs = render::VaryingPair; using Config = SurfaceGeometryPassConfig; using JobModel = render::Job::ModelIO; diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index df863e375b..899b1ffe12 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -108,8 +108,7 @@ protected: class BlurGaussianDepthAware { public: - using Inputs = VaryingPair;//; - // using InputPair = VaryingPairBase;//; + using Inputs = VaryingPair; using Config = BlurGaussianDepthAwareConfig; using JobModel = Job::ModelIO; diff --git a/libraries/render/src/render/Task.cpp b/libraries/render/src/render/Task.cpp index 663b1edc09..8727923c70 100644 --- a/libraries/render/src/render/Task.cpp +++ b/libraries/render/src/render/Task.cpp @@ -27,7 +27,7 @@ void TaskConfig::refresh() { namespace render{ - template <> void varyingGet(const VaryingPair& data, uint8_t index, Varying& var) { + template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var) { if (index == 0) { var = data.first; } else { @@ -35,6 +35,6 @@ namespace render{ } } - template <> uint8_t varyingLength(const VaryingPair& data) { return 2; } + template <> uint8_t varyingLength(const VaryingPairBase& data) { return 2; } } \ No newline at end of file diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 85bc2c44e8..44cb10aead 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -85,7 +85,12 @@ protected: -using VaryingPair = std::pair; +using VaryingPairBase = std::pair; + + +template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var); +template <> uint8_t varyingLength(const VaryingPairBase& data); + /* class VaryingPairBase { public: @@ -98,11 +103,11 @@ class VaryingPairBase { VaryingPairBase(const Varying& _first, const Varying& _second) : first(_first), second(_second) {} }; - */ /* + */ template < class T0, class T1 > -class VaryingPair : public std::pair { +class VaryingPair : public VaryingPairBase { public: - using Parent = std::pair; + using Parent = VaryingPairBase; VaryingPair() : Parent(Varying(T0()), Varying(T1())) {} VaryingPair(const VaryingPair& pair) : Parent(pair.first, pair.second) {} @@ -113,12 +118,10 @@ public: const T1& getSecond() const { return second.get(); } T1& editSecond() { return second.edit(); } - }; - */ + + - template <> void varyingGet(const VaryingPair& data, uint8_t index, Varying& var); - template <> uint8_t varyingLength(const VaryingPair& data); /* template Varying varyingGet(const T& data, uint8_t index) { return Varying(T()); }*/ From ab4bef7d55cbef9063ad0a53e85d5353e5fb66eb Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 11:19:47 -0700 Subject: [PATCH 0515/1237] Fixes based on PR feedback * The "Clear Overlay When Driving" avatar preference is obeyed. * sensor reset will also center the ui. --- interface/src/Application.cpp | 1 + interface/src/ui/OverlayConductor.cpp | 6 +++--- interface/src/ui/OverlayConductor.h | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1e6f7ba995..e494142302 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4209,6 +4209,7 @@ void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); getActiveDisplayPlugin()->resetSensors(); + _overlayConductor.centerUI(); getMyAvatar()->reset(andReload); QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); } diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 01649a0b3a..b653d0f445 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -139,7 +139,7 @@ void OverlayConductor::update(float dt) { switch (getState()) { case Enabled: - if (qApp->isHMDMode() && headOutsideOverlay()) { + if (myAvatar->getClearOverlayWhenDriving() && qApp->isHMDMode() && headOutsideOverlay()) { setState(DisabledByHead); setEnabled(false); } @@ -147,7 +147,7 @@ void OverlayConductor::update(float dt) { setState(DisabledByToggle); setEnabled(false); } - if (drivingChanged && isDriving) { + if (myAvatar->getClearOverlayWhenDriving() && drivingChanged && isDriving) { setState(DisabledByDrive); setEnabled(false); } @@ -192,7 +192,7 @@ void OverlayConductor::setEnabled(bool enabled) { // if the new state is visible/enabled... MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - if (_enabled && myAvatar->getClearOverlayWhenDriving() && qApp->isHMDMode()) { + if (_enabled && qApp->isHMDMode()) { centerUI(); } } diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 83d6957012..fcfdac72a5 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -20,13 +20,14 @@ public: void setEnabled(bool enable); bool getEnabled() const; + void centerUI(); + private: bool headOutsideOverlay() const; bool updateAvatarHasDriveInput(); bool updateAvatarIsAtRest(); bool userWishesToHide() const; bool userWishesToShow() const; - void centerUI(); enum State { Enabled = 0, From 52245f25f28645b66caaeee686fe752cf6e07d23 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 13 Jun 2016 11:48:45 -0700 Subject: [PATCH 0516/1237] rework code that transforms global _server* values into parent-frame values --- libraries/physics/src/EntityMotionState.cpp | 39 ++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index e5957ce7b2..5fffc9901b 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -274,9 +274,39 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { // if we've never checked before, our _lastStep will be 0, and we need to initialize our state if (_lastStep == 0) { btTransform xform = _body->getWorldTransform(); - _serverPosition = _entity->worldPositionToParent(bulletToGLM(xform.getOrigin())); - _serverRotation = _entity->worldRotationToParent(bulletToGLM(xform.getRotation())); - _serverVelocity = _entity->worldVelocityToParent(getBodyLinearVelocityGTSigma()); + + // _serverPosition = _entity->worldPositionToParent(bulletToGLM(xform.getOrigin())); + // _serverRotation = _entity->worldRotationToParent(bulletToGLM(xform.getRotation())); + // _serverVelocity = _entity->worldVelocityToParent(getBodyLinearVelocityGTSigma()); + + + _serverPosition = bulletToGLM(xform.getOrigin()); + _serverRotation = bulletToGLM(xform.getRotation()); + _serverVelocity = getBodyLinearVelocityGTSigma(); + bool success; + Transform parentTransform = _entity->getParentTransform(success); + if (success) { + Transform bodyTransform; + bodyTransform.setTranslation(_serverPosition); + bodyTransform.setRotation(_serverRotation); + Transform result; + Transform::inverseMult(result, parentTransform, bodyTransform); + _serverPosition = result.getTranslation(); + _serverRotation = result.getRotation(); + + // transform velocity into parent-frame + parentTransform.setTranslation(glm::vec3(0.0f)); + Transform velocityTransform; + velocityTransform.setTranslation(_serverVelocity); + Transform myWorldTransform; + Transform::mult(myWorldTransform, parentTransform, velocityTransform); + myWorldTransform.setTranslation(_serverVelocity); + Transform::inverseMult(result, parentTransform, myWorldTransform); + _serverVelocity = result.getTranslation(); + } + + + _serverAcceleration = Vectors::ZERO; _serverAngularVelocity = _entity->worldVelocityToParent(bulletToGLM(_body->getAngularVelocity())); _lastStep = simulationStep; @@ -614,8 +644,9 @@ uint32_t EntityMotionState::getIncomingDirtyFlags() { int bodyFlags = _body->getCollisionFlags(); bool isMoving = _entity->isMovingRelativeToParent(); - // XXX what's right, here? if (((bodyFlags & btCollisionObject::CF_STATIC_OBJECT) && isMoving) // || + // TODO -- there is opportunity for an optimization here, but this currently causes + // excessive re-insertion of the rigid body. // (bodyFlags & btCollisionObject::CF_KINEMATIC_OBJECT && !isMoving) ) { dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; From 1ca1f98034731127e00954405b8c8df5e8a36aff Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 12:01:42 -0700 Subject: [PATCH 0517/1237] Fix unused variable warning --- interface/src/ui/OverlayConductor.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index b653d0f445..a897d85472 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -191,7 +191,6 @@ void OverlayConductor::setEnabled(bool enabled) { _prevOverlayMenuChecked = _enabled; // if the new state is visible/enabled... - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); if (_enabled && qApp->isHMDMode()) { centerUI(); } From 21e67fc4d844c8811231029fbb6fe60e24ac5dfc Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 12:03:42 -0700 Subject: [PATCH 0518/1237] Another unused variable warning --- interface/src/ui/OverlayConductor.h | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index fcfdac72a5..375b2652f6 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -45,7 +45,6 @@ private: bool _prevOverlayMenuChecked { true }; bool _enabled { false }; bool _hmdMode { false }; - bool _disabledFromHead { false }; // used by updateAvatarHasDriveInput quint64 _desiredDrivingTimer { 0 }; From 5553c9f87fd0bcc6c8062aa8c7ddef80372aeaa6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 14 Jun 2016 07:46:41 +1200 Subject: [PATCH 0519/1237] Code review --- interface/src/scripting/WindowScriptingInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 6579366cf8..f0ae221566 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -123,7 +123,7 @@ void WindowScriptingInterface::setPreviousBrowseLocation(const QString& location /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { QString path = directory; - if (path == "") { + if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN @@ -144,7 +144,7 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { QString path = directory; - if (path == "") { + if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN From 9640727f518e06ad915746848ad3b8ebea030d7e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 8 Jun 2016 17:09:30 -0700 Subject: [PATCH 0520/1237] Hand laser rendering support in HMD plugins. --- .../src/scripting/HMDScriptingInterface.cpp | 10 ++ .../src/scripting/HMDScriptingInterface.h | 2 + .../display-plugins/OpenGLDisplayPlugin.cpp | 53 ++++--- .../src/display-plugins/OpenGLDisplayPlugin.h | 21 ++- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 149 +++++++++++++++++- .../display-plugins/hmd/HmdDisplayPlugin.h | 20 ++- libraries/gl/src/gl/OglplusHelpers.cpp | 92 ++++++++++- libraries/gl/src/gl/OglplusHelpers.h | 5 +- libraries/plugins/src/plugins/DisplayPlugin.h | 20 +++ .../oculus/src/OculusBaseDisplayPlugin.cpp | 5 +- .../src/OculusLegacyDisplayPlugin.cpp | 5 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 63 ++++++-- plugins/openvr/src/OpenVrDisplayPlugin.h | 1 - plugins/openvr/src/OpenVrHelpers.cpp | 4 + plugins/openvr/src/OpenVrHelpers.h | 1 + 15 files changed, 399 insertions(+), 52 deletions(-) diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 7bf1547a3c..02840a9775 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -105,3 +105,13 @@ QString HMDScriptingInterface::preferredAudioInput() const { QString HMDScriptingInterface::preferredAudioOutput() const { return qApp->getActiveDisplayPlugin()->getPreferredAudioOutDevice(); } + +bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const { + return qApp->getActiveDisplayPlugin()->setHandLaser(hands, + enabled ? DisplayPlugin::HandLaserMode::Overlay : DisplayPlugin::HandLaserMode::None, + color, direction); +} + +void HMDScriptingInterface::disableHandLasers(int hands) const { + qApp->getActiveDisplayPlugin()->setHandLaser(hands, DisplayPlugin::HandLaserMode::None); +} diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index d4c7b7cc0e..c55320ca83 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -36,6 +36,8 @@ public: Q_INVOKABLE glm::vec2 overlayToSpherical(const glm::vec2 & overlayPos) const; Q_INVOKABLE QString preferredAudioInput() const; Q_INVOKABLE QString preferredAudioOutput() const; + Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; + Q_INVOKABLE void disableHandLasers(int hands) const; public: HMDScriptingInterface(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index d9ee979777..363bde15e6 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -213,9 +213,10 @@ OpenGLDisplayPlugin::OpenGLDisplayPlugin() { } void OpenGLDisplayPlugin::cleanupForSceneTexture(const gpu::TexturePointer& sceneTexture) { - Lock lock(_mutex); - Q_ASSERT(_sceneTextureToFrameIndexMap.contains(sceneTexture)); - _sceneTextureToFrameIndexMap.remove(sceneTexture); + withRenderThreadLock([&] { + Q_ASSERT(_sceneTextureToFrameIndexMap.contains(sceneTexture)); + _sceneTextureToFrameIndexMap.remove(sceneTexture); + }); } @@ -394,10 +395,9 @@ void OpenGLDisplayPlugin::submitSceneTexture(uint32_t frameIndex, const gpu::Tex return; } - { - Lock lock(_mutex); + withRenderThreadLock([&] { _sceneTextureToFrameIndexMap[sceneTexture] = frameIndex; - } + }); // Submit it to the presentation thread via escrow _sceneTextureEscrow.submit(sceneTexture); @@ -431,11 +431,12 @@ void OpenGLDisplayPlugin::updateTextures() { } void OpenGLDisplayPlugin::updateFrameData() { - Lock lock(_mutex); - auto previousFrameIndex = _currentPresentFrameIndex; - _currentPresentFrameIndex = _sceneTextureToFrameIndexMap[_currentSceneTexture]; - auto skippedCount = (_currentPresentFrameIndex - previousFrameIndex) - 1; - _droppedFrameRate.increment(skippedCount); + withPresentThreadLock([&] { + auto previousFrameIndex = _currentPresentFrameIndex; + _currentPresentFrameIndex = _sceneTextureToFrameIndexMap[_currentSceneTexture]; + auto skippedCount = (_currentPresentFrameIndex - previousFrameIndex) - 1; + _droppedFrameRate.increment(skippedCount); + }); } void OpenGLDisplayPlugin::compositeOverlay() { @@ -492,14 +493,14 @@ void OpenGLDisplayPlugin::compositeLayers() { } _compositeFramebuffer->Bound(Framebuffer::Target::Draw, [&] { Context::Viewport(targetRenderSize.x, targetRenderSize.y); - Context::Clear().DepthBuffer(); - glBindTexture(GL_TEXTURE_2D, getSceneTextureId()); - compositeScene(); + auto sceneTextureId = getSceneTextureId(); auto overlayTextureId = getOverlayTextureId(); + glBindTexture(GL_TEXTURE_2D, sceneTextureId); + compositeScene(); if (overlayTextureId) { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBindTexture(GL_TEXTURE_2D, overlayTextureId); + Context::Enable(Capability::Blend); + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); compositeOverlay(); auto compositorHelper = DependencyManager::get(); @@ -507,10 +508,14 @@ void OpenGLDisplayPlugin::compositeLayers() { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; glBindTexture(GL_TEXTURE_2D, cursorData.texture); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, overlayTextureId); compositePointer(); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); } glBindTexture(GL_TEXTURE_2D, 0); - glDisable(GL_BLEND); + Context::Disable(Capability::Blend); } }); } @@ -549,7 +554,11 @@ float OpenGLDisplayPlugin::newFramePresentRate() const { } float OpenGLDisplayPlugin::droppedFrameRate() const { - return _droppedFrameRate.rate(); + float result; + withRenderThreadLock([&] { + result = _droppedFrameRate.rate(); + }); + return result; } float OpenGLDisplayPlugin::presentRate() const { @@ -664,3 +673,11 @@ void OpenGLDisplayPlugin::useProgram(const ProgramPtr& program) { _activeProgram = program; } } + +void OpenGLDisplayPlugin::assertIsRenderThread() const { + Q_ASSERT(QThread::currentThread() != _presentThread); +} + +void OpenGLDisplayPlugin::assertIsPresentThread() const { + Q_ASSERT(QThread::currentThread() == _presentThread); +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index c87ff1bc93..ec8aa45840 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -109,7 +109,6 @@ protected: int32_t _alphaUniform { -1 }; ShapeWrapperPtr _plane; - mutable Mutex _mutex; RateCounter<> _droppedFrameRate; RateCounter<> _newFrameRate; RateCounter<> _presentRate; @@ -135,7 +134,27 @@ protected: BasicFramebufferWrapperPtr _compositeFramebuffer; bool _lockCurrentTexture { false }; + void assertIsRenderThread() const; + void assertIsPresentThread() const; + + template + void withPresentThreadLock(F f) const { + assertIsPresentThread(); + Lock lock(_presentMutex); + f(); + } + + template + void withRenderThreadLock(F f) const { + assertIsRenderThread(); + Lock lock(_presentMutex); + f(); + } + private: + // Any resource shared by the main thread and the presntaion thread must + // be serialized through this mutex + mutable Mutex _presentMutex; ProgramPtr _activeProgram; }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 1616dcdb77..ce1a42971e 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -37,7 +38,6 @@ QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { return CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; } - bool HmdDisplayPlugin::internalActivate() { _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); @@ -197,14 +197,43 @@ static ProgramPtr getReprojectionProgram() { #endif +static const char * LASER_VS = R"VS(#version 410 core +uniform mat4 mvp = mat4(1); + +in vec3 Position; + +out vec3 vPosition; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vPosition = Position; +} + +)VS"; + +static const char * LASER_FS = R"FS(#version 410 core + +uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); +in vec3 vPosition; + +out vec4 FragColor; + +void main() { + FragColor = color; +} + +)FS"; + void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled enableVsync(false); _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); + compileProgram(_laserProgram, LASER_VS, LASER_FS); + _laserGeometry = loadLaser(_laserProgram); compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); - + using namespace oglplus; REPROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "reprojection").Location(); INVERSE_PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "inverseProjections").Location(); @@ -215,6 +244,8 @@ void HmdDisplayPlugin::uncustomizeContext() { _sphereSection.reset(); _compositeFramebuffer.reset(); _reprojectionProgram.reset(); + _laserProgram.reset(); + _laserGeometry.reset(); Parent::uncustomizeContext(); } @@ -285,6 +316,8 @@ void HmdDisplayPlugin::compositePointer() { Uniform(*_program, _mvpUniform).Set(mvp); _plane->Draw(); }); + + compositeLasers(); } void HmdDisplayPlugin::internalPresent() { @@ -343,22 +376,122 @@ void HmdDisplayPlugin::setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm: void HmdDisplayPlugin::updateFrameData() { // Check if we have old frame data to discard - { - Lock lock(_mutex); + withPresentThreadLock([&] { auto itr = _frameInfos.find(_currentPresentFrameIndex); if (itr != _frameInfos.end()) { _frameInfos.erase(itr); } - } + }); Parent::updateFrameData(); - { - Lock lock(_mutex); + withPresentThreadLock([&] { _currentPresentFrameInfo = _frameInfos[_currentPresentFrameIndex]; - } + }); } glm::mat4 HmdDisplayPlugin::getHeadPose() const { return _currentRenderFrameInfo.renderPose; } + +bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) { + HandLaserInfo info; + info.mode = mode; + info.color = color; + info.direction = direction; + withRenderThreadLock([&] { + if (hands & Hand::LeftHand) { + _handLasers[0] = info; + } + if (hands & Hand::RightHand) { + _handLasers[1] = info; + } + }); + // FIXME defer to a child class plugin to determine if hand lasers are actually + return true; +} + +static float calculateRayUiCollisionDistance(const glm::mat4& headPose, const glm::vec3& position, const glm::vec3& direction) { + auto relativePosition4 = glm::inverse(headPose) * vec4(position, 1); + auto relativePosition = vec3(relativePosition4) / relativePosition4.w; + auto relativeDirection = glm::inverse(glm::quat_cast(headPose)) * direction; + if (glm::abs(glm::length2(relativeDirection) - 1.0f) > EPSILON) { + relativeDirection = glm::normalize(relativeDirection); + } + float uiRadius = 1.0f; + float instersectionDistance; + if (!glm::intersectRaySphere(relativePosition, relativeDirection, vec3(0), uiRadius * uiRadius, instersectionDistance)) { + return -1; + } + return instersectionDistance; +} + +void HmdDisplayPlugin::compositeLasers() { + std::array handLasers; + std::array renderHandPoses; + withPresentThreadLock([&] { + handLasers = _handLasers; + renderHandPoses = _handPoses; + }); + + // If neither hand laser is activated, exit + if (!handLasers[0].valid() && !handLasers[1].valid()) { + return; + } + + static const glm::mat4 identity; + if (renderHandPoses[0] == identity && renderHandPoses[1] == identity) { + return; + } + + // Render hand lasers + using namespace oglplus; + useProgram(_laserProgram); + _laserGeometry->Use(); + std::array handLaserModelMatrices; + + for (int i = 0; i < 2; ++i) { + if (renderHandPoses[i] == identity) { + continue; + } + const auto& handLaser = handLasers[i]; + if (!handLaser.valid()) { + continue; + } + + const auto& laserDirection = handLaser.direction; + auto model = renderHandPoses[i]; + auto castDirection = glm::quat_cast(model) * laserDirection; + + // Find the intersection of the laser with he UI and use it to scale the model matrix + float distance = calculateRayUiCollisionDistance(_currentPresentFrameInfo.presentPose, vec3(renderHandPoses[i][3]), castDirection); + if (distance < 0) { + continue; + } + + // Make sure we rotate to match the desired laser direction + if (laserDirection != Vectors::UNIT_NEG_Z) { + auto rotation = glm::rotation(Vectors::UNIT_NEG_Z, laserDirection); + model = model * glm::mat4_cast(rotation); + } + + model = glm::scale(model, vec3(distance)); + handLaserModelMatrices[i] = model; + } + + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); + auto view = glm::inverse(eyePose); + const auto& projection = _eyeProjections[eye]; + for (int i = 0; i < 2; ++i) { + if (handLaserModelMatrices[i] == identity) { + continue; + } + Uniform(*_laserProgram, "mvp").Set(projection * view * handLaserModelMatrices[i]); + Uniform(*_laserProgram, "color").Set(handLasers[i].color); + _laserGeometry->Draw(); + // TODO render some kind of visual indicator at the intersection point with the UI. + } + }); +} diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index e6ceb7e376..7cdcf06e9a 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -30,7 +30,7 @@ public: virtual glm::mat4 getHeadPose() const override; - + bool setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) override; protected: virtual void hmdPresent() = 0; @@ -47,6 +47,22 @@ protected: void uncustomizeContext() override; void updateFrameData() override; + void compositeLasers(); + + + struct HandLaserInfo { + HandLaserMode mode { HandLaserMode::None }; + vec4 color { 1.0f }; + vec3 direction { 0, 0, -1 }; + + // Is this hand laser info suitable for drawing? + bool valid() const { + return (mode != HandLaserMode::None && color.a > 0.0f && direction != vec3()); + } + }; + + std::array _handLasers; + std::array _handPoses; std::array _eyeOffsets; std::array _eyeProjections; std::array _eyeInverseProjections; @@ -75,5 +91,7 @@ private: bool _enableReprojection { true }; ShapeWrapperPtr _sphereSection; ProgramPtr _reprojectionProgram; + ProgramPtr _laserProgram; + ShapeWrapperPtr _laserGeometry; }; diff --git a/libraries/gl/src/gl/OglplusHelpers.cpp b/libraries/gl/src/gl/OglplusHelpers.cpp index 5bf0298593..7a535a806d 100644 --- a/libraries/gl/src/gl/OglplusHelpers.cpp +++ b/libraries/gl/src/gl/OglplusHelpers.cpp @@ -45,9 +45,11 @@ in vec2 vTexCoord; out vec4 FragColor; void main() { - FragColor = texture(sampler, vTexCoord); FragColor.a *= alpha; + if (FragColor.a <= 0.0) { + discard; + } } )FS"; @@ -359,6 +361,94 @@ ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov, float aspect, i ); } +namespace oglplus { + namespace shapes { + + class Laser : public DrawingInstructionWriter, public DrawMode { + public: + using IndexArray = std::vector; + using PosArray = std::vector; + /// The type of the index container returned by Indices() + // vertex positions + PosArray _pos_data; + IndexArray _idx_data; + unsigned int _prim_count { 0 }; + + public: + Laser() { + int vertices = 2; + _pos_data.resize(vertices * 3); + _pos_data[0] = 0; + _pos_data[1] = 0; + _pos_data[2] = 0; + + _pos_data[3] = 0; + _pos_data[4] = 0; + _pos_data[5] = -1; + + _idx_data.push_back(0); + _idx_data.push_back(1); + _prim_count = 1; + } + + /// Returns the winding direction of faces + FaceOrientation FaceWinding(void) const { + return FaceOrientation::CCW; + } + + /// Queries the bounding sphere coordinates and dimensions + template + void BoundingSphere(Sphere& bounding_sphere) const { + bounding_sphere = Sphere(0, 0, -0.5, 0.5); + } + + typedef GLuint(Laser::*VertexAttribFunc)(std::vector&) const; + + /// Makes the vertex positions and returns the number of values per vertex + template + GLuint Positions(std::vector& dest) const { + dest.clear(); + dest.insert(dest.begin(), _pos_data.begin(), _pos_data.end()); + return 3; + } + + typedef VertexAttribsInfo< + Laser, + std::tuple + > VertexAttribs; + + + /// Returns element indices that are used with the drawing instructions + const IndexArray & Indices(Default = Default()) const { + return _idx_data; + } + + /// Returns the instructions for rendering of faces + DrawingInstructions Instructions(PrimitiveType primitive) const { + DrawingInstructions instr = MakeInstructions(); + DrawOperation operation; + operation.method = DrawOperation::Method::DrawElements; + operation.mode = primitive; + operation.first = 0; + operation.count = _prim_count * 3; + operation.restart_index = DrawOperation::NoRestartIndex(); + operation.phase = 0; + AddInstruction(instr, operation); + return instr; + } + + /// Returns the instructions for rendering of faces + DrawingInstructions Instructions(Default = Default()) const { + return Instructions(PrimitiveType::Lines); + } + }; + } +} + +ShapeWrapperPtr loadLaser(const ProgramPtr& program) { + return std::make_shared(shapes::ShapeWrapper("Position", shapes::Laser(), *program)); +} + void TextureRecycler::setSize(const uvec2& size) { if (size == _size) { return; diff --git a/libraries/gl/src/gl/OglplusHelpers.h b/libraries/gl/src/gl/OglplusHelpers.h index afb06069b8..8940205b21 100644 --- a/libraries/gl/src/gl/OglplusHelpers.h +++ b/libraries/gl/src/gl/OglplusHelpers.h @@ -64,8 +64,9 @@ ProgramPtr loadCubemapShader(); void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs); ShapeWrapperPtr loadSkybox(ProgramPtr program); ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect = 1.0f); -ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 32, int stacks = 32); - +ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 128, int stacks = 128); +ShapeWrapperPtr loadLaser(const ProgramPtr& program); + // A basic wrapper for constructing a framebuffer with a renderbuffer // for the depth attachment and an undefined type for the color attachement diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 1f6a16cd46..9cb4e071f3 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -168,6 +168,26 @@ public: static const QString& MENU_PATH(); + enum Hand { + LeftHand = 0x01, + RightHand = 0x02, + }; + + enum class HandLaserMode { + None, // Render no hand lasers + Overlay, // Render hand lasers only if they intersect with the UI layer, and stop at the UI layer + }; + + virtual bool setHandLaser( + uint32_t hands, // Bits from the Hand enum + HandLaserMode mode, // Mode in which to render + const vec4& color = vec4(1), // The color of the rendered laser + const vec3& direction = vec3(0, 0, -1) // The direction in which to render the hand lasers + ) { + return false; + } + + signals: void recommendedFramebufferSizeChanged(const QSize & size); // Indicates that this display plugin is no longer valid for use. diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index e9f8545cff..f5fdbd303c 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -24,8 +24,9 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { auto trackingState = ovr_GetTrackingState(_session, _currentRenderFrameInfo.predictedDisplayTime, ovrTrue); _currentRenderFrameInfo.renderPose = toGlm(trackingState.HeadPose.ThePose); _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; + withRenderThreadLock([&] { + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); return true; } diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 8e044fbc16..29a2a4bb1a 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -45,8 +45,9 @@ bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _trackingState = ovrHmd_GetTrackingState(_hmd, _currentRenderFrameInfo.predictedDisplayTime); _currentRenderFrameInfo.rawRenderPose = _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; + withRenderThreadLock([&]{ + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }) return true; } diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index fbade9fd68..92c01dc0a3 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -29,11 +29,13 @@ Q_DECLARE_LOGGING_CATEGORY(displayplugins) const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here -static vr::IVRCompositor* _compositor{ nullptr }; +static vr::IVRCompositor* _compositor { nullptr }; vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; + mat4 _trackedDevicePoseMat4[vr::k_unMaxTrackedDeviceCount]; vec3 _trackedDeviceLinearVelocities[vr::k_unMaxTrackedDeviceCount]; vec3 _trackedDeviceAngularVelocities[vr::k_unMaxTrackedDeviceCount]; + static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; bool _openVrDisplayActive { false }; @@ -59,16 +61,14 @@ bool OpenVrDisplayPlugin::internalActivate() { // left + right eyes _renderTargetSize.x *= 2; - { - Lock lock(_poseMutex); + withRenderThreadLock([&] { openvr_for_each_eye([&](vr::Hmd_Eye eye) { _eyeOffsets[eye] = toGlm(_system->GetEyeToHeadTransform(eye)); _eyeProjections[eye] = toGlm(_system->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, vr::API_OpenGL)); }); // FIXME Calculate the proper combined projection by using GetProjectionRaw values from both eyes _cullingProjection = _eyeProjections[0]; - - } + }); _compositor = vr::VRCompositor(); Q_ASSERT(_compositor); @@ -113,7 +113,7 @@ void OpenVrDisplayPlugin::internalDeactivate() { void OpenVrDisplayPlugin::customizeContext() { // Display plugins in DLLs must initialize glew locally static std::once_flag once; - std::call_once(once, []{ + std::call_once(once, [] { glewExperimental = true; GLenum err = glewInit(); glGetError(); // clear the potential error from glewExperimental @@ -123,9 +123,10 @@ void OpenVrDisplayPlugin::customizeContext() { } void OpenVrDisplayPlugin::resetSensors() { - Lock lock(_poseMutex); - glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); - _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + withRenderThreadLock([&] { + glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + }); } @@ -150,6 +151,24 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, _currentRenderFrameInfo.predictedDisplayTime, _trackedDevicePose, vr::k_unMaxTrackedDeviceCount); + + vr::TrackedDeviceIndex_t handIndices[2] { vr::k_unTrackedDeviceIndexInvalid, vr::k_unTrackedDeviceIndexInvalid }; + { + vr::TrackedDeviceIndex_t controllerIndices[2] ; + auto trackedCount = _system->GetSortedTrackedDeviceIndicesOfClass(vr::TrackedDeviceClass_Controller, controllerIndices, 2); + // Find the left and right hand controllers, if they exist + for (uint32_t i = 0; i < std::min(trackedCount, 2); ++i) { + if (_trackedDevicePose[i].bPoseIsValid) { + auto role = _system->GetControllerRoleForTrackedDeviceIndex(controllerIndices[i]); + if (vr::TrackedControllerRole_LeftHand == role) { + handIndices[0] = controllerIndices[i]; + } else if (vr::TrackedControllerRole_RightHand == role) { + handIndices[1] = controllerIndices[i]; + } + } + } + } + // copy and process predictedTrackedDevicePoses for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { _trackedDevicePoseMat4[i] = _sensorResetMat * toGlm(_trackedDevicePose[i].mDeviceToAbsoluteTracking); @@ -159,18 +178,27 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.rawRenderPose = toGlm(_trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking); _currentRenderFrameInfo.renderPose = _trackedDevicePoseMat4[vr::k_unTrackedDeviceIndex_Hmd]; - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; + bool keyboardVisible = isOpenVrKeyboardShown(); + withRenderThreadLock([&] { + // Make controller poses available to the presentation thread + for (int i = 0; i < 2; ++i) { + if (keyboardVisible || handIndices[i] == vr::k_unTrackedDeviceIndexInvalid) { + _handPoses[i] = glm::mat4(); + } else { + _handPoses[i] = _sensorResetMat * toGlm(_trackedDevicePose[handIndices[i]].mDeviceToAbsoluteTracking); + } + } + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); return true; } void OpenVrDisplayPlugin::hmdPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentPresentFrameIndex) // Flip y-axis since GL UV coords are backwards. - static vr::VRTextureBounds_t leftBounds{ 0, 0, 0.5f, 1 }; - static vr::VRTextureBounds_t rightBounds{ 0.5f, 0, 1, 1 }; + static vr::VRTextureBounds_t leftBounds { 0, 0, 0.5f, 1 }; + static vr::VRTextureBounds_t rightBounds { 0.5f, 0, 1, 1 }; vr::Texture_t texture { (void*)oglplus::GetName(_compositeFramebuffer->color), vr::API_OpenGL, vr::ColorSpace_Auto }; @@ -191,6 +219,10 @@ bool OpenVrDisplayPlugin::isHmdMounted() const { } void OpenVrDisplayPlugin::updatePresentPose() { + mat4 sensorResetMat; + withPresentThreadLock([&] { + sensorResetMat = _sensorResetMat; + }); { float fSecondsSinceLastVsync; _system->GetTimeSinceLastVsync(&fSecondsSinceLastVsync, nullptr); @@ -202,9 +234,8 @@ void OpenVrDisplayPlugin::updatePresentPose() { _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, fPredictedSecondsFromNow, &pose, 1); _currentPresentFrameInfo.rawPresentPose = toGlm(pose.mDeviceToAbsoluteTracking); } - _currentPresentFrameInfo.presentPose = _sensorResetMat * _currentPresentFrameInfo.rawPresentPose; + _currentPresentFrameInfo.presentPose = sensorResetMat * _currentPresentFrameInfo.rawPresentPose; mat3 renderRotation(_currentPresentFrameInfo.rawRenderPose); mat3 presentRotation(_currentPresentFrameInfo.rawPresentPose); _currentPresentFrameInfo.presentReprojection = glm::mat3(glm::inverse(renderRotation) * presentRotation); } - diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index fda5e37c2a..ee693b8091 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -43,5 +43,4 @@ private: vr::IVRSystem* _system { nullptr }; std::atomic _hmdActivityLevel { vr::k_EDeviceActivityLevel_Unknown }; static const QString NAME; - mutable Mutex _poseMutex; }; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 1ff1c65ef8..e4cca6ecd6 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -208,6 +208,10 @@ void disableOpenVrKeyboard() { QObject::disconnect(_focusConnection); } +bool isOpenVrKeyboardShown() { + return _keyboardShown; +} + void handleOpenVrEvents() { if (!activeHmd) { diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 426178cd65..db8bb4f2e8 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -20,6 +20,7 @@ void handleOpenVrEvents(); bool openVrQuitRequested(); void enableOpenVrKeyboard(); void disableOpenVrKeyboard(); +bool isOpenVrKeyboardShown(); template From bb3722d91516d120a6dc14592dc93ef330109efd Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 10 Jun 2016 01:36:25 -0700 Subject: [PATCH 0521/1237] trying again to fix mac build. --- plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 29a2a4bb1a..8d2bc24177 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -47,7 +47,7 @@ bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.rawRenderPose = _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); withRenderThreadLock([&]{ _frameInfos[frameIndex] = _currentRenderFrameInfo; - }) + }); return true; } From 130c0dda318335d70e1d26de6625fb1bc46112ee Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 10 Jun 2016 10:09:09 -0700 Subject: [PATCH 0522/1237] PR feedback --- .../display-plugins/src/display-plugins/OpenGLDisplayPlugin.h | 2 +- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index ec8aa45840..02a30a2570 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -152,7 +152,7 @@ protected: } private: - // Any resource shared by the main thread and the presntaion thread must + // Any resource shared by the main thread and the presentation thread must // be serialized through this mutex mutable Mutex _presentMutex; ProgramPtr _activeProgram; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ce1a42971e..9b71d3703b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -408,9 +408,11 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve } }); // FIXME defer to a child class plugin to determine if hand lasers are actually + // available based on the presence or absence of hand controllers return true; } +// FIXME try to consolidate the duplication of logic between this function and a similar one in CompsitorHelper. static float calculateRayUiCollisionDistance(const glm::mat4& headPose, const glm::vec3& position, const glm::vec3& direction) { auto relativePosition4 = glm::inverse(headPose) * vec4(position, 1); auto relativePosition = vec3(relativePosition4) / relativePosition4.w; @@ -418,6 +420,7 @@ static float calculateRayUiCollisionDistance(const glm::mat4& headPose, const gl if (glm::abs(glm::length2(relativeDirection) - 1.0f) > EPSILON) { relativeDirection = glm::normalize(relativeDirection); } + // FIXME fetch the actual UI radius from... somewhere? float uiRadius = 1.0f; float instersectionDistance; if (!glm::intersectRaySphere(relativePosition, relativeDirection, vec3(0), uiRadius * uiRadius, instersectionDistance)) { From 342fc07d2904192d32283a7eddeb15c425c5507a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 13 Jun 2016 13:06:56 -0700 Subject: [PATCH 0523/1237] select active element in edit.js after change --- scripts/system/html/entityProperties.html | 24 +++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 2a82d8fa74..1c4c3740cd 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -546,17 +546,11 @@ disableProperties(); } else { - var activeElement = document.activeElement; - - try { - var selected = (activeElement - && activeElement.selectionStart == 0 - && activeElement.selectionEnd == activeElement.value.length); - } catch (e) { - var selected = false; - } + properties = data.selections[0].properties; + + //WE SHOULD ONLY UPDATE CHANGED VALUES elID.innerHTML = properties.id; elType.innerHTML = properties.type; @@ -572,6 +566,7 @@ enableProperties(); } + console.log('updated properties :: ',properties) elName.value = properties.name; @@ -811,11 +806,10 @@ elYTextureURL.value = properties.yTextureURL; elZTextureURL.value = properties.zTextureURL; } - - if (selected) { - activeElement.focus(); - activeElement.select(); - } + + var activeElement = document.activeElement; + + activeElement.select(); } } }); @@ -1178,7 +1172,7 @@ for (var i = 0; i < els.length; i++) { var clicked = false; var originalText; - els[i].onfocus = function() { + els[i].onfocus = function(e) { originalText = this.value; this.select(); clicked = false; From 567118f4a9053aca53ade3fb079ba14b4a2a2491 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 13 Jun 2016 13:08:12 -0700 Subject: [PATCH 0524/1237] cleanup --- scripts/system/html/entityProperties.html | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 1c4c3740cd..ca0f9be072 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -550,7 +550,6 @@ properties = data.selections[0].properties; - //WE SHOULD ONLY UPDATE CHANGED VALUES elID.innerHTML = properties.id; elType.innerHTML = properties.type; From 188590a891010e44b0b5c188f3095db3a7af0498 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 13 Jun 2016 13:08:22 -0700 Subject: [PATCH 0525/1237] cleanup --- scripts/system/html/entityProperties.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index ca0f9be072..c8edbdb369 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -564,8 +564,6 @@ } else { enableProperties(); } - - console.log('updated properties :: ',properties) elName.value = properties.name; From eba518cb65634edb8dd47824ecb01f25eb9b27ed Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 13 Jun 2016 14:26:41 -0700 Subject: [PATCH 0526/1237] try to make code that converts bullet-calculated values to parent-frame values more effecient --- libraries/physics/src/EntityMotionState.cpp | 53 +++++++-------------- libraries/shared/src/SpatiallyNestable.cpp | 27 ----------- libraries/shared/src/SpatiallyNestable.h | 4 -- 3 files changed, 17 insertions(+), 67 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 5fffc9901b..8f22c576f0 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -271,44 +271,25 @@ bool EntityMotionState::isCandidateForOwnership() const { bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { // NOTE: we only get here if we think we own the simulation assert(_body); + + bool parentTransformSuccess; + Transform localToWorld = _entity->getParentTransform(parentTransformSuccess); + Transform worldToLocal; + Transform worldVelocityToLocal; + if (parentTransformSuccess) { + localToWorld.evalInverse(worldToLocal); + worldVelocityToLocal = worldToLocal; + worldVelocityToLocal.setTranslation(glm::vec3(0.0f)); + } + // if we've never checked before, our _lastStep will be 0, and we need to initialize our state if (_lastStep == 0) { btTransform xform = _body->getWorldTransform(); - - // _serverPosition = _entity->worldPositionToParent(bulletToGLM(xform.getOrigin())); - // _serverRotation = _entity->worldRotationToParent(bulletToGLM(xform.getRotation())); - // _serverVelocity = _entity->worldVelocityToParent(getBodyLinearVelocityGTSigma()); - - - _serverPosition = bulletToGLM(xform.getOrigin()); - _serverRotation = bulletToGLM(xform.getRotation()); - _serverVelocity = getBodyLinearVelocityGTSigma(); - bool success; - Transform parentTransform = _entity->getParentTransform(success); - if (success) { - Transform bodyTransform; - bodyTransform.setTranslation(_serverPosition); - bodyTransform.setRotation(_serverRotation); - Transform result; - Transform::inverseMult(result, parentTransform, bodyTransform); - _serverPosition = result.getTranslation(); - _serverRotation = result.getRotation(); - - // transform velocity into parent-frame - parentTransform.setTranslation(glm::vec3(0.0f)); - Transform velocityTransform; - velocityTransform.setTranslation(_serverVelocity); - Transform myWorldTransform; - Transform::mult(myWorldTransform, parentTransform, velocityTransform); - myWorldTransform.setTranslation(_serverVelocity); - Transform::inverseMult(result, parentTransform, myWorldTransform); - _serverVelocity = result.getTranslation(); - } - - - + _serverPosition = worldToLocal.transform(bulletToGLM(xform.getOrigin())); + _serverRotation = worldToLocal.getRotation() * bulletToGLM(xform.getRotation()); + _serverVelocity = worldVelocityToLocal.transform(getBodyLinearVelocityGTSigma()); _serverAcceleration = Vectors::ZERO; - _serverAngularVelocity = _entity->worldVelocityToParent(bulletToGLM(_body->getAngularVelocity())); + _serverAngularVelocity = worldVelocityToLocal.transform(bulletToGLM(_body->getAngularVelocity())); _lastStep = simulationStep; _serverActionData = _entity->getActionData(); _numInactiveUpdates = 1; @@ -381,7 +362,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { // compute position error btTransform worldTrans = _body->getWorldTransform(); - glm::vec3 position = _entity->worldPositionToParent(bulletToGLM(worldTrans.getOrigin())); + glm::vec3 position = worldToLocal.transform(bulletToGLM(worldTrans.getOrigin())); float dx2 = glm::distance2(position, _serverPosition); const float MAX_POSITION_ERROR_SQUARED = 0.000004f; // corresponds to 2mm @@ -416,7 +397,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { } } const float MIN_ROTATION_DOT = 0.99999f; // This corresponds to about 0.5 degrees of rotation - glm::quat actualRotation = _entity->worldRotationToParent(bulletToGLM(worldTrans.getRotation())); + glm::quat actualRotation = worldToLocal.getRotation() * bulletToGLM(worldTrans.getRotation()); #ifdef WANT_DEBUG if ((fabsf(glm::dot(actualRotation, _serverRotation)) < MIN_ROTATION_DOT)) { diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 6edf80ab98..29a033f340 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -174,15 +174,6 @@ glm::vec3 SpatiallyNestable::worldToLocal(const glm::vec3& position, return result.getTranslation(); } -glm::vec3 SpatiallyNestable::worldPositionToParent(const glm::vec3& position) { - bool success; - glm::vec3 result = SpatiallyNestable::worldToLocal(position, getParentID(), getParentJointIndex(), success); - if (!success) { - qDebug() << "Warning -- worldToLocal failed" << getID(); - } - return result; -} - glm::vec3 SpatiallyNestable::worldVelocityToLocal(const glm::vec3& velocity, // can be linear or angular const QUuid& parentID, int parentJointIndex, bool& success) { @@ -225,15 +216,6 @@ glm::vec3 SpatiallyNestable::worldVelocityToLocal(const glm::vec3& velocity, // return result.getTranslation(); } -glm::vec3 SpatiallyNestable::worldVelocityToParent(const glm::vec3& velocity) { - bool success; - glm::vec3 result = SpatiallyNestable::worldVelocityToLocal(velocity, getParentID(), getParentJointIndex(), success); - if (!success) { - qDebug() << "Warning -- worldVelocityToLocal failed" << getID(); - } - return result; -} - glm::quat SpatiallyNestable::worldToLocal(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success) { @@ -274,15 +256,6 @@ glm::quat SpatiallyNestable::worldToLocal(const glm::quat& orientation, return result.getRotation(); } -glm::quat SpatiallyNestable::worldRotationToParent(const glm::quat& orientation) { - bool success; - glm::quat result = SpatiallyNestable::worldToLocal(orientation, getParentID(), getParentJointIndex(), success); - if (!success) { - qDebug() << "Warning -- worldToLocal failed" << getID(); - } - return result; -} - glm::vec3 SpatiallyNestable::localToWorld(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success) { diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index ffb00ac040..23beffda53 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -53,10 +53,6 @@ public: static glm::vec3 localToWorld(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success); static glm::quat localToWorld(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success); - glm::vec3 worldPositionToParent(const glm::vec3& position); - glm::vec3 worldVelocityToParent(const glm::vec3& velocity); - glm::quat worldRotationToParent(const glm::quat& orientation); - // world frame virtual const Transform getTransform(bool& success, int depth = 0) const; virtual void setTransform(const Transform& transform, bool& success); From 62051ad297056a5fd0e07966c9ea0cc108f6f3f4 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 13 Jun 2016 14:40:15 -0700 Subject: [PATCH 0527/1237] Working on touch --- .../controllers/src/controllers/Forward.h | 1 + .../oculus/src/OculusBaseDisplayPlugin.cpp | 10 +++ .../oculus/src/OculusControllerManager.cpp | 82 +---------------- plugins/oculus/src/OculusHelpers.cpp | 88 +++++++++++++++++++ plugins/oculus/src/OculusHelpers.h | 5 ++ 5 files changed, 106 insertions(+), 80 deletions(-) diff --git a/libraries/controllers/src/controllers/Forward.h b/libraries/controllers/src/controllers/Forward.h index e1a62556d4..23dd162831 100644 --- a/libraries/controllers/src/controllers/Forward.h +++ b/libraries/controllers/src/controllers/Forward.h @@ -32,6 +32,7 @@ class Mapping; using MappingPointer = std::shared_ptr; using MappingList = std::list; +struct Pose; } #endif diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index f5fdbd303c..a16f630bf8 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -24,7 +24,17 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { auto trackingState = ovr_GetTrackingState(_session, _currentRenderFrameInfo.predictedDisplayTime, ovrTrue); _currentRenderFrameInfo.renderPose = toGlm(trackingState.HeadPose.ThePose); _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; + withRenderThreadLock([&] { + // Make controller poses available to the presentation thread + ovr_for_each_hand([&](ovrHandType hand){ + static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked & ovrStatus_PositionTracked; + if (REQUIRED_HAND_STATUS == (trackingState.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { + _handPoses[hand] = toGlm(trackingState.HandPoses[hand].ThePose); + } else { + _handPoses[hand] = glm::mat4(); + } + }); _frameInfos[frameIndex] = _currentRenderFrameInfo; }); return true; diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 9f0e76363b..0e9ca21804 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -243,91 +243,13 @@ void OculusControllerManager::TouchDevice::focusOutEvent() { void OculusControllerManager::TouchDevice::handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose) { - // When the sensor-to-world rotation is identity the coordinate axes look like this: - // - // user - // forward - // -z - // | - // y| user - // y o----x right - // o-----x user - // | up - // | - // z - // - // Rift - - // From ABOVE the hand canonical axes looks like this: - // - // | | | | y | | | | - // | | | | | | | | | - // | | | | | - // |left | / x---- + \ |right| - // | _/ z \_ | - // | | | | - // | | | | - // - - // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down - // the rotation to align the Touch axes with those of the hands is: - // - // touchToHand = halfTurnAboutY * quaterTurnAboutX - - // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. - // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that - // the combination (measurement * offset) is identity at this orientation. - // - // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) - // - // An approximate offset for the Touch can be obtained by inspection: - // - // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) - // - // So the full equation is: - // - // Q = combinedMeasurement * touchToHand - // - // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) - // - // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) - auto poseId = hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND; auto& pose = _poseStateMap[poseId]; - - static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); - static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); - static const glm::quat touchToHand = yFlip * quarterX; - - static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); - - static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; - static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; - - static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches - static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET * 2.0f); - static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; - static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; - - auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); - auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); - - glm::quat rotation = toGlm(handPose.ThePose.Orientation); - - pose.translation = toGlm(handPose.ThePose.Position); - pose.translation += rotation * translationOffset; - pose.rotation = rotation * rotationOffset; - pose.angularVelocity = toGlm(handPose.AngularVelocity); - pose.velocity = toGlm(handPose.LinearVelocity); - pose.valid = true; - + pose = ovrControllerPoseToHandPose(hand, handPose); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; pose = pose.transform(controllerToAvatar); + } bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 6ddace684b..705c0a0781 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -15,6 +15,9 @@ #include #include +#include +#include + using Mutex = std::mutex; using Lock = std::unique_lock; @@ -191,3 +194,88 @@ void SwapFramebufferWrapper::onBind(oglplus::Framebuffer::Target target) { void SwapFramebufferWrapper::onUnbind(oglplus::Framebuffer::Target target) { glFramebufferTexture2D(toEnum(target), GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); } + + +controller::Pose ovrControllerPoseToHandPose( + ovrHandType hand, + const ovrPoseStatef& handPose) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 2.0f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); + + glm::quat rotation = toGlm(handPose.ThePose.Orientation); + + controller::Pose pose; + pose.translation = toGlm(handPose.ThePose.Position); + pose.translation += rotation * translationOffset; + pose.rotation = rotation * rotationOffset; + pose.angularVelocity = toGlm(handPose.AngularVelocity); + pose.velocity = toGlm(handPose.LinearVelocity); + pose.valid = true; + return pose; +} \ No newline at end of file diff --git a/plugins/oculus/src/OculusHelpers.h b/plugins/oculus/src/OculusHelpers.h index 2f13c45466..66cdccf15a 100644 --- a/plugins/oculus/src/OculusHelpers.h +++ b/plugins/oculus/src/OculusHelpers.h @@ -13,6 +13,7 @@ #include #include +#include void logWarning(const char* what); void logFatal(const char* what); @@ -128,3 +129,7 @@ protected: private: ovrSession _session; }; + +controller::Pose ovrControllerPoseToHandPose( + ovrHandType hand, + const ovrPoseStatef& handPose); From 6d2181f0f137d8789efabb341bb5b64053edbcd1 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 11:59:38 -0700 Subject: [PATCH 0528/1237] Combine DISTANCE_HOLDING and CONTINUE_DISTANCE_HOLDING states. --- .../system/controllers/handControllerGrab.js | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 25cd100991..f4bcefe3ac 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -157,20 +157,19 @@ var STATE_OFF = 0; var STATE_SEARCHING = 1; var STATE_HOLD_SEARCHING = 2; var STATE_DISTANCE_HOLDING = 3; -var STATE_CONTINUE_DISTANCE_HOLDING = 4; -var STATE_NEAR_GRABBING = 5; -var STATE_CONTINUE_NEAR_GRABBING = 6; -var STATE_NEAR_TRIGGER = 7; -var STATE_CONTINUE_NEAR_TRIGGER = 8; -var STATE_FAR_TRIGGER = 9; -var STATE_CONTINUE_FAR_TRIGGER = 10; -var STATE_RELEASE = 11; -var STATE_EQUIP = 12; -var STATE_HOLD = 13; -var STATE_CONTINUE_HOLD = 14; -var STATE_CONTINUE_EQUIP = 15; -var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 16; -var STATE_WAITING_FOR_EQUIP_THUMB_RELEASE = 17; +var STATE_NEAR_GRABBING = 4; +var STATE_CONTINUE_NEAR_GRABBING = 5; +var STATE_NEAR_TRIGGER = 6; +var STATE_CONTINUE_NEAR_TRIGGER = 7; +var STATE_FAR_TRIGGER = 8; +var STATE_CONTINUE_FAR_TRIGGER = 9; +var STATE_RELEASE = 10; +var STATE_EQUIP = 11; +var STATE_HOLD = 12; +var STATE_CONTINUE_HOLD = 13; +var STATE_CONTINUE_EQUIP = 14; +var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 15; +var STATE_WAITING_FOR_EQUIP_THUMB_RELEASE = 16; // "collidesWith" is specified by comma-separated list of group names // the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar @@ -198,12 +197,9 @@ CONTROLLER_STATE_MACHINE[STATE_HOLD_SEARCHING] = { }; CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { name: "distance_holding", + enterMethod: "distanceHoldingEnter", updateMethod: "distanceHolding" }; -CONTROLLER_STATE_MACHINE[STATE_CONTINUE_DISTANCE_HOLDING] = { - name: "continue_distance_holding", - updateMethod: "continueDistanceHolding" -}; CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { name: "near_grabbing", updateMethod: "nearGrabbing" @@ -1207,7 +1203,7 @@ function MyController(hand) { return (dimensions.x * dimensions.y * dimensions.z) * density; } - this.distanceHolding = function() { + this.distanceHoldingEnter = function() { // controller pose is in avatar frame var avatarControllerPose = @@ -1256,9 +1252,12 @@ function MyController(hand) { this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); if (this.actionID !== null) { - this.setState(STATE_CONTINUE_DISTANCE_HOLDING); this.activateEntity(this.grabbedEntity, grabbedProperties, false); this.callEntityMethodOnGrabbed("startDistanceGrab"); + } else { + // addAction failed? + this.setState(STATE_RELEASE); + return; } this.turnOffVisualizations(); @@ -1267,7 +1266,7 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.continueDistanceHolding = function() { + this.distanceHolding = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); From bd3326d2fc45c66da5e403de99d4d1f905c8539c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 13:59:10 -0700 Subject: [PATCH 0529/1237] Collapse continueNearGrabbing and nearGrabbing into a single state. * Moved the update logic from STATE_NEAR_GRABBING, STATE_HOLD & STATE_EQUIP into the entryMethods for those states. * Removed STATE_CONTINUE_NEAR_GRABBING, STATE_CONTINUE_HOLD & STATE_CONTINUE_EQUIP states This functionality has been moved into the updateMethod for their respective states. This *should* be a pure re-factor no functionality was changed. --- .../system/controllers/handControllerGrab.js | 76 +++++-------------- 1 file changed, 19 insertions(+), 57 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f4bcefe3ac..3e611b9f00 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -19,7 +19,7 @@ Script.include("/~/system/libraries/utils.js"); // add lines where the hand ray picking is happening // var WANT_DEBUG = false; -var WANT_DEBUG_STATE = false; +var WANT_DEBUG_STATE = true; var WANT_DEBUG_SEARCH_NAME = null; // @@ -158,7 +158,6 @@ var STATE_SEARCHING = 1; var STATE_HOLD_SEARCHING = 2; var STATE_DISTANCE_HOLDING = 3; var STATE_NEAR_GRABBING = 4; -var STATE_CONTINUE_NEAR_GRABBING = 5; var STATE_NEAR_TRIGGER = 6; var STATE_CONTINUE_NEAR_TRIGGER = 7; var STATE_FAR_TRIGGER = 8; @@ -166,8 +165,6 @@ var STATE_CONTINUE_FAR_TRIGGER = 9; var STATE_RELEASE = 10; var STATE_EQUIP = 11; var STATE_HOLD = 12; -var STATE_CONTINUE_HOLD = 13; -var STATE_CONTINUE_EQUIP = 14; var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 15; var STATE_WAITING_FOR_EQUIP_THUMB_RELEASE = 16; @@ -202,27 +199,18 @@ CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { }; CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { name: "near_grabbing", + enterMethod: "nearGrabbingEnter", updateMethod: "nearGrabbing" }; CONTROLLER_STATE_MACHINE[STATE_EQUIP] = { name: "equip", + enterMethod: "nearGrabbingEnter", updateMethod: "nearGrabbing" }; CONTROLLER_STATE_MACHINE[STATE_HOLD] = { - name: "hold", - updateMethod: "nearGrabbing" -}; -CONTROLLER_STATE_MACHINE[STATE_CONTINUE_NEAR_GRABBING] = { - name: "continue_near_grabbing", - updateMethod: "continueNearGrabbing" -}; -CONTROLLER_STATE_MACHINE[STATE_CONTINUE_HOLD] = { name: "continue_hold", - updateMethod: "continueNearGrabbing" -}; -CONTROLLER_STATE_MACHINE[STATE_CONTINUE_EQUIP] = { - name: "continue_equip", - updateMethod: "continueNearGrabbing" + enterMethod: "nearGrabbingEnter", + updateMethod: "nearGrabbing" }; CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { name: "near_trigger", @@ -400,6 +388,8 @@ function MyController(hand) { print("WARNING: could not find state " + this.state + " in state machine"); } + this.state = newState; + // enter the new state if (CONTROLLER_STATE_MACHINE[newState]) { var enterMethodName = CONTROLLER_STATE_MACHINE[newState].enterMethod; @@ -410,8 +400,6 @@ function MyController(hand) { } else { print("WARNING: could not find newState " + newState + " in state machine"); } - - this.state = newState; }; this.debugLine = function(closePoint, farPoint, color) { @@ -1254,10 +1242,6 @@ function MyController(hand) { if (this.actionID !== null) { this.activateEntity(this.grabbedEntity, grabbedProperties, false); this.callEntityMethodOnGrabbed("startDistanceGrab"); - } else { - // addAction failed? - this.setState(STATE_RELEASE); - return; } this.turnOffVisualizations(); @@ -1483,20 +1467,9 @@ function MyController(hand) { } } - this.nearGrabbing = function() { + this.nearGrabbingEnter = function() { var now = Date.now(); - if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - return; - } - if (this.state == STATE_HOLD && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - return; - } - this.lineOff(); this.overlayLineOff(); @@ -1578,17 +1551,6 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("startEquip"); } - if (this.state == STATE_NEAR_GRABBING) { - // near grabbing - this.setState(STATE_CONTINUE_NEAR_GRABBING); - } else if (this.state == STATE_HOLD) { - // holding - this.setState(STATE_CONTINUE_HOLD); - } else { // (this.state == STATE_EQUIP) - // equipping - this.setState(STATE_CONTINUE_EQUIP); - } - this.currentHandControllerTipPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; this.currentObjectTime = Date.now(); @@ -1599,29 +1561,29 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.continueNearGrabbing = function() { - if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.triggerSmoothedReleased()) { + this.nearGrabbing = function() { + if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); return; } - if (this.state == STATE_CONTINUE_HOLD && this.secondaryReleased()) { + if (this.state == STATE_HOLD && this.secondaryReleased()) { this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseEquip"); return; } - if (this.state == STATE_CONTINUE_EQUIP && this.thumbPressed()) { + if (this.state == STATE_EQUIP && this.thumbPressed()) { this.setState(STATE_WAITING_FOR_RELEASE_THUMB_RELEASE); this.callEntityMethodOnGrabbed("releaseEquip"); return; } - if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.thumbPressed()) { + if (this.state == STATE_NEAR_GRABBING && this.thumbPressed()) { this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); this.callEntityMethodOnGrabbed("startEquip"); return; } - if (this.state == STATE_CONTINUE_HOLD && this.thumbPressed()) { + if (this.state == STATE_HOLD && this.thumbPressed()) { this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); return; } @@ -1652,9 +1614,9 @@ function MyController(hand) { print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + props.parentID + " " + vec3toStr(props.position)); this.setState(STATE_RELEASE); - if (this.state == STATE_CONTINUE_NEAR_GRABBING) { + if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); - } else { // (this.state == STATE_CONTINUE_EQUIP || this.state == STATE_CONTINUE_HOLD) + } else { // (this.state == STATE_EQUIP || this.state == STATE_HOLD) this.callEntityMethodOnGrabbed("releaseEquip"); } return; @@ -1693,10 +1655,10 @@ function MyController(hand) { this.currentObjectTime = now; var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - if (this.state === STATE_CONTINUE_EQUIP || this.state === STATE_CONTINUE_HOLD) { + if (this.state === STATE_EQUIP || this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("continueEquip"); } - if (this.state == STATE_CONTINUE_NEAR_GRABBING) { + if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("continueNearGrab"); } @@ -2029,7 +1991,7 @@ function MyController(hand) { this.activateEntity(this.grabbedEntity, loadedProps, true); this.isInitialGrab = true; this.callEntityMethodOnGrabbed("startEquip"); - this.setState(STATE_CONTINUE_EQUIP); + this.setState(STATE_EQUIP); } } }; From fe65df350bc23282cd5d3260a9de69dafba6efb1 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 15:05:34 -0700 Subject: [PATCH 0530/1237] Combined the release and off states. --- .../system/controllers/handControllerGrab.js | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 3e611b9f00..386a4905f1 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -162,7 +162,6 @@ var STATE_NEAR_TRIGGER = 6; var STATE_CONTINUE_NEAR_TRIGGER = 7; var STATE_FAR_TRIGGER = 8; var STATE_CONTINUE_FAR_TRIGGER = 9; -var STATE_RELEASE = 10; var STATE_EQUIP = 11; var STATE_HOLD = 12; var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 15; @@ -180,6 +179,7 @@ var CONTROLLER_STATE_MACHINE = {}; CONTROLLER_STATE_MACHINE[STATE_OFF] = { name: "off", + enterMethod: "offEnter", updateMethod: "off" }; CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { @@ -228,10 +228,6 @@ CONTROLLER_STATE_MACHINE[STATE_CONTINUE_FAR_TRIGGER] = { name: "continue_far_trigger", updateMethod: "continueFarTrigger" }; -CONTROLLER_STATE_MACHINE[STATE_RELEASE] = { - name: "release", - updateMethod: "release" -}; CONTROLLER_STATE_MACHINE[STATE_WAITING_FOR_EQUIP_THUMB_RELEASE] = { name: "waiting_for_equip_thumb_release", updateMethod: "waitingForEquipThumbRelease" @@ -918,12 +914,12 @@ function MyController(hand) { this.checkForStrayChildren(); if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); return; } if (this.state == STATE_HOLD_SEARCHING && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); return; } @@ -1252,7 +1248,7 @@ function MyController(hand) { this.distanceHolding = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("releaseGrab"); return; } @@ -1563,12 +1559,12 @@ function MyController(hand) { this.nearGrabbing = function() { if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("releaseGrab"); return; } if (this.state == STATE_HOLD && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("releaseEquip"); return; } @@ -1593,7 +1589,7 @@ function MyController(hand) { var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("releaseGrab"); return; } @@ -1613,7 +1609,7 @@ function MyController(hand) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + props.parentID + " " + vec3toStr(props.position)); - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); } else { // (this.state == STATE_EQUIP || this.state == STATE_HOLD) @@ -1691,13 +1687,13 @@ function MyController(hand) { }; this.waitingForReleaseThumbRelease = function() { if (this.thumbReleased() && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); } }; this.nearTrigger = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; } @@ -1707,7 +1703,7 @@ function MyController(hand) { this.farTrigger = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; } @@ -1717,7 +1713,7 @@ function MyController(hand) { this.continueNearTrigger = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; } @@ -1726,7 +1722,7 @@ function MyController(hand) { this.continueFarTrigger = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; } @@ -1743,7 +1739,7 @@ function MyController(hand) { if (intersection.accurate) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { - this.setState(STATE_RELEASE); + this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; } @@ -1757,7 +1753,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueFarTrigger"); }; - this.release = function() { + this.offEnter = function() { this.turnLightsOff(); this.turnOffVisualizations(); @@ -1780,7 +1776,6 @@ function MyController(hand) { this.deactivateEntity(this.grabbedEntity, noVelocity); this.actionID = null; - this.setState(STATE_OFF); Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'release', From f8f62a4ff1120f4a83cea57b82b614ed80fa0d4f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 13 Jun 2016 15:10:52 -0700 Subject: [PATCH 0531/1237] remove unused function --- libraries/shared/src/SpatiallyNestable.cpp | 42 ---------------------- libraries/shared/src/SpatiallyNestable.h | 2 -- 2 files changed, 44 deletions(-) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 29a033f340..2a3cb4af47 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -174,48 +174,6 @@ glm::vec3 SpatiallyNestable::worldToLocal(const glm::vec3& position, return result.getTranslation(); } -glm::vec3 SpatiallyNestable::worldVelocityToLocal(const glm::vec3& velocity, // can be linear or angular - const QUuid& parentID, int parentJointIndex, - bool& success) { - Transform result; - QSharedPointer parentFinder = DependencyManager::get(); - if (!parentFinder) { - success = false; - return glm::vec3(); - } - - Transform parentTransform; - auto parentWP = parentFinder->find(parentID, success); - if (!success) { - return glm::vec3(); - } - - auto parent = parentWP.lock(); - if (!parentID.isNull() && !parent) { - success = false; - return glm::vec3(); - } - - if (parent) { - parentTransform = parent->getTransform(parentJointIndex, success); - if (!success) { - return glm::vec3(); - } - parentTransform.setScale(1.0f); // TODO: scale - } - success = true; - - parentTransform.setTranslation(glm::vec3(0.0f)); - - Transform velocityTransform; - velocityTransform.setTranslation(velocity); - Transform myWorldTransform; - Transform::mult(myWorldTransform, parentTransform, velocityTransform); - myWorldTransform.setTranslation(velocity); - Transform::inverseMult(result, parentTransform, myWorldTransform); - return result.getTranslation(); -} - glm::quat SpatiallyNestable::worldToLocal(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success) { diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 23beffda53..c2563a1188 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -46,8 +46,6 @@ public: virtual void setParentJointIndex(quint16 parentJointIndex); static glm::vec3 worldToLocal(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success); - static glm::vec3 worldVelocityToLocal(const glm::vec3& position, const QUuid& parentID, - int parentJointIndex, bool& success); static glm::quat worldToLocal(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success); static glm::vec3 localToWorld(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success); From b9bf7977535e4780bf99cb14f97d4413927bf8b5 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 13 Jun 2016 15:26:48 -0700 Subject: [PATCH 0532/1237] Fixing issues with unclosed groups in settings persistence --- libraries/networking/src/AccountManager.cpp | 1 + libraries/script-engine/src/ScriptEngines.cpp | 1 + libraries/shared/src/SettingHandle.cpp | 9 +++++++++ libraries/shared/src/SettingHandle.h | 2 ++ libraries/shared/src/SettingInterface.cpp | 2 ++ 5 files changed, 15 insertions(+) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index bac031885f..26b3801ec1 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -173,6 +173,7 @@ void AccountManager::setAuthURL(const QUrl& authURL) { << "from previous settings file"; } } + settings.endGroup(); if (_accountInfo.getAccessToken().token.isEmpty()) { qCWarning(networking) << "Unable to load account file. No existing account settings will be loaded."; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 29c223f4b3..beddc21787 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -334,6 +334,7 @@ void ScriptEngines::clearScripts() { Settings settings; settings.beginWriteArray(SETTINGS_KEY); settings.remove(""); + settings.endArray(); } void ScriptEngines::saveScripts() { diff --git a/libraries/shared/src/SettingHandle.cpp b/libraries/shared/src/SettingHandle.cpp index b2f23f5a04..13f9ea48ce 100644 --- a/libraries/shared/src/SettingHandle.cpp +++ b/libraries/shared/src/SettingHandle.cpp @@ -18,6 +18,7 @@ const QString Settings::firstRun { "firstRun" }; + Settings::Settings() : _manager(DependencyManager::get()), _locker(&(_manager->getLock())) @@ -25,6 +26,9 @@ Settings::Settings() : } Settings::~Settings() { + if (_prefixes.size() != 0) { + qFatal("Unstable Settings Prefixes: You must call endGroup for every beginGroup and endArray for every begin*Array call"); + } } void Settings::remove(const QString& key) { @@ -50,14 +54,17 @@ bool Settings::contains(const QString& key) const { } int Settings::beginReadArray(const QString & prefix) { + _prefixes.push(prefix); return _manager->beginReadArray(prefix); } void Settings::beginWriteArray(const QString& prefix, int size) { + _prefixes.push(prefix); _manager->beginWriteArray(prefix, size); } void Settings::endArray() { + _prefixes.pop(); _manager->endArray(); } @@ -66,10 +73,12 @@ void Settings::setArrayIndex(int i) { } void Settings::beginGroup(const QString& prefix) { + _prefixes.push(prefix); _manager->beginGroup(prefix); } void Settings::endGroup() { + _prefixes.pop(); _manager->endGroup(); } diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index e83c563036..f19fc5875b 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -58,8 +58,10 @@ public: void setQuatValue(const QString& name, const glm::quat& quatValue); void getQuatValueIfValid(const QString& name, glm::quat& quatValue); +private: QSharedPointer _manager; QWriteLocker _locker; + QStack _prefixes; }; namespace Setting { diff --git a/libraries/shared/src/SettingInterface.cpp b/libraries/shared/src/SettingInterface.cpp index 630d52bf7d..1ebaa5cf82 100644 --- a/libraries/shared/src/SettingInterface.cpp +++ b/libraries/shared/src/SettingInterface.cpp @@ -98,6 +98,8 @@ namespace Setting { // Register Handle manager->registerHandle(this); _isInitialized = true; + } else { + qWarning() << "Settings interface used after manager destroyed"; } // Load value from disk From dc9121433d4ac09e3b783181a5613c93feb96b26 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 13 Jun 2016 16:37:34 -0700 Subject: [PATCH 0533/1237] trying to fix skeleton switching bug --- interface/src/main.cpp | 4 ++-- libraries/avatars/src/AvatarData.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 8fc0384aee..f0c7938b2a 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -90,11 +90,11 @@ int main(int argc, const char* argv[]) { qDebug() << "Interface instance appears to be running, exiting"; - return EXIT_SUCCESS; + //return EXIT_SUCCESS; } #ifdef Q_OS_WIN - return EXIT_SUCCESS; + //return EXIT_SUCCESS; #endif } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index ff7438bb17..26bf0fb2a2 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1052,6 +1052,8 @@ void AvatarData::setJointMappingsFromNetworkReply() { _jointIndices.insert(_jointNames.at(i), i + 1); } + sendIdentityPacket(); + networkReply->deleteLater(); } @@ -1094,6 +1096,7 @@ void AvatarData::sendIdentityPacket() { void AvatarData::updateJointMappings() { _jointIndices.clear(); _jointNames.clear(); + _jointData.clear(); if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); From 5ac946059258f7418d336d30c6ed95e5521b84b5 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 13 Jun 2016 16:43:33 -0700 Subject: [PATCH 0534/1237] fix branching --- scripts/system/users.js | 92 +++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index c010b7ea24..68d82040e9 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var PopUpMenu = function (properties) { +var PopUpMenu = function(properties) { var value = properties.value, promptOverlay, valueOverlay, @@ -217,7 +217,7 @@ var PopUpMenu = function (properties) { }; }; -var usersWindow = (function () { +var usersWindow = (function() { var baseURL = Script.resolvePath("assets/images/tools/"), WINDOW_WIDTH = 260, @@ -253,7 +253,11 @@ var usersWindow = (function () { WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_LEFT_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_RADIUS = 4, - WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, + WINDOW_BORDER_COLOR = { + red: 255, + green: 255, + blue: 255 + }, WINDOW_BORDER_ALPHA = 0.5, windowBorder, @@ -377,9 +381,12 @@ var usersWindow = (function () { isMirrorDisplay = false, isFullscreenMirror = false, - windowPosition = { }, // Bottom left corner of window pane. + windowPosition = {}, // Bottom left corner of window pane. isMovingWindow = false, - movingClickOffset = { x: 0, y: 0 }, + movingClickOffset = { + x: 0, + y: 0 + }, isUsingScrollbars = false, isMovingScrollbar = false, @@ -401,9 +408,7 @@ var usersWindow = (function () { } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER - + windowLineHeight + VISIBILITY_SPACER - + windowLineHeight + WINDOW_BASE_MARGIN; + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + windowLineHeight + VISIBILITY_SPACER + windowLineHeight + WINDOW_BASE_MARGIN; // Limit window to height of viewport above window position minus VU meter and mirror if displayed windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; @@ -456,17 +461,14 @@ var usersWindow = (function () { x: scrollbarBackgroundPosition.x, y: scrollbarBackgroundPosition.y }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 - + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); Overlays.editOverlay(scrollbarBar, { x: scrollbarBackgroundPosition.x + 1, y: scrollbarBarPosition.y }); x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER - - windowLineHeight - VISIBILITY_SPACER - - windowLineHeight - WINDOW_BASE_MARGIN; + y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER - windowLineHeight - VISIBILITY_SPACER - windowLineHeight - WINDOW_BASE_MARGIN; Overlays.editOverlay(friendsButton, { x: x, y: y @@ -554,9 +556,34 @@ var usersWindow = (function () { usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); + checkLoggedIn(); } - processUsers = function () { + var loggedIn = false; + + function checkLoggedIn() { + loggedIn = Account.isLoggedIn(); + if (loggedIn === false) { + Overlays.editOverlay(friendsButton, { + visible: false + }); + visibilityControl.setVisible(false); + displayControl.setVisible(false); + } else { + + Overlays.editOverlay(friendsButton, { + visible: true + }); + visibilityControl.setVisible(true); + displayControl.setVisible(true); + loggedIn = true; + + + } + } + + + processUsers = function() { var response, myUsername, user, @@ -609,7 +636,7 @@ var usersWindow = (function () { } }; - pollUsersTimedOut = function () { + pollUsersTimedOut = function() { print("Error: Request for users status timed out"); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; @@ -730,8 +757,7 @@ var usersWindow = (function () { userClicked = firstUserToDisplay + lineClicked; - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX - && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } @@ -800,12 +826,8 @@ var usersWindow = (function () { var isVisible; if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x - && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN - && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y - && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) - / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); updateOverlayPositions(); @@ -831,13 +853,9 @@ var usersWindow = (function () { isVisible = isBorderVisible; if (isVisible) { - isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x - && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH - && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y - && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; } else { - isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH - && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; } if (isVisible !== isBorderVisible) { isBorderVisible = isVisible; @@ -878,8 +896,7 @@ var usersWindow = (function () { isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay - || isFullscreenMirror !== oldIsFullscreenMirror) { + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay || isFullscreenMirror !== oldIsFullscreenMirror) { calculateWindowHeight(); updateUsersDisplay(); } @@ -929,8 +946,8 @@ var usersWindow = (function () { } else { hmdViewport = Controller.getRecommendedOverlayRect(); windowPosition = { - x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. - y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. + x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. + y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. }; } @@ -938,7 +955,7 @@ var usersWindow = (function () { windowBorder = Overlays.addOverlay("rectangle", { x: 0, - y: viewport.y, // Start up off-screen + y: viewport.y, // Start up off-screen width: WINDOW_BORDER_WIDTH, height: windowBorderHeight, radius: WINDOW_BORDER_RADIUS, @@ -1101,6 +1118,11 @@ var usersWindow = (function () { visible: isVisible && !isMinimized }); + + Script.setTimeout(function() { + checkLoggedIn() + }, 0); + Controller.mousePressEvent.connect(onMousePressEvent); Controller.mouseMoveEvent.connect(onMouseMoveEvent); Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); @@ -1143,4 +1165,4 @@ var usersWindow = (function () { setUp(); Script.scriptEnding.connect(tearDown); -}()); +}()); \ No newline at end of file From b01eb0439df25f813f1f7f9d329bd1cda0266cc5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 16:49:38 -0700 Subject: [PATCH 0535/1237] removed sticky thumb equip states. * removed STATE_WAITING_FOR_EQUIP_THUMB_RELEASE and waitingForEquipThumbRelease * removed STATE_EQUIP * removed STATE_WAITING_FOR_RELEASE_THUMB_RELEASE and waitingForReleaseThumbRelease * removed 'Hifi-Object-Manipulation' 'loaded' support and checkNewlyLoaded method. --- .../system/controllers/handControllerGrab.js | 84 ++++--------------- 1 file changed, 14 insertions(+), 70 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 386a4905f1..2857c91936 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -158,14 +158,11 @@ var STATE_SEARCHING = 1; var STATE_HOLD_SEARCHING = 2; var STATE_DISTANCE_HOLDING = 3; var STATE_NEAR_GRABBING = 4; -var STATE_NEAR_TRIGGER = 6; -var STATE_CONTINUE_NEAR_TRIGGER = 7; -var STATE_FAR_TRIGGER = 8; -var STATE_CONTINUE_FAR_TRIGGER = 9; -var STATE_EQUIP = 11; -var STATE_HOLD = 12; -var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 15; -var STATE_WAITING_FOR_EQUIP_THUMB_RELEASE = 16; +var STATE_NEAR_TRIGGER = 4; +var STATE_CONTINUE_NEAR_TRIGGER = 6; +var STATE_FAR_TRIGGER = 7; +var STATE_CONTINUE_FAR_TRIGGER = 8; +var STATE_HOLD = 9; // "collidesWith" is specified by comma-separated list of group names // the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar @@ -202,11 +199,6 @@ CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { enterMethod: "nearGrabbingEnter", updateMethod: "nearGrabbing" }; -CONTROLLER_STATE_MACHINE[STATE_EQUIP] = { - name: "equip", - enterMethod: "nearGrabbingEnter", - updateMethod: "nearGrabbing" -}; CONTROLLER_STATE_MACHINE[STATE_HOLD] = { name: "continue_hold", enterMethod: "nearGrabbingEnter", @@ -228,14 +220,6 @@ CONTROLLER_STATE_MACHINE[STATE_CONTINUE_FAR_TRIGGER] = { name: "continue_far_trigger", updateMethod: "continueFarTrigger" }; -CONTROLLER_STATE_MACHINE[STATE_WAITING_FOR_EQUIP_THUMB_RELEASE] = { - name: "waiting_for_equip_thumb_release", - updateMethod: "waitingForEquipThumbRelease" -}; -CONTROLLER_STATE_MACHINE[STATE_WAITING_FOR_RELEASE_THUMB_RELEASE] = { - name: "waiting_for_release_thumb_release", - updateMethod: "waitingForReleaseThumbRelease" -}; function stateToName(state) { return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; @@ -1483,7 +1467,7 @@ function MyController(hand) { var handPosition = this.getHandPosition(); var hasPresetPosition = false; - if ((this.state == STATE_EQUIP || this.state == STATE_HOLD) && this.hasPresetOffsets()) { + if (this.state == STATE_HOLD && this.hasPresetOffsets()) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; @@ -1499,7 +1483,7 @@ function MyController(hand) { var currentObjectPosition = grabbedProperties.position; var offset = Vec3.subtract(currentObjectPosition, handPosition); this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); - if (this.temporaryPositionOffset && (this.state == STATE_EQUIP)) { + if (this.temporaryPositionOffset) { this.offsetPosition = this.temporaryPositionOffset; // hasPresetPosition = true; } @@ -1543,7 +1527,7 @@ function MyController(hand) { if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("startNearGrab"); - } else { // this.state == STATE_EQUIP || this.state == STATE_HOLD + } else { // this.state == STATE_HOLD this.callEntityMethodOnGrabbed("startEquip"); } @@ -1568,21 +1552,6 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("releaseEquip"); return; } - if (this.state == STATE_EQUIP && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_RELEASE_THUMB_RELEASE); - this.callEntityMethodOnGrabbed("releaseEquip"); - return; - } - if (this.state == STATE_NEAR_GRABBING && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - this.callEntityMethodOnGrabbed("startEquip"); - return; - } - if (this.state == STATE_HOLD && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); - return; - } this.heartBeat(this.grabbedEntity); @@ -1612,7 +1581,7 @@ function MyController(hand) { this.setState(STATE_OFF); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); - } else { // (this.state == STATE_EQUIP || this.state == STATE_HOLD) + } else { // this.state == STATE_HOLD this.callEntityMethodOnGrabbed("releaseEquip"); } return; @@ -1651,7 +1620,7 @@ function MyController(hand) { this.currentObjectTime = now; var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - if (this.state === STATE_EQUIP || this.state === STATE_HOLD) { + if (this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("continueEquip"); } if (this.state == STATE_NEAR_GRABBING) { @@ -1680,17 +1649,6 @@ function MyController(hand) { } }; - this.waitingForEquipThumbRelease = function() { - if (this.thumbReleased() && this.triggerSmoothedReleased()) { - this.setState(STATE_EQUIP); - } - }; - this.waitingForReleaseThumbRelease = function() { - if (this.thumbReleased() && this.triggerSmoothedReleased()) { - this.setState(STATE_OFF); - } - }; - this.nearTrigger = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_OFF); @@ -1968,6 +1926,8 @@ function MyController(hand) { setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; + // AJT: WTF TO DO WITH THIS? + /* this.checkNewlyLoaded = function(loadedEntityID) { if (this.state == STATE_OFF || this.state == STATE_SEARCHING || @@ -1989,6 +1949,7 @@ function MyController(hand) { this.setState(STATE_EQUIP); } } + */ }; var rightController = new MyController(RIGHT_HAND); @@ -2044,7 +2005,7 @@ handleHandMessages = function(channel, message, sender) { var data = JSON.parse(message); var selectedController = (data.hand === 'left') ? leftController : rightController; selectedController.release(); - selectedController.setState(STATE_EQUIP); + selectedController.setState(STATE_HOLD); selectedController.grabbedEntity = data.entityID; } catch (e) {} @@ -2066,23 +2027,6 @@ handleHandMessages = function(channel, message, sender) { } } catch (e) {} - } else if (channel === 'Hifi-Object-Manipulation') { - if (sender !== MyAvatar.sessionUUID) { - return; - } - - var parsedMessage = null; - try { - parsedMessage = JSON.parse(message); - } catch (e) { - print('error parsing Hifi-Object-Manipulation message'); - return; - } - - if (parsedMessage.action === 'loaded') { - rightController.checkNewlyLoaded(parsedMessage['grabbedEntity']); - leftController.checkNewlyLoaded(parsedMessage['grabbedEntity']); - } } } } From 526fc7d062366dbf3b2be7c8e87b80c70d01e391 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 13 Jun 2016 17:13:10 -0700 Subject: [PATCH 0536/1237] Make handControllerGrab independent of whether we're using laser or Reticle. --- scripts/system/controllers/handControllerGrab.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 25cd100991..53c16f26c1 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -300,7 +300,10 @@ function propsArePhysical(props) { // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; function isIn2DMode() { - return EXTERNALLY_MANAGED_2D_MINOR_MODE && Reticle.visible; + // In this version, we make our own determination of whether we're aimed a HUD element, + // because other scripts (such as handControllerPointer) might be using some other visualization + // instead of setting Reticle.visible. + return EXTERNALLY_MANAGED_2D_MINOR_MODE && (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position)); } function restore2DMode() { if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { From dfd03d5e6184ffb7a0f3b9a0b81e584dcf78e2a6 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 13 Jun 2016 17:15:51 -0700 Subject: [PATCH 0537/1237] Prepare for working laser. But at this point, we still show the laser AND the Reticle. --- .../system/controllers/handControllerPointer.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 1728647e5e..f5c0c4bb7c 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -319,6 +319,7 @@ function toggleHand() { // unequivocally switch which hand controls mouse positi activeTrigger = rightTrigger; activeHudLaser = RIGHT_HUD_LASER; } + clearSystemLaser(); } function makeToggleAction(hand) { // return a function(0|1) that makes the specified hand control mouse when 1 return function (on) { @@ -335,8 +336,8 @@ Script.scriptEnding.connect(clickMapping.disable); clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); // Full smoothed trigger is a click. -clickMapping.from(rightTrigger.full).to(Controller.Actions.ReticleClick); -clickMapping.from(leftTrigger.full).to(Controller.Actions.ReticleClick); +clickMapping.from(rightTrigger.full).when(isPointingAtOverlay).to(Controller.Actions.ReticleClick); +clickMapping.from(leftTrigger.full).when(isPointingAtOverlay).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); // Partial smoothed trigger is activation. @@ -363,8 +364,8 @@ Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. -var systemLaserOn = false; var SYSTEM_LASER_DIRECTION = Vec3.normalize({x: 0, y: -1, z: -1}); // Guessing 45 degrees. +var systemLaserOn = false; function clearSystemLaser() { if (!systemLaserOn) { return; @@ -379,8 +380,12 @@ function turnOffVisualization(optionalEnableClicks) { // because we're showing c clearSystemLaser(); } else if (!systemLaserOn) { print('FIXME remove: setHandLasers', activeHudLaser, true, JSON.stringify(LASER_COLOR_XYZW), JSON.stringify(SYSTEM_LASER_DIRECTION)); - HMD.setHandLasers(activeHudLaser, true, LASER_COLOR_XYZW, SYSTEM_LASER_DIRECTION); + // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. + /*Reticle.visible = !*/HMD.setHandLasers(activeHudLaser, true, LASER_COLOR_XYZW, SYSTEM_LASER_DIRECTION); + Reticle.visible = true; // FIXME: just for now, while hand lasers has the bug that requires this. systemLaserOn = true; + } else { + Reticle.visible = true; } if (!visualizationIsShowing) { return; @@ -415,9 +420,11 @@ function updateVisualization(controllerPosition, controllerDirection, hudPositio // For now, though, we present a false projection of the cursor onto whatever is below it. This is // different from the hand beam termination because the false projection is from the camera, while // the hand beam termination is from the hand. + /* // FIXME: We can tighten this up later, once we know what will and won't be included. var eye = Camera.getPosition(); var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); + */ Reticle.visible = false; return visualizationIsShowing; // In case we change caller to act conditionally. @@ -465,7 +472,6 @@ function update() { if (HMD.active) { // Doesn't hurt anything without the guard, but consider it documentation. Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! } - Reticle.visible = true; return turnOffVisualization(true); } // We are not pointing at a HUD element (but it could be a 3d overlay). From 6707f889b82d2e8fdaa065ac32e4d536f24b8dd6 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 13 Jun 2016 17:21:09 -0700 Subject: [PATCH 0538/1237] Fixing laser offset, support laser in Oculus --- .../display-plugins/OpenGLDisplayPlugin.cpp | 1 + .../src/display-plugins/OpenGLDisplayPlugin.h | 1 + .../display-plugins/hmd/HmdDisplayPlugin.cpp | 32 +++---- .../display-plugins/hmd/HmdDisplayPlugin.h | 4 +- .../oculus/src/OculusBaseDisplayPlugin.cpp | 24 ++++-- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 26 ++++-- plugins/openvr/src/OpenVrHelpers.cpp | 86 ++++++++++++++++++- plugins/openvr/src/OpenVrHelpers.h | 4 + plugins/openvr/src/ViveControllerManager.cpp | 83 +----------------- 9 files changed, 138 insertions(+), 123 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 363bde15e6..23c836b3de 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -517,6 +517,7 @@ void OpenGLDisplayPlugin::compositeLayers() { glBindTexture(GL_TEXTURE_2D, 0); Context::Disable(Capability::Blend); } + compositeExtra(); }); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 02a30a2570..69653b8c76 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -74,6 +74,7 @@ protected: virtual void compositeScene(); virtual void compositeOverlay(); virtual void compositePointer(); + virtual void compositeExtra() {}; virtual bool hasFocus() const override; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 9b71d3703b..9ae843d45d 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -316,10 +316,9 @@ void HmdDisplayPlugin::compositePointer() { Uniform(*_program, _mvpUniform).Set(mvp); _plane->Draw(); }); - - compositeLasers(); } + void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) @@ -412,24 +411,7 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve return true; } -// FIXME try to consolidate the duplication of logic between this function and a similar one in CompsitorHelper. -static float calculateRayUiCollisionDistance(const glm::mat4& headPose, const glm::vec3& position, const glm::vec3& direction) { - auto relativePosition4 = glm::inverse(headPose) * vec4(position, 1); - auto relativePosition = vec3(relativePosition4) / relativePosition4.w; - auto relativeDirection = glm::inverse(glm::quat_cast(headPose)) * direction; - if (glm::abs(glm::length2(relativeDirection) - 1.0f) > EPSILON) { - relativeDirection = glm::normalize(relativeDirection); - } - // FIXME fetch the actual UI radius from... somewhere? - float uiRadius = 1.0f; - float instersectionDistance; - if (!glm::intersectRaySphere(relativePosition, relativeDirection, vec3(0), uiRadius * uiRadius, instersectionDistance)) { - return -1; - } - return instersectionDistance; -} - -void HmdDisplayPlugin::compositeLasers() { +void HmdDisplayPlugin::compositeExtra() { std::array handLasers; std::array renderHandPoses; withPresentThreadLock([&] { @@ -465,10 +447,16 @@ void HmdDisplayPlugin::compositeLasers() { const auto& laserDirection = handLaser.direction; auto model = renderHandPoses[i]; auto castDirection = glm::quat_cast(model) * laserDirection; + if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { + castDirection = glm::normalize(castDirection); + } + + // FIXME fetch the actual UI radius from... somewhere? + float uiRadius = 1.0f; // Find the intersection of the laser with he UI and use it to scale the model matrix - float distance = calculateRayUiCollisionDistance(_currentPresentFrameInfo.presentPose, vec3(renderHandPoses[i][3]), castDirection); - if (distance < 0) { + float distance; + if (!glm::intersectRaySphere(vec3(renderHandPoses[i][3]), castDirection, vec3(0), uiRadius * uiRadius, distance)) { continue; } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 7cdcf06e9a..738028d684 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -46,9 +46,7 @@ protected: void customizeContext() override; void uncustomizeContext() override; void updateFrameData() override; - - void compositeLasers(); - + void compositeExtra() override; struct HandLaserInfo { HandLaserMode mode { HandLaserMode::None }; diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index a16f630bf8..61c4696f51 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -8,6 +8,7 @@ #include "OculusBaseDisplayPlugin.h" #include +#include #include "OculusHelpers.h" @@ -25,16 +26,21 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.renderPose = toGlm(trackingState.HeadPose.ThePose); _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; + std::array handPoses; + // Make controller poses available to the presentation thread + ovr_for_each_hand([&](ovrHandType hand) { + static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked & ovrStatus_PositionTracked; + if (REQUIRED_HAND_STATUS != (trackingState.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { + return; + } + + auto correctedPose = ovrControllerPoseToHandPose(hand, trackingState.HandPoses[hand]); + static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); + handPoses[hand] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); + }); + withRenderThreadLock([&] { - // Make controller poses available to the presentation thread - ovr_for_each_hand([&](ovrHandType hand){ - static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked & ovrStatus_PositionTracked; - if (REQUIRED_HAND_STATUS == (trackingState.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { - _handPoses[hand] = toGlm(trackingState.HandPoses[hand].ThePose); - } else { - _handPoses[hand] = glm::mat4(); - } - }); + _handPoses = handPoses; _frameInfos[frameIndex] = _currentRenderFrameInfo; }); return true; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 92c01dc0a3..d57dddf512 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -179,15 +180,26 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.renderPose = _trackedDevicePoseMat4[vr::k_unTrackedDeviceIndex_Hmd]; bool keyboardVisible = isOpenVrKeyboardShown(); + + std::array handPoses; + if (!keyboardVisible) { + for (int i = 0; i < 2; ++i) { + if (handIndices[i] == vr::k_unTrackedDeviceIndexInvalid) { + continue; + } + auto deviceIndex = handIndices[i]; + const mat4& mat = _trackedDevicePoseMat4[deviceIndex]; + const vec3& linearVelocity = _trackedDeviceLinearVelocities[deviceIndex]; + const vec3& angularVelocity = _trackedDeviceAngularVelocities[deviceIndex]; + auto correctedPose = openVrControllerPoseToHandPose(i == 0, mat, linearVelocity, angularVelocity); + static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); + handPoses[i] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); + } + } + withRenderThreadLock([&] { // Make controller poses available to the presentation thread - for (int i = 0; i < 2; ++i) { - if (keyboardVisible || handIndices[i] == vr::k_unTrackedDeviceIndexInvalid) { - _handPoses[i] = glm::mat4(); - } else { - _handPoses[i] = _sensorResetMat * toGlm(_trackedDevicePose[handIndices[i]].mDeviceToAbsoluteTracking); - } - } + _handPoses = handPoses; _frameInfos[frameIndex] = _currentRenderFrameInfo; }); return true; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index e4cca6ecd6..4b4eef0538 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -18,8 +18,9 @@ #include #include - #include +#include +#include Q_DECLARE_LOGGING_CATEGORY(displayplugins) Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") @@ -242,3 +243,86 @@ void handleOpenVrEvents() { } +controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 2.0f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (isLeftHand ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (isLeftHand ? leftRotationOffset : rightRotationOffset); + + glm::vec3 position = extractTranslation(mat); + glm::quat rotation = glm::normalize(glm::quat_cast(mat)); + + position += rotation * translationOffset; + rotation = rotation * rotationOffset; + + // transform into avatar frame + auto result = controller::Pose(position, rotation); + // handle change in velocity due to translationOffset + result.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); + result.angularVelocity = angularVelocity; + return result; +} \ No newline at end of file diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index db8bb4f2e8..131db517ab 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -12,6 +12,8 @@ #include #include +#include + bool openVrSupported(); vr::IVRSystem* acquireOpenVrSystem(); @@ -55,3 +57,5 @@ inline vr::HmdMatrix34_t toOpenVr(const mat4& m) { } return result; } + +controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 4e17a2edf4..83b72c5c08 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -37,10 +37,6 @@ vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); -static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches -static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET * 2.0f); static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b"; static const QString MENU_PARENT = "Avatar"; @@ -382,86 +378,11 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand) { - // When the sensor-to-world rotation is identity the coordinate axes look like this: - // - // user - // forward - // -z - // | - // y| user - // y o----x right - // o-----x user - // | up - // | - // z - // - // Vive - // - - // From ABOVE the hand canonical axes looks like this: - // - // | | | | y | | | | - // | | | | | | | | | - // | | | | | - // |left | / x---- + \ |right| - // | _/ z \_ | - // | | | | - // | | | | - // - - // So when the user is standing in Vive space facing the -zAxis with hands outstretched and palms down - // the rotation to align the Vive axes with those of the hands is: - // - // QviveToHand = halfTurnAboutY * quaterTurnAboutX - - // Due to how the Vive controllers fit into the palm there is an offset that is different for each hand. - // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that - // the combination (measurement * offset) is identity at this orientation. - // - // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) - // - // An approximate offset for the Vive can be obtained by inspection: - // - // Qoffset = glm::inverse(glm::angleAxis(sign * PI/4.0f, zAxis) * glm::angleAxis(PI/2.0f, xAxis)) - // - // So the full equation is: - // - // Q = combinedMeasurement * viveToHand - // - // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) - // - // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) - - static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); - static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); - static const glm::quat viveToHand = yFlip * quarterX; - - static const glm::quat leftQuaterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat rightQuaterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); - - static const glm::quat leftRotationOffset = glm::inverse(leftQuaterZ * eighthX) * viveToHand; - static const glm::quat rightRotationOffset = glm::inverse(rightQuaterZ * eighthX) * viveToHand; - - static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; - static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; - - auto translationOffset = (isLeftHand ? leftTranslationOffset : rightTranslationOffset); - auto rotationOffset = (isLeftHand ? leftRotationOffset : rightRotationOffset); - - glm::vec3 position = extractTranslation(mat); - glm::quat rotation = glm::normalize(glm::quat_cast(mat)); - - position += rotation * translationOffset; - rotation = rotation * rotationOffset; + auto pose = openVrControllerPoseToHandPose(isLeftHand, mat, linearVelocity, angularVelocity); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; - auto avatarPose = controller::Pose(position, rotation); - // handle change in velocity due to translationOffset - avatarPose.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); - avatarPose.angularVelocity = angularVelocity; - _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = avatarPose.transform(controllerToAvatar); + _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = pose.transform(controllerToAvatar); } bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { From 1306417c8e8a9f68f71ca002f2b2a23e8495dadb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 13 Jun 2016 17:48:58 -0700 Subject: [PATCH 0539/1237] more verbose messaging about incorrect entity types --- libraries/entities/src/EntityTypes.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/entities/src/EntityTypes.cpp b/libraries/entities/src/EntityTypes.cpp index 7b1133c2aa..4ba4ad5676 100644 --- a/libraries/entities/src/EntityTypes.cpp +++ b/libraries/entities/src/EntityTypes.cpp @@ -17,6 +17,7 @@ #include "EntityItem.h" #include "EntityItemProperties.h" #include "EntityTypes.h" +#include "EntitiesLogging.h" #include "LightEntityItem.h" #include "ModelEntityItem.h" @@ -63,6 +64,9 @@ EntityTypes::EntityType EntityTypes::getEntityTypeFromName(const QString& name) if (matchedTypeName != _nameToTypeMap.end()) { return matchedTypeName.value(); } + if (name.size() > 0 && name[0].isLower()) { + qCDebug(entities) << "Entity types must start with an uppercase letter. Please change the type" << name; + } return Unknown; } From df9fd6c7fc7c172ebe9c751364071d0b8c1c7b3c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 13 Jun 2016 18:35:39 -0700 Subject: [PATCH 0540/1237] reset vive grip buttons to 0 if not pressed --- plugins/openvr/src/ViveControllerManager.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 0d2cdc940a..a7a0a2f56f 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -363,6 +363,10 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint } else if (button == vr::k_EButton_SteamVR_Touchpad) { _buttonPressedMap.insert(isLeftHand ? LS : RS); } + } else { + if (button == vr::k_EButton_Grip) { + _axisStateMap[isLeftHand ? LEFT_GRIP : RIGHT_GRIP] = 0.0f; + } } if (touched) { From 28b6cc2777a577474a027ace28b350e948bf70b3 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 18:54:21 -0700 Subject: [PATCH 0541/1237] handControllerGrab.js is now eslint clean. --- .../system/controllers/handControllerGrab.js | 127 ++++++------------ 1 file changed, 43 insertions(+), 84 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2857c91936..34d4ca6294 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,7 +10,7 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt, pointInExtents, vec3equal, setEntityCustomData, getEntityCustomData */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ Script.include("/~/system/libraries/utils.js"); @@ -47,7 +47,6 @@ var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects var FAR_TO_NEAR_GRAB_PADDING_FACTOR = 1.2; @@ -81,13 +80,6 @@ var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-g var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds -// -// equip -// - -var EQUIP_SPRING_SHUTOFF_DISTANCE = 0.05; -var EQUIP_SPRING_TIMEFRAME = 0.4; // how quickly objects move to their new position - // // other constants // @@ -158,7 +150,7 @@ var STATE_SEARCHING = 1; var STATE_HOLD_SEARCHING = 2; var STATE_DISTANCE_HOLDING = 3; var STATE_NEAR_GRABBING = 4; -var STATE_NEAR_TRIGGER = 4; +var STATE_NEAR_TRIGGER = 5; var STATE_CONTINUE_NEAR_TRIGGER = 6; var STATE_FAR_TRIGGER = 7; var STATE_CONTINUE_FAR_TRIGGER = 8; @@ -200,7 +192,7 @@ CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { updateMethod: "nearGrabbing" }; CONTROLLER_STATE_MACHINE[STATE_HOLD] = { - name: "continue_hold", + name: "hold", enterMethod: "nearGrabbingEnter", updateMethod: "nearGrabbing" }; @@ -505,7 +497,7 @@ function MyController(hand) { this.overlayLine = Overlays.addOverlay("line3d", lineProperties); } else { - var success = Overlays.editOverlay(this.overlayLine, { + Overlays.editOverlay(this.overlayLine, { lineWidth: 5, start: closePoint, end: farPoint, @@ -621,13 +613,6 @@ function MyController(hand) { }) }; - this.renewParticleBeamLifetime = function() { - var props = Entities.getEntityProperties(this.particleBeamObject, "age"); - Entities.editEntity(this.particleBeamObject, { - lifetime: TEMPORARY_PARTICLE_BEAM_LIFETIME + props.age // renew lifetime - }) - } - this.evalLightWorldTransform = function(modelPos, modelRot) { var MODEL_LIGHT_POSITION = { @@ -648,7 +633,7 @@ function MyController(hand) { }; }; - this.handleSpotlight = function(parentID, position) { + this.handleSpotlight = function(parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -672,7 +657,7 @@ function MyController(hand) { exponent: 0.3, cutoff: 20, lifetime: LIFETIME, - position: lightTransform.p, + position: lightTransform.p }; if (this.spotlight === null) { @@ -680,12 +665,12 @@ function MyController(hand) { } else { Entities.editEntity(this.spotlight, { //without this, this light would maintain rotation with its parent - rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0), + rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0) }) } }; - this.handlePointLight = function(parentID, position) { + this.handlePointLight = function(parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -709,13 +694,11 @@ function MyController(hand) { exponent: 0.3, cutoff: 20, lifetime: LIFETIME, - position: lightTransform.p, + position: lightTransform.p }; if (this.pointlight === null) { this.pointlight = Entities.addEntity(lightProperties); - } else { - } }; @@ -915,12 +898,9 @@ function MyController(hand) { } var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation; var currentControllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).translation), MyAvatar.position); - var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation)); - var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); @@ -941,7 +921,7 @@ function MyController(hand) { this.lastPickTime = now; } - rayPickedCandidateEntities = []; // the list of candidates to consider grabbing + var rayPickedCandidateEntities = []; // the list of candidates to consider grabbing this.intersectionDistance = 0.0; for (var index = 0; index < pickRays.length; ++index) { @@ -974,8 +954,8 @@ function MyController(hand) { } } - nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); - candidateEntities = rayPickedCandidateEntities.concat(nearPickedCandidateEntities); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var candidateEntities = rayPickedCandidateEntities.concat(nearPickedCandidateEntities); var forbiddenNames = ["Grab Debug Entity", "grab pointer"]; var forbiddenTypes = ['Unknown', 'Light', 'PolyLine', 'Zone']; @@ -990,9 +970,9 @@ function MyController(hand) { var propsForCandidate = Entities.getEntityProperties(candidateEntities[i], GRABBABLE_PROPERTIES); var near = (nearPickedCandidateEntities.indexOf(candidateEntities[i]) >= 0); - var isPhysical = propsArePhysical(propsForCandidate); + var physical = propsArePhysical(propsForCandidate); var grabbable; - if (isPhysical) { + if (physical) { // physical things default to grabbable grabbable = true; } else { @@ -1051,7 +1031,7 @@ function MyController(hand) { } if (this.state == STATE_SEARCHING && - !isPhysical && distance > NEAR_PICK_MAX_DISTANCE && !near && !grabbableDataForCandidate.wantsTrigger) { + !physical && distance > NEAR_PICK_MAX_DISTANCE && !near && !grabbableDataForCandidate.wantsTrigger) { // we can't distance-grab non-physical if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not physical and too far for near-grab"); @@ -1068,18 +1048,18 @@ function MyController(hand) { } if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.secondarySqueezed())) { // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. - var near = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; + var isNear = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; var isPhysical = propsArePhysical(props); // near or far trigger if (grabbableData.wantsTrigger) { - this.setState(near ? STATE_NEAR_TRIGGER : STATE_FAR_TRIGGER); + this.setState(isNear ? STATE_NEAR_TRIGGER : STATE_FAR_TRIGGER); return; } // near grab with action or equip var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); var refCount = ("refCount" in grabData) ? grabData.refCount : 0; - if (near && (refCount < 1 || entityHasActions(this.grabbedEntity))) { + if (isNear && (refCount < 1 || entityHasActions(this.grabbedEntity))) { if (this.state == STATE_SEARCHING) { this.setState(STATE_NEAR_GRABBING); } else { // (this.state == STATE_HOLD_SEARCHING) @@ -1092,7 +1072,7 @@ function MyController(hand) { return; } // far grab - if (isPhysical && !near) { + if (isPhysical && !isNear) { if (entityIsGrabbedByOther(this.grabbedEntity)) { // don't distance grab something that is already grabbed. if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { @@ -1142,9 +1122,6 @@ function MyController(hand) { } return; } - if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': fell through."); - } } //search line visualizations @@ -1266,10 +1243,13 @@ function MyController(hand) { radius); // double delta controller rotation + /* + var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, controllerRotation, DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), Quat.inverse(this.previousControllerRotation)); + */ // update the currentObject position and rotation. this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); @@ -1323,9 +1303,10 @@ function MyController(hand) { } } + /* var defaultConstraintData = { axisStart: false, - axisEnd: false, + axisEnd: false } var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); @@ -1343,6 +1324,7 @@ function MyController(hand) { z: this.currentObjectPosition.z } } + */ var handPosition = this.getHandPosition(); @@ -1448,7 +1430,6 @@ function MyController(hand) { } this.nearGrabbingEnter = function() { - var now = Date.now(); this.lineOff(); this.overlayLineOff(); @@ -1503,7 +1484,7 @@ function MyController(hand) { // grab entity via parenting this.actionID = null; var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - reparentProps = { + var reparentProps = { parentID: MyAvatar.sessionUUID, parentJointIndex: handJointIndex } @@ -1573,7 +1554,7 @@ function MyController(hand) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to autoequip -- we also // need to fail the findEntities test. - nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + @@ -1597,9 +1578,7 @@ function MyController(hand) { // from the palm. var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; - var now = Date.now(); - var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerTipPosition); // meters var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds if (deltaTime > 0.0) { @@ -1619,7 +1598,6 @@ function MyController(hand) { this.currentHandControllerTipPosition = handControllerPosition; this.currentObjectTime = now; - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); if (this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("continueEquip"); } @@ -1843,6 +1821,8 @@ function MyController(hand) { } this.deactivateEntity = function(entityID, noVelocity) { + var deactiveProps; + if (!this.entityActivated) { return; } @@ -1852,7 +1832,7 @@ function MyController(hand) { if (data && data["refCount"]) { data["refCount"] = data["refCount"] - 1; if (data["refCount"] < 1) { - var deactiveProps = { + deactiveProps = { gravity: data["gravity"], collidesWith: data["collidesWith"], collisionless: data["collisionless"], @@ -1865,7 +1845,6 @@ function MyController(hand) { // it looks like the dropped thing should fall, give it a little velocity. var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]) var parentID = props.parentID; - var forceVelocity = false; var doSetVelocity = false; if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && propsArePhysical(props)) { @@ -1900,7 +1879,7 @@ function MyController(hand) { // the parent causes it to go off in the wrong direction. This is a bug that should // be fixed. Entities.editEntity(entityID, { - velocity: this.currentVelocity, + velocity: this.currentVelocity // angularVelocity: this.currentAngularVelocity }); } @@ -1908,7 +1887,7 @@ function MyController(hand) { data = null; } else if (this.shouldResetParentOnRelease) { // we parent-grabbed this from another parent grab. try to put it back where we found it. - var deactiveProps = { + deactiveProps = { parentID: this.previousParentID, parentJointIndex: this.previousParentJointIndex, velocity: {x: 0.0, y: 0.0, z: 0.0}, @@ -1925,32 +1904,7 @@ function MyController(hand) { } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; - - // AJT: WTF TO DO WITH THIS? - /* - this.checkNewlyLoaded = function(loadedEntityID) { - if (this.state == STATE_OFF || - this.state == STATE_SEARCHING || - this.state == STATE_HOLD_SEARCHING) { - var loadedProps = Entities.getEntityProperties(loadedEntityID); - if (loadedProps.parentID != MyAvatar.sessionUUID) { - return; - } - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - if (loadedProps.parentJointIndex != handJointIndex) { - return; - } - print("--- handControllerGrab found loaded entity ---"); - // an entity has been loaded and it's where this script would have equipped something, so switch states. - this.grabbedEntity = loadedEntityID; - this.activateEntity(this.grabbedEntity, loadedProps, true); - this.isInitialGrab = true; - this.callEntityMethodOnGrabbed("startEquip"); - this.setState(STATE_EQUIP); - } - } - */ -}; +} var rightController = new MyController(RIGHT_HAND); var leftController = new MyController(LEFT_HAND); @@ -1988,7 +1942,8 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); -handleHandMessages = function(channel, message, sender) { +var handleHandMessages = function(channel, message, sender) { + var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { @@ -2002,17 +1957,19 @@ handleHandMessages = function(channel, message, sender) { } } else if (channel === 'Hifi-Hand-Grab') { try { - var data = JSON.parse(message); + data = JSON.parse(message); var selectedController = (data.hand === 'left') ? leftController : rightController; selectedController.release(); selectedController.setState(STATE_HOLD); selectedController.grabbedEntity = data.entityID; - } catch (e) {} + } catch (e) { + print("WARNING: error parsing Hifi-Hand-Grab message"); + } } else if (channel === 'Hifi-Hand-RayPick-Blacklist') { try { - var data = JSON.parse(message); + data = JSON.parse(message); var action = data.action; var id = data.id; var index = blacklist.indexOf(id); @@ -2026,7 +1983,9 @@ handleHandMessages = function(channel, message, sender) { } } - } catch (e) {} + } catch (e) { + print("WARNING: error parsing Hifi-Hand-RayPick-Blacklist message"); + } } } } From f11735550fbeae426aa0f2675c80764a95d04b94 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 18:55:53 -0700 Subject: [PATCH 0542/1237] Fix for Vive RIGHT_GRIP LEFT_GRIP button never becoming zero. --- plugins/openvr/src/ViveControllerManager.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 4e17a2edf4..97c502454b 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -370,6 +370,10 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint } else if (button == vr::k_EButton_SteamVR_Touchpad) { _buttonPressedMap.insert(isLeftHand ? LS : RS); } + } else { + if (button == vr::k_EButton_Grip) { + _axisStateMap[isLeftHand ? LEFT_GRIP : RIGHT_GRIP] = 0.0f; + } } if (touched) { From cb51d00c1d50090d43a1ae03eb236f3f8e2a8fdc Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Jun 2016 19:08:33 -0700 Subject: [PATCH 0543/1237] Removed CONTINUE_FAR_TRIGGER and CONTINUE_NEAR_TRIGGER --- .../system/controllers/handControllerGrab.js | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 34d4ca6294..6daafa425f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -151,10 +151,8 @@ var STATE_HOLD_SEARCHING = 2; var STATE_DISTANCE_HOLDING = 3; var STATE_NEAR_GRABBING = 4; var STATE_NEAR_TRIGGER = 5; -var STATE_CONTINUE_NEAR_TRIGGER = 6; -var STATE_FAR_TRIGGER = 7; -var STATE_CONTINUE_FAR_TRIGGER = 8; -var STATE_HOLD = 9; +var STATE_FAR_TRIGGER = 6; +var STATE_HOLD = 7; // "collidesWith" is specified by comma-separated list of group names // the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar @@ -197,21 +195,15 @@ CONTROLLER_STATE_MACHINE[STATE_HOLD] = { updateMethod: "nearGrabbing" }; CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { - name: "near_trigger", + name: "trigger", + enterMethod: "nearTriggerEnter", updateMethod: "nearTrigger" }; -CONTROLLER_STATE_MACHINE[STATE_CONTINUE_NEAR_TRIGGER] = { - name: "continue_near_trigger", - updateMethod: "continueNearTrigger" -}; CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { name: "far_trigger", + enterMethod: "farTriggerEnter", updateMethod: "farTrigger" }; -CONTROLLER_STATE_MACHINE[STATE_CONTINUE_FAR_TRIGGER] = { - name: "continue_far_trigger", - updateMethod: "continueFarTrigger" -}; function stateToName(state) { return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; @@ -1627,27 +1619,15 @@ function MyController(hand) { } }; - this.nearTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); - this.callEntityMethodOnGrabbed("stopNearTrigger"); - return; - } + this.nearTriggerEnter = function() { this.callEntityMethodOnGrabbed("startNearTrigger"); - this.setState(STATE_CONTINUE_NEAR_TRIGGER); }; - this.farTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); - this.callEntityMethodOnGrabbed("stopFarTrigger"); - return; - } + this.farTriggerEnter = function() { this.callEntityMethodOnGrabbed("startFarTrigger"); - this.setState(STATE_CONTINUE_FAR_TRIGGER); }; - this.continueNearTrigger = function() { + this.nearTrigger = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("stopNearTrigger"); @@ -1656,7 +1636,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueNearTrigger"); }; - this.continueFarTrigger = function() { + this.farTrigger = function() { if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.setState(STATE_OFF); this.callEntityMethodOnGrabbed("stopFarTrigger"); From 414c43c0124e924cd6dd04fbb856736aca0b1422 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 13 Jun 2016 19:10:50 -0700 Subject: [PATCH 0544/1237] Current state --- .../render-utils/src/DebugDeferredBuffer.cpp | 31 ++- .../render-utils/src/DebugDeferredBuffer.h | 7 +- .../render-utils/src/RenderDeferredTask.cpp | 19 +- .../render-utils/src/SubsurfaceScattering.cpp | 75 +++++-- .../render-utils/src/SubsurfaceScattering.h | 28 ++- .../src/debug_deferred_buffer.slf | 1 + .../subsurfaceScattering_drawScattering.slf | 177 +++++++++++------ libraries/render/src/render/BlurTask.cpp | 183 ++++++++---------- libraries/render/src/render/BlurTask.h | 55 +++--- libraries/render/src/render/BlurTask.slh | 2 +- libraries/render/src/render/Task.h | 85 ++++---- .../utilities/render/framebuffer.qml | 2 + .../utilities/render/surfaceGeometryPass.qml | 28 ++- 13 files changed, 401 insertions(+), 292 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 0e007b8314..076f4a3880 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -157,10 +157,26 @@ static const std::string DEFAULT_NORMAL_CURVATURE_SHADER{ " }" }; +static const std::string DEFAULT_DIFFUSED_CURVATURE_SHADER{ + "vec4 getFragmentColor() {" + " return vec4(pow(vec3(texture(diffusedCurvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" + // " return vec4(pow(vec3(texture(curvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" + " }" +}; + +static const std::string DEFAULT_DIFFUSED_NORMAL_CURVATURE_SHADER{ + "vec4 getFragmentColor() {" + //" return vec4(pow(vec3(texture(curvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" + " return vec4(pow(vec3(texture(diffusedCurvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" + " }" +}; + static const std::string DEFAULT_SCATTERING_SHADER{ "vec4 getFragmentColor() {" - // " return vec4(pow(vec3(texture(scatteringMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" - " return vec4(vec3(texture(scatteringMap, uv).xyz), 1.0);" + " return vec4(pow(vec3(texture(scatteringMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + // " return vec4(vec3(texture(scatteringMap, uv).xyz), 1.0);" " }" }; @@ -233,6 +249,10 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_CURVATURE_SHADER; case NormalCurvatureMode: return DEFAULT_NORMAL_CURVATURE_SHADER; + case DiffusedCurvatureMode: + return DEFAULT_DIFFUSED_CURVATURE_SHADER; + case DiffusedNormalCurvatureMode: + return DEFAULT_DIFFUSED_NORMAL_CURVATURE_SHADER; case ScatteringMode: return DEFAULT_SCATTERING_SHADER; case AmbientOcclusionMode: @@ -317,12 +337,13 @@ void DebugDeferredBuffer::configure(const Config& config) { _size = config.size; } -void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputBuffer) { +void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; - auto& scatteringFramebuffer = inputBuffer; + auto& diffusedCurvatureFramebuffer = inputs.getFirst(); + auto& scatteringFramebuffer = inputs.getSecond(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); @@ -354,7 +375,7 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setResourceTexture(Shadow, lightStage.lights[0]->shadow.framebuffer->getDepthStencilBuffer()); batch.setResourceTexture(Pyramid, framebufferCache->getDepthPyramidTexture()); batch.setResourceTexture(Curvature, framebufferCache->getCurvatureTexture()); - //batch.setResourceTexture(DiffusedCurvature, diffusedCurvatureBuffer); + batch.setResourceTexture(DiffusedCurvature, diffusedCurvatureFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(Scattering, scatteringFramebuffer->getRenderBuffer(0)); if (DependencyManager::get()->isAmbientOcclusionEnabled()) { batch.setResourceTexture(AmbientOcclusion, framebufferCache->getOcclusionTexture()); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index fc99cae82c..095e0ab9cc 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -34,13 +34,14 @@ signals: class DebugDeferredBuffer { public: + using Inputs = render::VaryingPair; using Config = DebugDeferredBufferConfig; - using JobModel = render::Job::ModelI; + using JobModel = render::Job::ModelI; DebugDeferredBuffer(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputBuffer); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); protected: friend class DebugDeferredBufferConfig; @@ -61,6 +62,8 @@ protected: PyramidDepthMode, CurvatureMode, NormalCurvatureMode, + DiffusedCurvatureMode, + DiffusedNormalCurvatureMode, ScatteringMode, AmbientOcclusionMode, AmbientOcclusionBlurredMode, diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index b4b6a979da..dcdf45e0c9 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -113,20 +113,22 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Opaque all rendered, generate surface geometry buffers const auto curvatureFramebufferAndDepth = addJob("SurfaceGeometry", deferredFrameTransform); - addJob("DiffuseCurvature", curvatureFramebufferAndDepth); - +#define SIMPLE_BLUR 1 +#if SIMPLE_BLUR + const auto curvatureFramebuffer = addJob("DiffuseCurvature", curvatureFramebufferAndDepth.get().first); + const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth.get().first, true); +#else + const auto curvatureFramebuffer = addJob("DiffuseCurvature", curvatureFramebufferAndDepth); const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth, true); - +#endif + // AO job addJob("AmbientOcclusion"); // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - curvatureFramebufferAndDepth.get().first; - - // const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebufferAndDepth[0])); - const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebufferAndDepth.get().first)); + const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer)); const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); // DeferredBuffer is complete, now let's shade it into the LightingBuffer @@ -150,7 +152,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Debugging stages { // Debugging Deferred buffer job - addJob("DebugDeferredBuffer", scatteringFramebuffer); + const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(diffusedCurvatureFramebuffer, scatteringFramebuffer)); + addJob("DebugDeferredBuffer", debugFramebuffers); // Scene Octree Debuging job { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 1dcc9c7196..741c563555 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -18,11 +18,20 @@ #include "subsurfaceScattering_makeLUT_frag.h" #include "subsurfaceScattering_drawScattering_frag.h" -const int SubsurfaceScattering_FrameTransformSlot = 0; -const int SubsurfaceScattering_ParamsSlot = 1; -const int SubsurfaceScattering_CurvatureMapSlot = 0; -const int SubsurfaceScattering_NormalMapSlot = 1; -const int SubsurfaceScattering_ScatteringTableSlot = 2; +enum ScatteringShaderBufferSlots { + ScatteringTask_FrameTransformSlot = 0, + ScatteringTask_ParamSlot, +}; +enum ScatteringShaderMapSlots { + ScatteringTask_ScatteringTableSlot = 0, + ScatteringTask_CurvatureMapSlot, + ScatteringTask_DiffusedCurvatureMapSlot, + ScatteringTask_NormalMapSlot, + + ScatteringTask_AlbedoMapSlot, + ScatteringTask_LinearMapSlot, + +}; SubsurfaceScattering::SubsurfaceScattering() { Parameters parameters; @@ -30,9 +39,19 @@ SubsurfaceScattering::SubsurfaceScattering() { } void SubsurfaceScattering::configure(const Config& config) { - - if (config.depthThreshold != getCurvatureDepthThreshold()) { - _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold; + auto& params = _parametersBuffer.get(); + + glm::vec4 bentInfo(config.bentRed, config.bentGreen, config.bentBlue, config.bentScale); + + if (bentInfo != params.normalBentInfo) { + _parametersBuffer.edit().normalBentInfo = bentInfo; + } + + if (config.curvatureOffset != params.curvatureInfo.x) { + _parametersBuffer.edit().curvatureInfo.x = config.curvatureOffset; + } + if (config.curvatureScale != params.curvatureInfo.y) { + _parametersBuffer.edit().curvatureInfo.y = config.curvatureScale; } _showLUT = config.showLUT; @@ -48,12 +67,17 @@ gpu::PipelinePointer SubsurfaceScattering::getScatteringPipeline() { gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SubsurfaceScattering_FrameTransformSlot)); - // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ScatteringTask_FrameTransformSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringParamsBuffer"), ScatteringTask_ParamSlot)); + + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), ScatteringTask_ScatteringTableSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), ScatteringTask_CurvatureMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("diffusedCurvatureMap"), ScatteringTask_DiffusedCurvatureMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), ScatteringTask_NormalMapSlot)); + + slotBindings.insert(gpu::Shader::Binding(std::string("albedoMap"), ScatteringTask_AlbedoMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), ScatteringTask_LinearMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), SubsurfaceScattering_CurvatureMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), SubsurfaceScattering_NormalMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), SubsurfaceScattering_ScatteringTableSlot)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -112,7 +136,7 @@ bool SubsurfaceScattering::updateScatteringFramebuffer(const gpu::FramebufferPoi if ((_scatteringFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_scatteringFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { _scatteringFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); if (sourceFramebuffer->hasDepthStencil()) { - _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + // _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); } } } @@ -139,8 +163,9 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, auto pipeline = getScatteringPipeline(); - auto& frameTransform = inputs.first. template get();//getFirst(); - auto& curvatureFramebuffer = inputs.second. template get();//getSecond(); + auto& frameTransform = inputs.getFirst(); + auto& curvatureFramebuffer = inputs.getSecond(); + auto& diffusedFramebuffer = inputs.getThird(); auto framebufferCache = DependencyManager::get(); @@ -155,13 +180,21 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setViewportTransform(args->_viewport); batch.setFramebuffer(_scatteringFramebuffer); - - batch.setUniformBuffer(SubsurfaceScattering_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); + // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(vec3(0), 0), false); batch.setPipeline(pipeline); - batch.setResourceTexture(SubsurfaceScattering_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); - batch.setResourceTexture(SubsurfaceScattering_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); - batch.setResourceTexture(SubsurfaceScattering_ScatteringTableSlot, _scatteringTable); + + batch.setUniformBuffer(ScatteringTask_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); + batch.setUniformBuffer(ScatteringTask_ParamSlot, _parametersBuffer); + + batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, _scatteringTable); + batch.setResourceTexture(ScatteringTask_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); + batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); + batch.setResourceTexture(ScatteringTask_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); + batch.setResourceTexture(ScatteringTask_AlbedoMapSlot, framebufferCache->getDeferredColorTexture()); + batch.setResourceTexture(ScatteringTask_LinearMapSlot, framebufferCache->getDepthPyramidTexture()); + + batch.draw(gpu::TRIANGLE_STRIP, 4); if (_showLUT) { diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 15369492a0..818c18bcb9 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -19,12 +19,27 @@ class SubsurfaceScatteringConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) + Q_PROPERTY(float bentRed MEMBER bentRed NOTIFY dirty) + Q_PROPERTY(float bentGreen MEMBER bentGreen NOTIFY dirty) + Q_PROPERTY(float bentBlue MEMBER bentBlue NOTIFY dirty) + Q_PROPERTY(float bentScale MEMBER bentScale NOTIFY dirty) + + Q_PROPERTY(float curvatureOffset MEMBER curvatureOffset NOTIFY dirty) + Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) + + Q_PROPERTY(bool showLUT MEMBER showLUT NOTIFY dirty) public: SubsurfaceScatteringConfig() : render::Job::Config(true) {} - float depthThreshold{ 0.1f }; + float bentRed{ 1.5f }; + float bentGreen{ 0.8f }; + float bentBlue{ 0.3f }; + float bentScale{ 1.0f }; + + float curvatureOffset{ 0.012f }; + float curvatureScale{ 0.25f }; + bool showLUT{ true }; signals: @@ -33,7 +48,7 @@ signals: class SubsurfaceScattering { public: - using Inputs = render::VaryingPair; + using Inputs = render::VaryingTrio; using Config = SubsurfaceScatteringConfig; using JobModel = render::Job::ModelIO; @@ -41,9 +56,6 @@ public: void configure(const Config& config); void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, gpu::FramebufferPointer& scatteringFramebuffer); - - float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } - static gpu::TexturePointer generatePreIntegratedScattering(RenderArgs* args); @@ -53,9 +65,7 @@ private: // Class describing the uniform buffer with all the parameters common to the AO shaders class Parameters { public: - // Resolution info - glm::vec4 resolutionInfo { -1.0f, 0.0f, 0.0f, 0.0f }; - // Curvature algorithm + glm::vec4 normalBentInfo { 0.0f }; glm::vec4 curvatureInfo{ 0.0f }; Parameters() {} diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index a6028f3c95..4b8e8d48ce 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -18,6 +18,7 @@ uniform sampler2D pyramidMap; uniform sampler2D occlusionMap; uniform sampler2D occlusionBlurredMap; uniform sampler2D curvatureMap; +uniform sampler2D diffusedCurvatureMap; uniform sampler2D scatteringMap; in vec2 uv; diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index ae78ab4577..bbe4af7335 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -14,47 +14,49 @@ <$declareDeferredFrameTransform()$> - - -vec2 signNotZero(vec2 v) { - return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); +uniform sampler2D linearDepthMap; +float getZEye(ivec2 pixel) { + return -texelFetch(linearDepthMap, pixel, 0).x; +} +float getZEyeLinear(vec2 texcoord) { + return -texture(linearDepthMap, texcoord).x; } -vec3 oct_to_float32x3(in vec2 e) { - vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); - if (v.z < 0) { - v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); - } - return normalize(v); +<@include DeferredBufferRead.slh@> + + +vec3 fresnelSchlick(vec3 fresnelColor, vec3 lightDir, vec3 halfDir) { + return fresnelColor + (1.0 - fresnelColor) * pow(1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0), 5); } -vec2 unorm8x3_to_snorm12x2(vec3 u) { - u *= 255.0; - u.y *= (1.0 / 16.0); - vec2 s = vec2( u.x * 16.0 + floor(u.y), - fract(u.y) * (16.0 * 256.0) + u.z); - return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); +float specularDistribution(float roughness, vec3 normal, vec3 halfDir) { + float ndoth = clamp(dot(halfDir, normal), 0.0, 1.0); + float gloss2 = pow(0.001 + roughness, 4); + float denom = (ndoth * ndoth*(gloss2 - 1) + 1); + float power = gloss2 / (3.14159 * denom * denom); + return power; } -vec3 unpackNormal(in vec3 p) { - return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); + +// Frag Shading returns the diffuse amount as W and the specular rgb as xyz +vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 fresnel, float roughness) { + // Diffuse Lighting + float diffuse = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir,halfDir); + float power = specularDistribution(roughness, fragNormal, halfDir); + vec3 specular = power * fresnelColor * diffuse; + + return vec4(specular, (1.0 - metallic) * diffuse * (1 - fresnelColor.x)); } + + vec2 sideToFrameTexcoord(vec2 side, vec2 texcoordPos) { return vec2((texcoordPos.x + side.x) * side.y, texcoordPos.y); } -uniform sampler2D normalMap; - -vec3 getRawNormal(vec2 texcoord) { - return texture(normalMap, texcoord).xyz; -} - -vec3 getWorldNormal(vec2 texcoord) { - vec3 rawNormal = getRawNormal(texcoord); - return unpackNormal(rawNormal); -} - - // the curvature texture uniform sampler2D curvatureMap; @@ -62,6 +64,13 @@ vec4 fetchCurvature(vec2 texcoord) { return texture(curvatureMap, texcoord); } +// the curvature texture +uniform sampler2D diffusedCurvatureMap; + +vec4 fetchDiffusedCurvature(vec2 texcoord) { + return texture(diffusedCurvatureMap, texcoord); +} + uniform sampler2D scatteringLUT; @@ -69,13 +78,33 @@ vec3 fetchBRDF(float LdotN, float curvature) { return texture(scatteringLUT, vec2( LdotN * 0.5 + 0.5, curvature)).xyz; } +vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { + return vec3( + fetchBRDF(LdotNSpectrum.r, curvature).r, + fetchBRDF(LdotNSpectrum.g, curvature).g, + fetchBRDF(LdotNSpectrum.b, curvature).b + ); +} + // Scattering parameters -float normalBendFactor = 1.0f; -float normalBendR = 1.5f; -float normalBendG = 0.8f; -float normalBendB = 0.3f; -float scatterBase = 0.012f; -float scatterCurve = 0.25f; + +struct ScatteringParameters { + vec4 normalBendInfo; // R, G, B, factor + vec4 curvatureInfo;// Offset, Scale +}; + +uniform scatteringParamsBuffer { + ScatteringParameters parameters; +}; + +vec3 getBendFactor() { + return parameters.normalBendInfo.xyz * parameters.normalBendInfo.w; +} + +float unpackCurvature(float packedCurvature) { + return abs(packedCurvature * 2 - 1) * 0.5f * parameters.curvatureInfo.y + parameters.curvatureInfo.x; +} + in vec2 varTexCoord0; out vec4 _fragColor; @@ -83,43 +112,69 @@ out vec4 _fragColor; uniform vec3 uniformLightVector = vec3(1.0); void main(void) { - // DeferredTransform deferredTransform = getDeferredTransform(); - // DeferredFragment frag = unpackDeferredFragment(deferredTransform, varTexCoord0); - vec3 normal = getWorldNormal(varTexCoord0); - vec4 diffusedCurvature = fetchCurvature(varTexCoord0); + DeferredFragment fragment = unpackDeferredFragmentNoPosition(varTexCoord0); + + vec3 normal = fragment.normal; // .getWorldNormal(varTexCoord0); + vec4 blurredCurvature = fetchCurvature(varTexCoord0); + vec4 diffusedCurvature = fetchDiffusedCurvature(varTexCoord0); + + // --> Get curvature data + vec3 bentNormalHigh = normalize( (blurredCurvature.xyz - 0.5f) * 2.0f ); + vec3 bentNormalLow = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); + float curvature = unpackCurvature(diffusedCurvature.w); + - // --> Calculate bent normals. - vec3 bentNormalN = normal; - vec3 bentNormalR = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); - float curvature = abs(diffusedCurvature.w * 2 - 1) * 0.5f * scatterCurve + scatterBase; // _fragColor = vec4(vec3(diffusedCurvature.xyz), 1.0); // --> Calculate the light vector. vec3 lightVector = normalize(uniformLightVector); //normalize(lightPos - sourcePos.xyz); - // _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); - - // _fragColor = vec4(vec3(abs(dot(bentNormalR, lightVector))), 1.0); - _fragColor = vec4(vec3(varTexCoord0, 0.0), 1.0); - + // _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); + // _fragColor = vec4(vec3(abs(dot(bentNormalR, lightVector))), 1.0); + // _fragColor = vec4(vec3(varTexCoord0, 0.0), 1.0); // _fragColor = vec4(vec3(bentNormalR * 0.5 + 0.5), 1.0); -/* - // --> Optimise for skin diffusion profile. - float diffuseBlendedR = dot(normalize(mix( bentNormalN.xyz, bentNormalN, normalBendR * normalBendFactor)), lightVector); - float diffuseBlendedG = dot(normalize(mix(normal.xyz, bentNormalN, normalBendG * normalBendFactor)), lightVector); - float diffuseBlendedB = dot(normalize(mix(normal.xyz, bentNormalN, normalBendB * normalBendFactor)), lightVector); - + vec3 rS = bentNormalHigh; + vec3 bendFactorSpectrum = getBendFactor(); + vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); + vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); + vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); + + vec3 NdotLSpectrum = vec3(dot(rN, lightVector), dot(gN, lightVector), dot(bN, lightVector)); + // --> Look up the pre-integrated curvature-dependent BDRF textures - vec3 bdrfR = fetchBRDF(diffuseBlendedR, curvature); - vec3 bdrfG = fetchBRDF(diffuseBlendedG, curvature); - vec3 bdrfB = fetchBRDF(diffuseBlendedB, curvature); - vec3 bdrf = vec3( bdrfR.x, bdrfG.y, bdrfB.z); - bdrf *= bdrf; - _fragColor = vec4(vec3(bdrf.xyz), 1.0);*/ + vec3 bdrf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + + + // Pixel being shaded + ivec2 pixelPos; + vec2 texcoordPos; + ivec4 stereoSide; + ivec2 framePixelPos = getPixelPosTexcoordPosAndSide(gl_FragCoord.xy, pixelPos, texcoordPos, stereoSide); + vec2 stereoSideClip = vec2(stereoSide.x, (isStereo() ? 0.5 : 1.0)); + vec2 frameTexcoordPos = sideToFrameTexcoord(stereoSideClip, texcoordPos); + + // Fetch the z under the pixel (stereo or not) + float Zeye = getZEye(framePixelPos); + + vec3 worldNormal = getWorldNormal(frameTexcoordPos); + + // The position of the pixel fragment in Eye space then in world space + vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); + vec3 fragEyeDir = -(frameTransform._viewInverse * vec4(normalize(eyePos), 0.0)).xyz; + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + if (fragment.metallic > 0.5) { + fresnel = albedo; + fragment.metallic = 1.0; + } + + vec4 shading = evalPBRShading(rS, lightVector, fragEyeDir, fragment.metallic, fresnel, fragment.roughness); + _fragColor = vec4(shading.w * albedo * vec3(bdrf.xyz), 1.0); + + } diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 004ac079c0..3f9a1cf111 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -72,7 +72,76 @@ void BlurParams::setDepthThreshold(float threshold) { } } -BlurGaussian::BlurGaussian() { +BlurInOutResource::BlurInOutResource(bool generateOutputFramebuffer) : +_generateOutputFramebuffer(generateOutputFramebuffer) +{ + +} + +bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFramebuffer, Resources& blurringResources) { + if (!sourceFramebuffer) { + return false; + } + + if (!_blurredFramebuffer) { + _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + + // attach depthStencil if present in source + if (sourceFramebuffer->hasDepthStencil()) { + _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + _blurredFramebuffer->setRenderBuffer(0, blurringTarget); + } else { + // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so + if ((_blurredFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_blurredFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { + _blurredFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); + if (sourceFramebuffer->hasDepthStencil()) { + _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + } + } + + blurringResources.sourceTexture = sourceFramebuffer->getRenderBuffer(0); + blurringResources.blurringFramebuffer = _blurredFramebuffer; + blurringResources.blurringTexture = _blurredFramebuffer->getRenderBuffer(0); + + if (_generateOutputFramebuffer) { + // The job output the blur result in a new Framebuffer spawning here. + // Let s make sure it s ready for this + if (!_outputFramebuffer) { + _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + + // attach depthStencil if present in source + if (sourceFramebuffer->hasDepthStencil()) { + _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + _outputFramebuffer->setRenderBuffer(0, blurringTarget); + } else { + if ((_outputFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_outputFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { + _outputFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); + if (sourceFramebuffer->hasDepthStencil()) { + _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + } + } + } + + // Should be good to use the output Framebuffer as final + blurringResources.finalFramebuffer = _outputFramebuffer; + } else { + // Just the reuse the input as output to blur itself. + blurringResources.finalFramebuffer = sourceFramebuffer; + } + + return true; +} + +BlurGaussian::BlurGaussian(bool generateOutputFramebuffer) : + _inOutResources(generateOutputFramebuffer) +{ _parameters = std::make_shared(); } @@ -120,57 +189,24 @@ gpu::PipelinePointer BlurGaussian::getBlurHPipeline() { return _blurHPipeline; } -bool BlurGaussian::updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources) { - if (!sourceFramebuffer) { - return false; - } - - if (!_blurredFramebuffer) { - _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - - // attach depthStencil if present in source - if (sourceFramebuffer->hasDepthStencil()) { - _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); - _blurredFramebuffer->setRenderBuffer(0, blurringTarget); - } - else { - // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so - if ((_blurredFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_blurredFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { - _blurredFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); - if (sourceFramebuffer->hasDepthStencil()) { - _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - } - } - - blurringResources.sourceTexture = sourceFramebuffer->getRenderBuffer(0); - blurringResources.blurringFramebuffer = _blurredFramebuffer; - blurringResources.blurringTexture = _blurredFramebuffer->getRenderBuffer(0); - blurringResources.finalFramebuffer = sourceFramebuffer; - - return true; -} - void BlurGaussian::configure(const Config& config) { _parameters->setFilterRadiusScale(config.filterScale); } -void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer) { +void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& blurredFramebuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; - BlurringResources blurringResources; - if (!updateBlurringResources(sourceFramebuffer, blurringResources)) { + BlurInOutResource::Resources blurringResources; + if (!_inOutResources.updateResources(sourceFramebuffer, blurringResources)) { // early exit if no valid blurring resources return; } + blurredFramebuffer = blurringResources.finalFramebuffer; auto blurVPipeline = getBlurVPipeline(); auto blurHPipeline = getBlurHPipeline(); @@ -191,6 +227,10 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont batch.draw(gpu::TRIANGLE_STRIP, 4); batch.setFramebuffer(blurringResources.finalFramebuffer); + if (_inOutResources._generateOutputFramebuffer) { + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + } + batch.setPipeline(blurHPipeline); batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.blurringTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); @@ -203,7 +243,7 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont BlurGaussianDepthAware::BlurGaussianDepthAware(bool generateOutputFramebuffer) : - _generateOutputFramebuffer(generateOutputFramebuffer) + _inOutResources(generateOutputFramebuffer) { _parameters = std::make_shared(); } @@ -254,67 +294,6 @@ gpu::PipelinePointer BlurGaussianDepthAware::getBlurHPipeline() { return _blurHPipeline; } -bool BlurGaussianDepthAware::updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources) { - if (!sourceFramebuffer) { - return false; - } - - if (!_blurredFramebuffer) { - _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - - // attach depthStencil if present in source - if (sourceFramebuffer->hasDepthStencil()) { - _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); - _blurredFramebuffer->setRenderBuffer(0, blurringTarget); - } else { - // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so - if ((_blurredFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_blurredFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { - _blurredFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); - if (sourceFramebuffer->hasDepthStencil()) { - _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - } - } - - blurringResources.sourceTexture = sourceFramebuffer->getRenderBuffer(0); - blurringResources.blurringFramebuffer = _blurredFramebuffer; - blurringResources.blurringTexture = _blurredFramebuffer->getRenderBuffer(0); - - if (_generateOutputFramebuffer) { - // The job output the blur result in a new Framebuffer spawning here. - // Let s make sure it s ready for this - if (!_outputFramebuffer) { - _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - - // attach depthStencil if present in source - if (sourceFramebuffer->hasDepthStencil()) { - _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); - _outputFramebuffer->setRenderBuffer(0, blurringTarget); - } else { - if ((_outputFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_outputFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { - _outputFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); - if (sourceFramebuffer->hasDepthStencil()) { - _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - } - } - - // Should be good to use the output Framebuffer as final - blurringResources.finalFramebuffer = _outputFramebuffer; - } else { - // Just the reuse the input as output to blur itself. - blurringResources.finalFramebuffer = sourceFramebuffer; - } - - return true; -} - void BlurGaussianDepthAware::configure(const Config& config) { _parameters->setFilterRadiusScale(config.filterScale); _parameters->setDepthThreshold(config.depthThreshold); @@ -330,8 +309,8 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const auto& sourceFramebuffer = SourceAndDepth.first. template get();//getFirst(); auto& depthTexture = SourceAndDepth.second. template get();//getSecond(); - BlurringResources blurringResources; - if (!updateBlurringResources(sourceFramebuffer, blurringResources)) { + BlurInOutResource::Resources blurringResources; + if (!_inOutResources.updateResources(sourceFramebuffer, blurringResources)) { // early exit if no valid blurring resources return; } diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 899b1ffe12..1f3b1000d7 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -50,13 +50,34 @@ public: }; using BlurParamsPointer = std::shared_ptr; +class BlurInOutResource { +public: + BlurInOutResource(bool generateOutputFramebuffer = false); + + struct Resources { + gpu::TexturePointer sourceTexture; + gpu::FramebufferPointer blurringFramebuffer; + gpu::TexturePointer blurringTexture; + gpu::FramebufferPointer finalFramebuffer; + }; + + bool updateResources(const gpu::FramebufferPointer& sourceFramebuffer, Resources& resources); + + gpu::FramebufferPointer _blurredFramebuffer; + + // the output framebuffer defined if the job needs to output the result in a new framebuffer and not in place in th einput buffer + gpu::FramebufferPointer _outputFramebuffer; + bool _generateOutputFramebuffer{ false }; +}; + + class BlurGaussianConfig : public Job::Config { Q_OBJECT Q_PROPERTY(bool enabled MEMBER enabled NOTIFY dirty) // expose enabled flag Q_PROPERTY(float filterScale MEMBER filterScale NOTIFY dirty) // expose enabled flag public: - float filterScale{ 2.0f }; + float filterScale{ 1.0f }; signals : void dirty(); @@ -67,12 +88,12 @@ protected: class BlurGaussian { public: using Config = BlurGaussianConfig; - using JobModel = Job::ModelI; + using JobModel = Job::ModelIO; - BlurGaussian(); + BlurGaussian(bool generateOutputFramebuffer = false); void configure(const Config& config); - void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer); + void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& blurredFramebuffer); protected: @@ -84,15 +105,7 @@ protected: gpu::PipelinePointer getBlurVPipeline(); gpu::PipelinePointer getBlurHPipeline(); - gpu::FramebufferPointer _blurredFramebuffer; - - struct BlurringResources { - gpu::TexturePointer sourceTexture; - gpu::FramebufferPointer blurringFramebuffer; - gpu::TexturePointer blurringTexture; - gpu::FramebufferPointer finalFramebuffer; - }; - bool updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources); + BlurInOutResource _inOutResources; }; class BlurGaussianDepthAwareConfig : public BlurGaussianConfig { @@ -127,22 +140,10 @@ protected: gpu::PipelinePointer getBlurVPipeline(); gpu::PipelinePointer getBlurHPipeline(); - gpu::FramebufferPointer _blurredFramebuffer; - - // the output framebuffer defined if the job needs to output the result in a new framebuffer and not in place in th einput buffer - gpu::FramebufferPointer _outputFramebuffer; - bool _generateOutputFramebuffer { false }; - - struct BlurringResources { - gpu::TexturePointer sourceTexture; - gpu::FramebufferPointer blurringFramebuffer; - gpu::TexturePointer blurringTexture; - gpu::FramebufferPointer finalFramebuffer; - }; - bool updateBlurringResources(const gpu::FramebufferPointer& sourceFramebuffer, BlurringResources& blurringResources); + BlurInOutResource _inOutResources; }; } -#endif // hifi_render_DrawTask_h +#endif // hifi_render_BlurTask_h diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index a8c96c12b2..2ed4021967 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -111,7 +111,7 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep // If the difference in depth is huge, we lerp color back. - float s = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); + float s = clamp(depthThreshold * distanceToProjectionWindow /* * filterScale*/ * abs(srcDepth - sampleDepth), 0.0, 1.0); srcSample = mix(srcSample, sampleCenter, s); // Accumulate. diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 44cb10aead..28d8d6151b 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -11,6 +11,7 @@ #ifndef hifi_render_Task_h #define hifi_render_Task_h +#include #include @@ -91,19 +92,6 @@ using VaryingPairBase = std::pair; template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var); template <> uint8_t varyingLength(const VaryingPairBase& data); -/* -class VaryingPairBase { - public: - Varying first; - Varying second; - - - // template < class T0, class T1> VaryingPairBase() : Parent(Varying(T0()), Varying(T1())) {} - // VaryingPairBase(const VaryingPairBase& pair) : Parent(pair.first, pair.second) {} - VaryingPairBase(const Varying& _first, const Varying& _second) : first(_first), second(_second) {} - -}; - */ template < class T0, class T1 > class VaryingPair : public VaryingPairBase { public: @@ -120,48 +108,41 @@ public: T1& editSecond() { return second.edit(); } }; - - - /* template Varying varyingGet(const T& data, uint8_t index) { - return Varying(T()); - }*/ - -//template Varying varyingGet(template VaryingPair& data, uint8_t index); -//template <> uint8_t varyingLength(template VaryingPair& data); - - -/* -template < class T0, class T1 > -class VaryingPair : Varying { +template +class VaryingTrio : public std::tuple{ public: - using Parent = Varying; - using Pair = std::pair; - - VaryingPair() : Parent(Pair(Varying(T0()), Varying(T1()))) {} - VaryingPair(const Varying& first, const Varying& second) : Parent(Pair(first, second)) {} - - - Pair& editPair() { return edit(); } - const Pair& getPair() const { return get(); } + using Parent = std::tuple; - const T0& getFirst() const { return getPair().first.template get(); } - T0& editFirst() { return editPair().first.template edit(); } - - const T1& getSecond() const { return getPair().second.template get(); } - T1& editSecond() { return editPair().second.template edit(); } - - // access potential sub varyings contained in this one. - virtual Varying operator[] (uint8_t index) const { - if (index == 0) { - return getPair().first; - } else { - return getPair().second; - } } - virtual uint8_t length() const { return 2; } - + VaryingTrio() : Parent(Varying(T0()), Varying(T1()), Varying(T2())) {} + VaryingTrio(const VaryingTrio& trio) : Parent(std::get<0>(trio), std::get<1>(trio), std::get<2>(trio)) {} + VaryingTrio(const Varying& first, const Varying& second, const Varying& third) : Parent(first, second, third) {} + + const T0& getFirst() const { return std::get<0>((*this)).get(); } + T0& editFirst() { return std::get<0>((*this)).edit(); } + + const T1& getSecond() const { return std::get<1>((*this)).get(); } + T1& editSecond() { return std::get<1>((*this)).edit(); } + + const T2& getThird() const { return std::get<2>((*this)).get(); } + T2& editThird() { return std::get<2>((*this)).edit(); } }; - */ - +/* +template +class VaryingTuple : public std::tuple<_Types>{ +public: + using Parent = std::tuple<_Types>; + + VaryingPair() : Parent(Varying(T0()), Varying(T1())) {} + VaryingPair(const VaryingPair& pair) : Parent(pair.first, pair.second) {} + VaryingPair(const Varying& first, const Varying& second) : Parent(first, second) {} + + const T0& getFirst() const { return first.get(); } + T0& editFirst() { return first.edit(); } + + const T1& getSecond() const { return second.get(); } + T1& editSecond() { return second.edit(); } +};*/ + template < class T, int NUM > class VaryingArray : public std::array { public: diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index 4ed0b7dcf0..9727829880 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -49,6 +49,8 @@ Column { "Pyramid Depth", "Curvature", "NormalCurvature", + "DiffusedCurvature", + "DiffusedNormalCurvature", "Scattering", "Ambient Occlusion", "Ambient Occlusion Blurred", diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index 4ec397addd..608731128b 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -32,13 +32,33 @@ Column { Column{ Repeater { - model: [ "Blur Scale:filterScale:2.0", "Blur Depth Threshold:depthThreshold:100.0" ] + model: [ "Blur Scale:DiffuseCurvature:filterScale:2.0", "Blur Depth Threshold:DiffuseCurvature:depthThreshold:10.0", "Blur Scale2:DiffuseCurvature2:filterScale:2.0", "Blur Depth Threshold 2:DiffuseCurvature2:depthThreshold:10.0"] ConfigSlider { label: qsTr(modelData.split(":")[0]) integral: false - config: Render.getConfig("DiffuseCurvature") - property: modelData.split(":")[1] - max: modelData.split(":")[2] + config: Render.getConfig(modelData.split(":")[1]) + property: modelData.split(":")[2] + max: modelData.split(":")[3] + min: 0.0 + } + } + } + + Column{ + Repeater { + model: [ "Scattering Bent Red:Scattering:bentRed:2.0", + "Scattering Bent Green:Scattering:bentGreen:2.0", + "Scattering Bent Blue:Scattering:bentBlue:2.0", + "Scattering Bent Scale:Scattering:bentScale:2.0", + "Scattering Curvature Offset:Scattering:curvatureOffset:1.0", + "Scattering Curvature Scale:Scattering:curvatureScale:1.0", + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: false + config: Render.getConfig(modelData.split(":")[1]) + property: modelData.split(":")[2] + max: modelData.split(":")[3] min: 0.0 } } From 3b2e0842b04121872976246a34b0d4fb7d7dabfa Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 13 Jun 2016 23:27:08 -0700 Subject: [PATCH 0545/1237] add face camera to text properties --- scripts/system/html/entityProperties.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 2a82d8fa74..51e0914db1 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -452,6 +452,7 @@ var elTextText = document.getElementById("property-text-text"); var elTextLineHeight = document.getElementById("property-text-line-height"); var elTextTextColor = document.getElementById("property-text-text-color"); + var elTextFaceCamera = document.getElementById("property-text-face-camera"); var elTextTextColorRed = document.getElementById("property-text-text-color-red"); var elTextTextColorGreen = document.getElementById("property-text-text-color-green"); var elTextTextColorBlue = document.getElementById("property-text-text-color-blue"); @@ -735,6 +736,7 @@ elTextText.value = properties.text; elTextLineHeight.value = properties.lineHeight.toFixed(4); + elTextFaceCamera = properties.faceCamera; elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + properties.textColor.green + "," + properties.textColor.blue + ")"; elTextTextColorRed.value = properties.textColor.red; elTextTextColorGreen.value = properties.textColor.green; @@ -996,8 +998,8 @@ elModelTextures.addEventListener('change', createEmitTextPropertyUpdateFunction('textures')); elTextText.addEventListener('change', createEmitTextPropertyUpdateFunction('text')); + elTextFaceCamera.addEventListener('change', createEmitCheckedPropertyUpdateFunction('faceCamera')); elTextLineHeight.addEventListener('change', createEmitNumberPropertyUpdateFunction('lineHeight')); - var textTextColorChangeFunction = createEmitColorPropertyUpdateFunction( 'textColor', elTextTextColorRed, elTextTextColorGreen, elTextTextColorBlue); elTextTextColorRed.addEventListener('change', textTextColorChangeFunction); @@ -1715,6 +1717,10 @@
+
+ + +
From f3c47acbb240871a9889606fe1b0f6127dd3f160 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 13 Jun 2016 18:20:55 -0700 Subject: [PATCH 0546/1237] Fix offset when UI transform is not identity --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 4 +++- .../src/display-plugins/hmd/HmdDisplayPlugin.h | 2 ++ plugins/oculus/src/OculusBaseDisplayPlugin.cpp | 2 ++ plugins/openvr/src/OpenVrDisplayPlugin.cpp | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 698e19fcab..6801d94f2f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -415,9 +415,11 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve void HmdDisplayPlugin::compositeExtra() { std::array handLasers; std::array renderHandPoses; + Transform uiModelTransform; withPresentThreadLock([&] { handLasers = _handLasers; renderHandPoses = _handPoses; + uiModelTransform = _uiModelTransform; }); // If neither hand laser is activated, exit @@ -457,7 +459,7 @@ void HmdDisplayPlugin::compositeExtra() { // Find the intersection of the laser with he UI and use it to scale the model matrix float distance; - if (!glm::intersectRaySphere(vec3(renderHandPoses[i][3]), castDirection, vec3(0), uiRadius * uiRadius, distance)) { + if (!glm::intersectRaySphere(vec3(renderHandPoses[i][3]), castDirection, uiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { continue; } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 738028d684..fada15d864 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -10,6 +10,7 @@ #include #include +#include #include "../OpenGLDisplayPlugin.h" @@ -59,6 +60,7 @@ protected: } }; + Transform _uiModelTransform; std::array _handLasers; std::array _handPoses; std::array _eyeOffsets; diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 61c4696f51..e188bea52e 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -9,6 +9,7 @@ #include #include +#include #include "OculusHelpers.h" @@ -40,6 +41,7 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { }); withRenderThreadLock([&] { + _uiModelTransform = DependencyManager::get()->getModelTransform(); _handPoses = handPoses; _frameInfos[frameIndex] = _currentRenderFrameInfo; }); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index d57dddf512..3a5f027013 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include "OpenVrHelpers.h" @@ -198,6 +199,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } withRenderThreadLock([&] { + _uiModelTransform = DependencyManager::get()->getModelTransform(); // Make controller poses available to the presentation thread _handPoses = handPoses; _frameInfos[frameIndex] = _currentRenderFrameInfo; From 34c8d257d2ac2638b3d8ea6232a02bad8222fdea Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 13 Jun 2016 15:26:48 -0700 Subject: [PATCH 0547/1237] Fixing issues with unclosed groups in settings persistence --- libraries/networking/src/AccountManager.cpp | 1 + libraries/script-engine/src/ScriptEngines.cpp | 1 + libraries/shared/src/SettingHandle.cpp | 9 +++++++++ libraries/shared/src/SettingHandle.h | 2 ++ libraries/shared/src/SettingInterface.cpp | 2 ++ 5 files changed, 15 insertions(+) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index bac031885f..26b3801ec1 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -173,6 +173,7 @@ void AccountManager::setAuthURL(const QUrl& authURL) { << "from previous settings file"; } } + settings.endGroup(); if (_accountInfo.getAccessToken().token.isEmpty()) { qCWarning(networking) << "Unable to load account file. No existing account settings will be loaded."; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 29c223f4b3..beddc21787 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -334,6 +334,7 @@ void ScriptEngines::clearScripts() { Settings settings; settings.beginWriteArray(SETTINGS_KEY); settings.remove(""); + settings.endArray(); } void ScriptEngines::saveScripts() { diff --git a/libraries/shared/src/SettingHandle.cpp b/libraries/shared/src/SettingHandle.cpp index b2f23f5a04..13f9ea48ce 100644 --- a/libraries/shared/src/SettingHandle.cpp +++ b/libraries/shared/src/SettingHandle.cpp @@ -18,6 +18,7 @@ const QString Settings::firstRun { "firstRun" }; + Settings::Settings() : _manager(DependencyManager::get()), _locker(&(_manager->getLock())) @@ -25,6 +26,9 @@ Settings::Settings() : } Settings::~Settings() { + if (_prefixes.size() != 0) { + qFatal("Unstable Settings Prefixes: You must call endGroup for every beginGroup and endArray for every begin*Array call"); + } } void Settings::remove(const QString& key) { @@ -50,14 +54,17 @@ bool Settings::contains(const QString& key) const { } int Settings::beginReadArray(const QString & prefix) { + _prefixes.push(prefix); return _manager->beginReadArray(prefix); } void Settings::beginWriteArray(const QString& prefix, int size) { + _prefixes.push(prefix); _manager->beginWriteArray(prefix, size); } void Settings::endArray() { + _prefixes.pop(); _manager->endArray(); } @@ -66,10 +73,12 @@ void Settings::setArrayIndex(int i) { } void Settings::beginGroup(const QString& prefix) { + _prefixes.push(prefix); _manager->beginGroup(prefix); } void Settings::endGroup() { + _prefixes.pop(); _manager->endGroup(); } diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index e83c563036..f19fc5875b 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -58,8 +58,10 @@ public: void setQuatValue(const QString& name, const glm::quat& quatValue); void getQuatValueIfValid(const QString& name, glm::quat& quatValue); +private: QSharedPointer _manager; QWriteLocker _locker; + QStack _prefixes; }; namespace Setting { diff --git a/libraries/shared/src/SettingInterface.cpp b/libraries/shared/src/SettingInterface.cpp index 630d52bf7d..1ebaa5cf82 100644 --- a/libraries/shared/src/SettingInterface.cpp +++ b/libraries/shared/src/SettingInterface.cpp @@ -98,6 +98,8 @@ namespace Setting { // Register Handle manager->registerHandle(this); _isInitialized = true; + } else { + qWarning() << "Settings interface used after manager destroyed"; } // Load value from disk From d52a1fe6f925e448b67a4b997690514389f5ca28 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 13 Jun 2016 20:26:27 -0700 Subject: [PATCH 0548/1237] Fix persistence for global scope settings handles, clean up invalid variants in settings --- libraries/shared/src/SettingHandle.h | 39 +++++++++++++++++++---- libraries/shared/src/SettingInterface.cpp | 4 +-- libraries/shared/src/SettingInterface.h | 11 ++++--- libraries/shared/src/SettingManager.cpp | 10 +++--- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index f19fc5875b..74587dcdcb 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -77,15 +77,40 @@ namespace Setting { virtual ~Handle() { deinit(); } // Returns setting value, returns its default value if not found - T get() { return get(_defaultValue); } + T get() const { + return get(_defaultValue); + } + // Returns setting value, returns other if not found - T get(const T& other) { maybeInit(); return (_isSet) ? _value : other; } - T getDefault() const { return _defaultValue; } + T get(const T& other) const { + maybeInit(); + return (_isSet) ? _value : other; + } + + const T& getDefault() const { + return _defaultValue; + } - void set(const T& value) { maybeInit(); _value = value; _isSet = true; } - void reset() { set(_defaultValue); } - - void remove() { maybeInit(); _isSet = false; } + void reset() { + set(_defaultValue); + } + + void set(const T& value) { + maybeInit(); + if (_value != value) { + _value = value; + _isSet = true; + save(); + } + } + + void remove() { + maybeInit(); + if (_isSet) { + _isSet = false; + save(); + } + } protected: virtual void setVariant(const QVariant& variant); diff --git a/libraries/shared/src/SettingInterface.cpp b/libraries/shared/src/SettingInterface.cpp index 1ebaa5cf82..95c6bc1efc 100644 --- a/libraries/shared/src/SettingInterface.cpp +++ b/libraries/shared/src/SettingInterface.cpp @@ -119,9 +119,9 @@ namespace Setting { } - void Interface::maybeInit() { + void Interface::maybeInit() const { if (!_isInitialized) { - init(); + const_cast(this)->init(); } } diff --git a/libraries/shared/src/SettingInterface.h b/libraries/shared/src/SettingInterface.h index 2b32e8a3b4..5e23d42223 100644 --- a/libraries/shared/src/SettingInterface.h +++ b/libraries/shared/src/SettingInterface.h @@ -39,19 +39,20 @@ namespace Setting { virtual ~Interface() = default; void init(); - void maybeInit(); + void maybeInit() const; void deinit(); void save(); void load(); - - bool _isInitialized = false; + bool _isSet = false; const QString _key; + + private: + mutable bool _isInitialized = false; friend class Manager; - - QWeakPointer _manager; + mutable QWeakPointer _manager; }; } diff --git a/libraries/shared/src/SettingManager.cpp b/libraries/shared/src/SettingManager.cpp index bacec2ee0c..ed8565d6c3 100644 --- a/libraries/shared/src/SettingManager.cpp +++ b/libraries/shared/src/SettingManager.cpp @@ -33,8 +33,8 @@ namespace Setting { void Manager::customDeleter() { } - void Manager::registerHandle(Setting::Interface* handle) { - QString key = handle->getKey(); + void Manager::registerHandle(Interface* handle) { + const QString& key = handle->getKey(); withWriteLock([&] { if (_handles.contains(key)) { qWarning() << "Setting::Manager::registerHandle(): Key registered more than once, overriding: " << key; @@ -58,7 +58,9 @@ namespace Setting { } else { loadedValue = value(key); } - handle->setVariant(loadedValue); + if (loadedValue.isValid()) { + handle->setVariant(loadedValue); + } }); } @@ -101,7 +103,7 @@ namespace Setting { if (newValue == savedValue) { continue; } - if (newValue == UNSET_VALUE) { + if (newValue == UNSET_VALUE || !newValue.isValid()) { remove(key); } else { setValue(key, newValue); From 8858f9dc82ce2dd7a86c310336b81cc329d829e3 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 14 Jun 2016 09:54:51 -0700 Subject: [PATCH 0549/1237] IUse the light stage finally for the scattering prototype --- .../render-utils/src/DeferredLightingEffect.h | 2 +- libraries/render-utils/src/LightStage.cpp | 5 +++-- libraries/render-utils/src/LightStage.h | 2 ++ .../render-utils/src/SubsurfaceScattering.cpp | 11 ++++++++++- .../src/subsurfaceScattering_drawScattering.slf | 16 ++++++++++------ libraries/render/src/render/BlurTask.cpp | 4 ++-- libraries/render/src/render/Task.h | 12 ++++++------ 7 files changed, 34 insertions(+), 18 deletions(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 63d8f4d175..6428cc5742 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -44,7 +44,7 @@ public: const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI); void prepare(RenderArgs* args); - void render(const render::RenderContextPointer& renderContext); + void render(const render::RenderContextPointer& renderContext, ); void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int skyboxCubemapUnit); diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index fc6c3ff514..f1726feca6 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -88,8 +88,9 @@ const glm::mat4& LightStage::Shadow::getProjection() const { } const LightStage::LightPointer LightStage::addLight(model::LightPointer light) { - Shadow stageShadow{light}; - LightPointer stageLight = std::make_shared(std::move(stageShadow)); + // Shadow stageShadow{light}; + LightPointer stageLight = std::make_shared(Shadow(light)); + stageLight->light = light; lights.push_back(stageLight); return stageLight; } diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index 9ed9789965..76d9a3b268 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -52,6 +52,8 @@ public: glm::float32 scale = 1 / MAP_SIZE; }; UniformBufferView _schemaBuffer = nullptr; + + friend class Light; }; using ShadowPointer = std::shared_ptr; diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 741c563555..5c728b7307 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -15,12 +15,15 @@ #include "FramebufferCache.h" +#include "DeferredLightingEffect.h" + #include "subsurfaceScattering_makeLUT_frag.h" #include "subsurfaceScattering_drawScattering_frag.h" enum ScatteringShaderBufferSlots { ScatteringTask_FrameTransformSlot = 0, ScatteringTask_ParamSlot, + ScatteringTask_LightSlot, }; enum ScatteringShaderMapSlots { ScatteringTask_ScatteringTableSlot = 0, @@ -30,6 +33,8 @@ enum ScatteringShaderMapSlots { ScatteringTask_AlbedoMapSlot, ScatteringTask_LinearMapSlot, + + SCatteringTask_IBLMapSlot, }; @@ -69,6 +74,7 @@ gpu::PipelinePointer SubsurfaceScattering::getScatteringPipeline() { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ScatteringTask_FrameTransformSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("scatteringParamsBuffer"), ScatteringTask_ParamSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), ScatteringTask_LightSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), ScatteringTask_ScatteringTableSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), ScatteringTask_CurvatureMapSlot)); @@ -174,6 +180,8 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, return; } + const auto theLight = DependencyManager::get()->getLightStage().lights[0]; + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); @@ -186,7 +194,8 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setUniformBuffer(ScatteringTask_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); batch.setUniformBuffer(ScatteringTask_ParamSlot, _parametersBuffer); - + if (theLight->light) + batch.setUniformBuffer(ScatteringTask_LightSlot, theLight->light->getSchemaBuffer()); batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, _scatteringTable); batch.setResourceTexture(ScatteringTask_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index bbe4af7335..eac4da0709 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -14,6 +14,8 @@ <$declareDeferredFrameTransform()$> +<@include model/Light.slh@> + uniform sampler2D linearDepthMap; float getZEye(ivec2 pixel) { return -texelFetch(linearDepthMap, pixel, 0).x; @@ -109,7 +111,7 @@ float unpackCurvature(float packedCurvature) { in vec2 varTexCoord0; out vec4 _fragColor; -uniform vec3 uniformLightVector = vec3(1.0); +//uniform vec3 uniformLightVector = vec3(1.0); void main(void) { @@ -128,7 +130,9 @@ void main(void) { // _fragColor = vec4(vec3(diffusedCurvature.xyz), 1.0); // --> Calculate the light vector. - vec3 lightVector = normalize(uniformLightVector); //normalize(lightPos - sourcePos.xyz); + + Light light = getLight(); + vec3 lightVector = -getLightDirection(light); //normalize(uniformLightVector); //normalize(lightPos - sourcePos.xyz); // _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); // _fragColor = vec4(vec3(abs(dot(bentNormalR, lightVector))), 1.0); @@ -160,19 +164,19 @@ void main(void) { // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); - vec3 worldNormal = getWorldNormal(frameTexcoordPos); + vec3 worldNormal = fragment.normal; // The position of the pixel fragment in Eye space then in world space vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); vec3 fragEyeDir = -(frameTransform._viewInverse * vec4(normalize(eyePos), 0.0)).xyz; vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value if (fragment.metallic > 0.5) { - fresnel = albedo; + fresnel = fragment.diffuse; fragment.metallic = 1.0; } - vec4 shading = evalPBRShading(rS, lightVector, fragEyeDir, fragment.metallic, fresnel, fragment.roughness); - _fragColor = vec4(shading.w * albedo * vec3(bdrf.xyz), 1.0); + //vec4 shading = evalPBRShading(rS, lightVector, fragEyeDir, fragment.metallic, fresnel, fragment.roughness); + _fragColor = vec4(fragment.diffuse * vec3(bdrf.xyz), 1.0); } diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 3f9a1cf111..53129f16ba 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -306,8 +306,8 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const RenderArgs* args = renderContext->args; - auto& sourceFramebuffer = SourceAndDepth.first. template get();//getFirst(); - auto& depthTexture = SourceAndDepth.second. template get();//getSecond(); + auto& sourceFramebuffer = SourceAndDepth.getFirst(); + auto& depthTexture = SourceAndDepth.getSecond(); BlurInOutResource::Resources blurringResources; if (!_inOutResources.updateResources(sourceFramebuffer, blurringResources)) { diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 28d8d6151b..5a67e68f20 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -117,14 +117,14 @@ public: VaryingTrio(const VaryingTrio& trio) : Parent(std::get<0>(trio), std::get<1>(trio), std::get<2>(trio)) {} VaryingTrio(const Varying& first, const Varying& second, const Varying& third) : Parent(first, second, third) {} - const T0& getFirst() const { return std::get<0>((*this)).get(); } - T0& editFirst() { return std::get<0>((*this)).edit(); } + const T0& getFirst() const { return std::get<0>((*this)).template get(); } + T0& editFirst() { return std::get<0>((*this)).template edit(); } - const T1& getSecond() const { return std::get<1>((*this)).get(); } - T1& editSecond() { return std::get<1>((*this)).edit(); } + const T1& getSecond() const { return std::get<1>((*this)).template get(); } + T1& editSecond() { return std::get<1>((*this)).template edit(); } - const T2& getThird() const { return std::get<2>((*this)).get(); } - T2& editThird() { return std::get<2>((*this)).edit(); } + const T2& getThird() const { return std::get<2>((*this)).template get(); } + T2& editThird() { return std::get<2>((*this)).template edit(); } }; /* template From 2163f9b5f34241d631324fce349c2e058cf54cfd Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 14 Jun 2016 10:43:45 -0700 Subject: [PATCH 0550/1237] signal once for domain protocol mismatch, case 918 --- libraries/networking/src/DomainHandler.cpp | 23 +++++++++++++++++++--- libraries/networking/src/DomainHandler.h | 3 ++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 6880b7a329..057dd3747d 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -118,6 +118,8 @@ void DomainHandler::hardReset() { _hostname = QString(); _sockAddr.clear(); + _hasSignalledProtocolMismatch = false; + _hasCheckedForAccessToken = false; // clear any pending path we may have wanted to ask the previous DS about @@ -405,9 +407,24 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 1328174e87..3ab583d597 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -144,7 +144,8 @@ private: QString _pendingPath; QTimer _settingsTimer; - QStringList _domainConnectionRefusals; + QList _domainConnectionRefusals; + bool _hasSignalledProtocolMismatch { false }; bool _hasCheckedForAccessToken { false }; int _connectionDenialsSinceKeypairRegen { 0 }; From 5759c2d29d1c058b8190ee0fff6b5662939fbc73 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 14 Jun 2016 11:28:44 -0700 Subject: [PATCH 0551/1237] final --- scripts/system/controllers/handControllerPointer.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index f5c0c4bb7c..7046ed16a5 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -364,13 +364,12 @@ Script.scriptEnding.connect(function () { overlays.forEach(Overlays.deleteOverlay); }); var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. -var SYSTEM_LASER_DIRECTION = Vec3.normalize({x: 0, y: -1, z: -1}); // Guessing 45 degrees. +var SYSTEM_LASER_DIRECTION = {x: 0, y: 0, z: -1}; var systemLaserOn = false; function clearSystemLaser() { if (!systemLaserOn) { return; } - print('FIXME remove: disableHandLasers', BOTH_HUD_LASERS); HMD.disableHandLasers(BOTH_HUD_LASERS); systemLaserOn = false; } @@ -379,13 +378,9 @@ function turnOffVisualization(optionalEnableClicks) { // because we're showing c expireMouseCursor(); clearSystemLaser(); } else if (!systemLaserOn) { - print('FIXME remove: setHandLasers', activeHudLaser, true, JSON.stringify(LASER_COLOR_XYZW), JSON.stringify(SYSTEM_LASER_DIRECTION)); // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. - /*Reticle.visible = !*/HMD.setHandLasers(activeHudLaser, true, LASER_COLOR_XYZW, SYSTEM_LASER_DIRECTION); - Reticle.visible = true; // FIXME: just for now, while hand lasers has the bug that requires this. - systemLaserOn = true; - } else { - Reticle.visible = true; + systemLaserOn = HMD.setHandLasers(activeHudLaser, true, LASER_COLOR_XYZW, SYSTEM_LASER_DIRECTION); + Reticle.visible = !systemLaserOn; } if (!visualizationIsShowing) { return; From b1c472e82b8cccc4ff3a1d500b229b74858ba016 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Tue, 14 Jun 2016 12:13:27 -0700 Subject: [PATCH 0552/1237] Fixing settings, even in release builds --- libraries/shared/src/SettingHandle.h | 2 +- libraries/shared/src/SettingManager.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index 74587dcdcb..8e07d28dad 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -97,7 +97,7 @@ namespace Setting { void set(const T& value) { maybeInit(); - if (_value != value) { + if ((!_isSet && (value != _defaultValue)) || _value != value) { _value = value; _isSet = true; save(); diff --git a/libraries/shared/src/SettingManager.cpp b/libraries/shared/src/SettingManager.cpp index ed8565d6c3..abb8525b03 100644 --- a/libraries/shared/src/SettingManager.cpp +++ b/libraries/shared/src/SettingManager.cpp @@ -96,6 +96,7 @@ namespace Setting { } void Manager::saveAll() { + bool forceSync = false; withWriteLock([&] { for (auto key : _pendingChanges.keys()) { auto newValue = _pendingChanges[key]; @@ -104,14 +105,20 @@ namespace Setting { continue; } if (newValue == UNSET_VALUE || !newValue.isValid()) { + forceSync = true; remove(key); } else { + forceSync = true; setValue(key, newValue); } } _pendingChanges.clear(); }); + if (forceSync) { + sync(); + } + // Restart timer if (_saveTimer) { _saveTimer->start(); From b97dc584ea59c8c69fb5e4d76b2d286488bbf4cb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 14 Jun 2016 14:14:31 -0700 Subject: [PATCH 0553/1237] fix bug where buttons would show when minimized --- scripts/system/users.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 68d82040e9..5b0ba42a45 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -570,7 +570,10 @@ var usersWindow = (function() { visibilityControl.setVisible(false); displayControl.setVisible(false); } else { - + if (isMinimized === true) { + loggedIn = true; + return + } Overlays.editOverlay(friendsButton, { visible: true }); @@ -578,7 +581,6 @@ var usersWindow = (function() { displayControl.setVisible(true); loggedIn = true; - } } @@ -660,11 +662,15 @@ var usersWindow = (function() { Overlays.editOverlay(scrollbarBar, { visible: isVisible && isUsingScrollbars && !isMinimized }); - Overlays.editOverlay(friendsButton, { - visible: isVisible && !isMinimized - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); + + if (loggedIn === true) { + Overlays.editOverlay(friendsButton, { + visible: isVisible && !isMinimized + }); + displayControl.setVisible(isVisible && !isMinimized); + visibilityControl.setVisible(isVisible && !isMinimized); + } + } function setVisible(visible) { @@ -758,7 +764,6 @@ var usersWindow = (function() { userClicked = firstUserToDisplay + lineClicked; if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { - //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } From 67aac09033d0e713adb1e41b4d5bd8dca2c607b2 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 14 Jun 2016 14:59:29 -0700 Subject: [PATCH 0554/1237] Set state debug flag to false --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 6daafa425f..6e39b16f82 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -19,7 +19,7 @@ Script.include("/~/system/libraries/utils.js"); // add lines where the hand ray picking is happening // var WANT_DEBUG = false; -var WANT_DEBUG_STATE = true; +var WANT_DEBUG_STATE = false; var WANT_DEBUG_SEARCH_NAME = null; // From 3c2b98b15a4cdd9819bf9db7103938aa88930589 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 15 Jun 2016 11:28:38 +1200 Subject: [PATCH 0555/1237] Let user choose an existing file in the file save dialog --- interface/resources/qml/dialogs/FileDialog.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 56f761e42d..f57d20de51 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -614,12 +614,6 @@ ModalWindow { readOnly: !root.saveDialog activeFocusOnTab: !readOnly onActiveFocusChanged: if (activeFocus) { selectAll(); } - onTextChanged: { - if (root.saveDialog && text !== "") { - fileTableView.selection.clear(); - fileTableView.currentRow = -1; - } - } onAccepted: okAction.trigger(); } From 96258a795a0e8cd91e552afccf4bd3945dc2cb19 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 8 Jun 2016 17:32:58 -0700 Subject: [PATCH 0556/1237] Initial Steamworks integration --- cmake/macros/TargetSteamworks.cmake | 12 +++++ cmake/modules/FindSteamworks.cmake | 45 +++++++++++++++++++ interface/CMakeLists.txt | 2 +- libraries/steamworks-wrapper/CMakeLists.txt | 5 +++ .../src/steamworks-wrapper/SteamClient.cpp | 19 ++++++++ .../src/steamworks-wrapper/SteamClient.h | 21 +++++++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 cmake/macros/TargetSteamworks.cmake create mode 100644 cmake/modules/FindSteamworks.cmake create mode 100644 libraries/steamworks-wrapper/CMakeLists.txt create mode 100644 libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp create mode 100644 libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h diff --git a/cmake/macros/TargetSteamworks.cmake b/cmake/macros/TargetSteamworks.cmake new file mode 100644 index 0000000000..24752ebd94 --- /dev/null +++ b/cmake/macros/TargetSteamworks.cmake @@ -0,0 +1,12 @@ +# +# Copyright 2015 High Fidelity, Inc. +# Created by Clement Brisset on 6/8/2016 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_STEAMWORKS) + find_package(Steamworks REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${STEAMWORKS_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${STEAMWORKS_LIBRARIES}) +endmacro() diff --git a/cmake/modules/FindSteamworks.cmake b/cmake/modules/FindSteamworks.cmake new file mode 100644 index 0000000000..a8f00ed335 --- /dev/null +++ b/cmake/modules/FindSteamworks.cmake @@ -0,0 +1,45 @@ +# +# FindSteamworks.cmake +# +# Try to find the Steamworks controller library +# +# This module defines the following variables +# +# STEAMWORKS_FOUND - Was Steamworks found +# STEAMWORKS_INCLUDE_DIRS - the Steamworks include directory +# STEAMWORKS_LIBRARIES - Link this to use Steamworks +# +# This module accepts the following variables +# +# STEAMWORKS_ROOT - Can be set to steamworks install path or Windows build path +# +# Created on 6/8/2016 by Clement Brisset +# Copyright 2016 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +set(STEAMWORKS_POSSIBLE_PATHS "/Users/clement/Downloads/steamworks_sdk_137/") + +find_path(STEAMWORKS_INCLUDE_DIRS + NAMES steam_api.h + PATH_SUFFIXES "public/steam" + PATHS ${STEAMWORKS_POSSIBLE_PATHS} +) + +get_filename_component(STEAMWORKS_INCLUDE_DIR_NAME ${STEAMWORKS_INCLUDE_DIRS} NAME) +if (STEAMWORKS_INCLUDE_DIR_NAME STREQUAL "steam") + get_filename_component(STEAMWORKS_INCLUDE_DIRS ${STEAMWORKS_INCLUDE_DIRS} PATH) +else() + message(STATUS "Include directory not named steam, this will cause issues with include statements") +endif() + +find_library(STEAMWORKS_LIBRARIES + NAMES libsteam_api.dylib + PATH_SUFFIXES "redistributable_bin/osx32" + PATHS ${STEAMWORKS_POSSIBLE_PATHS} +) + + +find_package_handle_standard_args(Steamworks DEFAULT_MSG STEAMWORKS_LIBRARIES STEAMWORKS_INCLUDE_DIRS) \ No newline at end of file diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 4381f3321b..ae84705da3 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -139,7 +139,7 @@ link_hifi_libraries(shared octree gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater - controllers plugins display-plugins input-plugins) + controllers plugins display-plugins input-plugins steamworks-wrapper) # include the binary directory of render-utils for shader includes target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils") diff --git a/libraries/steamworks-wrapper/CMakeLists.txt b/libraries/steamworks-wrapper/CMakeLists.txt new file mode 100644 index 0000000000..0cbe3bb5ad --- /dev/null +++ b/libraries/steamworks-wrapper/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET_NAME steamworks-wrapper) +setup_hifi_library() +link_hifi_libraries() + +target_steamworks() \ No newline at end of file diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp new file mode 100644 index 0000000000..0f06e03672 --- /dev/null +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -0,0 +1,19 @@ +// +// SteamClient.cpp +// steamworks-wrapper/src/steamworks-wrapper +// +// Created by Clement Brisset on 6/8/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "SteamClient.h" + +#include + + + + + diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h new file mode 100644 index 0000000000..369641b0c7 --- /dev/null +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -0,0 +1,21 @@ +// +// SteamClient.h +// steamworks-wrapper/src/steamworks-wrapper +// +// Created by Clement Brisset on 6/8/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_SteamClient_h +#define hifi_SteamClient_h + +class SteamClient { + + +}; + +#endif // hifi_SteamClient_h \ No newline at end of file From fc7b271e50bac7f44a5089c4e1e7f5eb66ae8a19 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 8 Jun 2016 17:33:17 -0700 Subject: [PATCH 0557/1237] Missed OS X Warning --- libraries/entities/src/EntityTreeElement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index a7baeb361b..0523933ee6 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -189,7 +189,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con // encoud our parent... this might happen. if (extraEncodeData->contains(childElement.get())) { EntityTreeElementExtraEncodeData* childExtraEncodeData - = static_cast(extraEncodeData->value(childElement.get())); + = static_cast((*extraEncodeData)[childElement.get()]); if (wantDebug) { qCDebug(entities) << "checking child: " << childElement->_cube; From 1a8d6ea614b401b8bdb21946571ffc7762712b97 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 9 Jun 2016 15:06:59 -0700 Subject: [PATCH 0558/1237] Make steamworks an external dependency --- cmake/externals/steamworks/CMakeLists.txt | 62 +++++++++++++++++++++++ cmake/macros/TargetSteamworks.cmake | 1 + cmake/modules/FindSteamworks.cmake | 32 +++--------- 3 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 cmake/externals/steamworks/CMakeLists.txt diff --git a/cmake/externals/steamworks/CMakeLists.txt b/cmake/externals/steamworks/CMakeLists.txt new file mode 100644 index 0000000000..b4c480f050 --- /dev/null +++ b/cmake/externals/steamworks/CMakeLists.txt @@ -0,0 +1,62 @@ +include(ExternalProject) + +set(EXTERNAL_NAME steamworks) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +set(STEAMWORKS_URL "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/Steamworks.zip") +set(STEAMWORKS_URL_MD5 "95ba9d0e3ddc04f8a8be17d2da806cbb") + +ExternalProject_Add( + ${EXTERNAL_NAME} + URL ${STEAMWORKS_URL} + URL_MD5 ${STEAMWORKS_URL_MD5} + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 +) + +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) + +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/public CACHE TYPE INTERNAL) + +if (WIN32) + + if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + set(ARCH_DIR ${SOURCE_DIR}/redistributable_bin/win64) + set(ARCH_SUFFIX "64") + else() + set(ARCH_DIR ${SOURCE_DIR}/redistributable_bin) + set(ARCH_SUFFIX "") + endif() + + set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${ARCH_DIR}) + set(${EXTERNAL_NAME_UPPER}_LIB_PATH ${ARCH_DIR}) + + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE "${${EXTERNAL_NAME_UPPER}_LIB_PATH}/steam_api${ARCH_SUFFIX}.lib" CACHE TYPE INTERNAL) + add_paths_to_fixup_libs("${${EXTERNAL_NAME_UPPER}_DLL_PATH}") + +elseif(APPLE) + + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/redistributable_bin/osx32/libsteam_api.dylib CACHE TYPE INTERNAL) + + set(_STEAMWORKS_LIB_DIR "${SOURCE_DIR}/redistributable_bin/osx32") + ExternalProject_Add_Step( + ${EXTERNAL_NAME} + change-install-name + COMMENT "Calling install_name_tool on libraries to fix install name for dylib linking" + COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${_STEAMWORKS_LIB_DIR} -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake + DEPENDEES install + WORKING_DIRECTORY + LOG 1 + ) + +elseif(NOT ANDROID) + + # FIXME need to account for different architectures + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/redistributable_bin/linux64/libsteam_api.so CACHE TYPE INTERNAL) + +endif() diff --git a/cmake/macros/TargetSteamworks.cmake b/cmake/macros/TargetSteamworks.cmake index 24752ebd94..67145050c2 100644 --- a/cmake/macros/TargetSteamworks.cmake +++ b/cmake/macros/TargetSteamworks.cmake @@ -6,6 +6,7 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # macro(TARGET_STEAMWORKS) + add_dependency_external_projects(steamworks) find_package(Steamworks REQUIRED) target_include_directories(${TARGET_NAME} PRIVATE ${STEAMWORKS_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${STEAMWORKS_LIBRARIES}) diff --git a/cmake/modules/FindSteamworks.cmake b/cmake/modules/FindSteamworks.cmake index a8f00ed335..515a9d4374 100644 --- a/cmake/modules/FindSteamworks.cmake +++ b/cmake/modules/FindSteamworks.cmake @@ -1,6 +1,6 @@ -# +# # FindSteamworks.cmake -# +# # Try to find the Steamworks controller library # # This module defines the following variables @@ -20,26 +20,10 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -set(STEAMWORKS_POSSIBLE_PATHS "/Users/clement/Downloads/steamworks_sdk_137/") +include(SelectLibraryConfigurations) +select_library_configurations(STEAMWORKS) -find_path(STEAMWORKS_INCLUDE_DIRS - NAMES steam_api.h - PATH_SUFFIXES "public/steam" - PATHS ${STEAMWORKS_POSSIBLE_PATHS} -) - -get_filename_component(STEAMWORKS_INCLUDE_DIR_NAME ${STEAMWORKS_INCLUDE_DIRS} NAME) -if (STEAMWORKS_INCLUDE_DIR_NAME STREQUAL "steam") - get_filename_component(STEAMWORKS_INCLUDE_DIRS ${STEAMWORKS_INCLUDE_DIRS} PATH) -else() - message(STATUS "Include directory not named steam, this will cause issues with include statements") -endif() - -find_library(STEAMWORKS_LIBRARIES - NAMES libsteam_api.dylib - PATH_SUFFIXES "redistributable_bin/osx32" - PATHS ${STEAMWORKS_POSSIBLE_PATHS} -) - - -find_package_handle_standard_args(Steamworks DEFAULT_MSG STEAMWORKS_LIBRARIES STEAMWORKS_INCLUDE_DIRS) \ No newline at end of file +set(STEAMWORKS_REQUIREMENTS STEAMWORKS_INCLUDE_DIRS STEAMWORKS_LIBRARIES) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Steamworks DEFAULT_MSG STEAMWORKS_INCLUDE_DIRS STEAMWORKS_LIBRARIES) +mark_as_advanced(STEAMWORKS_LIBRARIES STEAMWORKS_INCLUDE_DIRS STEAMWORKS_SEARCH_DIRS) From 5560ba9a4c938f4e41664f54d782344910741053 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 10 Jun 2016 16:17:17 -0700 Subject: [PATCH 0559/1237] Quit application when SteamVR requests it --- interface/src/Application.cpp | 7 ------- libraries/plugins/src/plugins/DisplayPlugin.h | 4 ---- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 5 +---- plugins/openvr/src/OpenVrHelpers.cpp | 10 ++-------- plugins/openvr/src/ViveControllerManager.cpp | 4 ---- 5 files changed, 3 insertions(+), 27 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 49c2e17d84..60c9de1223 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5144,13 +5144,6 @@ void Application::updateDisplayMode() { QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, [this](const QSize & size) { resizeGL(); }); - QObject::connect(displayPlugin.get(), &DisplayPlugin::outputDeviceLost, [this, displayPluginName] { - PluginManager::getInstance()->disableDisplayPlugin(displayPluginName); - auto menu = Menu::getInstance(); - if (menu->menuItemExists(MenuOption::OutputMenu, displayPluginName)) { - menu->removeMenuItem(MenuOption::OutputMenu, displayPluginName); - } - }); first = false; } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 1f6a16cd46..19f803e11e 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -170,10 +170,6 @@ public: signals: void recommendedFramebufferSizeChanged(const QSize & size); - // Indicates that this display plugin is no longer valid for use. - // For instance if a user exits Oculus Home or Steam VR while - // using the corresponding plugin, that plugin should be disabled. - void outputDeviceLost(); protected: void incrementPresentCount(); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index fe406cc29a..3e7e5abbf3 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -128,10 +128,7 @@ void OpenVrDisplayPlugin::resetSensors() { bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { handleOpenVrEvents(); - if (openVrQuitRequested()) { - emit outputDeviceLost(); - return false; - } + double displayFrequency = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float); double frameDuration = 1.f / displayFrequency; double vsyncToPhotons = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float); diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 155bc9f079..3c765c3fa8 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -26,11 +26,6 @@ using Lock = std::unique_lock; static int refCount { 0 }; static Mutex mutex; static vr::IVRSystem* activeHmd { nullptr }; -static bool _openVrQuitRequested { false }; - -bool openVrQuitRequested() { - return _openVrQuitRequested; -} static const uint32_t RELEASE_OPENVR_HMD_DELAY_MS = 5000; @@ -84,7 +79,6 @@ void releaseOpenVrSystem() { if (0 == refCount) { qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; vr::VR_Shutdown(); - _openVrQuitRequested = false; activeHmd = nullptr; } } @@ -103,14 +97,14 @@ void handleOpenVrEvents() { while (activeHmd->PollNextEvent(&event, sizeof(event))) { switch (event.eventType) { case vr::VREvent_Quit: - _openVrQuitRequested = true; activeHmd->AcknowledgeQuit_Exiting(); + QMetaObject::invokeMethod(qApp, "quit"); break; default: break; } - qDebug() << "OpenVR: Event " << event.eventType; + qDebug() << "OpenVR: Event " << activeHmd->GetEventTypeNameFromEnum((vr::EVREventType)event.eventType) << "(" << event.eventType << ")"; } } diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index a7a0a2f56f..b862aacb06 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -214,10 +214,6 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); handleOpenVrEvents(); - if (openVrQuitRequested()) { - deactivate(); - return; - } // because update mutates the internal state we need to lock userInputMapper->withLock([&, this]() { From 925375fb7a79405bab292b0d16176c6370f0919e Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 10 Jun 2016 17:19:50 -0700 Subject: [PATCH 0560/1237] Move steamworks archive zip to dependencies --- cmake/externals/steamworks/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/externals/steamworks/CMakeLists.txt b/cmake/externals/steamworks/CMakeLists.txt index b4c480f050..152e95cdcf 100644 --- a/cmake/externals/steamworks/CMakeLists.txt +++ b/cmake/externals/steamworks/CMakeLists.txt @@ -4,7 +4,7 @@ set(EXTERNAL_NAME steamworks) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(STEAMWORKS_URL "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/Steamworks.zip") +set(STEAMWORKS_URL "https://s3.amazonaws.com/hifi-public/dependencies/steamworks_sdk_137.zip") set(STEAMWORKS_URL_MD5 "95ba9d0e3ddc04f8a8be17d2da806cbb") ExternalProject_Add( From cc16dc07dfa80a0da3d889c004f722fdb6bdf9c7 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 13 Jun 2016 15:49:31 -0700 Subject: [PATCH 0561/1237] Add sandbox option to load home locally --- server-console/src/main.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server-console/src/main.js b/server-console/src/main.js index 6465f6b0a3..8f85872d0b 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -515,6 +515,20 @@ function maybeInstallDefaultContentSet(onComplete) { return; } + console.log("Found contentPath:" + argv.contentPath); + if (argv.contentPath) { + fs.copy(argv.contentPath, getRootHifiDataDirectory(), function (err) { + if (err) { + console.log('Could not copy home content: ' + err); + return console.error(err) + } + console.log('Copied home content over to: ' + getRootHifiDataDirectory()); + onComplete(); + }); + return; + } + + // Show popup var window = new BrowserWindow({ icon: appIcon, From 1403fad04ea26122cddd07d2a13192160929b43d Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 14 Jun 2016 16:26:39 -0700 Subject: [PATCH 0562/1237] Add cmd line opt to disable settings reset popup --- interface/src/Application.cpp | 5 ++++- interface/src/CrashHandler.cpp | 6 +++++- interface/src/CrashHandler.h | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 60c9de1223..8a38222abb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -389,7 +389,10 @@ bool setupEssentials(int& argc, char** argv) { Setting::preInit(); - bool previousSessionCrashed = CrashHandler::checkForResetSettings(); + + static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; + bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); + bool previousSessionCrashed = CrashHandler::checkForResetSettings(suppressPrompt); CrashHandler::writeRunningMarkerFiler(); qAddPostRoutine(CrashHandler::deleteRunningMarkerFile); diff --git a/interface/src/CrashHandler.cpp b/interface/src/CrashHandler.cpp index 8de6766c7a..3c5f03bef3 100644 --- a/interface/src/CrashHandler.cpp +++ b/interface/src/CrashHandler.cpp @@ -27,7 +27,7 @@ static const QString RUNNING_MARKER_FILENAME = "Interface.running"; -bool CrashHandler::checkForResetSettings() { +bool CrashHandler::checkForResetSettings(bool suppressPrompt) { QSettings::setDefaultFormat(QSettings::IniFormat); QSettings settings; settings.beginGroup("Developer"); @@ -42,6 +42,10 @@ bool CrashHandler::checkForResetSettings() { QFile runningMarkerFile(runningMarkerFilePath()); bool wasLikelyCrash = runningMarkerFile.exists(); + if (suppressPrompt) { + return wasLikelyCrash; + } + if (wasLikelyCrash || askToResetSettings) { if (displaySettingsResetOnCrash || askToResetSettings) { Action action = promptUserForAction(wasLikelyCrash); diff --git a/interface/src/CrashHandler.h b/interface/src/CrashHandler.h index 566b780d61..a65fed677d 100644 --- a/interface/src/CrashHandler.h +++ b/interface/src/CrashHandler.h @@ -17,7 +17,7 @@ class CrashHandler { public: - static bool checkForResetSettings(); + static bool checkForResetSettings(bool suppressPrompt = false); static void writeRunningMarkerFiler(); static void deleteRunningMarkerFile(); From f6994f2783cf17d7e30312784ddcf78865bb47e4 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Tue, 14 Jun 2016 17:41:53 -0700 Subject: [PATCH 0563/1237] Revert "Revert "refresh API info during re-connect - case 570"" This reverts commit 1624146fc2ecf6abf508b03769f81ff2220158ae. --- libraries/networking/src/AddressManager.cpp | 44 ++++++++++++++++++--- libraries/networking/src/AddressManager.h | 9 ++++- libraries/networking/src/DomainHandler.cpp | 18 +++------ libraries/networking/src/DomainHandler.h | 10 ++--- libraries/networking/src/NodeList.cpp | 14 +++++-- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 1b7ed11cce..9a3d147ead 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -144,12 +144,21 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // 4. domain network address (IP or dns resolvable hostname) // use our regex'ed helpers to figure out what we're supposed to do with this - if (!handleUsername(lookupUrl.authority())) { + if (handleUsername(lookupUrl.authority())) { + // handled a username for lookup + + // in case we're failing to connect to where we thought this user was + // store their username as previous lookup so we can refresh their location via API + _previousLookup = lookupUrl; + } else { // we're assuming this is either a network address or global place name // check if it is a network address first bool hostChanged; if (handleNetworkAddress(lookupUrl.host() - + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + // a network address lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); // If the host changed then we have already saved to history if (hostChanged) { @@ -165,10 +174,16 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info + _previousLookup = lookupUrl; + // no place name - this is probably a domain ID // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + // store this place name as the previous lookup in case we fail to connect and want to refresh API info + _previousLookup = lookupUrl; + // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger); @@ -180,9 +195,13 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); + // a path lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); + // if this is a relative path then handle it as a relative viewpoint handlePath(lookupUrl.path(), trigger, true); emit lookupResultsFinished(); + return true; } @@ -276,7 +295,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; - emit possibleDomainChangeRequired(domainHostname, domainPort); + emit possibleDomainChangeRequired(domainHostname, domainPort, domainID); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); @@ -315,7 +334,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty()) { - handlePath(overridePath, trigger); + // make sure we don't re-handle an overriden path if this was a refresh of info from API + if (trigger != LookupTrigger::AttemptedRefresh) { + handlePath(overridePath, trigger); + } } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; @@ -598,7 +620,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress); - emit possibleDomainChangeRequired(hostname, port); + emit possibleDomainChangeRequired(hostname, port, QUuid()); return hostChanged; } @@ -618,6 +640,13 @@ void AddressManager::goToUser(const QString& username) { QByteArray(), nullptr, requestParams); } +void AddressManager::refreshPreviousLookup() { + // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history) + if (!_previousLookup.isEmpty()) { + handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh); + } +} + void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } @@ -629,7 +658,10 @@ void AddressManager::copyPath() { void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { // if we're cold starting and this is called for the first address (from settings) we don't do anything - if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) { + if (trigger != LookupTrigger::StartupFromSettings + && trigger != LookupTrigger::DomainPathResponse + && trigger != LookupTrigger::AttemptedRefresh) { + if (trigger == LookupTrigger::Back) { // we're about to push to the forward stack // if it's currently empty emit our signal to say that going forward is now possible diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 643924ff5c..a3aaee3ba2 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -48,7 +48,8 @@ public: Forward, StartupFromSettings, DomainPathResponse, - Internal + Internal, + AttemptedRefresh }; bool isConnected(); @@ -89,6 +90,8 @@ public slots: void goToUser(const QString& username); + void refreshPreviousLookup(); + void storeCurrentAddress(); void copyAddress(); @@ -99,7 +102,7 @@ signals: void lookupResultIsOffline(); void lookupResultIsNotFound(); - void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort); + void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID); void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID); void locationChangeRequired(const glm::vec3& newPosition, @@ -152,6 +155,8 @@ private: quint64 _lastBackPush = 0; QString _newHostLookupPath; + + QUrl _previousLookup; }; #endif // hifi_AddressManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 4f85296f03..1efcfc7f27 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -28,16 +28,8 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), - _uuid(), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), - _assignmentUUID(), - _connectionToken(), - _iceDomainID(), - _iceClientID(), - _iceServerSockAddr(), _icePeer(this), - _isConnected(false), - _settingsObject(), _settingsTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -105,7 +97,7 @@ void DomainHandler::hardReset() { softReset(); qCDebug(networking) << "Hard reset in NodeList DomainHandler."; - _iceDomainID = QUuid(); + _pendingDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); _sockAddr.clear(); @@ -139,7 +131,7 @@ void DomainHandler::setUUID(const QUuid& uuid) { } } -void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { +void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded @@ -171,6 +163,8 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { // grab the port by reading the string after the colon _sockAddr.setPort(port); } + + _pendingDomainID = domainID; } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { @@ -181,7 +175,7 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - _iceDomainID = id; + _pendingDomainID = id; HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); @@ -343,7 +337,7 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation); - if (_icePeer.getUUID() != _iceDomainID) { + if (_icePeer.getUUID() != _pendingDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; _icePeer.reset(); } else { diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index bcee7668d1..226186f1d0 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -58,8 +58,8 @@ public: const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - - const QUuid& getICEDomainID() const { return _iceDomainID; } + + const QUuid& getPendingDomainID() const { return _pendingDomainID; } const QUuid& getICEClientID() const { return _iceClientID; } @@ -94,7 +94,7 @@ public: }; public slots: - void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); + void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid()); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); void processSettingsPacketList(QSharedPointer packetList); @@ -136,11 +136,11 @@ private: HifiSockAddr _sockAddr; QUuid _assignmentUUID; QUuid _connectionToken; - QUuid _iceDomainID; + QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection QUuid _iceClientID; HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; - bool _isConnected; + bool _isConnected { false }; QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 16a4083b08..082200fccc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, - &_domainHandler, &DomainHandler::setHostnameAndPort); + &_domainHandler, &DomainHandler::setSocketAndID); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); @@ -355,6 +355,14 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } + + if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { + // if we aren't connected to the domain-server, and we have an ID + // (that we presume belongs to a domain in the HF Metaverse) + // we request connection information for the domain every so often to make sure what we have is up to date + + DependencyManager::get()->refreshPreviousLookup(); + } } void NodeList::handleDSPathQuery(const QString& newPath) { @@ -462,7 +470,7 @@ void NodeList::handleICEConnectionToDomainServer() { LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), - _domainHandler.getICEDomainID()); + _domainHandler.getPendingDomainID()); } } @@ -475,7 +483,7 @@ void NodeList::pingPunchForDomainServer() { if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" - << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); + << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID()); } else { if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat From 747b33f7561b594002586aec0133b70de1ebcc42 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Tue, 14 Jun 2016 17:42:01 -0700 Subject: [PATCH 0564/1237] Revert "additional revert related change" This reverts commit 8586e91a3b78e37f54384721990b979caff96c3e. --- libraries/networking/src/AddressManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 9a3d147ead..80989acd2c 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -384,7 +384,7 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) { if (errorReply.error() == QNetworkReply::ContentNotFoundError) { // if this is a lookup that has no result, don't keep re-trying it - //_previousLookup.clear(); + _previousLookup.clear(); emit lookupResultIsNotFound(); } From d778ec96d7f7c67ceb3565df8b74c5d0785d5b6d Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Tue, 14 Jun 2016 17:42:13 -0700 Subject: [PATCH 0565/1237] Revert "fix shapes to property polymorph and persist" This reverts commit 3bee4b1f9f07e933c4a5e5a8d46e4503b96315ba. --- .../entities/src/EntityItemProperties.cpp | 19 ++------ .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 - scripts/system/html/entityProperties.html | 48 +++++++++++++------ 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ba39727ff9..f273507d0d 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -314,8 +314,6 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly); CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); - CHECK_PROPERTY_CHANGE(PROP_SHAPE, shape); - changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); changedProperties += _skybox.getChangedProperties(); @@ -437,9 +435,6 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool if (_type == EntityTypes::Sphere) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, QString("Sphere")); } - if (_type == EntityTypes::Box || _type == EntityTypes::Sphere || _type == EntityTypes::Shape) { - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SHAPE, shape); - } // FIXME - it seems like ParticleEffect should also support this if (_type == EntityTypes::Model || _type == EntityTypes::Zone) { @@ -1149,11 +1144,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_STROKE_WIDTHS, properties.getStrokeWidths()); APPEND_ENTITY_PROPERTY(PROP_TEXTURES, properties.getTextures()); } - // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE - // when encoding/decoding edits because otherwise they can't polymorph to other shape types - if (properties.getType() == EntityTypes::Shape || - properties.getType() == EntityTypes::Box || - properties.getType() == EntityTypes::Sphere) { + if (properties.getType() == EntityTypes::Shape) { APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); @@ -1220,6 +1211,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem } else { packetData->discardSubTree(); } + return success; } @@ -1442,12 +1434,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_WIDTHS, QVector, setStrokeWidths); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_TEXTURES, QString, setTextures); } - - // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE - // when encoding/decoding edits because otherwise they can't polymorph to other shape types - if (properties.getType() == EntityTypes::Shape || - properties.getType() == EntityTypes::Box || - properties.getType() == EntityTypes::Sphere) { + if (properties.getType() == EntityTypes::Shape) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index db743f81e4..92e5b52f75 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS; + return VERSION_ENTITIES_MORE_SHAPES; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 320635379d..97398c6744 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -179,7 +179,6 @@ const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; -const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 2a82d8fa74..5c87f753d9 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -557,6 +557,7 @@ } properties = data.selections[0].properties; + elID.innerHTML = properties.id; elType.innerHTML = properties.type; @@ -674,9 +675,6 @@ for (var i = 0; i < elShapeSections.length; i++) { elShapeSections[i].style.display = 'table'; } - elShape.value = properties.shape; - setDropdownText(elShape); - } else { for (var i = 0; i < elShapeSections.length; i++) { console.log("Hiding shape section " + elShapeSections[i]) @@ -1351,17 +1349,6 @@

-
@@ -1375,6 +1362,39 @@
+
+ M +
+ +
+ + +
+
+ + +
+ + From ae65790bf3f0341319e25217cd61d4d802ebc579 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 14 Jun 2016 18:07:08 -0700 Subject: [PATCH 0566/1237] no laser (or mouse cursor) from hand controllers unless trigger squeezed --- scripts/system/controllers/handControllerPointer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 7046ed16a5..7371680c32 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -377,7 +377,7 @@ function turnOffVisualization(optionalEnableClicks) { // because we're showing c if (!optionalEnableClicks) { expireMouseCursor(); clearSystemLaser(); - } else if (!systemLaserOn) { + } else if (!systemLaserOn && activeTrigger.state) { // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. systemLaserOn = HMD.setHandLasers(activeHudLaser, true, LASER_COLOR_XYZW, SYSTEM_LASER_DIRECTION); Reticle.visible = !systemLaserOn; From 42eaaf742d9ca358f7133fb886572392f707e99e Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 14 Jun 2016 18:29:06 -0700 Subject: [PATCH 0567/1237] Protoype done --- .../render-utils/src/DeferredLightingEffect.h | 2 +- .../render-utils/src/RenderDeferredTask.cpp | 3 + .../render-utils/src/SubsurfaceScattering.cpp | 11 ++- .../render-utils/src/SubsurfaceScattering.h | 8 +-- .../subsurfaceScattering_drawScattering.slf | 71 +++++++++---------- libraries/render/src/render/BlurTask.h | 2 +- libraries/render/src/render/Task.cpp | 8 +-- libraries/render/src/render/Task.h | 19 +++-- 8 files changed, 68 insertions(+), 56 deletions(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 6428cc5742..63d8f4d175 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -44,7 +44,7 @@ public: const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI); void prepare(RenderArgs* args); - void render(const render::RenderContextPointer& renderContext, ); + void render(const render::RenderContextPointer& renderContext); void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int skyboxCubemapUnit); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index dcdf45e0c9..fb505f83e9 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -113,6 +113,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Opaque all rendered, generate surface geometry buffers const auto curvatureFramebufferAndDepth = addJob("SurfaceGeometry", deferredFrameTransform); + + const auto theCurvatureVarying = curvatureFramebufferAndDepth[0]; + #define SIMPLE_BLUR 1 #if SIMPLE_BLUR const auto curvatureFramebuffer = addJob("DiffuseCurvature", curvatureFramebufferAndDepth.get().first); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 5c728b7307..cab12e7377 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -34,7 +34,7 @@ enum ScatteringShaderMapSlots { ScatteringTask_AlbedoMapSlot, ScatteringTask_LinearMapSlot, - SCatteringTask_IBLMapSlot, + ScatteringTask_IBLMapSlot, }; @@ -84,6 +84,8 @@ gpu::PipelinePointer SubsurfaceScattering::getScatteringPipeline() { slotBindings.insert(gpu::Shader::Binding(std::string("albedoMap"), ScatteringTask_AlbedoMapSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), ScatteringTask_LinearMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), ScatteringTask_IBLMapSlot)); + gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -194,8 +196,12 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setUniformBuffer(ScatteringTask_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); batch.setUniformBuffer(ScatteringTask_ParamSlot, _parametersBuffer); - if (theLight->light) + if (theLight->light) { batch.setUniformBuffer(ScatteringTask_LightSlot, theLight->light->getSchemaBuffer()); + if (theLight->light->getAmbientMap()) { + batch.setResourceTexture(ScatteringTask_IBLMapSlot, theLight->light->getAmbientMap()); + } + } batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, _scatteringTable); batch.setResourceTexture(ScatteringTask_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); @@ -203,7 +209,6 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setResourceTexture(ScatteringTask_AlbedoMapSlot, framebufferCache->getDeferredColorTexture()); batch.setResourceTexture(ScatteringTask_LinearMapSlot, framebufferCache->getDepthPyramidTexture()); - batch.draw(gpu::TRIANGLE_STRIP, 4); if (_showLUT) { diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 818c18bcb9..61bccab48f 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -35,12 +35,12 @@ public: float bentRed{ 1.5f }; float bentGreen{ 0.8f }; float bentBlue{ 0.3f }; - float bentScale{ 1.0f }; + float bentScale{ 1.5f }; - float curvatureOffset{ 0.012f }; - float curvatureScale{ 0.25f }; + float curvatureOffset{ 0.08f }; + float curvatureScale{ 0.8f }; - bool showLUT{ true }; + bool showLUT{ false }; signals: void dirty(); diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index eac4da0709..154f5f0e8c 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -14,7 +14,9 @@ <$declareDeferredFrameTransform()$> -<@include model/Light.slh@> +<@include DeferredGlobalLight.slh@> + +<$declareEvalGlobalSpecularIrradiance(0, 1, 0)$> uniform sampler2D linearDepthMap; float getZEye(ivec2 pixel) { @@ -26,35 +28,6 @@ float getZEyeLinear(vec2 texcoord) { <@include DeferredBufferRead.slh@> - -vec3 fresnelSchlick(vec3 fresnelColor, vec3 lightDir, vec3 halfDir) { - return fresnelColor + (1.0 - fresnelColor) * pow(1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0), 5); -} - -float specularDistribution(float roughness, vec3 normal, vec3 halfDir) { - float ndoth = clamp(dot(halfDir, normal), 0.0, 1.0); - float gloss2 = pow(0.001 + roughness, 4); - float denom = (ndoth * ndoth*(gloss2 - 1) + 1); - float power = gloss2 / (3.14159 * denom * denom); - return power; -} - -// Frag Shading returns the diffuse amount as W and the specular rgb as xyz -vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 fresnel, float roughness) { - // Diffuse Lighting - float diffuse = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); - - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir,halfDir); - float power = specularDistribution(roughness, fragNormal, halfDir); - vec3 specular = power * fresnelColor * diffuse; - - return vec4(specular, (1.0 - metallic) * diffuse * (1 - fresnelColor.x)); -} - - - vec2 sideToFrameTexcoord(vec2 side, vec2 texcoordPos) { return vec2((texcoordPos.x + side.x) * side.y, texcoordPos.y); } @@ -132,10 +105,10 @@ void main(void) { // --> Calculate the light vector. Light light = getLight(); - vec3 lightVector = -getLightDirection(light); //normalize(uniformLightVector); //normalize(lightPos - sourcePos.xyz); + vec3 fragLightDir = -getLightDirection(light); //normalize(uniformLightVector); //normalize(lightPos - sourcePos.xyz); - // _fragColor = vec4(fetchBRDF(dot(bentNormalR, lightVector), abs(diffusedCurvature.w * 2 - 1)), 1.0); - // _fragColor = vec4(vec3(abs(dot(bentNormalR, lightVector))), 1.0); + // _fragColor = vec4(fetchBRDF(dot(bentNormalR, fragLightDir), abs(diffusedCurvature.w * 2 - 1)), 1.0); + // _fragColor = vec4(vec3(abs(dot(bentNormalR, fragLightDir))), 1.0); // _fragColor = vec4(vec3(varTexCoord0, 0.0), 1.0); // _fragColor = vec4(vec3(bentNormalR * 0.5 + 0.5), 1.0); @@ -147,7 +120,7 @@ void main(void) { vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); - vec3 NdotLSpectrum = vec3(dot(rN, lightVector), dot(gN, lightVector), dot(bN, lightVector)); + vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); // --> Look up the pre-integrated curvature-dependent BDRF textures vec3 bdrf = fetchBRDFSpectrum(NdotLSpectrum, curvature); @@ -174,11 +147,37 @@ void main(void) { fresnel = fragment.diffuse; fragment.metallic = 1.0; } + vec3 albedo = fragment.diffuse; + vec3 fragNormal = fragment.normal; - //vec4 shading = evalPBRShading(rS, lightVector, fragEyeDir, fragment.metallic, fresnel, fragment.roughness); - _fragColor = vec4(fragment.diffuse * vec3(bdrf.xyz), 1.0); + vec4 shading; + + { // Key Sun Lighting + // Diffuse Lighting + float diffuse = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir,halfDir); + float power = specularDistribution(fragment.roughness, fragNormal, halfDir); + vec3 specular = power * fresnelColor * diffuse; + + shading = vec4(specular, (1.0 - fragment.metallic) * diffuse * (1 - fresnelColor.x)); + } + + vec3 color = vec3(albedo * vec3(bdrf.xyz) + shading.rgb) * getLightColor(light) * getLightIntensity(light); + // Diffuse from ambient + color += (1 - fragment.metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), bentNormalHigh).xyz * 1.0 * getLightAmbientIntensity(light); + + // Specular highlight from ambient + vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, fragment.roughness, fresnel, 1.0); + color += specularLighting; + + //_fragColor = vec4(evalSkyboxLight(rS, 0.0).rgb, 1.0); + + _fragColor = vec4(color, 1.0); } diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 1f3b1000d7..333cd22f8e 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -77,7 +77,7 @@ class BlurGaussianConfig : public Job::Config { Q_PROPERTY(float filterScale MEMBER filterScale NOTIFY dirty) // expose enabled flag public: - float filterScale{ 1.0f }; + float filterScale{ 1.2f }; signals : void dirty(); diff --git a/libraries/render/src/render/Task.cpp b/libraries/render/src/render/Task.cpp index 8727923c70..a180e7ff8e 100644 --- a/libraries/render/src/render/Task.cpp +++ b/libraries/render/src/render/Task.cpp @@ -27,14 +27,14 @@ void TaskConfig::refresh() { namespace render{ - template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var) { + template <> void varyingGet(const VaryingPairBase* data, uint8_t index, Varying& var) { if (index == 0) { - var = data.first; + var = data->first; } else { - var = data.second; + var = data->second; } } - template <> uint8_t varyingLength(const VaryingPairBase& data) { return 2; } + template <> uint8_t varyingLength(const VaryingPairBase* data) { return 2; } } \ No newline at end of file diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 5a67e68f20..947f1a3da0 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -31,8 +31,8 @@ namespace render { class Varying; - template void varyingGet(const T& data, uint8_t index, Varying& var) {} -template uint8_t varyingLength(const T& data) { return 0; } + template void varyingGet(const T* data, uint8_t index, Varying& var) {} +template uint8_t varyingLength(const T* data) { return 0; } // A varying piece of data, to be used as Job/Task I/O // TODO: Task IO @@ -65,6 +65,8 @@ protected: template class Model : public Concept { public: using Data = T; + // using VarContainer = std::enable_if; + //using VarContainer = std::conditional, T::VarContainer, T>; Model(const Data& data) : _data(data) {} virtual ~Model() = default; @@ -72,10 +74,10 @@ protected: virtual Varying operator[] (uint8_t index) const { Varying var; - varyingGet(_data, index, var); + // varyingGet(&_data, index, var); return var; } - virtual uint8_t length() const { return varyingLength(_data); } + virtual uint8_t length() const { return varyingLength(&_data); } Data _data; }; @@ -89,13 +91,14 @@ protected: using VaryingPairBase = std::pair; -template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var); -template <> uint8_t varyingLength(const VaryingPairBase& data); +template <> void varyingGet(const VaryingPairBase* data, uint8_t index, Varying& var); +template <> uint8_t varyingLength(const VaryingPairBase* data); -template < class T0, class T1 > +template < typename T0, typename T1 > class VaryingPair : public VaryingPairBase { public: using Parent = VaryingPairBase; + using VarContainer = VaryingPairBase; VaryingPair() : Parent(Varying(T0()), Varying(T1())) {} VaryingPair(const VaryingPair& pair) : Parent(pair.first, pair.second) {} @@ -108,6 +111,7 @@ public: T1& editSecond() { return second.edit(); } }; + template class VaryingTrio : public std::tuple{ public: @@ -496,4 +500,5 @@ protected: } + #endif // hifi_render_Task_h From 657f8fdad56d5241c0d24c1219192358fae28cf1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 15 Jun 2016 14:12:49 +1200 Subject: [PATCH 0568/1237] Fix reporting of JSON export failure --- interface/src/Application.cpp | 2 +- libraries/octree/src/Octree.cpp | 30 ++++++++++++++++++--------- libraries/octree/src/Octree.h | 6 +++--- libraries/ui/src/FileDialogHelper.cpp | 2 ++ 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 49c2e17d84..7a74258916 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2916,7 +2916,7 @@ bool Application::exportEntities(const QString& filename, const QVectorwriteToJSONFile(filename.toLocal8Bit().constData()); + success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); // restore the main window's active state _window->activateWindow(); diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 475beef03c..d8c8229ce3 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1868,24 +1868,26 @@ bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputSt return success; } -void Octree::writeToFile(const char* fileName, OctreeElementPointer element, QString persistAsFileType) { +bool Octree::writeToFile(const char* fileName, OctreeElementPointer element, QString persistAsFileType) { // make the sure file extension makes sense QString qFileName = fileNameWithoutExtension(QString(fileName), PERSIST_EXTENSIONS) + "." + persistAsFileType; QByteArray byteArray = qFileName.toUtf8(); const char* cFileName = byteArray.constData(); + bool success = false; if (persistAsFileType == "svo") { - writeToSVOFile(fileName, element); + success = writeToSVOFile(fileName, element); } else if (persistAsFileType == "json") { - writeToJSONFile(cFileName, element); + success = writeToJSONFile(cFileName, element); } else if (persistAsFileType == "json.gz") { - writeToJSONFile(cFileName, element, true); + success = writeToJSONFile(cFileName, element, true); } else { qCDebug(octree) << "unable to write octree to file of type" << persistAsFileType; } + return success; } -void Octree::writeToJSONFile(const char* fileName, OctreeElementPointer element, bool doGzip) { +bool Octree::writeToJSONFile(const char* fileName, OctreeElementPointer element, bool doGzip) { QVariantMap entityDescription; qCDebug(octree, "Saving JSON SVO to file %s...", fileName); @@ -1906,7 +1908,7 @@ void Octree::writeToJSONFile(const char* fileName, OctreeElementPointer element, bool entityDescriptionSuccess = writeToMap(entityDescription, top, true, true); if (!entityDescriptionSuccess) { qCritical("Failed to convert Entities to QVariantMap while saving to json."); - return; + return false; } // convert the QVariantMap to JSON @@ -1916,22 +1918,26 @@ void Octree::writeToJSONFile(const char* fileName, OctreeElementPointer element, if (doGzip) { if (!gzip(jsonData, jsonDataForFile, -1)) { qCritical("unable to gzip data while saving to json."); - return; + return false; } } else { jsonDataForFile = jsonData; } QFile persistFile(fileName); + bool success = false; if (persistFile.open(QIODevice::WriteOnly)) { - persistFile.write(jsonDataForFile); + success = persistFile.write(jsonDataForFile) != -1; } else { qCritical("Could not write to JSON description of entities."); } + + return success; } -void Octree::writeToSVOFile(const char* fileName, OctreeElementPointer element) { - qWarning() << "SVO file format depricated. Support for reading SVO files is no longer support and will be removed soon."; +bool Octree::writeToSVOFile(const char* fileName, OctreeElementPointer element) { + qWarning() << "SVO file format deprecated. Support for reading SVO files is no longer support and will be removed soon."; + bool success = false; std::ofstream file(fileName, std::ios::out|std::ios::binary); @@ -2010,8 +2016,12 @@ void Octree::writeToSVOFile(const char* fileName, OctreeElementPointer element) } releaseSceneEncodeData(&extraEncodeData); + + success = true; } file.close(); + + return success; } unsigned long Octree::getOctreeElementsCount() { diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 2f0ce3b807..6894f0aa1a 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -299,9 +299,9 @@ public: void loadOctreeFile(const char* fileName); // Octree exporters - void writeToFile(const char* filename, OctreeElementPointer element = NULL, QString persistAsFileType = "svo"); - void writeToJSONFile(const char* filename, OctreeElementPointer element = NULL, bool doGzip = false); - void writeToSVOFile(const char* filename, OctreeElementPointer element = NULL); + bool writeToFile(const char* filename, OctreeElementPointer element = NULL, QString persistAsFileType = "svo"); + bool writeToJSONFile(const char* filename, OctreeElementPointer element = NULL, bool doGzip = false); + bool writeToSVOFile(const char* filename, OctreeElementPointer element = NULL); virtual bool writeToMap(QVariantMap& entityDescription, OctreeElementPointer element, bool skipDefaultValues, bool skipThoseWithBadParents) = 0; diff --git a/libraries/ui/src/FileDialogHelper.cpp b/libraries/ui/src/FileDialogHelper.cpp index c9a8953f01..9840e55927 100644 --- a/libraries/ui/src/FileDialogHelper.cpp +++ b/libraries/ui/src/FileDialogHelper.cpp @@ -95,6 +95,8 @@ bool FileDialogHelper::urlIsWritable(const QUrl& url) { // No file, get the parent directory and check if writable return QFileInfo(fileInfo.absoluteDir().absolutePath()).isWritable(); + // Note: Does not correctly detect on Windows that the OS drive root (i.e., C:/) cannot be written to. This even if turn on + // NTFS permissions checking. } QStringList FileDialogHelper::drives() { From b9754c85649a80d1332a573f120f8f11efc3bf13 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 14 Jun 2016 20:17:52 -0700 Subject: [PATCH 0569/1237] continue grabbing through overlays --- scripts/system/controllers/handControllerGrab.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 53c16f26c1..25889c9c1c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -361,11 +361,15 @@ function MyController(hand) { var _this = this; + this.ignoreInput = function () { + return (_this.state <= STATE_HOLD_SEARCHING) && isIn2DMode(); + }; + this.update = function() { this.updateSmoothedTrigger(); - if (isIn2DMode()) { + if (_this.ignoreInput()) { _this.turnOffVisualizations(); return; } From 3e459085ac650ef121b7a7ca53c096da12a50a7f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 15 Jun 2016 09:25:46 -0700 Subject: [PATCH 0570/1237] don't log spam on empty URL --- libraries/procedural/src/procedural/Procedural.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index a9a297a2e1..7dd729384c 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -96,7 +96,9 @@ bool Procedural::parseVersion(const QJsonValue& version) { bool Procedural::parseUrl(const QUrl& shaderUrl) { if (!shaderUrl.isValid()) { - qWarning() << "Invalid shader URL: " << shaderUrl; + if (!shaderUrl.isEmpty()) { + qWarning() << "Invalid shader URL: " << shaderUrl; + } _networkShader.reset(); return false; } From 59912012c628906a1fee781545bb0698442290b7 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 15 Jun 2016 09:48:53 -0700 Subject: [PATCH 0571/1237] clear domain connection refusals on hard reset, not soft --- libraries/networking/src/DomainHandler.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 057dd3747d..170669dede 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -97,7 +97,6 @@ void DomainHandler::softReset() { clearSettings(); - _domainConnectionRefusals.clear(); _connectionDenialsSinceKeypairRegen = 0; // cancel the failure timeout for any pending requests for settings @@ -119,6 +118,7 @@ void DomainHandler::hardReset() { _sockAddr.clear(); _hasSignalledProtocolMismatch = false; + _domainConnectionRefusals.clear(); _hasCheckedForAccessToken = false; @@ -408,6 +408,7 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer Date: Tue, 14 Jun 2016 18:25:01 -0700 Subject: [PATCH 0572/1237] Support keyboard suppresion, control suppresion while in keyboard mode --- interface/resources/controllers/vive.json | 15 ++- .../src/scripting/HMDScriptingInterface.cpp | 12 ++ .../src/scripting/HMDScriptingInterface.h | 17 ++- libraries/plugins/src/plugins/DisplayPlugin.h | 126 ++++++++++-------- libraries/shared/src/shared/Bilateral.h | 49 +++++++ plugins/oculus/src/OculusHelpers.cpp | 8 ++ plugins/openvr/src/OpenVrDisplayPlugin.cpp | 24 ++++ plugins/openvr/src/OpenVrDisplayPlugin.h | 6 + plugins/openvr/src/ViveControllerManager.cpp | 6 + 9 files changed, 206 insertions(+), 57 deletions(-) create mode 100644 libraries/shared/src/shared/Bilateral.h diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 95c8f5a734..677ec1ccfc 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -4,15 +4,24 @@ { "from": "Vive.LY", "when": "Vive.LSY", "filters": ["invert"], "to": "Standard.LY" }, { "from": "Vive.LX", "when": "Vive.LSX", "to": "Standard.LX" }, - { "from": "Vive.LT", "to": "Standard.LT" }, + { + "from": "Vive.LT", "to": "Standard.LT", + "filters": [ + { "type": "deadZone", "min": 0.05 }, + ] + }, { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, { "from": "Vive.RY", "when": "Vive.RSY", "filters": ["invert"], "to": "Standard.RY" }, { "from": "Vive.RX", "when": "Vive.RSX", "to": "Standard.RX" }, - - { "from": "Vive.RT", "to": "Standard.RT" }, + { + "from": "Vive.RT", "to": "Standard.RT", + "filters": [ + { "type": "deadZone", "min": 0.05 }, + ] + }, { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 02840a9775..4dd43dfdf1 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -115,3 +115,15 @@ bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::ve void HMDScriptingInterface::disableHandLasers(int hands) const { qApp->getActiveDisplayPlugin()->setHandLaser(hands, DisplayPlugin::HandLaserMode::None); } + +bool HMDScriptingInterface::suppressKeyboard() { + return qApp->getActiveDisplayPlugin()->suppressKeyboard(); +} + +void HMDScriptingInterface::unsuppressKeyboard() { + qApp->getActiveDisplayPlugin()->unsuppressKeyboard(); +} + +bool HMDScriptingInterface::isKeyboardVisible() { + return qApp->getActiveDisplayPlugin()->isKeyboardVisible(); +} diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index c55320ca83..e2985b1ce5 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -1,4 +1,4 @@ -// + // HMDScriptingInterface.h // interface/src/scripting // @@ -12,6 +12,8 @@ #ifndef hifi_HMDScriptingInterface_h #define hifi_HMDScriptingInterface_h +#include + #include class QScriptContext; class QScriptEngine; @@ -38,6 +40,19 @@ public: Q_INVOKABLE QString preferredAudioOutput() const; Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; Q_INVOKABLE void disableHandLasers(int hands) const; + /// Suppress the activation of any on-screen keyboard so that a script operation will + /// not be interrupted by a keyboard popup + /// Returns false if there is already an active keyboard displayed. + /// Clients should re-enable the keyboard when the operation is complete and ensure + /// that they balance any call to suppressKeyboard() that returns true with a corresponding + /// call to unsuppressKeyboard() within a reasonable amount of time + Q_INVOKABLE bool suppressKeyboard(); + + /// Enable the keyboard following a suppressKeyboard call + Q_INVOKABLE void unsuppressKeyboard(); + + /// Query the display plugin to determine the current VR keyboard visibility + Q_INVOKABLE bool isKeyboardVisible(); public: HMDScriptingInterface(); diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 9cb4e071f3..5f659f45f9 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -16,16 +16,18 @@ #include #include #include -class QImage; #include #include +#include #include "Plugin.h" +class QImage; + enum Eye { - Left, - Right + Left = bilateral::Side::Left, + Right = bilateral::Side::Right }; /* @@ -56,7 +58,73 @@ namespace gpu { using TexturePointer = std::shared_ptr; } -class DisplayPlugin : public Plugin { +// Stereo display functionality +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// displayPlugin->isStereo returns true +class StereoDisplay { +public: + // Stereo specific methods + virtual glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { + return baseProjection; + } + + virtual glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const { + return baseProjection; + } + + virtual float getIPD() const { return AVERAGE_HUMAN_IPD; } +}; + +// HMD display functionality +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// displayPlugin->isHmd returns true +class HmdDisplay : public StereoDisplay { +public: + // HMD specific methods + // TODO move these into another class? + virtual glm::mat4 getEyeToHeadTransform(Eye eye) const { + static const glm::mat4 transform; return transform; + } + + // returns a copy of the most recent head pose, computed via updateHeadPose + virtual glm::mat4 getHeadPose() const { + return glm::mat4(); + } + + // Needed for timewarp style features + virtual void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { + // NOOP + } + + virtual void abandonCalibration() {} + + virtual void resetSensors() {} + + enum Hand { + LeftHand = 0x01, + RightHand = 0x02, + }; + + enum class HandLaserMode { + None, // Render no hand lasers + Overlay, // Render hand lasers only if they intersect with the UI layer, and stop at the UI layer + }; + + virtual bool setHandLaser( + uint32_t hands, // Bits from the Hand enum + HandLaserMode mode, // Mode in which to render + const vec4& color = vec4(1), // The color of the rendered laser + const vec3& direction = vec3(0, 0, -1) // The direction in which to render the hand lasers + ) { + return false; + } + + virtual bool suppressKeyboard() { return false; } + virtual void unsuppressKeyboard() {}; + virtual bool isKeyboardVisible() { return false; } +}; + +class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: @@ -117,42 +185,12 @@ public: return QRect(0, 0, recommendedSize.x, recommendedSize.y); } - // Stereo specific methods - virtual glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { - return baseProjection; - } - - virtual glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const { - return baseProjection; - } - - // Fetch the most recently displayed image as a QImage virtual QImage getScreenshot() const = 0; - // HMD specific methods - // TODO move these into another class? - virtual glm::mat4 getEyeToHeadTransform(Eye eye) const { - static const glm::mat4 transform; return transform; - } - // will query the underlying hmd api to compute the most recent head pose virtual bool beginFrameRender(uint32_t frameIndex) { return true; } - // returns a copy of the most recent head pose, computed via updateHeadPose - virtual glm::mat4 getHeadPose() const { - return glm::mat4(); - } - - // Needed for timewarp style features - virtual void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { - // NOOP - } - - virtual float getIPD() const { return AVERAGE_HUMAN_IPD; } - - virtual void abandonCalibration() {} - virtual void resetSensors() {} virtual float devicePixelRatio() { return 1.0f; } // Rate at which we present to the display device virtual float presentRate() const { return -1.0f; } @@ -160,6 +198,7 @@ public: virtual float newFramePresentRate() const { return -1.0f; } // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } + uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) int64_t getPaintDelayUsecs() const; @@ -168,25 +207,6 @@ public: static const QString& MENU_PATH(); - enum Hand { - LeftHand = 0x01, - RightHand = 0x02, - }; - - enum class HandLaserMode { - None, // Render no hand lasers - Overlay, // Render hand lasers only if they intersect with the UI layer, and stop at the UI layer - }; - - virtual bool setHandLaser( - uint32_t hands, // Bits from the Hand enum - HandLaserMode mode, // Mode in which to render - const vec4& color = vec4(1), // The color of the rendered laser - const vec3& direction = vec3(0, 0, -1) // The direction in which to render the hand lasers - ) { - return false; - } - signals: void recommendedFramebufferSizeChanged(const QSize & size); diff --git a/libraries/shared/src/shared/Bilateral.h b/libraries/shared/src/shared/Bilateral.h new file mode 100644 index 0000000000..c4daf60177 --- /dev/null +++ b/libraries/shared/src/shared/Bilateral.h @@ -0,0 +1,49 @@ +// +// Created by Bradley Austin Davis 2015/10/09 +// 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 +// + +#pragma once + +namespace bilateral { + enum class Side { + Left = 0, + Right = 1 + }; + + using Indices = Side; + + enum class Bits { + Left = 0x01, + Right = 0x02 + }; + + inline uint8_t bit(Side side) { + switch (side) { + case Side::Left: + return 0x01; + case Side::Right: + return 0x02; + } + return UINT8_MAX; + } + + inline uint8_t index(Side side) { + switch (side) { + case Side::Left: + return 0; + case Side::Right: + return 1; + } + return UINT8_MAX; + } + + template + void for_each_side(F f) { + f(Side::Left); + f(Side::Right); + } +} diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 705c0a0781..49c14c8d66 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -53,6 +54,13 @@ bool oculusAvailable() { static std::once_flag once; static bool result { false }; std::call_once(once, [&] { + + static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); + static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + if (enableDebugOpenVR) { + return; + } + ovrDetectResult detect = ovr_Detect(0); if (!detect.IsOculusServiceRunning || !detect.IsOculusHMDConnected) { return; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 3a5f027013..acbf386980 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -253,3 +253,27 @@ void OpenVrDisplayPlugin::updatePresentPose() { mat3 presentRotation(_currentPresentFrameInfo.rawPresentPose); _currentPresentFrameInfo.presentReprojection = glm::mat3(glm::inverse(renderRotation) * presentRotation); } + +bool OpenVrDisplayPlugin::suppressKeyboard() { + if (isOpenVrKeyboardShown()) { + return false; + } + if (!_keyboardSupressionCount.fetch_add(1)) { + disableOpenVrKeyboard(); + } + return true; +} + +void OpenVrDisplayPlugin::unsuppressKeyboard() { + if (_keyboardSupressionCount == 0) { + qWarning() << "Attempted to unsuppress a keyboard that was not suppressed"; + return; + } + if (1 == _keyboardSupressionCount.fetch_sub(1)) { + enableOpenVrKeyboard(); + } +} + +bool OpenVrDisplayPlugin::isKeyboardVisible() { + return isOpenVrKeyboardShown(); +} diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index ee693b8091..b22fadb52c 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -30,6 +30,10 @@ public: bool beginFrameRender(uint32_t frameIndex) override; void cycleDebugOutput() override { _lockCurrentTexture = !_lockCurrentTexture; } + bool suppressKeyboard() override; + void unsuppressKeyboard() override; + bool isKeyboardVisible() override; + protected: bool internalActivate() override; void internalDeactivate() override; @@ -39,8 +43,10 @@ protected: bool isHmdMounted() const override; void postPreview() override; + private: vr::IVRSystem* _system { nullptr }; std::atomic _hmdActivityLevel { vr::k_EDeviceActivityLevel_Unknown }; + std::atomic _keyboardSupressionCount{ 0 }; static const QString NAME; }; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 69c98d48c3..631efca4a2 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -240,6 +240,12 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle _poseStateMap.clear(); _buttonPressedMap.clear(); + // While the keyboard is open, we defer strictly to the keyboard values + if (isOpenVrKeyboardShown()) { + _axisStateMap.clear(); + return; + } + PerformanceTimer perfTimer("ViveControllerManager::update"); auto leftHandDeviceIndex = _system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand); From 60035486d3e2835708756e7bdcc5165138f34cff Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 15 Jun 2016 09:35:24 -0700 Subject: [PATCH 0573/1237] Build fix --- libraries/plugins/src/plugins/DisplayPlugin.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 5f659f45f9..a65e8e7371 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -26,8 +26,8 @@ class QImage; enum Eye { - Left = bilateral::Side::Left, - Right = bilateral::Side::Right + Left = (int)bilateral::Side::Left, + Right = (int)bilateral::Side::Right }; /* From 6366ca7508ce77de4230900141298669f3c49fcf Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 15 Jun 2016 11:08:59 -0700 Subject: [PATCH 0574/1237] Go red for full trigger. --- .../system/controllers/handControllerPointer.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 7371680c32..5873040dd9 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -349,7 +349,8 @@ clickMapping.enable(); // Same properties as handControllerGrab search sphere var BALL_SIZE = 0.011; var BALL_ALPHA = 0.5; -var LASER_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: BALL_ALPHA}; +var LASER_SEARCH_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: BALL_ALPHA}; +var LASER_TRIGGER_COLOR_XYZW = {x: 250 / 255, y: 10 / 255, z: 10 / 255, w: BALL_ALPHA}; var fakeProjectionBall = Overlays.addOverlay("sphere", { size: 5 * BALL_SIZE, color: {red: 255, green: 10, blue: 10}, @@ -373,15 +374,24 @@ function clearSystemLaser() { HMD.disableHandLasers(BOTH_HUD_LASERS); systemLaserOn = false; } +function setColoredLaser() { // answer trigger state if lasers supported, else falsey. + var color = (activeTrigger.state === 'full') ? LASER_TRIGGER_COLOR_XYZW : LASER_SEARCH_COLOR_XYZW; + return HMD.setHandLasers(activeHudLaser, true, color, SYSTEM_LASER_DIRECTION) && activeTrigger.state; + +} function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD if (!optionalEnableClicks) { expireMouseCursor(); clearSystemLaser(); - } else if (!systemLaserOn && activeTrigger.state) { + } else if (activeTrigger.state && (!systemLaserOn || (systemLaserOn !== activeTrigger.state))) { // last=>wrong color // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. - systemLaserOn = HMD.setHandLasers(activeHudLaser, true, LASER_COLOR_XYZW, SYSTEM_LASER_DIRECTION); + systemLaserOn = setColoredLaser(); Reticle.visible = !systemLaserOn; + } else if ((systemLaserOn || Reticle.visible) && !activeTrigger.state) { + clearSystemLaser(); + Reticle.visible = false; } + if (!visualizationIsShowing) { return; } From bc838ac0b476f586723ecf81a8e8cb86d647e8d6 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 15 Jun 2016 11:41:28 -0700 Subject: [PATCH 0575/1237] Fix vive json --- interface/resources/controllers/vive.json | 5 ++--- interface/src/scripting/HMDScriptingInterface.h | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 677ec1ccfc..6be672900a 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -3,11 +3,10 @@ "channels": [ { "from": "Vive.LY", "when": "Vive.LSY", "filters": ["invert"], "to": "Standard.LY" }, { "from": "Vive.LX", "when": "Vive.LSX", "to": "Standard.LX" }, - { "from": "Vive.LT", "to": "Standard.LT", "filters": [ - { "type": "deadZone", "min": 0.05 }, + { "type": "deadZone", "min": 0.05 } ] }, { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, @@ -19,7 +18,7 @@ { "from": "Vive.RT", "to": "Standard.RT", "filters": [ - { "type": "deadZone", "min": 0.05 }, + { "type": "deadZone", "min": 0.05 } ] }, { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index e2985b1ce5..2739522adf 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -33,12 +33,13 @@ public: Q_INVOKABLE glm::vec3 calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const; Q_INVOKABLE glm::vec2 overlayFromWorldPoint(const glm::vec3& position) const; Q_INVOKABLE glm::vec3 worldPointFromOverlay(const glm::vec2& overlay) const; - Q_INVOKABLE glm::vec2 sphericalToOverlay(const glm::vec2 & sphericalPos) const; Q_INVOKABLE glm::vec2 overlayToSpherical(const glm::vec2 & overlayPos) const; Q_INVOKABLE QString preferredAudioInput() const; Q_INVOKABLE QString preferredAudioOutput() const; + Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; + Q_INVOKABLE void disableHandLasers(int hands) const; /// Suppress the activation of any on-screen keyboard so that a script operation will /// not be interrupted by a keyboard popup From 2c7642a3674aa100e46c2dd4a6ba2dc6f6ba5e80 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 15 Jun 2016 12:28:42 -0700 Subject: [PATCH 0576/1237] Do not make ReticleClick when we enter an overlay with the trigger pressed. --- .../controllers/handControllerPointer.js | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 7046ed16a5..2faf2cb361 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -48,9 +48,10 @@ function TimeLock(expiration) { } var handControllerLockOut = new TimeLock(2000); -function Trigger() { +function Trigger(label) { // This part is copied and adapted from handControllerGrab.js. Maybe we should refactor this. var that = this; + that.label = label; that.TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing that.TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab that.TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab @@ -301,8 +302,8 @@ setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // CONTROLLER MAPPING --------- // -var leftTrigger = new Trigger(); -var rightTrigger = new Trigger(); +var leftTrigger = new Trigger('left'); +var rightTrigger = new Trigger('right'); var activeTrigger = rightTrigger; var activeHand = Controller.Standard.RightHand; var LEFT_HUD_LASER = 1; @@ -336,8 +337,26 @@ Script.scriptEnding.connect(clickMapping.disable); clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); // Full smoothed trigger is a click. -clickMapping.from(rightTrigger.full).when(isPointingAtOverlay).to(Controller.Actions.ReticleClick); -clickMapping.from(leftTrigger.full).when(isPointingAtOverlay).to(Controller.Actions.ReticleClick); +function isPointingAtOverlayStartedNonFullTrigger(trigger) { + // true if isPointingAtOverlay AND we were NOT full triggered when we became so. + // The idea is to not count clicks when we're full-triggering and reach the edge of a window. + var lockedIn = false; + return function () { + if (trigger !== activeTrigger) { + return lockedIn = false; + } + if (!isPointingAtOverlay()) { + return lockedIn = false; + } + if (lockedIn) { + return true; + } + lockedIn = !trigger.full(); + return lockedIn; + } +} +clickMapping.from(rightTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(rightTrigger)).to(Controller.Actions.ReticleClick); +clickMapping.from(leftTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(leftTrigger)).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); // Partial smoothed trigger is activation. From 775656eb1d8e5daaabc8e0861738c7ee06d658bc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 16 Jun 2016 08:48:30 +1200 Subject: [PATCH 0577/1237] Delete comment --- libraries/ui/src/FileDialogHelper.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/ui/src/FileDialogHelper.cpp b/libraries/ui/src/FileDialogHelper.cpp index 9840e55927..c9a8953f01 100644 --- a/libraries/ui/src/FileDialogHelper.cpp +++ b/libraries/ui/src/FileDialogHelper.cpp @@ -95,8 +95,6 @@ bool FileDialogHelper::urlIsWritable(const QUrl& url) { // No file, get the parent directory and check if writable return QFileInfo(fileInfo.absoluteDir().absolutePath()).isWritable(); - // Note: Does not correctly detect on Windows that the OS drive root (i.e., C:/) cannot be written to. This even if turn on - // NTFS permissions checking. } QStringList FileDialogHelper::drives() { From 1e3b20b66d9445f49368573902f29fd26cc5df8b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 15 Jun 2016 15:06:36 -0700 Subject: [PATCH 0578/1237] Explicit check enumerated states instead of relying on order, per feedback. --- scripts/system/controllers/handControllerGrab.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 25889c9c1c..ea2c4c1653 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -361,8 +361,9 @@ function MyController(hand) { var _this = this; + var suppressedIn2D = [STATE_OFF, STATE_SEARCHING, STATE_HOLD_SEARCHING]; this.ignoreInput = function () { - return (_this.state <= STATE_HOLD_SEARCHING) && isIn2DMode(); + return (-1 !== suppressedIn2D.indexOf(_this.state)) && isIn2DMode(); }; this.update = function() { From e31a3b4d08990cc8ce5fc23706e777eafe66d822 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 15 Jun 2016 15:13:51 -0700 Subject: [PATCH 0579/1237] After trying stop trying to expose a generic [] operator on Varying and Containers --- .../render-utils/src/DebugDeferredBuffer.cpp | 4 +- .../render-utils/src/DebugDeferredBuffer.h | 2 +- .../render-utils/src/SubsurfaceScattering.cpp | 6 +- .../render-utils/src/SubsurfaceScattering.h | 2 +- .../render-utils/src/SurfaceGeometryPass.cpp | 4 +- .../render-utils/src/SurfaceGeometryPass.h | 2 +- libraries/render/src/render/BlurTask.cpp | 4 +- libraries/render/src/render/BlurTask.h | 2 +- libraries/render/src/render/Task.cpp | 8 +- libraries/render/src/render/Task.h | 79 +++++++------------ 10 files changed, 46 insertions(+), 67 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 076f4a3880..844eaedbb9 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -342,8 +342,8 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; - auto& diffusedCurvatureFramebuffer = inputs.getFirst(); - auto& scatteringFramebuffer = inputs.getSecond(); + auto& diffusedCurvatureFramebuffer = inputs.get0(); + auto& scatteringFramebuffer = inputs.get1(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 095e0ab9cc..e1d76e1f71 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -34,7 +34,7 @@ signals: class DebugDeferredBuffer { public: - using Inputs = render::VaryingPair; + using Inputs = render::VaryingSet2; using Config = DebugDeferredBufferConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index cab12e7377..79527d36e1 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -171,9 +171,9 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, auto pipeline = getScatteringPipeline(); - auto& frameTransform = inputs.getFirst(); - auto& curvatureFramebuffer = inputs.getSecond(); - auto& diffusedFramebuffer = inputs.getThird(); + auto& frameTransform = inputs.get0(); + auto& curvatureFramebuffer = inputs.get1(); + auto& diffusedFramebuffer = inputs.get2(); auto framebufferCache = DependencyManager::get(); diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 61bccab48f..4322379e57 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -48,7 +48,7 @@ signals: class SubsurfaceScattering { public: - using Inputs = render::VaryingTrio; + using Inputs = render::VaryingSet3; using Config = SubsurfaceScatteringConfig; using JobModel = render::Job::ModelIO; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 9e5d455725..00fce50bb4 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -60,8 +60,8 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); - curvatureAndDepth.first. template edit() = curvatureFBO; - curvatureAndDepth.second. template edit() = pyramidTexture; + curvatureAndDepth.edit0() = curvatureFBO; + curvatureAndDepth.edit1() = pyramidTexture; auto curvatureTexture = framebufferCache->getCurvatureTexture(); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index bfad811340..b2d72891bd 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -40,7 +40,7 @@ signals: class SurfaceGeometryPass { public: - using Outputs = render::VaryingPair; + using Outputs = render::VaryingSet2; using Config = SurfaceGeometryPassConfig; using JobModel = render::Job::ModelIO; diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 53129f16ba..1d5fe18f4a 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -306,8 +306,8 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const RenderArgs* args = renderContext->args; - auto& sourceFramebuffer = SourceAndDepth.getFirst(); - auto& depthTexture = SourceAndDepth.getSecond(); + auto& sourceFramebuffer = SourceAndDepth.get0(); + auto& depthTexture = SourceAndDepth.get1(); BlurInOutResource::Resources blurringResources; if (!_inOutResources.updateResources(sourceFramebuffer, blurringResources)) { diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 333cd22f8e..aea596cc2f 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -121,7 +121,7 @@ protected: class BlurGaussianDepthAware { public: - using Inputs = VaryingPair; + using Inputs = VaryingSet2; using Config = BlurGaussianDepthAwareConfig; using JobModel = Job::ModelIO; diff --git a/libraries/render/src/render/Task.cpp b/libraries/render/src/render/Task.cpp index a180e7ff8e..8727923c70 100644 --- a/libraries/render/src/render/Task.cpp +++ b/libraries/render/src/render/Task.cpp @@ -27,14 +27,14 @@ void TaskConfig::refresh() { namespace render{ - template <> void varyingGet(const VaryingPairBase* data, uint8_t index, Varying& var) { + template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var) { if (index == 0) { - var = data->first; + var = data.first; } else { - var = data->second; + var = data.second; } } - template <> uint8_t varyingLength(const VaryingPairBase* data) { return 2; } + template <> uint8_t varyingLength(const VaryingPairBase& data) { return 2; } } \ No newline at end of file diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 947f1a3da0..a496e1d876 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -30,9 +30,11 @@ namespace render { class Varying; - - template void varyingGet(const T* data, uint8_t index, Varying& var) {} -template uint8_t varyingLength(const T* data) { return 0; } + + + +template < class T > void varyingGet(const T& data, uint8_t index, Varying& var) {} +template uint8_t varyingLength(const T& data) { return 0; } // A varying piece of data, to be used as Job/Task I/O // TODO: Task IO @@ -52,7 +54,7 @@ public: // access potential sub varyings contained in this one. Varying operator[] (uint8_t index) const { return (*_concept)[index]; } - uint8_t length() const { return _concept->length(); } + uint8_t length() const { return (*_concept).length(); } protected: class Concept { @@ -65,19 +67,16 @@ protected: template class Model : public Concept { public: using Data = T; - // using VarContainer = std::enable_if; - //using VarContainer = std::conditional, T::VarContainer, T>; Model(const Data& data) : _data(data) {} virtual ~Model() = default; - virtual Varying operator[] (uint8_t index) const { Varying var; - // varyingGet(&_data, index, var); + varyingGet< T >(_data, index, var); return var; } - virtual uint8_t length() const { return varyingLength(&_data); } + virtual uint8_t length() const { return varyingLength(_data); } Data _data; }; @@ -85,67 +84,47 @@ protected: std::shared_ptr _concept; }; - - - using VaryingPairBase = std::pair; - -template <> void varyingGet(const VaryingPairBase* data, uint8_t index, Varying& var); -template <> uint8_t varyingLength(const VaryingPairBase* data); +template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var); +template <> uint8_t varyingLength(const VaryingPairBase& data); template < typename T0, typename T1 > -class VaryingPair : public VaryingPairBase { +class VaryingSet2 : public VaryingPairBase { public: using Parent = VaryingPairBase; - using VarContainer = VaryingPairBase; + typedef void is_proxy_tag; - VaryingPair() : Parent(Varying(T0()), Varying(T1())) {} - VaryingPair(const VaryingPair& pair) : Parent(pair.first, pair.second) {} - VaryingPair(const Varying& first, const Varying& second) : Parent(first, second) {} + VaryingSet2() : Parent(Varying(T0()), Varying(T1())) {} + VaryingSet2(const VaryingSet2& pair) : Parent(pair.first, pair.second) {} + VaryingSet2(const Varying& first, const Varying& second) : Parent(first, second) {} - const T0& getFirst() const { return first.get(); } - T0& editFirst() { return first.edit(); } + const T0& get0() const { return first.get(); } + T0& edit0() { return first.edit(); } - const T1& getSecond() const { return second.get(); } - T1& editSecond() { return second.edit(); } + const T1& get1() const { return second.get(); } + T1& edit1() { return second.edit(); } }; template -class VaryingTrio : public std::tuple{ +class VaryingSet3 : public std::tuple{ public: using Parent = std::tuple; - VaryingTrio() : Parent(Varying(T0()), Varying(T1()), Varying(T2())) {} - VaryingTrio(const VaryingTrio& trio) : Parent(std::get<0>(trio), std::get<1>(trio), std::get<2>(trio)) {} - VaryingTrio(const Varying& first, const Varying& second, const Varying& third) : Parent(first, second, third) {} + VaryingSet3() : Parent(Varying(T0()), Varying(T1()), Varying(T2())) {} + VaryingSet3(const VaryingSet3& trio) : Parent(std::get<0>(trio), std::get<1>(trio), std::get<2>(trio)) {} + VaryingSet3(const Varying& first, const Varying& second, const Varying& third) : Parent(first, second, third) {} - const T0& getFirst() const { return std::get<0>((*this)).template get(); } - T0& editFirst() { return std::get<0>((*this)).template edit(); } + const T0& get0() const { return std::get<0>((*this)).template get(); } + T0& edit0() { return std::get<0>((*this)).template edit(); } - const T1& getSecond() const { return std::get<1>((*this)).template get(); } - T1& editSecond() { return std::get<1>((*this)).template edit(); } + const T1& get1() const { return std::get<1>((*this)).template get(); } + T1& edit1() { return std::get<1>((*this)).template edit(); } - const T2& getThird() const { return std::get<2>((*this)).template get(); } - T2& editThird() { return std::get<2>((*this)).template edit(); } + const T2& get2() const { return std::get<2>((*this)).template get(); } + T2& edit2() { return std::get<2>((*this)).template edit(); } }; -/* -template -class VaryingTuple : public std::tuple<_Types>{ -public: - using Parent = std::tuple<_Types>; - - VaryingPair() : Parent(Varying(T0()), Varying(T1())) {} - VaryingPair(const VaryingPair& pair) : Parent(pair.first, pair.second) {} - VaryingPair(const Varying& first, const Varying& second) : Parent(first, second) {} - - const T0& getFirst() const { return first.get(); } - T0& editFirst() { return first.edit(); } - - const T1& getSecond() const { return second.get(); } - T1& editSecond() { return second.edit(); } -};*/ template < class T, int NUM > class VaryingArray : public std::array { From 87dc6649ceacffb819516125af4ee3d284d6a997 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 15 Jun 2016 15:16:29 -0700 Subject: [PATCH 0580/1237] Better track keyboard open state, fix text replacement --- plugins/openvr/src/OpenVrHelpers.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 4b4eef0538..4a756e12b0 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -164,12 +164,13 @@ void finishOpenVrKeyboardInput() { auto offscreenUi = DependencyManager::get(); auto chars = _overlay->GetKeyboardText(textArray, 8192); auto newText = QString(QByteArray(textArray, chars)); - // TODO modify the new text to match the possible input hints: - // ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly - // ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly - QInputMethodEvent event(_existingText, QList()); - event.setCommitString(newText, 0, _existingText.size()); - qApp->sendEvent(_keyboardFocusObject, &event); + _keyboardFocusObject->setProperty("text", newText); + //// TODO modify the new text to match the possible input hints: + //// ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly + //// ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly + //QInputMethodEvent event(_existingText, QList()); + //event.setCommitString(newText, 0, _existingText.size()); + //qApp->sendEvent(_keyboardFocusObject, &event); // Simulate an enter press on the top level window to trigger the action if (0 == (_currentHints & Qt::ImhMultiLine)) { qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n"))); @@ -233,6 +234,11 @@ void handleOpenVrEvents() { case vr::VREvent_KeyboardDone: finishOpenVrKeyboardInput(); + + // FALL THROUGH + case vr::VREvent_KeyboardClosed: + _keyboardFocusObject = nullptr; + _keyboardShown = false; break; default: From 6f671b7cde167005965a47539e048b09bf8f5171 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 15 Jun 2016 15:17:34 -0700 Subject: [PATCH 0581/1237] see comment. --- scripts/system/controllers/handControllerGrab.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ea2c4c1653..316fe0ff88 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -363,15 +363,17 @@ function MyController(hand) { var suppressedIn2D = [STATE_OFF, STATE_SEARCHING, STATE_HOLD_SEARCHING]; this.ignoreInput = function () { - return (-1 !== suppressedIn2D.indexOf(_this.state)) && isIn2DMode(); + // We've made the decision to use 'this' for new code, even though it is fragile, + // in order to keep/ the code uniform without making any no-op line changes. + return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; this.update = function() { this.updateSmoothedTrigger(); - if (_this.ignoreInput()) { - _this.turnOffVisualizations(); + if (this.ignoreInput()) { + this.turnOffVisualizations(); return; } From 59b785a33bbfcc818a8597b262dec15749b83847 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 15 Jun 2016 16:55:34 -0700 Subject: [PATCH 0582/1237] trying to debug --- libraries/animation/src/Rig.cpp | 4 +--- libraries/avatars/src/AvatarData.cpp | 27 ++++++++++++++++++--------- libraries/avatars/src/AvatarData.h | 2 +- libraries/render-utils/src/Model.cpp | 3 +++ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b21f5a0e84..8f0bd3fd87 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -163,7 +163,6 @@ void Rig::destroyAnimGraph() { } void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset) { - _geometryOffset = AnimPose(geometry.offset); _invGeometryOffset = _geometryOffset.inverse(); setModelOffset(modelOffset); @@ -1224,8 +1223,7 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { } void Rig::copyJointsFromJointData(const QVector& jointDataVec) { - - if (_animSkeleton) { + if (_animSkeleton && jointDataVec.size() == _internalPoseSet._overrideFlags.size()) { // transform all the default poses into rig space. const AnimPose geometryToRigPose(_geometryToRigTransform); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 26bf0fb2a2..0ae58ca1de 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -261,7 +261,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { unsigned char validity = 0; int validityBit = 0; - #ifdef WANT_DEBUG + #if 1 int rotationSentCount = 0; unsigned char* beforeRotations = destinationBuffer; #endif @@ -276,7 +276,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= AVATAR_MIN_ROTATION_DOT) { if (data.rotationSet) { validity |= (1 << validityBit); - #ifdef WANT_DEBUG + #if 1 rotationSentCount++; #endif } @@ -310,7 +310,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { validity = 0; validityBit = 0; - #ifdef WANT_DEBUG + #if 1 int translationSentCount = 0; unsigned char* beforeTranslations = destinationBuffer; #endif @@ -324,7 +324,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { glm::distance(data.translation, _lastSentJointData[i].translation) > AVATAR_MIN_TRANSLATION) { if (data.translationSet) { validity |= (1 << validityBit); - #ifdef WANT_DEBUG + #if 1 translationSentCount++; #endif maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); @@ -359,7 +359,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { } } - #ifdef WANT_DEBUG + #if 1 if (sendAll) { qDebug() << "AvatarData::toByteArray" << cullSmallChanges << sendAll << "rotations:" << rotationSentCount << "translations:" << translationSentCount @@ -584,6 +584,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); _hasNewJointRotations = true; data.rotationSet = true; + } else if (numValidJointRotations == numJoints) { + _hasNewJointRotations = true; + data.rotationSet = false; } } @@ -620,10 +623,13 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); _hasNewJointTranslations = true; data.translationSet = true; + } else if (numValidJointRotations == numJoints) { + _hasNewJointTranslations = true; + data.translationSet = false; } } - #ifdef WANT_DEBUG + #if 1 if (numValidJointRotations > 15) { qDebug() << "RECEIVING -- rotations:" << numValidJointRotations << "translations:" << numValidJointTranslations @@ -1052,17 +1058,20 @@ void AvatarData::setJointMappingsFromNetworkReply() { _jointIndices.insert(_jointNames.at(i), i + 1); } - sendIdentityPacket(); + //sendIdentityPacket(); + //sendAvatarDataPacket(true); + qDebug() << "set joint mapping froms network reply, num joints: " << _jointIndices.size(); networkReply->deleteLater(); } -void AvatarData::sendAvatarDataPacket() { +void AvatarData::sendAvatarDataPacket(bool sendFull) { auto nodeList = DependencyManager::get(); // about 2% of the time, we send a full update (meaning, we transmit all the joint data), even if nothing has changed. // this is to guard against a joint moving once, the packet getting lost, and the joint never moving again. - bool sendFullUpdate = randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO; + bool sendFullUpdate = sendFull || (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); + if (sendFullUpdate) qDebug() << sendFullUpdate; QByteArray avatarByteArray = toByteArray(true, sendFullUpdate); doneEncoding(true); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 61ee649273..7fe339c2b9 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -352,7 +352,7 @@ public: AvatarEntityIDs getAndClearRecentlyDetachedIDs(); public slots: - void sendAvatarDataPacket(); + void sendAvatarDataPacket(bool sendFull = false); void sendIdentityPacket(); void setJointMappingsFromNetworkReply(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ded1184c24..2112e64a39 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -254,6 +254,7 @@ bool Model::updateGeometry() { _needsReload = false; if (_rig->jointStatesEmpty() && getFBXGeometry().joints.size() > 0) { + qDebug() << "initJointStates, num joints: " << getFBXGeometry().joints.size(); initJointStates(); const FBXGeometry& fbxGeometry = getFBXGeometry(); @@ -817,7 +818,9 @@ void Model::setURL(const QUrl& url) { invalidCalculatedMeshBoxes(); deleteGeometry(); + if (_geometry && _geometry->getGeometry()) qDebug() << "geometry1: " << _geometry->getGeometry()->getGeometry().joints.size(); _geometry = DependencyManager::get()->getGeometry(url); + if (_geometry && _geometry->getGeometry()) qDebug() << "geometry2: " << _geometry->getGeometry()->getGeometry().joints.size(); onInvalidate(); } From 6ad1008a566f179e7752af7456c5d3a4a3e55efa Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 15 Jun 2016 13:42:48 -0700 Subject: [PATCH 0583/1237] Allow explicit overlay alpha from scripts --- .../scripting/DesktopScriptingInterface.cpp | 6 +++++ .../src/scripting/DesktopScriptingInterface.h | 2 ++ .../src/display-plugins/CompositorHelper.h | 6 +++++ .../display-plugins/OpenGLDisplayPlugin.cpp | 27 +++++++++++++++++++ .../src/display-plugins/OpenGLDisplayPlugin.h | 8 +++++- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 9 ++++++- .../oculus/src/OculusBaseDisplayPlugin.cpp | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 3 ++- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 2 +- 9 files changed, 60 insertions(+), 5 deletions(-) diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index 843a40348e..f7bc8afe36 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -16,6 +16,7 @@ #include "Application.h" #include "MainWindow.h" +#include int DesktopScriptingInterface::getWidth() { QSize size = qApp->getWindow()->windowHandle()->screen()->virtualSize(); @@ -25,3 +26,8 @@ int DesktopScriptingInterface::getHeight() { QSize size = qApp->getWindow()->windowHandle()->screen()->virtualSize(); return size.height(); } + +void DesktopScriptingInterface::setOverlayAlpha(float alpha) { + qApp->getApplicationCompositor().setAlpha(alpha); +} + diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index be4eaadbfb..8da502cb11 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -22,6 +22,8 @@ class DesktopScriptingInterface : public QObject, public Dependency { Q_PROPERTY(int height READ getHeight) // Physical height of screen(s) including task bars and system menus public: + Q_INVOKABLE void setOverlayAlpha(float alpha); + int getWidth(); int getHeight(); }; diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index 868beec53f..2a3dd0c852 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -38,6 +38,7 @@ const float MAGNIFY_MULT = 2.0f; class CompositorHelper : public QObject, public Dependency { Q_OBJECT + Q_PROPERTY(float alpha READ getAlpha WRITE setAlpha NOTIFY alphaChanged) Q_PROPERTY(bool reticleOverDesktop READ getReticleOverDesktop WRITE setReticleOverDesktop) public: static const uvec2 VIRTUAL_SCREEN_SIZE; @@ -74,6 +75,9 @@ public: void setModelTransform(const Transform& transform) { _modelTransform = transform; } const Transform& getModelTransform() const { return _modelTransform; } + float getAlpha() const { return _alpha; } + void setAlpha(float alpha) { if (alpha != _alpha) { emit alphaChanged(); _alpha = alpha; } } + bool getReticleVisible() const { return _reticleVisible; } void setReticleVisible(bool visible) { _reticleVisible = visible; } @@ -109,6 +113,7 @@ public: signals: void allowMouseCaptureChanged(); + void alphaChanged(); protected slots: void sendFakeMouseEvent(); @@ -134,6 +139,7 @@ private: float _textureFov { VIRTUAL_UI_TARGET_FOV.y }; float _textureAspectRatio { VIRTUAL_UI_ASPECT_RATIO }; + float _alpha { 1.0f }; float _hmdUIRadius { 1.0f }; int _previousBorderWidth { -1 }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 23c836b3de..b49b41d8b2 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -273,11 +273,23 @@ bool OpenGLDisplayPlugin::activate() { _container->makeRenderingContextCurrent(); #endif + auto compositorHelper = DependencyManager::get(); + connect(compositorHelper.data(), &CompositorHelper::alphaChanged, [this] { + auto compositorHelper = DependencyManager::get(); + auto animation = new QPropertyAnimation(this, "overlayAlpha"); + animation->setDuration(200); + animation->setEndValue(compositorHelper->getAlpha()); + animation->start(); + }); + return DisplayPlugin::activate(); } void OpenGLDisplayPlugin::deactivate() { + auto compositorHelper = DependencyManager::get(); + disconnect(compositorHelper.data()); + #if THREADED_PRESENT auto presentThread = DependencyManager::get(); // Does not return until the GL transition has completeed @@ -445,6 +457,8 @@ void OpenGLDisplayPlugin::compositeOverlay() { auto compositorHelper = DependencyManager::get(); useProgram(_program); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); // check the alpha // Overlay draw if (isStereo()) { @@ -458,6 +472,8 @@ void OpenGLDisplayPlugin::compositeOverlay() { Uniform(*_program, _mvpUniform).Set(mat4()); drawUnitQuad(); } + // restore the alpha + Uniform(*_program, _alphaUniform).Set(1.0); } void OpenGLDisplayPlugin::compositePointer() { @@ -465,6 +481,8 @@ void OpenGLDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); useProgram(_program); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); if (isStereo()) { for_each_eye([&](Eye eye) { @@ -475,6 +493,8 @@ void OpenGLDisplayPlugin::compositePointer() { drawUnitQuad(); } Uniform(*_program, _mvpUniform).Set(mat4()); + // restore the alpha + Uniform(*_program, _alphaUniform).Set(1.0); } void OpenGLDisplayPlugin::compositeScene() { @@ -682,3 +702,10 @@ void OpenGLDisplayPlugin::assertIsRenderThread() const { void OpenGLDisplayPlugin::assertIsPresentThread() const { Q_ASSERT(QThread::currentThread() == _presentThread); } + +bool OpenGLDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + withRenderThreadLock([&] { + _compositeOverlayAlpha = _overlayAlpha; + }); + return Parent::beginFrameRender(frameIndex); +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 69653b8c76..d8d4d8ff8c 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -24,6 +24,9 @@ #define THREADED_PRESENT 1 class OpenGLDisplayPlugin : public DisplayPlugin { + Q_OBJECT + Q_PROPERTY(float overlayAlpha MEMBER _overlayAlpha) + using Parent = DisplayPlugin; protected: using Mutex = std::mutex; using Lock = std::unique_lock; @@ -60,6 +63,7 @@ public: float droppedFrameRate() const override; + bool beginFrameRender(uint32_t frameIndex) override; protected: #if THREADED_PRESENT friend class PresentThread; @@ -115,7 +119,8 @@ protected: RateCounter<> _presentRate; QMap _sceneTextureToFrameIndexMap; uint32_t _currentPresentFrameIndex { 0 }; - + float _compositeOverlayAlpha{ 1.0f }; + gpu::TexturePointer _currentSceneTexture; gpu::TexturePointer _currentOverlayTexture; @@ -157,6 +162,7 @@ private: // be serialized through this mutex mutable Mutex _presentMutex; ProgramPtr _activeProgram; + float _overlayAlpha{ 1.0f }; }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 6801d94f2f..efdb68226b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -287,6 +287,8 @@ void HmdDisplayPlugin::compositeOverlay() { glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); useProgram(_program); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); _sphereSection->Use(); for_each_eye([&](Eye eye) { eyeViewport(eye); @@ -295,6 +297,8 @@ void HmdDisplayPlugin::compositeOverlay() { Uniform(*_program, _mvpUniform).Set(mvp); _sphereSection->Draw(); }); + // restore the alpha + Uniform(*_program, _alphaUniform).Set(1.0); } void HmdDisplayPlugin::compositePointer() { @@ -302,8 +306,9 @@ void HmdDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); - // check the alpha useProgram(_program); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); // Mouse pointer _plane->Use(); @@ -317,6 +322,8 @@ void HmdDisplayPlugin::compositePointer() { Uniform(*_program, _mvpUniform).Set(mvp); _plane->Draw(); }); + // restore the alpha + Uniform(*_program, _alphaUniform).Set(1.0); } diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index e188bea52e..e26a48b89c 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -45,7 +45,7 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _handPoses = handPoses; _frameInfos[frameIndex] = _currentRenderFrameInfo; }); - return true; + return Parent::beginFrameRender(frameIndex); } bool OculusBaseDisplayPlugin::isSupported() const { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 8d2bc24177..699891deaa 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -41,6 +41,7 @@ void OculusLegacyDisplayPlugin::resetSensors() { } bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + _currentRenderFrameInfo = FrameInfo(); _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _trackingState = ovrHmd_GetTrackingState(_hmd, _currentRenderFrameInfo.predictedDisplayTime); @@ -48,7 +49,7 @@ bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { withRenderThreadLock([&]{ _frameInfos[frameIndex] = _currentRenderFrameInfo; }); - return true; + return Parent::beginFrameRender(frameIndex); } bool OculusLegacyDisplayPlugin::isSupported() const { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index acbf386980..a92c930cbf 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -204,7 +204,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _handPoses = handPoses; _frameInfos[frameIndex] = _currentRenderFrameInfo; }); - return true; + return Parent::beginFrameRender(frameIndex); } void OpenVrDisplayPlugin::hmdPresent() { From 33e9caa63687f0682d3eff619e963b8952a23b2a Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 15 Jun 2016 17:20:18 -0700 Subject: [PATCH 0584/1237] Simplify overlay conductor, menu interation --- .../resources/qml/hifi/ToggleHudButton.qml | 4 +- interface/src/Application.cpp | 13 +- interface/src/ui/OverlayConductor.cpp | 112 +++++------------- interface/src/ui/OverlayConductor.h | 23 +--- 4 files changed, 42 insertions(+), 110 deletions(-) diff --git a/interface/resources/qml/hifi/ToggleHudButton.qml b/interface/resources/qml/hifi/ToggleHudButton.qml index 6454f42305..63c056b352 100644 --- a/interface/resources/qml/hifi/ToggleHudButton.qml +++ b/interface/resources/qml/hifi/ToggleHudButton.qml @@ -16,6 +16,8 @@ Window { height: 50 clip: true visible: true + // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' + activator: Item {} Item { width: 50 @@ -28,7 +30,7 @@ Window { MouseArea { readonly property string overlayMenuItem: "Overlays" anchors.fill: parent - onClicked: MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)) + onClicked: MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)); } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ec86f5f1e0..29238cf153 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2716,7 +2716,6 @@ void Application::idle(float nsecsElapsed) { if (firstIdle) { firstIdle = false; connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); - _overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays)); } PROFILE_RANGE(__FUNCTION__); @@ -3217,13 +3216,13 @@ void Application::updateThreads(float deltaTime) { } void Application::toggleOverlays() { - auto newOverlaysVisible = !_overlayConductor.getEnabled(); - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, newOverlaysVisible); - _overlayConductor.setEnabled(newOverlaysVisible); + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, menu->isOptionChecked(MenuOption::Overlays)); } void Application::setOverlaysVisible(bool visible) { - _overlayConductor.setEnabled(visible); + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, true); } void Application::cycleCamera() { @@ -5306,9 +5305,7 @@ void Application::readArgumentsFromLocalSocket() const { } void Application::showDesktop() { - if (!_overlayConductor.getEnabled()) { - _overlayConductor.setEnabled(true); - } + Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, true); } CompositorHelper& Application::getApplicationCompositor() const { diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index a897d85472..a455147bb8 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -99,104 +99,50 @@ void OverlayConductor::centerUI() { qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); } -bool OverlayConductor::userWishesToHide() const { - // user pressed toggle button. - return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) != _prevOverlayMenuChecked && Menu::getInstance()->isOptionChecked(MenuOption::Overlays); -} - -bool OverlayConductor::userWishesToShow() const { - // user pressed toggle button. - return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) != _prevOverlayMenuChecked && !Menu::getInstance()->isOptionChecked(MenuOption::Overlays); -} - -void OverlayConductor::setState(State state) { -#ifdef WANT_DEBUG - static QString stateToString[NumStates] = { "Enabled", "DisabledByDrive", "DisabledByHead", "DisabledByToggle" }; - qDebug() << "OverlayConductor " << stateToString[state] << "<--" << stateToString[_state]; -#endif - _state = state; -} - -OverlayConductor::State OverlayConductor::getState() const { - return _state; -} - void OverlayConductor::update(float dt) { + auto offscreenUi = DependencyManager::get(); + bool currentVisible = !offscreenUi->getDesktop()->property("pinned").toBool(); MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - - // centerUI when hmd mode is first enabled - if (qApp->isHMDMode() && !_hmdMode) { + // centerUI when hmd mode is first enabled and mounted + if (qApp->isHMDMode() && qApp->getActiveDisplayPlugin()->isDisplayVisible() && !_hmdMode) { + _hmdMode = true; centerUI(); + } else { + _hmdMode = false; } - _hmdMode = qApp->isHMDMode(); bool prevDriving = _currentDriving; bool isDriving = updateAvatarHasDriveInput(); bool drivingChanged = prevDriving != isDriving; - bool isAtRest = updateAvatarIsAtRest(); - switch (getState()) { - case Enabled: - if (myAvatar->getClearOverlayWhenDriving() && qApp->isHMDMode() && headOutsideOverlay()) { - setState(DisabledByHead); - setEnabled(false); - } - if (userWishesToHide()) { - setState(DisabledByToggle); - setEnabled(false); - } - if (myAvatar->getClearOverlayWhenDriving() && drivingChanged && isDriving) { - setState(DisabledByDrive); - setEnabled(false); - } - break; - case DisabledByDrive: - if (!isDriving || userWishesToShow()) { - setState(Enabled); - setEnabled(true); - } - break; - case DisabledByHead: - if (isAtRest || userWishesToShow()) { - setState(Enabled); - setEnabled(true); - } - break; - case DisabledByToggle: - if (userWishesToShow()) { - setState(Enabled); - setEnabled(true); - } - break; - default: - break; + if (_flags & SuppressedByDrive) { + if (!isDriving) { + _flags &= ~SuppressedByDrive; + } + } else { + if (myAvatar->getClearOverlayWhenDriving() && drivingChanged && isDriving) { + _flags |= SuppressedByDrive; + } } - _prevOverlayMenuChecked = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); -} - -void OverlayConductor::setEnabled(bool enabled) { - if (enabled == _enabled) { - return; + if (_flags & SuppressedByHead) { + if (isAtRest) { + _flags &= ~SuppressedByHead; + } + } else { + if (_hmdMode && headOutsideOverlay()) { + _flags |= SuppressedByHead; + } } - _enabled = enabled; // set the new value - auto offscreenUi = DependencyManager::get(); - offscreenUi->setPinned(!_enabled); - // ensure that the the state of the menu item reflects the state of the overlay. - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, _enabled); - _prevOverlayMenuChecked = _enabled; - - // if the new state is visible/enabled... - if (_enabled && qApp->isHMDMode()) { - centerUI(); + bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); + if (targetVisible != currentVisible) { + offscreenUi->setPinned(!targetVisible); + if (targetVisible && _hmdMode) { + centerUI(); + } } } - -bool OverlayConductor::getEnabled() const { - return _enabled; -} - diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 375b2652f6..1bdfe2ed79 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -17,33 +17,20 @@ public: ~OverlayConductor(); void update(float dt); - void setEnabled(bool enable); - bool getEnabled() const; - void centerUI(); private: bool headOutsideOverlay() const; bool updateAvatarHasDriveInput(); bool updateAvatarIsAtRest(); - bool userWishesToHide() const; - bool userWishesToShow() const; - enum State { - Enabled = 0, - DisabledByDrive, - DisabledByHead, - DisabledByToggle, - NumStates + enum SupressionFlags { + SuppressedByDrive = 0x01, + SuppressedByHead = 0x02, + SuppressMask = 0x03, }; - void setState(State state); - State getState() const; - - State _state { DisabledByDrive }; - - bool _prevOverlayMenuChecked { true }; - bool _enabled { false }; + uint8_t _flags { SuppressedByDrive }; bool _hmdMode { false }; // used by updateAvatarHasDriveInput From 067539ff6d3bb1de82ba5ebd79542e79228ac767 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 15 Jun 2016 17:44:58 -0700 Subject: [PATCH 0585/1237] Fix overlay stuck to face --- interface/src/ui/OverlayConductor.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index a455147bb8..74e27122e4 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -105,9 +105,11 @@ void OverlayConductor::update(float dt) { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); // centerUI when hmd mode is first enabled and mounted - if (qApp->isHMDMode() && qApp->getActiveDisplayPlugin()->isDisplayVisible() && !_hmdMode) { - _hmdMode = true; - centerUI(); + if (qApp->isHMDMode() && qApp->getActiveDisplayPlugin()->isDisplayVisible()) { + if (!_hmdMode) { + _hmdMode = true; + centerUI(); + } } else { _hmdMode = false; } From f02ffa92fdb03b8a6afd85e95761c62843b113f9 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 15 Jun 2016 18:00:08 -0700 Subject: [PATCH 0586/1237] undo debugging --- libraries/avatars/src/AvatarData.cpp | 33 ++++++++++++++-------------- libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 0ae58ca1de..c98fec0d7d 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -261,7 +261,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { unsigned char validity = 0; int validityBit = 0; - #if 1 + #ifdef WANT_DEBUG int rotationSentCount = 0; unsigned char* beforeRotations = destinationBuffer; #endif @@ -276,7 +276,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= AVATAR_MIN_ROTATION_DOT) { if (data.rotationSet) { validity |= (1 << validityBit); - #if 1 + #ifdef WANT_DEBUG rotationSentCount++; #endif } @@ -310,7 +310,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { validity = 0; validityBit = 0; - #if 1 + #ifdef WANT_DEBUG int translationSentCount = 0; unsigned char* beforeTranslations = destinationBuffer; #endif @@ -324,7 +324,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { glm::distance(data.translation, _lastSentJointData[i].translation) > AVATAR_MIN_TRANSLATION) { if (data.translationSet) { validity |= (1 << validityBit); - #if 1 + #ifdef WANT_DEBUG translationSentCount++; #endif maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); @@ -359,7 +359,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { } } - #if 1 + #ifdef WANT_DEBUG if (sendAll) { qDebug() << "AvatarData::toByteArray" << cullSmallChanges << sendAll << "rotations:" << rotationSentCount << "translations:" << translationSentCount @@ -575,6 +575,14 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } } + // If all the rotations were sent, this is a full packet + bool fullPacket = numValidJointRotations == numJoints; + + if (fullPacket) { + _hasNewJointRotations = true; + _hasNewJointTranslations = true; + } + // each joint rotation is stored in 6 bytes. const int COMPRESSED_QUATERNION_SIZE = 6; PACKET_READ_CHECK(JointRotations, numValidJointRotations * COMPRESSED_QUATERNION_SIZE); @@ -584,9 +592,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, data.rotation); _hasNewJointRotations = true; data.rotationSet = true; - } else if (numValidJointRotations == numJoints) { - _hasNewJointRotations = true; - data.rotationSet = false; } } @@ -623,13 +628,10 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); _hasNewJointTranslations = true; data.translationSet = true; - } else if (numValidJointRotations == numJoints) { - _hasNewJointTranslations = true; - data.translationSet = false; } } - #if 1 + #ifdef WANT_DEBUG if (numValidJointRotations > 15) { qDebug() << "RECEIVING -- rotations:" << numValidJointRotations << "translations:" << numValidJointTranslations @@ -1058,20 +1060,17 @@ void AvatarData::setJointMappingsFromNetworkReply() { _jointIndices.insert(_jointNames.at(i), i + 1); } - //sendIdentityPacket(); - //sendAvatarDataPacket(true); qDebug() << "set joint mapping froms network reply, num joints: " << _jointIndices.size(); networkReply->deleteLater(); } -void AvatarData::sendAvatarDataPacket(bool sendFull) { +void AvatarData::sendAvatarDataPacket() { auto nodeList = DependencyManager::get(); // about 2% of the time, we send a full update (meaning, we transmit all the joint data), even if nothing has changed. // this is to guard against a joint moving once, the packet getting lost, and the joint never moving again. - bool sendFullUpdate = sendFull || (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); - if (sendFullUpdate) qDebug() << sendFullUpdate; + bool sendFullUpdate = randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO; QByteArray avatarByteArray = toByteArray(true, sendFullUpdate); doneEncoding(true); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 7fe339c2b9..61ee649273 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -352,7 +352,7 @@ public: AvatarEntityIDs getAndClearRecentlyDetachedIDs(); public slots: - void sendAvatarDataPacket(bool sendFull = false); + void sendAvatarDataPacket(); void sendIdentityPacket(); void setJointMappingsFromNetworkReply(); From 5dae20975a620c6d022b1df27a09c98d270bbfa8 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 16 Jun 2016 08:14:31 -0700 Subject: [PATCH 0587/1237] fix v1 procedural shaders --- libraries/procedural/src/procedural/ProceduralCommon.slf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slf b/libraries/procedural/src/procedural/ProceduralCommon.slf index 128723265f..d4144ad537 100644 --- a/libraries/procedural/src/procedural/ProceduralCommon.slf +++ b/libraries/procedural/src/procedural/ProceduralCommon.slf @@ -264,12 +264,18 @@ float snoise(vec2 v) { return 130.0 * dot(m, g); } + #define PROCEDURAL 1 //PROCEDURAL_VERSION #ifdef PROCEDURAL_V1 +// shader playback time (in seconds) +uniform float iGlobalTime; +// the dimensions of the object being rendered +uniform vec3 iWorldScale; + #else // Unimplemented uniforms From ed88232fb06bc8096d20637dc6abdcd8b52ed31d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 16 Jun 2016 09:19:42 -0700 Subject: [PATCH 0588/1237] Move session id storage to AccountManager --- interface/src/DiscoverabilityManager.cpp | 14 ++++++-------- interface/src/DiscoverabilityManager.h | 1 - libraries/networking/src/AccountManager.cpp | 7 +++++-- libraries/networking/src/AccountManager.h | 3 ++- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 24256fdf39..c4d985419e 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -80,7 +80,8 @@ void DiscoverabilityManager::updateLocation() { locationObject.insert(FRIENDS_ONLY_KEY_IN_LOCATION, (_mode.get() == Discoverability::Friends)); // if we have a session ID add it now, otherwise add a null value - rootObject[SESSION_ID_KEY] = _sessionID.isEmpty() ? QJsonValue() : _sessionID; + auto sessionID = accountManager->getSessionID(); + rootObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = this; @@ -110,11 +111,8 @@ void DiscoverabilityManager::updateLocation() { callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; QJsonObject heartbeatObject; - if (!_sessionID.isEmpty()) { - heartbeatObject[SESSION_ID_KEY] = _sessionID; - } else { - heartbeatObject[SESSION_ID_KEY] = QJsonValue(); - } + auto sessionID = accountManager->getSessionID(); + heartbeatObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, @@ -126,11 +124,11 @@ void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply auto dataObject = AccountManager::dataObjectFromResponse(requestReply); if (!dataObject.isEmpty()) { - _sessionID = dataObject[SESSION_ID_KEY].toString(); + auto sessionID = dataObject[SESSION_ID_KEY].toString(); // give that session ID to the account manager auto accountManager = DependencyManager::get(); - accountManager->setSessionID(_sessionID); + accountManager->setSessionID(sessionID); } } diff --git a/interface/src/DiscoverabilityManager.h b/interface/src/DiscoverabilityManager.h index 9a1fa7b39c..196b0cdf81 100644 --- a/interface/src/DiscoverabilityManager.h +++ b/interface/src/DiscoverabilityManager.h @@ -49,7 +49,6 @@ private: DiscoverabilityManager(); Setting::Handle _mode; - QString _sessionID; QJsonObject _lastLocationObject; }; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index bac031885f..2ee49455b7 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -44,6 +44,7 @@ Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; +static const auto METAVERSE_SESSION_ID_HEADER = QString("HFM-SessionID").toLocal8Bit(); JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, const QString& jsonCallbackMethod, QObject* errorCallbackReceiver, const QString& errorCallbackMethod, @@ -221,8 +222,7 @@ void AccountManager::sendRequest(const QString& path, // if we're allowed to send usage data, include whatever the current session ID is with this request auto& activityLogger = UserActivityLogger::getInstance(); if (activityLogger.isEnabled()) { - static const QString METAVERSE_SESSION_ID_HEADER = "HFM-SessionID"; - networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER.toLocal8Bit(), + networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER, uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); } @@ -321,6 +321,9 @@ void AccountManager::processReply() { QNetworkReply* requestReply = reinterpret_cast(sender()); if (requestReply->error() == QNetworkReply::NoError) { + if (requestReply->hasRawHeader(METAVERSE_SESSION_ID_HEADER)) { + _sessionID = requestReply->rawHeader(METAVERSE_SESSION_ID_HEADER); + } passSuccessToCallback(requestReply); } else { passErrorToCallback(requestReply); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 4803d2625f..6e26a56739 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -86,6 +86,7 @@ public: static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); + QUuid getSessionID() const { return _sessionID; } void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } public slots: @@ -139,7 +140,7 @@ private: bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; - QUuid _sessionID; + QUuid _sessionID { QUuid::createUuid() }; }; #endif // hifi_AccountManager_h From 8b3b62aad7ac3861f044fdaebf8ecdc5f6388de5 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 16 Jun 2016 10:25:38 -0700 Subject: [PATCH 0589/1237] Add UserActivityLogger scripting interface --- interface/src/Application.cpp | 3 +++ .../UserActivityLoggerScriptingInterface.cpp | 19 ++++++++++++++ .../UserActivityLoggerScriptingInterface.h | 26 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 libraries/networking/src/UserActivityLoggerScriptingInterface.cpp create mode 100644 libraries/networking/src/UserActivityLoggerScriptingInterface.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e789b7c508..e8effefd6e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -67,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -4558,6 +4559,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); + + scriptEngine->registerGlobalObject("UserActivityLogger", new UserActivityLoggerScriptingInterface()); } bool Application::canAcceptURL(const QString& urlString) const { diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp new file mode 100644 index 0000000000..c0ed4fce19 --- /dev/null +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -0,0 +1,19 @@ +// +// UserActivityLoggerScriptingInterface.h +// libraries/networking/src +// +// Created by Ryan Huffman on 6/06/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "UserActivityLoggerScriptingInterface.h" +#include "UserActivityLogger.h" + +void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) const { + QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", + Q_ARG(QString, action), + Q_ARG(QJsonObject, QJsonObject::fromVariantMap(details))); +} diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h new file mode 100644 index 0000000000..1e6043491e --- /dev/null +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -0,0 +1,26 @@ +// +// UserActivityLoggerScriptingInterface.h +// libraries/networking/src +// +// Created by Ryan Huffman on 6/06/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_UserActivityLoggerScriptingInterface_h +#define hifi_UserActivityLoggerScriptingInterface_h + +#include +#include + +class QScriptValue; + +class UserActivityLoggerScriptingInterface : public QObject { + Q_OBJECT +public: + Q_INVOKABLE void logAction(QString action, QVariantMap details) const; +}; + +#endif // hifi_UserActivityLoggerScriptingInterface_h \ No newline at end of file From 96d6cb12ac9ddaf43151577e77cee3a501efef35 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 16 Jun 2016 12:27:03 -0700 Subject: [PATCH 0590/1237] supress some messages and errors --- scripts/system/html/entityProperties.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index c8edbdb369..82387cafa5 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -671,7 +671,6 @@ } else { for (var i = 0; i < elShapeSections.length; i++) { - console.log("Hiding shape section " + elShapeSections[i]) elShapeSections[i].style.display = 'none'; } } @@ -805,8 +804,10 @@ } var activeElement = document.activeElement; - - activeElement.select(); + + if(typeof activeElement.select!=="undefined"){ + activeElement.select(); + } } } }); From 1ce9e96cba6427ed98d8ff0235004752fe56a43b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 16 Jun 2016 01:20:28 -0700 Subject: [PATCH 0591/1237] Send domain metadata for authed domains --- domain-server/src/DomainServer.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7c596bb187..1f666455c6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -108,6 +108,9 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions, &_gatekeeper, &DomainGatekeeper::updateNodePermissions); + // update the metadata with current descriptors + _metadata.setDescriptors(_settingsManager.getSettingsMap()); + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -122,10 +125,17 @@ DomainServer::DomainServer(int argc, char* argv[]) : _gatekeeper.preloadAllowedUserPublicKeys(); optionallyGetTemporaryName(args); - } - // update the metadata with current descriptors - _metadata.setDescriptors(_settingsManager.getSettingsMap()); + // send metadata descriptors + QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(_metadata.getDescriptors()).toJson(QJsonDocument::Compact))); + const QUuid& domainID = DependencyManager::get()->getSessionUUID(); + static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; + DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), + AccountManagerAuth::Required, + QNetworkAccessManager::PutOperation, + JSONCallbackParameters(), + domainUpdateJSON.toUtf8()); + } } DomainServer::~DomainServer() { From c7955900ab9c35a4f3da1dc5eaa35b77b495e8ad Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 16 Jun 2016 13:17:18 -0700 Subject: [PATCH 0592/1237] Add new user activity events --- interface/src/Application.cpp | 42 +++++++++++++++++++++++-- interface/src/Application.h | 2 ++ libraries/avatars/src/AvatarHashMap.cpp | 14 +++++++++ libraries/avatars/src/AvatarHashMap.h | 1 + 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e8effefd6e..f2f265aae0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -435,7 +435,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -1057,6 +1057,44 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } }); + // Add periodic checks to send user activity data + static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; + static int SEND_FPS_INTERVAL_MS = 10000; + + // Periodically send fps as a user activity event + QTimer* sendFPSTimer = new QTimer(this); + sendFPSTimer->setInterval(SEND_FPS_INTERVAL_MS); + connect(sendFPSTimer, &QTimer::timeout, this, [this]() { + UserActivityLogger::getInstance().logAction("fps", { { "rate", _frameCounter.rate() } }); + }); + sendFPSTimer->start(); + + + // Periodically check for count of nearby avatars + static int lastCountOfNearbyAvatars = -1; + QTimer* checkNearbyAvatarsTimer = new QTimer(this); + checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); + connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, [this]() { + auto avatarManager = DependencyManager::get(); + int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), 10) - 1; + if (nearbyAvatars != lastCountOfNearbyAvatars) { + UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); + } + }); + + // Track user activity event when we receive a mute packet + auto onMutedByMixer = []() { + UserActivityLogger::getInstance().logAction("received_mute_packet"); + }; + connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); + + // Track when the address bar is opened + auto onAddressBarToggled = [this]() { + // Record time + UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); + }; + connect(DependencyManager::get().data(), &DialogsManager::addressBarToggled, this, onAddressBarToggled); + // Make sure we don't time out during slow operations at startup updateHeartbeat(); @@ -4560,7 +4598,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); - scriptEngine->registerGlobalObject("UserActivityLogger", new UserActivityLoggerScriptingInterface()); + scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); } bool Application::canAcceptURL(const QString& urlString) const { diff --git a/interface/src/Application.h b/interface/src/Application.h index a17250a58e..d728359ed1 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -195,6 +195,8 @@ public: float getRenderResolutionScale() const; + qint64 getCurrentSessionRuntime() const { return _sessionRunTimer.elapsed(); } + bool isAboutToQuit() const { return _aboutToQuit; } // the isHMDMode is true whenever we use the interface from an HMD and not a standard flat display diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 9084fd837b..d153cfd977 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -44,6 +44,20 @@ bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range return false; } +int AvatarHashMap::numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters) { + auto hashCopy = getHashCopy(); + auto rangeMeters2 = rangeMeters * rangeMeters; + int count = 0; + for (const AvatarSharedPointer& sharedAvatar : hashCopy) { + glm::vec3 avatarPosition = sharedAvatar->getPosition(); + auto distance2 = glm::distance2(avatarPosition, position); + if (distance2 < rangeMeters2) { + ++count; + } + } + return count; +} + AvatarSharedPointer AvatarHashMap::newSharedAvatar() { return std::make_shared(); } diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 5f58074427..9d3ebb60f5 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -39,6 +39,7 @@ public: Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID); virtual AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) { return findAvatar(sessionID); } + int numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters); signals: void avatarAddedEvent(const QUuid& sessionUUID); From 365037774570a28b1dd2f8883ac1290ad4a670e8 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 16 Jun 2016 13:17:31 -0700 Subject: [PATCH 0593/1237] Clean up NULL => nullptr --- libraries/networking/src/AccountManager.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 6e26a56739..d30a05fb2c 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -26,9 +26,9 @@ class JSONCallbackParameters { public: - JSONCallbackParameters(QObject* jsonCallbackReceiver = NULL, const QString& jsonCallbackMethod = QString(), - QObject* errorCallbackReceiver = NULL, const QString& errorCallbackMethod = QString(), - QObject* updateReceiver = NULL, const QString& updateSlot = QString()); + JSONCallbackParameters(QObject* jsonCallbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), + QObject* errorCallbackReceiver = nullptr, const QString& errorCallbackMethod = QString(), + QObject* updateReceiver = nullptr, const QString& updateSlot = QString()); bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; } From 56c84bbc27cf34cf1a173d67b28e1c0fdc8cc871 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 16 Jun 2016 13:18:08 -0700 Subject: [PATCH 0594/1237] Add opened_marketplace and enabled_edit to edit.js --- scripts/system/edit.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index afbc679ec4..4c5d9bf4f0 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -150,6 +150,8 @@ function showMarketplace(marketplaceID) { marketplaceWindow.setURL(url); marketplaceWindow.setVisible(true); marketplaceWindow.raise(); + + UserActivityLogger.logAction("opened_marketplace"); } function hideMarketplace() { @@ -358,6 +360,9 @@ var toolBar = (function() { } that.showTools(isActive); } + if (active) { + UserActivityLogger.logAction("enabled_edit"); + } } toolBar.selectTool(activeButton, isActive); lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); From bce05df56b5718cc1f21a1238d8c77effe667800 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 16 Jun 2016 13:25:14 -0700 Subject: [PATCH 0595/1237] Update enabled_edit to only get sent when you have permission --- scripts/system/edit.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 4c5d9bf4f0..f20676efc2 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -340,6 +340,7 @@ var toolBar = (function() { Messages.sendLocalMessage("edit-events", JSON.stringify({ enabled: active })); + UserActivityLogger.logAction("enabled_edit"); isActive = active; if (!isActive) { entityListTool.setVisible(false); @@ -360,9 +361,6 @@ var toolBar = (function() { } that.showTools(isActive); } - if (active) { - UserActivityLogger.logAction("enabled_edit"); - } } toolBar.selectTool(activeButton, isActive); lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); From 0f9c637c5a490dd43da16ca74dbfde34e80fd843 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 16 Jun 2016 01:32:38 -0700 Subject: [PATCH 0596/1237] Wireframe a metadata overlay tutorial --- scripts/tutorials/getDomainMetadata.js | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 scripts/tutorials/getDomainMetadata.js diff --git a/scripts/tutorials/getDomainMetadata.js b/scripts/tutorials/getDomainMetadata.js new file mode 100644 index 0000000000..1c78d640e8 --- /dev/null +++ b/scripts/tutorials/getDomainMetadata.js @@ -0,0 +1,28 @@ +// +// Created by Zach Pomerantz on June 16, 2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Avoid polluting the namespace +// !function() { + var MetadataNotification = null; + + // Every time you enter a domain, display the domain's metadata + SomeEvent.connect(function() { + // Fetch the domain metadata from the directory + + // Display the fetched metadata in an overlay + + }); + + // Remove the overlay if the script is stopped + Script.scriptEnding.connect(teardown); + + function teardown() { + // Close the overlay, if it exists + + } +// }(); From c2ebcd1f77234350a974a03b30c1aac1223e7f8a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 16 Jun 2016 13:49:25 -0700 Subject: [PATCH 0597/1237] Make activity logging from scripts only available for certain events --- .../src/UserActivityLoggerScriptingInterface.cpp | 12 ++++++++++-- .../src/UserActivityLoggerScriptingInterface.h | 14 +++++++++----- scripts/system/edit.js | 2 +- scripts/system/examples.js | 2 ++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index c0ed4fce19..012a569639 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -12,8 +12,16 @@ #include "UserActivityLoggerScriptingInterface.h" #include "UserActivityLogger.h" -void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) const { +void UserActivityLoggerScriptingInterface::enabledEdit() { + logAction("enabled_edit"); +} + +void UserActivityLoggerScriptingInterface::openedMarketplace() { + logAction("opened_marketplace"); +} + +void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) { QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", Q_ARG(QString, action), - Q_ARG(QJsonObject, QJsonObject::fromVariantMap(details))); + Q_ARG(QJsonObject, details)); } diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index 1e6043491e..cad24b1967 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -13,14 +13,18 @@ #define hifi_UserActivityLoggerScriptingInterface_h #include -#include +#include -class QScriptValue; +#include -class UserActivityLoggerScriptingInterface : public QObject { +class UserActivityLoggerScriptingInterface : public QObject, public Dependency { Q_OBJECT public: - Q_INVOKABLE void logAction(QString action, QVariantMap details) const; + Q_INVOKABLE void enabledEdit(); + Q_INVOKABLE void openedMarketplace(); + +private: + void logAction(QString action, QJsonObject details = {}); }; -#endif // hifi_UserActivityLoggerScriptingInterface_h \ No newline at end of file +#endif // hifi_UserActivityLoggerScriptingInterface_h diff --git a/scripts/system/edit.js b/scripts/system/edit.js index f20676efc2..64c6a06c58 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -340,7 +340,7 @@ var toolBar = (function() { Messages.sendLocalMessage("edit-events", JSON.stringify({ enabled: active })); - UserActivityLogger.logAction("enabled_edit"); + UserActivityLogger.enabledEdit(); isActive = active; if (!isActive) { entityListTool.setVisible(false); diff --git a/scripts/system/examples.js b/scripts/system/examples.js index 9d33e473af..fb5ba02441 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -37,6 +37,8 @@ function showExamples(marketplaceID) { print("setting examples URL to " + url); examplesWindow.setURL(url); examplesWindow.setVisible(true); + + UserActivityLogger.openedMarketplace(); } function hideExamples() { From 77976281abcfceccfb476b5a64484b5d69726552 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 17 Jun 2016 09:58:44 +1200 Subject: [PATCH 0598/1237] Make audio device selection available without advanced menus enabled --- scripts/system/selectAudioDevice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index fd4cd43a59..663f96b59c 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -48,7 +48,7 @@ var selectedInputMenu = ""; var selectedOutputMenu = ""; function setupAudioMenus() { - Menu.addMenu("Audio > Devices", "Advanced"); + Menu.addMenu("Audio > Devices"); Menu.addSeparator("Audio > Devices","Output Audio Device"); var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); From aca3d4d90b0acd3522eeabad214152a48406ac0b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 16 Jun 2016 15:22:34 -0700 Subject: [PATCH 0599/1237] add example script for metadata from metaverse --- scripts/tutorials/getDomainMetadata.js | 118 ++++++++++++++++++++++--- 1 file changed, 105 insertions(+), 13 deletions(-) diff --git a/scripts/tutorials/getDomainMetadata.js b/scripts/tutorials/getDomainMetadata.js index 1c78d640e8..0a6f742823 100644 --- a/scripts/tutorials/getDomainMetadata.js +++ b/scripts/tutorials/getDomainMetadata.js @@ -6,23 +6,115 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// Avoid polluting the namespace -// !function() { - var MetadataNotification = null; +var SERVER = 'https://metaverse.highfidelity.com/api/v1'; - // Every time you enter a domain, display the domain's metadata - SomeEvent.connect(function() { - // Fetch the domain metadata from the directory +var OVERLAY = null; - // Display the fetched metadata in an overlay +// Every time you enter a domain, display the domain's metadata +location.hostChanged.connect(function(host) { + print('Detected host change:', host); - }); + // Fetch the domain ID from the metaverse + var placeData = request(SERVER + '/places/' + host); + if (!placeData) { return; } + var domainID = placeData.data.place.domain.id; + print('Domain ID:', domainID); - // Remove the overlay if the script is stopped - Script.scriptEnding.connect(teardown); + // Fetch the domain metadata from the metaverse + var domainData = request(SERVER + '/domains/' + domainID); + print(SERVER + '/domains/' + domainID); + if (!domainData) { return; } + var metadata = domainData.domain; + print('Domain metadata:', JSON.stringify(metadata)); - function teardown() { - // Close the overlay, if it exists + // Display the fetched metadata in an overlay + displayMetadata(host, metadata); +}); +Script.scriptEnding.connect(clearMetadata); + +function displayMetadata(place, metadata) { + clearMetadata(); + + var COLOR_TEXT = { red: 255, green: 255, blue: 255 }; + var COLOR_BACKGROUND = { red: 0, green: 0, blue: 0 }; + var MARGIN = 200; + var STARTING_OPACITY = 0.8; + var FADE_AFTER_SEC = 2; + var FADE_FOR_SEC = 4; + + var fade_per_sec = STARTING_OPACITY / FADE_FOR_SEC; + var properties = { + color: COLOR_TEXT, + alpha: STARTING_OPACITY, + backgroundColor: COLOR_BACKGROUND, + backgroundAlpha: STARTING_OPACITY, + font: { size: 24 }, + x: MARGIN, + y: MARGIN + }; + + // Center the overlay on the screen + properties.width = Window.innerWidth - MARGIN*2; + properties.height = Window.innerHeight - MARGIN*2; + + // Parse the metadata into text + parsed = [ 'Welcome to ' + place + '!',, ]; + if (metadata.description) { + parsed.push(description); } -// }(); + if (metadata.tags && metadata.tags.length) { + parsed.push('Tags: ' + metadata.tags.join(',')); + } + if (metadata.capacity) { + parsed.push('Capacity (max users): ' + metadata.capacity); + } + if (metadata.maturity) { + parsed.push('Maturity: ' + metadata.maturity); + } + if (metadata.hosts && metadata.hosts.length) { + parsed.push('Hosts: ' + metadata.tags.join(',')); + } + if (metadata.online_users) { + parsed.push('Users online: ' + metadata.online_users); + } + + properties.text = parsed.join('\n\n'); + + // Display the overlay + OVERLAY = Overlays.addOverlay('text', properties); + + // Fade out the overlay over 10 seconds + !function() { + var overlay = OVERLAY; + var alpha = STARTING_OPACITY; + + var fade = function() { + // Only fade so long as the same overlay is up + if (overlay == OVERLAY) { + alpha -= fade_per_sec / 10; + if (alpha <= 0) { + clearMetadata(); + } else { + Overlays.editOverlay(overlay, { alpha: alpha, backgroundAlpha: alpha }); + Script.setTimeout(fade, 100); + } + } + }; + Script.setTimeout(fade, FADE_AFTER_SEC * 1000); + }(); +} + +function clearMetadata() { + if (OVERLAY) { Overlays.deleteOverlay(OVERLAY); } +} + +// Request JSON from a url, synchronously +function request(url) { + var req = new XMLHttpRequest(); + req.responseType = 'json'; + req.open('GET', url, false); + req.send(); + return req.status == 200 ? req.response : null; +} + From 350f06465d05594776591689a7a33cddd3812457 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 16 Jun 2016 18:28:08 -0700 Subject: [PATCH 0600/1237] Introducing the deferredFRameTransform to DeferredLighting Effect and simplifying the rendering! --- .../render-utils/src/DeferredBufferRead.slh | 120 +++++++++++------- .../src/DeferredLightingEffect.cpp | 76 ++++++----- .../render-utils/src/DeferredLightingEffect.h | 22 +++- .../render-utils/src/DeferredTransform.slh | 4 + .../render-utils/src/RenderDeferredTask.cpp | 10 +- .../render-utils/src/RenderDeferredTask.h | 22 ---- .../render-utils/src/SubsurfaceScattering.cpp | 3 +- .../src/directional_ambient_light.slf | 6 +- .../src/directional_ambient_light_shadow.slf | 8 +- .../render-utils/src/directional_light.slf | 6 +- .../src/directional_light_shadow.slf | 8 +- .../src/directional_skybox_light.slf | 6 +- .../src/directional_skybox_light_shadow.slf | 8 +- libraries/render-utils/src/point_light.slf | 4 +- libraries/render-utils/src/spot_light.slf | 4 +- .../subsurfaceScattering_drawScattering.slf | 5 +- .../developer/utilities/render/currentZone.js | 73 +++++++++++ .../utilities/render/framebuffer.qml | 17 ++- .../utilities/render/globalLight.qml | 36 ++++++ .../utilities/render/surfaceGeometryPass.qml | 7 + 20 files changed, 303 insertions(+), 142 deletions(-) create mode 100644 scripts/developer/utilities/render/currentZone.js create mode 100644 scripts/developer/utilities/render/globalLight.qml diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index 7714859f5a..bd4aea350f 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -32,38 +32,6 @@ uniform sampler2D obscuranceMap; uniform sampler2D lightingMap; -struct DeferredTransform { - mat4 projection; - mat4 viewInverse; - float stereoSide; - vec3 _spareABC; -}; - -layout(std140) uniform deferredTransformBuffer { - DeferredTransform _deferredTransform; -}; -DeferredTransform getDeferredTransform() { - return _deferredTransform; -} - -bool getStereoMode(DeferredTransform deferredTransform) { - return (deferredTransform.stereoSide != 0.0); -} -float getStereoSide(DeferredTransform deferredTransform) { - return (deferredTransform.stereoSide); -} - -vec4 evalEyePositionFromZ(DeferredTransform deferredTransform, float depthVal, vec2 texcoord) { - vec3 nPos = vec3(texcoord.xy * 2.0f - 1.0f, depthVal * 2.0f - 1.0f); - - // compute the view space position using the depth - // basically manually pick the proj matrix components to do the inverse - float Ze = -deferredTransform.projection[3][2] / (nPos.z + deferredTransform.projection[2][2]); - float Xe = (-Ze * nPos.x - Ze * deferredTransform.projection[2][0] - deferredTransform.projection[3][0]) / deferredTransform.projection[0][0]; - float Ye = (-Ze * nPos.y - Ze * deferredTransform.projection[2][1] - deferredTransform.projection[3][1]) / deferredTransform.projection[1][1]; - return vec4(Xe, Ye, Ze, 1.0f); -} - struct DeferredFragment { vec4 normalVal; vec4 diffuseVal; @@ -80,16 +48,6 @@ struct DeferredFragment { float depthVal; }; -vec4 unpackDeferredPosition(DeferredTransform deferredTransform, float depthValue, vec2 texcoord) { - if (getStereoMode(deferredTransform)) { - if (texcoord.x > 0.5) { - texcoord.x -= 0.5; - } - texcoord.x *= 2.0; - } - return evalEyePositionFromZ(deferredTransform, depthValue, texcoord); -} - DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { DeferredFragment frag; @@ -122,6 +80,52 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { return frag; } + +<@include DeferredTransform.slh@> +<$declareDeferredFrameTransform()$> + + 0.5) { + texcoord.x -= 0.5; + } + texcoord.x *= 2.0; + } + return evalEyePositionFromZ(deferredTransform, depthValue, texcoord); +} DeferredFragment unpackDeferredFragment(DeferredTransform deferredTransform, vec2 texcoord) { float depthValue = texture(depthMap, texcoord).r; @@ -133,8 +137,36 @@ DeferredFragment unpackDeferredFragment(DeferredTransform deferredTransform, vec return frag; } +mat4 getViewInverse() { + return _deferredTransform.viewInverse; +} + +!> + +vec4 unpackDeferredPosition(DeferredFrameTransform deferredTransform, float depthValue, vec2 texcoord) { + int side = 0; + if (isStereo()) { + if (texcoord.x > 0.5) { + texcoord.x -= 0.5; + side = 1; + } + texcoord.x *= 2.0; + } + float Zeye = evalZeyeFromZdb(depthValue); + + return vec4(evalEyePositionFromZeye(side, Zeye, texcoord), 1.0); +} +DeferredFragment unpackDeferredFragment(DeferredFrameTransform deferredTransform, vec2 texcoord) { + + float depthValue = texture(depthMap, texcoord).r; + + DeferredFragment frag = unpackDeferredFragmentNoPosition(texcoord); + + frag.depthVal = depthValue; + frag.position = unpackDeferredPosition(deferredTransform, frag.depthVal, texcoord); + + return frag; +} - - <@endif@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 0a562de3ba..a684851aee 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -38,6 +38,8 @@ #include "point_light_frag.h" #include "spot_light_frag.h" +using namespace render; + struct LightLocations { int radius; int ambientSphere; @@ -45,6 +47,7 @@ struct LightLocations { int texcoordMat; int coneParam; int deferredTransformBuffer; + int deferredFrameTransformBuffer; int shadowTransformBuffer; }; @@ -56,6 +59,9 @@ enum { DEFERRED_BUFFER_OBSCURANCE_UNIT = 4, SHADOW_MAP_UNIT = 5, SKYBOX_MAP_UNIT = 6, +}; +enum { + }; static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& program, LightLocationsPtr& locations); @@ -132,33 +138,7 @@ void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radiu } } -void DeferredLightingEffect::prepare(RenderArgs* args) { - gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.setViewportTransform(args->_viewport); - batch.setStateScissorRect(args->_viewport); - - // Clear Lighting buffer - auto lightingFbo = DependencyManager::get()->getLightingFramebuffer(); - - batch.setFramebuffer(lightingFbo); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(vec3(0), 0), true); - - // Clear deferred - auto deferredFbo = DependencyManager::get()->getDeferredFramebuffer(); - - batch.setFramebuffer(deferredFbo); - - // Clear Color, Depth and Stencil for deferred buffer - batch.clearFramebuffer( - gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_COLOR1 | gpu::Framebuffer::BUFFER_COLOR2 | - gpu::Framebuffer::BUFFER_DEPTH | - gpu::Framebuffer::BUFFER_STENCIL, - vec4(vec3(0), 0), 1.0, 0.0, true); - }); -} - -void DeferredLightingEffect::render(const render::RenderContextPointer& renderContext) { +void DeferredLightingEffect::render(const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { auto args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { @@ -185,6 +165,7 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); + // Bind the G-Buffer surfaces batch.setResourceTexture(DEFERRED_BUFFER_COLOR_UNIT, framebufferCache->getDeferredColorTexture()); batch.setResourceTexture(DEFERRED_BUFFER_NORMAL_UNIT, framebufferCache->getDeferredNormalTexture()); @@ -294,6 +275,7 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo auto eyePoint = viewFrustum.getPosition(); float nearRadius = glm::distance(eyePoint, viewFrustum.getNearTopLeft()); + batch.setUniformBuffer(_directionalLightLocations->deferredFrameTransformBuffer, frameTransform->getFrameTransformBuffer()); for (int side = 0; side < numPasses; side++) { // Render in this side's viewport @@ -302,7 +284,9 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo // Sync and Bind the correct DeferredTransform ubo _deferredTransformBuffer[side]._buffer->setSubData(0, sizeof(DeferredTransform), (const gpu::Byte*) &deferredTransforms[side]); - batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, _deferredTransformBuffer[side]); + // batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, _deferredTransformBuffer[side]); + + glm::vec2 topLeft(-1.0f, -1.0f); glm::vec2 bottomRight(1.0f, 1.0f); @@ -482,7 +466,8 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo batch.setResourceTexture(SHADOW_MAP_UNIT, nullptr); batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); - batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, nullptr); + // batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, nullptr); + batch.setUniformBuffer(_directionalLightLocations->deferredFrameTransformBuffer, nullptr); }); // End of the Lighting pass @@ -524,8 +509,10 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo static const int LIGHT_GPU_SLOT = 3; static const int DEFERRED_TRANSFORM_BUFFER_SLOT = 2; + static const int DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 4; slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("deferredTransformBuffer"), DEFERRED_TRANSFORM_BUFFER_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); @@ -537,6 +524,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo locations->lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); locations->deferredTransformBuffer = program->getBuffers().findLocation("deferredTransformBuffer"); + locations->deferredFrameTransformBuffer = program->getBuffers().findLocation("deferredFrameTransformBuffer"); locations->shadowTransformBuffer = program->getBuffers().findLocation("shadowTransformBuffer"); auto state = std::make_shared(); @@ -667,3 +655,33 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { return _spotLightMesh; } +void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { + auto args = renderContext->args; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + + // Clear Lighting buffer + auto lightingFbo = DependencyManager::get()->getLightingFramebuffer(); + + batch.setFramebuffer(lightingFbo); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(vec3(0), 0), true); + + // Clear deferred + auto deferredFbo = DependencyManager::get()->getDeferredFramebuffer(); + + batch.setFramebuffer(deferredFbo); + + // Clear Color, Depth and Stencil for deferred buffer + batch.clearFramebuffer( + gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_COLOR1 | gpu::Framebuffer::BUFFER_COLOR2 | + gpu::Framebuffer::BUFFER_DEPTH | + gpu::Framebuffer::BUFFER_STENCIL, + vec4(vec3(0), 0), 1.0, 0.0, true); + }); +} + +void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFrameTransformPointer& deferredTransform) { + DependencyManager::get()->render(renderContext, deferredTransform); +} diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 63d8f4d175..871493f076 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -21,6 +21,9 @@ #include "model/Geometry.h" #include "render/Context.h" +#include + +#include "DeferredFrameTransform.h" #include "LightStage.h" @@ -42,9 +45,8 @@ public: void addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color = glm::vec3(1.0f, 1.0f, 1.0f), float intensity = 0.5f, float falloffRadius = 0.01f, const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI); - - void prepare(RenderArgs* args); - void render(const render::RenderContextPointer& renderContext); + + void render(const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int skyboxCubemapUnit); @@ -110,4 +112,18 @@ private: UniformBufferView _deferredTransformBuffer[2]; }; +class PrepareDeferred { +public: + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + + using JobModel = render::Job::Model; +}; + +class RenderDeferred { +public: + using JobModel = render::Job::ModelI; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); +}; + #endif // hifi_DeferredLightingEffect_h diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 536d77d7c1..20c1cfdd52 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -27,6 +27,10 @@ uniform deferredFrameTransformBuffer { DeferredFrameTransform frameTransform; }; +DeferredFrameTransform getDeferredFrameTransform() { + return frameTransform; +} + vec2 getWidthHeight(int resolutionLevel) { return vec2(ivec2(frameTransform._pixelInfo.zw) >> resolutionLevel); } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index fb505f83e9..4d04814954 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -43,14 +43,6 @@ extern void initStencilPipeline(gpu::PipelinePointer& pipeline); extern void initOverlay3DPipelines(render::ShapePlumber& plumber); extern void initDeferredPipelines(render::ShapePlumber& plumber); -void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { - DependencyManager::get()->prepare(renderContext->args); -} - -void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { - DependencyManager::get()->render(renderContext); -} - RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; }; @@ -135,7 +127,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); // DeferredBuffer is complete, now let's shade it into the LightingBuffer - addJob("RenderDeferred"); + addJob("RenderDeferred", deferredFrameTransform); // AA job to be revisited diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 5588d1ac28..5a80d4bcea 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -15,28 +15,6 @@ #include #include -class SetupDeferred { -public: - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - - using JobModel = render::Job::Model; -}; - - -class PrepareDeferred { -public: - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - - using JobModel = render::Job::Model; -}; - -class RenderDeferred { -public: - using JobModel = render::Job::Model; - - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); -}; - class DrawConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY newStats) diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 79527d36e1..2d2769ad0b 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -137,7 +137,7 @@ bool SubsurfaceScattering::updateScatteringFramebuffer(const gpu::FramebufferPoi // _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); } auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); _scatteringFramebuffer->setRenderBuffer(0, blurringTarget); } else { // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so @@ -190,7 +190,6 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setViewportTransform(args->_viewport); batch.setFramebuffer(_scatteringFramebuffer); - // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(vec3(0), 0), false); batch.setPipeline(pipeline); diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index 2ee818fdba..b1142ffe9e 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -22,7 +22,7 @@ in vec2 _texCoord0; out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); float shadowAttenuation = 1.0; @@ -31,7 +31,7 @@ void main(void) { _fragColor = vec4(frag.diffuse, 1.0); } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { vec3 color = evalLightmappedColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.normal, @@ -40,7 +40,7 @@ void main(void) { _fragColor = vec4(color, 1.0); } else { vec3 color = evalAmbientSphereGlobalColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.position.xyz, diff --git a/libraries/render-utils/src/directional_ambient_light_shadow.slf b/libraries/render-utils/src/directional_ambient_light_shadow.slf index 20ceea9379..1729350687 100644 --- a/libraries/render-utils/src/directional_ambient_light_shadow.slf +++ b/libraries/render-utils/src/directional_ambient_light_shadow.slf @@ -23,17 +23,17 @@ in vec2 _texCoord0; out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); - vec4 worldPos = deferredTransform.viewInverse * vec4(frag.position.xyz, 1.0); + vec4 worldPos = getViewInverse() * vec4(frag.position.xyz, 1.0); float shadowAttenuation = evalShadowAttenuation(worldPos); if (frag.mode == FRAG_MODE_UNLIT) { _fragColor = vec4(frag.diffuse, 1.0); } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { vec3 color = evalLightmappedColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.normal, @@ -42,7 +42,7 @@ void main(void) { _fragColor = vec4(color, 1.0); } else { vec3 color = evalAmbientSphereGlobalColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.position.xyz, diff --git a/libraries/render-utils/src/directional_light.slf b/libraries/render-utils/src/directional_light.slf index ef61e9a030..7e19007260 100644 --- a/libraries/render-utils/src/directional_light.slf +++ b/libraries/render-utils/src/directional_light.slf @@ -22,7 +22,7 @@ in vec2 _texCoord0; out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); float shadowAttenuation = 1.0; @@ -32,7 +32,7 @@ void main(void) { _fragColor = vec4(frag.diffuse, 1.0); } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { vec3 color = evalLightmappedColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.normal, @@ -41,7 +41,7 @@ void main(void) { _fragColor = vec4(color, 1.0); } else { vec3 color = evalAmbientGlobalColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.position.xyz, diff --git a/libraries/render-utils/src/directional_light_shadow.slf b/libraries/render-utils/src/directional_light_shadow.slf index 5b09d47e80..2698aec1ac 100644 --- a/libraries/render-utils/src/directional_light_shadow.slf +++ b/libraries/render-utils/src/directional_light_shadow.slf @@ -23,10 +23,10 @@ in vec2 _texCoord0; out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); - vec4 worldPos = deferredTransform.viewInverse * vec4(frag.position.xyz, 1.0); + vec4 worldPos = getViewInverse() * vec4(frag.position.xyz, 1.0); float shadowAttenuation = evalShadowAttenuation(worldPos); // Light mapped or not ? @@ -34,7 +34,7 @@ void main(void) { _fragColor = vec4(frag.diffuse, 1.0); } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { vec3 color = evalLightmappedColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.normal, @@ -43,7 +43,7 @@ void main(void) { _fragColor = vec4(color, 1.0); } else { vec3 color = evalAmbientGlobalColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.position.xyz, diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index f0c7bb476f..074ee90fca 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -22,7 +22,7 @@ in vec2 _texCoord0; out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); float shadowAttenuation = 1.0; @@ -32,7 +32,7 @@ void main(void) { _fragColor = vec4(frag.diffuse, 1.0); } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { vec3 color = evalLightmappedColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.normal, @@ -41,7 +41,7 @@ void main(void) { _fragColor = vec4(color, 1.0); } else { vec3 color = evalSkyboxGlobalColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.position.xyz, diff --git a/libraries/render-utils/src/directional_skybox_light_shadow.slf b/libraries/render-utils/src/directional_skybox_light_shadow.slf index 6a233e5985..98c6912d22 100644 --- a/libraries/render-utils/src/directional_skybox_light_shadow.slf +++ b/libraries/render-utils/src/directional_skybox_light_shadow.slf @@ -23,10 +23,10 @@ in vec2 _texCoord0; out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); - vec4 worldPos = deferredTransform.viewInverse * vec4(frag.position.xyz, 1.0); + vec4 worldPos = getViewInverse() * vec4(frag.position.xyz, 1.0); float shadowAttenuation = evalShadowAttenuation(worldPos); // Light mapped or not ? @@ -34,7 +34,7 @@ void main(void) { _fragColor = vec4(frag.diffuse, 1.0); } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { vec3 color = evalLightmappedColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.normal, @@ -43,7 +43,7 @@ void main(void) { _fragColor = vec4(color, 1.0); } else { vec3 color = evalSkyboxGlobalColor( - deferredTransform.viewInverse, + getViewInverse(), shadowAttenuation, frag.obscurance, frag.position.xyz, diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 96cf7152d9..e9c6ee90a0 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -26,7 +26,7 @@ in vec4 _texCoord0; out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); // Grab the fragment data from the uv vec2 texCoord = _texCoord0.st / _texCoord0.q; @@ -36,7 +36,7 @@ void main(void) { discard; } - mat4 invViewMat = deferredTransform.viewInverse; + mat4 invViewMat = getViewInverse(); // Kill if in front of the light volume float depth = frag.depthVal; diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index e5bdac1325..bf046a613b 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -26,7 +26,7 @@ out vec4 _fragColor; void main(void) { - DeferredTransform deferredTransform = getDeferredTransform(); + DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); // Grab the fragment data from the uv vec2 texCoord = _texCoord0.st / _texCoord0.q; @@ -36,7 +36,7 @@ void main(void) { discard; } - mat4 invViewMat = deferredTransform.viewInverse; + mat4 invViewMat = getViewInverse(); // Kill if in front of the light volume float depth = frag.depthVal; diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index 154f5f0e8c..6151cfbd2e 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -10,9 +10,11 @@ // -<@include DeferredTransform.slh@> + <$declareDeferredFrameTransform()$> +!> +<@include DeferredBufferRead.slh@> <@include DeferredGlobalLight.slh@> @@ -26,7 +28,6 @@ float getZEyeLinear(vec2 texcoord) { return -texture(linearDepthMap, texcoord).x; } -<@include DeferredBufferRead.slh@> vec2 sideToFrameTexcoord(vec2 side, vec2 texcoordPos) { return vec2((texcoordPos.x + side.x) * side.y, texcoordPos.y); diff --git a/scripts/developer/utilities/render/currentZone.js b/scripts/developer/utilities/render/currentZone.js new file mode 100644 index 0000000000..28de78b80f --- /dev/null +++ b/scripts/developer/utilities/render/currentZone.js @@ -0,0 +1,73 @@ +// +// currentZone.js +// examples/utilities/tools/render +// +// Sam Gateau created on 6/18/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Set up the qml ui +/*var qml = Script.resolvePath('framebuffer.qml'); +var window = new OverlayWindow({ + title: 'Framebuffer Debug', + source: qml, + width: 400, height: 400, +}); +window.setPosition(25, 50); +window.closed.connect(function() { Script.stop(); }); +*/ + + + +function findCurrentZones() { + var foundEntitiesArray = Entities.findEntities(MyAvatar.position, 2.0); + //print(foundEntitiesArray.length); + var zones = []; + + foundEntitiesArray.forEach(function(foundID){ + var properties = Entities.getEntityProperties(foundID); + if (properties.type == "Zone") { + zones.push(foundID); + } + }); + return zones; +} + + +var currentZone; +var currentZoneProperties; + +function setCurrentZone(newCurrentZone) { + if (currentZone == newCurrentZone) { + return; + } + + currentZone = newCurrentZone; + currentZoneProperties = Entities.getEntityProperties(currentZone); + + print(JSON.stringify(currentZoneProperties)); +} + +var checkCurrentZone = function() { + + var currentZones = findCurrentZones(); + if (currentZones.length > 0) { + if (currentZone != currentZones[0]) { + print("New Zone"); + setCurrentZone(currentZones[0]); + } + } + +} +var ticker = Script.setInterval(checkCurrentZone, 2000); + +//checkCurrentZone(); + +function onQuit() { + Script.clearInterval(ticker); + print("Quit Zone"); +} +Script.scriptEnding.connect(onQuit); diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index 9727829880..d74ca03076 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -10,6 +10,7 @@ // import QtQuick 2.5 import QtQuick.Controls 1.4 +import "configSlider" Column { spacing: 8 @@ -22,13 +23,17 @@ Column { debug.config.mode = mode; } - function setLayout(layout) { - debug.config.size = { x: -1, y: -1, z: 1, w: 1 }; - } + function setX(x) { + print(x) - Button { - text: "Fullscreen" - onClicked: { debug.setLayout(1); } + debug.config.size = Vec4({ x: x, y: -1, z: 1, w: 1 }); + } + Slider { + minimumValue: -1.0 + value: debug.config.size.x + onValueChanged: { + debug.setX( value); + } } ExclusiveGroup { id: bufferGroup } diff --git a/scripts/developer/utilities/render/globalLight.qml b/scripts/developer/utilities/render/globalLight.qml new file mode 100644 index 0000000000..ac0d7ebcd5 --- /dev/null +++ b/scripts/developer/utilities/render/globalLight.qml @@ -0,0 +1,36 @@ +// +// globalLight.qml +// examples/utilities/render +// +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "configSlider" + +Column { + id: root + spacing: 8 + property var currentZoneID + property var zoneProperties + + Component.onCompleted: { + Entities.getProperties + sceneOctree.enabled = true; + itemSelection.enabled = true; + sceneOctree.showVisibleCells = false; + sceneOctree.showEmptyCells = false; + itemSelection.showInsideItems = false; + itemSelection.showInsideSubcellItems = false; + itemSelection.showPartialItems = false; + itemSelection.showPartialSubcellItems = false; + } + Component.onDestruction: { + sceneOctree.enabled = false; + itemSelection.enabled = false; + Render.getConfig("FetchSceneSelection").freezeFrustum = false; + Render.getConfig("CullSceneSelection").freezeFrustum = false; + } diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index 608731128b..b99605978c 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -15,6 +15,7 @@ Column { spacing: 8 Column { id: surfaceGeometry + spacing: 10 Column{ Repeater { @@ -63,5 +64,11 @@ Column { } } } + CheckBox { + text: "Show scatteringLUT" + checked: false + onCheckedChanged: { Render.getConfig("Scattering").showLUT = checked } + } + } } From 904efc807672fd8914c3a85d579041613d8dec7f Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 17 Jun 2016 09:31:32 -0700 Subject: [PATCH 0601/1237] SPlitting appart the DeferredLIghting Effect --- .../render-utils/src/DeferredBufferRead.slh | 60 +--- .../src/DeferredLightingEffect.cpp | 279 +++++++++++++++--- .../render-utils/src/DeferredLightingEffect.h | 51 +++- 3 files changed, 282 insertions(+), 108 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index bd4aea350f..ffa1cc4771 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -84,65 +84,6 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { <@include DeferredTransform.slh@> <$declareDeferredFrameTransform()$> - 0.5) { - texcoord.x -= 0.5; - } - texcoord.x *= 2.0; - } - return evalEyePositionFromZ(deferredTransform, depthValue, texcoord); -} -DeferredFragment unpackDeferredFragment(DeferredTransform deferredTransform, vec2 texcoord) { - - float depthValue = texture(depthMap, texcoord).r; - - DeferredFragment frag = unpackDeferredFragmentNoPosition(texcoord); - - frag.depthVal = depthValue; - frag.position = unpackDeferredPosition(deferredTransform, frag.depthVal, texcoord); - - return frag; -} -mat4 getViewInverse() { - return _deferredTransform.viewInverse; -} - -!> - vec4 unpackDeferredPosition(DeferredFrameTransform deferredTransform, float depthValue, vec2 texcoord) { int side = 0; if (isStereo()) { @@ -156,6 +97,7 @@ vec4 unpackDeferredPosition(DeferredFrameTransform deferredTransform, float dept return vec4(evalEyePositionFromZeye(side, Zeye, texcoord), 1.0); } + DeferredFragment unpackDeferredFragment(DeferredFrameTransform deferredTransform, vec2 texcoord) { float depthValue = texture(depthMap, texcoord).r; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index a684851aee..dc5b25b39e 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -46,12 +46,11 @@ struct LightLocations { int lightBufferUnit; int texcoordMat; int coneParam; - int deferredTransformBuffer; int deferredFrameTransformBuffer; int shadowTransformBuffer; }; -enum { +enum DeferredShader_MapSlot { DEFERRED_BUFFER_COLOR_UNIT = 0, DEFERRED_BUFFER_NORMAL_UNIT = 1, DEFERRED_BUFFER_EMISSIVE_UNIT = 2, @@ -60,9 +59,11 @@ enum { SHADOW_MAP_UNIT = 5, SKYBOX_MAP_UNIT = 6, }; -enum { - +enum DeferredShader_BufferSlot { + DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 2, + LIGHT_GPU_SLOT = 3, }; + static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& program, LightLocationsPtr& locations); void DeferredLightingEffect::init() { @@ -142,13 +143,6 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo auto args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - // Allocate the parameters buffer used by all the deferred shaders - if (!_deferredTransformBuffer[0]._buffer) { - DeferredTransform parameters; - _deferredTransformBuffer[0] = gpu::BufferView(std::make_shared(sizeof(DeferredTransform), (const gpu::Byte*) ¶meters)); - _deferredTransformBuffer[1] = gpu::BufferView(std::make_shared(sizeof(DeferredTransform), (const gpu::Byte*) ¶meters)); - } - // Framebuffer copy operations cannot function as multipass stereo operations. batch.enableStereo(false); @@ -220,7 +214,6 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo vec2 screenTopRightCorners[2]; vec4 fetchTexcoordRects[2]; - DeferredTransform deferredTransforms[2]; auto geometryCache = DependencyManager::get(); if (isStereo) { @@ -237,15 +230,10 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo int sideWidth = monoViewport.z >> 1; viewports[i] = ivec4(monoViewport.x + (i * sideWidth), monoViewport.y, sideWidth, monoViewport.w); - deferredTransforms[i].projection = projMats[i]; - auto sideViewMat = monoViewMat * glm::inverse(eyeViews[i]); // viewTransforms[i].evalFromRawMatrix(sideViewMat); viewTransforms[i] = monoViewTransform; viewTransforms[i].postTranslate(-glm::vec3((eyeViews[i][3])));// evalFromRawMatrix(sideViewMat); - deferredTransforms[i].viewInverse = sideViewMat; - - deferredTransforms[i].stereoSide = (i == 0 ? -1.0f : 1.0f); clipQuad[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); screenBottomLeftCorners[i] = glm::vec2(-1.0f + i * 1.0f, -1.0f); @@ -258,13 +246,8 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo viewports[0] = monoViewport; projMats[0] = monoProjMat; - deferredTransforms[0].projection = monoProjMat; - - deferredTransforms[0].viewInverse = monoViewMat; viewTransforms[0] = monoViewTransform; - deferredTransforms[0].stereoSide = 0.0f; - clipQuad[0] = glm::vec4(sMin, tMin, sWidth, tHeight); screenBottomLeftCorners[0] = glm::vec2(-1.0f, -1.0f); screenTopRightCorners[0] = glm::vec2(1.0f, 1.0f); @@ -282,12 +265,6 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo batch.setViewportTransform(viewports[side]); batch.setStateScissorRect(viewports[side]); - // Sync and Bind the correct DeferredTransform ubo - _deferredTransformBuffer[side]._buffer->setSubData(0, sizeof(DeferredTransform), (const gpu::Byte*) &deferredTransforms[side]); - // batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, _deferredTransformBuffer[side]); - - - glm::vec2 topLeft(-1.0f, -1.0f); glm::vec2 bottomRight(1.0f, 1.0f); glm::vec2 texCoordTopLeft(clipQuad[side].x, clipQuad[side].y); @@ -507,12 +484,8 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo slotBindings.insert(gpu::Shader::Binding(std::string("shadowMap"), SHADOW_MAP_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), SKYBOX_MAP_UNIT)); - static const int LIGHT_GPU_SLOT = 3; - static const int DEFERRED_TRANSFORM_BUFFER_SLOT = 2; - static const int DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 4; - slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); - slotBindings.insert(gpu::Shader::Binding(std::string("deferredTransformBuffer"), DEFERRED_TRANSFORM_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); @@ -523,7 +496,6 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo locations->coneParam = program->getUniforms().findLocation("coneParam"); locations->lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); - locations->deferredTransformBuffer = program->getBuffers().findLocation("deferredTransformBuffer"); locations->deferredFrameTransformBuffer = program->getBuffers().findLocation("deferredFrameTransformBuffer"); locations->shadowTransformBuffer = program->getBuffers().findLocation("shadowTransformBuffer"); @@ -682,6 +654,241 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC }); } -void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFrameTransformPointer& deferredTransform) { - DependencyManager::get()->render(renderContext, deferredTransform); + +void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { + + auto args = renderContext->args; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + + // Framebuffer copy operations cannot function as multipass stereo operations. + batch.enableStereo(false); + + // perform deferred lighting, rendering to free fbo + auto framebufferCache = DependencyManager::get(); + auto textureCache = DependencyManager::get(); + auto deferredLightingEffect = DependencyManager::get(); + + QSize framebufferSize = framebufferCache->getFrameBufferSize(); + + // binding the first framebuffer + auto lightingFBO = framebufferCache->getLightingFramebuffer(); + batch.setFramebuffer(lightingFBO); + + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + + + // Bind the G-Buffer surfaces + batch.setResourceTexture(DEFERRED_BUFFER_COLOR_UNIT, framebufferCache->getDeferredColorTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_NORMAL_UNIT, framebufferCache->getDeferredNormalTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_EMISSIVE_UNIT, framebufferCache->getDeferredSpecularTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_DEPTH_UNIT, framebufferCache->getPrimaryDepthTexture()); + + // FIXME: Different render modes should have different tasks + if (args->_renderMode == RenderArgs::DEFAULT_RENDER_MODE && deferredLightingEffect->isAmbientOcclusionEnabled()) { + batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, framebufferCache->getOcclusionTexture()); + } else { + // need to assign the white texture if ao is off + batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, textureCache->getWhiteTexture()); + } + + assert(deferredLightingEffect->getLightStage().lights.size() > 0); + const auto& globalShadow = deferredLightingEffect->getLightStage().lights[0]->shadow; + + // Bind the shadow buffer + batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow.map); + + // THe main viewport is assumed to be the mono viewport (or the 2 stereo faces side by side within that viewport) + auto monoViewport = args->_viewport; + float sMin = args->_viewport.x / (float)framebufferSize.width(); + float sWidth = args->_viewport.z / (float)framebufferSize.width(); + float tMin = args->_viewport.y / (float)framebufferSize.height(); + float tHeight = args->_viewport.w / (float)framebufferSize.height(); + + // The view frustum is the mono frustum base + auto viewFrustum = args->getViewFrustum(); + + // Eval the mono projection + mat4 monoProjMat; + viewFrustum.evalProjectionMatrix(monoProjMat); + + // The mono view transform + Transform monoViewTransform; + viewFrustum.evalViewTransform(monoViewTransform); + + // THe mono view matrix coming from the mono view transform + glm::mat4 monoViewMat; + monoViewTransform.getMatrix(monoViewMat); + + // Running in stero ? + bool isStereo = args->_context->isStereo(); + int numPasses = 1; + + mat4 projMats[2]; + Transform viewTransforms[2]; + ivec4 viewports[2]; + vec4 clipQuad[2]; + vec2 screenBottomLeftCorners[2]; + vec2 screenTopRightCorners[2]; + vec4 fetchTexcoordRects[2]; + + auto geometryCache = DependencyManager::get(); + + if (isStereo) { + numPasses = 2; + + mat4 eyeViews[2]; + args->_context->getStereoProjections(projMats); + args->_context->getStereoViews(eyeViews); + + float halfWidth = 0.5f * sWidth; + + for (int i = 0; i < numPasses; i++) { + // In stereo, the 2 sides are layout side by side in the mono viewport and their width is half + int sideWidth = monoViewport.z >> 1; + viewports[i] = ivec4(monoViewport.x + (i * sideWidth), monoViewport.y, sideWidth, monoViewport.w); + + auto sideViewMat = monoViewMat * glm::inverse(eyeViews[i]); + // viewTransforms[i].evalFromRawMatrix(sideViewMat); + viewTransforms[i] = monoViewTransform; + viewTransforms[i].postTranslate(-glm::vec3((eyeViews[i][3])));// evalFromRawMatrix(sideViewMat); + + clipQuad[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); + screenBottomLeftCorners[i] = glm::vec2(-1.0f + i * 1.0f, -1.0f); + screenTopRightCorners[i] = glm::vec2(i * 1.0f, 1.0f); + + fetchTexcoordRects[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); + } + } else { + + viewports[0] = monoViewport; + projMats[0] = monoProjMat; + + viewTransforms[0] = monoViewTransform; + + clipQuad[0] = glm::vec4(sMin, tMin, sWidth, tHeight); + screenBottomLeftCorners[0] = glm::vec2(-1.0f, -1.0f); + screenTopRightCorners[0] = glm::vec2(1.0f, 1.0f); + + fetchTexcoordRects[0] = glm::vec4(sMin, tMin, sWidth, tHeight); + } + + auto eyePoint = viewFrustum.getPosition(); + float nearRadius = glm::distance(eyePoint, viewFrustum.getNearTopLeft()); + + batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, frameTransform->getFrameTransformBuffer()); + + + + // Render in this side's viewport + // batch.setViewportTransform(viewports[side]); + // batch.setStateScissorRect(viewports[side]); + int side = 0; + glm::vec2 topLeft(-1.0f, -1.0f); + glm::vec2 bottomRight(1.0f, 1.0f); + glm::vec2 texCoordTopLeft(clipQuad[side].x, clipQuad[side].y); + glm::vec2 texCoordBottomRight(clipQuad[side].x + clipQuad[side].z, clipQuad[side].y + clipQuad[side].w); + + // First Global directional light and ambient pass + { + auto& program = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadow : deferredLightingEffect->_directionalLight; + LightLocationsPtr locations = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadowLocations : deferredLightingEffect->_directionalLightLocations; + const auto& keyLight = deferredLightingEffect->_allocatedLights[deferredLightingEffect->_globalLights.front()]; + + // Setup the global directional pass pipeline + { + if (deferredLightingEffect->_shadowMapEnabled) { + if (keyLight->getAmbientMap()) { + program = deferredLightingEffect->_directionalSkyboxLightShadow; + locations = deferredLightingEffect->_directionalSkyboxLightShadowLocations; + } else { + program = deferredLightingEffect->_directionalAmbientSphereLightShadow; + locations = deferredLightingEffect->_directionalAmbientSphereLightShadowLocations; + } + } else { + if (keyLight->getAmbientMap()) { + program = deferredLightingEffect->_directionalSkyboxLight; + locations = deferredLightingEffect->_directionalSkyboxLightLocations; + } else { + program = deferredLightingEffect->_directionalAmbientSphereLight; + locations = deferredLightingEffect->_directionalAmbientSphereLightLocations; + } + } + + if (locations->shadowTransformBuffer >= 0) { + batch.setUniformBuffer(locations->shadowTransformBuffer, globalShadow.getBuffer()); + } + batch.setPipeline(program); + } + + { // Setup the global lighting + deferredLightingEffect->setupKeyLightBatch(batch, locations->lightBufferUnit, SKYBOX_MAP_UNIT); + } + + { + batch.setModelTransform(Transform()); + batch.setProjectionTransform(glm::mat4()); + batch.setViewTransform(Transform()); + + glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); + //geometryCache->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + + if (keyLight->getAmbientMap()) { + batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); + } + } + + }); + +} + +void RenderDeferredGlobal::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { + + auto args = renderContext->args; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + + }); + +} + +void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { + +} + +void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { + auto args = renderContext->args; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + // Probably not necessary in the long run because the gpu layer would unbound this texture if used as render target + batch.setResourceTexture(DEFERRED_BUFFER_COLOR_UNIT, nullptr); + batch.setResourceTexture(DEFERRED_BUFFER_NORMAL_UNIT, nullptr); + batch.setResourceTexture(DEFERRED_BUFFER_EMISSIVE_UNIT, nullptr); + batch.setResourceTexture(DEFERRED_BUFFER_DEPTH_UNIT, nullptr); + batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, nullptr); + batch.setResourceTexture(SHADOW_MAP_UNIT, nullptr); + batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); + + batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, nullptr); + }); + + auto deferredLightingEffect = DependencyManager::get(); + + // End of the Lighting pass + if (!deferredLightingEffect->_pointLights.empty()) { + deferredLightingEffect->_pointLights.clear(); + } + if (!deferredLightingEffect->_spotLights.empty()) { + deferredLightingEffect->_spotLights.clear(); + } +} + + + +void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFrameTransformPointer& deferredTransform) { +// DependencyManager::get()->render(renderContext, deferredTransform); + + setupJob.run(sceneContext, renderContext, deferredTransform); + + cleanupJob.run(sceneContext, renderContext); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 871493f076..a8bf7a8caf 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -97,19 +97,11 @@ private: std::vector _globalLights; std::vector _pointLights; std::vector _spotLights; - - // Class describing the uniform buffer with all the parameters common to the deferred shaders - class DeferredTransform { - public: - glm::mat4 projection; - glm::mat4 viewInverse; - float stereoSide { 0.f }; - float spareA, spareB, spareC; - - DeferredTransform() {} - }; - typedef gpu::BufferView UniformBufferView; - UniformBufferView _deferredTransformBuffer[2]; + + friend class RenderDeferredSetup; + friend class RenderDeferredGlobal; + friend class RenderDeferredLocals; + friend class RenderDeferredCleanup; }; class PrepareDeferred { @@ -119,11 +111,44 @@ public: using JobModel = render::Job::Model; }; +class RenderDeferredSetup { +public: + using JobModel = render::Job::ModelI; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); +}; + +class RenderDeferredGlobal { +public: + using JobModel = render::Job::ModelI; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); +}; + +class RenderDeferredLocals { +public: + using JobModel = render::Job::ModelI; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); +}; + + +class RenderDeferredCleanup { +public: + using JobModel = render::Job::Model; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); +}; + class RenderDeferred { public: using JobModel = render::Job::ModelI; void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); + + RenderDeferredSetup setupJob; + + RenderDeferredCleanup cleanupJob; }; #endif // hifi_DeferredLightingEffect_h From 74a3db4e1c488ece88e793e020e4d62133e568af Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 17 Jun 2016 11:14:56 -0700 Subject: [PATCH 0602/1237] When setting Reticle.depth, don't assume that we're in the center of the HUD. --- .../controllers/handControllerPointer.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index e87f5da946..0f1e23b45c 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -153,9 +153,8 @@ function isPointingAtOverlay(optionalHudPosition2d) { } // Generalized HUD utilities, with or without HMD: -// These two "vars" are for documentation. Do not change their values! -var SPHERICAL_HUD_DISTANCE = 1; // meters. -var PLANAR_PERPENDICULAR_HUD_DISTANCE = SPHERICAL_HUD_DISTANCE; +// This "var" is for documentation. Do not change the value! +var PLANAR_PERPENDICULAR_HUD_DISTANCE = 1; function calculateRayUICollisionPoint(position, direction) { // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. if (HMD.active) { @@ -274,6 +273,11 @@ function expireMouseCursor(now) { Reticle.visible = false; } } +function hudReticleDistance() { // 3d distance from camera to the reticle position on hud + // (The camera is only in the center of the sphere on reset.) + var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position); + return Vec3.distance(reticlePositionOnHUD, HMD.position); +} function onMouseMove() { // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. if (ignoreMouseActivity()) { @@ -283,11 +287,10 @@ function onMouseMove() { if (HMD.active) { // set depth updateSeeking(); if (isPointingAtOverlay()) { - Reticle.setDepth(SPHERICAL_HUD_DISTANCE); // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + Reticle.depth = hudReticleDistance(); } else { var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); - var depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; - Reticle.setDepth(depth); + Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; } } updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. @@ -493,8 +496,8 @@ function update() { setReticlePosition(hudPoint2d); // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { - if (HMD.active) { // Doesn't hurt anything without the guard, but consider it documentation. - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + if (HMD.active) { + Reticle.depth = hudReticleDistance(); } return turnOffVisualization(true); } From 5f7e49b0870f72239e07ad5d7c575e9461c04768 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 17 Jun 2016 11:50:56 -0700 Subject: [PATCH 0603/1237] possible fix, needs testing --- libraries/avatars/src/AvatarData.cpp | 25 ++++++++----------------- libraries/render-utils/src/Model.cpp | 3 --- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c98fec0d7d..33a6d1305f 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -269,7 +269,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { _lastSentJointData.resize(_jointData.size()); for (int i=0; i < _jointData.size(); i++) { - const JointData& data = _jointData.at(i); + const JointData& data = _jointData[i]; if (sendAll || _lastSentJointData[i].rotation != data.rotation) { if (sendAll || !cullSmallChanges || @@ -294,7 +294,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { validityBit = 0; validity = *validityPosition++; for (int i = 0; i < _jointData.size(); i ++) { - const JointData& data = _jointData[ i ]; + const JointData& data = _jointData[i]; if (validity & (1 << validityBit)) { destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); } @@ -317,7 +317,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { float maxTranslationDimension = 0.0; for (int i=0; i < _jointData.size(); i++) { - const JointData& data = _jointData.at(i); + const JointData& data = _jointData[i]; if (sendAll || _lastSentJointData[i].translation != data.translation) { if (sendAll || !cullSmallChanges || @@ -348,7 +348,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { validityBit = 0; validity = *validityPosition++; for (int i = 0; i < _jointData.size(); i ++) { - const JointData& data = _jointData[ i ]; + const JointData& data = _jointData[i]; if (validity & (1 << validityBit)) { destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); @@ -425,7 +425,6 @@ bool AvatarData::shouldLogError(const quint64& now) { // read data in packet starting at byte offset and return number of bytes parsed int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { - // lazily allocate memory for HeadData in case we're not an Avatar instance if (!_headData) { _headData = new HeadData(this); @@ -575,14 +574,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } } - // If all the rotations were sent, this is a full packet - bool fullPacket = numValidJointRotations == numJoints; - - if (fullPacket) { - _hasNewJointRotations = true; - _hasNewJointTranslations = true; - } - // each joint rotation is stored in 6 bytes. const int COMPRESSED_QUATERNION_SIZE = 6; PACKET_READ_CHECK(JointRotations, numValidJointRotations * COMPRESSED_QUATERNION_SIZE); @@ -677,7 +668,9 @@ void AvatarData::setJointData(int index, const glm::quat& rotation, const glm::v } JointData& data = _jointData[index]; data.rotation = rotation; + data.rotationSet = true; data.translation = translation; + data.translationSet = true; } void AvatarData::clearJointData(int index) { @@ -782,6 +775,7 @@ void AvatarData::setJointRotation(int index, const glm::quat& rotation) { } JointData& data = _jointData[index]; data.rotation = rotation; + data.rotationSet = true; } void AvatarData::setJointTranslation(int index, const glm::vec3& translation) { @@ -797,6 +791,7 @@ void AvatarData::setJointTranslation(int index, const glm::vec3& translation) { } JointData& data = _jointData[index]; data.translation = translation; + data.translationSet = true; } void AvatarData::clearJointData(const QString& name) { @@ -866,7 +861,6 @@ void AvatarData::setJointTranslations(QVector jointTranslations) { "setJointTranslations", Qt::BlockingQueuedConnection, Q_ARG(QVector, jointTranslations)); } - if (_jointData.size() < jointTranslations.size()) { _jointData.resize(jointTranslations.size()); } @@ -1060,8 +1054,6 @@ void AvatarData::setJointMappingsFromNetworkReply() { _jointIndices.insert(_jointNames.at(i), i + 1); } - qDebug() << "set joint mapping froms network reply, num joints: " << _jointIndices.size(); - networkReply->deleteLater(); } @@ -1460,7 +1452,6 @@ void AvatarData::fromJson(const QJsonObject& json) { auto joint = jointDataFromJsonValue(jointJson); jointArray.push_back(joint); setJointData(i, joint.rotation, joint.translation); - _jointData[i].rotationSet = true; // Have to do that to broadcast the avatar new pose i++; } setRawJointData(jointArray); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 2112e64a39..ded1184c24 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -254,7 +254,6 @@ bool Model::updateGeometry() { _needsReload = false; if (_rig->jointStatesEmpty() && getFBXGeometry().joints.size() > 0) { - qDebug() << "initJointStates, num joints: " << getFBXGeometry().joints.size(); initJointStates(); const FBXGeometry& fbxGeometry = getFBXGeometry(); @@ -818,9 +817,7 @@ void Model::setURL(const QUrl& url) { invalidCalculatedMeshBoxes(); deleteGeometry(); - if (_geometry && _geometry->getGeometry()) qDebug() << "geometry1: " << _geometry->getGeometry()->getGeometry().joints.size(); _geometry = DependencyManager::get()->getGeometry(url); - if (_geometry && _geometry->getGeometry()) qDebug() << "geometry2: " << _geometry->getGeometry()->getGeometry().joints.size(); onInvalidate(); } From 7d602986cdd9688172cc739901096aa5ed4febe9 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 17 Jun 2016 12:01:02 -0700 Subject: [PATCH 0604/1237] removed multiple interface instances hack which you should never do anyways because it breaks everything --- interface/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index f0c7938b2a..8fc0384aee 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -90,11 +90,11 @@ int main(int argc, const char* argv[]) { qDebug() << "Interface instance appears to be running, exiting"; - //return EXIT_SUCCESS; + return EXIT_SUCCESS; } #ifdef Q_OS_WIN - //return EXIT_SUCCESS; + return EXIT_SUCCESS; #endif } From e04d86a75a202d9ad108c70e9266429a29ed05e2 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 17 Jun 2016 12:10:26 -0700 Subject: [PATCH 0605/1237] Change rendering of reticle when in HMD mode Properly take the overlay model matrix into account. Reticle depth is now relative to the eye, not relative to the overlay sphere. --- .../src/display-plugins/CompositorHelper.cpp | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 2d3c79071f..a17fe2fd49 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -409,30 +409,25 @@ void CompositorHelper::updateTooltips() { //} } +// eyePose and headPosition are in sensor space. +// the resulting matrix should be in view space. glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const glm::vec3& headPosition) const { glm::mat4 result; if (isHMD()) { - vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize); - auto reticlePosition = getReticlePosition(); - auto spherical = overlayToSpherical(reticlePosition); - // The pointer transform relative to the sensor - auto pointerTransform = glm::mat4_cast(quat(vec3(-spherical.y, spherical.x, 0.0f))) * glm::translate(mat4(), vec3(0, 0, -1)); - float reticleDepth = getReticleDepth(); - if (reticleDepth != 1.0f) { - // Cursor position in UI space - auto cursorPosition = vec3(pointerTransform[3]) / pointerTransform[3].w; - // Ray to the cursor, in UI space - auto cursorRay = glm::normalize(cursorPosition - headPosition) * reticleDepth; - // Move the ray to be relative to the head pose - pointerTransform[3] = vec4(cursorRay + headPosition, 1); - // Scale up the cursor because of distance - reticleScale *= reticleDepth; + vec2 spherical = overlayToSpherical(getReticlePosition()); + vec3 overlaySurfacePoint = getPoint(spherical.x, spherical.y); // overlay space + vec3 sensorSurfacePoint = _modelTransform.transform(overlaySurfacePoint); // sensor space + vec3 d = sensorSurfacePoint - headPosition; + vec3 reticlePosition; + if (glm::length(d) >= EPSILON) { + d = glm::normalize(d); + } else { + d = glm::normalize(overlaySurfacePoint); } - glm::mat4 overlayXfm; - _modelTransform.getMatrix(overlayXfm); - pointerTransform = overlayXfm * pointerTransform; - pointerTransform = glm::inverse(eyePose) * pointerTransform; - result = glm::scale(pointerTransform, reticleScale); + reticlePosition = headPosition + (d * getReticleDepth()); + quat reticleOrientation = quat(vec3(-spherical.y, spherical.x, 0.0f)); + vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize * getReticleDepth()); + return glm::inverse(eyePose) * createMatFromScaleQuatAndPos(reticleScale, reticleOrientation, reticlePosition); } else { static const float CURSOR_PIXEL_SIZE = 32.0f; const auto canvasSize = vec2(toGlm(_renderingWidget->size()));; From ec615caa80d66ec3e7f2544a904951ef85856522 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 17 Jun 2016 10:56:35 -0700 Subject: [PATCH 0606/1237] Support toolbar API --- interface/resources/qml/AssetServer.qml | 2 +- interface/resources/qml/Browser.qml | 2 +- interface/resources/qml/InfoView.qml | 2 +- interface/resources/qml/LoginDialog.qml | 2 +- interface/resources/qml/QmlWebWindow.qml | 2 +- interface/resources/qml/ToolWindow.qml | 2 +- interface/resources/qml/UpdateDialog.qml | 2 +- .../qml/dialogs/PreferencesDialog.qml | 2 +- .../qml/dialogs/preferences/AvatarBrowser.qml | 15 +- interface/resources/qml/hifi/Desktop.qml | 46 +++-- .../resources/qml/hifi/ToggleHudButton.qml | 38 ----- .../qml/hifi/dialogs/AttachmentsDialog.qml | 2 +- .../qml/hifi/dialogs/ModelBrowserDialog.qml | 2 +- .../qml/hifi/dialogs/RunningScripts.qml | 2 +- .../qml/hifi/dialogs/SnapshotShareDialog.qml | 2 +- .../resources/qml/hifi/toolbars/Toolbar.qml | 117 +++++++++++++ .../qml/hifi/toolbars/ToolbarButton.qml | 39 +++++ interface/resources/qml/windows/Frame.qml | 2 +- .../resources/qml/windows/ModalWindow.qml | 2 +- .../resources/qml/windows/ScrollingWindow.qml | 157 ++++++++++++++++++ interface/resources/qml/windows/ToolFrame.qml | 49 ++++++ interface/resources/qml/windows/Window.qml | 146 ++-------------- interface/src/Application.cpp | 3 + .../scripting/ToolbarScriptingInterface.cpp | 124 ++++++++++++++ .../src/scripting/ToolbarScriptingInterface.h | 26 +++ scripts/defaultScripts.js | 3 +- scripts/developer/tests/toolbarTest.js | 118 +++++++++++++ .../system/assets/images/tools/microphone.svg | 13 ++ scripts/system/examples.js | 98 ++--------- scripts/system/goto.js | 42 ++--- scripts/system/hmd.js | 55 +++--- scripts/system/mute.js | 29 ++++ tests/ui/qml/Stubs.qml | 12 ++ tests/ui/qml/main.qml | 89 +++++++--- tests/ui/qmlscratch.pro | 1 + tests/ui/src/main.cpp | 3 + 36 files changed, 870 insertions(+), 381 deletions(-) delete mode 100644 interface/resources/qml/hifi/ToggleHudButton.qml create mode 100644 interface/resources/qml/hifi/toolbars/Toolbar.qml create mode 100644 interface/resources/qml/hifi/toolbars/ToolbarButton.qml create mode 100644 interface/resources/qml/windows/ScrollingWindow.qml create mode 100644 interface/resources/qml/windows/ToolFrame.qml create mode 100644 interface/src/scripting/ToolbarScriptingInterface.cpp create mode 100644 interface/src/scripting/ToolbarScriptingInterface.h create mode 100644 scripts/developer/tests/toolbarTest.js create mode 100644 scripts/system/assets/images/tools/microphone.svg create mode 100644 scripts/system/mute.js diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 5b02020272..c9b6305258 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -18,7 +18,7 @@ import "controls-uit" as HifiControls import "windows" import "dialogs" -Window { +ScrollingWindow { id: root objectName: "AssetServer" title: "Asset Browser" diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 01e1bb7680..8c8cf05444 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -6,7 +6,7 @@ import "controls-uit" import "styles-uit" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } title: "Browser" diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 0f17a88614..f18969fb2f 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -14,7 +14,7 @@ import Hifi 1.0 as Hifi import "controls-uit" import "windows" as Windows -Windows.Window { +Windows.ScrollingWindow { id: root width: 800 height: 800 diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index f5030cb88d..f75e83e36e 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -14,7 +14,7 @@ import "controls" import "styles" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } objectName: "LoginDialog" diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 09c3bd7f28..542b44b95e 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -17,7 +17,7 @@ import "windows" as Windows import "controls-uit" as Controls import "styles-uit" -Windows.Window { +Windows.ScrollingWindow { id: root HifiConstants { id: hifi } title: "WebWindow" diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index faa96fac5a..bbfc74493d 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -19,7 +19,7 @@ import "windows" import "controls-uit" import "styles-uit" -Window { +ScrollingWindow { id: toolWindow resizable: true objectName: "ToolWindow" diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 52fdab25f9..91dc210eda 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -7,7 +7,7 @@ import "controls-uit" import "styles-uit" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } objectName: "UpdateDialog" diff --git a/interface/resources/qml/dialogs/PreferencesDialog.qml b/interface/resources/qml/dialogs/PreferencesDialog.qml index 398e0abd8e..5278118a22 100644 --- a/interface/resources/qml/dialogs/PreferencesDialog.qml +++ b/interface/resources/qml/dialogs/PreferencesDialog.qml @@ -16,7 +16,7 @@ import "../styles-uit" import "../windows" import "preferences" -Window { +ScrollingWindow { id: root title: "Preferences" resizable: true diff --git a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml index 90d2dc5284..16d25b3c4c 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml @@ -23,15 +23,10 @@ Windows.Window { resizable: true modality: Qt.ApplicationModal - Item { - width: pane.contentWidth - implicitHeight: pane.scrollHeight - - Controls.WebView { - id: webview - anchors.fill: parent - url: "https://metaverse.highfidelity.com/marketplace?category=avatars" - focus: true - } + Controls.WebView { + id: webview + anchors.fill: parent + url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + focus: true } } diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index e127a235e6..c14d55cb00 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -1,10 +1,12 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1; +import Qt.labs.settings 1.0 import "../desktop" import ".." import "." +import "./toolbars" Desktop { id: desktop @@ -20,13 +22,6 @@ Desktop { acceptedButtons: Qt.NoButton } - Component.onCompleted: { - WebEngine.settings.javascriptCanOpenWindows = true; - WebEngine.settings.javascriptCanAccessClipboard = false; - WebEngine.settings.spatialNavigationEnabled = false; - WebEngine.settings.localContentCanAccessRemoteUrls = true; - } - // The tool window, one instance property alias toolWindow: toolWindow ToolWindow { id: toolWindow } @@ -49,11 +44,40 @@ Desktop { } } + property var toolbars: ({}) + Component { id: toolbarBuilder; Toolbar { } } - ToggleHudButton { - anchors.bottom: parent.bottom - anchors.bottomMargin: 32 - anchors.horizontalCenter: parent.horizontalCenter + Component.onCompleted: { + WebEngine.settings.javascriptCanOpenWindows = true; + WebEngine.settings.javascriptCanAccessClipboard = false; + WebEngine.settings.spatialNavigationEnabled = false; + WebEngine.settings.localContentCanAccessRemoteUrls = true; + + var sysToolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); + //toolbars[sysToolbar.objectName] = sysToolbar + var toggleHudButton = sysToolbar.addButton({ + imageURL: "../../../icons/hud-01.svg", + visible: true, + + }); + toggleHudButton.yOffset = Qt.binding(function(){ + return desktop.pinned ? 50 : 0 + }); + toggleHudButton.clicked.connect(function(){ + console.log("Clicked on hud button") + var overlayMenuItem = "Overlays" + MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)); + }); + } + + // Create or fetch a toolbar with the given name + function getToolbar(name) { + var result = toolbars[name]; + if (!result) { + result = toolbars[name] = toolbarBuilder.createObject(desktop, {}); + result.objectName = name; + } + return result; } } diff --git a/interface/resources/qml/hifi/ToggleHudButton.qml b/interface/resources/qml/hifi/ToggleHudButton.qml deleted file mode 100644 index 63c056b352..0000000000 --- a/interface/resources/qml/hifi/ToggleHudButton.qml +++ /dev/null @@ -1,38 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -import "../windows" - -Window { - //frame: HiddenFrame {} - hideBackground: true - resizable: false - destroyOnCloseButton: false - destroyOnHidden: false - closable: false - shown: true - pinned: true - width: 50 - height: 50 - clip: true - visible: true - // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' - activator: Item {} - - Item { - width: 50 - height: 50 - Image { - y: desktop.pinned ? -50 : 0 - id: hudToggleImage - source: "../../icons/hud-01.svg" - } - MouseArea { - readonly property string overlayMenuItem: "Overlays" - anchors.fill: parent - onClicked: MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)); - } - } -} - - diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 7b3a368bb0..15467f8021 100755 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -9,7 +9,7 @@ import "../../controls-uit" as HifiControls import "../../windows" import "attachments" -Window { +ScrollingWindow { id: root title: "Attachments" objectName: "AttachmentsDialog" diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index 29f0498f59..aeffb8e4bf 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -11,7 +11,7 @@ import "../../styles-uit" import "../../controls-uit" as HifiControls import "../../windows" -Window { +ScrollingWindow { id: root resizable: true width: 600 diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index ccf2413421..5457caccf1 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -17,7 +17,7 @@ import "../../styles-uit" import "../../controls-uit" as HifiControls import "../../windows" -Window { +ScrollingWindow { id: root objectName: "RunningScripts" title: "Running Scripts" diff --git a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml index f99b770a78..3dacb3b39c 100644 --- a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml +++ b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml @@ -7,7 +7,7 @@ import "../../windows" import "../../js/Utils.js" as Utils import "../models" -Window { +ScrollingWindow { id: root resizable: true width: 516 diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml new file mode 100644 index 0000000000..35c816569b --- /dev/null +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -0,0 +1,117 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../windows" +import "." + +Window { + id: window + frame: ToolFrame { } + hideBackground: true + resizable: false + destroyOnCloseButton: false + destroyOnHidden: false + closable: false + shown: true + pinned: true + width: content.width + height: content.height + visible: true + // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' + activator: Item {} + property bool horizontal: true + property real buttonSize: 50; + property var buttons: [] + property var container: horizontal ? row : column + + Settings { + category: "toolbar/" + window.objectName + property alias x: window.x + property alias y: window.y + } + + onHorizontalChanged: { + var oldParent = horizontal ? column : row; + var newParent = horizontal ? row : column; + var move = []; + + var i; + for (i in oldParent.children) { + var child = oldParent.children[i]; + if (child.spacer) { + continue; + } + move.push(oldParent.children[i]); + } + for (i in move) { + move[i].parent = newParent; + if (horizontal) { + move[i].y = 0 + } else { + move[i].x = 0 + } + } + fixSpacers(); + } + + Item { + id: content + implicitHeight: horizontal ? row.height : column.height + implicitWidth: horizontal ? row.width : column.width + Row { + id: row + spacing: 6 + visible: window.horizontal + Rectangle{ readonly property bool spacer: true; id: rowSpacer1; width: 1; height: row.height } + Rectangle{ readonly property bool spacer: true; id: rowSpacer2; width: 1; height: row.height } + Rectangle{ readonly property bool spacer: true; id: rowSpacer3; width: 1; height: row.height } + Rectangle{ readonly property bool spacer: true; id: rowSpacer4; width: 1; height: row.height } + } + + Column { + id: column + spacing: 6 + visible: !window.horizontal + Rectangle{ readonly property bool spacer: true; id: colSpacer1; width: column.width; height: 1 } + Rectangle{ readonly property bool spacer: true; id: colSpacer2; width: column.width; height: 1 } + Rectangle{ readonly property bool spacer: true; id: colSpacer3; width: column.width; height: 1 } + Rectangle{ readonly property bool spacer: true; id: colSpacer4; width: column.width; height: 1 } + } + + Component { id: toolbarButtonBuilder; ToolbarButton { } } + } + + function addButton(properties) { + properties = properties || {} + + // If a name is specified, then check if there's an existing button with that name + // and return it if so. This will allow multiple clients to listen to a single button, + // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded + if (properties.objectName) { + for (var i in buttons) { + var child = buttons[i]; + if (child.objectName === properties.objectName) { + return child; + } + } + } + + properties.toolbar = this; + var result = toolbarButtonBuilder.createObject(container, properties); + buttons.push(result); + fixSpacers(); + return result; + } + + function fixSpacers() { + colSpacer3.parent = null + colSpacer4.parent = null + rowSpacer3.parent = null + rowSpacer4.parent = null + colSpacer3.parent = column + colSpacer4.parent = column + rowSpacer3.parent = row + rowSpacer4.parent = row + } +} diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml new file mode 100644 index 0000000000..a8514689e8 --- /dev/null +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -0,0 +1,39 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + id: button + property alias imageURL: image.source + property alias alpha: button.opacity + property var subImage; + property int yOffset: 0 + property var toolbar; + property real size: 50 // toolbar ? toolbar.buttonSize : 50 + width: size; height: size + clip: true + + Component.onCompleted: { + if (subImage) { + if (subImage.y) { + yOffset = subImage.y; + } + } + } + + signal clicked() + + Image { + id: image + y: -button.yOffset; + width: parent.width + } + + MouseArea { + anchors.fill: parent + onClicked: { + console.log("Clicked on button " + image.source + " named " + button.objectName) + button.clicked(); + } + } +} + diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 9519a44cf0..bc8ecc35ec 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -80,7 +80,7 @@ Item { border.width: 3 border.color: hifi.colors.white50 radius: hifi.dimensions.borderRadius - visible: window ? !pane.visible : false + visible: window ? !window.content.visible : false } MouseArea { diff --git a/interface/resources/qml/windows/ModalWindow.qml b/interface/resources/qml/windows/ModalWindow.qml index f79dc9084f..2d56099051 100644 --- a/interface/resources/qml/windows/ModalWindow.qml +++ b/interface/resources/qml/windows/ModalWindow.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import "." -Window { +ScrollingWindow { id: window modality: Qt.ApplicationModal destroyOnCloseButton: true diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml new file mode 100644 index 0000000000..f1dc744344 --- /dev/null +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -0,0 +1,157 @@ +// +// Window.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +// FIXME how do I set the initial position of a window without +// overriding places where the a individual client of the window +// might be setting the position with a Settings{} element? + +// FIXME how to I enable dragging without allowing the window to lay outside +// of the desktop? How do I ensure when the desktop resizes all the windows +// are still at least partially visible? +Window { + id: window + HifiConstants { id: hifi } + children: [ swallower, frame, pane, activator ] + + property var footer: Item { } // Optional static footer at the bottom of the dialog. + + // Scrollable window content. + // FIXME this should not define any visual content in this type. The base window + // type should only consist of logic sized areas, with nothing drawn (although the + // default value for the frame property does include visual decorations) + property var pane: Item { + property bool isScrolling: scrollView.height < scrollView.contentItem.height + property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) + property int scrollHeight: scrollView.height + + anchors.fill: parent + anchors.rightMargin: isScrolling ? 11 : 0 + + Rectangle { + id: contentBackground + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 11 : 0 + color: hifi.colors.baseGray + visible: !window.hideBackground && modality != Qt.ApplicationModal + } + + + LinearGradient { + visible: !window.hideBackground && gradientsSupported && modality != Qt.ApplicationModal + anchors.top: contentBackground.bottom + anchors.left: contentBackground.left + width: contentBackground.width - 1 + height: 4 + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.darkGray } + GradientStop { position: 1.0; color: hifi.colors.darkGray0 } + } + cached: true + } + + ScrollView { + id: scrollView + contentItem: content + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + verticalScrollBarPolicy: Qt.ScrollBarAsNeeded + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 1 : 0 + anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 + + style: ScrollViewStyle { + + padding.right: -7 // Move to right away from content. + + handle: Item { + implicitWidth: 8 + Rectangle { + radius: 4 + color: hifi.colors.white30 + anchors { + fill: parent + leftMargin: 2 // Finesse size and position. + topMargin: 1 + bottomMargin: 1 + } + } + } + + scrollBarBackground: Item { + implicitWidth: 10 + Rectangle { + color: hifi.colors.darkGray30 + radius: 4 + anchors { + fill: parent + topMargin: -1 // Finesse size + bottomMargin: -2 + } + } + } + + incrementControl: Item { + visible: false + } + + decrementControl: Item { + visible: false + } + } + } + + Rectangle { + // Optional non-scrolling footer. + id: footerPane + anchors { + left: parent.left + bottom: parent.bottom + } + width: parent.contentWidth + height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 + color: hifi.colors.baseGray + visible: footer.height > 0 + + Item { + // Horizontal rule. + anchors.fill: parent + + Rectangle { + width: parent.width + height: 1 + y: 1 // Stop displaying content just above horizontal rule/=. + color: hifi.colors.baseGrayShadow + } + + Rectangle { + width: parent.width + height: 1 + y: 2 + color: hifi.colors.baseGrayHighlight + } + } + + Item { + anchors.fill: parent + anchors.topMargin: 3 // Horizontal rule. + children: [ footer ] + } + } + } +} diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml new file mode 100644 index 0000000000..ac1093092e --- /dev/null +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -0,0 +1,49 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +Frame { + HifiConstants { id: hifi } + + Rectangle { + // Dialog frame + id: frameContent + readonly property int frameMargin: 6 + readonly property int frameMarginLeft: frameMargin + readonly property int frameMarginRight: frameMargin + readonly property int frameMarginTop: frameMargin + readonly property int frameMarginBottom: frameMargin + anchors { + topMargin: -frameMargin + leftMargin: -frameMargin + rightMargin: -frameMargin + bottomMargin: -frameMargin + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius / 2 + + // Enable dragging of the window + MouseArea { + anchors.fill: parent + drag.target: window + } + } +} + diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index ed1b820fc8..e3e70c1e74 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -41,7 +41,7 @@ Fadable { implicitHeight: content ? content.height : 0 implicitWidth: content ? content.width : 0 x: desktop.invalid_position; y: desktop.invalid_position; - children: [ swallower, frame, pane, activator ] + children: [ swallower, frame, content, activator ] // // Custom properties @@ -106,10 +106,10 @@ Fadable { // in the window and have a higher Z-order than the content, but follow // the position and size of frame decoration property var activator: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible @@ -124,10 +124,10 @@ Fadable { // to prevent things like mouse wheel events from reaching the application and changing // the camera if the user is scrolling through a list and gets to the end. property var swallower: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 hoverEnabled: true acceptedButtons: Qt.AllButtons enabled: window.visible @@ -140,133 +140,11 @@ Fadable { // Default to a standard frame. Can be overriden to provide custom // frame styles, like a full desktop frame to simulate a modal window - property var frame: DefaultFrame { } - - // Scrollable window content. - // FIXME this should not define any visual content in this type. The base window - // type should only consist of logic sized areas, with nothing drawn (although the - // default value for the frame property does include visual decorations) - property var pane: Item { - property bool isScrolling: scrollView.height < scrollView.contentItem.height - property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) - property int scrollHeight: scrollView.height - - anchors.fill: parent - anchors.rightMargin: isScrolling ? 11 : 0 - - Rectangle { - id: contentBackground - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 11 : 0 - color: hifi.colors.baseGray - visible: !window.hideBackground && modality != Qt.ApplicationModal - } - - - LinearGradient { - visible: !window.hideBackground && gradientsSupported && modality != Qt.ApplicationModal - anchors.top: contentBackground.bottom - anchors.left: contentBackground.left - width: contentBackground.width - 1 - height: 4 - start: Qt.point(0, 0) - end: Qt.point(0, 4) - gradient: Gradient { - GradientStop { position: 0.0; color: hifi.colors.darkGray } - GradientStop { position: 1.0; color: hifi.colors.darkGray0 } - } - cached: true - } - - ScrollView { - id: scrollView - contentItem: content - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - verticalScrollBarPolicy: Qt.ScrollBarAsNeeded - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 1 : 0 - anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 - - style: ScrollViewStyle { - - padding.right: -7 // Move to right away from content. - - handle: Item { - implicitWidth: 8 - Rectangle { - radius: 4 - color: hifi.colors.white30 - anchors { - fill: parent - leftMargin: 2 // Finesse size and position. - topMargin: 1 - bottomMargin: 1 - } - } - } - - scrollBarBackground: Item { - implicitWidth: 10 - Rectangle { - color: hifi.colors.darkGray30 - radius: 4 - anchors { - fill: parent - topMargin: -1 // Finesse size - bottomMargin: -2 - } - } - } - - incrementControl: Item { - visible: false - } - - decrementControl: Item { - visible: false - } - } - } - - Rectangle { - // Optional non-scrolling footer. - id: footerPane - anchors { - left: parent.left - bottom: parent.bottom - } - width: parent.contentWidth - height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 - color: hifi.colors.baseGray - visible: footer.height > 0 - - Item { - // Horizontal rule. - anchors.fill: parent - - Rectangle { - width: parent.width - height: 1 - y: 1 // Stop displaying content just above horizontal rule/=. - color: hifi.colors.baseGrayShadow - } - - Rectangle { - width: parent.width - height: 1 - y: 2 - color: hifi.colors.baseGrayHighlight - } - } - - Item { - anchors.fill: parent - anchors.topMargin: 3 // Horizontal rule. - children: [ footer ] - } - } + property var frame: DefaultFrame { + //window: window } + // // Handlers // diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ee365cd5a1..6dc575b572 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -133,6 +133,7 @@ #include "scripting/WebWindowClass.h" #include "scripting/WindowScriptingInterface.h" #include "scripting/ControllerScriptingInterface.h" +#include "scripting/ToolbarScriptingInterface.h" #include "scripting/RatesScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" @@ -437,6 +438,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) @@ -4527,6 +4529,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri RayToOverlayIntersectionResultFromScriptValue); scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp new file mode 100644 index 0000000000..fd96b6a809 --- /dev/null +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -0,0 +1,124 @@ +// +// Created by Bradley Austin Davis on 2016-06-16 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ToolbarScriptingInterface.h" + +#include + +class QmlWrapper : public QObject { + Q_OBJECT +public: + QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) + : QObject(parent), _qmlObject(qmlObject) { + + const QMetaObject *metaobject = qmlObject->metaObject(); + int count = metaobject->propertyCount(); + qDebug() << "Scanning properties for " << qmlObject; + for (int i = 0; i < count; ++i) { + QMetaProperty metaproperty = metaobject->property(i); + const char *name = metaproperty.name(); + qDebug() << "Property " << name; + } + } + + Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + _qmlObject->setProperty(propertyName.toStdString().c_str(), propertyValue); + }); + } + + Q_INVOKABLE void writeProperties(QVariant propertyMap) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + QVariantMap map = propertyMap.toMap(); + for (const QString& key : map.keys()) { + _qmlObject->setProperty(key.toStdString().c_str(), map[key]); + } + }); + } + + Q_INVOKABLE QVariant readProperty(const QString& propertyName) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + return _qmlObject->property(propertyName.toStdString().c_str()); + }); + } + + Q_INVOKABLE QVariant readProperties(const QVariant& propertyList) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + QVariantMap result; + for (const QVariant& property : propertyList.toList()) { + QString propertyString = property.toString(); + result.insert(propertyString, _qmlObject->property(propertyString.toStdString().c_str())); + } + return result; + }); + } + + +protected: + QObject* _qmlObject{ nullptr }; +}; + + +class ToolbarButtonProxy : public QmlWrapper { + Q_OBJECT + +public: + ToolbarButtonProxy(QObject* qmlObject, QObject* parent = nullptr) : QmlWrapper(qmlObject, parent) { + connect(qmlObject, SIGNAL(clicked()), this, SIGNAL(clicked())); + } + +signals: + void clicked(); +}; + +class ToolbarProxy : public QmlWrapper { + Q_OBJECT + +public: + ToolbarProxy(QObject* qmlObject, QObject* parent = nullptr) : QmlWrapper(qmlObject, parent) { } + + Q_INVOKABLE QObject* addButton(const QVariant& properties) { + QVariant resultVar; + bool invokeResult = QMetaObject::invokeMethod(_qmlObject, "addButton", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, properties)); + if (!invokeResult) { + return nullptr; + } + + QObject* rawButton = qvariant_cast(resultVar); + if (!rawButton) { + return nullptr; + } + + return new ToolbarButtonProxy(rawButton, this); + } +}; + + +QObject* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { + auto offscreenUi = DependencyManager::get(); + auto desktop = offscreenUi->getDesktop(); + QVariant resultVar; + bool invokeResult = QMetaObject::invokeMethod(desktop, "getToolbar", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, toolbarId)); + if (!invokeResult) { + return nullptr; + } + + QObject* rawToolbar = qvariant_cast(resultVar); + if (!rawToolbar) { + return nullptr; + } + + return new ToolbarProxy(rawToolbar); +} + + +#include "ToolbarScriptingInterface.moc" \ No newline at end of file diff --git a/interface/src/scripting/ToolbarScriptingInterface.h b/interface/src/scripting/ToolbarScriptingInterface.h new file mode 100644 index 0000000000..d3706cb6e1 --- /dev/null +++ b/interface/src/scripting/ToolbarScriptingInterface.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2016-06-16 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ToolbarScriptingInterface_h +#define hifi_ToolbarScriptingInterface_h + +#include + +#include + +#include + +class ToolbarProxy; + +class ToolbarScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + Q_INVOKABLE QObject* getToolbar(const QString& toolbarId); +}; + +#endif // hifi_ToolbarScriptingInterface_h diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 218cab8903..bbbe049f39 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -12,9 +12,10 @@ Script.load("system/progress.js"); Script.load("system/away.js"); Script.load("system/users.js"); -Script.load("system/examples.js"); +Script.load("system/mute.js"); Script.load("system/goto.js"); Script.load("system/hmd.js"); +Script.load("system/examples.js"); Script.load("system/edit.js"); Script.load("system/selectAudioDevice.js"); Script.load("system/notifications.js"); diff --git a/scripts/developer/tests/toolbarTest.js b/scripts/developer/tests/toolbarTest.js new file mode 100644 index 0000000000..20f349929f --- /dev/null +++ b/scripts/developer/tests/toolbarTest.js @@ -0,0 +1,118 @@ +var isActive = false; + +var toolBar = (function() { + var that = {}, + toolBar, + activeButton, + newModelButton, + newCubeButton, + newSphereButton, + newLightButton, + newTextButton, + newWebButton, + newZoneButton, + newParticleButton + + var toolIconUrl = Script.resolvePath("assets/images/tools/"); + + function initialize() { + print("Toolbars: " + Toolbars); + toolBar = Toolbars.getToolbar("highfidelity.edit.toolbar"); + print("Toolbar: " + toolBar); + activeButton = toolBar.addButton({ + objectName: "activeButton", + imageURL: toolIconUrl + "edit-01.svg", + visible: true, + alpha: 0.9, + }); + + print("Button " + activeButton); + print("Button signal " + activeButton.clicked); + activeButton.clicked.connect(function(){ + print("Clicked on button " + isActive); + that.setActive(!isActive); + }); + + newModelButton = toolBar.addButton({ + objectName: "newModelButton", + imageURL: toolIconUrl + "model-01.svg", + alpha: 0.9, + visible: false + }); + + newCubeButton = toolBar.addButton({ + objectName: "newCubeButton", + imageURL: toolIconUrl + "cube-01.svg", + alpha: 0.9, + visible: false + }); + + newSphereButton = toolBar.addButton({ + objectName: "newSphereButton", + imageURL: toolIconUrl + "sphere-01.svg", + alpha: 0.9, + visible: false + }); + + newLightButton = toolBar.addButton({ + objectName: "newLightButton", + imageURL: toolIconUrl + "light-01.svg", + alpha: 0.9, + visible: false + }); + + newTextButton = toolBar.addButton({ + objectName: "newTextButton", + imageURL: toolIconUrl + "text-01.svg", + alpha: 0.9, + visible: false + }); + + newWebButton = toolBar.addButton({ + objectName: "newWebButton", + imageURL: toolIconUrl + "web-01.svg", + alpha: 0.9, + visible: false + }); + + newZoneButton = toolBar.addButton({ + objectName: "newZoneButton", + imageURL: toolIconUrl + "zone-01.svg", + alpha: 0.9, + visible: false + }); + + newParticleButton = toolBar.addButton({ + objectName: "newParticleButton", + imageURL: toolIconUrl + "particle-01.svg", + alpha: 0.9, + visible: false + }); + + that.setActive(false); + newModelButton.clicked(); + } + + that.setActive = function(active) { + if (active != isActive) { + isActive = active; + that.showTools(isActive); + } + }; + + // Sets visibility of tool buttons, excluding the power button + that.showTools = function(doShow) { + newModelButton.writeProperty('visible', doShow); + newCubeButton.writeProperty('visible', doShow); + newSphereButton.writeProperty('visible', doShow); + newLightButton.writeProperty('visible', doShow); + newTextButton.writeProperty('visible', doShow); + newWebButton.writeProperty('visible', doShow); + newZoneButton.writeProperty('visible', doShow); + newModelButton.writeProperty('visible', doShow); + newParticleButton.writeProperty('visible', doShow); + }; + + initialize(); + return that; +}()); diff --git a/scripts/system/assets/images/tools/microphone.svg b/scripts/system/assets/images/tools/microphone.svg new file mode 100644 index 0000000000..bd5e8afac7 --- /dev/null +++ b/scripts/system/assets/images/tools/microphone.svg @@ -0,0 +1,13 @@ + + + image/svg+xml + + + Layer 1 + + + + Mute + + + \ No newline at end of file diff --git a/scripts/system/examples.js b/scripts/system/examples.js index 6f4268182c..f850a9789e 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -9,10 +9,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include([ - "libraries/toolBars.js", -]); - var toolIconUrl = Script.resolvePath("assets/images/tools/"); var EXAMPLES_URL = "https://metaverse.highfidelity.com/examples"; @@ -52,87 +48,21 @@ function toggleExamples() { } } -var toolBar = (function() { - var that = {}, - toolBar, - browseExamplesButton; +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - function initialize() { - toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.examples.toolbar", function(windowDimensions, toolbar) { - return { - x: (windowDimensions.x / 2) + (Tool.IMAGE_WIDTH * 2), - y: windowDimensions.y - }; - }, { - x: -toolWidth / 2, - y: -TOOLBAR_MARGIN_Y - toolHeight - }); - browseExamplesButton = toolBar.addTool({ - imageURL: toolIconUrl + "examples-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - visible: true, - showButtonDown: true - }); +var browseExamplesButton = toolBar.addButton({ + imageURL: toolIconUrl + "examples-01.svg", + objectName: "examples", + yOffset: 50, + alpha: 0.9, +}); - toolBar.showTool(browseExamplesButton, true); - } +var browseExamplesButtonDown = false; - var browseExamplesButtonDown = false; - that.mousePressEvent = function(event) { - var clickedOverlay, - url, - file; +browseExamplesButton.clicked.connect(function(){ + toggleExamples(); +}); - if (!event.isLeftButton) { - // if another mouse button than left is pressed ignore it - return false; - } - - clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - - if (browseExamplesButton === toolBar.clicked(clickedOverlay)) { - toggleExamples(); - return true; - } - - return false; - }; - - that.mouseReleaseEvent = function(event) { - var handled = false; - - - if (browseExamplesButtonDown) { - var clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - } - - newModelButtonDown = false; - browseExamplesButtonDown = false; - - return handled; - } - - that.cleanup = function() { - toolBar.cleanup(); - }; - - initialize(); - return that; -}()); - -Controller.mousePressEvent.connect(toolBar.mousePressEvent) -Script.scriptEnding.connect(toolBar.cleanup); +Script.scriptEnding.connect(function () { + browseExamplesButton.clicked.disconnect(); +}); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 00b5e912c0..a2ade02a78 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -9,39 +9,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include("libraries/toolBars.js"); +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); -function initialPosition(windowDimensions, toolbar) { - return { - x: (windowDimensions.x / 2) - (Tool.IMAGE_WIDTH * 1), - y: windowDimensions.y - }; -} -var toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.goto.toolbar", initialPosition, { - x: -Tool.IMAGE_WIDTH / 2, - y: -Tool.IMAGE_HEIGHT -}); -var button = toolBar.addTool({ + +var button = toolBar.addButton({ + objectName: "goto", imageURL: Script.resolvePath("assets/images/tools/directory-01.svg"), - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT, - alpha: 0.9, visible: true, - showButtonDown: true + yOffset: 50, + alpha: 0.9, }); -function onMousePress (event) { - if (event.isLeftButton && button === toolBar.clicked(Overlays.getOverlayAtPoint(event))) { - DialogsManager.toggleAddressBar(); - } -}; -Controller.mousePressEvent.connect(onMousePress) + +button.clicked.connect(function(){ + DialogsManager.toggleAddressBar(); +}); + Script.scriptEnding.connect(function () { - Controller.mousePressEvent.disconnect(onMousePress) - toolBar.cleanup(); + button.clicked.disconnect(); }); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 8b91e45676..2965c0d254 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -9,8 +9,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include("libraries/toolBars.js"); - var headset; // The preferred headset. Default to the first one found in the following list. var displayMenuName = "Display"; var desktopMenuItemName = "Desktop"; @@ -20,38 +18,25 @@ var desktopMenuItemName = "Desktop"; } }); -function initialPosition(windowDimensions, toolbar) { - return { - x: (windowDimensions.x / 2) - (Tool.IMAGE_WIDTH * 2.5), - y: windowDimensions.y - }; -} -var toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.hmd.toolbar", initialPosition, { - x: -Tool.IMAGE_WIDTH / 2, - y: -Tool.IMAGE_HEIGHT -}); -var button = toolBar.addTool({ - imageURL: Script.resolvePath("assets/images/tools/hmd-switch-01.svg"), - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT, - alpha: 0.9, - visible: true, - showButtonDown: true -}); -function onMousePress (event) { - if (event.isLeftButton && button === toolBar.clicked(Overlays.getOverlayAtPoint(event))) { +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var button; + +if (headset) { + button = toolBar.addButton({ + objectName: "hmdToggle", + imageURL: Script.resolvePath("assets/images/tools/hmd-switch-01.svg"), + visible: true, + yOffset: 50, + alpha: 0.9, + }); + + button.clicked.connect(function(){ var isDesktop = Menu.isOptionChecked(desktopMenuItemName); Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); - } -}; -Controller.mousePressEvent.connect(onMousePress) -Script.scriptEnding.connect(function () { - Controller.mousePressEvent.disconnect(onMousePress) - toolBar.cleanup(); -}); + }); + + Script.scriptEnding.connect(function () { + button.clicked.disconnect(); + }); +} + diff --git a/scripts/system/mute.js b/scripts/system/mute.js new file mode 100644 index 0000000000..f66b6852ea --- /dev/null +++ b/scripts/system/mute.js @@ -0,0 +1,29 @@ +// +// goto.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + + +var button = toolBar.addButton({ + objectName: "mute", + imageURL: Script.resolvePath("assets/images/tools/microphone.svg"), + visible: true, + alpha: 0.9, +}); + +button.clicked.connect(function(){ + var menuItem = "Mute Microphone"; + Menu.setIsOptionChecked(menuItem, !Menu.isOptionChecked(menuItem)); +}); + +Script.scriptEnding.connect(function () { + button.clicked.disconnect(); +}); diff --git a/tests/ui/qml/Stubs.qml b/tests/ui/qml/Stubs.qml index 8f828a0186..8c1465d54c 100644 --- a/tests/ui/qml/Stubs.qml +++ b/tests/ui/qml/Stubs.qml @@ -23,11 +23,23 @@ Item { function getUsername() { return "Jherico"; } } + Item { + objectName: "GL" + property string vendor: "" + } + Item { objectName: "ApplicationCompositor" property bool reticleOverDesktop: true } + Item { + objectName: "Controller" + function getRecommendedOverlayRect() { + return Qt.rect(0, 0, 1920, 1080); + } + } + Item { objectName: "Preferences" // List of categories obtained by logging categories as they are added in Interface in Preferences::addPreference(). diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 33408fb821..19f6a55bfd 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -25,35 +25,67 @@ ApplicationWindow { property var tabs: []; property var urls: []; + property var toolbar; + property var lastButton; // Window visibility - - Button { - text: "restore all" - onClicked: { - for (var i = 0; i < desktop.windows.length; ++i) { - desktop.windows[i].shown = true - } - } - } - Button { - text: "toggle blue visible" - onClicked: { - blue.shown = !blue.shown - } - } - Button { - text: "toggle blue enabled" - onClicked: { - blue.enabled = !blue.enabled - } - } - Button { text: "toggle desktop" onClicked: desktop.togglePinned() } + Button { + text: "Create Toolbar" + onClicked: testButtons.toolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); + } + + Button { + text: "Toggle Toolbar Direction" + onClicked: testButtons.toolbar.horizontal = !testButtons.toolbar.horizontal + } + + Button { + readonly property var icons: [ + "edit-01.svg", + "model-01.svg", + "cube-01.svg", + "sphere-01.svg", + "light-01.svg", + "text-01.svg", + "web-01.svg", + "zone-01.svg", + "particle-01.svg", + ] + property int iconIndex: 0 + readonly property string toolIconUrl: "file:///C:/Users/bdavi/git/hifi/scripts/system/assets/images/tools/" + text: "Create Button" + onClicked: { + var name = icons[iconIndex]; + var url = toolIconUrl + name; + iconIndex = (iconIndex + 1) % icons.length; + var button = testButtons.lastButton = testButtons.toolbar.addButton({ + imageURL: url, + objectName: name, + subImage: { + y: 50, + }, + alpha: 0.9 + }); + + button.clicked.connect(function(){ + console.log("Clicked on button " + button.imageURL + " alpha " + button.alpha) + }); + } + } + + Button { + text: "Toggle Button Visible" + onClicked: testButtons.lastButton.visible = !testButtons.lastButton.visible + } + + + + // Error alerts /* Button { @@ -106,7 +138,7 @@ ApplicationWindow { */ // Browser - + /* Button { text: "Open Browser" onClicked: builder.createObject(desktop); @@ -114,8 +146,11 @@ ApplicationWindow { Browser {} } } + */ + // file dialog + /* Button { text: "Open Directory" @@ -153,7 +188,6 @@ ApplicationWindow { }) } } - /* */ // tabs @@ -306,6 +340,7 @@ ApplicationWindow { } */ + /* Window { id: blue closable: true @@ -350,6 +385,8 @@ ApplicationWindow { height: green.height; } } + */ + /* Window { id: yellow @@ -379,3 +416,7 @@ ApplicationWindow { } } } + + + + diff --git a/tests/ui/qmlscratch.pro b/tests/ui/qmlscratch.pro index 95be6a480e..151893de2f 100644 --- a/tests/ui/qmlscratch.pro +++ b/tests/ui/qmlscratch.pro @@ -29,6 +29,7 @@ DISTFILES += \ ../../interface/resources/qml/styles-uit/*.qml \ ../../interface/resources/qml/windows/*.qml \ ../../interface/resources/qml/hifi/*.qml \ + ../../interface/resources/qml/hifi/toolbars/*.qml \ ../../interface/resources/qml/hifi/dialogs/*.qml \ ../../interface/resources/qml/hifi/dialogs/preferences/*.qml \ ../../interface/resources/qml/hifi/overlays/*.qml diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 0cabfe28f5..e3cf37ba04 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -86,9 +86,11 @@ int main(int argc, char *argv[]) { setChild(engine, "offscreenFlags"); setChild(engine, "Account"); setChild(engine, "ApplicationCompositor"); + setChild(engine, "Controller"); setChild(engine, "Desktop"); setChild(engine, "ScriptDiscoveryService"); setChild(engine, "HMD"); + setChild(engine, "GL"); setChild(engine, "MenuHelper"); setChild(engine, "Preferences"); setChild(engine, "urlHandler"); @@ -101,3 +103,4 @@ int main(int argc, char *argv[]) { } #include "main.moc" + From caf4a9fa34f0a52b9f4e89293f1106af248f1bb7 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 17 Jun 2016 14:17:32 -0700 Subject: [PATCH 0607/1237] got rid of comparison warning --- libraries/animation/src/Rig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 8f0bd3fd87..8aee2245c0 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1223,7 +1223,7 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { } void Rig::copyJointsFromJointData(const QVector& jointDataVec) { - if (_animSkeleton && jointDataVec.size() == _internalPoseSet._overrideFlags.size()) { + if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._overrideFlags.size()) { // transform all the default poses into rig space. const AnimPose geometryToRigPose(_geometryToRigTransform); From d8a67850f2108fae22898d9b6dc87c71ed9adcc7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 17 Jun 2016 15:17:51 -0700 Subject: [PATCH 0608/1237] treat usernames in permissions-grid on domain-server settings page as case-insensitive --- .../src/DomainServerSettingsManager.cpp | 6 ++--- .../src/DomainServerSettingsManager.h | 6 ++--- libraries/networking/src/NodePermissions.h | 23 ++++++++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 5790eb9178..b03d2c07ae 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -241,7 +241,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } QList> permissionsSets; - permissionsSets << _standardAgentPermissions << _agentPermissions; + permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); foreach (auto permissionsSet, permissionsSets) { foreach (QString userName, permissionsSet.keys()) { if (onlyEditorsAreRezzers) { @@ -267,7 +267,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } void DomainServerSettingsManager::packPermissionsForMap(QString mapName, - QHash agentPermissions, + NodePermissionsMap& agentPermissions, QString keyPath) { QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security"); if (!security || !security->canConvert(QMetaType::QVariantMap)) { @@ -378,7 +378,7 @@ void DomainServerSettingsManager::unpackPermissions() { #ifdef WANT_DEBUG qDebug() << "--------------- permissions ---------------------"; QList> permissionsSets; - permissionsSets << _standardAgentPermissions << _agentPermissions; + permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); foreach (auto permissionSet, permissionsSets) { QHashIterator i(permissionSet); while (i.hasNext()) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 446e9a2eed..ec1d3b637d 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -72,11 +72,11 @@ private: friend class DomainServer; - void packPermissionsForMap(QString mapName, QHash agentPermissions, QString keyPath); + void packPermissionsForMap(QString mapName, NodePermissionsMap& agentPermissions, QString keyPath); void packPermissions(); void unpackPermissions(); - QHash _standardAgentPermissions; // anonymous, logged-in, localhost - QHash _agentPermissions; // specific account-names + NodePermissionsMap _standardAgentPermissions; // anonymous, logged-in, localhost + NodePermissionsMap _agentPermissions; // specific account-names }; #endif // hifi_DomainServerSettingsManager_h diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index c153878a7e..de46a0dae5 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -24,9 +24,9 @@ using NodePermissionsPointer = std::shared_ptr; class NodePermissions { public: NodePermissions() { _id = QUuid::createUuid().toString(); } - NodePermissions(const QString& name) { _id = name; } + NodePermissions(const QString& name) { _id = name.toLower(); } NodePermissions(QMap perms) { - _id = perms["permissions_id"].toString(); + _id = perms["permissions_id"].toString().toLower(); canConnectToDomain = perms["id_can_connect"].toBool(); canAdjustLocks = perms["id_can_adjust_locks"].toBool(); canRezPermanentEntities = perms["id_can_rez"].toBool(); @@ -38,7 +38,7 @@ public: QString getID() const { return _id; } // the _id member isn't authenticated and _username is. - void setUserName(QString userName) { _userName = userName; } + void setUserName(QString userName) { _userName = userName.toLower(); } QString getUserName() { return _userName; } bool isAssignment { false }; @@ -88,6 +88,23 @@ protected: QString _userName; }; + +// wrap QHash in a class that forces all keys to be lowercase +class NodePermissionsMap { +public: + NodePermissionsMap() { } + NodePermissionsPointer& operator[](const QString& key) { return _data[key.toLower()]; } + NodePermissionsPointer operator[](const QString& key) const { return _data.value(key.toLower()); } + bool contains(const QString& key) const { return _data.contains(key.toLower()); } + QList keys() const { return _data.keys(); } + QHash get() { return _data; } + void clear() { _data.clear(); } + +private: + QHash _data; +}; + + const NodePermissions DEFAULT_AGENT_PERMISSIONS; QDebug operator<<(QDebug debug, const NodePermissions& perms); From 37a6d29406ec3821dce48601cec7db10f3b8a9e0 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 17 Jun 2016 17:19:49 -0700 Subject: [PATCH 0609/1237] Finally clean the lighting pass for global and local lights, split it into global and locals and make it work with the stereo single drawcall --- .../stereo/StereoDisplayPlugin.cpp | 2 +- libraries/gpu/src/gpu/Context.cpp | 7 +- libraries/gpu/src/gpu/Context.h | 1 + libraries/gpu/src/gpu/Transform.slh | 11 + .../src/DeferredLightingEffect.cpp | 565 ++++-------------- .../render-utils/src/DeferredLightingEffect.h | 15 +- libraries/render-utils/src/deferred_light.slv | 18 +- .../src/deferred_light_limited.slv | 40 +- .../render-utils/src/deferred_light_spot.slv | 39 +- tests/gpu-test/src/TestWindow.h | 2 +- 10 files changed, 226 insertions(+), 474 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index 6c6716c8fa..d915509d87 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -75,7 +75,7 @@ bool StereoDisplayPlugin::internalActivate() { _container->removeMenu(FRAMERATE); _screen = qApp->primaryScreen(); - _container->setFullscreen(_screen); + // _container->setFullscreen(_screen); return Parent::internalActivate(); } diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index c8e379480d..2c27260331 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -91,6 +91,9 @@ const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived(const Mat4 viewUntranslated = _view; viewUntranslated[3] = Vec4(0.0f, 0.0f, 0.0f, 1.0f); _projectionViewUntranslated = _projection * viewUntranslated; + + _stereoInfo = Vec4(0.0f); + return *this; } @@ -104,7 +107,9 @@ Backend::TransformCamera Backend::TransformCamera::getEyeCamera(int eye, const S } result._projection = _stereo._eyeProjections[eye]; result.recomputeDerived(offsetTransform); - + + result._stereoInfo = Vec4(1.0f, (float) eye, 0.0f, 0.0f); + return result; } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 652338f911..3c9f4aee62 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -104,6 +104,7 @@ public: Mat4 _projection; mutable Mat4 _projectionInverse; Vec4 _viewport; // Public value is int but float in the shader to stay in floats for all the transform computations. + mutable Vec4 _stereoInfo; const Backend::TransformCamera& recomputeDerived(const Transform& xformView) const; TransformCamera getEyeCamera(int eye, const StereoState& stereo, const Transform& xformView) const; diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index 20629c5f32..12a1cd10f1 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -18,6 +18,7 @@ struct TransformCamera { mat4 _projection; mat4 _projectionInverse; vec4 _viewport; + vec4 _stereoInfo; }; layout(std140) uniform transformCameraBuffer { @@ -31,6 +32,16 @@ TransformCamera getTransformCamera() { vec3 getEyeWorldPos() { return _camera._viewInverse[3].xyz; } + + +bool cam_isStereo() { + return _camera._stereoInfo.x > 0.0; +} + +float cam_getStereoSide() { + return _camera._stereoInfo.y; +} + <@endfunc@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index dc5b25b39e..f70b68b4f0 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -44,7 +44,7 @@ struct LightLocations { int radius; int ambientSphere; int lightBufferUnit; - int texcoordMat; + int sphereParam; int coneParam; int deferredFrameTransformBuffer; int shadowTransformBuffer; @@ -139,323 +139,6 @@ void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radiu } } -void DeferredLightingEffect::render(const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { - auto args = renderContext->args; - gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - - // Framebuffer copy operations cannot function as multipass stereo operations. - batch.enableStereo(false); - - // perform deferred lighting, rendering to free fbo - auto framebufferCache = DependencyManager::get(); - auto textureCache = DependencyManager::get(); - - QSize framebufferSize = framebufferCache->getFrameBufferSize(); - - // binding the first framebuffer - auto lightingFBO = framebufferCache->getLightingFramebuffer(); - batch.setFramebuffer(lightingFBO); - - batch.setViewportTransform(args->_viewport); - batch.setStateScissorRect(args->_viewport); - - - // Bind the G-Buffer surfaces - batch.setResourceTexture(DEFERRED_BUFFER_COLOR_UNIT, framebufferCache->getDeferredColorTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_NORMAL_UNIT, framebufferCache->getDeferredNormalTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_EMISSIVE_UNIT, framebufferCache->getDeferredSpecularTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_DEPTH_UNIT, framebufferCache->getPrimaryDepthTexture()); - - // FIXME: Different render modes should have different tasks - if (args->_renderMode == RenderArgs::DEFAULT_RENDER_MODE && _ambientOcclusionEnabled) { - batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, framebufferCache->getOcclusionTexture()); - } else { - // need to assign the white texture if ao is off - batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, textureCache->getWhiteTexture()); - } - - assert(_lightStage.lights.size() > 0); - const auto& globalShadow = _lightStage.lights[0]->shadow; - - // Bind the shadow buffer - batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow.map); - - // THe main viewport is assumed to be the mono viewport (or the 2 stereo faces side by side within that viewport) - auto monoViewport = args->_viewport; - float sMin = args->_viewport.x / (float)framebufferSize.width(); - float sWidth = args->_viewport.z / (float)framebufferSize.width(); - float tMin = args->_viewport.y / (float)framebufferSize.height(); - float tHeight = args->_viewport.w / (float)framebufferSize.height(); - - // The view frustum is the mono frustum base - auto viewFrustum = args->getViewFrustum(); - - // Eval the mono projection - mat4 monoProjMat; - viewFrustum.evalProjectionMatrix(monoProjMat); - - // The mono view transform - Transform monoViewTransform; - viewFrustum.evalViewTransform(monoViewTransform); - - // THe mono view matrix coming from the mono view transform - glm::mat4 monoViewMat; - monoViewTransform.getMatrix(monoViewMat); - - // Running in stero ? - bool isStereo = args->_context->isStereo(); - int numPasses = 1; - - mat4 projMats[2]; - Transform viewTransforms[2]; - ivec4 viewports[2]; - vec4 clipQuad[2]; - vec2 screenBottomLeftCorners[2]; - vec2 screenTopRightCorners[2]; - vec4 fetchTexcoordRects[2]; - - auto geometryCache = DependencyManager::get(); - - if (isStereo) { - numPasses = 2; - - mat4 eyeViews[2]; - args->_context->getStereoProjections(projMats); - args->_context->getStereoViews(eyeViews); - - float halfWidth = 0.5f * sWidth; - - for (int i = 0; i < numPasses; i++) { - // In stereo, the 2 sides are layout side by side in the mono viewport and their width is half - int sideWidth = monoViewport.z >> 1; - viewports[i] = ivec4(monoViewport.x + (i * sideWidth), monoViewport.y, sideWidth, monoViewport.w); - - auto sideViewMat = monoViewMat * glm::inverse(eyeViews[i]); - // viewTransforms[i].evalFromRawMatrix(sideViewMat); - viewTransforms[i] = monoViewTransform; - viewTransforms[i].postTranslate(-glm::vec3((eyeViews[i][3])));// evalFromRawMatrix(sideViewMat); - - clipQuad[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); - screenBottomLeftCorners[i] = glm::vec2(-1.0f + i * 1.0f, -1.0f); - screenTopRightCorners[i] = glm::vec2(i * 1.0f, 1.0f); - - fetchTexcoordRects[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); - } - } else { - - viewports[0] = monoViewport; - projMats[0] = monoProjMat; - - viewTransforms[0] = monoViewTransform; - - clipQuad[0] = glm::vec4(sMin, tMin, sWidth, tHeight); - screenBottomLeftCorners[0] = glm::vec2(-1.0f, -1.0f); - screenTopRightCorners[0] = glm::vec2(1.0f, 1.0f); - - fetchTexcoordRects[0] = glm::vec4(sMin, tMin, sWidth, tHeight); - } - - auto eyePoint = viewFrustum.getPosition(); - float nearRadius = glm::distance(eyePoint, viewFrustum.getNearTopLeft()); - - batch.setUniformBuffer(_directionalLightLocations->deferredFrameTransformBuffer, frameTransform->getFrameTransformBuffer()); - - for (int side = 0; side < numPasses; side++) { - // Render in this side's viewport - batch.setViewportTransform(viewports[side]); - batch.setStateScissorRect(viewports[side]); - - glm::vec2 topLeft(-1.0f, -1.0f); - glm::vec2 bottomRight(1.0f, 1.0f); - glm::vec2 texCoordTopLeft(clipQuad[side].x, clipQuad[side].y); - glm::vec2 texCoordBottomRight(clipQuad[side].x + clipQuad[side].z, clipQuad[side].y + clipQuad[side].w); - - // First Global directional light and ambient pass - { - auto& program = _shadowMapEnabled ? _directionalLightShadow : _directionalLight; - LightLocationsPtr locations = _shadowMapEnabled ? _directionalLightShadowLocations : _directionalLightLocations; - const auto& keyLight = _allocatedLights[_globalLights.front()]; - - // Setup the global directional pass pipeline - { - if (_shadowMapEnabled) { - if (keyLight->getAmbientMap()) { - program = _directionalSkyboxLightShadow; - locations = _directionalSkyboxLightShadowLocations; - } else { - program = _directionalAmbientSphereLightShadow; - locations = _directionalAmbientSphereLightShadowLocations; - } - } else { - if (keyLight->getAmbientMap()) { - program = _directionalSkyboxLight; - locations = _directionalSkyboxLightLocations; - } else { - program = _directionalAmbientSphereLight; - locations = _directionalAmbientSphereLightLocations; - } - } - - if (locations->shadowTransformBuffer >= 0) { - batch.setUniformBuffer(locations->shadowTransformBuffer, globalShadow.getBuffer()); - } - batch.setPipeline(program); - } - - { // Setup the global lighting - setupKeyLightBatch(batch, locations->lightBufferUnit, SKYBOX_MAP_UNIT); - } - - { - batch.setModelTransform(Transform()); - batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); - - glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); - geometryCache->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); - } - - if (keyLight->getAmbientMap()) { - batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); - } - } - - auto texcoordMat = glm::mat4(); - /* texcoordMat[0] = glm::vec4(sWidth / 2.0f, 0.0f, 0.0f, sMin + sWidth / 2.0f); - texcoordMat[1] = glm::vec4(0.0f, tHeight / 2.0f, 0.0f, tMin + tHeight / 2.0f); - */ texcoordMat[0] = glm::vec4(fetchTexcoordRects[side].z / 2.0f, 0.0f, 0.0f, fetchTexcoordRects[side].x + fetchTexcoordRects[side].z / 2.0f); - texcoordMat[1] = glm::vec4(0.0f, fetchTexcoordRects[side].w / 2.0f, 0.0f, fetchTexcoordRects[side].y + fetchTexcoordRects[side].w / 2.0f); - texcoordMat[2] = glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); - texcoordMat[3] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - - // enlarge the scales slightly to account for tesselation - const float SCALE_EXPANSION = 0.05f; - - - batch.setProjectionTransform(projMats[side]); - batch.setViewTransform(viewTransforms[side]); - - // Splat Point lights - if (!_pointLights.empty()) { - batch.setPipeline(_pointLight); - - batch._glUniformMatrix4fv(_pointLightLocations->texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); - - for (auto lightID : _pointLights) { - auto& light = _allocatedLights[lightID]; - // IN DEBUG: light->setShowContour(true); - batch.setUniformBuffer(_pointLightLocations->lightBufferUnit, light->getSchemaBuffer()); - - float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); - // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, - // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... - if (glm::distance(eyePoint, glm::vec3(light->getPosition())) < expandedRadius + nearRadius) { - Transform model; - model.setTranslation(glm::vec3(0.0f, 0.0f, -1.0f)); - batch.setModelTransform(model); - batch.setViewTransform(Transform()); - batch.setProjectionTransform(glm::mat4()); - - glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); - - batch.setProjectionTransform(projMats[side]); - batch.setViewTransform(viewTransforms[side]); - } else { - Transform model; - model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z)); - batch.setModelTransform(model.postScale(expandedRadius)); - batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); - geometryCache->renderSphere(batch); - } - } - } - - // Splat spot lights - if (!_spotLights.empty()) { - batch.setPipeline(_spotLight); - - batch._glUniformMatrix4fv(_spotLightLocations->texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); - - for (auto lightID : _spotLights) { - auto light = _allocatedLights[lightID]; - // IN DEBUG: light->setShowContour(true); - batch.setUniformBuffer(_spotLightLocations->lightBufferUnit, light->getSchemaBuffer()); - - auto eyeLightPos = eyePoint - light->getPosition(); - auto eyeHalfPlaneDistance = glm::dot(eyeLightPos, light->getDirection()); - - const float TANGENT_LENGTH_SCALE = 0.666f; - glm::vec4 coneParam(light->getSpotAngleCosSin(), TANGENT_LENGTH_SCALE * tanf(0.5f * light->getSpotAngle()), 1.0f); - - float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); - // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, - // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... - const float OVER_CONSERVATIVE_SCALE = 1.1f; - if ((eyeHalfPlaneDistance > -nearRadius) && - (glm::distance(eyePoint, glm::vec3(light->getPosition())) < (expandedRadius * OVER_CONSERVATIVE_SCALE) + nearRadius)) { - coneParam.w = 0.0f; - batch._glUniform4fv(_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); - - Transform model; - model.setTranslation(glm::vec3(0.0f, 0.0f, -1.0f)); - batch.setModelTransform(model); - batch.setViewTransform(Transform()); - batch.setProjectionTransform(glm::mat4()); - - glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); - - batch.setProjectionTransform( projMats[side]); - batch.setViewTransform(viewTransforms[side]); - } else { - light->setShowContour(false); - coneParam.w = 1.0f; - batch._glUniform4fv(_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); - - Transform model; - model.setTranslation(light->getPosition()); - model.postRotate(light->getOrientation()); - model.postScale(glm::vec3(expandedRadius, expandedRadius, expandedRadius)); - - batch.setModelTransform(model); - auto mesh = getSpotLightMesh(); - - batch.setIndexBuffer(mesh->getIndexBuffer()); - batch.setInputBuffer(0, mesh->getVertexBuffer()); - batch.setInputFormat(mesh->getVertexFormat()); - - { - auto& part = mesh->getPartBuffer().get(0); - batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); - } - } - } - } - } - - // Probably not necessary in the long run because the gpu layer would unbound this texture if used as render target - batch.setResourceTexture(DEFERRED_BUFFER_COLOR_UNIT, nullptr); - batch.setResourceTexture(DEFERRED_BUFFER_NORMAL_UNIT, nullptr); - batch.setResourceTexture(DEFERRED_BUFFER_EMISSIVE_UNIT, nullptr); - batch.setResourceTexture(DEFERRED_BUFFER_DEPTH_UNIT, nullptr); - batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, nullptr); - batch.setResourceTexture(SHADOW_MAP_UNIT, nullptr); - batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); - - // batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, nullptr); - batch.setUniformBuffer(_directionalLightLocations->deferredFrameTransformBuffer, nullptr); - }); - - // End of the Lighting pass - if (!_pointLights.empty()) { - _pointLights.clear(); - } - if (!_spotLights.empty()) { - _spotLights.clear(); - } -} - void DeferredLightingEffect::setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int skyboxCubemapUnit) { PerformanceTimer perfTimer("DLE->setupBatch()"); auto keyLight = _allocatedLights[_globalLights.front()]; @@ -492,7 +175,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo locations->radius = program->getUniforms().findLocation("radius"); locations->ambientSphere = program->getUniforms().findLocation("ambientSphere.L00"); - locations->texcoordMat = program->getUniforms().findLocation("texcoordMat"); + locations->sphereParam = program->getUniforms().findLocation("sphereParam"); locations->coneParam = program->getUniforms().findLocation("coneParam"); locations->lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); @@ -667,9 +350,7 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c auto framebufferCache = DependencyManager::get(); auto textureCache = DependencyManager::get(); auto deferredLightingEffect = DependencyManager::get(); - - QSize framebufferSize = framebufferCache->getFrameBufferSize(); - + // binding the first framebuffer auto lightingFBO = framebufferCache->getLightingFramebuffer(); batch.setFramebuffer(lightingFBO); @@ -697,99 +378,11 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c // Bind the shadow buffer batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow.map); - - // THe main viewport is assumed to be the mono viewport (or the 2 stereo faces side by side within that viewport) - auto monoViewport = args->_viewport; - float sMin = args->_viewport.x / (float)framebufferSize.width(); - float sWidth = args->_viewport.z / (float)framebufferSize.width(); - float tMin = args->_viewport.y / (float)framebufferSize.height(); - float tHeight = args->_viewport.w / (float)framebufferSize.height(); - - // The view frustum is the mono frustum base - auto viewFrustum = args->getViewFrustum(); - - // Eval the mono projection - mat4 monoProjMat; - viewFrustum.evalProjectionMatrix(monoProjMat); - - // The mono view transform - Transform monoViewTransform; - viewFrustum.evalViewTransform(monoViewTransform); - - // THe mono view matrix coming from the mono view transform - glm::mat4 monoViewMat; - monoViewTransform.getMatrix(monoViewMat); - - // Running in stero ? - bool isStereo = args->_context->isStereo(); - int numPasses = 1; - - mat4 projMats[2]; - Transform viewTransforms[2]; - ivec4 viewports[2]; - vec4 clipQuad[2]; - vec2 screenBottomLeftCorners[2]; - vec2 screenTopRightCorners[2]; - vec4 fetchTexcoordRects[2]; - - auto geometryCache = DependencyManager::get(); - - if (isStereo) { - numPasses = 2; - - mat4 eyeViews[2]; - args->_context->getStereoProjections(projMats); - args->_context->getStereoViews(eyeViews); - - float halfWidth = 0.5f * sWidth; - - for (int i = 0; i < numPasses; i++) { - // In stereo, the 2 sides are layout side by side in the mono viewport and their width is half - int sideWidth = monoViewport.z >> 1; - viewports[i] = ivec4(monoViewport.x + (i * sideWidth), monoViewport.y, sideWidth, monoViewport.w); - - auto sideViewMat = monoViewMat * glm::inverse(eyeViews[i]); - // viewTransforms[i].evalFromRawMatrix(sideViewMat); - viewTransforms[i] = monoViewTransform; - viewTransforms[i].postTranslate(-glm::vec3((eyeViews[i][3])));// evalFromRawMatrix(sideViewMat); - - clipQuad[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); - screenBottomLeftCorners[i] = glm::vec2(-1.0f + i * 1.0f, -1.0f); - screenTopRightCorners[i] = glm::vec2(i * 1.0f, 1.0f); - - fetchTexcoordRects[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); - } - } else { - - viewports[0] = monoViewport; - projMats[0] = monoProjMat; - - viewTransforms[0] = monoViewTransform; - - clipQuad[0] = glm::vec4(sMin, tMin, sWidth, tHeight); - screenBottomLeftCorners[0] = glm::vec2(-1.0f, -1.0f); - screenTopRightCorners[0] = glm::vec2(1.0f, 1.0f); - - fetchTexcoordRects[0] = glm::vec4(sMin, tMin, sWidth, tHeight); - } - - auto eyePoint = viewFrustum.getPosition(); - float nearRadius = glm::distance(eyePoint, viewFrustum.getNearTopLeft()); - + batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, frameTransform->getFrameTransformBuffer()); - - - - // Render in this side's viewport - // batch.setViewportTransform(viewports[side]); - // batch.setStateScissorRect(viewports[side]); - int side = 0; - glm::vec2 topLeft(-1.0f, -1.0f); - glm::vec2 bottomRight(1.0f, 1.0f); - glm::vec2 texCoordTopLeft(clipQuad[side].x, clipQuad[side].y); - glm::vec2 texCoordBottomRight(clipQuad[side].x + clipQuad[side].z, clipQuad[side].y + clipQuad[side].w); - - // First Global directional light and ambient pass + + + // Global directional light and ambient pass { auto& program = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadow : deferredLightingEffect->_directionalLight; LightLocationsPtr locations = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadowLocations : deferredLightingEffect->_directionalLightLocations; @@ -825,16 +418,8 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c deferredLightingEffect->setupKeyLightBatch(batch, locations->lightBufferUnit, SKYBOX_MAP_UNIT); } - { - batch.setModelTransform(Transform()); - batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); - - glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); - //geometryCache->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - + batch.draw(gpu::TRIANGLE_STRIP, 4); + if (keyLight->getAmbientMap()) { batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); } @@ -844,17 +429,128 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c } -void RenderDeferredGlobal::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { - +void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { auto args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - - }); - -} -void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { - + // THe main viewport is assumed to be the mono viewport (or the 2 stereo faces side by side within that viewport) + auto monoViewport = args->_viewport; + + // The view frustum is the mono frustum base + auto viewFrustum = args->getViewFrustum(); + + // Eval the mono projection + mat4 monoProjMat; + viewFrustum.evalProjectionMatrix(monoProjMat); + + // The mono view transform + Transform monoViewTransform; + viewFrustum.evalViewTransform(monoViewTransform); + + // THe mono view matrix coming from the mono view transform + glm::mat4 monoViewMat; + monoViewTransform.getMatrix(monoViewMat); + + auto geometryCache = DependencyManager::get(); + + auto eyePoint = viewFrustum.getPosition(); + float nearRadius = glm::distance(eyePoint, viewFrustum.getNearTopLeft()); + float nearClip = 1.01f * viewFrustum.getNearClip(); + + auto deferredLightingEffect = DependencyManager::get(); + + // Render in this side's viewport + batch.setViewportTransform(monoViewport); + batch.setStateScissorRect(monoViewport); + + // enlarge the scales slightly to account for tesselation + const float SCALE_EXPANSION = 0.05f; + + + batch.setProjectionTransform(monoProjMat); + batch.setViewTransform(monoViewTransform); + + // Splat Point lights + if (!deferredLightingEffect->_pointLights.empty()) { + // POint light pipeline + batch.setPipeline(deferredLightingEffect->_pointLight); + + for (auto lightID : deferredLightingEffect->_pointLights) { + auto& light = deferredLightingEffect->_allocatedLights[lightID]; + // IN DEBUG: light->setShowContour(true); + batch.setUniformBuffer(deferredLightingEffect->_pointLightLocations->lightBufferUnit, light->getSchemaBuffer()); + + float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); + glm::vec4 sphereParam(expandedRadius, 0.0f, 0.0f, 1.0f); + + // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, + // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... + if (glm::distance(eyePoint, glm::vec3(light->getPosition())) < expandedRadius + nearRadius) { + sphereParam.w = 0.0f; + batch._glUniform4fv(deferredLightingEffect->_pointLightLocations->sphereParam, 1, reinterpret_cast< const float* >(&sphereParam)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } else { + sphereParam.w = 1.0f; + batch._glUniform4fv(deferredLightingEffect->_pointLightLocations->sphereParam, 1, reinterpret_cast< const float* >(&sphereParam)); + + Transform model; + model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z)); + batch.setModelTransform(model.postScale(expandedRadius)); + batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + geometryCache->renderSphere(batch); + } + } + } + + // Splat spot lights + if (!deferredLightingEffect->_spotLights.empty()) { + // Spot light pipeline + batch.setPipeline(deferredLightingEffect->_spotLight); + + // Spot mesh + auto mesh = deferredLightingEffect->getSpotLightMesh(); + batch.setIndexBuffer(mesh->getIndexBuffer()); + batch.setInputBuffer(0, mesh->getVertexBuffer()); + batch.setInputFormat(mesh->getVertexFormat()); + auto& conePart = mesh->getPartBuffer().get(0); + + for (auto lightID : deferredLightingEffect->_spotLights) { + auto light = deferredLightingEffect->_allocatedLights[lightID]; + // IN DEBUG: + light->setShowContour(true); + batch.setUniformBuffer(deferredLightingEffect->_spotLightLocations->lightBufferUnit, light->getSchemaBuffer()); + + auto eyeLightPos = eyePoint - light->getPosition(); + auto eyeHalfPlaneDistance = glm::dot(eyeLightPos, light->getDirection()); + + const float TANGENT_LENGTH_SCALE = 0.666f; + glm::vec4 coneParam(light->getSpotAngleCosSin(), TANGENT_LENGTH_SCALE * tanf(0.5f * light->getSpotAngle()), 1.0f); + + float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); + // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, + // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... + const float OVER_CONSERVATIVE_SCALE = 1.1f; + if ((eyeHalfPlaneDistance > -nearRadius) && + (glm::distance(eyePoint, glm::vec3(light->getPosition())) < (expandedRadius * OVER_CONSERVATIVE_SCALE) + nearRadius)) { + coneParam.w = 0.0f; + batch._glUniform4fv(deferredLightingEffect->_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } else { + coneParam.w = 1.0f; + batch._glUniform4fv(deferredLightingEffect->_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); + + Transform model; + model.setTranslation(light->getPosition()); + model.postRotate(light->getOrientation()); + model.postScale(glm::vec3(expandedRadius, expandedRadius, expandedRadius)); + + batch.setModelTransform(model); + + batch.drawIndexed(model::Mesh::topologyToPrimitive(conePart._topology), conePart._numIndices, conePart._startIndex); + } + } + } + }); } void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { @@ -886,9 +582,8 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFrameTransformPointer& deferredTransform) { -// DependencyManager::get()->render(renderContext, deferredTransform); - setupJob.run(sceneContext, renderContext, deferredTransform); - + lightsJob.run(sceneContext, renderContext, deferredTransform); + cleanupJob.run(sceneContext, renderContext); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index a8bf7a8caf..c88242bdc5 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -30,7 +30,8 @@ class RenderArgs; struct LightLocations; using LightLocationsPtr = std::shared_ptr; -/// Handles deferred lighting for the bits that require it (voxels...) + +// THis is where we currently accumulate the local lights, let s change that sooner than later class DeferredLightingEffect : public Dependency { SINGLETON_DEPENDENCY @@ -46,8 +47,6 @@ public: float intensity = 0.5f, float falloffRadius = 0.01f, const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI); - void render(const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); - void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int skyboxCubemapUnit); // update global lighting @@ -99,7 +98,6 @@ private: std::vector _spotLights; friend class RenderDeferredSetup; - friend class RenderDeferredGlobal; friend class RenderDeferredLocals; friend class RenderDeferredCleanup; }; @@ -118,13 +116,6 @@ public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); }; -class RenderDeferredGlobal { -public: - using JobModel = render::Job::ModelI; - - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); -}; - class RenderDeferredLocals { public: using JobModel = render::Job::ModelI; @@ -147,7 +138,7 @@ public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); RenderDeferredSetup setupJob; - + RenderDeferredLocals lightsJob; RenderDeferredCleanup cleanupJob; }; diff --git a/libraries/render-utils/src/deferred_light.slv b/libraries/render-utils/src/deferred_light.slv index d0e3a754d3..fa2fcf8ef9 100644 --- a/libraries/render-utils/src/deferred_light.slv +++ b/libraries/render-utils/src/deferred_light.slv @@ -5,18 +5,26 @@ // deferred_light.vert // vertex shader // -// Created by Andrzej Kapolka on 9/18/14. +// Created by Sam Gateau on 6/16/16. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include gpu/Inputs.slh@> - out vec2 _texCoord0; void main(void) { - _texCoord0 = inTexCoord0.st; - gl_Position = inPosition; + const float depth = 1.0; + const vec4 UNIT_QUAD[4] = vec4[4]( + vec4(-1.0, -1.0, depth, 1.0), + vec4(1.0, -1.0, depth, 1.0), + vec4(-1.0, 1.0, depth, 1.0), + vec4(1.0, 1.0, depth, 1.0) + ); + vec4 pos = UNIT_QUAD[gl_VertexID]; + + _texCoord0 = (pos.xy + 1) * 0.5; + + gl_Position = pos; } diff --git a/libraries/render-utils/src/deferred_light_limited.slv b/libraries/render-utils/src/deferred_light_limited.slv index e9f3e1bdb3..164e2a27d5 100644 --- a/libraries/render-utils/src/deferred_light_limited.slv +++ b/libraries/render-utils/src/deferred_light_limited.slv @@ -5,7 +5,7 @@ // deferred_light_limited.vert // vertex shader // -// Created by Andrzej Kapolka on 9/19/14. +// Created by Sam Gateau on 6/16/16. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -18,17 +18,39 @@ <$declareStandardTransform()$> -uniform mat4 texcoordMat; +uniform vec4 sphereParam; out vec4 _texCoord0; void main(void) { - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>; + if (sphereParam.w != 0.0) { - vec4 projected = gl_Position / gl_Position.w; - _texCoord0 = vec4(dot(projected, texcoordMat[0]) * gl_Position.w, - dot(projected, texcoordMat[1]) * gl_Position.w, 0.0, gl_Position.w); + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>; + + vec4 projected = gl_Position / gl_Position.w; + projected.xy = (projected.xy + 1.0) * 0.5; + + if (cam_isStereo()) { + projected.x = 0.5 * (projected.x + cam_getStereoSide()); + } + _texCoord0 = vec4(projected.xy, 0.0, 1.0) * gl_Position.w; + } else { + const float depth = -1.0; //Draw at near plane + const vec4 UNIT_QUAD[4] = vec4[4]( + vec4(-1.0, -1.0, depth, 1.0), + vec4(1.0, -1.0, depth, 1.0), + vec4(-1.0, 1.0, depth, 1.0), + vec4(1.0, 1.0, depth, 1.0) + ); + vec4 pos = UNIT_QUAD[gl_VertexID]; + + _texCoord0 = vec4((pos.xy + 1) * 0.5, 0.0, 1.0); + if (cam_isStereo()) { + _texCoord0.x = 0.5 * (_texCoord0.x + cam_getStereoSide()); + } + gl_Position = pos; + } } diff --git a/libraries/render-utils/src/deferred_light_spot.slv b/libraries/render-utils/src/deferred_light_spot.slv index 8365899fde..3e11e2fc1e 100755 --- a/libraries/render-utils/src/deferred_light_spot.slv +++ b/libraries/render-utils/src/deferred_light_spot.slv @@ -18,7 +18,6 @@ <$declareStandardTransform()$> -uniform mat4 texcoordMat; uniform vec4 coneParam; out vec4 _texCoord0; @@ -38,14 +37,34 @@ void main(void) { } else { coneVertex.z = 0.0; } + + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, coneVertex, gl_Position)$>; + vec4 projected = gl_Position / gl_Position.w; + projected.xy = (projected.xy + 1.0) * 0.5; + + if (cam_isStereo()) { + projected.x = 0.5 * (projected.x + cam_getStereoSide()); + } + _texCoord0 = vec4(projected.xy, 0.0, 1.0) * gl_Position.w; + + } else { + const float depth = -1.0; //Draw at near plane + const vec4 UNIT_QUAD[4] = vec4[4]( + vec4(-1.0, -1.0, depth, 1.0), + vec4(1.0, -1.0, depth, 1.0), + vec4(-1.0, 1.0, depth, 1.0), + vec4(1.0, 1.0, depth, 1.0) + ); + vec4 pos = UNIT_QUAD[gl_VertexID]; + + _texCoord0 = vec4((pos.xy + 1) * 0.5, 0.0, 1.0); + if (cam_isStereo()) { + _texCoord0.x = 0.5 * (_texCoord0.x + cam_getStereoSide()); + } + gl_Position = pos; } - - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, coneVertex, gl_Position)$>; - - vec4 projected = gl_Position / gl_Position.w; - _texCoord0 = vec4(dot(projected, texcoordMat[0]) * gl_Position.w, - dot(projected, texcoordMat[1]) * gl_Position.w, 0.0, gl_Position.w); } diff --git a/tests/gpu-test/src/TestWindow.h b/tests/gpu-test/src/TestWindow.h index b7f8df48f5..5b6b205721 100644 --- a/tests/gpu-test/src/TestWindow.h +++ b/tests/gpu-test/src/TestWindow.h @@ -19,7 +19,7 @@ #include #include -#define DEFERRED_LIGHTING +//#define DEFERRED_LIGHTING class TestWindow : public QWindow { protected: From c30c2b64b21bec14f3acf65f0d73fcb006e48259 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 17 Jun 2016 17:49:27 -0700 Subject: [PATCH 0610/1237] first pass at preventing repeated compileShader error prints --- libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp | 7 +++++++ libraries/gpu/src/gpu/Shader.h | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp index 19cf798b19..fa54e7c8fe 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp @@ -24,8 +24,15 @@ GLPipeline* GLPipeline::sync(const Pipeline& pipeline) { // No object allocated yet, let's see if it's worth it... ShaderPointer shader = pipeline.getProgram(); + + // If this pipeline's shader has already failed to compile, don't try again + if (shader->compilationHasFailed()) { + return nullptr; + } + GLShader* programObject = GLShader::sync(*shader); if (programObject == nullptr) { + shader->setCompilationHasFailed(true); return nullptr; } diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index 59c6401150..ec712bfa78 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -120,6 +120,9 @@ public: bool isProgram() const { return getType() > NUM_DOMAINS; } bool isDomain() const { return getType() < NUM_DOMAINS; } + void setCompilationHasFailed(bool compilationHasFailed) { _compilationHasFailed = compilationHasFailed; } + bool compilationHasFailed() const { return _compilationHasFailed; } + const Source& getSource() const { return _source; } const Shaders& getShaders() const { return _shaders; } @@ -180,6 +183,9 @@ protected: // The type of the shader, the master key Type _type; + + // Whether or not the shader compilation failed + mutable bool _compilationHasFailed { false }; }; typedef Shader::Pointer ShaderPointer; From 6196c657f8e5fe8263bcd9bce09b4a4f0c860985 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sat, 18 Jun 2016 14:17:47 -0700 Subject: [PATCH 0611/1237] Minimize triggering distance to HUD. --- interface/src/ui/OverlayConductor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 74e27122e4..003de23030 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -32,7 +32,7 @@ bool OverlayConductor::headOutsideOverlay() const { glm::vec3 uiPos = uiTransform.getTranslation(); glm::vec3 uiForward = uiTransform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - const float MAX_COMPOSITOR_DISTANCE = 0.6f; + const float MAX_COMPOSITOR_DISTANCE = 0.99f; // If you're 1m from center of ui sphere, you're at the surface. const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled if (glm::distance(uiPos, hmdPos) > MAX_COMPOSITOR_DISTANCE || glm::dot(uiForward, hmdForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE))) { From 81300ec1270f1d11eda3d5a530be5d59a4ad3041 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sat, 18 Jun 2016 14:35:37 -0700 Subject: [PATCH 0612/1237] turn hud reset behavior on by default --- interface/src/avatar/MyAvatar.cpp | 4 ++-- interface/src/avatar/MyAvatar.h | 6 +++--- interface/src/ui/OverlayConductor.cpp | 2 +- interface/src/ui/PreferencesDialog.cpp | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5d1efca902..6a69ee9a9a 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -722,7 +722,7 @@ void MyAvatar::saveData() { settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); settings.setValue("useSnapTurn", _useSnapTurn); - settings.setValue("clearOverlayWhenDriving", _clearOverlayWhenDriving); + settings.setValue("clearOverlayWhenMoving", _clearOverlayWhenMoving); settings.endGroup(); } @@ -842,7 +842,7 @@ void MyAvatar::loadData() { setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); - setClearOverlayWhenDriving(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool()); + setClearOverlayWhenMoving(settings.value("clearOverlayWhenMoving", _clearOverlayWhenMoving).toBool()); settings.endGroup(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 6fa6e1fd19..96fa999de5 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -159,8 +159,8 @@ public: Q_INVOKABLE bool getSnapTurn() const { return _useSnapTurn; } Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } - Q_INVOKABLE bool getClearOverlayWhenDriving() const { return _clearOverlayWhenDriving; } - Q_INVOKABLE void setClearOverlayWhenDriving(bool on) { _clearOverlayWhenDriving = on; } + Q_INVOKABLE bool getClearOverlayWhenMoving() const { return _clearOverlayWhenMoving; } + Q_INVOKABLE void setClearOverlayWhenMoving(bool on) { _clearOverlayWhenMoving = on; } Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } @@ -405,7 +405,7 @@ private: QString _fullAvatarModelName; QUrl _animGraphUrl {""}; bool _useSnapTurn { true }; - bool _clearOverlayWhenDriving { false }; + bool _clearOverlayWhenMoving { true }; // cache of the current HMD sensor position and orientation // in sensor space. diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 74e27122e4..91046c4cc6 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -124,7 +124,7 @@ void OverlayConductor::update(float dt) { _flags &= ~SuppressedByDrive; } } else { - if (myAvatar->getClearOverlayWhenDriving() && drivingChanged && isDriving) { + if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { _flags |= SuppressedByDrive; } } diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 6decef3240..c1705da206 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -62,9 +62,9 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Snap turn when in HMD", getter, setter)); } { - auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenDriving(); }; - auto setter = [=](bool value) { myAvatar->setClearOverlayWhenDriving(value); }; - preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when driving", getter, setter)); + auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenMoving(); }; + auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; + preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } { auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; From e65e6dc9ce10d10522eab5ca6c9023cc81315fac Mon Sep 17 00:00:00 2001 From: humbletim Date: Sun, 19 Jun 2016 13:40:03 -0400 Subject: [PATCH 0613/1237] Fix entity lifetime guards (re: canRez/canRezTmp) --- libraries/entities/src/EntityTree.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 77a0c6d6fe..ec1f8a50bc 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -150,8 +150,10 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) { // we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones - qCDebug(entities) << "Refusing disallowed entity lifetime adjustment."; - return false; + if (properties.lifetimeChanged()) { + qCDebug(entities) << "Refusing disallowed entity lifetime adjustment."; + return false; + } } // enforce support for locked entities. If an entity is currently locked, then the only @@ -322,8 +324,8 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) { float lifeTime = properties.getLifetime(); - if (lifeTime == 0.0f || lifeTime > _maxTmpEntityLifetime) { - // this is an attempt to rez a permanent entity. + if (lifeTime == ENTITY_ITEM_IMMORTAL_LIFETIME || lifeTime > _maxTmpEntityLifetime) { + // this is an attempt to rez a permanent or non-temporary entity. if (!canRez) { return false; } From 17591323777d46f350221e8d34b6bc6d831ca054 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 14 Jun 2016 18:01:10 -0700 Subject: [PATCH 0614/1237] Use trigger to both grab and equip objects * Removed entry into HOLD state via secondary aka grip buttons. * Changed equip logic to prioritize equip over near over far grabs. * Added drop gesture, upside down controller + slight shake + trigger press * Bug fix for near grab snapping to an incorrect offset after a far grab (this bug is present in master) --- .../system/controllers/handControllerGrab.js | 743 +++++++++++------- 1 file changed, 466 insertions(+), 277 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4ec1675ede..8c5d48667d 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -14,12 +14,11 @@ Script.include("/~/system/libraries/utils.js"); - // // add lines where the hand ray picking is happening // var WANT_DEBUG = false; -var WANT_DEBUG_STATE = false; +var WANT_DEBUG_STATE = true; var WANT_DEBUG_SEARCH_NAME = null; // @@ -126,8 +125,6 @@ var DEFAULT_GRABBABLE_DATA = { disableReleaseVelocity: false }; - - // sometimes we want to exclude objects from being picked var USE_BLACKLIST = true; var blacklist = []; @@ -140,19 +137,20 @@ var USE_ENTITY_LINES_FOR_MOVING = false; var USE_OVERLAY_LINES_FOR_MOVING = false; var USE_PARTICLE_BEAM_FOR_MOVING = true; - var USE_SPOTLIGHT = false; var USE_POINTLIGHT = false; +var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; +var FORBIDDEN_GRAB_TYPES = ['Unknown', 'Light', 'PolyLine', 'Zone']; + // states for the state machine var STATE_OFF = 0; var STATE_SEARCHING = 1; -var STATE_HOLD_SEARCHING = 2; -var STATE_DISTANCE_HOLDING = 3; -var STATE_NEAR_GRABBING = 4; -var STATE_NEAR_TRIGGER = 5; -var STATE_FAR_TRIGGER = 6; -var STATE_HOLD = 7; +var STATE_DISTANCE_HOLDING = 2; +var STATE_NEAR_GRABBING = 3; +var STATE_NEAR_TRIGGER = 4; +var STATE_FAR_TRIGGER = 5; +var STATE_HOLD = 6; // "collidesWith" is specified by comma-separated list of group names // the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar @@ -175,10 +173,6 @@ CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { enterMethod: "searchEnter", exitMethod: "searchExit" }; -CONTROLLER_STATE_MACHINE[STATE_HOLD_SEARCHING] = { - name: "hold_searching", - updateMethod: "search" -}; CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { name: "distance_holding", enterMethod: "distanceHoldingEnter", @@ -259,6 +253,58 @@ function restore2DMode() { } } +function filter(array, predicate) { + var i, l = array.length; + var result = []; + for (i = 0; i < l; i++) { + if (predicate(array[i])) { + result.push(array[i]); + } + } + return result; +} + +// constructor +function EntityPropertiesCache() { + this.cache = {}; +} +EntityPropertiesCache.prototype.clear = function() { + this.cache = {}; +}; +EntityPropertiesCache.prototype.findEntities = function(position, radius) { + var entities = Entities.findEntities(position, radius); + var i, l = entities.length; + for (i = 0; i < l; i++) { + this.addEntity(entities[i]); + } +}; +EntityPropertiesCache.prototype.addEntity = function(entityID) { + var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); + var grabbableProps = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA); + var grabProps = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); + var wearableProps = getEntityCustomData("wearable", entityID, {}); + this.cache[entityID] = { props: props, grabbableProps: grabbableProps, grabProps: grabProps, wearableProps: wearableProps }; +}; +EntityPropertiesCache.prototype.getEntities = function() { + return Object.keys(this.cache); +} +EntityPropertiesCache.prototype.getProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.props : undefined; +}; +EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.grabbableProps : undefined; +}; +EntityPropertiesCache.prototype.getGrabProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.grabProps : undefined; +}; +EntityPropertiesCache.prototype.getWearableProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.wearableProps : undefined; +}; + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -295,6 +341,8 @@ function MyController(hand) { this.overlayLine = null; this.searchSphere = null; + this.waitForTriggerRelease = false; + // how far from camera to search intersection? var DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; this.intersectionDistance = 0.0; @@ -307,16 +355,18 @@ function MyController(hand) { this.lastPickTime = 0; this.lastUnequipCheckTime = 0; + this.entityPropertyCache = new EntityPropertiesCache(); + var _this = this; - var suppressedIn2D = [STATE_OFF, STATE_SEARCHING, STATE_HOLD_SEARCHING]; + var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; this.ignoreInput = function () { // We've made the decision to use 'this' for new code, even though it is fragile, // in order to keep/ the code uniform without making any no-op line changes. return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; - this.update = function() { + this.update = function(deltaTime) { this.updateSmoothedTrigger(); @@ -329,7 +379,7 @@ function MyController(hand) { var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; var updateMethod = this[updateMethodName]; if (updateMethod) { - updateMethod.call(this); + updateMethod.call(this, deltaTime); } else { print("WARNING: could not find updateMethod for state " + stateToName(this.state)); } @@ -343,12 +393,12 @@ function MyController(hand) { Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); } - this.setState = function(newState) { + this.setState = function(newState, reason) { this.grabSphereOff(); if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); - print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName); + print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName + ", reason = " + reason); } // exit the old state @@ -819,14 +869,16 @@ function MyController(hand) { }; this.off = function() { - if (this.triggerSmoothedSqueezed() || this.secondarySqueezed()) { + + if (this.triggerSmoothedReleased()) { + this.waitForTriggerRelease = false; + } + if (!this.waitForTriggerRelease && this.triggerSmoothedSqueezed()) { this.lastPickTime = 0; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { - this.setState(STATE_SEARCHING); - } else if (this.secondarySqueezed()) { - this.setState(STATE_HOLD_SEARCHING); + this.setState(STATE_SEARCHING, "trigger squeeze detected"); } } }; @@ -875,265 +927,345 @@ function MyController(hand) { this.equipHotspotOverlays = []; }; + /** + * Performs ray pick test from the hand controller into the world + * @param {number} which hand to use, RIGHT_HAND or LEFT_HAND + * @returns {object} returns object with two keys entityID and distance + */ + this.calcRayPickInfo = function(hand) { + + var pose = Controller.getPoseValue((hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + var pickRay = { + origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, + direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), + length: PICK_MAX_DISTANCE + }; + + var result = { entityID: null, + searchRay: pickRay, + distance: PICK_MAX_DISTANCE }; + + var now = Date.now(); + if (now - this.lastPickTime < MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { + return result; + } + this.lastPickTime = now; + + var directionNormalized = Vec3.normalize(pickRay.direction); + var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); + var pickRayBacked = { + origin: Vec3.subtract(pickRay.origin, directionBacked), + direction: pickRay.direction + }; + + var intersection; + if (USE_BLACKLIST === true && blacklist.length !== 0) { + intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); + } else { + intersection = Entities.findRayIntersection(pickRayBacked, true); + } + + var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); + if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { + intersection = overlayIntersection; + } + + if (intersection.intersects) { + return { entityID: intersection.entityID, + searchRay: pickRay, + distance: Vec3.distance(pickRay.origin, intersection.intersection) } + } else { + return result; + } + }; + + this.entityWantsTrigger = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + return grabbableProps && grabbableProps.wantsTrigger; + }; + + this.entityIsEquippable = function (entityID, handPosition) { + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var debug = true;//(WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; + if (refCount > 0) { + if (debug) { + print("equip is skipping '" + props.name + "': it is already grabbed"); + } + return false; + } + + if (distance > NEAR_PICK_MAX_DISTANCE) { + if (debug) { + print("equip is skipping '" + props.name + "': too far away."); + } + return false; + } + + var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + if (!wearableProps || !wearableProps.joints) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable attach-point"); + } + return false; + } + + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (!wearableProps.joints[handJointName]) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName); + } + return false; + } + + return true; + }; + + this.entityIsGrabbable = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var props = this.entityPropertyCache.getProps(entityID); + var physical = propsArePhysical(props); + var grabbable = false; + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + if (physical) { + // physical things default to grabbable + grabbable = true; + } else { + // non-physical things default to non-grabbable unless they are already grabbed + if ("refCount" in grabProps && grabProps.refCount > 0) { + grabbable = true; + } else { + grabbable = false; + } + } + + if (grabbableProps.hasOwnProperty("grabbable")) { + grabbable = grabbableProps.grabbable; + } + + if (!grabbable && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': not grabbable."); + } + return false; + } + if (FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden entity type."); + } + return false; + } + if (props.locked && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': locked and not triggerable."); + } + return false; + } + if (FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden name."); + } + return false; + } + + return true; + }; + + this.entityIsDistanceGrabbable = function(entityID, handPosition) { + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + // we can't distance-grab non-physical + var isPhysical = propsArePhysical(props); + if (!isPhysical) { + if (debug) { + print("distance grab is skipping '" + props.name + "': not physical"); + } + return false; + } + + if (distance > PICK_MAX_DISTANCE) { + // too far away, don't grab + if (debug) { + print("distance grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + if (entityIsGrabbedByOther(entityID)) { + // don't distance grab something that is already grabbed. + if (debug) { + print("distance grab is skipping '" + props.name + "': already grabbed by another."); + } + return false; + } + + return true; + }; + + this.entityIsNearGrabbable = function(entityID, handPosition) { + + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + if (distance > NEAR_PICK_MAX_DISTANCE) { + // too far away, don't grab + if (debug) { + print(" grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + return true; + }; + this.search = function() { + var _this = this; + var name; + this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; this.checkForStrayChildren(); - if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); return; } - if (this.state == STATE_HOLD_SEARCHING && this.secondaryReleased()) { - this.setState(STATE_OFF); - return; - } - - // the trigger is being pressed, so do a ray test to see what we are hitting var handPosition = this.getHandPosition(); if (SHOW_GRAB_SPHERE) { this.grabSphereOn(); } - var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - var currentControllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, - Controller.getPoseValue(controllerHandInput).translation), - MyAvatar.position); - var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); - var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS); + var candidateEntities = this.entityPropertyCache.getEntities(); - var distantPickRay = { - origin: PICK_WITH_HAND_RAY ? currentControllerPosition : Camera.position, - direction: PICK_WITH_HAND_RAY ? Quat.getUp(controllerRotation) : Vec3.mix(Quat.getUp(controllerRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), - length: PICK_MAX_DISTANCE - }; + var equippableEntities = filter(candidateEntities, function (entity) { + return _this.entityIsEquippable(entity, handPosition); + }); - // Pick at some maximum rate, not always - var pickRays = []; - var now = Date.now(); - if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { - pickRays = [distantPickRay]; - this.lastPickTime = now; - } - - var rayPickedCandidateEntities = []; // the list of candidates to consider grabbing - - this.intersectionDistance = 0.0; - for (var index = 0; index < pickRays.length; ++index) { - var pickRay = pickRays[index]; - var directionNormalized = Vec3.normalize(pickRay.direction); - var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); - var pickRayBacked = { - origin: Vec3.subtract(pickRay.origin, directionBacked), - direction: pickRay.direction - }; - - var intersection; - - if (USE_BLACKLIST === true && blacklist.length !== 0) { - intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); - } else { - intersection = Entities.findRayIntersection(pickRayBacked, true); - } - var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); - if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { - intersection = overlayIntersection; - } - // If we want to share results with other scripts, this is where we would do it. - - if (intersection.intersects) { - if (intersection.entityID) { - rayPickedCandidateEntities.push(intersection.entityID); - } - this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); - } - } - - var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); - var candidateEntities = rayPickedCandidateEntities.concat(nearPickedCandidateEntities); - - var forbiddenNames = ["Grab Debug Entity", "grab pointer"]; - var forbiddenTypes = ['Unknown', 'Light', 'PolyLine', 'Zone']; - - var minDistance = PICK_MAX_DISTANCE; - var i, props, distance, grabbableData; - this.grabbedEntity = null; - for (i = 0; i < candidateEntities.length; i++) { - var grabbableDataForCandidate = - getEntityCustomData(GRABBABLE_DATA_KEY, candidateEntities[i], DEFAULT_GRABBABLE_DATA); - var grabDataForCandidate = getEntityCustomData(GRAB_USER_DATA_KEY, candidateEntities[i], {}); - var propsForCandidate = Entities.getEntityProperties(candidateEntities[i], GRABBABLE_PROPERTIES); - var near = (nearPickedCandidateEntities.indexOf(candidateEntities[i]) >= 0); - - var physical = propsArePhysical(propsForCandidate); - var grabbable; - if (physical) { - // physical things default to grabbable - grabbable = true; - } else { - // non-physical things default to non-grabbable unless they are already grabbed - if ("refCount" in grabDataForCandidate && grabDataForCandidate.refCount > 0) { - grabbable = true; - } else { - grabbable = false; - } - } - - if ("grabbable" in grabbableDataForCandidate) { - // if userData indicates that this is grabbable or not, override the default. - grabbable = grabbableDataForCandidate.grabbable; - } - - if (!grabbable && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not grabbable."); - } - continue; - } - if (forbiddenTypes.indexOf(propsForCandidate.type) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden entity type."); - } - continue; - } - if (propsForCandidate.locked && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': locked and not triggerable."); - } - continue; - } - if (forbiddenNames.indexOf(propsForCandidate.name) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden name."); - } - continue; - } - - distance = Vec3.distance(propsForCandidate.position, handPosition); - if (distance > PICK_MAX_DISTANCE) { - // too far away, don't grab - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': too far away."); - } - continue; - } - if (propsForCandidate.parentID != NULL_UUID && this.state == STATE_HOLD_SEARCHING) { - // don't allow a double-equip - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': it's a child"); - } - continue; - } - - if (this.state == STATE_SEARCHING && - !physical && distance > NEAR_PICK_MAX_DISTANCE && !near && !grabbableDataForCandidate.wantsTrigger) { - // we can't distance-grab non-physical - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not physical and too far for near-grab"); - } - continue; - } - - if (distance < minDistance) { - this.grabbedEntity = candidateEntities[i]; - minDistance = distance; - props = propsForCandidate; - grabbableData = grabbableDataForCandidate; - } - } - if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.secondarySqueezed())) { - // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. - var isNear = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; - var isPhysical = propsArePhysical(props); - - // near or far trigger - if (grabbableData.wantsTrigger) { - this.setState(isNear ? STATE_NEAR_TRIGGER : STATE_FAR_TRIGGER); + var entity; + if (equippableEntities.length > 0) { + // sort by distance + equippableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = equippableEntities[0]; + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(entity).name + "'"); return; + } else { + // TODO: highlight the equippable object? } - // near grab with action or equip - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - var refCount = ("refCount" in grabData) ? grabData.refCount : 0; - if (isNear && (refCount < 1 || entityHasActions(this.grabbedEntity))) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - // if there was already an action, we'll need to set the parent back to null once we release - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - this.setState(STATE_HOLD); - } - return; - } - // far grab - if (isPhysical && !isNear) { - if (entityIsGrabbedByOther(this.grabbedEntity)) { - // don't distance grab something that is already grabbed. - if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': already grabbed by another."); - } + } + + var rayPickInfo = this.calcRayPickInfo(this.hand); + this.intersectionDistance = rayPickInfo.distance; + if (rayPickInfo.entityID) { + candidateEntities.push(rayPickInfo.entityID); + this.entityPropertyCache.addEntity(rayPickInfo.entityID); + } + + var grabbableEntities = filter(candidateEntities, function (entity) { + return _this.entityIsNearGrabbable(entity, handPosition); + }); + + if (grabbableEntities.length > 0) { + // sort by distance + grabbableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = grabbableEntities[0]; + name = this.entityPropertyCache.getProps(entity).name; + this.grabbedEntity = entity; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); return; + } else { + // TODO: highlight the near-triggerable object? } - this.temporaryPositionOffset = null; - if (!this.hasPresetOffsets()) { - // We want to give a temporary position offset to this object so it is pulled close to hand - var intersectionPointToCenterDistance = Vec3.length(Vec3.subtract(intersection.intersection, - intersection.properties.position)); - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - var handJointPosition = MyAvatar.getJointPosition(handJointIndex); - this.temporaryPositionOffset = - Vec3.normalize(Vec3.subtract(intersection.properties.position, handJointPosition)); - this.temporaryPositionOffset = Vec3.multiply(this.temporaryPositionOffset, - intersectionPointToCenterDistance * - FAR_TO_NEAR_GRAB_PADDING_FACTOR); - } - this.setState(STATE_DISTANCE_HOLDING); - - this.searchSphereOff(); - return; - } - - // else this thing isn't physical. grab it by reparenting it (but not if we've already - // grabbed it). - if (refCount < 1) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); - } - return; } else { - // it's not physical and it's already held via parenting. go ahead and grab it, but - // save off the current parent and joint. this wont always be right if there are more than - // two grabs and the order of release isn't opposite of the order of grabs. - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); + if (this.triggerSmoothedGrab()) { + + var props = this.entityPropertyCache.getProps(entity); + var grabProps = this.entityPropertyCache.getGrabProps(entity); + var refCount = grabProps.refCount ? grabProps.refCount : 0; + if (refCount >= 1) { + // if another person is holding the object, remember to restore the + // parent info, when we are finished grabbing it. + this.shouldResetParentOnRelease = true; + this.previousParentID = props.parentID; + this.previousParentJointIndex = props.parentJointIndex; + } + + this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); + return; + } else { + // TODO: highlight the grabbable object? + } + } + return; + } + + if (rayPickInfo.entityID) { + entity = rayPickInfo.entityID; + name = this.entityPropertyCache.getProps(entity).name; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); + return; + } else { + // TODO: highlight the far-triggerable object? + } + } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); + return; + } else { + // TODO: highlight the far-grabbable object? } - return; } } //search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { - this.lineOn(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + this.lineOn(rayPickInfo.searchRay.origin, Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } - this.searchIndicatorOn(distantPickRay); + this.searchIndicatorOn(rayPickInfo.searchRay); Reticle.setVisible(false); - }; this.distanceGrabTimescale = function(mass, distance) { @@ -1210,8 +1342,8 @@ function MyController(hand) { }; this.distanceHolding = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("releaseGrab"); return; } @@ -1230,7 +1362,7 @@ function MyController(hand) { var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); var now = Date.now(); - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds this.currentObjectTime = now; // the action was set up when this.distanceHolding was called. update the targets. @@ -1267,17 +1399,17 @@ function MyController(hand) { // Update radialVelocity var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); - lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaTime); + lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; - this.grabRadialVelocity = (deltaTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + - (1.0 - (deltaTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; + this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + + (1.0 - (deltaObjectTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1431,11 +1563,56 @@ function MyController(hand) { } } + this.dropGestureReset = function() { + this.fastHandMoveDetected = false; + this.fastHandMoveTimer = 0; + }; + + this.dropGestureProcess = function(deltaTime) { + var pose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + if (this.fastHandMoveDetected) { + this.fastHandMoveTimer -= deltaTime; + } + if (this.fastHandMoveTimer < 0) { + this.fastHandMoveDetected = false; + } + var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec + if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { + this.fastHandMoveDetected = true; + this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; + } + + var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; + var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); + var DOWN = {x: 0, y: -1, z: 0}; + var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); + + var handIsUpsideDown = false; + if (Vec3.dot(worldHandUpAxis, DOWN) > ROTATION_THRESHOLD) { + handIsUpsideDown = true; + } + + var WANT_DEBUG = false; + if (WANT_DEBUG) { + print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z); + print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD); + print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); + } + + return this.fastHandMoveDetected && handIsUpsideDown; + }; + this.nearGrabbingEnter = function() { this.lineOff(); this.overlayLineOff(); + this.dropGestureReset(); + if (this.entityActivated) { var saveGrabbedID = this.grabbedEntity; this.release(); @@ -1524,14 +1701,18 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.nearGrabbing = function() { + this.nearGrabbing = function(deltaTime) { + + var dropDetected = this.dropGestureProcess(deltaTime); + if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_OFF); + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("releaseGrab"); return; } - if (this.state == STATE_HOLD && this.secondaryReleased()) { - this.setState(STATE_OFF); + + if (this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) { + this.setState(STATE_OFF, "drop detected"); this.callEntityMethodOnGrabbed("releaseEquip"); return; } @@ -1541,7 +1722,7 @@ function MyController(hand) { var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte - this.setState(STATE_OFF); + this.setState(STATE_OFF, "entity has no position property"); this.callEntityMethodOnGrabbed("releaseGrab"); return; } @@ -1561,7 +1742,7 @@ function MyController(hand) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + props.parentID + " " + vec3toStr(props.position)); - this.setState(STATE_OFF); + this.setState(STATE_OFF, "held object too far away"); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); } else { // this.state == STATE_HOLD @@ -1581,17 +1762,17 @@ function MyController(hand) { var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - if (deltaTime > 0.0) { + if (deltaObjectTime > 0.0) { var worldDeltaPosition = Vec3.subtract(props.position, this.currentObjectPosition); var previousEulers = Quat.safeEulerAngles(this.currentObjectRotation); var newEulers = Quat.safeEulerAngles(props.rotation); var worldDeltaRotation = Vec3.subtract(newEulers, previousEulers); - this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaTime); - this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaTime * 180.0)); + this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaObjectTime); + this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaObjectTime * 180.0)); this.currentObjectPosition = props.position; this.currentObjectRotation = props.rotation; @@ -1638,8 +1819,8 @@ function MyController(hand) { }; this.nearTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("stopNearTrigger"); return; } @@ -1647,8 +1828,8 @@ function MyController(hand) { }; this.farTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_OFF); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; } @@ -1665,7 +1846,7 @@ function MyController(hand) { if (intersection.accurate) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { - this.setState(STATE_OFF); + this.setState(STATE_OFF, "laser moved off of entity"); this.callEntityMethodOnGrabbed("stopFarTrigger"); return; } @@ -1680,6 +1861,10 @@ function MyController(hand) { }; this.offEnter = function() { + this.release(); + }; + + this.release = function() { this.turnLightsOff(); this.turnOffVisualizations(); @@ -1710,6 +1895,10 @@ function MyController(hand) { })); this.grabbedEntity = null; + + if (this.triggerSmoothedGrab()) { + this.waitForTriggerRelease = true; + } }; this.cleanup = function() { @@ -1918,12 +2107,12 @@ Controller.enableMapping(MAPPING_NAME); //the section below allows the grab script to listen for messages that disable either one or both hands. useful for two handed items var handToDisable = 'none'; -function update() { +function update(deltaTime) { if (handToDisable !== LEFT_HAND && handToDisable !== 'both') { - leftController.update(); + leftController.update(deltaTime); } if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { - rightController.update(); + rightController.update(deltaTime); } } @@ -1950,7 +2139,7 @@ var handleHandMessages = function(channel, message, sender) { data = JSON.parse(message); var selectedController = (data.hand === 'left') ? leftController : rightController; selectedController.release(); - selectedController.setState(STATE_HOLD); + selectedController.setState(STATE_HOLD, "Hifi-Hand-Grab msg received"); selectedController.grabbedEntity = data.entityID; } catch (e) { From 14efd5dc127480f41007d3359de3c1251035c818 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 10:44:09 -0700 Subject: [PATCH 0615/1237] Disable WANT_DEBUG_STATE --- scripts/system/controllers/handControllerGrab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 8c5d48667d..8727bbdd2d 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -18,7 +18,7 @@ Script.include("/~/system/libraries/utils.js"); // add lines where the hand ray picking is happening // var WANT_DEBUG = false; -var WANT_DEBUG_STATE = true; +var WANT_DEBUG_STATE = false; var WANT_DEBUG_SEARCH_NAME = null; // @@ -1711,7 +1711,7 @@ function MyController(hand) { return; } - if (this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) { + if (this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) { this.setState(STATE_OFF, "drop detected"); this.callEntityMethodOnGrabbed("releaseEquip"); return; From 34d18da4e4ecd18f330c7ae5294003628e893dfa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 20 Jun 2016 11:11:54 -0700 Subject: [PATCH 0616/1237] remove mutable --- libraries/gpu/src/gpu/Shader.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index ec712bfa78..e4643f2b7c 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -185,7 +185,7 @@ protected: Type _type; // Whether or not the shader compilation failed - mutable bool _compilationHasFailed { false }; + bool _compilationHasFailed { false }; }; typedef Shader::Pointer ShaderPointer; From 7ccbc9e6eb6c3ea8efee260e4bd4d7dd56afa038 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 11:22:56 -0700 Subject: [PATCH 0617/1237] Primary thumb press can be used to drop an equipped object --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 8727bbdd2d..1471decbfa 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1711,7 +1711,7 @@ function MyController(hand) { return; } - if (this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) { + if ((this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) || this.thumbPressed()) { this.setState(STATE_OFF, "drop detected"); this.callEntityMethodOnGrabbed("releaseEquip"); return; From 35276c38932a153785f6888ec72582834e75fdce Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 11:30:40 -0700 Subject: [PATCH 0618/1237] Use Array.prototype.filter instead of my hand rolled filter function --- scripts/system/controllers/handControllerGrab.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 1471decbfa..bbf9dcb793 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -253,17 +253,6 @@ function restore2DMode() { } } -function filter(array, predicate) { - var i, l = array.length; - var result = []; - for (i = 0; i < l; i++) { - if (predicate(array[i])) { - result.push(array[i]); - } - } - return result; -} - // constructor function EntityPropertiesCache() { this.cache = {}; @@ -1164,7 +1153,7 @@ function MyController(hand) { this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS); var candidateEntities = this.entityPropertyCache.getEntities(); - var equippableEntities = filter(candidateEntities, function (entity) { + var equippableEntities = candidateEntities.filter(function (entity) { return _this.entityIsEquippable(entity, handPosition); }); @@ -1193,7 +1182,7 @@ function MyController(hand) { this.entityPropertyCache.addEntity(rayPickInfo.entityID); } - var grabbableEntities = filter(candidateEntities, function (entity) { + var grabbableEntities = candidateEntities.filter(function (entity) { return _this.entityIsNearGrabbable(entity, handPosition); }); From 40f2d364877c7566cf04f4530ae8f137362cf746 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 20 Jun 2016 11:38:38 -0700 Subject: [PATCH 0619/1237] fix hosts/tags in domain-server settings --- domain-server/resources/describe-settings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index bad24dd3a1..dce6c3449f 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -116,6 +116,7 @@ "name": "hosts", "label": "Hosts", "type": "table", + "can_add_new_rows": true, "help": "Usernames of hosts who can reliably show your domain to new visitors.", "numbered": false, "columns": [ @@ -130,6 +131,7 @@ "name": "tags", "label": "Tags", "type": "table", + "can_add_new_rows": true, "help": "Common categories under which your domain falls.", "numbered": false, "columns": [ From a14bbe4e682e18c38d5c9a8fd21420d5bf2f99b7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 11:40:59 -0700 Subject: [PATCH 0620/1237] Remove /* style multi-line comments --- .../system/controllers/handControllerGrab.js | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index bbf9dcb793..76e13ce992 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -916,11 +916,11 @@ function MyController(hand) { this.equipHotspotOverlays = []; }; - /** - * Performs ray pick test from the hand controller into the world - * @param {number} which hand to use, RIGHT_HAND or LEFT_HAND - * @returns {object} returns object with two keys entityID and distance - */ + /// + // Performs ray pick test from the hand controller into the world + // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND + // @returns {object} returns object with two keys entityID and distance + // this.calcRayPickInfo = function(hand) { var pose = Controller.getPoseValue((hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); @@ -1365,14 +1365,12 @@ function MyController(hand) { var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), radius); - // double delta controller rotation - /* - var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did - var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, - controllerRotation, - DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - Quat.inverse(this.previousControllerRotation)); - */ + /// double delta controller rotation + // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did + // var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, + // controllerRotation, + // DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + // Quat.inverse(this.previousControllerRotation)); // update the currentObject position and rotation. this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); @@ -1426,28 +1424,26 @@ function MyController(hand) { } } - /* - var defaultConstraintData = { - axisStart: false, - axisEnd: false - } - - var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); - var clampedVector; - var targetPosition; - if (constraintData.axisStart !== false) { - clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, - constraintData.axisStart, - constraintData.axisEnd); - targetPosition = clampedVector; - } else { - targetPosition = { - x: this.currentObjectPosition.x, - y: this.currentObjectPosition.y, - z: this.currentObjectPosition.z - } - } - */ + // var defaultConstraintData = { + // axisStart: false, + // axisEnd: false + // } + // + // var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); + // var clampedVector; + // var targetPosition; + // if (constraintData.axisStart !== false) { + // clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, + // constraintData.axisStart, + // constraintData.axisEnd); + // targetPosition = clampedVector; + // } else { + // targetPosition = { + // x: this.currentObjectPosition.x, + // y: this.currentObjectPosition.y, + // z: this.currentObjectPosition.z + // } + // } var handPosition = this.getHandPosition(); From 359483d9ba617637b01ad7f18e168d0476f8a5e2 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 11:43:42 -0700 Subject: [PATCH 0621/1237] coding standard fix --- scripts/system/controllers/handControllerGrab.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 76e13ce992..8cef751e07 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -935,9 +935,11 @@ function MyController(hand) { length: PICK_MAX_DISTANCE }; - var result = { entityID: null, - searchRay: pickRay, - distance: PICK_MAX_DISTANCE }; + var result = { + entityID: null, + searchRay: pickRay, + distance: PICK_MAX_DISTANCE + }; var now = Date.now(); if (now - this.lastPickTime < MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { From cd1780efefc1f0ad1a8cbad53e98f08631ecfbb0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 20 Jun 2016 13:21:06 -0700 Subject: [PATCH 0622/1237] brush clearing --- .../controllers/handControllerPointer.js | 115 ++++-------------- 1 file changed, 27 insertions(+), 88 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 0f1e23b45c..0fe199098d 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -14,15 +14,11 @@ // // Control the "mouse" using hand controller. (HMD and desktop.) -// For now: -// Hydra thumb button 3 is left-mouse, button 4 is right-mouse. -// A click in the center of the vive thumb pad is left mouse. Vive menu button is context menu (right mouse). // First-person only. // Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. // (For now, the thumb buttons on both controllers are always on.) -// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. -// Otherwise, the active hand controller shows a red ball where a click will act. - +// When partially squeezing over a HUD element, a laser or the reticle is shown where the active hand +// controller beam intersects the HUD. // UTILITIES ------------- @@ -102,7 +98,7 @@ function Trigger(label) { } that.state = state; }; - // Answer a controller source function (answering either 0.0 or 1.0), with hysteresis. + // Answer a controller source function (answering either 0.0 or 1.0). that.partial = function () { return that.state ? 1.0 : 0.0; // either 'partial' or 'full' }; @@ -369,24 +365,9 @@ clickMapping.enable(); // VISUAL AID ----------- // Same properties as handControllerGrab search sphere -var BALL_SIZE = 0.011; -var BALL_ALPHA = 0.5; -var LASER_SEARCH_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: BALL_ALPHA}; -var LASER_TRIGGER_COLOR_XYZW = {x: 250 / 255, y: 10 / 255, z: 10 / 255, w: BALL_ALPHA}; -var fakeProjectionBall = Overlays.addOverlay("sphere", { - size: 5 * BALL_SIZE, - color: {red: 255, green: 10, blue: 10}, - ignoreRayIntersection: true, - alpha: BALL_ALPHA, - visible: false, - solid: true, - drawInFront: true // Even when burried inside of something, show it. -}); -var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. -Script.scriptEnding.connect(function () { - overlays.forEach(Overlays.deleteOverlay); -}); -var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. +var LASER_ALPHA = 0.5; +var LASER_SEARCH_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: LASER_ALPHA}; +var LASER_TRIGGER_COLOR_XYZW = {x: 250 / 255, y: 10 / 255, z: 10 / 255, w: LASER_ALPHA}; var SYSTEM_LASER_DIRECTION = {x: 0, y: 0, z: -1}; var systemLaserOn = false; function clearSystemLaser() { @@ -401,81 +382,30 @@ function setColoredLaser() { // answer trigger state if lasers supported, else f return HMD.setHandLasers(activeHudLaser, true, color, SYSTEM_LASER_DIRECTION) && activeTrigger.state; } -function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD - if (!optionalEnableClicks) { - expireMouseCursor(); - clearSystemLaser(); - } else if (activeTrigger.state && (!systemLaserOn || (systemLaserOn !== activeTrigger.state))) { // last=>wrong color - // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. - systemLaserOn = setColoredLaser(); - Reticle.visible = !systemLaserOn; - } else if ((systemLaserOn || Reticle.visible) && !activeTrigger.state) { - clearSystemLaser(); - Reticle.visible = false; - } - - if (!visualizationIsShowing) { - return; - } - visualizationIsShowing = false; - overlays.forEach(function (overlay) { - Overlays.editOverlay(overlay, {visible: false}); - }); -} -var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { - ignore(controllerPosition, controllerDirection, hudPosition2d); - clearSystemLaser(); - // Show an indication of where the cursor will appear when crossing a HUD element, - // and where in-world clicking will occur. - // - // There are a number of ways we could do this, but for now, it's a blue sphere that rolls along - // the HUD surface, and a red sphere that rolls along the 3d objects that will receive the click. - // We'll leave it to other scripts (like handControllerGrab) to show a search beam when desired. - - function intersection3d(position, direction) { - // Answer in-world intersection (entity or 3d overlay), or way-out point - var pickRay = {origin: position, direction: direction}; - var result = findRayIntersection(pickRay); - return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); - } - - visualizationIsShowing = true; - // We'd rather in-world interactions be done at the termination of the hand beam - // -- intersection3d(controllerPosition, controllerDirection). Maybe have handControllerGrab - // direclty manipulate both entity and 3d overlay objects. - // For now, though, we present a false projection of the cursor onto whatever is below it. This is - // different from the hand beam termination because the false projection is from the camera, while - // the hand beam termination is from the hand. - /* // FIXME: We can tighten this up later, once we know what will and won't be included. - var eye = Camera.getPosition(); - var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); - Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); - */ - Reticle.visible = false; - - return visualizationIsShowing; // In case we change caller to act conditionally. -} // MAIN OPERATIONS ----------- // function update() { var now = Date.now(); + function off() { + expireMouseCursor(); + clearSystemLaser(); + } if (!handControllerLockOut.expired(now)) { - return turnOffVisualization(); // Let them use mouse it in peace. + return off(); // Let them use mouse it in peace. } if (!Menu.isOptionChecked("First Person")) { - return turnOffVisualization(); // What to do? menus can be behind hand! + return off(); // What to do? menus can be behind hand! } if (!Window.hasFocus() || !Reticle.allowMouseCapture) { - return turnOffVisualization(); // Don't mess with other apps or paused mouse activity + return off(); // Don't mess with other apps or paused mouse activity } leftTrigger.update(); rightTrigger.update(); var controllerPose = Controller.getPoseValue(activeHand); // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) if (!controllerPose.valid) { - return turnOffVisualization(); // Controller is cradled. + return off(); // Controller is cradled. } var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); @@ -487,7 +417,7 @@ function update() { if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. } - return turnOffVisualization(); + return off(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); @@ -499,13 +429,22 @@ function update() { if (HMD.active) { Reticle.depth = hudReticleDistance(); } - return turnOffVisualization(true); + if (activeTrigger.state && (!systemLaserOn || (systemLaserOn !== activeTrigger.state))) { // last=>wrong color + // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. + systemLaserOn = setColoredLaser(); + Reticle.visible = !systemLaserOn; + } else if ((systemLaserOn || Reticle.visible) && !activeTrigger.state) { + clearSystemLaser(); + Reticle.visible = false; + } + return; } // We are not pointing at a HUD element (but it could be a 3d overlay). if (!activeTrigger.state) { - return turnOffVisualization(); // No trigger (with hysteresis). + return off(); // No trigger } - updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); + clearSystemLaser(); + Reticle.visible = false; } var UPDATE_INTERVAL = 50; // milliseconds. Script.update is too frequent. From 8792812884814237bff55fcded366d88bc5362fa Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 13:26:41 -0700 Subject: [PATCH 0623/1237] Change raw for loop to Array.prototype.forEach instead --- scripts/system/controllers/handControllerGrab.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 8cef751e07..4143cdd403 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -47,7 +47,6 @@ var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to thei var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects -var FAR_TO_NEAR_GRAB_PADDING_FACTOR = 1.2; var NO_INTERSECT_COLOR = { red: 10, @@ -262,10 +261,10 @@ EntityPropertiesCache.prototype.clear = function() { }; EntityPropertiesCache.prototype.findEntities = function(position, radius) { var entities = Entities.findEntities(position, radius); - var i, l = entities.length; - for (i = 0; i < l; i++) { - this.addEntity(entities[i]); - } + var _this = this; + entities.forEach(function (x) { + _this.addEntity(x); + }); }; EntityPropertiesCache.prototype.addEntity = function(entityID) { var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); From 408f65100be61ec3eae0e651b61a1217f778b12f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 13:34:05 -0700 Subject: [PATCH 0624/1237] Bug fix for dropping near-grabbed entities via thump press. --- scripts/system/controllers/handControllerGrab.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4143cdd403..4300c32171 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1697,10 +1697,18 @@ function MyController(hand) { return; } - if ((this.state == STATE_HOLD && dropDetected && this.triggerSmoothedGrab()) || this.thumbPressed()) { - this.setState(STATE_OFF, "drop detected"); - this.callEntityMethodOnGrabbed("releaseEquip"); - return; + if (this.state == STATE_HOLD) { + if (dropDetected && this.triggerSmoothedGrab()) { + this.setState(STATE_OFF, "drop gesture detected"); + this.callEntityMethodOnGrabbed("releaseEquip"); + return; + } + + if (this.thumbPressed()) { + this.setState(STATE_OFF, "drop via thumb press"); + this.callEntityMethodOnGrabbed("releaseEquip"); + return; + } } this.heartBeat(this.grabbedEntity); From 25b21dacda246e7ff691bdf4db9cf67d8b57cbd0 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 20 Jun 2016 12:57:32 -0700 Subject: [PATCH 0625/1237] clean domain metadata and update acl --- domain-server/src/DomainMetadata.cpp | 156 +++++++++++++++++++-------- domain-server/src/DomainMetadata.h | 60 ++++++----- domain-server/src/DomainServer.cpp | 30 ++---- domain-server/src/DomainServer.h | 8 +- 4 files changed, 154 insertions(+), 100 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 26d2bb87ce..d3eb7a97b0 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -10,16 +10,18 @@ #include "DomainMetadata.h" -#include +#include #include +#include #include +#include "DomainServer.h" #include "DomainServerNodeData.h" const QString DomainMetadata::USERS = "users"; -const QString DomainMetadata::USERS_NUM_TOTAL = "num_users"; -const QString DomainMetadata::USERS_NUM_ANON = "num_anon_users"; -const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames"; +const QString DomainMetadata::Users::NUM_TOTAL = "num_users"; +const QString DomainMetadata::Users::NUM_ANON = "num_anon_users"; +const QString DomainMetadata::Users::HOSTNAMES = "user_hostnames"; // users metadata will appear as (JSON): // { "num_users": Number, // "num_anon_users": Number, @@ -27,25 +29,20 @@ const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames"; // } const QString DomainMetadata::DESCRIPTORS = "descriptors"; -const QString DomainMetadata::DESCRIPTORS_DESCRIPTION = "description"; -const QString DomainMetadata::DESCRIPTORS_CAPACITY = "capacity"; // parsed from security -const QString DomainMetadata::DESCRIPTORS_RESTRICTION = "restriction"; // parsed from ACL -const QString DomainMetadata::DESCRIPTORS_MATURITY = "maturity"; -const QString DomainMetadata::DESCRIPTORS_HOSTS = "hosts"; -const QString DomainMetadata::DESCRIPTORS_TAGS = "tags"; +const QString DomainMetadata::Descriptors::DESCRIPTION = "description"; +const QString DomainMetadata::Descriptors::CAPACITY = "capacity"; // parsed from security +const QString DomainMetadata::Descriptors::HOURS = "hours"; +const QString DomainMetadata::Descriptors::RESTRICTION = "restriction"; // parsed from ACL +const QString DomainMetadata::Descriptors::MATURITY = "maturity"; +const QString DomainMetadata::Descriptors::HOSTS = "hosts"; +const QString DomainMetadata::Descriptors::TAGS = "tags"; // descriptors metadata will appear as (JSON): -// { "capacity": Number, -// TODO: "hours": String, // UTF-8 representation of the week, split into 15" segments +// { "description": String, // capped description +// "capacity": Number, +// "hours": String, // UTF-8 representation of the week, split into 15" segments // "restriction": String, // enum of either open, hifi, or acl // "maturity": String, // enum corresponding to ESRB ratings // "hosts": [ String ], // capped list of usernames -// "description": String, // capped description -// TODO: "img": { -// "src": String, -// "type": String, -// "size": Number, -// "updated_at": Number, -// }, // "tags": [ String ], // capped list of tags // } @@ -54,36 +51,103 @@ const QString DomainMetadata::DESCRIPTORS_TAGS = "tags"; // // it is meant to be sent to and consumed by an external API -DomainMetadata::DomainMetadata() { +DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { _metadata[USERS] = {}; _metadata[DESCRIPTORS] = {}; + + assert(dynamic_cast(domainServer)); + DomainServer* server = static_cast(domainServer); + + // update the metadata when a user (dis)connects + connect(server, &DomainServer::userConnected, this, &DomainMetadata::usersChanged); + connect(server, &DomainServer::userDisconnected, this, &DomainMetadata::usersChanged); + + // update the metadata when security changes + connect(&server->_settingsManager, &DomainServerSettingsManager::updateNodePermissions, + this, static_cast(&DomainMetadata::securityChanged)); + + // initialize the descriptors + descriptorsChanged(); } -void DomainMetadata::setDescriptors(QVariantMap& settings) { +QJsonObject DomainMetadata::get() { + maybeUpdateUsers(); + return QJsonObject::fromVariantMap(_metadata); +} + +QJsonObject DomainMetadata::get(const QString& group) { + maybeUpdateUsers(); + return QJsonObject::fromVariantMap(_metadata[group].toMap()); +} + +void DomainMetadata::descriptorsChanged() { const QString CAPACITY = "security.maximum_user_capacity"; + auto settings = static_cast(parent())->_settingsManager.getSettingsMap(); const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; - // TODO: Keep parity with ACL development. - const QString RESTRICTION = "security.restricted_access"; - const QString RESTRICTION_OPEN = "open"; - // const QString RESTRICTION_HIFI = "hifi"; - const QString RESTRICTION_ACL = "acl"; - const QVariant* isRestrictedVariant = valueForKeyPath(settings, RESTRICTION); - bool isRestricted = isRestrictedVariant ? isRestrictedVariant->toBool() : false; - QString restriction = isRestricted ? RESTRICTION_ACL : RESTRICTION_OPEN; + auto descriptors = settings[DESCRIPTORS].toMap(); + descriptors[Descriptors::CAPACITY] = capacity; + _metadata[DESCRIPTORS] = descriptors; - QVariantMap descriptors = settings[DESCRIPTORS].toMap(); - descriptors[DESCRIPTORS_CAPACITY] = capacity; - descriptors[DESCRIPTORS_RESTRICTION] = restriction; + // update overwritten fields + securityChanged(false); + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata descriptors set:" << _metadata[DESCRIPTORS]; +#endif + + sendDescriptors(); +} + +void DomainMetadata::securityChanged(bool send) { + const QString RESTRICTION_OPEN = "open"; + const QString RESTRICTION_ANON = "anon"; + const QString RESTRICTION_HIFI = "hifi"; + const QString RESTRICTION_ACL = "acl"; + + QString restriction; + + const auto& settingsManager = static_cast(parent())->_settingsManager; + bool hasAnonymousAccess = + settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous).canConnectToDomain; + bool hasHifiAccess = + settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).canConnectToDomain; + if (hasAnonymousAccess) { + restriction = hasHifiAccess ? RESTRICTION_OPEN : RESTRICTION_ANON; + } else if (hasHifiAccess) { + restriction = RESTRICTION_HIFI; + } else { + restriction = RESTRICTION_ACL; + } + + auto descriptors = _metadata[DESCRIPTORS].toMap(); + descriptors[Descriptors::RESTRICTION] = restriction; _metadata[DESCRIPTORS] = descriptors; #if DEV_BUILD || PR_BUILD - qDebug() << "Domain metadata descriptors set:" << descriptors; + qDebug() << "Domain metadata restriction set:" << restriction; +#endif + + if (send) { + sendDescriptors(); + } +} + +void DomainMetadata::usersChanged() { + ++_tic; + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata users change detected"; #endif } -void DomainMetadata::updateUsers() { +void DomainMetadata::maybeUpdateUsers() { + if (_lastTic == _tic) { + return; + } + _lastTic = _tic; + static const QString DEFAULT_HOSTNAME = "*"; auto nodeList = DependencyManager::get(); @@ -113,20 +177,26 @@ void DomainMetadata::updateUsers() { }); QVariantMap users = { - { USERS_NUM_TOTAL, numConnected }, - { USERS_NUM_ANON, numConnectedAnonymously }, - { USERS_HOSTNAMES, userHostnames }}; + { Users::NUM_TOTAL, numConnected }, + { Users::NUM_ANON, numConnectedAnonymously }, + { Users::HOSTNAMES, userHostnames }}; _metadata[USERS] = users; + ++_tic; #if DEV_BUILD || PR_BUILD qDebug() << "Domain metadata users updated:" << users; #endif } -void DomainMetadata::usersChanged() { - ++_tic; - -#if DEV_BUILD || PR_BUILD - qDebug() << "Domain metadata users change detected"; -#endif +void DomainMetadata::sendDescriptors() { + QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(get(DESCRIPTORS)).toJson(QJsonDocument::Compact))); + const QUuid& domainID = DependencyManager::get()->getSessionUUID(); + if (!domainID.isNull()) { + static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; + DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), + AccountManagerAuth::Required, + QNetworkAccessManager::PutOperation, + JSONCallbackParameters(), + domainUpdateJSON.toUtf8()); + } } diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index 7d58d43182..86918f5c78 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -19,46 +19,48 @@ class DomainMetadata : public QObject { Q_OBJECT +public: + using Tic = uint32_t; + static const QString USERS; - static const QString USERS_NUM_TOTAL; - static const QString USERS_NUM_ANON; - static const QString USERS_HOSTNAMES; + class Users { + public: + static const QString NUM_TOTAL; + static const QString NUM_ANON; + static const QString HOSTNAMES; + }; static const QString DESCRIPTORS; - static const QString DESCRIPTORS_DESCRIPTION; - static const QString DESCRIPTORS_CAPACITY; - static const QString DESCRIPTORS_HOURS; - static const QString DESCRIPTORS_RESTRICTION; - static const QString DESCRIPTORS_MATURITY; - static const QString DESCRIPTORS_HOSTS; - static const QString DESCRIPTORS_TAGS; - static const QString DESCRIPTORS_IMG; - static const QString DESCRIPTORS_IMG_SRC; - static const QString DESCRIPTORS_IMG_TYPE; - static const QString DESCRIPTORS_IMG_SIZE; - static const QString DESCRIPTORS_IMG_UPDATED_AT; + class Descriptors { + public: + static const QString DESCRIPTION; + static const QString CAPACITY; + static const QString HOURS; + static const QString RESTRICTION; + static const QString MATURITY; + static const QString HOSTS; + static const QString TAGS; + }; -public: - DomainMetadata(); + DomainMetadata(QObject* domainServer); + DomainMetadata() = delete; - // Returns the last set metadata - // If connected users have changed, metadata may need to be updated - // this should be checked by storing tic = getTic() between calls - // and testing it for equality before the next get (tic == getTic()) - QJsonObject get() { return QJsonObject::fromVariantMap(_metadata); } - QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS].toMap()); } - QJsonObject getDescriptors() { return QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); } - - uint32_t getTic() { return _tic; } - - void setDescriptors(QVariantMap& settings); - void updateUsers(); + // Get cached metadata + QJsonObject get(); + QJsonObject get(const QString& group); public slots: + void descriptorsChanged(); + void securityChanged(bool send); + void securityChanged() { securityChanged(true); } void usersChanged(); protected: + void maybeUpdateUsers(); + void sendDescriptors(); + QVariantMap _metadata; + uint32_t _lastTic{ -1 }; uint32_t _tic{ 0 }; }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 1f666455c6..223cab61da 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -94,10 +94,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); - // update the metadata when a user (dis)connects - connect(this, &DomainServer::userConnected, &_metadata, &DomainMetadata::usersChanged); - connect(this, &DomainServer::userDisconnected, &_metadata, &DomainMetadata::usersChanged); - // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); @@ -108,9 +104,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions, &_gatekeeper, &DomainGatekeeper::updateNodePermissions); - // update the metadata with current descriptors - _metadata.setDescriptors(_settingsManager.getSettingsMap()); - if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -125,17 +118,9 @@ DomainServer::DomainServer(int argc, char* argv[]) : _gatekeeper.preloadAllowedUserPublicKeys(); optionallyGetTemporaryName(args); - - // send metadata descriptors - QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(_metadata.getDescriptors()).toJson(QJsonDocument::Compact))); - const QUuid& domainID = DependencyManager::get()->getSessionUUID(); - static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), - AccountManagerAuth::Required, - QNetworkAccessManager::PutOperation, - JSONCallbackParameters(), - domainUpdateJSON.toUtf8()); } + + _metadata = new DomainMetadata(this); } DomainServer::~DomainServer() { @@ -1111,14 +1096,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain; - // Add the metadata to the heartbeat - static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; - auto tic = _metadata.getTic(); - if (_metadataTic != tic) { - _metadataTic = tic; - _metadata.updateUsers(); + if (_metadata) { + // Add the metadata to the heartbeat + static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata->get(DomainMetadata::USERS); } - domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.getUsers(); QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index bdcc36c1ac..c742dbc9b3 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -172,13 +172,12 @@ private: DomainServerSettingsManager _settingsManager; - DomainMetadata _metadata; - uint32_t _metadataTic{ 0 }; - HifiSockAddr _iceServerSocket; std::unique_ptr _iceServerHeartbeatPacket; - QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer + // These will be parented to this, they are not dangling + DomainMetadata* _metadata { nullptr }; + QTimer* _iceHeartbeatTimer { nullptr }; QList _iceServerAddresses; QSet _failedIceServerAddresses; @@ -190,6 +189,7 @@ private: bool _hasAccessToken { false }; friend class DomainGatekeeper; + friend class DomainMetadata; }; From cfe9ac32d4ff761fe07817959e9f89a19a0c4535 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 20 Jun 2016 14:46:20 -0700 Subject: [PATCH 0626/1237] fix narrowing warning by casting -1 --- domain-server/src/DomainMetadata.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index 86918f5c78..0bfd6c611b 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -60,7 +60,7 @@ protected: void sendDescriptors(); QVariantMap _metadata; - uint32_t _lastTic{ -1 }; + uint32_t _lastTic{ (uint32_t)-1 }; uint32_t _tic{ 0 }; }; From e5159ad213a437e63feb509796f3961ce9bd8399 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 14:50:15 -0700 Subject: [PATCH 0627/1237] Added "Developer > Hands > Drop Without Shake" menu option --- .../system/controllers/handControllerGrab.js | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4300c32171..c85e113bc4 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -37,6 +37,7 @@ var THUMB_ON_VALUE = 0.5; var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. var PICK_WITH_HAND_RAY = true; +var DROP_WITHOUT_SHAKE = false; // // distant manipulation @@ -1589,7 +1590,7 @@ function MyController(hand) { print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); } - return this.fastHandMoveDetected && handIsUpsideDown; + return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown; }; this.nearGrabbingEnter = function() { @@ -2170,6 +2171,24 @@ function cleanup() { leftController.cleanup(); Controller.disableMapping(MAPPING_NAME); Reticle.setVisible(true); + Menu.removeMenuItem("Developer > Hands", "Drop Without Shake"); } + Script.scriptEnding.connect(cleanup); Script.update.connect(update); + +Menu.addMenuItem({ + menuName: "Developer > Hands", + menuItemName: "Drop Without Shake", + isCheckable: true, + isChecked: DROP_WITHOUT_SHAKE +}); + +function handleMenuItemEvent(menuItem) { + if (menuItem === "Drop Without Shake") { + DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); + } +} + +Menu.menuItemEvent.connect(handleMenuItemEvent); + From 37e9f666390c6f92dcdd0d731d2404b5fa2b4781 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 20 Jun 2016 15:41:21 -0700 Subject: [PATCH 0628/1237] Additional toolbar work --- interface/resources/qml/hifi/Desktop.qml | 9 +- .../resources/qml/hifi/toolbars/Toolbar.qml | 128 +++++++++++------- .../qml/hifi/toolbars/ToolbarButton.qml | 34 ++++- interface/resources/qml/windows/Frame.qml | 2 +- interface/resources/qml/windows/ToolFrame.qml | 63 +++++++-- .../scripting/ToolbarScriptingInterface.cpp | 9 -- scripts/developer/tests/toolbarTest.js | 2 +- tests/ui/qml/main.qml | 2 +- 8 files changed, 174 insertions(+), 75 deletions(-) diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index c14d55cb00..169542c0f0 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -3,12 +3,12 @@ import QtQuick.Controls 1.4 import QtWebEngine 1.1; import Qt.labs.settings 1.0 -import "../desktop" +import "../desktop" as OriginalDesktop import ".." import "." import "./toolbars" -Desktop { +OriginalDesktop.Desktop { id: desktop MouseArea { @@ -54,12 +54,13 @@ Desktop { WebEngine.settings.localContentCanAccessRemoteUrls = true; var sysToolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); - //toolbars[sysToolbar.objectName] = sysToolbar var toggleHudButton = sysToolbar.addButton({ + objectName: "hudToggle", imageURL: "../../../icons/hud-01.svg", visible: true, - + pinned: true, }); + toggleHudButton.yOffset = Qt.binding(function(){ return desktop.pinned ? 50 : 0 }); diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 35c816569b..75c06e4199 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -7,14 +7,16 @@ import "." Window { id: window - frame: ToolFrame { } + frame: ToolFrame { + horizontalSpacers: horizontal + verticalSpacers: !horizontal + } hideBackground: true resizable: false destroyOnCloseButton: false destroyOnHidden: false closable: false shown: true - pinned: true width: content.width height: content.height visible: true @@ -32,54 +34,77 @@ Window { } onHorizontalChanged: { - var oldParent = horizontal ? column : row; var newParent = horizontal ? row : column; - var move = []; - - var i; - for (i in oldParent.children) { - var child = oldParent.children[i]; - if (child.spacer) { - continue; - } - move.push(oldParent.children[i]); - } - for (i in move) { - move[i].parent = newParent; + for (var i in buttons) { + var child = buttons[i]; + child.parent = newParent; if (horizontal) { - move[i].y = 0 + child.y = 0 } else { - move[i].x = 0 + child.x = 0 } } - fixSpacers(); } Item { id: content implicitHeight: horizontal ? row.height : column.height implicitWidth: horizontal ? row.width : column.width + Row { id: row spacing: 6 - visible: window.horizontal - Rectangle{ readonly property bool spacer: true; id: rowSpacer1; width: 1; height: row.height } - Rectangle{ readonly property bool spacer: true; id: rowSpacer2; width: 1; height: row.height } - Rectangle{ readonly property bool spacer: true; id: rowSpacer3; width: 1; height: row.height } - Rectangle{ readonly property bool spacer: true; id: rowSpacer4; width: 1; height: row.height } } Column { id: column spacing: 6 - visible: !window.horizontal - Rectangle{ readonly property bool spacer: true; id: colSpacer1; width: column.width; height: 1 } - Rectangle{ readonly property bool spacer: true; id: colSpacer2; width: column.width; height: 1 } - Rectangle{ readonly property bool spacer: true; id: colSpacer3; width: column.width; height: 1 } - Rectangle{ readonly property bool spacer: true; id: colSpacer4; width: column.width; height: 1 } } Component { id: toolbarButtonBuilder; ToolbarButton { } } + + Connections { + target: desktop + onPinnedChanged: { + if (!window.pinned) { + return; + } + var newPinned = desktop.pinned; + for (var i in buttons) { + var child = buttons[i]; + if (desktop.pinned) { + if (!child.pinned) { + child.visible = false; + } + } else { + child.visible = true; + } + } + } + } + } + + + function findButtonIndex(name) { + if (!name) { + return -1; + } + + for (var i in buttons) { + var child = buttons[i]; + if (child.objectName === name) { + return i; + } + } + return -1; + } + + function findButton(name) { + var index = findButtonIndex(name); + if (index < 0) { + return; + } + return buttons[index]; } function addButton(properties) { @@ -88,30 +113,39 @@ Window { // If a name is specified, then check if there's an existing button with that name // and return it if so. This will allow multiple clients to listen to a single button, // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded - if (properties.objectName) { - for (var i in buttons) { - var child = buttons[i]; - if (child.objectName === properties.objectName) { - return child; - } - } + var result = findButton(properties.objectName); + if (result) { + return result; } - properties.toolbar = this; - var result = toolbarButtonBuilder.createObject(container, properties); + properties.opacity = 0; + result = toolbarButtonBuilder.createObject(container, properties); buttons.push(result); - fixSpacers(); + result.opacity = 1; + updatePinned(); return result; } - function fixSpacers() { - colSpacer3.parent = null - colSpacer4.parent = null - rowSpacer3.parent = null - rowSpacer4.parent = null - colSpacer3.parent = column - colSpacer4.parent = column - rowSpacer3.parent = row - rowSpacer4.parent = row + function removeButton(name) { + var index = findButtonIndex(name); + if (index < -1) { + console.warn("Tried to remove non-existent button " + name); + return; + } + buttons[index].destroy(); + buttons.splice(index, 1); + updatePinned(); + } + + function updatePinned() { + var newPinned = false; + for (var i in buttons) { + var child = buttons[i]; + if (child.pinned) { + newPinned = true; + break; + } + } + pinned = newPinned; } } diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index a8514689e8..a3be4533d2 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -7,11 +7,40 @@ Item { property alias alpha: button.opacity property var subImage; property int yOffset: 0 + property int buttonState: 0 property var toolbar; property real size: 50 // toolbar ? toolbar.buttonSize : 50 width: size; height: size + property bool pinned: false clip: true + Behavior on opacity { + NumberAnimation { + duration: 150 + easing.type: Easing.InOutCubic + } + } + + property alias fadeTargetProperty: button.opacity + + onFadeTargetPropertyChanged: { + visible = (fadeTargetProperty !== 0.0); + } + + onVisibleChanged: { + if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { + var target = visible; + visible = !visible; + fadeTargetProperty = target ? 1.0 : 0.0; + return; + } + } + + + onButtonStateChanged: { + yOffset = size * buttonState + } + Component.onCompleted: { if (subImage) { if (subImage.y) { @@ -30,10 +59,7 @@ Item { MouseArea { anchors.fill: parent - onClicked: { - console.log("Clicked on button " + image.source + " named " + button.objectName) - button.clicked(); - } + onClicked: button.clicked(); } } diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index bc8ecc35ec..88d8c3ad41 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -59,7 +59,7 @@ Item { height: 1.66 * window.height x: (window.width - width) / 2 y: window.height / 2 - 0.375 * height - visible: gradientsSupported && window && window.focus && pane.visible + visible: gradientsSupported && window && window.focus && window.content.visible gradient: Gradient { // GradientStop position 0.5 is at full circumference of circle that fits inside the square. GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml index ac1093092e..eff5fc0377 100644 --- a/interface/resources/qml/windows/ToolFrame.qml +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -16,20 +16,67 @@ import "../styles-uit" Frame { HifiConstants { id: hifi } + property bool horizontalSpacers: false + property bool verticalSpacers: false Rectangle { // Dialog frame id: frameContent readonly property int frameMargin: 6 - readonly property int frameMarginLeft: frameMargin - readonly property int frameMarginRight: frameMargin - readonly property int frameMarginTop: frameMargin - readonly property int frameMarginBottom: frameMargin + readonly property int frameMarginLeft: frameMargin + (horizontalSpacers ? 12 : 0) + readonly property int frameMarginRight: frameMargin + (horizontalSpacers ? 12 : 0) + readonly property int frameMarginTop: frameMargin + (verticalSpacers ? 12 : 0) + readonly property int frameMarginBottom: frameMargin + (verticalSpacers ? 12 : 0) + + Rectangle { + visible: horizontalSpacers + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 8 + height: window.height + color: "gray"; + radius: 4 + } + + Rectangle { + visible: horizontalSpacers + anchors.right: parent.right + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 8 + height: window.height + color: "gray"; + radius: 4 + } + + Rectangle { + visible: verticalSpacers + anchors.top: parent.top + anchors.topMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: 8 + width: window.width + color: "gray"; + radius: 4 + } + + Rectangle { + visible: verticalSpacers + anchors.bottom: parent.bottom + anchors.bottomMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: 8 + width: window.width + color: "gray"; + radius: 4 + } + anchors { - topMargin: -frameMargin - leftMargin: -frameMargin - rightMargin: -frameMargin - bottomMargin: -frameMargin + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + topMargin: -frameMarginTop + bottomMargin: -frameMarginBottom } anchors.fill: parent color: hifi.colors.baseGrayHighlight40 diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp index fd96b6a809..82332b3187 100644 --- a/interface/src/scripting/ToolbarScriptingInterface.cpp +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -15,15 +15,6 @@ class QmlWrapper : public QObject { public: QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) : QObject(parent), _qmlObject(qmlObject) { - - const QMetaObject *metaobject = qmlObject->metaObject(); - int count = metaobject->propertyCount(); - qDebug() << "Scanning properties for " << qmlObject; - for (int i = 0; i < count; ++i) { - QMetaProperty metaproperty = metaobject->property(i); - const char *name = metaproperty.name(); - qDebug() << "Property " << name; - } } Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { diff --git a/scripts/developer/tests/toolbarTest.js b/scripts/developer/tests/toolbarTest.js index 20f349929f..e21fbd8e19 100644 --- a/scripts/developer/tests/toolbarTest.js +++ b/scripts/developer/tests/toolbarTest.js @@ -13,7 +13,7 @@ var toolBar = (function() { newZoneButton, newParticleButton - var toolIconUrl = Script.resolvePath("assets/images/tools/"); + var toolIconUrl = Script.resolvePath("../../system/assets/images/tools/"); function initialize() { print("Toolbars: " + Toolbars); diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 19f6a55bfd..47d0f6d601 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -57,7 +57,7 @@ ApplicationWindow { "particle-01.svg", ] property int iconIndex: 0 - readonly property string toolIconUrl: "file:///C:/Users/bdavi/git/hifi/scripts/system/assets/images/tools/" + readonly property string toolIconUrl: "../../../../../scripts/system/assets/images/tools/" text: "Create Button" onClicked: { var name = icons[iconIndex]; From 7579102b1ac0526ee8ad136b11caa5e588843841 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 20 Jun 2016 15:50:49 -0700 Subject: [PATCH 0629/1237] updates --- .../DomainContent/Toybox/bow/bow.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/unpublishedScripts/DomainContent/Toybox/bow/bow.js b/unpublishedScripts/DomainContent/Toybox/bow/bow.js index b287966d2c..9a0da44961 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/bow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/bow.js @@ -125,6 +125,18 @@ Entities.deleteEntity(this.arrow); }, + startNearGrab:function(entityID, args){ + _this.startEquip(entityID, args); + }, + + continueNearGrab:function(entityID, args){ + _this.continueEquip(entityID, args); + }, + + releaseGrab:function(){ + _this.releaseEquip(); + }, + startEquip: function(entityID, args) { this.hand = args[0]; avatarID = args[1]; @@ -137,6 +149,9 @@ var data = getEntityCustomData('grabbableKey', this.entityID, {}); data.grabbable = false; setEntityCustomData('grabbableKey', this.entityID, data); + Entities.editEntity(_this.entityID, { + collidesWith: "" + }) }, continueEquip: function(entityID, args) { @@ -181,6 +196,9 @@ Entities.deleteEntity(this.arrow); this.aiming = false; this.hasArrowNotched = false; + Entities.editEntity(_this.entityID, { + collidesWith: "static,dynamic,kinematic,otherAvatar,myAvatar" + }) }, createArrow: function() { @@ -526,4 +544,4 @@ }; return new Bow(); -}); +}); \ No newline at end of file From 8817bbea0921af134dd522eb21f593ed5ba058d6 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 15:53:33 -0700 Subject: [PATCH 0630/1237] reduce log spam --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index c85e113bc4..4afbae451f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -984,7 +984,7 @@ function MyController(hand) { var props = this.entityPropertyCache.getProps(entityID); var distance = Vec3.distance(props.position, handPosition); var grabProps = this.entityPropertyCache.getGrabProps(entityID); - var debug = true;//(WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; if (refCount > 0) { From 1fa274a5270f4154ee490cf10e3b919164d68808 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:01:03 -0700 Subject: [PATCH 0631/1237] Fix GPUIdent name trailing whitespace --- libraries/shared/src/GPUIdent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/GPUIdent.cpp b/libraries/shared/src/GPUIdent.cpp index 19838964a4..02f92d87e7 100644 --- a/libraries/shared/src/GPUIdent.cpp +++ b/libraries/shared/src/GPUIdent.cpp @@ -122,7 +122,7 @@ GPUIdent* GPUIdent::ensureQuery(const QString& vendor, const QString& renderer) } if (count > bestCount) { bestCount = count; - _name = sString; + _name = QString(sString).trimmed(); hr = spInstance->Get(CComBSTR(_T("DriverVersion")), 0, &var, 0, 0); if (hr == S_OK) { From 0393777b036b7e17a16e8d7836027082ca204ae6 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:01:44 -0700 Subject: [PATCH 0632/1237] Add getMemoryInfo helper function --- libraries/shared/src/SharedUtil.cpp | 25 +++++++++++++++++++++++++ libraries/shared/src/SharedUtil.h | 10 ++++++++++ 2 files changed, 35 insertions(+) diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index b80fac637c..a6866fdc93 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -28,6 +28,7 @@ #ifdef Q_OS_WIN #include "CPUIdent.h" +#include #endif @@ -843,3 +844,27 @@ void printSystemInformation() { (envVariables.contains(env) ? " = " + envVariables.value(env) : " NOT FOUND"); } } + +bool getMemoryInfo(MemoryInfo& info) { +#ifdef Q_OS_WIN + MEMORYSTATUSEX ms; + ms.dwLength = sizeof(ms); + if (!GlobalMemoryStatusEx(&ms)) { + return false; + } + + info.totalMemoryBytes = ms.ullTotalPhys; + info.availMemoryBytes = ms.ullAvailPhys; + info.usedMemoryBytes = ms.ullTotalPhys - ms.ullAvailPhys; + + + PROCESS_MEMORY_COUNTERS_EX pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&pmc), sizeof(pmc))) { + return false; + } + info.processUsedMemoryBytes = pmc.PrivateUsage; + info.processPeakUsedMemoryBytes = pmc.PeakPagefileUsage; +#endif + + return true; +} \ No newline at end of file diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 042396f474..f3e5625484 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -204,4 +204,14 @@ void disableQtBearerPoll(); void printSystemInformation(); +struct MemoryInfo { + uint64_t totalMemoryBytes; + uint64_t availMemoryBytes; + uint64_t usedMemoryBytes; + uint64_t processUsedMemoryBytes; + uint64_t processPeakUsedMemoryBytes; +}; + +bool getMemoryInfo(MemoryInfo& info); + #endif // hifi_SharedUtil_h From 7b3b01a96a54b03099a3bc5d8d7a469ab0c5b5cf Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:02:14 -0700 Subject: [PATCH 0633/1237] Add more values to launch UserActivity --- interface/src/Application.cpp | 39 +++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f2f265aae0..42cfa9484e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -151,6 +151,8 @@ #include "InterfaceParentFinder.h" #include "FrameTimingsScriptingInterface.h" +#include +#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -669,10 +671,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : accountManager->setIsAgent(true); accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); - // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. - // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. - UserActivityLogger::getInstance().launch(applicationVersion(), _previousSessionCrashed, sessionRunTime.get()); - auto addressManager = DependencyManager::get(); // use our MyAvatar position and quat for address manager path @@ -762,6 +760,39 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Make sure we don't time out during slow operations at startup updateHeartbeat(); + + // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. + // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. + auto gpuIdent = GPUIdent::getInstance(); + auto glContextData = getGLContextData(); + QJsonObject properties = { + { "previousSessionCrashed", _previousSessionCrashed }, + { "previousSessionRuntime", sessionRunTime.get() }, + { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, + { "kernel_type", QSysInfo::kernelType() }, + { "kernel_version", QSysInfo::kernelVersion() }, + { "os_type", QSysInfo::productType() }, + { "os_version", QSysInfo::productVersion() }, + { "gpu_name", gpuIdent->getName() }, + { "gpu_driver", gpuIdent->getDriver() }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, + { "gl_version", glContextData["version"] }, + { "gl_vender", glContextData["vendor"] }, + { "gl_sl_version", glContextData["slVersion"] }, + { "gl_renderer", glContextData["renderer"] } + }; + auto macVersion = QSysInfo::macVersion(); + if (macVersion != QSysInfo::MV_None) { + properties["os_osx_version"] = QSysInfo::macVersion(); + } + auto windowsVersion = QSysInfo::windowsVersion(); + if (windowsVersion != QSysInfo::WV_None) { + properties["os_win_version"] = QSysInfo::windowsVersion(); + } + UserActivityLogger::getInstance().logAction("launch", properties); + + // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); _entityEditSender.setMyAvatar(getMyAvatar()); From 3aac0cc4fb17fce1b1c528e8a9c2fcfd02036397 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:02:36 -0700 Subject: [PATCH 0634/1237] Update fps useractivity with more general stats --- interface/src/Application.cpp | 54 +++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 42cfa9484e..8071a46e5f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1090,15 +1090,57 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Add periodic checks to send user activity data static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; - static int SEND_FPS_INTERVAL_MS = 10000; + static int SEND_STATS_INTERVAL_MS = 10000; + static int NEARBY_AVATAR_RADIUS_METERS = 10; // Periodically send fps as a user activity event - QTimer* sendFPSTimer = new QTimer(this); - sendFPSTimer->setInterval(SEND_FPS_INTERVAL_MS); - connect(sendFPSTimer, &QTimer::timeout, this, [this]() { - UserActivityLogger::getInstance().logAction("fps", { { "rate", _frameCounter.rate() } }); + QTimer* sendStatsTimer = new QTimer(this); + sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); + connect(sendStatsTimer, &QTimer::timeout, this, [this]() { + QJsonObject properties = {}; + MemoryInfo memInfo; + if (getMemoryInfo(memInfo)) { + properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); + properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); + properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); + } + + auto displayPlugin = qApp->getActiveDisplayPlugin(); + + properties["fps"] = _frameCounter.rate(); + properties["present_rate"] = displayPlugin->presentRate(); + properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); + properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); + properties["sim_rate"] = getAverageSimsPerSecond(); + properties["avatar_sim_rate"] = getAvatarSimrate(); + + auto bandwidthRecorder = DependencyManager::get(); + properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); + properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond(); + properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond(); + properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond(); + + auto nodeList = DependencyManager::get(); + SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); + SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); + SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); + SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); + properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; + properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; + properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; + properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; + properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; + + auto loadingRequests = ResourceCache::getLoadingRequests(); + properties["active_downloads"] = loadingRequests.size(); + properties["pending_downloads"] = ResourceCache::getPendingRequestCount(); + + properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + UserActivityLogger::getInstance().logAction("stats", properties); }); - sendFPSTimer->start(); + sendStatsTimer->start(); // Periodically check for count of nearby avatars From a52fab00a9852b2e0f4560920820df8ecf09ce2d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 20 Jun 2016 16:04:39 -0700 Subject: [PATCH 0635/1237] commit possible fix for testing --- interface/src/ui/ApplicationOverlay.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 2395f62468..41d249b635 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -166,13 +166,11 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { batch.setViewTransform(Transform()); float screenRatio = ((float)qApp->getDevicePixelRatio()); - float renderRatio = ((float)screenRatio * qApp->getRenderResolutionScale()); + float renderRatio = ((float)qApp->getRenderResolutionScale()); auto viewport = qApp->getMirrorViewRect(); - glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); - glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); - bottomLeft *= screenRatio; - topRight *= screenRatio; + glm::vec2 bottomLeft(viewport.left(), viewport.top() + screenRatio * viewport.height()); + glm::vec2 topRight(viewport.left() + screenRatio * viewport.width(), viewport.top()); glm::vec2 texCoordMinCorner(0.0f, 0.0f); glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); From 1eaa9e40cdc8fc365843e18b30a92076735318b5 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:09:17 -0700 Subject: [PATCH 0636/1237] Cleanup nearby avatar tracking --- interface/src/Application.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8071a46e5f..a3ab1ca8c0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1149,7 +1149,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, [this]() { auto avatarManager = DependencyManager::get(); - int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), 10) - 1; + int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), + NEARBY_AVATAR_RADIUS_METERS) - 1; if (nearbyAvatars != lastCountOfNearbyAvatars) { UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); } From ab057010d6c5c1c58880869ffc56b35c01edb135 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:09:34 -0700 Subject: [PATCH 0637/1237] Add changed display mode tracking --- interface/src/Application.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a3ab1ca8c0..93a6df2d3b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5266,6 +5266,11 @@ void Application::updateDisplayMode() { return; } + UserActivityLogger::getInstance().logAction("changed_display_mode", { + { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, + { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" } + }); + auto offscreenUi = DependencyManager::get(); // Make the switch atomic from the perspective of other threads From 154ccb8932095f2f180cda149eb912c68fef0348 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:10:07 -0700 Subject: [PATCH 0638/1237] Add glVersionToInteger to GLHelpers --- libraries/gl/src/gl/GLHelpers.cpp | 8 ++++++++ libraries/gl/src/gl/GLHelpers.h | 1 + 2 files changed, 9 insertions(+) diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 302e0b8515..79b39a2331 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -5,6 +5,7 @@ #include #include #include +#include const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { static QSurfaceFormat format; @@ -39,6 +40,13 @@ const QGLFormat& getDefaultGLFormat() { return glFormat; } +int glVersionToInteger(QString glVersion) { + QStringList versionParts = glVersion.split(QRegularExpression("[\\.\\s]")); + int majorNumber = versionParts[0].toInt(); + int minorNumber = versionParts[1].toInt(); + return majorNumber * 100 + minorNumber * 10; +} + QJsonObject getGLContextData() { if (!QOpenGLContext::currentContext()) { return QJsonObject(); diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index ddb254f1c5..477bf7abc8 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -27,5 +27,6 @@ void setGLFormatVersion(F& format, int major = 4, int minor = 5) { format.setVer const QSurfaceFormat& getDefaultOpenGLSurfaceFormat(); const QGLFormat& getDefaultGLFormat(); QJsonObject getGLContextData(); +int glVersionToInteger(QString glVersion); #endif From 4e0aeea2b8715f33f0c15914e4751d38f7bc3a7b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 20 Jun 2016 16:19:19 -0700 Subject: [PATCH 0639/1237] add time offset to domain-server UI --- .../resources/describe-settings.json | 164 ++++++++++++++++++ domain-server/src/DomainMetadata.cpp | 7 +- 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index dce6c3449f..3633685365 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -141,6 +141,170 @@ "can_set": true } ] + }, + { + "name": "utc_offset", + "label": "Time Zone", + "help": "This server's time zone, used for searching open servers", + "type": "select", + "options": [ + { + "value": "-12", + "label": "UTC-12:00" + }, + { + "value": "-11", + "label": "UTC-11:00" + }, + { + "value": "-10", + "label": "UTC-10:00" + }, + { + "value": "-9.5", + "label": "UTC-09:30" + }, + { + "value": "-9", + "label": "UTC-09:00" + }, + { + "value": "-8", + "label": "UTC-08:00" + }, + { + "value": "-7", + "label": "UTC-07:00" + }, + { + "value": "-6", + "label": "UTC-06:00" + }, + { + "value": "-5", + "label": "UTC-05:00" + }, + { + "value": "-4", + "label": "UTC-04:00" + }, + { + "value": "-3.5", + "label": "UTC-03:30" + }, + { + "value": "-3", + "label": "UTC-03:00" + }, + { + "value": "-2", + "label": "UTC-02:00" + }, + { + "value": "-1", + "label": "UTC-01:00" + }, + { + "value": "", + "label": "UTC±00:00" + }, + { + "value": "1", + "label": "UTC+01:00" + }, + { + "value": "2", + "label": "UTC+02:00" + }, + { + "value": "3", + "label": "UTC+03:00" + }, + { + "value": "3.5", + "label": "UTC+03:30" + }, + { + "value": "4", + "label": "UTC+04:00" + }, + { + "value": "4.5", + "label": "UTC+04:30" + }, + { + "value": "5", + "label": "UTC+05:00" + }, + { + "value": "5.5", + "label": "UTC+05:30" + }, + { + "value": "5.75", + "label": "UTC+05:45" + }, + { + "value": "6", + "label": "UTC+06:00" + }, + { + "value": "6.5", + "label": "UTC+06:30" + }, + { + "value": "7", + "label": "UTC+07:00" + }, + { + "value": "8", + "label": "UTC+08:00" + }, + { + "value": "8.5", + "label": "UTC+08:30" + }, + { + "value": "8.75", + "label": "UTC+08:45" + }, + { + "value": "9", + "label": "UTC+09:00" + }, + { + "value": "9.5", + "label": "UTC+09:30" + }, + { + "value": "10", + "label": "UTC+10:00" + }, + { + "value": "10.5", + "label": "UTC+10:30" + }, + { + "value": "11", + "label": "UTC+11:00" + }, + { + "value": "12", + "label": "UTC+12:00" + }, + { + "value": "12.75", + "label": "UTC+12:45" + }, + { + "value": "13", + "label": "UTC+13:00" + }, + { + "value": "14", + "label": "UTC+14:00" + } + ] } ] }, diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index d3eb7a97b0..bdf24c000a 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -39,7 +39,11 @@ const QString DomainMetadata::Descriptors::TAGS = "tags"; // descriptors metadata will appear as (JSON): // { "description": String, // capped description // "capacity": Number, -// "hours": String, // UTF-8 representation of the week, split into 15" segments +// "hours": { +// "utc_offset": Number, +// "weekday": [ Number, Number ], +// "weekend": [ Number, Number ] +// } // "restriction": String, // enum of either open, hifi, or acl // "maturity": String, // enum corresponding to ESRB ratings // "hosts": [ String ], // capped list of usernames @@ -50,6 +54,7 @@ const QString DomainMetadata::Descriptors::TAGS = "tags"; // { users: , descriptors: } // // it is meant to be sent to and consumed by an external API +// NOTE: metadata may not appear as documented, as parts are generated by describe-settings.js DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { _metadata[USERS] = {}; From aa742a50ca27549e3641221b9678b18b9ff1eac5 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 20 Jun 2016 16:19:34 -0700 Subject: [PATCH 0640/1237] release --- unpublishedScripts/DomainContent/Toybox/bow/bow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unpublishedScripts/DomainContent/Toybox/bow/bow.js b/unpublishedScripts/DomainContent/Toybox/bow/bow.js index 9a0da44961..7dccf06628 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/bow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/bow.js @@ -133,8 +133,8 @@ _this.continueEquip(entityID, args); }, - releaseGrab:function(){ - _this.releaseEquip(); + releaseGrab:function(entityID, args){ + _this.releaseEquip(entityID, args); }, startEquip: function(entityID, args) { From f6d19ba32fc53edbe5172c44a07fef21e2b4a0a5 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 20 Jun 2016 16:25:48 -0700 Subject: [PATCH 0641/1237] add short haptic pulse on release --- unpublishedScripts/DomainContent/Toybox/bow/bow.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/unpublishedScripts/DomainContent/Toybox/bow/bow.js b/unpublishedScripts/DomainContent/Toybox/bow/bow.js index 7dccf06628..cba45087a9 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/bow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/bow.js @@ -479,10 +479,14 @@ // rotation: arrowProps.rotation }; + //actually shoot the arrow and play its sound Entities.editEntity(this.arrow, arrowProperties); this.playShootArrowSound(); + var whichHand = this.hand==='left' ? 0 :1; + Controller.triggerShortHapticPulse(whichHand, 2); + //clear the strings back to only the single straight one this.deleteStrings(); Entities.editEntity(this.preNotchString, { From d8aeb26a6ceae98c97550ad9d9d54239200f801e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 20 Jun 2016 16:26:48 -0700 Subject: [PATCH 0642/1237] haptic --- unpublishedScripts/DomainContent/Toybox/bow/bow.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unpublishedScripts/DomainContent/Toybox/bow/bow.js b/unpublishedScripts/DomainContent/Toybox/bow/bow.js index cba45087a9..2430b68076 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/bow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/bow.js @@ -125,15 +125,15 @@ Entities.deleteEntity(this.arrow); }, - startNearGrab:function(entityID, args){ + startNearGrab: function(entityID, args) { _this.startEquip(entityID, args); }, - continueNearGrab:function(entityID, args){ + continueNearGrab: function(entityID, args) { _this.continueEquip(entityID, args); }, - releaseGrab:function(entityID, args){ + releaseGrab: function(entityID, args) { _this.releaseEquip(entityID, args); }, @@ -484,7 +484,7 @@ Entities.editEntity(this.arrow, arrowProperties); this.playShootArrowSound(); - var whichHand = this.hand==='left' ? 0 :1; + var whichHand = this.hand === 'left' ? 0 : 1; Controller.triggerShortHapticPulse(whichHand, 2); //clear the strings back to only the single straight one From cee897d6d3e306a00704818336f4105c0377cc2a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 20 Jun 2016 16:44:02 -0700 Subject: [PATCH 0643/1237] Fix osx warning --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 93a6df2d3b..b92fc2d3e6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -775,7 +775,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : { "os_version", QSysInfo::productVersion() }, { "gpu_name", gpuIdent->getName() }, { "gpu_driver", gpuIdent->getDriver() }, - { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gpu_memory", QJsonValue(static_cast(gpuIdent->getMemory())) }, { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, { "gl_version", glContextData["version"] }, { "gl_vender", glContextData["vendor"] }, From bfb697bc7789f5cd6dbb073be47f846fc39a66da Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 20 Jun 2016 17:04:23 -0700 Subject: [PATCH 0644/1237] No hysteresis. --- scripts/system/controllers/handControllerPointer.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 0fe199098d..c94325d3c0 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -85,15 +85,10 @@ function Trigger(label) { state = 'full'; } else if (that.triggerSmoothedReleased()) { state = null; - // These depend on previous state: - // null -> squeezed ==> partial - // full -> !squeezed ==> partial - // Otherwise no change. } else if (that.triggerSmoothedSqueezed()) { - if (!state) { - state = 'partial'; - } - } else if (state === 'full') { + // Another way to do this would be to have hysteresis in this branch, but that seems to make things harder to use. + // In particular, the vive has a nice detent as you release off of full, and we want that to be a transition from + // full to partial. state = 'partial'; } that.state = state; From 0bc4beeffd4e85d353ece1219cdd3697a2b14ce6 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 20 Jun 2016 17:09:30 -0700 Subject: [PATCH 0645/1237] Fix initial toolbar positioning --- interface/resources/qml/desktop/Desktop.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index e269a63cde..58d1cba1b8 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -65,7 +65,7 @@ FocusScope { var oldChildren = expectedChildren; var newChildren = d.getRepositionChildren(); - if (oldRecommendedRect != Qt.rect(0,0,0,0) + if (oldRecommendedRect != Qt.rect(0,0,0,0) && oldRecommendedRect != Qt.rect(0,0,1,1) && (oldRecommendedRect != newRecommendedRect || oldChildren != newChildren) ) { From 2ed88bca6d569db24f49fe59a6afa9a280b99f60 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 20 Jun 2016 17:24:36 -0700 Subject: [PATCH 0646/1237] Bug fix for bow and arrow. * before this change the releaseEquip message was not getting sent to the bow, which it would use to re-enable the grab script via the 'Hifi-Hand-Disabler' msg. --- .../system/controllers/handControllerGrab.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4afbae451f..f1ecc15392 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1334,8 +1334,8 @@ function MyController(hand) { this.distanceHolding = function() { if (this.triggerSmoothedReleased()) { - this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "trigger released"); return; } @@ -1693,21 +1693,21 @@ function MyController(hand) { var dropDetected = this.dropGestureProcess(deltaTime); if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "trigger released"); return; } if (this.state == STATE_HOLD) { if (dropDetected && this.triggerSmoothedGrab()) { - this.setState(STATE_OFF, "drop gesture detected"); this.callEntityMethodOnGrabbed("releaseEquip"); + this.setState(STATE_OFF, "drop gesture detected"); return; } if (this.thumbPressed()) { - this.setState(STATE_OFF, "drop via thumb press"); this.callEntityMethodOnGrabbed("releaseEquip"); + this.setState(STATE_OFF, "drop via thumb press"); return; } } @@ -1717,8 +1717,8 @@ function MyController(hand) { var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte - this.setState(STATE_OFF, "entity has no position property"); this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "entity has no position property"); return; } @@ -1737,12 +1737,13 @@ function MyController(hand) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + props.parentID + " " + vec3toStr(props.position)); - this.setState(STATE_OFF, "held object too far away"); + if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); } else { // this.state == STATE_HOLD this.callEntityMethodOnGrabbed("releaseEquip"); } + this.setState(STATE_OFF, "held object too far away"); return; } } @@ -1815,8 +1816,8 @@ function MyController(hand) { this.nearTrigger = function() { if (this.triggerSmoothedReleased()) { - this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("stopNearTrigger"); + this.setState(STATE_OFF, "trigger released"); return; } this.callEntityMethodOnGrabbed("continueNearTrigger"); @@ -1824,8 +1825,8 @@ function MyController(hand) { this.farTrigger = function() { if (this.triggerSmoothedReleased()) { - this.setState(STATE_OFF, "trigger released"); this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.setState(STATE_OFF, "trigger released"); return; } @@ -1841,8 +1842,8 @@ function MyController(hand) { if (intersection.accurate) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { - this.setState(STATE_OFF, "laser moved off of entity"); this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.setState(STATE_OFF, "laser moved off of entity"); return; } if (intersection.intersects) { From 57760bca7d1f3de23af220b2a10cdecee747d659 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 8 Jun 2016 16:35:38 -0700 Subject: [PATCH 0647/1237] sort ShapeInfo data members --- libraries/shared/src/ShapeInfo.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index c853666d90..ad7a76c6a4 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -74,12 +74,12 @@ public: const DoubleHashKey& getHash() const; protected: - ShapeType _type = SHAPE_TYPE_NONE; + QUrl _url; // url for model of convex collision hulls + QVector> _points; // points for convex collision hulls glm::vec3 _halfExtents = glm::vec3(0.0f); glm::vec3 _offset = glm::vec3(0.0f); DoubleHashKey _doubleHashKey; - QVector> _points; // points for convex collision hulls - QUrl _url; // url for model of convex collision hulls + ShapeType _type = SHAPE_TYPE_NONE; }; #endif // hifi_ShapeInfo_h From d64729372a1cedc9cfc99464e5b15787bd063650 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 9 Jun 2016 16:14:49 -0700 Subject: [PATCH 0648/1237] ShapeInfo name changes --- .../src/RenderableModelEntityItem.cpp | 22 ++++---- .../src/RenderablePolyVoxEntityItem.cpp | 18 +++---- .../src/RenderablePolyVoxEntityItem.h | 2 +- .../physics/src/PhysicalEntitySimulation.cpp | 2 +- libraries/physics/src/ShapeFactory.cpp | 6 +-- libraries/shared/src/ShapeInfo.cpp | 50 +++++++++++-------- libraries/shared/src/ShapeInfo.h | 19 ++++--- tests/physics/src/ShapeManagerTests.cpp | 10 ++-- 8 files changed, 70 insertions(+), 59 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7b3b3c3efe..07bcc05572 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -608,8 +608,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { const FBXGeometry& renderGeometry = _model->getFBXGeometry(); const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry(); - QVector>& points = info.getPoints(); - points.clear(); + ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + pointCollection.clear(); uint32_t i = 0; // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect @@ -619,8 +619,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { - points.push_back(QVector()); - QVector& pointsInPart = points[i]; + pointCollection.push_back(QVector()); + ShapeInfo::PointList& pointsInPart = pointCollection[i]; // run through all the triangles and (uniquely) add each point to the hull uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); @@ -664,7 +664,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { if (pointsInPart.size() == 0) { qCDebug(entitiesrenderer) << "Warning -- meshPart has no faces"; - points.pop_back(); + pointCollection.pop_back(); continue; } ++i; @@ -681,16 +681,16 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. AABox box; - for (int i = 0; i < points.size(); i++) { - for (int j = 0; j < points[i].size(); j++) { + for (int i = 0; i < pointCollection.size(); i++) { + for (int j = 0; j < pointCollection[i].size(); j++) { // compensate for registration - points[i][j] += _model->getOffset(); + pointCollection[i][j] += _model->getOffset(); // scale so the collision points match the model points - points[i][j] *= scale; + pointCollection[i][j] *= scale; // this next subtraction is done so we can give info the offset, which will cause // the shape-key to change. - points[i][j] -= _model->getOffset(); - box += points[i][j]; + pointCollection[i][j] -= _model->getOffset(); + box += pointCollection[i][j]; } } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index ad35a1a00c..7d5f227558 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1198,7 +1198,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { QtConcurrent::run([entity, voxelSurfaceStyle, voxelVolumeSize, mesh] { auto polyVoxEntity = std::static_pointer_cast(entity); - QVector> points; + QVector> pointCollection; AABox box; glm::mat4 vtoM = std::static_pointer_cast(entity)->voxelToLocalMatrix(); @@ -1241,9 +1241,9 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { pointsInPart << p3Model; // add next convex hull QVector newMeshPoints; - points << newMeshPoints; + pointCollection << newMeshPoints; // add points to the new convex hull - points[i++] << pointsInPart; + pointCollection[i++] << pointsInPart; } } else { unsigned int i = 0; @@ -1299,19 +1299,19 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // add next convex hull QVector newMeshPoints; - points << newMeshPoints; + pointCollection << newMeshPoints; // add points to the new convex hull - points[i++] << pointsInPart; + pointCollection[i++] << pointsInPart; } }); } - polyVoxEntity->setCollisionPoints(points, box); + polyVoxEntity->setCollisionPoints(pointCollection, box); }); } -void RenderablePolyVoxEntityItem::setCollisionPoints(const QVector> points, AABox box) { +void RenderablePolyVoxEntityItem::setCollisionPoints(ShapeInfo::PointCollection pointCollection, AABox box) { // this catches the payload from computeShapeInfoWorker - if (points.isEmpty()) { + if (pointCollection.isEmpty()) { EntityItem::computeShapeInfo(_shapeInfo); return; } @@ -1325,7 +1325,7 @@ void RenderablePolyVoxEntityItem::setCollisionPoints(const QVector thunk); void setMesh(model::MeshPointer mesh); - void setCollisionPoints(const QVector> points, AABox box); + void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } uint8_t getVoxelInternal(int x, int y, int z); diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 6806b3a398..c5134fc027 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -217,7 +217,7 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re } else if (entity->isReadyToComputeShape()) { ShapeInfo shapeInfo; entity->computeShapeInfo(shapeInfo); - int numPoints = shapeInfo.getMaxNumPoints(); + int numPoints = shapeInfo.getLargestSubshapePointCount(); if (numPoints > MAX_HULL_POINTS) { qWarning() << "convex hull with" << numPoints << "points for entity" << entity->getName() diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index d667d1075d..a23ef97007 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -179,15 +179,15 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } break; case SHAPE_TYPE_COMPOUND: { - const QVector>& points = info.getPoints(); + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); uint32_t numSubShapes = info.getNumSubShapes(); if (numSubShapes == 1) { - shape = createConvexHull(info.getPoints()[0]); + shape = createConvexHull(pointCollection[0]); } else { auto compound = new btCompoundShape(); btTransform trans; trans.setIdentity(); - foreach (QVector hullPoints, points) { + foreach (const ShapeInfo::PointList& hullPoints, pointCollection) { btConvexHullShape* hull = createConvexHull(hullPoints); compound->addChildShape (trans, hull); } diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 9c1e5c3816..cd0cb6fe8a 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -16,9 +16,13 @@ #include "NumericalConstants.h" // for MILLIMETERS_PER_METER void ShapeInfo::clear() { - _type = SHAPE_TYPE_NONE; - _halfExtents = _offset = glm::vec3(0.0f); + _url.clear(); + _pointCollection.clear(); + _triangleIndices.clear(); + _halfExtents = glm::vec3(0.0f); + _offset = glm::vec3(0.0f); _doubleHashKey.clear(); + _type = SHAPE_TYPE_NONE; } void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString url) { @@ -61,9 +65,9 @@ void ShapeInfo::setSphere(float radius) { _doubleHashKey.clear(); } -void ShapeInfo::setConvexHulls(const QVector>& points) { - _points = points; - _type = (_points.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; +void ShapeInfo::setPointCollection(const ShapeInfo::PointCollection& pointCollection) { + _pointCollection = pointCollection; + _type = (_pointCollection.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; _doubleHashKey.clear(); } @@ -83,15 +87,15 @@ uint32_t ShapeInfo::getNumSubShapes() const { if (_type == SHAPE_TYPE_NONE) { return 0; } else if (_type == SHAPE_TYPE_COMPOUND) { - return _points.size(); + return _pointCollection.size(); } return 1; } -int ShapeInfo::getMaxNumPoints() const { +int ShapeInfo::getLargestSubshapePointCount() const { int numPoints = 0; - for (int i = 0; i < _points.size(); ++i) { - int n = _points[i].size(); + for (int i = 0; i < _pointCollection.size(); ++i) { + int n = _pointCollection[i].size(); if (n > numPoints) { numPoints = n; } @@ -187,23 +191,23 @@ const DoubleHashKey& ShapeInfo::getHash() const { // TODO?: provide lookup table for hash/hash2 of _type rather than recompute? uint32_t primeIndex = 0; key.computeHash((uint32_t)_type, primeIndex++); - - // compute hash1 + + // compute hash1 uint32_t hash = key.getHash(); for (int j = 0; j < 3; ++j) { // NOTE: 0.49f is used to bump the float up almost half a millimeter // so the cast to int produces a round() effect rather than a floor() hash ^= DoubleHashKey::hashFunction( - (uint32_t)(_halfExtents[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _halfExtents[j]) * 0.49f), + (uint32_t)(_halfExtents[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _halfExtents[j]) * 0.49f), primeIndex++); if (useOffset) { hash ^= DoubleHashKey::hashFunction( - (uint32_t)(_offset[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _offset[j]) * 0.49f), + (uint32_t)(_offset[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _offset[j]) * 0.49f), primeIndex++); } } key.setHash(hash); - + // compute hash2 hash = key.getHash2(); for (int j = 0; j < 3; ++j) { @@ -224,14 +228,16 @@ const DoubleHashKey& ShapeInfo::getHash() const { } key.setHash2(hash); - QString url = _url.toString(); - if (!url.isEmpty()) { - // fold the urlHash into both parts - QByteArray baUrl = url.toLocal8Bit(); - const char *cUrl = baUrl.data(); - uint32_t urlHash = qChecksum(cUrl, baUrl.count()); - key.setHash(key.getHash() ^ urlHash); - key.setHash2(key.getHash2() ^ urlHash); + if (_type == SHAPE_TYPE_COMPOUND || _type == SHAPE_TYPE_MESH) { + QString url = _url.toString(); + if (!url.isEmpty()) { + // fold the urlHash into both parts + QByteArray baUrl = url.toLocal8Bit(); + const char *cUrl = baUrl.data(); + uint32_t urlHash = qChecksum(cUrl, baUrl.count()); + key.setHash(key.getHash() ^ urlHash); + key.setHash2(key.getHash2() ^ urlHash); + } } } return _doubleHashKey; diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index ad7a76c6a4..7f178bb53a 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -38,18 +38,23 @@ enum ShapeType { SHAPE_TYPE_CYLINDER_X, SHAPE_TYPE_CYLINDER_Y, SHAPE_TYPE_CYLINDER_Z, - SHAPE_TYPE_STATIC_MESH + SHAPE_TYPE_MESH }; class ShapeInfo { public: + + using PointList = QVector; + using PointCollection = QVector; + using TriangleIndices = QVector; + void clear(); void setParams(ShapeType type, const glm::vec3& halfExtents, QString url=""); void setBox(const glm::vec3& halfExtents); void setSphere(float radius); - void setConvexHulls(const QVector>& points); + void setPointCollection(const PointCollection& pointCollection); void setCapsuleY(float radius, float halfHeight); void setOffset(const glm::vec3& offset); @@ -58,12 +63,11 @@ public: const glm::vec3& getHalfExtents() const { return _halfExtents; } const glm::vec3& getOffset() const { return _offset; } - QVector>& getPoints() { return _points; } - const QVector>& getPoints() const { return _points; } + PointCollection& getPointCollection() { return _pointCollection; } + const PointCollection& getPointCollection() const { return _pointCollection; } uint32_t getNumSubShapes() const; - void appendToPoints (const QVector& newPoints) { _points << newPoints; } - int getMaxNumPoints() const; + int getLargestSubshapePointCount() const; float computeVolume() const; @@ -75,7 +79,8 @@ public: protected: QUrl _url; // url for model of convex collision hulls - QVector> _points; // points for convex collision hulls + PointCollection _pointCollection; + TriangleIndices _triangleIndices; glm::vec3 _halfExtents = glm::vec3(0.0f); glm::vec3 _offset = glm::vec3(0.0f); DoubleHashKey _doubleHashKey; diff --git a/tests/physics/src/ShapeManagerTests.cpp b/tests/physics/src/ShapeManagerTests.cpp index 66ac9d0c4a..c8805132fa 100644 --- a/tests/physics/src/ShapeManagerTests.cpp +++ b/tests/physics/src/ShapeManagerTests.cpp @@ -194,23 +194,23 @@ void ShapeManagerTests::addCompoundShape() { int numHullPoints = tetrahedron.size(); // compute the points of the hulls - QVector< QVector > hulls; + ShapeInfo::PointCollection pointCollection; int numHulls = 5; glm::vec3 offsetNormal(1.0f, 0.0f, 0.0f); for (int i = 0; i < numHulls; ++i) { glm::vec3 offset = (float)(i - numHulls/2) * offsetNormal; - QVector hull; + ShapeInfo::PointList pointList; float radius = (float)(i + 1); for (int j = 0; j < numHullPoints; ++j) { glm::vec3 point = radius * tetrahedron[j] + offset; - hull.push_back(point); + pointList.push_back(point); } - hulls.push_back(hull); + pointCollection.push_back(pointList); } // create the ShapeInfo ShapeInfo info; - info.setConvexHulls(hulls); + info.setPointCollection(hulls); // create the shape ShapeManager shapeManager; From 5d5dc2837b09984a9d7bcfb324cea0d632812f36 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 14 Jun 2016 13:58:53 -0700 Subject: [PATCH 0649/1237] fix comment --- libraries/render-utils/src/Model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ded1184c24..0470a238fc 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1002,7 +1002,7 @@ void Model::scaleToFit() { Extents modelMeshExtents = getUnscaledMeshExtents(); // size is our "target size in world space" - // we need to set our model scale so that the extents of the mesh, fit in a cube that size... + // we need to set our model scale so that the extents of the mesh, fit in a box that size... glm::vec3 meshDimensions = modelMeshExtents.maximum - modelMeshExtents.minimum; glm::vec3 rescaleDimensions = _scaleToFitDimensions / meshDimensions; setScaleInternal(rescaleDimensions); From 13bb174b8b6e84e4fb6ac735dfb6cfc6dec1dee8 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 14 Jun 2016 14:16:49 -0700 Subject: [PATCH 0650/1237] small change to commented out debug code --- libraries/physics/src/PhysicalEntitySimulation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index c5134fc027..cdf33a6edb 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -231,7 +231,7 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re result.push_back(motionState); entityItr = _entitiesToAddToPhysics.erase(entityItr); } else { - //qDebug() << "Warning! Failed to generate new shape for entity." << entity->getName(); + //qWarning() << "Failed to generate new shape for entity." << entity->getName(); ++entityItr; } } else { From 9fc77ccfa2f048c61b956b5cd64796097c32a5ea Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 14 Jun 2016 14:19:45 -0700 Subject: [PATCH 0651/1237] use reference to avoid big copy --- libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 7d5f227558..eb6db2874f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1207,7 +1207,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // pull each triangle in the mesh into a polyhedron which can be collided with unsigned int i = 0; - const gpu::BufferView vertexBufferView = mesh->getVertexBuffer(); + const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); const gpu::BufferView& indexBufferView = mesh->getIndexBuffer(); gpu::BufferView::Iterator it = indexBufferView.cbegin(); From a519b77ae7f2ca513bbc6f8456b4dd144dcbe56b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 14 Jun 2016 14:22:28 -0700 Subject: [PATCH 0652/1237] add SHAPE_TYPE_MESH and build mesh shapes --- .../src/RenderableModelEntityItem.cpp | 102 +++++++++++++++++- libraries/physics/src/ShapeFactory.cpp | 97 ++++++++++++++++- libraries/physics/src/ShapeFactory.h | 15 ++- libraries/physics/src/ShapeManager.cpp | 18 ++-- libraries/shared/src/ShapeInfo.cpp | 23 ++-- libraries/shared/src/ShapeInfo.h | 12 ++- 6 files changed, 235 insertions(+), 32 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 07bcc05572..e7991eb638 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -599,13 +599,13 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType type = getShapeType(); + glm::vec3 dimensions = getDimensions(); if (type == SHAPE_TYPE_COMPOUND) { updateModelBounds(); // should never fall in here when collision model not fully loaded // hence we assert that all geometries exist and are loaded assert(_model->isLoaded() && _model->isCollisionLoaded()); - const FBXGeometry& renderGeometry = _model->getFBXGeometry(); const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry(); ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); @@ -677,7 +677,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scale = getDimensions() / renderGeometry.getUnscaledMeshExtents().size(); + const FBXGeometry& renderGeometry = _model->getFBXGeometry(); + glm::vec3 scale = dimensions / renderGeometry.getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. AABox box; @@ -697,9 +698,104 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 collisionModelDimensions = box.getDimensions(); info.setParams(type, collisionModelDimensions, _compoundShapeURL); info.setOffset(_model->getOffset()); + } else if (type == SHAPE_TYPE_MESH) { + updateModelBounds(); + + // should never fall in here when collision model not fully loaded + assert(_model->isLoaded()); + + ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + pointCollection.clear(); + + ShapeInfo::PointList points; + ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); + + glm::vec3 modelOffset = _model->getOffset(); + for (auto& mesh : meshes) { + const gpu::BufferView& vertices = mesh->getVertexBuffer(); + const gpu::BufferView& indices = mesh->getIndexBuffer(); + const gpu::BufferView& parts = mesh->getPartBuffer(); + + // copy points + uint32_t meshIndexOffset = (uint32_t)points.size(); + gpu::BufferView::Iterator vertexItr = vertices.cbegin(); + points.reserve(points.size() + vertices.getNumElements()); + Extents extents; + while (vertexItr != vertices.cend()) { + points.push_back(*vertexItr); + extents.addPoint(*vertexItr); + ++vertexItr; + } + + // scale points and shift by modelOffset + glm::vec3 extentsSize = extents.size(); + glm::vec3 scale = dimensions / extents.size(); + for (int i = 0; i < 3; ++i) { + if (extentsSize[i] < 1.0e-6f) { + scale[i] = 1.0f; + } + } + for (int i = 0; i < points.size(); ++i) { + points[i] = (points[i] * scale) + modelOffset; + } + + // copy triangleIndices + triangleIndices.reserve(triangleIndices.size() + indices.getNumElements()); + gpu::BufferView::Iterator partItr = parts.cbegin(); + while (partItr != parts.cend()) { + + if (partItr->_topology == model::Mesh::TRIANGLES) { + assert(partItr->_numIndices % 3 == 0); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + partItr->_numIndices; + while (indexItr != indexEnd) { + triangleIndices.push_back(*indexItr + meshIndexOffset); + ++indexItr; + } + } else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) { + assert(partItr->_numIndices > 2); + uint32_t approxNumIndices = 3 * partItr->_numIndices; + if (approxNumIndices > (uint32_t)(triangleIndices.capacity() - triangleIndices.size())) { + // we underestimated the final size of triangleIndices so we pre-emptively expand it + triangleIndices.reserve(triangleIndices.size() + approxNumIndices); + } + + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + (partItr->_numIndices - 2); + + // first triangle uses the first three indices + triangleIndices.push_back(*indexItr + meshIndexOffset); + triangleIndices.push_back(*(++indexItr) + meshIndexOffset); + triangleIndices.push_back(*(indexItr + 1) + meshIndexOffset); + + // the rest use previous and next index + uint32_t triangleCount = 1; + while (indexItr != indexEnd) { + if (triangleCount % 2 == 0) { + // even triangles use first two indices in order + triangleIndices.push_back(*(indexItr) + meshIndexOffset); + triangleIndices.push_back(*(++indexItr) + meshIndexOffset); // yes pre-increment + } else { + // odd triangles swap order of first two indices + triangleIndices.push_back(*(indexItr + 1) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); // yes post-increment + } + triangleIndices.push_back(*(indexItr + 1) + meshIndexOffset); + ++triangleCount; + } + } else if (partItr->_topology == model::Mesh::QUADS) { + // TODO: support model::Mesh::QUADS + } + // TODO? support model::Mesh::QUAD_STRIP? + ++partItr; + } + } + pointCollection.push_back(points); + info.setParams(SHAPE_TYPE_MESH, 0.5f * dimensions, _modelURL); } else { ModelEntityItem::computeShapeInfo(info); - info.setParams(type, 0.5f * getDimensions()); + info.setParams(type, 0.5f * dimensions); adjustShapeInfoByRegistration(info); } } diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index a23ef97007..4d5f56853d 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -67,7 +67,8 @@ static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = { }; -btConvexHullShape* ShapeFactory::createConvexHull(const QVector& points) { +// util method +btConvexHullShape* createConvexHull(const ShapeInfo::PointList& points) { assert(points.size() > 0); btConvexHullShape* hull = new btConvexHullShape(); @@ -158,6 +159,84 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin return hull; } +// util method +btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info) { + assert(info.getType() == SHAPE_TYPE_MESH); // should only get here for mesh shapes + + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + assert(pointCollection.size() == 1); // should only have one mesh + + const ShapeInfo::PointList& pointList = pointCollection[0]; + assert(pointList.size() > 2); // should have at least one triangle's worth of points + + const ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + assert(triangleIndices.size() > 2); // should have at least one triangle's worth of indices + + // allocate mesh buffers + btIndexedMesh mesh; + int32_t numIndices = triangleIndices.size(); + const int32_t VERTICES_PER_TRIANGLE = 3; + mesh.m_numTriangles = numIndices / VERTICES_PER_TRIANGLE; + if (numIndices < INT16_MAX) { + // small number of points so we can use 16-bit indices + mesh.m_triangleIndexBase = new unsigned char[sizeof(int16_t) * (size_t)numIndices]; + mesh.m_indexType = PHY_SHORT; + mesh.m_triangleIndexStride = VERTICES_PER_TRIANGLE * sizeof(int16_t); + } else { + mesh.m_triangleIndexBase = new unsigned char[sizeof(int32_t) * (size_t)numIndices]; + mesh.m_indexType = PHY_INTEGER; + mesh.m_triangleIndexStride = VERTICES_PER_TRIANGLE * sizeof(int32_t); + } + mesh.m_numVertices = pointList.size(); + mesh.m_vertexBase = new unsigned char[VERTICES_PER_TRIANGLE * sizeof(btScalar) * (size_t)mesh.m_numVertices]; + mesh.m_vertexStride = VERTICES_PER_TRIANGLE * sizeof(btScalar); + mesh.m_vertexType = PHY_FLOAT; + + // copy data into buffers + btScalar* vertexData = static_cast((void*)(mesh.m_vertexBase)); + for (int32_t i = 0; i < mesh.m_numVertices; ++i) { + int32_t j = i * VERTICES_PER_TRIANGLE; + const glm::vec3& point = pointList[i]; + vertexData[j] = point.x; + vertexData[j + 1] = point.y; + vertexData[j + 2] = point.z; + } + if (numIndices < INT16_MAX) { + int16_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); + for (int32_t i = 0; i < numIndices; ++i) { + indices[i] = triangleIndices[i]; + } + } else { + int32_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); + for (int32_t i = 0; i < numIndices; ++i) { + indices[i] = triangleIndices[i]; + } + } + + // store buffers in a new dataArray and return the pointer + // (external StaticMeshShape will own all of the data that was allocated here) + btTriangleIndexVertexArray* dataArray = new btTriangleIndexVertexArray; + dataArray->addIndexedMesh(mesh, mesh.m_indexType); + return dataArray; +} + +// util method +void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray) { + assert(dataArray); + IndexedMeshArray& meshes = dataArray->getIndexedMeshArray(); + for (int32_t i = 0; i < meshes.size(); ++i) { + btIndexedMesh mesh = meshes[i]; + mesh.m_numTriangles = 0; + delete [] mesh.m_triangleIndexBase; + mesh.m_triangleIndexBase = nullptr; + mesh.m_numVertices = 0; + delete [] mesh.m_vertexBase; + mesh.m_vertexBase = nullptr; + } + meshes.clear(); + delete dataArray; +} + btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { btCollisionShape* shape = NULL; int type = info.getType(); @@ -195,6 +274,11 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } } break; + case SHAPE_TYPE_MESH: { + btTriangleIndexVertexArray* dataArray = createStaticMeshArray(info); + shape = new StaticMeshShape(dataArray); + } + break; } if (shape) { if (glm::length2(info.getOffset()) > MIN_SHAPE_OFFSET * MIN_SHAPE_OFFSET) { @@ -228,3 +312,14 @@ void ShapeFactory::deleteShape(btCollisionShape* shape) { } delete shape; } + +// the dataArray must be created before we create the StaticMeshShape +ShapeFactory::StaticMeshShape::StaticMeshShape(btTriangleIndexVertexArray* dataArray) +: btBvhTriangleMeshShape(dataArray, true), _dataArray(dataArray) { + assert(dataArray); +} + +ShapeFactory::StaticMeshShape::~StaticMeshShape() { + deleteStaticMeshArray(_dataArray); + _dataArray = nullptr; +} diff --git a/libraries/physics/src/ShapeFactory.h b/libraries/physics/src/ShapeFactory.h index 1ba2bdb619..6202612eb9 100644 --- a/libraries/physics/src/ShapeFactory.h +++ b/libraries/physics/src/ShapeFactory.h @@ -20,9 +20,22 @@ // translates between ShapeInfo and btShape namespace ShapeFactory { - btConvexHullShape* createConvexHull(const QVector& points); btCollisionShape* createShapeFromInfo(const ShapeInfo& info); void deleteShape(btCollisionShape* shape); + + //btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info); + //void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray); + + class StaticMeshShape : public btBvhTriangleMeshShape { + public: + StaticMeshShape() = delete; + StaticMeshShape(btTriangleIndexVertexArray* dataArray); + ~StaticMeshShape(); + + private: + // the StaticMeshShape owns its vertex/index data + btTriangleIndexVertexArray* _dataArray; + }; }; #endif // hifi_ShapeFactory_h diff --git a/libraries/physics/src/ShapeManager.cpp b/libraries/physics/src/ShapeManager.cpp index 4231d1eb60..4fa660239c 100644 --- a/libraries/physics/src/ShapeManager.cpp +++ b/libraries/physics/src/ShapeManager.cpp @@ -32,15 +32,13 @@ btCollisionShape* ShapeManager::getShape(const ShapeInfo& info) { if (info.getType() == SHAPE_TYPE_NONE) { return NULL; } - if (info.getType() != SHAPE_TYPE_COMPOUND) { - // Very small or large non-compound objects are not supported. - float diagonal = 4.0f * glm::length2(info.getHalfExtents()); - const float MIN_SHAPE_DIAGONAL_SQUARED = 3.0e-4f; // 1 cm cube - if (diagonal < MIN_SHAPE_DIAGONAL_SQUARED) { - // qCDebug(physics) << "ShapeManager::getShape -- not making shape due to size" << diagonal; - return NULL; - } + const float MIN_SHAPE_DIAGONAL_SQUARED = 3.0e-4f; // 1 cm cube + if (4.0f * glm::length2(info.getHalfExtents()) < MIN_SHAPE_DIAGONAL_SQUARED) { + // tiny shapes are not supported + // qCDebug(physics) << "ShapeManager::getShape -- not making shape due to size" << diagonal; + return NULL; } + DoubleHashKey key = info.getHash(); ShapeReference* shapeRef = _shapeMap.find(key); if (shapeRef) { @@ -66,8 +64,8 @@ bool ShapeManager::releaseShapeByKey(const DoubleHashKey& key) { shapeRef->refCount--; if (shapeRef->refCount == 0) { _pendingGarbage.push_back(key); - const int MAX_GARBAGE_CAPACITY = 255; - if (_pendingGarbage.size() > MAX_GARBAGE_CAPACITY) { + const int MAX_SHAPE_GARBAGE_CAPACITY = 255; + if (_pendingGarbage.size() > MAX_SHAPE_GARBAGE_CAPACITY) { collectGarbage(); } } diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index cd0cb6fe8a..ed1a76ef99 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -41,9 +41,9 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString break; } case SHAPE_TYPE_COMPOUND: + case SHAPE_TYPE_MESH: _url = QUrl(url); - _halfExtents = halfExtents; - break; + // yes, fall through default: _halfExtents = halfExtents; break; @@ -182,18 +182,15 @@ const DoubleHashKey& ShapeInfo::getHash() const { // NOTE: we cache the key so we only ever need to compute it once for any valid ShapeInfo instance. if (_doubleHashKey.isNull() && _type != SHAPE_TYPE_NONE) { bool useOffset = glm::length2(_offset) > MIN_SHAPE_OFFSET * MIN_SHAPE_OFFSET; - // The key is not yet cached therefore we must compute it! To this end we bypass the const-ness - // of this method by grabbing a non-const pointer to "this" and a non-const reference to _doubleHashKey. - ShapeInfo* thisPtr = const_cast(this); - DoubleHashKey& key = thisPtr->_doubleHashKey; + // The key is not yet cached therefore we must compute it. // compute hash1 // TODO?: provide lookup table for hash/hash2 of _type rather than recompute? uint32_t primeIndex = 0; - key.computeHash((uint32_t)_type, primeIndex++); + _doubleHashKey.computeHash((uint32_t)_type, primeIndex++); // compute hash1 - uint32_t hash = key.getHash(); + uint32_t hash = _doubleHashKey.getHash(); for (int j = 0; j < 3; ++j) { // NOTE: 0.49f is used to bump the float up almost half a millimeter // so the cast to int produces a round() effect rather than a floor() @@ -206,10 +203,10 @@ const DoubleHashKey& ShapeInfo::getHash() const { primeIndex++); } } - key.setHash(hash); + _doubleHashKey.setHash(hash); // compute hash2 - hash = key.getHash2(); + hash = _doubleHashKey.getHash2(); for (int j = 0; j < 3; ++j) { // NOTE: 0.49f is used to bump the float up almost half a millimeter // so the cast to int produces a round() effect rather than a floor() @@ -226,7 +223,7 @@ const DoubleHashKey& ShapeInfo::getHash() const { hash += ~(floatHash << 10); hash = (hash << 16) | (hash >> 16); } - key.setHash2(hash); + _doubleHashKey.setHash2(hash); if (_type == SHAPE_TYPE_COMPOUND || _type == SHAPE_TYPE_MESH) { QString url = _url.toString(); @@ -235,8 +232,8 @@ const DoubleHashKey& ShapeInfo::getHash() const { QByteArray baUrl = url.toLocal8Bit(); const char *cUrl = baUrl.data(); uint32_t urlHash = qChecksum(cUrl, baUrl.count()); - key.setHash(key.getHash() ^ urlHash); - key.setHash2(key.getHash2() ^ urlHash); + _doubleHashKey.setHash(_doubleHashKey.getHash() ^ urlHash); + _doubleHashKey.setHash2(_doubleHashKey.getHash2() ^ urlHash); } } } diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 7f178bb53a..794f31a987 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -30,14 +30,15 @@ enum ShapeType { SHAPE_TYPE_NONE, SHAPE_TYPE_BOX, SHAPE_TYPE_SPHERE, - SHAPE_TYPE_PLANE, - SHAPE_TYPE_COMPOUND, SHAPE_TYPE_CAPSULE_X, SHAPE_TYPE_CAPSULE_Y, SHAPE_TYPE_CAPSULE_Z, SHAPE_TYPE_CYLINDER_X, SHAPE_TYPE_CYLINDER_Y, SHAPE_TYPE_CYLINDER_Z, + SHAPE_TYPE_HULL, + SHAPE_TYPE_PLANE, + SHAPE_TYPE_COMPOUND, SHAPE_TYPE_MESH }; @@ -62,10 +63,13 @@ public: const glm::vec3& getHalfExtents() const { return _halfExtents; } const glm::vec3& getOffset() const { return _offset; } + uint32_t getNumSubShapes() const; PointCollection& getPointCollection() { return _pointCollection; } const PointCollection& getPointCollection() const { return _pointCollection; } - uint32_t getNumSubShapes() const; + + TriangleIndices& getTriangleIndices() { return _triangleIndices; } + const TriangleIndices& getTriangleIndices() const { return _triangleIndices; } int getLargestSubshapePointCount() const; @@ -83,7 +87,7 @@ protected: TriangleIndices _triangleIndices; glm::vec3 _halfExtents = glm::vec3(0.0f); glm::vec3 _offset = glm::vec3(0.0f); - DoubleHashKey _doubleHashKey; + mutable DoubleHashKey _doubleHashKey; ShapeType _type = SHAPE_TYPE_NONE; }; From f41fb30aceb4f6c9a66013397fa899c17c84ad11 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 14 Jun 2016 14:59:52 -0700 Subject: [PATCH 0653/1237] add "Static Mesh" option to edit.js --- .../entities/src/EntityItemProperties.cpp | 23 +++++++++++++++---- scripts/system/html/entityProperties.html | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 89bf9f1a21..c521962976 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -88,8 +88,21 @@ void EntityItemProperties::setLastEdited(quint64 usecTime) { _lastEdited = usecTime > _created ? usecTime : _created; } -const char* shapeTypeNames[] = {"none", "box", "sphere", "plane", "compound", "capsule-x", - "capsule-y", "capsule-z", "cylinder-x", "cylinder-y", "cylinder-z"}; +const char* shapeTypeNames[] = { + "none", + "box", + "sphere", + "capsule-x", + "capsule-y", + "capsule-z", + "cylinder-x", + "cylinder-y", + "cylinder-z", + "hull", + "plane", + "compound", + "static-mesh" +}; QHash stringToShapeTypeLookup; @@ -101,14 +114,16 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_NONE); addShapeType(SHAPE_TYPE_BOX); addShapeType(SHAPE_TYPE_SPHERE); - addShapeType(SHAPE_TYPE_PLANE); - addShapeType(SHAPE_TYPE_COMPOUND); addShapeType(SHAPE_TYPE_CAPSULE_X); addShapeType(SHAPE_TYPE_CAPSULE_Y); addShapeType(SHAPE_TYPE_CAPSULE_Z); addShapeType(SHAPE_TYPE_CYLINDER_X); addShapeType(SHAPE_TYPE_CYLINDER_Y); addShapeType(SHAPE_TYPE_CYLINDER_Z); + addShapeType(SHAPE_TYPE_HULL); + addShapeType(SHAPE_TYPE_PLANE); + addShapeType(SHAPE_TYPE_COMPOUND); + addShapeType(SHAPE_TYPE_MESH); } QString getCollisionGroupAsString(uint8_t group) { diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 0af199ef56..121e38c340 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1646,6 +1646,7 @@ +
From 2f6e5ab2ee3bfcf30b1b98194e2ca2b60d39c849 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 14 Jun 2016 15:26:13 -0700 Subject: [PATCH 0654/1237] remove fall-through in switch/case logic --- libraries/shared/src/ShapeInfo.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index ed1a76ef99..af81a0f96b 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -27,12 +27,12 @@ void ShapeInfo::clear() { void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString url) { _type = type; + _halfExtents = halfExtents; switch(type) { case SHAPE_TYPE_NONE: _halfExtents = glm::vec3(0.0f); break; case SHAPE_TYPE_BOX: - _halfExtents = halfExtents; break; case SHAPE_TYPE_SPHERE: { // sphere radius is max of halfExtents @@ -43,9 +43,8 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString case SHAPE_TYPE_COMPOUND: case SHAPE_TYPE_MESH: _url = QUrl(url); - // yes, fall through + break; default: - _halfExtents = halfExtents; break; } _doubleHashKey.clear(); From d1752211e6181c29a4814151f29d10256e31e032 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 14 Jun 2016 15:52:41 -0700 Subject: [PATCH 0655/1237] make implicit casts explict --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index e7991eb638..73c3c2e50c 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -720,7 +720,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // copy points uint32_t meshIndexOffset = (uint32_t)points.size(); gpu::BufferView::Iterator vertexItr = vertices.cbegin(); - points.reserve(points.size() + vertices.getNumElements()); + points.reserve((int32_t)((gpu::Size)points.size() + vertices.getNumElements())); Extents extents; while (vertexItr != vertices.cend()) { points.push_back(*vertexItr); @@ -741,7 +741,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } // copy triangleIndices - triangleIndices.reserve(triangleIndices.size() + indices.getNumElements()); + triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); gpu::BufferView::Iterator partItr = parts.cbegin(); while (partItr != parts.cend()) { From ab3548cac089189db0f4bf38c15b98f8872e4cf4 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 15 Jun 2016 14:56:57 -0700 Subject: [PATCH 0656/1237] fix crash --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 73c3c2e50c..8f0c9f0a02 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -593,6 +593,8 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { // the model is still being downloaded. return false; + } else if (type == SHAPE_TYPE_MESH) { + return (_model && _model->isLoaded()); } return true; } From f444b70fdc6d018c2e634eaa90e06a6e9d80c123 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 15 Jun 2016 18:15:22 -0700 Subject: [PATCH 0657/1237] fix collision shape for scaled models --- .../src/RenderableModelEntityItem.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 8f0c9f0a02..c01ad8b92a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -713,7 +713,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); - glm::vec3 modelOffset = _model->getOffset(); + glm::vec3 scaledModelOffset = _model->getOffset() * _model->getScale(); for (auto& mesh : meshes) { const gpu::BufferView& vertices = mesh->getVertexBuffer(); const gpu::BufferView& indices = mesh->getIndexBuffer(); @@ -730,16 +730,16 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ++vertexItr; } - // scale points and shift by modelOffset + // scale and shift glm::vec3 extentsSize = extents.size(); - glm::vec3 scale = dimensions / extents.size(); + glm::vec3 scaleToFit = dimensions / extents.size(); for (int i = 0; i < 3; ++i) { if (extentsSize[i] < 1.0e-6f) { - scale[i] = 1.0f; + scaleToFit[i] = 1.0f; } } for (int i = 0; i < points.size(); ++i) { - points[i] = (points[i] * scale) + modelOffset; + points[i] = (points[i] * scaleToFit) + scaledModelOffset; } // copy triangleIndices From c0c77e90272a74afeea6556013468a12afe89d39 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 16 Jun 2016 09:26:33 -0700 Subject: [PATCH 0658/1237] optimize/cleanup compound/mesh shape computation --- .../src/RenderableModelEntityItem.cpp | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c01ad8b92a..13d757e9b4 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -618,6 +618,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. const uint32_t TRIANGLE_STRIDE = 3; const uint32_t QUAD_STRIDE = 4; + Extents extents; foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { @@ -631,14 +632,18 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 p0 = mesh.vertices[meshPart.triangleIndices[j]]; glm::vec3 p1 = mesh.vertices[meshPart.triangleIndices[j + 1]]; glm::vec3 p2 = mesh.vertices[meshPart.triangleIndices[j + 2]]; + if (!pointsInPart.contains(p0)) { pointsInPart << p0; + extents.addPoint(p0); } if (!pointsInPart.contains(p1)) { pointsInPart << p1; + extents.addPoint(p1); } if (!pointsInPart.contains(p2)) { pointsInPart << p2; + extents.addPoint(p2); } } @@ -652,15 +657,19 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 p3 = mesh.vertices[meshPart.quadIndices[j + 3]]; if (!pointsInPart.contains(p0)) { pointsInPart << p0; + extents.addPoint(p0); } if (!pointsInPart.contains(p1)) { pointsInPart << p1; + extents.addPoint(p1); } if (!pointsInPart.contains(p2)) { pointsInPart << p2; + extents.addPoint(p2); } if (!pointsInPart.contains(p3)) { pointsInPart << p3; + extents.addPoint(p3); } } @@ -673,33 +682,30 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } } + glm::vec3 extentsSize = extents.size(); + glm::vec3 scaleToFit = dimensions / extentsSize; + for (int i = 0; i < 3; ++i) { + if (extentsSize[i] < 1.0e-6f) { + scaleToFit[i] = 1.0f; + } + } + // We expect that the collision model will have the same units and will be displaced // from its origin in the same way the visual model is. The visual model has // been centered and probably scaled. We take the scaling and offset which were applied // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - const FBXGeometry& renderGeometry = _model->getFBXGeometry(); - glm::vec3 scale = dimensions / renderGeometry.getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. - AABox box; + glm::vec3 scaledModelOffset = _model->getOffset() * _model->getScale(); for (int i = 0; i < pointCollection.size(); i++) { for (int j = 0; j < pointCollection[i].size(); j++) { - // compensate for registration - pointCollection[i][j] += _model->getOffset(); - // scale so the collision points match the model points - pointCollection[i][j] *= scale; - // this next subtraction is done so we can give info the offset, which will cause - // the shape-key to change. - pointCollection[i][j] -= _model->getOffset(); - box += pointCollection[i][j]; + pointCollection[i][j] = (pointCollection[i][j] * scaleToFit) + scaledModelOffset; } } - glm::vec3 collisionModelDimensions = box.getDimensions(); - info.setParams(type, collisionModelDimensions, _compoundShapeURL); - info.setOffset(_model->getOffset()); + info.setParams(type, dimensions, _compoundShapeURL); } else if (type == SHAPE_TYPE_MESH) { updateModelBounds(); @@ -713,6 +719,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); + Extents extents; glm::vec3 scaledModelOffset = _model->getOffset() * _model->getScale(); for (auto& mesh : meshes) { const gpu::BufferView& vertices = mesh->getVertexBuffer(); @@ -723,25 +730,12 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { uint32_t meshIndexOffset = (uint32_t)points.size(); gpu::BufferView::Iterator vertexItr = vertices.cbegin(); points.reserve((int32_t)((gpu::Size)points.size() + vertices.getNumElements())); - Extents extents; while (vertexItr != vertices.cend()) { points.push_back(*vertexItr); extents.addPoint(*vertexItr); ++vertexItr; } - // scale and shift - glm::vec3 extentsSize = extents.size(); - glm::vec3 scaleToFit = dimensions / extents.size(); - for (int i = 0; i < 3; ++i) { - if (extentsSize[i] < 1.0e-6f) { - scaleToFit[i] = 1.0f; - } - } - for (int i = 0; i < points.size(); ++i) { - points[i] = (points[i] * scaleToFit) + scaledModelOffset; - } - // copy triangleIndices triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); gpu::BufferView::Iterator partItr = parts.cbegin(); @@ -793,6 +787,19 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ++partItr; } } + + // scale and shift + glm::vec3 extentsSize = extents.size(); + glm::vec3 scaleToFit = dimensions / extentsSize; + for (int i = 0; i < 3; ++i) { + if (extentsSize[i] < 1.0e-6f) { + scaleToFit[i] = 1.0f; + } + } + for (int i = 0; i < points.size(); ++i) { + points[i] = (points[i] * scaleToFit) + scaledModelOffset; + } + pointCollection.push_back(points); info.setParams(SHAPE_TYPE_MESH, 0.5f * dimensions, _modelURL); } else { From 5484b6fbb7e9a545007275a2da0129000c285529 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 16 Jun 2016 12:15:47 -0700 Subject: [PATCH 0659/1237] more correct extraction from triangle strips --- .../src/RenderableModelEntityItem.cpp | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 13d757e9b4..1adbd4fe1a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -761,24 +761,27 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { auto indexEnd = indexItr + (partItr->_numIndices - 2); // first triangle uses the first three indices - triangleIndices.push_back(*indexItr + meshIndexOffset); - triangleIndices.push_back(*(++indexItr) + meshIndexOffset); - triangleIndices.push_back(*(indexItr + 1) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); // the rest use previous and next index uint32_t triangleCount = 1; while (indexItr != indexEnd) { - if (triangleCount % 2 == 0) { - // even triangles use first two indices in order - triangleIndices.push_back(*(indexItr) + meshIndexOffset); - triangleIndices.push_back(*(++indexItr) + meshIndexOffset); // yes pre-increment - } else { - // odd triangles swap order of first two indices - triangleIndices.push_back(*(indexItr + 1) + meshIndexOffset); - triangleIndices.push_back(*(indexItr++) + meshIndexOffset); // yes post-increment + if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) { + if (triangleCount % 2 == 0) { + // even triangles use first two indices in order + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + } else { + // odd triangles swap order of first two indices + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + } + triangleIndices.push_back(*indexItr + meshIndexOffset); + ++triangleCount; } - triangleIndices.push_back(*(indexItr + 1) + meshIndexOffset); - ++triangleCount; + ++indexItr; } } else if (partItr->_topology == model::Mesh::QUADS) { // TODO: support model::Mesh::QUADS From f22a5613bd1b7a2ba0b86de3424e92edda0bd4e7 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 20 Jun 2016 17:27:39 -0700 Subject: [PATCH 0660/1237] fix static mesh for model with per-mesh transforms --- .../src/RenderableModelEntityItem.cpp | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 1adbd4fe1a..7b1dddfcab 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -707,6 +707,22 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { info.setParams(type, dimensions, _compoundShapeURL); } else if (type == SHAPE_TYPE_MESH) { + // compute meshPart local transforms + QVector localTransforms; + const FBXGeometry& geometry = _model->getFBXGeometry(); + int numberOfMeshes = geometry.meshes.size(); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& mesh = geometry.meshes.at(i); + if (mesh.clusters.size() > 0) { + const FBXCluster& cluster = mesh.clusters.at(0); + auto jointMatrix = _model->getRig()->getJointTransform(cluster.jointIndex); + localTransforms.push_back(jointMatrix * cluster.inverseBindMatrix); + } else { + glm::mat4 identity; + localTransforms.push_back(identity); + } + } + updateModelBounds(); // should never fall in here when collision model not fully loaded @@ -720,19 +736,21 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); Extents extents; - glm::vec3 scaledModelOffset = _model->getOffset() * _model->getScale(); + int meshCount = 0; for (auto& mesh : meshes) { const gpu::BufferView& vertices = mesh->getVertexBuffer(); const gpu::BufferView& indices = mesh->getIndexBuffer(); const gpu::BufferView& parts = mesh->getPartBuffer(); // copy points + const glm::mat4& localTransform = localTransforms[meshCount]; uint32_t meshIndexOffset = (uint32_t)points.size(); gpu::BufferView::Iterator vertexItr = vertices.cbegin(); points.reserve((int32_t)((gpu::Size)points.size() + vertices.getNumElements())); while (vertexItr != vertices.cend()) { - points.push_back(*vertexItr); - extents.addPoint(*vertexItr); + glm::vec3 point = extractTranslation(localTransform * glm::translate(*vertexItr)); + points.push_back(point); + extents.addPoint(point); ++vertexItr; } @@ -783,12 +801,10 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } ++indexItr; } - } else if (partItr->_topology == model::Mesh::QUADS) { - // TODO: support model::Mesh::QUADS } - // TODO? support model::Mesh::QUAD_STRIP? ++partItr; } + ++meshCount; } // scale and shift @@ -800,7 +816,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } } for (int i = 0; i < points.size(); ++i) { - points[i] = (points[i] * scaleToFit) + scaledModelOffset; + points[i] = (points[i] * scaleToFit); } pointCollection.push_back(points); From 0c882832bf11827323e7f22a8113d7115b589a84 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 20 Jun 2016 17:58:35 -0700 Subject: [PATCH 0661/1237] Bringing the Scattering type to the pixels and having a different shading during the lighting pass (for global right now) --- libraries/entities-renderer/src/polyvox.slf | 2 +- libraries/fbx/src/FBXReader_Material.cpp | 4 + libraries/model/src/model/Material.cpp | 7 + libraries/model/src/model/Material.h | 27 +++- libraries/model/src/model/Material.slh | 23 +-- .../render-utils/src/DebugDeferredBuffer.cpp | 15 +- .../render-utils/src/DebugDeferredBuffer.h | 3 +- libraries/render-utils/src/DeferredBuffer.slh | 16 +- .../render-utils/src/DeferredBufferRead.slh | 20 +++ .../render-utils/src/DeferredBufferWrite.slh | 5 +- .../render-utils/src/DeferredGlobalLight.slh | 43 ++++++ .../src/DeferredLightingEffect.cpp | 145 +++++++++++------- .../render-utils/src/DeferredLightingEffect.h | 8 +- .../render-utils/src/SubsurfaceScattering.cpp | 65 +++++--- .../render-utils/src/SubsurfaceScattering.h | 61 ++++++-- .../render-utils/src/SubsurfaceScattering.slh | 45 ++++++ .../src/directional_skybox_light.slf | 17 ++ libraries/render-utils/src/model.slf | 5 +- .../render-utils/src/model_normal_map.slf | 5 +- .../src/model_normal_specular_map.slf | 4 +- .../render-utils/src/model_specular_map.slf | 5 +- libraries/render-utils/src/simple.slf | 2 +- .../render-utils/src/simple_textured.slf | 3 +- tests/gpu-test/src/unlit.slf | 2 +- 24 files changed, 405 insertions(+), 127 deletions(-) create mode 100644 libraries/render-utils/src/SubsurfaceScattering.slh diff --git a/libraries/entities-renderer/src/polyvox.slf b/libraries/entities-renderer/src/polyvox.slf index a77049b733..b7682913a7 100644 --- a/libraries/entities-renderer/src/polyvox.slf +++ b/libraries/entities-renderer/src/polyvox.slf @@ -41,5 +41,5 @@ void main(void) { vec3 yzDiffuseScaled = yzDiffuse.rgb * abs(worldNormal.x); vec4 diffuse = vec4(xyDiffuseScaled + xzDiffuseScaled + yzDiffuseScaled, 1.0); - packDeferredFragment(_normal, 1.0, vec3(diffuse), DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION); + packDeferredFragment(_normal, 1.0, vec3(diffuse), DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION, DEFAULT_SCATTERING); } diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index 01878d6ccf..6ad12e4d9a 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -253,6 +253,10 @@ void FBXReader::consolidateFBXMaterials() { } } + if (material.name.contains("body_mat") || material.name.contains("skin")) { + material._material->setScattering(1.0); + } + if (material.opacity <= 0.0f) { material._material->setOpacity(1.0f); } else { diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index dbe3cabdeb..6e7968a571 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -104,6 +104,13 @@ void Material::setMetallic(float metallic) { _schemaBuffer.edit()._metallic = metallic; } +void Material::setScattering(float scattering) { + scattering = glm::clamp(scattering, 0.0f, 1.0f); + _key.setMetallic(scattering > 0.0f); + _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); + _schemaBuffer.edit()._scattering = scattering; +} + void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textureMap) { if (textureMap) { _key.setMapChannel(channel, (true)); diff --git a/libraries/model/src/model/Material.h b/libraries/model/src/model/Material.h index 8dd9dd7960..304ef2e93b 100755 --- a/libraries/model/src/model/Material.h +++ b/libraries/model/src/model/Material.h @@ -35,6 +35,7 @@ public: OPACITY_VAL_BIT, OPACITY_MASK_MAP_BIT, // Opacity Map and Opacity MASK map are mutually exclusive OPACITY_TRANSLUCENT_MAP_BIT, + SCATTERING_VAL_BIT, // THe map bits must be in the same sequence as the enum names for the map channels EMISSIVE_MAP_BIT, @@ -44,6 +45,7 @@ public: NORMAL_MAP_BIT, OCCLUSION_MAP_BIT, LIGHTMAP_MAP_BIT, + SCATTERING_MAP_BIT, NUM_FLAGS, }; @@ -57,6 +59,7 @@ public: NORMAL_MAP, OCCLUSION_MAP, LIGHTMAP_MAP, + SCATTERING_MAP, NUM_MAP_CHANNELS, }; @@ -83,6 +86,8 @@ public: Builder& withTranslucentFactor() { _flags.set(OPACITY_VAL_BIT); return (*this); } + Builder& withScattering() { _flags.set(SCATTERING_VAL_BIT); return (*this); } + Builder& withEmissiveMap() { _flags.set(EMISSIVE_MAP_BIT); return (*this); } Builder& withAlbedoMap() { _flags.set(ALBEDO_MAP_BIT); return (*this); } Builder& withMetallicMap() { _flags.set(METALLIC_MAP_BIT); return (*this); } @@ -94,6 +99,7 @@ public: Builder& withNormalMap() { _flags.set(NORMAL_MAP_BIT); return (*this); } Builder& withOcclusionMap() { _flags.set(OCCLUSION_MAP_BIT); return (*this); } Builder& withLightmapMap() { _flags.set(LIGHTMAP_MAP_BIT); return (*this); } + Builder& withScatteringMap() { _flags.set(SCATTERING_MAP_BIT); return (*this); } // Convenient standard keys that we will keep on using all over the place static MaterialKey opaqueAlbedo() { return Builder().withAlbedo().build(); } @@ -135,7 +141,7 @@ public: void setOpacityMaskMap(bool value) { _flags.set(OPACITY_MASK_MAP_BIT, value); } bool isOpacityMaskMap() const { return _flags[OPACITY_MASK_MAP_BIT]; } - + void setNormalMap(bool value) { _flags.set(NORMAL_MAP_BIT, value); } bool isNormalMap() const { return _flags[NORMAL_MAP_BIT]; } @@ -145,6 +151,12 @@ public: void setLightmapMap(bool value) { _flags.set(LIGHTMAP_MAP_BIT, value); } bool isLightmapMap() const { return _flags[LIGHTMAP_MAP_BIT]; } + void setScattering(bool value) { _flags.set(SCATTERING_VAL_BIT, value); } + bool isScattering() const { return _flags[SCATTERING_VAL_BIT]; } + + void setScatteringMap(bool value) { _flags.set(SCATTERING_MAP_BIT, value); } + bool isScatteringMap() const { return _flags[SCATTERING_MAP_BIT]; } + void setMapChannel(MapChannel channel, bool value) { _flags.set(EMISSIVE_MAP_BIT + channel, value); } bool isMapChannel(MapChannel channel) const { return _flags[EMISSIVE_MAP_BIT + channel]; } @@ -218,6 +230,13 @@ public: Builder& withoutLightmapMap() { _value.reset(MaterialKey::LIGHTMAP_MAP_BIT); _mask.set(MaterialKey::LIGHTMAP_MAP_BIT); return (*this); } Builder& withLightmapMap() { _value.set(MaterialKey::LIGHTMAP_MAP_BIT); _mask.set(MaterialKey::LIGHTMAP_MAP_BIT); return (*this); } + Builder& withoutScattering() { _value.reset(MaterialKey::SCATTERING_VAL_BIT); _mask.set(MaterialKey::SCATTERING_VAL_BIT); return (*this); } + Builder& withScattering() { _value.set(MaterialKey::SCATTERING_VAL_BIT); _mask.set(MaterialKey::SCATTERING_VAL_BIT); return (*this); } + + Builder& withoutScatteringMap() { _value.reset(MaterialKey::SCATTERING_MAP_BIT); _mask.set(MaterialKey::SCATTERING_MAP_BIT); return (*this); } + Builder& withScatteringMap() { _value.set(MaterialKey::SCATTERING_MAP_BIT); _mask.set(MaterialKey::SCATTERING_MAP_BIT); return (*this); } + + // Convenient standard keys that we will keep on using all over the place static MaterialFilter opaqueAlbedo() { return Builder().withAlbedo().withoutTranslucentFactor().build(); } }; @@ -275,6 +294,8 @@ public: void setRoughness(float roughness); float getRoughness() const { return _schemaBuffer.get()._roughness; } + void setScattering(float scattering); + float getScattering() const { return _schemaBuffer.get()._scattering; } // Schema to access the attribute values of the material class Schema { @@ -288,7 +309,9 @@ public: glm::vec3 _fresnel{ 0.03f }; // Fresnel value for a default non metallic float _metallic{ 0.0f }; // Not Metallic - glm::vec3 _spare{ 0.0f }; + float _scattering{ 0.0f }; // Scattering info + + glm::vec2 _spare{ 0.0f }; uint32_t _key{ 0 }; // a copy of the materialKey diff --git a/libraries/model/src/model/Material.slh b/libraries/model/src/model/Material.slh index 4a6139c664..2faf2d8569 100644 --- a/libraries/model/src/model/Material.slh +++ b/libraries/model/src/model/Material.slh @@ -15,7 +15,7 @@ struct Material { vec4 _emissiveOpacity; vec4 _albedoRoughness; vec4 _fresnelMetallic; - vec4 _spareKey; + vec4 _scatteringSpare2Key; }; uniform materialBuffer { @@ -37,7 +37,9 @@ float getMaterialMetallic(Material m) { return m._fresnelMetallic.a; } float getMaterialShininess(Material m) { return 1.0 - getMaterialRoughness(m); } -int getMaterialKey(Material m) { return floatBitsToInt(m._spareKey.w); } +float getMaterialScattering(Material m) { return m._scatteringSpare2Key.x; } + +int getMaterialKey(Material m) { return floatBitsToInt(m._scatteringSpare2Key.w); } const int EMISSIVE_VAL_BIT = 0x00000001; const int UNLIT_VAL_BIT = 0x00000002; @@ -47,14 +49,17 @@ const int GLOSSY_VAL_BIT = 0x00000010; const int OPACITY_VAL_BIT = 0x00000020; const int OPACITY_MASK_MAP_BIT = 0x00000040; const int OPACITY_TRANSLUCENT_MAP_BIT = 0x00000080; +const int SCATTERING_VAL_BIT = 0x00000100; -const int EMISSIVE_MAP_BIT = 0x00000100; -const int ALBEDO_MAP_BIT = 0x00000200; -const int METALLIC_MAP_BIT = 0x00000400; -const int ROUGHNESS_MAP_BIT = 0x00000800; -const int NORMAL_MAP_BIT = 0x00001000; -const int OCCLUSION_MAP_BIT = 0x00002000; -const int LIGHTMAP_MAP_BIT = 0x00004000; + +const int EMISSIVE_MAP_BIT = 0x00000200; +const int ALBEDO_MAP_BIT = 0x00000400; +const int METALLIC_MAP_BIT = 0x00000800; +const int ROUGHNESS_MAP_BIT = 0x00001000; +const int NORMAL_MAP_BIT = 0x00002000; +const int OCCLUSION_MAP_BIT = 0x00004000; +const int LIGHTMAP_MAP_BIT = 0x00008000; +const int SCATTERING_MAP_BIT = 0x00010000; <@endif@> diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 844eaedbb9..0e6697ae4c 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -111,6 +111,13 @@ static const std::string DEFAULT_LIGHTMAP_SHADER{ " }" }; +static const std::string DEFAULT_SCATTERING_SHADER{ + "vec4 getFragmentColor() {" + " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" + " return (frag.mode == FRAG_MODE_SCATTERING ? vec4(vec3(1.0), 1.0) : vec4(vec3(0.0), 1.0));" + " }" +}; + static const std::string DEFAULT_DEPTH_SHADER { "vec4 getFragmentColor() {" " return vec4(vec3(texture(depthMap, uv).x), 1.0);" @@ -173,7 +180,7 @@ static const std::string DEFAULT_DIFFUSED_NORMAL_CURVATURE_SHADER{ " }" }; -static const std::string DEFAULT_SCATTERING_SHADER{ +static const std::string DEFAULT_DEBUG_SCATTERING_SHADER{ "vec4 getFragmentColor() {" " return vec4(pow(vec3(texture(scatteringMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" // " return vec4(vec3(texture(scatteringMap, uv).xyz), 1.0);" @@ -239,6 +246,8 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_OCCLUSION_SHADER; case LightmapMode: return DEFAULT_LIGHTMAP_SHADER; + case ScatteringMode: + return DEFAULT_SCATTERING_SHADER; case LightingMode: return DEFAULT_LIGHTING_SHADER; case ShadowMode: @@ -253,8 +262,8 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_DIFFUSED_CURVATURE_SHADER; case DiffusedNormalCurvatureMode: return DEFAULT_DIFFUSED_NORMAL_CURVATURE_SHADER; - case ScatteringMode: - return DEFAULT_SCATTERING_SHADER; + case ScatteringDebugMode: + return DEFAULT_DEBUG_SCATTERING_SHADER; case AmbientOcclusionMode: return DEFAULT_AMBIENT_OCCLUSION_SHADER; case AmbientOcclusionBlurredMode: diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index e1d76e1f71..61ebec8954 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -57,6 +57,7 @@ protected: UnlitMode, OcclusionMode, LightmapMode, + ScatteringMode, LightingMode, ShadowMode, PyramidDepthMode, @@ -64,7 +65,7 @@ protected: NormalCurvatureMode, DiffusedCurvatureMode, DiffusedNormalCurvatureMode, - ScatteringMode, + ScatteringDebugMode, AmbientOcclusionMode, AmbientOcclusionBlurredMode, CustomMode // Needs to stay last diff --git a/libraries/render-utils/src/DeferredBuffer.slh b/libraries/render-utils/src/DeferredBuffer.slh index b9c65a3bff..5f80354585 100755 --- a/libraries/render-utils/src/DeferredBuffer.slh +++ b/libraries/render-utils/src/DeferredBuffer.slh @@ -20,11 +20,16 @@ const float FRAG_PACK_LIGHTMAPPED_NON_METALLIC = 0.2; const float FRAG_PACK_LIGHTMAPPED_METALLIC = 0.3; const float FRAG_PACK_LIGHTMAPPED_RANGE_INV = 1.0 / (FRAG_PACK_LIGHTMAPPED_METALLIC - FRAG_PACK_LIGHTMAPPED_NON_METALLIC); -const float FRAG_PACK_UNLIT = 0.5; +const float FRAG_PACK_SCATTERING_NON_METALLIC = 0.4; +const float FRAG_PACK_SCATTERING_METALLIC = 0.5; +const float FRAG_PACK_SCATTERING_RANGE_INV = 1.0 / (FRAG_PACK_SCATTERING_METALLIC - FRAG_PACK_SCATTERING_NON_METALLIC); + +const float FRAG_PACK_UNLIT = 0.6; const int FRAG_MODE_UNLIT = 0; const int FRAG_MODE_SHADED = 1; const int FRAG_MODE_LIGHTMAPPED = 2; +const int FRAG_MODE_SCATTERING = 3; void unpackModeMetallic(float rawValue, out int mode, out float metallic) { if (rawValue <= FRAG_PACK_SHADED_METALLIC) { @@ -32,7 +37,10 @@ void unpackModeMetallic(float rawValue, out int mode, out float metallic) { metallic = clamp((rawValue - FRAG_PACK_SHADED_NON_METALLIC) * FRAG_PACK_SHADED_RANGE_INV, 0.0, 1.0); } else if (rawValue <= FRAG_PACK_LIGHTMAPPED_METALLIC) { mode = FRAG_MODE_LIGHTMAPPED; - metallic = clamp((rawValue - FRAG_PACK_LIGHTMAPPED_NON_METALLIC) * FRAG_PACK_SHADED_RANGE_INV, 0.0, 1.0); + metallic = clamp((rawValue - FRAG_PACK_LIGHTMAPPED_NON_METALLIC) * FRAG_PACK_LIGHTMAPPED_RANGE_INV, 0.0, 1.0); + } else if (rawValue <= FRAG_PACK_SCATTERING_METALLIC) { + mode = FRAG_MODE_SCATTERING; + metallic = clamp((rawValue - FRAG_PACK_SCATTERING_NON_METALLIC) * FRAG_PACK_SCATTERING_RANGE_INV, 0.0, 1.0); } else if (rawValue >= FRAG_PACK_UNLIT) { mode = FRAG_MODE_UNLIT; metallic = 0.0; @@ -47,6 +55,10 @@ float packLightmappedMetallic(float metallic) { return mix(FRAG_PACK_LIGHTMAPPED_NON_METALLIC, FRAG_PACK_LIGHTMAPPED_METALLIC, metallic); } +float packScatteringMetallic(float metallic) { + return mix(FRAG_PACK_SCATTERING_NON_METALLIC, FRAG_PACK_SCATTERING_METALLIC, metallic); +} + float packUnlit() { return FRAG_PACK_UNLIT; } diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index ffa1cc4771..cabfe001c7 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -111,4 +111,24 @@ DeferredFragment unpackDeferredFragment(DeferredFrameTransform deferredTransform } + +<@func declareDeferredCurvature()@> + +// the curvature texture +uniform sampler2D curvatureMap; + +vec4 fetchCurvature(vec2 texcoord) { + return texture(curvatureMap, texcoord); +} + +// the curvature texture +uniform sampler2D diffusedCurvatureMap; + +vec4 fetchDiffusedCurvature(vec2 texcoord) { + return texture(diffusedCurvatureMap, texcoord); +} + + +<@endfunc@> + <@endif@> diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index e869f32dc6..71236c71ad 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -30,13 +30,14 @@ const float DEFAULT_METALLIC = 0; const vec3 DEFAULT_SPECULAR = vec3(0.1); const vec3 DEFAULT_EMISSIVE = vec3(0.0); const float DEFAULT_OCCLUSION = 1.0; +const float DEFAULT_SCATTERING = 0.0; const vec3 DEFAULT_FRESNEL = DEFAULT_EMISSIVE; -void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 emissive, float occlusion) { +void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 emissive, float occlusion, float scattering) { if (alpha != 1.0) { discard; } - _fragColor0 = vec4(albedo, packShadedMetallic(metallic)); + _fragColor0 = vec4(albedo, ((scattering > 0.0) ? packScatteringMetallic(metallic) : packShadedMetallic(metallic))); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(emissive, occlusion); } diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index d29c8bea87..bfab0fafb8 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -152,6 +152,49 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur <@endfunc@> +<@func declareEvalSkyboxGlobalColorScattering()@> + +<$declareDeferredCurvature()$> +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringResource()$> + +vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { + // prepareGlobalLight + + // Transform directions to worldspace + vec3 fragNormal = vec3((normal)); + vec3 fragEyeVector = vec3(invViewMat * vec4(-position, 0.0)); + vec3 fragEyeDir = normalize(fragEyeVector); + + // Get light + Light light = getLight(); + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + float metallic = 1.0; + + vec3 fragLightDir = -normalize(getLightDirection(light)); + + vec3 bentNormalHigh = normalize( (blurredCurvature.xyz - 0.5f) * 2.0f ); + vec3 bentNormalLow = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); + float curvature = unpackCurvature(diffusedCurvature.w); + + + vec3 rS = bentNormalHigh; + + vec3 bendFactorSpectrum = getBendFactor(); + vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); + vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); + vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); + + vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); + + // --> Look up the pre-integrated curvature-dependent BDRF textures + vec3 bdrf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + + + return vec3(bdrf); +} +<@endfunc@> + <@func declareEvalGlobalLightingAlphaBlended()@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index f70b68b4f0..3e2607413e 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -41,13 +41,14 @@ using namespace render; struct LightLocations { - int radius; - int ambientSphere; - int lightBufferUnit; - int sphereParam; - int coneParam; - int deferredFrameTransformBuffer; - int shadowTransformBuffer; + int radius{ -1 }; + int ambientSphere{ -1 }; + int lightBufferUnit{ -1 }; + int sphereParam{ -1 }; + int coneParam{ -1 }; + int deferredFrameTransformBuffer{ -1 }; + int subsurfaceScatteringParametersBuffer{ -1 }; + int shadowTransformBuffer{ -1 }; }; enum DeferredShader_MapSlot { @@ -58,10 +59,14 @@ enum DeferredShader_MapSlot { DEFERRED_BUFFER_OBSCURANCE_UNIT = 4, SHADOW_MAP_UNIT = 5, SKYBOX_MAP_UNIT = 6, + DEFERRED_BUFFER_CURVATURE_UNIT, + DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, + SCATTERING_LUT_UNIT, }; enum DeferredShader_BufferSlot { - DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 2, - LIGHT_GPU_SLOT = 3, + DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 0, + SCATTERING_PARAMETERS_BUFFER_SLOT, + LIGHT_GPU_SLOT, }; static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& program, LightLocationsPtr& locations); @@ -167,7 +172,13 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo slotBindings.insert(gpu::Shader::Binding(std::string("shadowMap"), SHADOW_MAP_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), SKYBOX_MAP_UNIT)); + slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), DEFERRED_BUFFER_CURVATURE_UNIT)); + slotBindings.insert(gpu::Shader::Binding(std::string("diffusedCurvatureMap"), DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT)); + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), SCATTERING_LUT_UNIT)); + + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("subsurfaceScatteringParametersBuffer"), SCATTERING_PARAMETERS_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); @@ -180,6 +191,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo locations->lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); locations->deferredFrameTransformBuffer = program->getBuffers().findLocation("deferredFrameTransformBuffer"); + locations->subsurfaceScatteringParametersBuffer = program->getBuffers().findLocation("subsurfaceScatteringParametersBuffer"); locations->shadowTransformBuffer = program->getBuffers().findLocation("shadowTransformBuffer"); auto state = std::make_shared(); @@ -338,7 +350,7 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC } -void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { +void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource) { auto args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { @@ -372,59 +384,68 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c // need to assign the white texture if ao is off batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, textureCache->getWhiteTexture()); } - - assert(deferredLightingEffect->getLightStage().lights.size() > 0); - const auto& globalShadow = deferredLightingEffect->getLightStage().lights[0]->shadow; - - // Bind the shadow buffer - batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow.map); + // The Deferred Frame Transform buffer batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, frameTransform->getFrameTransformBuffer()); + // Subsurface scattering specific + batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, framebufferCache->getCurvatureTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, framebufferCache->getCurvatureTexture()); + + batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, subsurfaceScatteringResource->getParametersBuffer()); + + + batch.setResourceTexture(SCATTERING_LUT_UNIT, subsurfaceScatteringResource->getScatteringTable()); + // Global directional light and ambient pass - { - auto& program = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadow : deferredLightingEffect->_directionalLight; - LightLocationsPtr locations = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadowLocations : deferredLightingEffect->_directionalLightLocations; - const auto& keyLight = deferredLightingEffect->_allocatedLights[deferredLightingEffect->_globalLights.front()]; - - // Setup the global directional pass pipeline - { - if (deferredLightingEffect->_shadowMapEnabled) { - if (keyLight->getAmbientMap()) { - program = deferredLightingEffect->_directionalSkyboxLightShadow; - locations = deferredLightingEffect->_directionalSkyboxLightShadowLocations; - } else { - program = deferredLightingEffect->_directionalAmbientSphereLightShadow; - locations = deferredLightingEffect->_directionalAmbientSphereLightShadowLocations; - } - } else { - if (keyLight->getAmbientMap()) { - program = deferredLightingEffect->_directionalSkyboxLight; - locations = deferredLightingEffect->_directionalSkyboxLightLocations; - } else { - program = deferredLightingEffect->_directionalAmbientSphereLight; - locations = deferredLightingEffect->_directionalAmbientSphereLightLocations; - } - } - - if (locations->shadowTransformBuffer >= 0) { - batch.setUniformBuffer(locations->shadowTransformBuffer, globalShadow.getBuffer()); - } - batch.setPipeline(program); - } - - { // Setup the global lighting - deferredLightingEffect->setupKeyLightBatch(batch, locations->lightBufferUnit, SKYBOX_MAP_UNIT); - } - - batch.draw(gpu::TRIANGLE_STRIP, 4); - if (keyLight->getAmbientMap()) { - batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); + assert(deferredLightingEffect->getLightStage().lights.size() > 0); + const auto& globalShadow = deferredLightingEffect->getLightStage().lights[0]->shadow; + + // Bind the shadow buffer + batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow.map); + + auto& program = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadow : deferredLightingEffect->_directionalLight; + LightLocationsPtr locations = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadowLocations : deferredLightingEffect->_directionalLightLocations; + const auto& keyLight = deferredLightingEffect->_allocatedLights[deferredLightingEffect->_globalLights.front()]; + + // Setup the global directional pass pipeline + { + if (deferredLightingEffect->_shadowMapEnabled) { + if (keyLight->getAmbientMap()) { + program = deferredLightingEffect->_directionalSkyboxLightShadow; + locations = deferredLightingEffect->_directionalSkyboxLightShadowLocations; + } else { + program = deferredLightingEffect->_directionalAmbientSphereLightShadow; + locations = deferredLightingEffect->_directionalAmbientSphereLightShadowLocations; + } + } else { + if (keyLight->getAmbientMap()) { + program = deferredLightingEffect->_directionalSkyboxLight; + locations = deferredLightingEffect->_directionalSkyboxLightLocations; + } else { + program = deferredLightingEffect->_directionalAmbientSphereLight; + locations = deferredLightingEffect->_directionalAmbientSphereLightLocations; + } } + + if (locations->shadowTransformBuffer >= 0) { + batch.setUniformBuffer(locations->shadowTransformBuffer, globalShadow.getBuffer()); + } + batch.setPipeline(program); } - + + { // Setup the global lighting + deferredLightingEffect->setupKeyLightBatch(batch, locations->lightBufferUnit, SKYBOX_MAP_UNIT); + } + + batch.draw(gpu::TRIANGLE_STRIP, 4); + + if (keyLight->getAmbientMap()) { + batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); + } + batch.setResourceTexture(SHADOW_MAP_UNIT, nullptr); }); } @@ -562,9 +583,12 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, batch.setResourceTexture(DEFERRED_BUFFER_EMISSIVE_UNIT, nullptr); batch.setResourceTexture(DEFERRED_BUFFER_DEPTH_UNIT, nullptr); batch.setResourceTexture(DEFERRED_BUFFER_OBSCURANCE_UNIT, nullptr); - batch.setResourceTexture(SHADOW_MAP_UNIT, nullptr); - batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); + + batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, nullptr); + batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, nullptr); + batch.setResourceTexture(SCATTERING_LUT_UNIT, nullptr); + batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, nullptr); batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, nullptr); }); @@ -582,7 +606,12 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFrameTransformPointer& deferredTransform) { - setupJob.run(sceneContext, renderContext, deferredTransform); + if (!_subsurfaceScatteringResource) { + _subsurfaceScatteringResource = std::make_shared(); + _subsurfaceScatteringResource->generateScatteringTable(renderContext->args); + } + + setupJob.run(sceneContext, renderContext, deferredTransform, _subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform); cleanupJob.run(sceneContext, renderContext); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index c88242bdc5..da59a98e37 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -27,6 +27,8 @@ #include "LightStage.h" +#include "SubsurfaceScattering.h" + class RenderArgs; struct LightLocations; using LightLocationsPtr = std::shared_ptr; @@ -111,9 +113,9 @@ public: class RenderDeferredSetup { public: - using JobModel = render::Job::ModelI; + // using JobModel = render::Job::ModelI; - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource); }; class RenderDeferredLocals { @@ -140,6 +142,8 @@ public: RenderDeferredSetup setupJob; RenderDeferredLocals lightsJob; RenderDeferredCleanup cleanupJob; + + SubsurfaceScatteringResourcePointer _subsurfaceScatteringResource; }; #endif // hifi_DeferredLightingEffect_h diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 2d2769ad0b..25df2c388b 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -38,26 +38,49 @@ enum ScatteringShaderMapSlots { }; -SubsurfaceScattering::SubsurfaceScattering() { +SubsurfaceScatteringResource::SubsurfaceScatteringResource() { Parameters parameters; _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); + +} + +void SubsurfaceScatteringResource::setBentNormalFactors(const glm::vec4& rgbsBentFactors) { + if (rgbsBentFactors != getBentNormalFactors()) { + _parametersBuffer.edit().normalBentInfo = rgbsBentFactors; + } +} + +glm::vec4 SubsurfaceScatteringResource::getBentNormalFactors() const { + return _parametersBuffer.get().normalBentInfo; +} + +void SubsurfaceScatteringResource::setCurvatureFactors(const glm::vec2& sbCurvatureFactors) { + if (sbCurvatureFactors != getCurvatureFactors()) { + _parametersBuffer.edit().curvatureInfo = sbCurvatureFactors; + } +} + +glm::vec2 SubsurfaceScatteringResource::getCurvatureFactors() const { + return _parametersBuffer.get().curvatureInfo; +} + +void SubsurfaceScatteringResource::generateScatteringTable(RenderArgs* args) { + if (!_scatteringTable) { + _scatteringTable = generatePreIntegratedScattering(args); + } +} + +SubsurfaceScattering::SubsurfaceScattering() { + _scatteringResource = std::make_shared(); } void SubsurfaceScattering::configure(const Config& config) { - auto& params = _parametersBuffer.get(); glm::vec4 bentInfo(config.bentRed, config.bentGreen, config.bentBlue, config.bentScale); + _scatteringResource->setBentNormalFactors(bentInfo); - if (bentInfo != params.normalBentInfo) { - _parametersBuffer.edit().normalBentInfo = bentInfo; - } - - if (config.curvatureOffset != params.curvatureInfo.x) { - _parametersBuffer.edit().curvatureInfo.x = config.curvatureOffset; - } - if (config.curvatureScale != params.curvatureInfo.y) { - _parametersBuffer.edit().curvatureInfo.y = config.curvatureScale; - } + glm::vec2 curvatureInfo(config.curvatureOffset, config.curvatureScale); + _scatteringResource->setCurvatureFactors(curvatureInfo); _showLUT = config.showLUT; } @@ -164,9 +187,8 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, RenderArgs* args = renderContext->args; - if (!_scatteringTable) { - _scatteringTable = SubsurfaceScattering::generatePreIntegratedScattering(args); - } + _scatteringResource->generateScatteringTable(args); + auto scatteringTable = _scatteringResource->getScatteringTable(); auto pipeline = getScatteringPipeline(); @@ -194,14 +216,14 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setPipeline(pipeline); batch.setUniformBuffer(ScatteringTask_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); - batch.setUniformBuffer(ScatteringTask_ParamSlot, _parametersBuffer); + batch.setUniformBuffer(ScatteringTask_ParamSlot, _scatteringResource->getParametersBuffer()); if (theLight->light) { batch.setUniformBuffer(ScatteringTask_LightSlot, theLight->light->getSchemaBuffer()); if (theLight->light->getAmbientMap()) { batch.setResourceTexture(ScatteringTask_IBLMapSlot, theLight->light->getAmbientMap()); } } - batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, _scatteringTable); + batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, scatteringTable); batch.setResourceTexture(ScatteringTask_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(ScatteringTask_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); @@ -214,7 +236,7 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, auto viewportSize = std::min(args->_viewport.z, args->_viewport.w) >> 1; batch.setViewportTransform(glm::ivec4(0, 0, viewportSize, viewportSize)); batch.setPipeline(getShowLUTPipeline()); - batch.setResourceTexture(0, _scatteringTable); + batch.setResourceTexture(0, scatteringTable); batch.draw(gpu::TRIANGLE_STRIP, 4); } }); @@ -420,18 +442,15 @@ void diffuseProfile(gpu::TexturePointer& profile) { profile->assignStoredMip(0, gpu::Element::COLOR_RGBA_32, bytes.size(), bytes.data()); } +gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScattering(RenderArgs* args) { - -gpu::TexturePointer SubsurfaceScattering::generatePreIntegratedScattering(RenderArgs* args) { - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 128, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); diffuseProfile(profileMap); const int WIDTH = 128; const int HEIGHT = 128; auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - diffuseScatter(scatteringLUT); + diffuseScatter(scatteringLUT); //diffuseScatterGPU(profileMap, scatteringLUT, args); return scatteringLUT; } - diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 4322379e57..c0ce14cc8c 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -17,6 +17,49 @@ #include "render/DrawTask.h" #include "DeferredFrameTransform.h" +class SubsurfaceScatteringResource { +public: + using UniformBufferView = gpu::BufferView; + + SubsurfaceScatteringResource(); + + void setBentNormalFactors(const glm::vec4& rgbsBentFactors); + glm::vec4 getBentNormalFactors() const; + + void setCurvatureFactors(const glm::vec2& sbCurvatureFactors); + glm::vec2 getCurvatureFactors() const; + + UniformBufferView getParametersBuffer() const { return _parametersBuffer; } + + gpu::TexturePointer getScatteringTable() const { return _scatteringTable; } + + void generateScatteringTable(RenderArgs* args); + static gpu::TexturePointer generatePreIntegratedScattering(RenderArgs* args); + +protected: + + + // Class describing the uniform buffer with the transform info common to the AO shaders + // It s changing every frame + class Parameters { + public: + glm::vec4 normalBentInfo{ 1.5f, 0.8f, 0.3f, 1.5f }; + glm::vec2 curvatureInfo{ 0.08f, 0.8f }; + glm::vec2 spare{ 0.0f }; + + Parameters() {} + }; + UniformBufferView _parametersBuffer; + + + + gpu::TexturePointer _scatteringTable; +}; + +using SubsurfaceScatteringResourcePointer = std::shared_ptr; + + + class SubsurfaceScatteringConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(float bentRed MEMBER bentRed NOTIFY dirty) @@ -57,24 +100,8 @@ public: void configure(const Config& config); void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, gpu::FramebufferPointer& scatteringFramebuffer); - static gpu::TexturePointer generatePreIntegratedScattering(RenderArgs* args); - private: - typedef gpu::BufferView UniformBufferView; - - // Class describing the uniform buffer with all the parameters common to the AO shaders - class Parameters { - public: - glm::vec4 normalBentInfo { 0.0f }; - glm::vec4 curvatureInfo{ 0.0f }; - - Parameters() {} - }; - gpu::BufferView _parametersBuffer; - - - gpu::TexturePointer _scatteringTable; - + SubsurfaceScatteringResourcePointer _scatteringResource; bool updateScatteringFramebuffer(const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& scatteringFramebuffer); gpu::FramebufferPointer _scatteringFramebuffer; diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh new file mode 100644 index 0000000000..985d3d5404 --- /dev/null +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -0,0 +1,45 @@ +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/8/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +<@func declareSubsurfaceScatteringResource()@> + +uniform sampler2D scatteringLUT; + +vec3 fetchBRDF(float LdotN, float curvature) { + return texture(scatteringLUT, vec2( LdotN * 0.5 + 0.5, curvature)).xyz; +} + +vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { + return vec3( + fetchBRDF(LdotNSpectrum.r, curvature).r, + fetchBRDF(LdotNSpectrum.g, curvature).g, + fetchBRDF(LdotNSpectrum.b, curvature).b + ); +} + +// Subsurface Scattering parameters +struct ScatteringParameters { + vec4 normalBendInfo; // R, G, B, factor + vec4 curvatureInfo;// Offset, Scale +}; + +uniform subsurfaceScatteringParametersBuffer { + ScatteringParameters parameters; +}; + +vec3 getBendFactor() { + return parameters.normalBendInfo.xyz * parameters.normalBendInfo.w; +} + +float unpackCurvature(float packedCurvature) { + return abs(packedCurvature * 2 - 1) * 0.5f * parameters.curvatureInfo.y + parameters.curvatureInfo.x; +} + +<@endfunc@> diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index 074ee90fca..544271d20d 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -17,6 +17,7 @@ <$declareEvalLightmappedColor()$> <$declareEvalSkyboxGlobalColor()$> +<$declareEvalSkyboxGlobalColorScattering()$> in vec2 _texCoord0; out vec4 _fragColor; @@ -39,6 +40,22 @@ void main(void) { frag.diffuse, frag.specularVal.xyz); _fragColor = vec4(color, 1.0); + } else if (frag.mode == FRAG_MODE_SCATTERING) { + + vec4 blurredCurvature = fetchCurvature(_texCoord0); + vec4 diffusedCurvature = fetchDiffusedCurvature(_texCoord0); + + vec3 color = evalSkyboxGlobalColorScattering( + getViewInverse(), + shadowAttenuation, + frag.obscurance, + frag.position.xyz, + frag.normal, + frag.diffuse, + blurredCurvature, + diffusedCurvature, + frag.roughness); + _fragColor = vec4(color, 1.0); } else { vec3 color = evalSkyboxGlobalColor( getViewInverse(), diff --git a/libraries/render-utils/src/model.slf b/libraries/render-utils/src/model.slf index c1f5cb1f88..daeead65ec 100755 --- a/libraries/render-utils/src/model.slf +++ b/libraries/render-utils/src/model.slf @@ -45,6 +45,8 @@ void main(void) { vec3 emissive = getMaterialEmissive(mat); <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; + float scattering = getMaterialScattering(mat); + packDeferredFragment( normalize(_normal.xyz), opacity, @@ -52,5 +54,6 @@ void main(void) { roughness, getMaterialMetallic(mat), emissive, - occlusionTex); + occlusionTex, + scattering); } diff --git a/libraries/render-utils/src/model_normal_map.slf b/libraries/render-utils/src/model_normal_map.slf index daaa1ed977..e6baac4e04 100755 --- a/libraries/render-utils/src/model_normal_map.slf +++ b/libraries/render-utils/src/model_normal_map.slf @@ -49,6 +49,8 @@ void main(void) { vec3 viewNormal; <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> + float scattering = getMaterialScattering(mat); + packDeferredFragment( viewNormal, opacity, @@ -56,5 +58,6 @@ void main(void) { roughness, getMaterialMetallic(mat), emissive, - occlusionTex); + occlusionTex, + scattering); } diff --git a/libraries/render-utils/src/model_normal_specular_map.slf b/libraries/render-utils/src/model_normal_specular_map.slf index dd2d3cc951..d5dd607b8f 100755 --- a/libraries/render-utils/src/model_normal_specular_map.slf +++ b/libraries/render-utils/src/model_normal_specular_map.slf @@ -52,6 +52,7 @@ void main(void) { float metallic = getMaterialMetallic(mat); <$evalMaterialMetallic(metallicTex, metallic, matKey, metallic)$>; + float scattering = getMaterialScattering(mat); packDeferredFragment( normalize(viewNormal.xyz), @@ -60,5 +61,6 @@ void main(void) { roughness, metallic, emissive, - occlusionTex); + occlusionTex, + scattering); } diff --git a/libraries/render-utils/src/model_specular_map.slf b/libraries/render-utils/src/model_specular_map.slf index f0fe20293c..47b5e3389d 100755 --- a/libraries/render-utils/src/model_specular_map.slf +++ b/libraries/render-utils/src/model_specular_map.slf @@ -49,6 +49,8 @@ void main(void) { float metallic = getMaterialMetallic(mat); <$evalMaterialMetallic(metallicTex, metallic, matKey, metallic)$>; + float scattering = getMaterialScattering(mat); + packDeferredFragment( normalize(_normal), opacity, @@ -56,5 +58,6 @@ void main(void) { roughness, metallic, emissive, - occlusionTex); + occlusionTex, + scattering); } diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index 0f848ee231..4a16c1c46a 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -54,6 +54,6 @@ void main(void) { normal, 1.0, diffuse, max(0, 1.0 - shininess / 128.0), DEFAULT_METALLIC, specular, specular); } else { packDeferredFragment( - normal, 1.0, diffuse, max(0, 1.0 - shininess / 128.0), length(specular), DEFAULT_EMISSIVE, DEFAULT_OCCLUSION); + normal, 1.0, diffuse, max(0, 1.0 - shininess / 128.0), length(specular), DEFAULT_EMISSIVE, DEFAULT_OCCLUSION, DEFAULT_SCATTERING); } } diff --git a/libraries/render-utils/src/simple_textured.slf b/libraries/render-utils/src/simple_textured.slf index 062fb96f7d..f045af2ce5 100644 --- a/libraries/render-utils/src/simple_textured.slf +++ b/libraries/render-utils/src/simple_textured.slf @@ -36,5 +36,6 @@ void main(void) { DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, - DEFAULT_OCCLUSION); + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING); } \ No newline at end of file diff --git a/tests/gpu-test/src/unlit.slf b/tests/gpu-test/src/unlit.slf index f88fcb510b..71954489af 100644 --- a/tests/gpu-test/src/unlit.slf +++ b/tests/gpu-test/src/unlit.slf @@ -24,5 +24,5 @@ void main(void) { normalize(_normal.xyz), 1.0, _color.rgb, - DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION); + DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION, DEFAULT_SCATTERING); } From 7fa7abdb1df8c650e538d33d425ac60757174bdb Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 20 Jun 2016 22:43:28 -0700 Subject: [PATCH 0662/1237] add operating hours to domain-server settings/UI --- .../resources/describe-settings.json | 54 +++++++++++++++++- .../resources/web/settings/js/settings.js | 14 ++++- domain-server/src/DomainMetadata.cpp | 56 ++++++++++++++----- domain-server/src/DomainMetadata.h | 8 ++- .../src/DomainServerSettingsManager.cpp | 27 ++++++++- 5 files changed, 139 insertions(+), 20 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 3633685365..59fa9c4f3d 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.4, + "version": 1.5, "settings": [ { "name": "metaverse", @@ -143,9 +143,57 @@ ] }, { - "name": "utc_offset", + "label": "Operating Hours", + "help": "\"Open\" domains can be searched using their operating hours. Hours are entered in the local timezone, selected below.", + + "name": "weekday_hours", + "caption": "Weekday Hours (Monday-Friday)", + "type": "table", + "can_add_new_rows": false, + "columns": [ + { + "name": "open", + "label": "Opening Time", + "type": "time", + "default": "00:00", + "editable": true + }, + { + "name": "close", + "label": "Closing Time", + "type": "time", + "default": "23:59", + "editable": true + } + ] + }, + { + "name": "weekend_hours", + "label": "Weekend Hours (Saturday/Sunday)", + "type": "table", + "can_add_new_rows": false, + "columns": [ + { + "name": "open", + "label": "Opening Time", + "type": "time", + "default": "00:00", + "editable": true + }, + { + "name": "close", + "label": "Closing Time", + "type": "time", + "default": "23:59", + "editable": true + } + ] + }, + { "label": "Time Zone", - "help": "This server's time zone, used for searching open servers", + "name": "utc_offset", + "caption": "Time Zone", + "help": "This server's time zone. Used to define your server's operating hours.", "type": "select", "options": [ { diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index aecc48b31f..709e0e05ce 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -987,7 +987,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" + rowIndexOrName + "" } - var isNonDeletableRow = false; + var isNonDeletableRow = !setting.can_add_new_rows; _.each(setting.columns, function(col) { @@ -1007,6 +1007,10 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" + ""; + } else if (isArray && col.type === "time" && col.editable) { + html += "" + + ""; } else { // Use a hidden input so that the values are posted. html += "" @@ -1196,15 +1200,21 @@ function addTableRow(add_glyphicon) { // Hide inputs var input = $(element).find("input") var isCheckbox = false; + var isTime = false; if (input.hasClass("table-checkbox")) { input = $(input).parent(); isCheckbox = true; + } else if (input.hasClass("table-time")) { + input = $(input).parent(); + isTime = true; } var val = input.val(); if (isCheckbox) { - val = $(input).find("input").is(':checked'); // don't hide the checkbox + val = $(input).find("input").is(':checked'); + } else if (isTime) { + // don't hide the time } else { input.attr("type", "hidden") } diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index bdf24c000a..c1656af5b7 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -31,30 +31,32 @@ const QString DomainMetadata::Users::HOSTNAMES = "user_hostnames"; const QString DomainMetadata::DESCRIPTORS = "descriptors"; const QString DomainMetadata::Descriptors::DESCRIPTION = "description"; const QString DomainMetadata::Descriptors::CAPACITY = "capacity"; // parsed from security -const QString DomainMetadata::Descriptors::HOURS = "hours"; const QString DomainMetadata::Descriptors::RESTRICTION = "restriction"; // parsed from ACL const QString DomainMetadata::Descriptors::MATURITY = "maturity"; const QString DomainMetadata::Descriptors::HOSTS = "hosts"; const QString DomainMetadata::Descriptors::TAGS = "tags"; +const QString DomainMetadata::Descriptors::HOURS = "hours"; +const QString DomainMetadata::Descriptors::Hours::WEEKDAY = "weekday"; +const QString DomainMetadata::Descriptors::Hours::WEEKEND = "weekend"; +const QString DomainMetadata::Descriptors::Hours::UTC_OFFSET = "utc_offset"; // descriptors metadata will appear as (JSON): // { "description": String, // capped description // "capacity": Number, -// "hours": { -// "utc_offset": Number, -// "weekday": [ Number, Number ], -// "weekend": [ Number, Number ] -// } // "restriction": String, // enum of either open, hifi, or acl // "maturity": String, // enum corresponding to ESRB ratings // "hosts": [ String ], // capped list of usernames // "tags": [ String ], // capped list of tags +// "hours": { +// "utc_offset": Number, +// "weekday": [ { "open": Time, "close": Time } ], +// "weekend": [ { "open": Time, "close": Time } ], +// } // } // metadata will appear as (JSON): // { users: , descriptors: } // // it is meant to be sent to and consumed by an external API -// NOTE: metadata may not appear as documented, as parts are generated by describe-settings.js DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { _metadata[USERS] = {}; @@ -86,20 +88,42 @@ QJsonObject DomainMetadata::get(const QString& group) { } void DomainMetadata::descriptorsChanged() { - const QString CAPACITY = "security.maximum_user_capacity"; + // get descriptors auto settings = static_cast(parent())->_settingsManager.getSettingsMap(); + auto descriptors = settings[DESCRIPTORS].toMap(); + + // parse capacity + const QString CAPACITY = "security.maximum_user_capacity"; const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; - - auto descriptors = settings[DESCRIPTORS].toMap(); descriptors[Descriptors::CAPACITY] = capacity; + + // parse operating hours + const QString WEEKDAY_HOURS = "weekday_hours"; + const QString WEEKEND_HOURS = "weekday_hours"; + const QString UTC_OFFSET = "utc_offset"; + auto weekdayHours = descriptors[WEEKDAY_HOURS]; + auto weekendHours = descriptors[WEEKEND_HOURS]; + auto utcOffset = descriptors[UTC_OFFSET]; + descriptors.remove(WEEKDAY_HOURS); + descriptors.remove(WEEKEND_HOURS); + descriptors.remove(UTC_OFFSET); + QVariantMap hours { + { Descriptors::Hours::UTC_OFFSET, utcOffset }, + { Descriptors::Hours::WEEKDAY, weekdayHours }, + { Descriptors::Hours::WEEKEND, weekendHours } + }; + descriptors[Descriptors::HOURS] = hours; + + // update metadata _metadata[DESCRIPTORS] = descriptors; // update overwritten fields + // this further overwrites metadata, so it must be done after commiting descriptors securityChanged(false); #if DEV_BUILD || PR_BUILD - qDebug() << "Domain metadata descriptors set:" << _metadata[DESCRIPTORS]; + qDebug() << "Domain metadata descriptors set:" << QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); #endif sendDescriptors(); @@ -189,7 +213,7 @@ void DomainMetadata::maybeUpdateUsers() { ++_tic; #if DEV_BUILD || PR_BUILD - qDebug() << "Domain metadata users updated:" << users; + qDebug() << "Domain metadata users set:" << QJsonObject::fromVariantMap(_metadata[USERS].toMap()); #endif } @@ -198,10 +222,16 @@ void DomainMetadata::sendDescriptors() { const QUuid& domainID = DependencyManager::get()->getSessionUUID(); if (!domainID.isNull()) { static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), + QString path { DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)) }; + DependencyManager::get()->sendRequest(path, AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, JSONCallbackParameters(), domainUpdateJSON.toUtf8()); + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata sent to" << path; + qDebug() << "Domain metadata update:" << domainUpdateJSON; +#endif } } diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index 0bfd6c611b..6573a9076c 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -35,11 +35,17 @@ public: public: static const QString DESCRIPTION; static const QString CAPACITY; - static const QString HOURS; static const QString RESTRICTION; static const QString MATURITY; static const QString HOSTS; static const QString TAGS; + static const QString HOURS; + class Hours { + public: + static const QString WEEKDAY; + static const QString WEEKEND; + static const QString UTC_OFFSET; + }; }; DomainMetadata(QObject* domainServer); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index b03d2c07ae..f5ff479cd3 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -21,6 +21,8 @@ #include #include +#include + #include #include #include @@ -69,7 +71,7 @@ DomainServerSettingsManager::DomainServerSettingsManager() : } static const QString MISSING_SETTINGS_DESC_MSG = - QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") + QString("Did not find settings description in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset); static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6; @@ -258,6 +260,29 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _standardAgentPermissions.clear(); _agentPermissions.clear(); } + + if (oldVersion < 1.5) { + // This was prior to operating hours, so add default hours + static const QString WEEKDAY_HOURS{ "descriptors.weekday_hours" }; + static const QString WEEKEND_HOURS{ "descriptors.weekend_hours" }; + static const QString UTC_OFFSET{ "descriptors.utc_offset" }; + + QVariant* weekdayHours = valueForKeyPath(_configMap.getUserConfig(), WEEKDAY_HOURS, true); + QVariant* weekendHours = valueForKeyPath(_configMap.getUserConfig(), WEEKEND_HOURS, true); + QVariant* utcOffset = valueForKeyPath(_configMap.getUserConfig(), UTC_OFFSET, true); + + + QVariantList allHours { QVariantMap{ { "open", QVariant("00:00") }, { "close", QVariant("23:59") } } }; + *weekdayHours = allHours; + *weekendHours = allHours; + *utcOffset = QVariant(QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime()) / (float)3600); + + // write the new settings to file + persistToFile(); + + // reload the master and user config so the merged config is correct + _configMap.loadMasterAndUserConfig(_argumentList); + } } unpackPermissions(); From 807596596f2923241f57f1349cafc7d0261aca5a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 21 Jun 2016 00:18:38 -0700 Subject: [PATCH 0663/1237] fix badging, parsing of operating hours --- .../resources/web/settings/js/settings.js | 12 +++- domain-server/src/DomainMetadata.cpp | 55 +++++++++++++++---- domain-server/src/DomainMetadata.h | 2 + .../src/DomainServerSettingsManager.cpp | 6 +- 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 709e0e05ce..c2cb2ecb80 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -243,6 +243,16 @@ $(document).ready(function(){ } }); + $('#' + Settings.FORM_ID).on('change', 'input.table-time', function() { + // Bootstrap switches in table: set the changed data attribute for all rows in table. + var row = $(this).closest('tr'); + if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. + row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); + updateDataChangedForSiblingRows(row, true); + badgeSidebarForDifferences($(this)); + } + }); + $('.advanced-toggle').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) @@ -1009,7 +1019,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { + "name='" + colName + "'" + (colValue ? " checked" : "") + " />"; } else if (isArray && col.type === "time" && col.editable) { html += "" - + ""; } else { // Use a hidden input so that the values are posted. diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index c1656af5b7..cfb48e6c49 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -39,6 +39,8 @@ const QString DomainMetadata::Descriptors::HOURS = "hours"; const QString DomainMetadata::Descriptors::Hours::WEEKDAY = "weekday"; const QString DomainMetadata::Descriptors::Hours::WEEKEND = "weekend"; const QString DomainMetadata::Descriptors::Hours::UTC_OFFSET = "utc_offset"; +const QString DomainMetadata::Descriptors::Hours::OPEN = "open"; +const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; // descriptors metadata will appear as (JSON): // { "description": String, // capped description // "capacity": Number, @@ -59,8 +61,13 @@ const QString DomainMetadata::Descriptors::Hours::UTC_OFFSET = "utc_offset"; // it is meant to be sent to and consumed by an external API DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { - _metadata[USERS] = {}; - _metadata[DESCRIPTORS] = {}; + _metadata[USERS] = QVariantMap {}; + _metadata[DESCRIPTORS] = QVariantMap { + { Descriptors::HOURS, QVariantMap { + { Descriptors::Hours::WEEKDAY, QVariantList() }, + { Descriptors::Hours::WEEKEND, QVariantList() } + } } + }; assert(dynamic_cast(domainServer)); DomainServer* server = static_cast(domainServer); @@ -87,10 +94,38 @@ QJsonObject DomainMetadata::get(const QString& group) { return QJsonObject::fromVariantMap(_metadata[group].toMap()); } +QVariant parseHours(QVariant base, QVariant delta) { + using Hours = DomainMetadata::Descriptors::Hours; + + auto& baseList = base.toList(); + auto& deltaList = delta.toList(); + if (baseList.isEmpty() || !baseList.length()) { + return delta; + } else if (deltaList.isEmpty() || !deltaList.length()) { + return base; + } + + auto& baseMap = baseList[0].toMap(); + auto& deltaMap = deltaList[0].toMap(); + if (baseMap.isEmpty()) { + return delta; + } else if (deltaMap.isEmpty()) { + return base; + } + + // merge delta into base + // hours should be of the form [ { open: Time, close: Time } ], so one level is sufficient + foreach(auto key, baseMap.keys()) { + deltaMap[key] = baseMap[key]; + } + + return base; +} + void DomainMetadata::descriptorsChanged() { // get descriptors auto settings = static_cast(parent())->_settingsManager.getSettingsMap(); - auto descriptors = settings[DESCRIPTORS].toMap(); + auto& descriptors = settings[DESCRIPTORS].toMap(); // parse capacity const QString CAPACITY = "security.maximum_user_capacity"; @@ -100,20 +135,21 @@ void DomainMetadata::descriptorsChanged() { // parse operating hours const QString WEEKDAY_HOURS = "weekday_hours"; - const QString WEEKEND_HOURS = "weekday_hours"; + const QString WEEKEND_HOURS = "weekend_hours"; const QString UTC_OFFSET = "utc_offset"; - auto weekdayHours = descriptors[WEEKDAY_HOURS]; - auto weekendHours = descriptors[WEEKEND_HOURS]; + auto& hours = _metadata[DESCRIPTORS].toMap()[Descriptors::HOURS].toMap(); + assert(!hours.isEmpty()); + auto weekdayHours = parseHours(hours[Descriptors::Hours::WEEKDAY], descriptors[WEEKDAY_HOURS]); + auto weekendHours = parseHours(hours[Descriptors::Hours::WEEKEND], descriptors[WEEKEND_HOURS]); auto utcOffset = descriptors[UTC_OFFSET]; descriptors.remove(WEEKDAY_HOURS); descriptors.remove(WEEKEND_HOURS); descriptors.remove(UTC_OFFSET); - QVariantMap hours { + descriptors[Descriptors::HOURS] = QVariantMap { { Descriptors::Hours::UTC_OFFSET, utcOffset }, { Descriptors::Hours::WEEKDAY, weekdayHours }, { Descriptors::Hours::WEEKEND, weekendHours } }; - descriptors[Descriptors::HOURS] = hours; // update metadata _metadata[DESCRIPTORS] = descriptors; @@ -150,9 +186,8 @@ void DomainMetadata::securityChanged(bool send) { restriction = RESTRICTION_ACL; } - auto descriptors = _metadata[DESCRIPTORS].toMap(); + auto& descriptors = _metadata[DESCRIPTORS].toMap(); descriptors[Descriptors::RESTRICTION] = restriction; - _metadata[DESCRIPTORS] = descriptors; #if DEV_BUILD || PR_BUILD qDebug() << "Domain metadata restriction set:" << restriction; diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index 6573a9076c..41f3a60832 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -45,6 +45,8 @@ public: static const QString WEEKDAY; static const QString WEEKEND; static const QString UTC_OFFSET; + static const QString OPEN; + static const QString CLOSE; }; }; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index f5ff479cd3..543e61f485 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -271,10 +271,8 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList QVariant* weekendHours = valueForKeyPath(_configMap.getUserConfig(), WEEKEND_HOURS, true); QVariant* utcOffset = valueForKeyPath(_configMap.getUserConfig(), UTC_OFFSET, true); - - QVariantList allHours { QVariantMap{ { "open", QVariant("00:00") }, { "close", QVariant("23:59") } } }; - *weekdayHours = allHours; - *weekendHours = allHours; + *weekdayHours = QVariantList { QVariantMap{ { "open", QVariant("00:00") }, { "close", QVariant("23:59") } } }; + *weekendHours = QVariantList { QVariantMap{ { "open", QVariant("00:00") }, { "close", QVariant("23:59") } } }; *utcOffset = QVariant(QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime()) / (float)3600); // write the new settings to file From 2ac6dc8798995490cf4a9f02cf83aac85c773ab8 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 21 Jun 2016 09:48:01 -0700 Subject: [PATCH 0664/1237] do not tilt virtual keyboard --- plugins/openvr/src/OpenVrHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index dc38aa0a0a..4f02c0384d 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -138,7 +138,7 @@ void showOpenVrKeyboard(bool show = true) { if (vr::VROverlayError_None == showKeyboardResult) { _keyboardShown = true; // Try to position the keyboard slightly below where the user is looking. - mat4 headPose = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + mat4 headPose = cancelOutRollAndPitch(toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking)); mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); auto keyboardTransformVr = toOpenVr(keyboardTransform); From a77dea904876725d583638e8d382ff14917c3aef Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 08:31:40 -0700 Subject: [PATCH 0665/1237] Fix osx warning --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b92fc2d3e6..17a6110ea2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -775,7 +775,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : { "os_version", QSysInfo::productVersion() }, { "gpu_name", gpuIdent->getName() }, { "gpu_driver", gpuIdent->getDriver() }, - { "gpu_memory", QJsonValue(static_cast(gpuIdent->getMemory())) }, + { "gpu_memory", QJsonValue(static_cast(gpuIdent->getMemory())) }, { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, { "gl_version", glContextData["version"] }, { "gl_vender", glContextData["vendor"] }, From 31706a39093c88b87b5a58884d18a1d306b7ddfd Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 21 Jun 2016 10:38:44 -0700 Subject: [PATCH 0666/1237] force activate kinmatic objects when necessary --- libraries/physics/src/EntityMotionState.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 8f22c576f0..dc57a82fd3 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -135,7 +135,14 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { _nextOwnershipBid = 0; } if ((flags & Simulation::DIRTY_PHYSICS_ACTIVATION) && !_body->isActive()) { - _body->activate(); + if (_body->isKinematicObject()) { + // only force activate kinematic bodies (dynamic shouldn't need force and + // active static bodies are special (see PhysicsEngine::_activeStaticBodies)) + _body->activate(true); + _lastKinematicStep = ObjectMotionState::getWorldSimulationStep(); + } else { + _body->activate(); + } } } From 6d753e317b60dfb3ee9b6c5b3be826b11e999bbe Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 10:51:31 -0700 Subject: [PATCH 0667/1237] Fix more osx warning --- interface/src/Application.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 17a6110ea2..0b6bc2e01a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -775,7 +775,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : { "os_version", QSysInfo::productVersion() }, { "gpu_name", gpuIdent->getName() }, { "gpu_driver", gpuIdent->getDriver() }, - { "gpu_memory", QJsonValue(static_cast(gpuIdent->getMemory())) }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, { "gl_version", glContextData["version"] }, { "gl_vender", glContextData["vendor"] }, @@ -1100,9 +1100,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QJsonObject properties = {}; MemoryInfo memInfo; if (getMemoryInfo(memInfo)) { - properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); - properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); - properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); + properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); + properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); + properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); } auto displayPlugin = qApp->getActiveDisplayPlugin(); From 52170eb12e3a1bd4b12bc66b49a15043407a0a48 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 21 Jun 2016 11:16:08 -0700 Subject: [PATCH 0668/1237] use refs for domain metadata parsing --- domain-server/src/DomainMetadata.cpp | 102 +++++++++++++-------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index cfb48e6c49..f18aa8c71b 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -61,11 +61,12 @@ const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; // it is meant to be sent to and consumed by an external API DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { + // set up the structure necessary for casting during parsing (see parseHours, esp.) _metadata[USERS] = QVariantMap {}; _metadata[DESCRIPTORS] = QVariantMap { { Descriptors::HOURS, QVariantMap { - { Descriptors::Hours::WEEKDAY, QVariantList() }, - { Descriptors::Hours::WEEKEND, QVariantList() } + { Descriptors::Hours::WEEKDAY, QVariantList { QVariantMap{} } }, + { Descriptors::Hours::WEEKEND, QVariantList { QVariantMap{} } } } } }; @@ -81,6 +82,7 @@ DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { this, static_cast(&DomainMetadata::securityChanged)); // initialize the descriptors + securityChanged(false); descriptorsChanged(); } @@ -94,69 +96,65 @@ QJsonObject DomainMetadata::get(const QString& group) { return QJsonObject::fromVariantMap(_metadata[group].toMap()); } -QVariant parseHours(QVariant base, QVariant delta) { +void parseHours(QVariant delta, QVariant& target) { using Hours = DomainMetadata::Descriptors::Hours; - auto& baseList = base.toList(); - auto& deltaList = delta.toList(); - if (baseList.isEmpty() || !baseList.length()) { - return delta; - } else if (deltaList.isEmpty() || !deltaList.length()) { - return base; - } + // hours should be of the form [ { open: Time, close: Time } ] + assert(target.canConvert()); + auto& targetList = *static_cast(target.data()); - auto& baseMap = baseList[0].toMap(); - auto& deltaMap = deltaList[0].toMap(); - if (baseMap.isEmpty()) { - return delta; - } else if (deltaMap.isEmpty()) { - return base; + // if/when multiple ranges are allowed, this list will need to be iterated + assert(targetList[0].canConvert()); + auto& targetMap = *static_cast(targetList[0].data()); + + auto deltaMap = delta.toList()[0].toMap(); + if (deltaMap.isEmpty()) { + return; } // merge delta into base - // hours should be of the form [ { open: Time, close: Time } ], so one level is sufficient - foreach(auto key, baseMap.keys()) { - deltaMap[key] = baseMap[key]; + auto open = deltaMap.find(Hours::OPEN); + if (open != deltaMap.end()) { + targetMap[Hours::OPEN] = open.value(); } - - return base; + assert(targetMap[Hours::OPEN].canConvert()); + auto close = deltaMap.find(Hours::CLOSE); + if (close != deltaMap.end()) { + targetMap[Hours::CLOSE] = close.value(); + } + assert(targetMap[Hours::CLOSE].canConvert()); } void DomainMetadata::descriptorsChanged() { // get descriptors + assert(_metadata[DESCRIPTORS].canConvert()); + auto& state = *static_cast(_metadata[DESCRIPTORS].data()); auto settings = static_cast(parent())->_settingsManager.getSettingsMap(); - auto& descriptors = settings[DESCRIPTORS].toMap(); + auto descriptors = settings[DESCRIPTORS].toMap(); + + // copy simple descriptors (description/maturity) + state[Descriptors::DESCRIPTION] = descriptors[Descriptors::DESCRIPTION]; + state[Descriptors::MATURITY] = descriptors[Descriptors::MATURITY]; + + // copy array descriptors (hosts/tags) + state[Descriptors::HOSTS] = descriptors[Descriptors::HOSTS].toList(); + state[Descriptors::TAGS] = descriptors[Descriptors::TAGS].toList(); // parse capacity const QString CAPACITY = "security.maximum_user_capacity"; const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; - descriptors[Descriptors::CAPACITY] = capacity; + state[Descriptors::CAPACITY] = capacity; // parse operating hours const QString WEEKDAY_HOURS = "weekday_hours"; const QString WEEKEND_HOURS = "weekend_hours"; const QString UTC_OFFSET = "utc_offset"; - auto& hours = _metadata[DESCRIPTORS].toMap()[Descriptors::HOURS].toMap(); - assert(!hours.isEmpty()); - auto weekdayHours = parseHours(hours[Descriptors::Hours::WEEKDAY], descriptors[WEEKDAY_HOURS]); - auto weekendHours = parseHours(hours[Descriptors::Hours::WEEKEND], descriptors[WEEKEND_HOURS]); - auto utcOffset = descriptors[UTC_OFFSET]; - descriptors.remove(WEEKDAY_HOURS); - descriptors.remove(WEEKEND_HOURS); - descriptors.remove(UTC_OFFSET); - descriptors[Descriptors::HOURS] = QVariantMap { - { Descriptors::Hours::UTC_OFFSET, utcOffset }, - { Descriptors::Hours::WEEKDAY, weekdayHours }, - { Descriptors::Hours::WEEKEND, weekendHours } - }; - - // update metadata - _metadata[DESCRIPTORS] = descriptors; - - // update overwritten fields - // this further overwrites metadata, so it must be done after commiting descriptors - securityChanged(false); + assert(state[Descriptors::HOURS].canConvert()); + auto& hours = *static_cast(state[Descriptors::HOURS].data()); + parseHours(descriptors.take(WEEKDAY_HOURS), hours[Descriptors::Hours::WEEKDAY]); + parseHours(descriptors.take(WEEKEND_HOURS), hours[Descriptors::Hours::WEEKEND]); + hours[Descriptors::Hours::UTC_OFFSET] = descriptors.take(UTC_OFFSET); #if DEV_BUILD || PR_BUILD qDebug() << "Domain metadata descriptors set:" << QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); @@ -166,6 +164,10 @@ void DomainMetadata::descriptorsChanged() { } void DomainMetadata::securityChanged(bool send) { + // get descriptors + assert(_metadata[DESCRIPTORS].canConvert()); + auto& state = *static_cast(_metadata[DESCRIPTORS].data()); + const QString RESTRICTION_OPEN = "open"; const QString RESTRICTION_ANON = "anon"; const QString RESTRICTION_HIFI = "hifi"; @@ -186,8 +188,7 @@ void DomainMetadata::securityChanged(bool send) { restriction = RESTRICTION_ACL; } - auto& descriptors = _metadata[DESCRIPTORS].toMap(); - descriptors[Descriptors::RESTRICTION] = restriction; + state[Descriptors::RESTRICTION] = restriction; #if DEV_BUILD || PR_BUILD qDebug() << "Domain metadata restriction set:" << restriction; @@ -240,12 +241,11 @@ void DomainMetadata::maybeUpdateUsers() { } }); - QVariantMap users = { - { Users::NUM_TOTAL, numConnected }, - { Users::NUM_ANON, numConnectedAnonymously }, - { Users::HOSTNAMES, userHostnames }}; - _metadata[USERS] = users; - ++_tic; + assert(_metadata[USERS].canConvert()); + auto& users = *static_cast(_metadata[USERS].data()); + users[Users::NUM_TOTAL] = numConnected; + users[Users::NUM_ANON] = numConnectedAnonymously; + users[Users::HOSTNAMES] = userHostnames; #if DEV_BUILD || PR_BUILD qDebug() << "Domain metadata users set:" << QJsonObject::fromVariantMap(_metadata[USERS].toMap()); From c784fa42e99e6b2862ddf73f80a7ae9a040dc9c6 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 21 Jun 2016 11:34:23 -0700 Subject: [PATCH 0669/1237] log failure to fetch metadata --- scripts/tutorials/getDomainMetadata.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/tutorials/getDomainMetadata.js b/scripts/tutorials/getDomainMetadata.js index 0a6f742823..8c347a09ae 100644 --- a/scripts/tutorials/getDomainMetadata.js +++ b/scripts/tutorials/getDomainMetadata.js @@ -16,14 +16,21 @@ location.hostChanged.connect(function(host) { // Fetch the domain ID from the metaverse var placeData = request(SERVER + '/places/' + host); - if (!placeData) { return; } + if (!placeData) { + print('Cannot find place name - abandoning metadata request for', host); + return; + + } var domainID = placeData.data.place.domain.id; print('Domain ID:', domainID); // Fetch the domain metadata from the metaverse var domainData = request(SERVER + '/domains/' + domainID); print(SERVER + '/domains/' + domainID); - if (!domainData) { return; } + if (!domainData) { + print('Cannot find domain data - abandoning metadata request for', domainID); + return; + } var metadata = domainData.domain; print('Domain metadata:', JSON.stringify(metadata)); From 972c292857d76ccdd92c38cdedbb500b369772c4 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 21 Jun 2016 11:59:11 -0700 Subject: [PATCH 0670/1237] Fix for equip-hotspot being visible for whiteboard markers --- .../system/controllers/handControllerGrab.js | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f1ecc15392..39d85f1224 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -879,28 +879,23 @@ function MyController(hand) { var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); var i, l = entities.length; for (i = 0; i < l; i++) { - - // is this entity equipable? - var grabData = getEntityCustomData(GRABBABLE_DATA_KEY, entities[i], undefined); var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); - if (grabData) { - // does this entity have an attach point? - var wearableData = getEntityCustomData("wearable", entities[i], undefined); - if (wearableData && wearableData.joints) { - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (wearableData.joints[handJointName]) { - // draw the hotspot - this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { - position: grabProps.position, - size: 0.2, - color: { red: 90, green: 255, blue: 90 }, - alpha: 0.7, - solid: true, - visible: true, - ignoreRayIntersection: false, - drawInFront: false - })); - } + // does this entity have an attach point? + var wearableData = getEntityCustomData("wearable", entities[i], undefined); + if (wearableData && wearableData.joints) { + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (wearableData.joints[handJointName]) { + // draw the hotspot + this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { + position: grabProps.position, + size: 0.2, + color: { red: 90, green: 255, blue: 90 }, + alpha: 0.7, + solid: true, + visible: true, + ignoreRayIntersection: false, + drawInFront: false + })); } } } From c3223178783c8418bc8e468578d4393c03463abf Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 21 Jun 2016 12:07:05 -0700 Subject: [PATCH 0671/1237] rework plugins to not depend on PluginContainer unless they want to --- interface/CMakeLists.txt | 2 +- interface/src/Application.cpp | 63 +++++++++++++-- interface/src/Application.h | 26 +++++-- interface/src/PluginContainerProxy.cpp | 78 ------------------- interface/src/PluginContainerProxy.h | 33 -------- libraries/display-plugins/CMakeLists.txt | 2 +- .../AbstractHMDScriptingInterface.cpp | 2 +- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 2 - .../src/display-plugins/NullDisplayPlugin.cpp | 1 - .../src/display-plugins/NullDisplayPlugin.h | 2 + .../display-plugins/OpenGLDisplayPlugin.cpp | 25 ++++-- .../src/display-plugins/OpenGLDisplayPlugin.h | 7 ++ .../display-plugins/hmd/HmdDisplayPlugin.cpp | 1 - .../display-plugins/hmd/HmdDisplayPlugin.h | 2 + .../stereo/SideBySideStereoDisplayPlugin.cpp | 1 - .../stereo/StereoDisplayPlugin.cpp | 1 - libraries/input-plugins/CMakeLists.txt | 2 +- .../plugins/src/plugins/DisplayPlugin.cpp | 22 ------ libraries/plugins/src/plugins/DisplayPlugin.h | 2 - libraries/plugins/src/plugins/Plugin.cpp | 4 - libraries/plugins/src/plugins/Plugin.h | 11 ++- .../plugins/src/plugins/PluginManager.cpp | 11 +-- libraries/plugins/src/plugins/PluginManager.h | 3 + libraries/ui-plugins/CMakeLists.txt | 3 + .../src/ui-plugins}/PluginContainer.cpp | 0 .../src/ui-plugins}/PluginContainer.h | 17 +++- plugins/hifiSixense/CMakeLists.txt | 2 +- plugins/hifiSixense/src/SixenseManager.cpp | 1 - plugins/hifiSixense/src/SixenseManager.h | 2 + plugins/oculus/CMakeLists.txt | 2 +- .../oculus/src/OculusControllerManager.cpp | 2 +- plugins/oculusLegacy/CMakeLists.txt | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 1 - plugins/openvr/CMakeLists.txt | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 1 - plugins/openvr/src/ViveControllerManager.cpp | 1 - plugins/openvr/src/ViveControllerManager.h | 4 +- tests/controllers/CMakeLists.txt | 2 +- tests/controllers/src/main.cpp | 6 +- 39 files changed, 159 insertions(+), 192 deletions(-) delete mode 100644 interface/src/PluginContainerProxy.cpp delete mode 100644 interface/src/PluginContainerProxy.h create mode 100644 libraries/ui-plugins/CMakeLists.txt rename libraries/{plugins/src/plugins => ui-plugins/src/ui-plugins}/PluginContainer.cpp (100%) rename libraries/{plugins/src/plugins => ui-plugins/src/ui-plugins}/PluginContainer.h (77%) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index ae84705da3..cf5a2b60ad 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -139,7 +139,7 @@ link_hifi_libraries(shared octree gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater - controllers plugins display-plugins input-plugins steamworks-wrapper) + controllers plugins ui-plugins display-plugins input-plugins steamworks-wrapper) # include the binary directory of render-utils for shader includes target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils") diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9bcd85fd02..babcd877c4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -83,7 +83,6 @@ #include #include #include -#include #include #include #include @@ -119,7 +118,6 @@ #include "InterfaceLogging.h" #include "LODManager.h" #include "ModelPackager.h" -#include "PluginContainerProxy.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/AudioDeviceScriptingInterface.h" @@ -464,7 +462,6 @@ bool setupEssentials(int& argc, char** argv) { // continuing to overburden Application.cpp Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; -PluginContainer* _pluginContainer; // FIXME hack access to the internal share context for the Chromium helper @@ -504,6 +501,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _maxOctreePPS(maxOctreePacketsPerSecond.get()), _lastFaceTrackerUpdate(0) { + + + PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care + PluginManager::getInstance()->setContainer(pluginContainer); + // FIXME this may be excessively conservative. On the other hand // maybe I'm used to having an 8-core machine // Perhaps find the ideal thread count and subtract 2 or 3 @@ -521,7 +523,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _entityClipboard->createRootElement(); - _pluginContainer = new PluginContainerProxy(); #ifdef Q_OS_WIN installNativeEventFilter(&MyNativeEventFilter::getInstance()); #endif @@ -2035,9 +2036,9 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_Return: if (isOption) { if (_window->isFullScreen()) { - _pluginContainer->unsetFullscreen(); + unsetFullscreen(); } else { - _pluginContainer->setFullscreen(nullptr); + setFullscreen(nullptr); } } else { Menu::getInstance()->triggerOption(MenuOption::AddressBar); @@ -2951,9 +2952,10 @@ void Application::loadSettings() { //DependencyManager::get()->setAutomaticLODAdjust(false); Menu::getInstance()->loadSettings(); - // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. auto pluginManager = PluginManager::getInstance(); + + auto plugins = pluginManager->getPreferredDisplayPlugins(); for (auto plugin : plugins) { auto menu = Menu::getInstance(); @@ -5190,6 +5192,7 @@ void Application::updateDisplayMode() { // FIXME probably excessive and useless context switching _offscreenContext->makeCurrent(); + qDebug() << "Application::updateDisplayMode()... line:" << __LINE__ << "about to call newDisplayPlugin->activate()"; bool active = newDisplayPlugin->activate(); if (!active) { @@ -5308,3 +5311,49 @@ void Application::showDesktop() { CompositorHelper& Application::getApplicationCompositor() const { return *DependencyManager::get(); } + + +// virtual functions required for PluginContainer +ui::Menu* Application::getPrimaryMenu() { + auto appMenu = _window->menuBar(); + auto uiMenu = dynamic_cast(appMenu); + return uiMenu; +} + +void Application::showDisplayPluginsTools(bool show) { + DependencyManager::get()->hmdTools(show); +} + +GLWidget* Application::getPrimaryWidget() { + return _glWidget; +} + +MainWindow* Application::getPrimaryWindow() { + return getWindow(); +} + +QOpenGLContext* Application::getPrimaryContext() { + return _glWidget->context()->contextHandle(); +} + +bool Application::makeRenderingContextCurrent() { + return _offscreenContext->makeCurrent(); +} + +void Application::releaseSceneTexture(const gpu::TexturePointer& texture) { + Q_ASSERT(QThread::currentThread() == thread()); + auto& framebufferMap = _lockedFramebufferMap; + Q_ASSERT(framebufferMap.contains(texture)); + auto framebufferPointer = framebufferMap[texture]; + framebufferMap.remove(texture); + auto framebufferCache = DependencyManager::get(); + framebufferCache->releaseFramebuffer(framebufferPointer); +} + +void Application::releaseOverlayTexture(const gpu::TexturePointer& texture) { + _applicationOverlay.releaseOverlay(texture); +} + +bool Application::isForeground() const { + return _isForeground && !_window->isMinimized(); +} diff --git a/interface/src/Application.h b/interface/src/Application.h index 6b6148be32..114ce27144 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -86,14 +87,32 @@ class Application; #endif #define qApp (static_cast(QCoreApplication::instance())) -class Application : public QApplication, public AbstractViewStateInterface, public AbstractScriptingServicesInterface, public AbstractUriHandler { +class Application : public QApplication, + public AbstractViewStateInterface, + public AbstractScriptingServicesInterface, + public AbstractUriHandler, + public PluginContainer { Q_OBJECT // TODO? Get rid of those friend class OctreePacketProcessor; - friend class PluginContainerProxy; public: + + // virtual functions required for PluginContainer + virtual ui::Menu* getPrimaryMenu() override; + virtual void requestReset() override { resetSensors(true); } + virtual void showDisplayPluginsTools(bool show) override; + virtual GLWidget* getPrimaryWidget() override; + virtual MainWindow* getPrimaryWindow() override; + virtual QOpenGLContext* getPrimaryContext() override; + virtual bool makeRenderingContextCurrent() override; + virtual void releaseSceneTexture(const gpu::TexturePointer& texture) override; + virtual void releaseOverlayTexture(const gpu::TexturePointer& texture) override; + virtual bool isForeground() const override; + + virtual DisplayPluginPointer getActiveDisplayPlugin() const override; + enum Event { Present = DisplayPlugin::Present, Paint = Present + 1, @@ -163,7 +182,6 @@ public: Overlays& getOverlays() { return _overlays; } - bool isForeground() const { return _isForeground; } size_t getFrameCount() const { return _frameCount; } float getFps() const { return _frameCounter.rate(); } @@ -185,8 +203,6 @@ public: void setActiveDisplayPlugin(const QString& pluginName); - DisplayPluginPointer getActiveDisplayPlugin() const; - FileLogger* getLogger() const { return _logger; } glm::vec2 getViewportDimensions() const; diff --git a/interface/src/PluginContainerProxy.cpp b/interface/src/PluginContainerProxy.cpp deleted file mode 100644 index b651a1520d..0000000000 --- a/interface/src/PluginContainerProxy.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "PluginContainerProxy.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "Application.h" -#include "MainWindow.h" -#include "GLCanvas.h" -#include "ui/DialogsManager.h" - -#include -#include - -PluginContainerProxy::PluginContainerProxy() { -} - -PluginContainerProxy::~PluginContainerProxy() { -} - -ui::Menu* PluginContainerProxy::getPrimaryMenu() { - auto appMenu = qApp->_window->menuBar(); - auto uiMenu = dynamic_cast(appMenu); - return uiMenu; -} - -bool PluginContainerProxy::isForeground() { - return qApp->isForeground() && !qApp->getWindow()->isMinimized(); -} - -void PluginContainerProxy::requestReset() { - // We could signal qApp to sequence this, but it turns out that requestReset is only used from within the main thread anyway. - qApp->resetSensors(true); -} - -void PluginContainerProxy::showDisplayPluginsTools(bool show) { - DependencyManager::get()->hmdTools(show); -} - -GLWidget* PluginContainerProxy::getPrimaryWidget() { - return qApp->_glWidget; -} - -MainWindow* PluginContainerProxy::getPrimaryWindow() { - return qApp->getWindow(); -} - -QOpenGLContext* PluginContainerProxy::getPrimaryContext() { - return qApp->_glWidget->context()->contextHandle(); -} - -const DisplayPluginPointer PluginContainerProxy::getActiveDisplayPlugin() const { - return qApp->getActiveDisplayPlugin(); -} - -bool PluginContainerProxy::makeRenderingContextCurrent() { - return qApp->_offscreenContext->makeCurrent(); -} - -void PluginContainerProxy::releaseSceneTexture(const gpu::TexturePointer& texture) { - Q_ASSERT(QThread::currentThread() == qApp->thread()); - auto& framebufferMap = qApp->_lockedFramebufferMap; - Q_ASSERT(framebufferMap.contains(texture)); - auto framebufferPointer = framebufferMap[texture]; - framebufferMap.remove(texture); - auto framebufferCache = DependencyManager::get(); - framebufferCache->releaseFramebuffer(framebufferPointer); -} - -void PluginContainerProxy::releaseOverlayTexture(const gpu::TexturePointer& texture) { - qApp->_applicationOverlay.releaseOverlay(texture); -} - diff --git a/interface/src/PluginContainerProxy.h b/interface/src/PluginContainerProxy.h deleted file mode 100644 index a04a1b2977..0000000000 --- a/interface/src/PluginContainerProxy.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#ifndef hifi_PluginContainerProxy_h -#define hifi_PluginContainerProxy_h - -#include -#include - -#include -#include - -class QActionGroup; - -class PluginContainerProxy : public QObject, PluginContainer { - Q_OBJECT - PluginContainerProxy(); - virtual ~PluginContainerProxy(); - virtual void showDisplayPluginsTools(bool show = true) override; - virtual void requestReset() override; - virtual bool makeRenderingContextCurrent() override; - virtual void releaseSceneTexture(const gpu::TexturePointer& texture) override; - virtual void releaseOverlayTexture(const gpu::TexturePointer& texture) override; - virtual GLWidget* getPrimaryWidget() override; - virtual MainWindow* getPrimaryWindow() override; - virtual ui::Menu* getPrimaryMenu() override; - virtual QOpenGLContext* getPrimaryContext() override; - virtual bool isForeground() override; - virtual const DisplayPluginPointer getActiveDisplayPlugin() const override; - - friend class Application; - -}; - -#endif diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index f2d58d825e..fe08647074 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME display-plugins) setup_hifi_library(OpenGL) -link_hifi_libraries(shared plugins gl gpu-gl ui) +link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui) target_opengl() diff --git a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp index 4b8d957e5f..d068bef3b0 100644 --- a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp +++ b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp @@ -11,7 +11,7 @@ #include #include "DisplayPlugin.h" -#include +#include static Setting::Handle IPD_SCALE_HANDLE("hmd.ipdScale", 1.0f); diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index 48dda1f73d..3bdb96bde3 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -13,8 +13,6 @@ #include #include -#include - const QString Basic2DWindowOpenGLDisplayPlugin::NAME("Desktop"); static const QString FULLSCREEN = "Fullscreen"; diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 3f642072a0..8ebbf4d74b 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -10,7 +10,6 @@ #include "NullDisplayPlugin.h" #include -#include const QString NullDisplayPlugin::NAME("NullDisplayPlugin"); diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index dfa4232a86..c8770393c0 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -7,9 +7,11 @@ // #pragma once +#include #include "DisplayPlugin.h" class NullDisplayPlugin : public DisplayPlugin { + ACCESS_PLUGIN_CONTAINER_MIXIN public: virtual ~NullDisplayPlugin() final {} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index d34b698410..2128488718 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -22,12 +22,12 @@ #include #include #include -#include #include #include #include #include #include "CompositorHelper.h" +#include #if THREADED_PRESENT @@ -202,6 +202,7 @@ private: #endif + OpenGLDisplayPlugin::OpenGLDisplayPlugin() { _sceneTextureEscrow.setRecycler([this](const gpu::TexturePointer& texture){ cleanupForSceneTexture(texture); @@ -233,10 +234,11 @@ bool OpenGLDisplayPlugin::activate() { cursorData.hotSpot = vec2(0.5f); } } - + if (!_container) { + return false; + } _vsyncSupported = _container->getPrimaryWidget()->isVsyncSupported(); - #if THREADED_PRESENT // Start the present thread if necessary QSharedPointer presentThread; @@ -272,7 +274,11 @@ bool OpenGLDisplayPlugin::activate() { _container->makeRenderingContextCurrent(); #endif - return DisplayPlugin::activate(); + if (isHmd() && (getHmdScreen() >= 0)) { + _container->showDisplayPluginsTools(); + } + + return Parent::activate(); } void OpenGLDisplayPlugin::deactivate() { @@ -288,7 +294,16 @@ void OpenGLDisplayPlugin::deactivate() { _container->makeRenderingContextCurrent(); #endif internalDeactivate(); - DisplayPlugin::deactivate(); + + _container->showDisplayPluginsTools(false); + if (!_container->currentDisplayActions().isEmpty()) { + auto menu = _container->getPrimaryMenu(); + foreach(auto itemInfo, _container->currentDisplayActions()) { + menu->removeMenuItem(itemInfo.first, itemInfo.second); + } + _container->currentDisplayActions().clear(); + } + Parent::deactivate(); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index c87ff1bc93..10362b8181 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -21,9 +21,14 @@ #include #include +#include + #define THREADED_PRESENT 1 class OpenGLDisplayPlugin : public DisplayPlugin { + + ACCESS_PLUGIN_CONTAINER_MIXIN + protected: using Mutex = std::mutex; using Lock = std::unique_lock; @@ -135,7 +140,9 @@ protected: BasicFramebufferWrapperPtr _compositeFramebuffer; bool _lockCurrentTexture { false }; + private: + using Parent = DisplayPlugin; ProgramPtr _activeProgram; }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 4e594d89ed..b7739a7fb6 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include #include diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index e6ceb7e376..b87f68231c 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -13,6 +13,8 @@ #include "../OpenGLDisplayPlugin.h" +#include + class HmdDisplayPlugin : public OpenGLDisplayPlugin { using Parent = OpenGLDisplayPlugin; public: diff --git a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp index 5f55841be1..be095ecd39 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp @@ -9,7 +9,6 @@ #include "SideBySideStereoDisplayPlugin.h" #include #include -#include #include #include "../CompositorHelper.h" diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index 6c6716c8fa..afb99e3174 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include #include "../CompositorHelper.h" diff --git a/libraries/input-plugins/CMakeLists.txt b/libraries/input-plugins/CMakeLists.txt index b81554511d..b0ea13843b 100644 --- a/libraries/input-plugins/CMakeLists.txt +++ b/libraries/input-plugins/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME input-plugins) setup_hifi_library() -link_hifi_libraries(shared plugins controllers) +link_hifi_libraries(shared plugins ui-plugins controllers) GroupSources("src/input-plugins") diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index a217041f4e..747c72c08e 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -1,28 +1,6 @@ #include "DisplayPlugin.h" #include -#include - -#include "PluginContainer.h" - -bool DisplayPlugin::activate() { - if (isHmd() && (getHmdScreen() >= 0)) { - _container->showDisplayPluginsTools(); - } - return Parent::activate(); -} - -void DisplayPlugin::deactivate() { - _container->showDisplayPluginsTools(false); - if (!_container->currentDisplayActions().isEmpty()) { - auto menu = _container->getPrimaryMenu(); - foreach(auto itemInfo, _container->currentDisplayActions()) { - menu->removeMenuItem(itemInfo.first, itemInfo.second); - } - _container->currentDisplayActions().clear(); - } - Parent::deactivate(); -} int64_t DisplayPlugin::getPaintDelayUsecs() const { std::lock_guard lock(_paintDelayMutex); diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 19f803e11e..837694372a 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -64,8 +64,6 @@ public: Present = QEvent::User + 1 }; - bool activate() override; - void deactivate() override; virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } /// By default, all HMDs are stereo diff --git a/libraries/plugins/src/plugins/Plugin.cpp b/libraries/plugins/src/plugins/Plugin.cpp index 7c30f252c9..54f76b3482 100644 --- a/libraries/plugins/src/plugins/Plugin.cpp +++ b/libraries/plugins/src/plugins/Plugin.cpp @@ -9,10 +9,6 @@ QString Plugin::UNKNOWN_PLUGIN_ID("unknown"); -void Plugin::setContainer(PluginContainer* container) { - _container = container; -} - bool Plugin::isSupported() const { return true; } void Plugin::init() {} diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index fb5bf0ba55..cb03f67bd6 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -14,6 +14,8 @@ #include "Forward.h" +#include + class Plugin : public QObject { public: /// \return human-readable name @@ -28,8 +30,11 @@ public: virtual const QString& getID() const { assert(false); return UNKNOWN_PLUGIN_ID; } virtual bool isSupported() const; - - void setContainer(PluginContainer* container); + + /// Some plugins may need access to the PluginContainer, if the individual plugin + /// needs access to this, they should override these methods and store their own + /// type safe version of the pointer to the container. + virtual void setContainer(void* container) { } /// Called when plugin is initially loaded, typically at application start virtual void init(); @@ -65,7 +70,5 @@ public: protected: bool _active { false }; - PluginContainer* _container { nullptr }; static QString UNKNOWN_PLUGIN_ID; - }; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 7161132c5e..2692de931f 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -17,7 +17,6 @@ #include "RuntimePlugin.h" #include "DisplayPlugin.h" #include "InputPlugin.h" -#include "PluginContainer.h" PluginManager* PluginManager::getInstance() { @@ -133,10 +132,11 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { } } } - auto& container = PluginContainer::getInstance(); for (auto plugin : displayPlugins) { - plugin->setContainer(&container); plugin->init(); + if (_container) { + plugin->setContainer(_container); + } } }); @@ -171,10 +171,11 @@ const InputPluginList& PluginManager::getInputPlugins() { } } - auto& container = PluginContainer::getInstance(); for (auto plugin : inputPlugins) { - plugin->setContainer(&container); plugin->init(); + if (_container) { + plugin->setContainer(_container); + } } }); return inputPlugins; diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 2a94e6490b..b0aa13d2d0 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -26,4 +26,7 @@ public: void disableDisplays(const QStringList& displays); void disableInputs(const QStringList& inputs); void saveSettings(); + void setContainer(void* container) { _container = container; } +private: + void* _container { nullptr }; }; diff --git a/libraries/ui-plugins/CMakeLists.txt b/libraries/ui-plugins/CMakeLists.txt new file mode 100644 index 0000000000..9ce189b117 --- /dev/null +++ b/libraries/ui-plugins/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME ui-plugins) +setup_hifi_library(OpenGL) +link_hifi_libraries(shared plugins ui) diff --git a/libraries/plugins/src/plugins/PluginContainer.cpp b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp similarity index 100% rename from libraries/plugins/src/plugins/PluginContainer.cpp rename to libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp diff --git a/libraries/plugins/src/plugins/PluginContainer.h b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h similarity index 77% rename from libraries/plugins/src/plugins/PluginContainer.h rename to libraries/ui-plugins/src/ui-plugins/PluginContainer.h index e1d1a212e2..57c3c62704 100644 --- a/libraries/plugins/src/plugins/PluginContainer.h +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h @@ -7,16 +7,18 @@ // #pragma once +#include #include #include #include +#include #include #include #include #include -#include "Forward.h" +#include class QAction; class GLWidget; @@ -63,8 +65,8 @@ public: virtual GLWidget* getPrimaryWidget() = 0; virtual MainWindow* getPrimaryWindow() = 0; virtual QOpenGLContext* getPrimaryContext() = 0; - virtual bool isForeground() = 0; - virtual const DisplayPluginPointer getActiveDisplayPlugin() const = 0; + virtual bool isForeground() const = 0; + virtual DisplayPluginPointer getActiveDisplayPlugin() const = 0; /// settings interface bool getBoolSetting(const QString& settingName, bool defaultValue); @@ -84,3 +86,12 @@ protected: std::map _exclusiveGroups; QRect _savedGeometry { 10, 120, 800, 600 }; }; + +/// Mixin this class to your class to get easy access to the PluginContainer +#define ACCESS_PLUGIN_CONTAINER_MIXIN \ +public: \ + virtual void setContainer(void* container) override { \ + _container = static_cast(container); \ + } \ +protected: \ + PluginContainer* _container { nullptr }; diff --git a/plugins/hifiSixense/CMakeLists.txt b/plugins/hifiSixense/CMakeLists.txt index 589b5b8964..f907d7865f 100644 --- a/plugins/hifiSixense/CMakeLists.txt +++ b/plugins/hifiSixense/CMakeLists.txt @@ -8,5 +8,5 @@ set(TARGET_NAME hifiSixense) setup_hifi_plugin(Script Qml Widgets) -link_hifi_libraries(shared controllers ui plugins input-plugins) +link_hifi_libraries(shared controllers ui plugins ui-plugins input-plugins) target_sixense() diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 9ea79a8b96..4faec959ea 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include diff --git a/plugins/hifiSixense/src/SixenseManager.h b/plugins/hifiSixense/src/SixenseManager.h index 6aec9fd4ad..c9e8f5d4ad 100644 --- a/plugins/hifiSixense/src/SixenseManager.h +++ b/plugins/hifiSixense/src/SixenseManager.h @@ -17,6 +17,7 @@ #include #include +#include #include struct _sixenseControllerData; @@ -24,6 +25,7 @@ using SixenseControllerData = _sixenseControllerData; // Handles interaction with the Sixense SDK (e.g., Razer Hydra). class SixenseManager : public InputPlugin { + ACCESS_PLUGIN_CONTAINER_MIXIN Q_OBJECT public: // Plugin functions diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index a91690ecdd..778be08dcf 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -13,7 +13,7 @@ if (WIN32) set(TARGET_NAME oculus) setup_hifi_plugin(Multimedia) - link_hifi_libraries(shared gl gpu controllers ui plugins display-plugins input-plugins audio-client networking) + link_hifi_libraries(shared gl gpu controllers ui plugins ui-plugins display-plugins input-plugins audio-client networking) include_hifi_library_headers(octree) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 9f0e76363b..128b980558 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -13,7 +13,7 @@ #include -#include +#include #include #include diff --git a/plugins/oculusLegacy/CMakeLists.txt b/plugins/oculusLegacy/CMakeLists.txt index a4e00013f1..c1f2c6249f 100644 --- a/plugins/oculusLegacy/CMakeLists.txt +++ b/plugins/oculusLegacy/CMakeLists.txt @@ -12,7 +12,7 @@ if (APPLE) set(TARGET_NAME oculusLegacy) setup_hifi_plugin() - link_hifi_libraries(shared gl gpu plugins ui display-plugins input-plugins) + link_hifi_libraries(shared gl gpu plugins ui ui-plugins display-plugins input-plugins) include_hifi_library_headers(octree) diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 8e044fbc16..3b2c9808ab 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -26,7 +26,6 @@ #include #include -#include "plugins/PluginContainer.h" #include "OculusHelpers.h" using namespace oglplus; diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 1ba8d05b92..8263e87767 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -12,7 +12,7 @@ if (WIN32) set(TARGET_NAME openvr) setup_hifi_plugin(OpenGL Script Qml Widgets) link_hifi_libraries(shared gl networking controllers ui - plugins display-plugins input-plugins script-engine + plugins display-plugins ui-plugins input-plugins script-engine render-utils model gpu render model-networking fbx) include_hifi_library_headers(octree) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 3e7e5abbf3..552a62514d 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include "OpenVrHelpers.h" diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index b862aacb06..88c76a97b8 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 3a2ef1573f..f3cd2b4a93 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -23,12 +23,14 @@ #include #include #include +#include namespace vr { class IVRSystem; } -class ViveControllerManager : public InputPlugin { +class ViveControllerManager : public InputPlugin { + ACCESS_PLUGIN_CONTAINER_MIXIN Q_OBJECT public: // Plugin functions diff --git a/tests/controllers/CMakeLists.txt b/tests/controllers/CMakeLists.txt index cf1152da02..3aac4db0a8 100644 --- a/tests/controllers/CMakeLists.txt +++ b/tests/controllers/CMakeLists.txt @@ -6,7 +6,7 @@ setup_hifi_project(Script Qml) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared gl script-engine plugins render-utils input-plugins display-plugins controllers) +link_hifi_libraries(shared gl script-engine plugins render-utils ui-plugins input-plugins display-plugins controllers) if (WIN32) diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index 36ed566ea7..15b768bb36 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -34,7 +34,7 @@ #include #include -#include +#include #include #include #include @@ -90,8 +90,8 @@ public: virtual MainWindow* getPrimaryWindow() override { return nullptr; } virtual QOpenGLContext* getPrimaryContext() override { return nullptr; } virtual ui::Menu* getPrimaryMenu() override { return nullptr; } - virtual bool isForeground() override { return true; } - virtual const DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } + virtual bool isForeground() const override { return true; } + virtual DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } }; class MyControllerScriptingInterface : public controller::ScriptingInterface { From 160a5de271ccf15d1dd708611c1dc12f2211a912 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 21 Jun 2016 12:34:42 -0700 Subject: [PATCH 0672/1237] CR feedback --- interface/src/Application.cpp | 3 --- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 2 ++ .../src/display-plugins/NullDisplayPlugin.cpp | 1 + .../src/display-plugins/NullDisplayPlugin.h | 2 -- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 1 + .../src/display-plugins/OpenGLDisplayPlugin.h | 4 ---- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 1 + .../src/display-plugins/hmd/HmdDisplayPlugin.h | 2 -- .../stereo/SideBySideStereoDisplayPlugin.cpp | 1 + .../display-plugins/stereo/StereoDisplayPlugin.cpp | 1 + libraries/plugins/src/plugins/Plugin.cpp | 4 ++++ libraries/plugins/src/plugins/Plugin.h | 11 ++++------- libraries/plugins/src/plugins/PluginManager.cpp | 8 ++------ libraries/plugins/src/plugins/PluginManager.h | 4 ++-- libraries/ui-plugins/src/ui-plugins/PluginContainer.h | 10 ---------- plugins/hifiSixense/src/SixenseManager.cpp | 1 + plugins/hifiSixense/src/SixenseManager.h | 2 -- .../oculusLegacy/src/OculusLegacyDisplayPlugin.cpp | 1 + plugins/openvr/src/OpenVrDisplayPlugin.cpp | 1 + plugins/openvr/src/ViveControllerManager.cpp | 1 + plugins/openvr/src/ViveControllerManager.h | 4 +--- 21 files changed, 24 insertions(+), 41 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index babcd877c4..f553b4bbdf 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2954,8 +2954,6 @@ void Application::loadSettings() { Menu::getInstance()->loadSettings(); // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. auto pluginManager = PluginManager::getInstance(); - - auto plugins = pluginManager->getPreferredDisplayPlugins(); for (auto plugin : plugins) { auto menu = Menu::getInstance(); @@ -5192,7 +5190,6 @@ void Application::updateDisplayMode() { // FIXME probably excessive and useless context switching _offscreenContext->makeCurrent(); - qDebug() << "Application::updateDisplayMode()... line:" << __LINE__ << "about to call newDisplayPlugin->activate()"; bool active = newDisplayPlugin->activate(); if (!active) { diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index 3bdb96bde3..f488a805c6 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -13,6 +13,8 @@ #include #include +#include + const QString Basic2DWindowOpenGLDisplayPlugin::NAME("Desktop"); static const QString FULLSCREEN = "Fullscreen"; diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 8ebbf4d74b..4fadbdb94b 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -10,6 +10,7 @@ #include "NullDisplayPlugin.h" #include +#include const QString NullDisplayPlugin::NAME("NullDisplayPlugin"); diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index c8770393c0..dfa4232a86 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -7,11 +7,9 @@ // #pragma once -#include #include "DisplayPlugin.h" class NullDisplayPlugin : public DisplayPlugin { - ACCESS_PLUGIN_CONTAINER_MIXIN public: virtual ~NullDisplayPlugin() final {} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 2128488718..4bca48aeb0 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 10362b8181..aa3699584a 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -21,14 +21,10 @@ #include #include -#include - #define THREADED_PRESENT 1 class OpenGLDisplayPlugin : public DisplayPlugin { - ACCESS_PLUGIN_CONTAINER_MIXIN - protected: using Mutex = std::mutex; using Lock = std::unique_lock; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index b7739a7fb6..b29348f646 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index b87f68231c..e6ceb7e376 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -13,8 +13,6 @@ #include "../OpenGLDisplayPlugin.h" -#include - class HmdDisplayPlugin : public OpenGLDisplayPlugin { using Parent = OpenGLDisplayPlugin; public: diff --git a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp index be095ecd39..5d9f812edf 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp @@ -9,6 +9,7 @@ #include "SideBySideStereoDisplayPlugin.h" #include #include +#include #include #include "../CompositorHelper.h" diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index afb99e3174..cfdfb1fc21 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include "../CompositorHelper.h" diff --git a/libraries/plugins/src/plugins/Plugin.cpp b/libraries/plugins/src/plugins/Plugin.cpp index 54f76b3482..7c30f252c9 100644 --- a/libraries/plugins/src/plugins/Plugin.cpp +++ b/libraries/plugins/src/plugins/Plugin.cpp @@ -9,6 +9,10 @@ QString Plugin::UNKNOWN_PLUGIN_ID("unknown"); +void Plugin::setContainer(PluginContainer* container) { + _container = container; +} + bool Plugin::isSupported() const { return true; } void Plugin::init() {} diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index cb03f67bd6..fb5bf0ba55 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -14,8 +14,6 @@ #include "Forward.h" -#include - class Plugin : public QObject { public: /// \return human-readable name @@ -30,11 +28,8 @@ public: virtual const QString& getID() const { assert(false); return UNKNOWN_PLUGIN_ID; } virtual bool isSupported() const; - - /// Some plugins may need access to the PluginContainer, if the individual plugin - /// needs access to this, they should override these methods and store their own - /// type safe version of the pointer to the container. - virtual void setContainer(void* container) { } + + void setContainer(PluginContainer* container); /// Called when plugin is initially loaded, typically at application start virtual void init(); @@ -70,5 +65,7 @@ public: protected: bool _active { false }; + PluginContainer* _container { nullptr }; static QString UNKNOWN_PLUGIN_ID; + }; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 2692de931f..d5c860200a 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -133,10 +133,8 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { } } for (auto plugin : displayPlugins) { + plugin->setContainer(_container); plugin->init(); - if (_container) { - plugin->setContainer(_container); - } } }); @@ -172,10 +170,8 @@ const InputPluginList& PluginManager::getInputPlugins() { } for (auto plugin : inputPlugins) { + plugin->setContainer(_container); plugin->init(); - if (_container) { - plugin->setContainer(_container); - } } }); return inputPlugins; diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index b0aa13d2d0..7903bdd724 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -26,7 +26,7 @@ public: void disableDisplays(const QStringList& displays); void disableInputs(const QStringList& inputs); void saveSettings(); - void setContainer(void* container) { _container = container; } + void setContainer(PluginContainer* container) { _container = container; } private: - void* _container { nullptr }; + PluginContainer* _container { nullptr }; }; diff --git a/libraries/ui-plugins/src/ui-plugins/PluginContainer.h b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h index 57c3c62704..74ac834057 100644 --- a/libraries/ui-plugins/src/ui-plugins/PluginContainer.h +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h @@ -7,12 +7,10 @@ // #pragma once -#include #include #include #include -#include #include #include #include @@ -87,11 +85,3 @@ protected: QRect _savedGeometry { 10, 120, 800, 600 }; }; -/// Mixin this class to your class to get easy access to the PluginContainer -#define ACCESS_PLUGIN_CONTAINER_MIXIN \ -public: \ - virtual void setContainer(void* container) override { \ - _container = static_cast(container); \ - } \ -protected: \ - PluginContainer* _container { nullptr }; diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 4faec959ea..566f879f69 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include diff --git a/plugins/hifiSixense/src/SixenseManager.h b/plugins/hifiSixense/src/SixenseManager.h index c9e8f5d4ad..6aec9fd4ad 100644 --- a/plugins/hifiSixense/src/SixenseManager.h +++ b/plugins/hifiSixense/src/SixenseManager.h @@ -17,7 +17,6 @@ #include #include -#include #include struct _sixenseControllerData; @@ -25,7 +24,6 @@ using SixenseControllerData = _sixenseControllerData; // Handles interaction with the Sixense SDK (e.g., Razer Hydra). class SixenseManager : public InputPlugin { - ACCESS_PLUGIN_CONTAINER_MIXIN Q_OBJECT public: // Plugin functions diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 3b2c9808ab..4aadb890d5 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -26,6 +26,7 @@ #include #include +#include #include "OculusHelpers.h" using namespace oglplus; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 552a62514d..c5d3be25b2 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include "OpenVrHelpers.h" diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 88c76a97b8..12813dae93 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index f3cd2b4a93..3a2ef1573f 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -23,14 +23,12 @@ #include #include #include -#include namespace vr { class IVRSystem; } -class ViveControllerManager : public InputPlugin { - ACCESS_PLUGIN_CONTAINER_MIXIN +class ViveControllerManager : public InputPlugin { Q_OBJECT public: // Plugin functions From c8ead2ad048845e46f5d127c132c7239e7c4d011 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 21 Jun 2016 13:28:15 -0700 Subject: [PATCH 0673/1237] conform to coding braces standard --- scripts/tutorials/getDomainMetadata.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/tutorials/getDomainMetadata.js b/scripts/tutorials/getDomainMetadata.js index 8c347a09ae..54c356ae7b 100644 --- a/scripts/tutorials/getDomainMetadata.js +++ b/scripts/tutorials/getDomainMetadata.js @@ -113,7 +113,9 @@ function displayMetadata(place, metadata) { } function clearMetadata() { - if (OVERLAY) { Overlays.deleteOverlay(OVERLAY); } + if (OVERLAY) { + Overlays.deleteOverlay(OVERLAY); + } } // Request JSON from a url, synchronously From 6c2c7f1eec2213affbcc82bf8f5ac32c6d2c0f74 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 13:28:40 -0700 Subject: [PATCH 0674/1237] Fix connected_device user activity event --- libraries/networking/src/UserActivityLogger.cpp | 15 +++++++++++++++ libraries/plugins/src/plugins/PluginManager.cpp | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 83c6eb304e..8b20420a34 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -125,6 +125,21 @@ void UserActivityLogger::changedDomain(QString domainURL) { } void UserActivityLogger::connectedDevice(QString typeOfDevice, QString deviceName) { + static QStringList DEVICE_BLACKLIST = { + "Desktop", + "NullDisplayPlugin", + "3D TV - Side by Side Stereo", + "3D TV - Interleaved", + + "Keyboard/Mouse", + "Neuron", + "SDL2" + }; + + if (DEVICE_BLACKLIST.contains(deviceName)) { + return; + } + const QString ACTION_NAME = "connected_device"; QJsonObject actionDetails; const QString TYPE_OF_DEVICE = "type_of_device"; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index eb6465aab2..86bdcfe818 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -14,6 +14,9 @@ #include #include +#include +#include + #include "RuntimePlugin.h" #include "DisplayPlugin.h" #include "InputPlugin.h" @@ -81,6 +84,7 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { } auto& container = PluginContainer::getInstance(); for (auto plugin : displayPlugins) { + UserActivityLogger::getInstance().connectedDevice("display", plugin->getName()); plugin->setContainer(&container); plugin->init(); } @@ -117,6 +121,7 @@ const InputPluginList& PluginManager::getInputPlugins() { auto& container = PluginContainer::getInstance(); for (auto plugin : inputPlugins) { + UserActivityLogger::getInstance().connectedDevice("input", plugin->getName()); plugin->setContainer(&container); plugin->init(); } From 251e2137d33b285c6e3db9c10c1382cb8c240751 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 13:31:10 -0700 Subject: [PATCH 0675/1237] Remove old use of connectedDevice --- plugins/hifiSpacemouse/src/SpacemouseManager.cpp | 2 -- plugins/openvr/src/ViveControllerManager.cpp | 1 - 2 files changed, 3 deletions(-) diff --git a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp index 3d9b93ff44..dc60fe4cbb 100644 --- a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp @@ -59,7 +59,6 @@ bool SpacemouseManager::activate() { if (instance->getDeviceID() == controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", NAME); } return true; } @@ -330,7 +329,6 @@ bool SpacemouseManager::RawInputEventFilter(void* msg, long* result) { auto userInputMapper = DependencyManager::get(); if (Is3dmouseAttached() && instance->getDeviceID() == controller::Input::INVALID_DEVICE) { userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", "Spacemouse"); } else if (!Is3dmouseAttached() && instance->getDeviceID() != controller::Input::INVALID_DEVICE) { userInputMapper->removeDevice(instance->getDeviceID()); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 953501ccec..d3e1690155 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -234,7 +234,6 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu if (!_registeredWithInputMapper && _inputDevice->_trackedControllers > 0) { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; - UserActivityLogger::getInstance().connectedDevice("spatial_controller", "steamVR"); } } From 0ea9c5c26da41848f112d2fad62135aa073648a8 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 13:32:17 -0700 Subject: [PATCH 0676/1237] Fix getMemoryInfo not returning false on non-Windows --- libraries/shared/src/SharedUtil.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index a6866fdc93..edb6fe437d 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -864,7 +864,9 @@ bool getMemoryInfo(MemoryInfo& info) { } info.processUsedMemoryBytes = pmc.PrivateUsage; info.processPeakUsedMemoryBytes = pmc.PeakPagefileUsage; -#endif return true; +#endif + + return false; } \ No newline at end of file From 937bd0c1bea53ed3cd3954b91f138a8cce46f16c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 21 Jun 2016 13:54:21 -0700 Subject: [PATCH 0677/1237] SHAPE_TYPE_MESH --> SHAPE_TYPE_STATIC_MESH --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 6 +++--- libraries/entities/src/EntityItemProperties.cpp | 2 +- libraries/physics/src/ShapeFactory.cpp | 4 ++-- libraries/shared/src/ShapeInfo.cpp | 4 ++-- libraries/shared/src/ShapeInfo.h | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7b1dddfcab..ab91edd294 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -593,7 +593,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { // the model is still being downloaded. return false; - } else if (type == SHAPE_TYPE_MESH) { + } else if (type == SHAPE_TYPE_STATIC_MESH) { return (_model && _model->isLoaded()); } return true; @@ -706,7 +706,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } info.setParams(type, dimensions, _compoundShapeURL); - } else if (type == SHAPE_TYPE_MESH) { + } else if (type == SHAPE_TYPE_STATIC_MESH) { // compute meshPart local transforms QVector localTransforms; const FBXGeometry& geometry = _model->getFBXGeometry(); @@ -820,7 +820,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } pointCollection.push_back(points); - info.setParams(SHAPE_TYPE_MESH, 0.5f * dimensions, _modelURL); + info.setParams(SHAPE_TYPE_STATIC_MESH, 0.5f * dimensions, _modelURL); } else { ModelEntityItem::computeShapeInfo(info); info.setParams(type, 0.5f * dimensions); diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index c521962976..a62f4b182a 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -123,7 +123,7 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_HULL); addShapeType(SHAPE_TYPE_PLANE); addShapeType(SHAPE_TYPE_COMPOUND); - addShapeType(SHAPE_TYPE_MESH); + addShapeType(SHAPE_TYPE_STATIC_MESH); } QString getCollisionGroupAsString(uint8_t group) { diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 4d5f56853d..3afc170a8c 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -161,7 +161,7 @@ btConvexHullShape* createConvexHull(const ShapeInfo::PointList& points) { // util method btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info) { - assert(info.getType() == SHAPE_TYPE_MESH); // should only get here for mesh shapes + assert(info.getType() == SHAPE_TYPE_STATIC_MESH); // should only get here for mesh shapes const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); assert(pointCollection.size() == 1); // should only have one mesh @@ -274,7 +274,7 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } } break; - case SHAPE_TYPE_MESH: { + case SHAPE_TYPE_STATIC_MESH: { btTriangleIndexVertexArray* dataArray = createStaticMeshArray(info); shape = new StaticMeshShape(dataArray); } diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index af81a0f96b..e0f4cc18b2 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -41,7 +41,7 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString break; } case SHAPE_TYPE_COMPOUND: - case SHAPE_TYPE_MESH: + case SHAPE_TYPE_STATIC_MESH: _url = QUrl(url); break; default: @@ -224,7 +224,7 @@ const DoubleHashKey& ShapeInfo::getHash() const { } _doubleHashKey.setHash2(hash); - if (_type == SHAPE_TYPE_COMPOUND || _type == SHAPE_TYPE_MESH) { + if (_type == SHAPE_TYPE_COMPOUND || _type == SHAPE_TYPE_STATIC_MESH) { QString url = _url.toString(); if (!url.isEmpty()) { // fold the urlHash into both parts diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 794f31a987..96132a4b23 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -39,7 +39,7 @@ enum ShapeType { SHAPE_TYPE_HULL, SHAPE_TYPE_PLANE, SHAPE_TYPE_COMPOUND, - SHAPE_TYPE_MESH + SHAPE_TYPE_STATIC_MESH }; class ShapeInfo { From 4c9ec7ca432bfac30bf84c0bf829845d111c6191 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 21 Jun 2016 13:55:11 -0700 Subject: [PATCH 0678/1237] protect physics against bad entity properties --- libraries/physics/src/EntityMotionState.cpp | 5 +++ libraries/physics/src/ObjectMotionState.cpp | 48 +++++++++++---------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 8f22c576f0..e0d355911a 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -152,6 +152,11 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { } assert(entityTreeIsLocked()); + if (_entity->getShapeType() == SHAPE_TYPE_STATIC_MESH + || (_body && _body->getCollisionShape()->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)) { + return MOTION_TYPE_STATIC; + } + if (_entity->getDynamic()) { if (!_entity->getParentID().isNull()) { // if something would have been dynamic but is a child of something else, force it to be kinematic, instead. diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index de435e80da..f915121718 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -203,35 +203,37 @@ void ObjectMotionState::handleEasyChanges(uint32_t& flags) { } } - if (flags & Simulation::DIRTY_LINEAR_VELOCITY) { - btVector3 newLinearVelocity = glmToBullet(getObjectLinearVelocity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newLinearVelocity - _body->getLinearVelocity()).length(); - if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + if (_body->getCollisionShape()->getShapeType() != TRIANGLE_MESH_SHAPE_PROXYTYPE) { + if (flags & Simulation::DIRTY_LINEAR_VELOCITY) { + btVector3 newLinearVelocity = glmToBullet(getObjectLinearVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newLinearVelocity - _body->getLinearVelocity()).length(); + if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } - } - _body->setLinearVelocity(newLinearVelocity); + _body->setLinearVelocity(newLinearVelocity); - btVector3 newGravity = glmToBullet(getObjectGravity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newGravity - _body->getGravity()).length(); - if (delta > ACTIVATION_GRAVITY_DELTA || - (delta > 0.0f && _body->getGravity().length2() == 0.0f)) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + btVector3 newGravity = glmToBullet(getObjectGravity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newGravity - _body->getGravity()).length(); + if (delta > ACTIVATION_GRAVITY_DELTA || + (delta > 0.0f && _body->getGravity().length2() == 0.0f)) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } + _body->setGravity(newGravity); } - _body->setGravity(newGravity); - } - if (flags & Simulation::DIRTY_ANGULAR_VELOCITY) { - btVector3 newAngularVelocity = glmToBullet(getObjectAngularVelocity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newAngularVelocity - _body->getAngularVelocity()).length(); - if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + if (flags & Simulation::DIRTY_ANGULAR_VELOCITY) { + btVector3 newAngularVelocity = glmToBullet(getObjectAngularVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newAngularVelocity - _body->getAngularVelocity()).length(); + if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } + _body->setAngularVelocity(newAngularVelocity); } - _body->setAngularVelocity(newAngularVelocity); } if (flags & Simulation::DIRTY_MATERIAL) { From 702e83ba6a8a9d6db90eb4c40caf81caa1a005c4 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 21 Jun 2016 14:03:21 -0700 Subject: [PATCH 0679/1237] prevent incompatible entity properties combos --- libraries/entities/src/EntityItem.cpp | 50 ++++++++++++++++++--------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 64b6a2c655..2abb9f12e2 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1602,14 +1602,20 @@ void EntityItem::updateMass(float mass) { void EntityItem::updateVelocity(const glm::vec3& value) { glm::vec3 velocity = getLocalVelocity(); if (velocity != value) { - const float MIN_LINEAR_SPEED = 0.001f; - if (glm::length(value) < MIN_LINEAR_SPEED) { - velocity = ENTITY_ITEM_ZERO_VEC3; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + if (velocity != Vectors::ZERO) { + setLocalVelocity(Vectors::ZERO); + } } else { - velocity = value; + const float MIN_LINEAR_SPEED = 0.001f; + if (glm::length(value) < MIN_LINEAR_SPEED) { + velocity = ENTITY_ITEM_ZERO_VEC3; + } else { + velocity = value; + } + setLocalVelocity(velocity); + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } - setLocalVelocity(velocity); - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } } @@ -1630,22 +1636,30 @@ void EntityItem::updateDamping(float value) { void EntityItem::updateGravity(const glm::vec3& value) { if (_gravity != value) { - _gravity = value; - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + _gravity = Vectors::ZERO; + } else { + _gravity = value; + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + } } } void EntityItem::updateAngularVelocity(const glm::vec3& value) { glm::vec3 angularVelocity = getLocalAngularVelocity(); if (angularVelocity != value) { - const float MIN_ANGULAR_SPEED = 0.0002f; - if (glm::length(value) < MIN_ANGULAR_SPEED) { - angularVelocity = ENTITY_ITEM_ZERO_VEC3; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + setLocalAngularVelocity(Vectors::ZERO); } else { - angularVelocity = value; + const float MIN_ANGULAR_SPEED = 0.0002f; + if (glm::length(value) < MIN_ANGULAR_SPEED) { + angularVelocity = ENTITY_ITEM_ZERO_VEC3; + } else { + angularVelocity = value; + } + setLocalAngularVelocity(angularVelocity); + _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } - setLocalAngularVelocity(angularVelocity); - _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } } @@ -1680,8 +1694,12 @@ void EntityItem::updateCollisionMask(uint8_t value) { void EntityItem::updateDynamic(bool value) { if (_dynamic != value) { - _dynamic = value; - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + _dynamic = false; + } else { + _dynamic = value; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } } } From 9a17f5b439a49498377d48c77d069c5283331103 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 21 Jun 2016 14:12:27 -0700 Subject: [PATCH 0680/1237] update homebrew instructions --- BUILD_OSX.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 44f27d3d02..ab84bc4711 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -13,11 +13,11 @@ We no longer require install of qt5 via our [homebrew formulas repository](https Assuming you've installed OpenSSL or Qt 5 using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR and QT_CMAKE_PREFIX_PATH so CMake can find your installations. For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR: - export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2d_1 + export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2h_1/ For Qt 5.5.1 installed via homebrew, set QT_CMAKE_PREFIX_PATH as follows. - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.5.1_2/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt55/5.5.1/lib/cmake Not that these use the versions from homebrew formulae at the time of this writing, and the version in the path will likely change. From f3bad3a63b5df2f2622e539bb02ff2c6f17afa23 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 21 Jun 2016 12:43:02 -0700 Subject: [PATCH 0681/1237] Don't trigger keyboard on revealing the overlay layer --- interface/resources/qml/desktop/Desktop.qml | 5 ++++ plugins/openvr/src/OpenVrDisplayPlugin.cpp | 2 +- plugins/openvr/src/OpenVrHelpers.cpp | 28 +++++++++++++++++--- plugins/openvr/src/OpenVrHelpers.h | 3 ++- plugins/openvr/src/ViveControllerManager.cpp | 2 +- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 58d1cba1b8..9f10cfc64a 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -297,6 +297,9 @@ FocusScope { onPinnedChanged: { if (pinned) { + nullFocus.focus = true; + nullFocus.forceActiveFocus(); + // recalculate our non-pinned children hiddenChildren = d.findMatchingChildren(desktop, function(child){ return !d.isTopLevelWindow(child) && child.visible && !child.pinned; @@ -478,6 +481,8 @@ FocusScope { FocusHack { id: focusHack; } + FocusScope { id: nullFocus; } + Rectangle { id: focusDebugger; objectName: "focusDebugger" diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 9c4313dc13..4f81f711e3 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -267,7 +267,7 @@ void OpenVrDisplayPlugin::unsuppressKeyboard() { return; } if (1 == _keyboardSupressionCount.fetch_sub(1)) { - enableOpenVrKeyboard(); + enableOpenVrKeyboard(_container); } } diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 4f02c0384d..1bff0073f6 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -21,6 +21,9 @@ #include #include #include +#include +#include +#include "../../interface/src/Menu.h" Q_DECLARE_LOGGING_CATEGORY(displayplugins) Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") @@ -91,7 +94,7 @@ void releaseOpenVrSystem() { static char textArray[8192]; -static QMetaObject::Connection _focusConnection, _focusTextConnection; +static QMetaObject::Connection _focusConnection, _focusTextConnection, _overlayMenuConnection; extern bool _openVrDisplayActive; static vr::IVROverlay* _overlay { nullptr }; static QObject* _keyboardFocusObject { nullptr }; @@ -99,7 +102,8 @@ static QString _existingText; static Qt::InputMethodHints _currentHints; extern vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; static bool _keyboardShown { false }; -static const uint32_t SHOW_KEYBOARD_DELAY_MS = 100; +static bool _overlayRevealed { false }; +static const uint32_t SHOW_KEYBOARD_DELAY_MS = 400; void showOpenVrKeyboard(bool show = true) { if (!_overlay) { @@ -175,13 +179,25 @@ void finishOpenVrKeyboardInput() { static const QString DEBUG_FLAG("HIFI_DISABLE_STEAM_VR_KEYBOARD"); bool disableSteamVrKeyboard = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); -void enableOpenVrKeyboard() { +void enableOpenVrKeyboard(PluginContainer* container) { if (disableSteamVrKeyboard) { return; } auto offscreenUi = DependencyManager::get(); _overlay = vr::VROverlay(); + + auto menu = container->getPrimaryMenu(); + auto action = menu->getActionForOption(MenuOption::Overlays); + + // When the overlays are revealed, suppress the keyboard from appearing on text focus for a tenth of a second. + _overlayMenuConnection = QObject::connect(action, &QAction::triggered, [action] { + if (action->isChecked()) { + _overlayRevealed = true; + QTimer::singleShot(100, [&] { _overlayRevealed = false; }); + } + }); + _focusConnection = QObject::connect(offscreenUi->getWindow(), &QQuickWindow::focusObjectChanged, [](QObject* object) { if (object != _keyboardFocusObject) { showOpenVrKeyboard(false); @@ -190,6 +206,11 @@ void enableOpenVrKeyboard() { _focusTextConnection = QObject::connect(offscreenUi.data(), &OffscreenUi::focusTextChanged, [](bool focusText) { if (_openVrDisplayActive) { + if (_overlayRevealed) { + // suppress at most one text focus event + _overlayRevealed = false; + return; + } showOpenVrKeyboard(focusText); } }); @@ -200,6 +221,7 @@ void disableOpenVrKeyboard() { if (disableSteamVrKeyboard) { return; } + QObject::disconnect(_overlayMenuConnection); QObject::disconnect(_focusTextConnection); QObject::disconnect(_focusConnection); } diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 131db517ab..19c9cbfff5 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -13,6 +13,7 @@ #include #include +#include bool openVrSupported(); @@ -20,7 +21,7 @@ vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); void handleOpenVrEvents(); bool openVrQuitRequested(); -void enableOpenVrKeyboard(); +void enableOpenVrKeyboard(PluginContainer* container); void disableOpenVrKeyboard(); bool isOpenVrKeyboardShown(); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 7f78ab8553..6c272d2a27 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -63,7 +63,7 @@ bool ViveControllerManager::activate() { } Q_ASSERT(_system); - enableOpenVrKeyboard(); + enableOpenVrKeyboard(_container); // OpenVR provides 3d mesh representations of the controllers // Disabled controller rendering code From f2f7e21eaf3b56b777a2819f0c2499b039b97bf8 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 21 Jun 2016 16:17:43 -0700 Subject: [PATCH 0682/1237] remove dead refereince to PluginContainer.h from space mouse --- plugins/hifiSpacemouse/src/SpacemouseManager.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp index 3d9b93ff44..0c29ced6f9 100644 --- a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp @@ -18,7 +18,6 @@ #include #include -#include #include const QString SpacemouseManager::NAME { "Spacemouse" }; From da71fcb57fdffadcc320ab7092f8e0318746b929 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 16:37:58 -0700 Subject: [PATCH 0683/1237] Update connected device detection implementation --- libraries/plugins/src/plugins/Plugin.h | 8 +++++++ .../plugins/src/plugins/PluginManager.cpp | 23 ++++++++++++++++++- plugins/hifiNeuron/src/NeuronPlugin.cpp | 2 ++ plugins/hifiSdl2/src/SDL2Manager.cpp | 2 ++ plugins/hifiSixense/src/SixenseManager.cpp | 6 +++++ plugins/oculus/src/OculusDisplayPlugin.cpp | 6 +++++ plugins/oculus/src/OculusDisplayPlugin.h | 2 ++ plugins/openvr/src/OpenVrDisplayPlugin.cpp | 6 +++++ plugins/openvr/src/OpenVrDisplayPlugin.h | 2 ++ 9 files changed, 56 insertions(+), 1 deletion(-) diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index fb5bf0ba55..0452c7fbfe 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -15,6 +15,7 @@ #include "Forward.h" class Plugin : public QObject { + Q_OBJECT public: /// \return human-readable name virtual const QString& getName() const = 0; @@ -63,6 +64,13 @@ public: virtual void saveSettings() const {} virtual void loadSettings() {} +signals: + // These signals should be emitted when a device is first known to be available. In some cases this will + // be in `init()`, in other cases, like Neuron, this isn't known until activation. + // SDL2 isn't a device itself, but can have 0+ subdevices. subdeviceConnected is used in this case. + void deviceConnected(QString pluginName) const; + void subdeviceConnected(QString pluginName, QString subdeviceName) const; + protected: bool _active { false }; PluginContainer* _container { nullptr }; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 86bdcfe818..64d771da6a 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -69,6 +69,15 @@ static DisplayPluginList displayPlugins; const DisplayPluginList& PluginManager::getDisplayPlugins() { static std::once_flag once; + static auto deviceAddedCallback = [](QString deviceName) { + qDebug() << "Added device: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("display", deviceName); + }; + static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) { + qDebug() << "Added subdevice: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("display", pluginName + " | " + deviceName); + }; + std::call_once(once, [&] { // Grab the built in plugins displayPlugins = ::getDisplayPlugins(); @@ -84,7 +93,8 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { } auto& container = PluginContainer::getInstance(); for (auto plugin : displayPlugins) { - UserActivityLogger::getInstance().connectedDevice("display", plugin->getName()); + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback); plugin->setContainer(&container); plugin->init(); } @@ -106,6 +116,15 @@ void PluginManager::disableDisplayPlugin(const QString& name) { const InputPluginList& PluginManager::getInputPlugins() { static InputPluginList inputPlugins; static std::once_flag once; + static auto deviceAddedCallback = [](QString deviceName) { + qDebug() << "Added device: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("input", deviceName); + }; + static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) { + qDebug() << "Added subdevice: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("input", pluginName + " | " + deviceName); + }; + std::call_once(once, [&] { inputPlugins = ::getInputPlugins(); @@ -122,6 +141,8 @@ const InputPluginList& PluginManager::getInputPlugins() { auto& container = PluginContainer::getInstance(); for (auto plugin : inputPlugins) { UserActivityLogger::getInstance().connectedDevice("input", plugin->getName()); + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback); plugin->setContainer(&container); plugin->init(); } diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 0a4bc7f8d2..e41472a8c5 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -387,6 +387,8 @@ bool NeuronPlugin::activate() { } else { qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; + emit deviceConnected(getName()); + BRRegisterAutoSyncParmeter(_socketRef, Cmd_CombinationMode); return true; } diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index 0bdb68f830..14a1b0fbac 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -66,6 +66,7 @@ void SDL2Manager::init() { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(joystick); emit joystickAdded(joystick.get()); + emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); } } } @@ -157,6 +158,7 @@ void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrati _openJoysticks[id] = joystick; userInputMapper->registerDevice(joystick); emit joystickAdded(joystick.get()); + emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); } } else if (event.type == SDL_CONTROLLERDEVICEREMOVED) { if (_openJoysticks.contains(event.cdevice.which)) { diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 9ea79a8b96..0993f9d12f 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -137,6 +137,12 @@ void SixenseManager::setSixenseFilter(bool filter) { void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { BAIL_IF_NOT_LOADED + static bool sixenseHasBeenConnected { false }; + if (!sixenseHasBeenConnected && sixenseIsBaseConnected(0)) { + sixenseHasBeenConnected = true; + emit deviceConnected(getName()); + } + auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 1006d69f06..2b2ec5bdb0 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -28,6 +28,12 @@ bool OculusDisplayPlugin::internalActivate() { return result; } +void OculusDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + void OculusDisplayPlugin::cycleDebugOutput() { if (_session) { currentDebugMode = static_cast((currentDebugMode + 1) % ovrPerfHud_Count); diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index d6cd6f6f3d..ed6e0d13ea 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -17,6 +17,8 @@ class OculusDisplayPlugin : public OculusBaseDisplayPlugin { public: const QString& getName() const override { return NAME; } + void init() override; + QString getPreferredAudioInDevice() const override; QString getPreferredAudioOutDevice() const override; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index fe406cc29a..f805132edd 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -41,6 +41,12 @@ bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } +void OpenVrDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + bool OpenVrDisplayPlugin::internalActivate() { _container->setIsOptionChecked(StandingHMDSensorMode, true); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index fda5e37c2a..2e31bfa2c6 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -21,6 +21,8 @@ public: bool isSupported() const override; const QString& getName() const override { return NAME; } + void init() override; + float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } void customizeContext() override; From 310adb0f45758db8a2ea7050a5d2d1a9b903a8ba Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 16:42:39 -0700 Subject: [PATCH 0684/1237] Update spacemouse deviceConnected implementation --- plugins/hifiSpacemouse/src/SpacemouseManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp index dc60fe4cbb..b4c15e6571 100644 --- a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp @@ -855,7 +855,7 @@ void SpacemouseManager::init() { if (Is3dmouseAttached() && instance->getDeviceID() == controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", "Spacemouse"); + emit deviceConnected(getName()); } //let one axis be dominant //ConnexionClientControl(fConnexionClientID, kConnexionCtlSetSwitches, kConnexionSwitchDominant | kConnexionSwitchEnableAll, NULL); From 4e52521f8cd05d4a30a0cc219c119d9f1fc37d79 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 16:43:16 -0700 Subject: [PATCH 0685/1237] Update UserActivityLogger device blacklist --- libraries/networking/src/UserActivityLogger.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 8b20420a34..9a5f0541e5 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -131,9 +131,7 @@ void UserActivityLogger::connectedDevice(QString typeOfDevice, QString deviceNam "3D TV - Side by Side Stereo", "3D TV - Interleaved", - "Keyboard/Mouse", - "Neuron", - "SDL2" + "Keyboard/Mouse" }; if (DEVICE_BLACKLIST.contains(deviceName)) { From 9ca27f267d542d5fbe4ead577d3f8c4a4a3259b4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 21 Jun 2016 16:48:04 -0700 Subject: [PATCH 0686/1237] cap lifetime rather than reject edits from nodes which only have tmp-rez rights --- .../resources/describe-settings.json | 6 +- libraries/entities/src/EntityTree.cpp | 61 +++++++++---------- libraries/entities/src/EntityTree.h | 1 - scripts/system/edit.js | 2 +- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 59fa9c4f3d..7375a0f650 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -384,7 +384,7 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which users or groups can have which domain-wide permissions.", + "help": "Indicate which users or groups can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, @@ -394,7 +394,7 @@ "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 6 } ], @@ -463,7 +463,7 @@ "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 6 } ], diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index ec1f8a50bc..820d97c915 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -148,11 +148,11 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return false; } - if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) { - // we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones - if (properties.lifetimeChanged()) { - qCDebug(entities) << "Refusing disallowed entity lifetime adjustment."; - return false; + if (!canRezPermanentEntities) { + // we don't allow a Node that can't create permanent entities to raise lifetimes on existing ones + if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || properties.getLifetime() > _maxTmpEntityLifetime) { + qCDebug(entities) << "Capping disallowed entity lifetime adjustment."; + properties.setLifetime(_maxTmpEntityLifetime); } } @@ -321,26 +321,9 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return true; } -bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) { - float lifeTime = properties.getLifetime(); - - if (lifeTime == ENTITY_ITEM_IMMORTAL_LIFETIME || lifeTime > _maxTmpEntityLifetime) { - // this is an attempt to rez a permanent or non-temporary entity. - if (!canRez) { - return false; - } - } else { - // this is an attempt to rez a temporary entity. - if (!canRezTmp) { - return false; - } - } - - return true; -} - EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer result = NULL; + EntityItemProperties props = properties; auto nodeList = DependencyManager::get(); if (!nodeList) { @@ -348,16 +331,19 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti return nullptr; } - bool clientOnly = properties.getClientOnly(); + bool clientOnly = props.getClientOnly(); - if (!clientOnly && getIsClient() && - !permissionsAllowRez(properties, nodeList->getThisNodeCanRez(), nodeList->getThisNodeCanRezTmp())) { - // if our Node isn't allowed to create entities in this domain, don't try. - return nullptr; + if (!clientOnly && getIsClient() && !nodeList->getThisNodeCanRez() && nodeList->getThisNodeCanRezTmp()) { + // we are a client which is only allowed to rez temporary entities. cap the lifetime. + if (props.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME) { + props.setLifetime(_maxTmpEntityLifetime); + } else { + props.setLifetime(glm::min(props.getLifetime(), _maxTmpEntityLifetime)); + } } bool recordCreationTime = false; - if (properties.getCreated() == UNKNOWN_CREATED_TIME) { + if (props.getCreated() == UNKNOWN_CREATED_TIME) { // the entity's creation time was not specified in properties, which means this is a NEW entity // and we must record its creation time recordCreationTime = true; @@ -372,8 +358,8 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti } // construct the instance of the entity - EntityTypes::EntityType type = properties.getType(); - result = EntityTypes::constructEntityItem(type, entityID, properties); + EntityTypes::EntityType type = props.getType(); + result = EntityTypes::constructEntityItem(type, entityID, props); if (result) { if (recordCreationTime) { @@ -922,11 +908,20 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c EntityItemID entityItemID; EntityItemProperties properties; startDecode = usecTimestampNow(); - + bool validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, entityItemID, properties); endDecode = usecTimestampNow(); + if (!senderNode->getCanRez() && senderNode->getCanRezTmp()) { + // this node is only allowed to rez temporary entities. cap the lifetime. + if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME) { + properties.setLifetime(_maxTmpEntityLifetime); + } else { + properties.setLifetime(glm::min(properties.getLifetime(), _maxTmpEntityLifetime)); + } + } + // If we got a valid edit packet, then it could be a new entity or it could be an update to // an existing entity... handle appropriately if (validEditPacket) { @@ -955,7 +950,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endUpdate = usecTimestampNow(); _totalUpdates++; } else if (message.getType() == PacketType::EntityAdd) { - if (permissionsAllowRez(properties, senderNode->getCanRez(), senderNode->getCanRezTmp())) { + if (senderNode->getCanRez() || senderNode->getCanRezTmp()) { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); startCreate = usecTimestampNow(); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 8afb8d878f..15daf3bf3c 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -64,7 +64,6 @@ public: void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; } - bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp); /// Implements our type specific root element factory virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 42eddf11c3..b439de2c9d 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -332,7 +332,7 @@ var toolBar = (function() { that.setActive = function(active) { if (active != isActive) { - if (active && !Entities.canAdjustLocks()) { + if (active && !Entities.canRez() && !Entities.canRezTmp()) { Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG); } else { Messages.sendLocalMessage("edit-events", JSON.stringify({ From f0b78eae478e69cbcefe78b1efc9ef75595f924f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 21 Jun 2016 16:48:32 -0700 Subject: [PATCH 0687/1237] fix mini mirror --- interface/src/ui/ApplicationOverlay.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 41d249b635..9330f1f6c4 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -169,8 +169,10 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { float renderRatio = ((float)qApp->getRenderResolutionScale()); auto viewport = qApp->getMirrorViewRect(); - glm::vec2 bottomLeft(viewport.left(), viewport.top() + screenRatio * viewport.height()); - glm::vec2 topRight(viewport.left() + screenRatio * viewport.width(), viewport.top()); + glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); + glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); + bottomLeft *= screenRatio; + topRight *= screenRatio; glm::vec2 texCoordMinCorner(0.0f, 0.0f); glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); From db8190ad4985fd43162119d5b97819f3d93c9440 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 21 Jun 2016 16:56:00 -0700 Subject: [PATCH 0688/1237] fix typo --- BUILD_OSX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD_OSX.md b/BUILD_OSX.md index ab84bc4711..1c9c5a9796 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -19,7 +19,7 @@ For Qt 5.5.1 installed via homebrew, set QT_CMAKE_PREFIX_PATH as follows. export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt55/5.5.1/lib/cmake -Not that these use the versions from homebrew formulae at the time of this writing, and the version in the path will likely change. +Note that these use the versions from homebrew formulae at the time of this writing, and the version in the path will likely change. ###Xcode If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles. From 223f9bda2e6dcf92487ab4dac4cd3b44a762eef7 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 21 Jun 2016 17:03:49 -0700 Subject: [PATCH 0689/1237] gratuitous change to force things. --- scripts/system/controllers/handControllerPointer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index c94325d3c0..374be0d1a1 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars: true, plusplus: true*/ -/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print */ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print*/ // // handControllerPointer.js From e03974181acca42ee1a333ded811e09b2b951709 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 21 Jun 2016 17:13:47 -0700 Subject: [PATCH 0690/1237] reformat operating hours to [[open,close]] --- domain-server/src/DomainMetadata.cpp | 46 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index f18aa8c71b..c5048ea9d8 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -63,12 +63,16 @@ const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { // set up the structure necessary for casting during parsing (see parseHours, esp.) _metadata[USERS] = QVariantMap {}; - _metadata[DESCRIPTORS] = QVariantMap { - { Descriptors::HOURS, QVariantMap { - { Descriptors::Hours::WEEKDAY, QVariantList { QVariantMap{} } }, - { Descriptors::Hours::WEEKEND, QVariantList { QVariantMap{} } } - } } - }; + _metadata[DESCRIPTORS] = QVariantMap { { + Descriptors::HOURS, QVariantMap { + { Descriptors::Hours::WEEKDAY, QVariantList { + QVariantList{ QVariant{}, QVariant{} } } + }, + { Descriptors::Hours::WEEKEND, QVariantList { + QVariantList{ QVariant{}, QVariant{} } } + } + } + } }; assert(dynamic_cast(domainServer)); DomainServer* server = static_cast(domainServer); @@ -96,33 +100,37 @@ QJsonObject DomainMetadata::get(const QString& group) { return QJsonObject::fromVariantMap(_metadata[group].toMap()); } +// merge delta into target +// target should be of the form [ OpenTime, CloseTime ], +// delta should be of the form [ { open: Time, close: Time } ] void parseHours(QVariant delta, QVariant& target) { using Hours = DomainMetadata::Descriptors::Hours; - // hours should be of the form [ { open: Time, close: Time } ] assert(target.canConvert()); auto& targetList = *static_cast(target.data()); // if/when multiple ranges are allowed, this list will need to be iterated - assert(targetList[0].canConvert()); - auto& targetMap = *static_cast(targetList[0].data()); + assert(targetList[0].canConvert()); + auto& hours = *static_cast(targetList[0].data()); - auto deltaMap = delta.toList()[0].toMap(); - if (deltaMap.isEmpty()) { + auto deltaHours = delta.toList()[0].toMap(); + if (deltaHours.isEmpty()) { return; } // merge delta into base - auto open = deltaMap.find(Hours::OPEN); - if (open != deltaMap.end()) { - targetMap[Hours::OPEN] = open.value(); + static const int OPEN_INDEX = 0; + static const int CLOSE_INDEX = 1; + auto open = deltaHours.find(Hours::OPEN); + if (open != deltaHours.end()) { + hours[OPEN_INDEX] = open.value(); } - assert(targetMap[Hours::OPEN].canConvert()); - auto close = deltaMap.find(Hours::CLOSE); - if (close != deltaMap.end()) { - targetMap[Hours::CLOSE] = close.value(); + assert(hours[OPEN_INDEX].canConvert()); + auto close = deltaHours.find(Hours::CLOSE); + if (close != deltaHours.end()) { + hours[CLOSE_INDEX] = close.value(); } - assert(targetMap[Hours::CLOSE].canConvert()); + assert(hours[CLOSE_INDEX].canConvert()); } void DomainMetadata::descriptorsChanged() { From 89c50ab3df3585094c8752e341305c82b8d11dd7 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 21 Jun 2016 16:46:10 -0700 Subject: [PATCH 0691/1237] Update neuron archive in cmakelists --- cmake/externals/neuron/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/externals/neuron/CMakeLists.txt b/cmake/externals/neuron/CMakeLists.txt index 6936725571..76dda8f8c5 100644 --- a/cmake/externals/neuron/CMakeLists.txt +++ b/cmake/externals/neuron/CMakeLists.txt @@ -4,8 +4,8 @@ set(EXTERNAL_NAME neuron) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(NEURON_URL "https://s3.amazonaws.com/hifi-public/dependencies/neuron_datareader_b.12.zip") -set(NEURON_URL_MD5 "0ab54ca04c9cc8094e0fa046c226e574") +set(NEURON_URL "https://s3.amazonaws.com/hifi-public/dependencies/neuron_datareader_b.12.2.zip") +set(NEURON_URL_MD5 "84273ad2200bf86a9279d1f412a822ca") ExternalProject_Add(${EXTERNAL_NAME} URL ${NEURON_URL} From 2a82dddc2b019e53e2b6cfc1b9a7d9f2eb8932d7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 21 Jun 2016 17:36:36 -0700 Subject: [PATCH 0692/1237] Draw attach points as if they were equip-hotspots --- .../system/controllers/handControllerGrab.js | 17 ++++++-- scripts/system/libraries/Xform.js | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 scripts/system/libraries/Xform.js diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 39d85f1224..31f8ebd73f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,9 +10,10 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, Xform */ Script.include("/~/system/libraries/utils.js"); +Script.include("../libraries/Xform.js"); // // add lines where the hand ray picking is happening @@ -879,21 +880,29 @@ function MyController(hand) { var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); var i, l = entities.length; for (i = 0; i < l; i++) { - var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); + var props = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); // does this entity have an attach point? var wearableData = getEntityCustomData("wearable", entities[i], undefined); if (wearableData && wearableData.joints) { var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; if (wearableData.joints[handJointName]) { + var handOffsetPos = wearableData.joints[handJointName][0]; + var handOffsetRot = wearableData.joints[handJointName][1]; + + var handOffsetXform = new Xform(handOffsetRot, handOffsetPos); + var objectXform = new Xform(props.rotation, props.position); + var overlayXform = Xform.mul(objectXform, handOffsetXform.inv()); + // draw the hotspot this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { - position: grabProps.position, + rotation: overlayXform.rot, + position: overlayXform.pos, size: 0.2, color: { red: 90, green: 255, blue: 90 }, alpha: 0.7, solid: true, visible: true, - ignoreRayIntersection: false, + ignoreRayIntersection: true, drawInFront: false })); } diff --git a/scripts/system/libraries/Xform.js b/scripts/system/libraries/Xform.js new file mode 100644 index 0000000000..75051c4983 --- /dev/null +++ b/scripts/system/libraries/Xform.js @@ -0,0 +1,40 @@ +// +// Created by Anthony J. Thibault on 2016/06/21 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// ctor +Xform = function(rot, pos) { + this.rot = rot; + this.pos = pos; +} + +Xform.ident = function() { + return new Xform({x: 0, y: 0, z: 0, w: 1}, {x: 0, y: 0, z: 0}); +}; + +Xform.mul = function(lhs, rhs) { + var rot = Quat.multiply(lhs.rot, rhs.rot); + var pos = Vec3.sum(lhs.pos, Vec3.multiplyQbyV(lhs.rot, rhs.pos)); + return new Xform(rot, pos); +}; + +Xform.prototype.inv = function() { + var invRot = Quat.inverse(this.rot); + var invPos = Vec3.multiply(-1, this.pos); + return new Xform(invRot, Vec3.multiplyQbyV(invRot, invPos)); +}; + +Xform.prototype.mirrorX = function() { + return new Xform({x: this.rot.x, y: -this.rot.y, z: -this.rot.z, w: this.rot.w}, + {x: -this.pos.x, y: this.pos.y, z: this.pos.z}); +}; + +Xform.prototype.toString = function() { + var rot = this.rot; + var pos = this.pos; + return "Xform rot = (" + rot.x + ", " + rot.y + ", " + rot.z + ", " + rot.w + "), pos = (" + pos.x + ", " + pos.y + ", " + pos.z + ")"; +}; From a1844d8c19ab2d28ffc2870829bfc424c57aadb0 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 21 Jun 2016 17:36:41 -0700 Subject: [PATCH 0693/1237] Fix plugin include. (How did this build for me locally?) --- plugins/openvr/src/OpenVrHelpers.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 1bff0073f6..436cd7fc30 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include #include #include "../../interface/src/Menu.h" @@ -347,4 +347,4 @@ controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat result.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); result.angularVelocity = angularVelocity; return result; -} \ No newline at end of file +} From 78a1845afe63c23c41a6f52b203b490b6955c9f6 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 21 Jun 2016 18:13:11 -0700 Subject: [PATCH 0694/1237] fixed resizing of qml overlays when device pixel ratio changes --- interface/src/Application.cpp | 11 +++-------- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 4 ++-- libraries/gl/src/gl/OffscreenQmlSurface.h | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9bcd85fd02..cff36dcf99 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1459,13 +1459,7 @@ void Application::initializeUi() { }); offscreenUi->resume(); connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ - static qreal oldDevicePixelRatio = 0; - qreal devicePixelRatio = getActiveDisplayPlugin()->devicePixelRatio(); - if (devicePixelRatio != oldDevicePixelRatio) { - oldDevicePixelRatio = devicePixelRatio; - qDebug() << "Device pixel ratio changed, triggering GL resize"; - resizeGL(); - } + resizeGL(); }); // This will set up the input plugins UI @@ -1840,7 +1834,8 @@ void Application::resizeGL() { static qreal lastDevicePixelRatio = 0; qreal devicePixelRatio = _window->devicePixelRatio(); if (offscreenUi->size() != fromGlm(uiSize) || devicePixelRatio != lastDevicePixelRatio) { - offscreenUi->resize(fromGlm(uiSize)); + qDebug() << "Device pixel ratio changed, triggering resize"; + offscreenUi->resize(fromGlm(uiSize), true); _offscreenContext->makeCurrent(); lastDevicePixelRatio = devicePixelRatio; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 388ca26482..9ba4b5e134 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -414,7 +414,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _updateTimer.start(); } -void OffscreenQmlSurface::resize(const QSize& newSize_) { +void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { if (!_renderer || !_renderer->_quickWindow) { return; @@ -433,7 +433,7 @@ void OffscreenQmlSurface::resize(const QSize& newSize_) { } QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { + if (newSize == currentSize && !forceResize) { return; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 22a1b99fe6..4014f2ff25 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -38,7 +38,7 @@ public: using MouseTranslator = std::function; virtual void create(QOpenGLContext* context); - void resize(const QSize& size); + void resize(const QSize& size, bool forceResize = false); QSize size() const; Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { From bd28498833d72686b92e31c8dda5dacbd3cc9192 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 21 Jun 2016 18:25:02 -0700 Subject: [PATCH 0695/1237] trying to fix the curvature artefact --- .../render-utils/src/DeferredTransform.slh | 7 ++++ .../src/surfaceGeometry_makeCurvature.slf | 37 +++++++++++++------ .../utilities/render/framebuffer.qml | 3 +- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 20c1cfdd52..520ac77fde 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -50,6 +50,13 @@ mat4 getProjection(int side) { return frameTransform._projection[side]; } +// positive near distance of the projection +float getProjectionNear() { + float planeC = frameTransform._projection[0][2][3] + frameTransform._projection[0][2][2]; + float planeD = frameTransform._projection[0][3][2]; + return planeD / planeC; +} + mat4 getViewInverse() { return frameTransform._viewInverse; } diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index dc222dab4c..5316ef4211 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -118,6 +118,8 @@ void main(void) { // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); + float nearPlaneScale = min(-Zeye / getCurvatureBasisScale(), 1.0); + vec3 worldNormal = getWorldNormal(frameTexcoordPos); // The position of the pixel fragment in Eye space then in world space @@ -125,26 +127,40 @@ void main(void) { vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; // Calculate the perspective scale. - float perspectiveScale = (-getProjScaleEye() / Zeye); - - vec2 viewportScale = perspectiveScale * getInvWidthHeight(); + // Clamp to 0.5 + float perspectiveScale = max(0.5, (-getProjScaleEye() / Zeye)); + // float perspectiveScale = max(0.5, (-getProjectionNear() / Zeye)); // Calculate dF/du and dF/dv - float threshold = getCurvatureDepthThreshold(); - vec2 du = vec2( 1.0f, 0.0f ) * viewportScale.x; - vec2 dv = vec2( 0.0f, 1.0f ) * viewportScale.y; + vec2 viewportScale = perspectiveScale * getInvWidthHeight(); + vec2 du = vec2( viewportScale.x, 0.0f ); + vec2 dv = vec2( 0.0f, viewportScale.y ); vec4 dFdu = vec4(getWorldNormalDiff(frameTexcoordPos, du), getEyeDepthDiff(frameTexcoordPos, du)); vec4 dFdv = vec4(getWorldNormalDiff(frameTexcoordPos, dv), getEyeDepthDiff(frameTexcoordPos, dv)); + + float threshold = getCurvatureDepthThreshold(); dFdu *= step(abs(dFdu.w), threshold); dFdv *= step(abs(dFdv.w), threshold); - // outFragColor = vec4(dFdu.xyz, 1.0); - + //outFragColor = vec4(du.x, du.y, 0.0, 1.0); + // outFragColor = vec4(viewportScale, 0.0, 1.0); + /* if (perspectiveScale < getCurvatureBasisScale()) { + //outFragColor = vec4(0.0, 0.0, 4 * perspectiveScale, 1.0); + } else if (perspectiveScale < 0.5) { + outFragColor = vec4(0.0, 0.0, 2 * perspectiveScale, 1.0); + return; + } else if (perspectiveScale > 1.0) { + outFragColor = vec4(perspectiveScale, 0.0, 0.0, 1.0);s + return; + } else { + outFragColor = vec4(0.0, 0.5 * perspectiveScale, 0.0, 1.0); + return; + }*/ // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) // Eval px, py, pz world positions of the basis centered on the world pos of the fragment - float dist = getCurvatureBasisScale(); + float dist = getCurvatureBasisScale() * nearPlaneScale; vec4 px = vec4(worldPos, 1.0) + vec4(dist, 0.0f, 0.0f, 0.0f); vec4 py = vec4(worldPos, 1.0) + vec4(0.0f, dist, 0.0f, 0.0f); vec4 pz = vec4(worldPos, 1.0) + vec4(0.0f, 0.0f, dist, 0.0f); @@ -183,8 +199,7 @@ void main(void) { return; */ - float pixPerspectiveScaleInv = 1.0 / (perspectiveScale); - //vec2 pixPerspectiveScaleInv = 1.0 / viewportScale; + float pixPerspectiveScaleInv = 1.0 / (perspectiveScale * nearPlaneScale); px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv; py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv; pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv; diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index d74ca03076..48f1550409 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -49,6 +49,7 @@ Column { "Unlit", "Occlusion", "Lightmap", + "Scattering", "Lighting", "Shadow", "Pyramid Depth", @@ -56,7 +57,7 @@ Column { "NormalCurvature", "DiffusedCurvature", "DiffusedNormalCurvature", - "Scattering", + "Debug Scattering", "Ambient Occlusion", "Ambient Occlusion Blurred", "Custom Shader" From a7f30ced29516864b4c8f94367c1b02c3e23e534 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 21 Jun 2016 21:39:40 -0700 Subject: [PATCH 0696/1237] Fix infinite recursion in PluginManager --- libraries/plugins/src/plugins/PluginManager.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index fcd17eeb9a..29658eeb6b 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -145,8 +145,8 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { } } for (auto plugin : displayPlugins) { - connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback); - connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback); + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection); plugin->setContainer(_container); plugin->init(); } @@ -193,9 +193,8 @@ const InputPluginList& PluginManager::getInputPlugins() { } for (auto plugin : inputPlugins) { - UserActivityLogger::getInstance().connectedDevice("input", plugin->getName()); - connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback); - connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback); + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection); plugin->setContainer(_container); plugin->init(); } From 1be30ccce9806ad8900ed97aa630e8f4a70cdde1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 22 Jun 2016 08:24:50 -0700 Subject: [PATCH 0697/1237] Fix enabled_edit firing on disable --- scripts/system/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index bb24a3c7a4..c10f938bde 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -340,7 +340,6 @@ var toolBar = (function() { Messages.sendLocalMessage("edit-events", JSON.stringify({ enabled: active })); - UserActivityLogger.enabledEdit(); isActive = active; if (!isActive) { entityListTool.setVisible(false); @@ -350,6 +349,7 @@ var toolBar = (function() { selectionManager.clearSelections(); cameraManager.disable(); } else { + UserActivityLogger.enabledEdit(); hasShownPropertiesTool = false; entityListTool.setVisible(true); gridTool.setVisible(true); From 890de1bfea6569b8b569d012638a70641bc9a470 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 22 Jun 2016 08:54:58 -0700 Subject: [PATCH 0698/1237] Add went-to user activity back in --- libraries/networking/src/AddressManager.cpp | 13 ++++++++++ .../networking/src/UserActivityLogger.cpp | 25 ++++++++++++++++++- libraries/networking/src/UserActivityLogger.h | 3 ++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 80989acd2c..df9b4094b0 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -23,6 +23,7 @@ #include "AddressManager.h" #include "NodeList.h" #include "NetworkLogging.h" +#include "UserActivityLogger.h" const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; @@ -130,6 +131,10 @@ const JSONCallbackParameters& AddressManager::apiCallbackParameters() { } bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { + static QString URL_TYPE_USER = "user"; + static QString URL_TYPE_DOMAIN_ID = "domain_id"; + static QString URL_TYPE_PLACE = "place"; + static QString URL_TYPE_NETWORK_ADDRESS = "network_address"; if (lookupUrl.scheme() == HIFI_URL_SCHEME) { qCDebug(networking) << "Trying to go to URL" << lookupUrl.toString(); @@ -147,6 +152,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { if (handleUsername(lookupUrl.authority())) { // handled a username for lookup + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_USER, lookupUrl.toString()); + // in case we're failing to connect to where we thought this user was // store their username as previous lookup so we can refresh their location via API _previousLookup = lookupUrl; @@ -157,6 +164,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { if (handleNetworkAddress(lookupUrl.host() + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_NETWORK_ADDRESS, lookupUrl.toString()); + // a network address lookup clears the previous lookup since we don't expect to re-attempt it _previousLookup.clear(); @@ -174,6 +183,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_DOMAIN_ID, lookupUrl.toString()); + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info _previousLookup = lookupUrl; @@ -181,6 +192,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_PLACE, lookupUrl.toString()); + // store this place name as the previous lookup in case we fail to connect and want to refresh API info _previousLookup = lookupUrl; diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 9a5f0541e5..eba4d31167 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -18,6 +18,7 @@ #include "UserActivityLogger.h" #include +#include "AddressManager.h" static const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; @@ -161,12 +162,34 @@ void UserActivityLogger::loadedScript(QString scriptName) { } -void UserActivityLogger::wentTo(QString destinationType, QString destinationName) { +void UserActivityLogger::wentTo(AddressManager::LookupTrigger lookupTrigger, QString destinationType, QString destinationName) { + // Only accept these types of triggers. Other triggers are usually used internally in AddressManager. + QString trigger; + switch (lookupTrigger) { + case AddressManager::UserInput: + trigger = "UserInput"; + break; + case AddressManager::Back: + trigger = "Back"; + break; + case AddressManager::Forward: + trigger = "Forward"; + break; + case AddressManager::StartupFromSettings: + trigger = "StartupFromSettings"; + break; + default: + return; + } + + const QString ACTION_NAME = "went_to"; QJsonObject actionDetails; + const QString TRIGGER_TYPE_KEY = "trigger"; const QString DESTINATION_TYPE_KEY = "destination_type"; const QString DESTINATION_NAME_KEY = "detination_name"; + actionDetails.insert(TRIGGER_TYPE_KEY, trigger); actionDetails.insert(DESTINATION_TYPE_KEY, destinationType); actionDetails.insert(DESTINATION_NAME_KEY, destinationName); diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index c2ab93db2f..b41960a8ad 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -20,6 +20,7 @@ #include #include +#include "AddressManager.h" class UserActivityLogger : public QObject { Q_OBJECT @@ -42,7 +43,7 @@ public slots: void changedDomain(QString domainURL); void connectedDevice(QString typeOfDevice, QString deviceName); void loadedScript(QString scriptName); - void wentTo(QString destinationType, QString destinationName); + void wentTo(AddressManager::LookupTrigger trigger, QString destinationType, QString destinationName); private slots: void requestError(QNetworkReply& errorReply); From 847685d2243bcf6dc50531f2b2788140925cfa7f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 22 Jun 2016 09:06:59 -0700 Subject: [PATCH 0699/1237] Add tracking of away.js --- .../networking/src/UserActivityLoggerScriptingInterface.cpp | 4 ++++ .../networking/src/UserActivityLoggerScriptingInterface.h | 1 + scripts/system/away.js | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index 012a569639..8b22b8ff58 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -20,6 +20,10 @@ void UserActivityLoggerScriptingInterface::openedMarketplace() { logAction("opened_marketplace"); } +void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) { + logAction("toggled_away", { { "is_away", isAway } }); +} + void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) { QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", Q_ARG(QString, action), diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index cad24b1967..9d60d666e2 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -22,6 +22,7 @@ class UserActivityLoggerScriptingInterface : public QObject, public Dependency { public: Q_INVOKABLE void enabledEdit(); Q_INVOKABLE void openedMarketplace(); + Q_INVOKABLE void toggledAway(bool isAway); private: void logAction(QString action, QJsonObject details = {}); diff --git a/scripts/system/away.js b/scripts/system/away.js index 38b0f13c00..8f252cc449 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -158,6 +158,8 @@ function goAway() { return; } + UserActivityLogger.toggledAway(true); + isAway = true; print('going "away"'); wasMuted = AudioDevice.getMuted(); @@ -189,6 +191,9 @@ function goActive() { if (!isAway) { return; } + + UserActivityLogger.toggledAway(false); + isAway = false; print('going "active"'); if (!wasMuted) { From 2621add8e30b8a9d766c74f5eabdfe92acc14d64 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 22 Jun 2016 09:17:28 -0700 Subject: [PATCH 0700/1237] Fix nearby_avatars not tracking --- interface/src/Application.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 319da861dc..d20a62fc7b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1157,9 +1157,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), NEARBY_AVATAR_RADIUS_METERS) - 1; if (nearbyAvatars != lastCountOfNearbyAvatars) { + lastCountOfNearbyAvatars = nearbyAvatars; UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); } }); + checkNearbyAvatarsTimer->start(); // Track user activity event when we receive a mute packet auto onMutedByMixer = []() { From e88b264864807d7f8518ded3c447e8fe2f1fb64e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 22 Jun 2016 09:30:44 -0700 Subject: [PATCH 0701/1237] revert compound hull shape generation --- .../src/RenderableModelEntityItem.cpp | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index ab91edd294..366e365107 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -618,7 +618,6 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. const uint32_t TRIANGLE_STRIDE = 3; const uint32_t QUAD_STRIDE = 4; - Extents extents; foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { @@ -632,18 +631,14 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 p0 = mesh.vertices[meshPart.triangleIndices[j]]; glm::vec3 p1 = mesh.vertices[meshPart.triangleIndices[j + 1]]; glm::vec3 p2 = mesh.vertices[meshPart.triangleIndices[j + 2]]; - if (!pointsInPart.contains(p0)) { pointsInPart << p0; - extents.addPoint(p0); } if (!pointsInPart.contains(p1)) { pointsInPart << p1; - extents.addPoint(p1); } if (!pointsInPart.contains(p2)) { pointsInPart << p2; - extents.addPoint(p2); } } @@ -657,19 +652,15 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 p3 = mesh.vertices[meshPart.quadIndices[j + 3]]; if (!pointsInPart.contains(p0)) { pointsInPart << p0; - extents.addPoint(p0); } if (!pointsInPart.contains(p1)) { pointsInPart << p1; - extents.addPoint(p1); } if (!pointsInPart.contains(p2)) { pointsInPart << p2; - extents.addPoint(p2); } if (!pointsInPart.contains(p3)) { pointsInPart << p3; - extents.addPoint(p3); } } @@ -682,29 +673,23 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } } - glm::vec3 extentsSize = extents.size(); - glm::vec3 scaleToFit = dimensions / extentsSize; - for (int i = 0; i < 3; ++i) { - if (extentsSize[i] < 1.0e-6f) { - scaleToFit[i] = 1.0f; - } - } - // We expect that the collision model will have the same units and will be displaced // from its origin in the same way the visual model is. The visual model has // been centered and probably scaled. We take the scaling and offset which were applied // to the visual model and apply them to the collision model (without regard for the // collision model's extents). + glm::vec3 scaleToFit = dimensions / _model->getFBXGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. - glm::vec3 scaledModelOffset = _model->getOffset() * _model->getScale(); for (int i = 0; i < pointCollection.size(); i++) { for (int j = 0; j < pointCollection[i].size(); j++) { - pointCollection[i][j] = (pointCollection[i][j] * scaleToFit) + scaledModelOffset; + // compensate for registration + pointCollection[i][j] += _model->getOffset(); + // scale so the collision points match the model points + pointCollection[i][j] *= scaleToFit; } } - info.setParams(type, dimensions, _compoundShapeURL); } else if (type == SHAPE_TYPE_STATIC_MESH) { // compute meshPart local transforms From e4dbb5e61a8ff9fd69f9a78039bb8ec95eb51f61 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 22 Jun 2016 09:57:38 -0700 Subject: [PATCH 0702/1237] Make mouse reticle be perpendicular to head. --- .../display-plugins/src/display-plugins/CompositorHelper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index a17fe2fd49..032350a07c 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -425,7 +425,7 @@ glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const d = glm::normalize(overlaySurfacePoint); } reticlePosition = headPosition + (d * getReticleDepth()); - quat reticleOrientation = quat(vec3(-spherical.y, spherical.x, 0.0f)); + quat reticleOrientation = glm::quat_cast(_currentDisplayPlugin->getHeadPose()); vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize * getReticleDepth()); return glm::inverse(eyePose) * createMatFromScaleQuatAndPos(reticleScale, reticleOrientation, reticlePosition); } else { From c62b73bf61e14d745f9ce15125999442c9578d2c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 22 Jun 2016 11:42:57 -0700 Subject: [PATCH 0703/1237] when the entity-server caps a lifetime of an edit from a tmp-rez only interface, bump the lastEdited time forward so that the interface accepts the change back into its local tree --- libraries/entities/src/EntityTree.cpp | 40 ++++++++++----------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 820d97c915..ad0f929e59 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -130,16 +130,13 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityItemProperties properties = origProperties; bool allowLockChange; - bool canRezPermanentEntities; QUuid senderID; if (senderNode.isNull()) { auto nodeList = DependencyManager::get(); allowLockChange = nodeList->isAllowedEditor(); - canRezPermanentEntities = nodeList->getThisNodeCanRez(); senderID = nodeList->getSessionUUID(); } else { allowLockChange = senderNode->isAllowedEditor(); - canRezPermanentEntities = senderNode->getCanRez(); senderID = senderNode->getUUID(); } @@ -148,14 +145,6 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return false; } - if (!canRezPermanentEntities) { - // we don't allow a Node that can't create permanent entities to raise lifetimes on existing ones - if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || properties.getLifetime() > _maxTmpEntityLifetime) { - qCDebug(entities) << "Capping disallowed entity lifetime adjustment."; - properties.setLifetime(_maxTmpEntityLifetime); - } - } - // enforce support for locked entities. If an entity is currently locked, then the only // property we allow you to change is the locked property. if (entity->getLocked()) { @@ -331,17 +320,6 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti return nullptr; } - bool clientOnly = props.getClientOnly(); - - if (!clientOnly && getIsClient() && !nodeList->getThisNodeCanRez() && nodeList->getThisNodeCanRezTmp()) { - // we are a client which is only allowed to rez temporary entities. cap the lifetime. - if (props.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME) { - props.setLifetime(_maxTmpEntityLifetime); - } else { - props.setLifetime(glm::min(props.getLifetime(), _maxTmpEntityLifetime)); - } - } - bool recordCreationTime = false; if (props.getCreated() == UNKNOWN_CREATED_TIME) { // the entity's creation time was not specified in properties, which means this is a NEW entity @@ -876,6 +854,13 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList= 0) { + float value = properties.getLifetime(); + changedProperties[index] = QString("lifetime:") + QString::number((int)value); + } + } } int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, @@ -913,12 +898,15 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c entityItemID, properties); endDecode = usecTimestampNow(); + const quint64 LAST_EDITED_SERVERSIDE_BUMP = 10000; // usec if (!senderNode->getCanRez() && senderNode->getCanRezTmp()) { - // this node is only allowed to rez temporary entities. cap the lifetime. - if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME) { + // this node is only allowed to rez temporary entities. if need be, cap the lifetime. + if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || + properties.getLifetime() > _maxTmpEntityLifetime) { properties.setLifetime(_maxTmpEntityLifetime); - } else { - properties.setLifetime(glm::min(properties.getLifetime(), _maxTmpEntityLifetime)); + // also bump up the lastEdited time of the properties so that the interface that created this edit + // will accept our adjustment to lifetime back into its own entity-tree. + properties.setLastEdited(properties.getLastEdited() + LAST_EDITED_SERVERSIDE_BUMP); } } From 2233b5ba64f7c2339a802ccbb95dcc1fd831096c Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 22 Jun 2016 11:57:41 -0700 Subject: [PATCH 0704/1237] Add user activity tracking for OculusLegacyDisplayPlugin --- plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp | 6 ++++++ plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h | 2 ++ 2 files changed, 8 insertions(+) diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 4aadb890d5..efd9cbec88 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -36,6 +36,12 @@ const QString OculusLegacyDisplayPlugin::NAME("Oculus Rift"); OculusLegacyDisplayPlugin::OculusLegacyDisplayPlugin() { } +void OculusDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + void OculusLegacyDisplayPlugin::resetSensors() { ovrHmd_RecenterPose(_hmd); } diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 453a6f9168..6ffc1a7f44 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -23,6 +23,8 @@ public: bool isSupported() const override; const QString& getName() const override { return NAME; } + void init() override; + int getHmdScreen() const override; // Stereo specific methods From b6b73af2b487b10e9cabe85299f498ef0b683df2 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 17 Jun 2016 14:27:45 -0700 Subject: [PATCH 0705/1237] Clean up domain-server setup --- domain-server/src/DomainServer.cpp | 70 +++++++++++++----------------- domain-server/src/DomainServer.h | 2 +- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 223cab61da..a4b57226ed 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -104,23 +104,25 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions, &_gatekeeper, &DomainGatekeeper::updateNodePermissions); - if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { - // we either read a certificate and private key or were not passed one - // and completed login or did not need to - - qDebug() << "Setting up LimitedNodeList and assignments."; - setupNodeListAndAssignments(); - - // setup automatic networking settings with data server - setupAutomaticNetworking(); - - // preload some user public keys so they can connect on first request - _gatekeeper.preloadAllowedUserPublicKeys(); - - optionallyGetTemporaryName(args); + // if we were given a certificate/private key or oauth credentials they must succeed + if (!(optionallyReadX509KeyAndCertificate() && optionallySetupOAuth())) { + return; } + qDebug() << "Setting up domain-server"; + setupNodeListAndAssignments(); + setupAutomaticNetworking(); + _gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request + _metadata = new DomainMetadata(this); + + // check for the temporary name parameter + const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; + if (args.contains(GET_TEMPORARY_NAME_SWITCH)) { + getTemporaryName(); + } + + qDebug() << "domain-server" << nullptr << "is running"; } DomainServer::~DomainServer() { @@ -233,34 +235,25 @@ bool DomainServer::optionallySetupOAuth() { static const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; -void DomainServer::optionallyGetTemporaryName(const QStringList& arguments) { - // check for the temporary name parameter - const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; +void DomainServer::getTemporaryName(bool force) { + // check if we already have a domain ID + const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); - if (arguments.contains(GET_TEMPORARY_NAME_SWITCH)) { - - // make sure we don't already have a domain ID - const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); - if (idValueVariant) { - qWarning() << "Temporary domain name requested but a domain ID is already present in domain-server settings." - << "Will not request temporary name."; + if (idValueVariant) { + qWarning() << "Temporary domain name requested but a domain ID is already present in domain-server settings."; + if (force) { + qWarning() << "Temporary domain name will be requested to replace it."; + } else { + qWarning() << "Temporary domain name will not be requested."; return; } - - // we've been asked to grab a temporary name from the API - // so fire off that request now - auto accountManager = DependencyManager::get(); - - // get callbacks for temporary domain result - JSONCallbackParameters callbackParameters; - callbackParameters.jsonCallbackReceiver = this; - callbackParameters.jsonCallbackMethod = "handleTempDomainSuccess"; - callbackParameters.errorCallbackReceiver = this; - callbackParameters.errorCallbackMethod = "handleTempDomainError"; - - accountManager->sendRequest("/api/v1/domains/temporary", AccountManagerAuth::None, - QNetworkAccessManager::PostOperation, callbackParameters); } + + // request a temporary name from the metaverse + auto accountManager = DependencyManager::get(); + JSONCallbackParameters callbackParameters { this, "handleTempDomainSuccess", this, "handleTempDomainError" }; + accountManager->sendRequest("/api/v1/domains/temporary", AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParameters); } void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { @@ -333,7 +326,6 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { - const QString CUSTOM_LOCAL_PORT_OPTION = "metaverse.local_port"; QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index c742dbc9b3..8a25591605 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -100,7 +100,7 @@ private: bool optionallySetupOAuth(); bool optionallyReadX509KeyAndCertificate(); - void optionallyGetTemporaryName(const QStringList& arguments); + void getTemporaryName(bool force = false); static bool packetVersionMatch(const udt::Packet& packet); From 3a36b0a2e5307094c106c73d4ddfb3cb5a289838 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 17 Jun 2016 18:31:23 -0700 Subject: [PATCH 0706/1237] add temporary domain info to account info --- libraries/networking/src/AccountManager.cpp | 5 +++++ libraries/networking/src/AccountManager.h | 3 +++ libraries/networking/src/DataServerAccountInfo.cpp | 13 ++++++++++--- libraries/networking/src/DataServerAccountInfo.h | 8 +++++++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 26b3801ec1..ac6d0cd4a0 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -471,6 +471,11 @@ void AccountManager::setAccessTokenForCurrentAuthURL(const QString& accessToken) persistAccountToFile(); } +void AccountManager::setTemporaryDomain(const QUuid& domainID, const QString& key) { + _accountInfo.setTemporaryDomain(domainID, key); + persistAccountToFile(); +} + void AccountManager::requestAccessToken(const QString& login, const QString& password) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 4803d2625f..dc3315eb45 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -88,6 +88,9 @@ public: void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } + void setTemporaryDomain(const QUuid& domainID, const QString& key); + const QString& getTemporaryDomainKey(const QUuid& domainID) { return _accountInfo.getTemporaryDomainKey(domainID); } + public slots: void requestAccessToken(const QString& login, const QString& password); diff --git a/libraries/networking/src/DataServerAccountInfo.cpp b/libraries/networking/src/DataServerAccountInfo.cpp index 211bfdccfa..6c6f3eb90c 100644 --- a/libraries/networking/src/DataServerAccountInfo.cpp +++ b/libraries/networking/src/DataServerAccountInfo.cpp @@ -25,6 +25,8 @@ #pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif +const QString DataServerAccountInfo::EMPTY_KEY = QString(); + DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherInfo) : QObject() { _accessToken = otherInfo._accessToken; _username = otherInfo._username; @@ -33,6 +35,8 @@ DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherI _walletID = otherInfo._walletID; _privateKey = otherInfo._privateKey; _domainID = otherInfo._domainID; + _temporaryDomainID = otherInfo._temporaryDomainID; + _temporaryDomainApiKey = otherInfo._temporaryDomainApiKey; } DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountInfo& otherInfo) { @@ -51,6 +55,8 @@ void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) { swap(_walletID, otherInfo._walletID); swap(_privateKey, otherInfo._privateKey); swap(_domainID, otherInfo._domainID); + swap(_temporaryDomainID, otherInfo._temporaryDomainID); + swap(_temporaryDomainApiKey, otherInfo._temporaryDomainApiKey); } void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) { @@ -145,13 +151,14 @@ QByteArray DataServerAccountInfo::signPlaintext(const QByteArray& plaintext) { QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) { out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey - << info._walletID << info._privateKey << info._domainID; - + << info._walletID << info._privateKey << info._domainID + << info._temporaryDomainID << info._temporaryDomainApiKey; return out; } QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info) { in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey - >> info._walletID >> info._privateKey >> info._domainID; + >> info._walletID >> info._privateKey >> info._domainID + >> info._temporaryDomainID >> info._temporaryDomainApiKey; return in; } diff --git a/libraries/networking/src/DataServerAccountInfo.h b/libraries/networking/src/DataServerAccountInfo.h index 6ee726efde..8cb416cf34 100644 --- a/libraries/networking/src/DataServerAccountInfo.h +++ b/libraries/networking/src/DataServerAccountInfo.h @@ -22,6 +22,7 @@ const float SATOSHIS_PER_CREDIT = 100000000.0f; class DataServerAccountInfo : public QObject { Q_OBJECT + const static QString EMPTY_KEY; public: DataServerAccountInfo() {}; DataServerAccountInfo(const DataServerAccountInfo& otherInfo); @@ -52,6 +53,9 @@ public: void setDomainID(const QUuid& domainID) { _domainID = domainID; } const QUuid& getDomainID() const { return _domainID; } + void setTemporaryDomain(const QUuid& domainID, const QString& key) { _temporaryDomainID = domainID; _temporaryDomainApiKey = key; } + const QString& getTemporaryDomainKey(const QUuid& domainID) { return domainID == _temporaryDomainID ? _temporaryDomainApiKey : EMPTY_KEY; } + bool hasProfile() const; void setProfileInfoFromJSON(const QJsonObject& jsonObject); @@ -67,7 +71,9 @@ private: QString _xmppPassword; QString _discourseApiKey; QUuid _walletID; - QUuid _domainID; // if this holds account info for a domain, this holds the ID of that domain + QUuid _domainID; + QUuid _temporaryDomainID; + QString _temporaryDomainApiKey; QByteArray _privateKey; }; From eebf8e91c6bdcca2308e64b75a4b8ee97b1b970b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 17 Jun 2016 19:02:27 -0700 Subject: [PATCH 0707/1237] add api_key to domains/public_key updates --- libraries/networking/src/AccountManager.cpp | 27 +++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index ac6d0cd4a0..38f33da6ce 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -652,22 +652,33 @@ void AccountManager::processGeneratedKeypair() { const QString DOMAIN_PUBLIC_KEY_UPDATE_PATH = "api/v1/domains/%1/public_key"; QString uploadPath; - if (keypairGenerator->getDomainID().isNull()) { + const auto& domainID = keypairGenerator->getDomainID(); + if (domainID.isNull()) { uploadPath = USER_PUBLIC_KEY_UPDATE_PATH; } else { - uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(keypairGenerator->getDomainID())); + uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(domainID)); } // setup a multipart upload to send up the public key QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - QHttpPart keyPart; - keyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - keyPart.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); - keyPart.setBody(keypairGenerator->getPublicKey()); + QHttpPart publicKeyPart; + publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - requestMultiPart->append(keyPart); + publicKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); + publicKeyPart.setBody(keypairGenerator->getPublicKey()); + requestMultiPart->append(publicKeyPart); + + if (!domainID.isNull()) { + const auto& key = getTemporaryDomainKey(domainID); + QHttpPart apiKeyPart; + publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); + apiKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"api_key\"")); + apiKeyPart.setBody(key.toUtf8()); + requestMultiPart->append(apiKeyPart); + } // setup callback parameters so we know once the keypair upload has succeeded or failed JSONCallbackParameters callbackParameters; From a6115cba6e981f17477edd98a0b6695770cc5c19 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 17 Jun 2016 19:04:03 -0700 Subject: [PATCH 0708/1237] update temporary domains to use api_key --- domain-server/src/DomainServer.cpp | 149 +++++++++++++++++++---------- domain-server/src/DomainServer.h | 10 +- 2 files changed, 105 insertions(+), 54 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index a4b57226ed..df1697af28 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -76,6 +76,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : setApplicationVersion(BuildInfo::VERSION); QSettings::setDefaultFormat(QSettings::IniFormat); + qDebug() << "Setting up domain-server"; + // make sure we have a fresh AccountManager instance // (need this since domain-server can restart itself and maintain static variables) DependencyManager::set(); @@ -109,20 +111,26 @@ DomainServer::DomainServer(int argc, char* argv[]) : return; } - qDebug() << "Setting up domain-server"; - setupNodeListAndAssignments(); - setupAutomaticNetworking(); - _gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request - - _metadata = new DomainMetadata(this); - // check for the temporary name parameter const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; if (args.contains(GET_TEMPORARY_NAME_SWITCH)) { getTemporaryName(); } - qDebug() << "domain-server" << nullptr << "is running"; + setupNodeListAndAssignments(); + setupAutomaticNetworking(); + if (!getID().isNull()) { + setupHeartbeatToMetaverse(); + // send the first heartbeat immediately + sendHeartbeatToMetaverse(); + } + + _gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request + + _metadata = new DomainMetadata(this); + + + qDebug() << "domain-server is running"; } DomainServer::~DomainServer() { @@ -150,6 +158,10 @@ void DomainServer::restart() { exit(DomainServer::EXIT_CODE_REBOOT); } +const QUuid& DomainServer::getID() { + return DependencyManager::get()->getSessionUUID(); +} + bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_CERTIFICATE_OPTION = "cert"; const QString X509_PRIVATE_KEY_OPTION = "key"; @@ -264,11 +276,13 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { static const QString DOMAIN_KEY = "domain"; static const QString ID_KEY = "id"; static const QString NAME_KEY = "name"; + static const QString KEY_KEY = "api_key"; auto domainObject = jsonObject[DATA_KEY].toObject()[DOMAIN_KEY].toObject(); if (!domainObject.isEmpty()) { auto id = domainObject[ID_KEY].toString(); auto name = domainObject[NAME_KEY].toString(); + auto key = domainObject[KEY_KEY].toString(); qInfo() << "Received new temporary domain name" << name; qDebug() << "The temporary domain ID is" << id; @@ -284,9 +298,13 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { // change our domain ID immediately DependencyManager::get()->setSessionUUID(QUuid { id }); - // change our automatic networking settings so that we're communicating with the ICE server - setupICEHeartbeatForFullNetworking(); + // store the new token to the account info + auto accountManager = DependencyManager::get(); + accountManager->setTemporaryDomain(id, key); + // update our heartbeats to use the correct id + setupICEHeartbeatForFullNetworking(); + setupHeartbeatToMetaverse(); } else { qWarning() << "There were problems parsing the API response containing a temporary domain name. Please try again" << "via domain-server relaunch or from the domain-server settings."; @@ -325,7 +343,7 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { } -void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { +void DomainServer::setupNodeListAndAssignments() { const QString CUSTOM_LOCAL_PORT_OPTION = "metaverse.local_port"; QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); @@ -450,29 +468,20 @@ bool DomainServer::resetAccountManagerAccessToken() { } void DomainServer::setupAutomaticNetworking() { - auto nodeList = DependencyManager::get(); - + qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; _automaticNetworkingSetting = _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString(); + auto nodeList = DependencyManager::get(); + const QUuid& domainID = getID(); + if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { setupICEHeartbeatForFullNetworking(); } - _hasAccessToken = resetAccountManagerAccessToken(); - - if (!_hasAccessToken) { - qDebug() << "Will not send heartbeat to Metaverse API without an access token."; - qDebug() << "If this is not a temporary domain add an access token to your config file or via the web interface."; - - return; - } - if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE || _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { - const QUuid& domainID = nodeList->getSessionUUID(); - if (!domainID.isNull()) { qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID" << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString(); @@ -484,9 +493,6 @@ void DomainServer::setupAutomaticNetworking() { // have the LNL enable public socket updating via STUN nodeList->startSTUNPublicSocketUpdate(); - } else { - // send our heartbeat to data server so it knows what our network settings are - sendHeartbeatToMetaverse(); } } else { qDebug() << "Cannot enable domain-server automatic networking without a domain ID." @@ -494,18 +500,20 @@ void DomainServer::setupAutomaticNetworking() { return; } - } else { - sendHeartbeatToMetaverse(); } +} - qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; - - // no matter the auto networking settings we should heartbeat to the data-server every 15s +void DomainServer::setupHeartbeatToMetaverse() { + // heartbeat to the data-server every 15s const int DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS = 15 * 1000; - QTimer* dataHeartbeatTimer = new QTimer(this); - connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToMetaverse())); - dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); + if (!_metaverseHeartbeatTimer) { + // setup a timer to heartbeat with the metaverse-server + _metaverseHeartbeatTimer = new QTimer { this }; + connect(_metaverseHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToMetaverse())); + // do not send a heartbeat immediately - this avoids flooding if the heartbeat fails with a 401 + _metaverseHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); + } } void DomainServer::setupICEHeartbeatForFullNetworking() { @@ -524,22 +532,21 @@ void DomainServer::setupICEHeartbeatForFullNetworking() { limitedNodeList->startSTUNPublicSocketUpdate(); // to send ICE heartbeats we'd better have a private key locally with an uploaded public key - auto accountManager = DependencyManager::get(); - auto domainID = accountManager->getAccountInfo().getDomainID(); - // if we have an access token and we don't have a private key or the current domain ID has changed // we should generate a new keypair - if (!accountManager->getAccountInfo().hasPrivateKey() || domainID != limitedNodeList->getSessionUUID()) { - accountManager->generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + auto accountManager = DependencyManager::get(); + if (!accountManager->getAccountInfo().hasPrivateKey() || accountManager->getAccountInfo().getDomainID() != getID()) { + accountManager->generateNewDomainKeypair(getID()); } // hookup to the signal from account manager that tells us when keypair is available connect(accountManager.data(), &AccountManager::newKeypair, this, &DomainServer::handleKeypairChange); if (!_iceHeartbeatTimer) { - // setup a timer to heartbeat with the ice-server every so often + // setup a timer to heartbeat with the ice-server _iceHeartbeatTimer = new QTimer { this }; connect(_iceHeartbeatTimer, &QTimer::timeout, this, &DomainServer::sendHeartbeatToIceServer); + sendHeartbeatToIceServer(); _iceHeartbeatTimer->start(ICE_HEARBEAT_INTERVAL_MSECS); } } @@ -1067,9 +1074,6 @@ void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) } void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { - auto nodeList = DependencyManager::get(); - const QUuid& domainID = nodeList->getSessionUUID(); - // Setup the domain object to send to the data server QJsonObject domainObject; @@ -1088,6 +1092,13 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain; + const auto& temporaryDomainKey = DependencyManager::get()->getTemporaryDomainKey(getID()); + if (!temporaryDomainKey.isEmpty()) { + // add the temporary domain token + const QString KEY_KEY = "api_key"; + domainObject[KEY_KEY] = temporaryDomainKey; + } + if (_metadata) { // Add the metadata to the heartbeat static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; @@ -1097,18 +1108,47 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), - AccountManagerAuth::Required, + DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())), + AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, - JSONCallbackParameters(), + JSONCallbackParameters(nullptr, QString(), this, "handleMetaverseHeartbeatError"), domainUpdateJSON.toUtf8()); } +void DomainServer::handleMetaverseHeartbeatError(QNetworkReply& requestReply) { + if (!_metaverseHeartbeatTimer) { + // avoid rehandling errors from the same issue + return; + } + + // if we have a temporary domain with a bad token, we will get a 401 + if (requestReply.error() == QNetworkReply::NetworkError::AuthenticationRequiredError) { + static const QString DATA_KEY = "data"; + static const QString TOKEN_KEY = "api_key"; + + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY]; + + if (!tokenFailure.isNull()) { + qWarning() << "Temporary domain name lacks a valid API key, and is being reset."; + + // halt heartbeats until we have a token + _metaverseHeartbeatTimer->deleteLater(); + _metaverseHeartbeatTimer = nullptr; + + // give up eventually to avoid flooding traffic + static const int MAX_ATTEMPTS = 5; + static int attempt = 0; + if (++attempt < MAX_ATTEMPTS) { + // get a new temporary name and token + getTemporaryName(true); + } + } + } +} + void DomainServer::sendICEServerAddressToMetaverseAPI() { if (!_iceServerSocket.isNull()) { - auto nodeList = DependencyManager::get(); - const QUuid& domainID = nodeList->getSessionUUID(); - const QString ICE_SERVER_ADDRESS = "ice_server_address"; QJsonObject domainObject; @@ -1116,6 +1156,13 @@ void DomainServer::sendICEServerAddressToMetaverseAPI() { // we're using full automatic networking and we have a current ice-server socket, use that now domainObject[ICE_SERVER_ADDRESS] = _iceServerSocket.getAddress().toString(); + const auto& temporaryDomainKey = DependencyManager::get()->getTemporaryDomainKey(getID()); + if (!temporaryDomainKey.isEmpty()) { + // add the temporary domain token + const QString KEY_KEY = "api_key"; + domainObject[KEY_KEY] = temporaryDomainKey; + } + QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); // make sure we hear about failure so we can retry @@ -1127,7 +1174,7 @@ void DomainServer::sendICEServerAddressToMetaverseAPI() { static const QString DOMAIN_ICE_ADDRESS_UPDATE = "/api/v1/domains/%1/ice_server_address"; - DependencyManager::get()->sendRequest(DOMAIN_ICE_ADDRESS_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), + DependencyManager::get()->sendRequest(DOMAIN_ICE_ADDRESS_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())), AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 8a25591605..138cb9ca2d 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -80,6 +80,8 @@ private slots: void handleTempDomainSuccess(QNetworkReply& requestReply); void handleTempDomainError(QNetworkReply& requestReply); + void handleMetaverseHeartbeatError(QNetworkReply& requestReply); + void queuedQuit(QString quitMessage, int exitCode); void handleKeypairChange(); @@ -96,7 +98,9 @@ signals: void userDisconnected(); private: - void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid()); + const QUuid& getID(); + + void setupNodeListAndAssignments(); bool optionallySetupOAuth(); bool optionallyReadX509KeyAndCertificate(); @@ -108,6 +112,7 @@ private: void setupAutomaticNetworking(); void setupICEHeartbeatForFullNetworking(); + void setupHeartbeatToMetaverse(); void sendHeartbeatToMetaverse(const QString& networkAddress); void randomizeICEServerAddress(bool shouldTriggerHostLookup); @@ -178,6 +183,7 @@ private: // These will be parented to this, they are not dangling DomainMetadata* _metadata { nullptr }; QTimer* _iceHeartbeatTimer { nullptr }; + QTimer* _metaverseHeartbeatTimer { nullptr }; QList _iceServerAddresses; QSet _failedIceServerAddresses; @@ -186,8 +192,6 @@ private: int _numHeartbeatDenials { 0 }; bool _connectedToICEServer { false }; - bool _hasAccessToken { false }; - friend class DomainGatekeeper; friend class DomainMetadata; }; From 4a30d549add061e408c95d692e2098ef6eb70207 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 17 Jun 2016 19:26:22 -0700 Subject: [PATCH 0709/1237] force temp domain reset on 404 too (401 already) --- domain-server/src/DomainServer.cpp | 51 +++++++++++++++++++----------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index df1697af28..192c0d26e6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1121,29 +1121,42 @@ void DomainServer::handleMetaverseHeartbeatError(QNetworkReply& requestReply) { return; } - // if we have a temporary domain with a bad token, we will get a 401 - if (requestReply.error() == QNetworkReply::NetworkError::AuthenticationRequiredError) { - static const QString DATA_KEY = "data"; - static const QString TOKEN_KEY = "api_key"; + // check if we need to force a new temporary domain name + switch (requestReply.error()) { + // if we have a temporary domain with a bad token, we get a 401 + case QNetworkReply::NetworkError::AuthenticationRequiredError: { + static const QString DATA_KEY = "data"; + static const QString TOKEN_KEY = "api_key"; - QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); - auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY]; + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY]; - if (!tokenFailure.isNull()) { - qWarning() << "Temporary domain name lacks a valid API key, and is being reset."; - - // halt heartbeats until we have a token - _metaverseHeartbeatTimer->deleteLater(); - _metaverseHeartbeatTimer = nullptr; - - // give up eventually to avoid flooding traffic - static const int MAX_ATTEMPTS = 5; - static int attempt = 0; - if (++attempt < MAX_ATTEMPTS) { - // get a new temporary name and token - getTemporaryName(true); + if (!tokenFailure.isNull()) { + qWarning() << "Temporary domain name lacks a valid API key, and is being reset."; } + break; } + // if the domain does not (or no longer) exists, we get a 404 + case QNetworkReply::NetworkError::ContentNotFoundError: + qWarning() << "Domain not found, getting a new temporary domain."; + break; + // otherwise, we erred on something else, and should not force a temporary domain + default: + return; + } + + // halt heartbeats until we have a token + _metaverseHeartbeatTimer->deleteLater(); + _metaverseHeartbeatTimer = nullptr; + + // give up eventually to avoid flooding traffic + static const int MAX_ATTEMPTS = 5; + static int attempt = 0; + if (++attempt < MAX_ATTEMPTS) { + // get a new temporary name and token + getTemporaryName(true); + } else { + qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart."; } } From 9ff742c97c4c400a0eea97ed366389d8005843c9 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 22 Jun 2016 12:09:12 -0700 Subject: [PATCH 0710/1237] Fix typo --- plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index efd9cbec88..f1a803ee19 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -36,7 +36,7 @@ const QString OculusLegacyDisplayPlugin::NAME("Oculus Rift"); OculusLegacyDisplayPlugin::OculusLegacyDisplayPlugin() { } -void OculusDisplayPlugin::init() { +void OculusLegacyDisplayPlugin::init() { Plugin::init(); emit deviceConnected(getName()); From d3735322a5c9baa97312d4504069a476b18a16f3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 22 Jun 2016 13:03:13 -0700 Subject: [PATCH 0711/1237] Whitespace. --- interface/src/scripting/ToolbarScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/ToolbarScriptingInterface.h b/interface/src/scripting/ToolbarScriptingInterface.h index d3706cb6e1..9379284e55 100644 --- a/interface/src/scripting/ToolbarScriptingInterface.h +++ b/interface/src/scripting/ToolbarScriptingInterface.h @@ -20,7 +20,7 @@ class ToolbarProxy; class ToolbarScriptingInterface : public QObject, public Dependency { Q_OBJECT public: - Q_INVOKABLE QObject* getToolbar(const QString& toolbarId); + Q_INVOKABLE QObject* getToolbar(const QString& toolbarId); }; #endif // hifi_ToolbarScriptingInterface_h From d4cfe6256a93884bf13f6869f1c2192bd6e4c442 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 22 Jun 2016 14:46:08 -0700 Subject: [PATCH 0712/1237] don't render mini mirror if rendering fullscreen mirror --- interface/src/ui/ApplicationOverlay.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 9330f1f6c4..6d5df31766 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -150,7 +150,8 @@ void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { } void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { - if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { + if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && + !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { gpu::Batch& batch = *renderArgs->_batch; auto geometryCache = DependencyManager::get(); From 8009d23f7007fbf311ab80f593346d4cf5658764 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 22 Jun 2016 15:20:45 -0700 Subject: [PATCH 0713/1237] more STATIC_MESH and dynamic overlap prevention --- libraries/entities/src/EntityItem.cpp | 12 ++-- libraries/entities/src/EntityItem.h | 4 +- libraries/entities/src/ModelEntityItem.cpp | 57 ++++++++++++------- libraries/entities/src/ModelEntityItem.h | 6 +- .../entities/src/ParticleEffectEntityItem.cpp | 6 +- .../entities/src/ParticleEffectEntityItem.h | 4 +- libraries/entities/src/ZoneEntityItem.cpp | 4 +- libraries/entities/src/ZoneEntityItem.h | 6 +- 8 files changed, 61 insertions(+), 38 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 2abb9f12e2..f0a4d40860 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1693,9 +1693,13 @@ void EntityItem::updateCollisionMask(uint8_t value) { } void EntityItem::updateDynamic(bool value) { - if (_dynamic != value) { - if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { - _dynamic = false; + if (getDynamic() != value) { + // dynamic and STATIC_MESH are incompatible so we check for that case + if (value && getShapeType() == SHAPE_TYPE_STATIC_MESH) { + if (_dynamic) { + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } } else { _dynamic = value; _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; @@ -1749,7 +1753,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask group = BULLET_COLLISION_GROUP_COLLISIONLESS; mask = 0; } else { - if (_dynamic) { + if (getDynamic()) { group = BULLET_COLLISION_GROUP_DYNAMIC; } else if (isMovingRelativeToParent() || hasActions()) { group = BULLET_COLLISION_GROUP_KINEMATIC; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4a691462ab..9fa13690f1 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -283,7 +283,7 @@ public: void computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask) const; - bool getDynamic() const { return _dynamic; } + bool getDynamic() const { return SHAPE_TYPE_STATIC_MESH == getShapeType() ? false : _dynamic; } void setDynamic(bool value) { _dynamic = value; } virtual bool shouldBePhysical() const { return false; } @@ -348,7 +348,7 @@ public: void updateDynamic(bool value); void updateLifetime(float value); void updateCreated(uint64_t value); - virtual void updateShapeType(ShapeType type) { /* do nothing */ } + virtual void setShapeType(ShapeType type) { /* do nothing */ } uint32_t getDirtyFlags() const { return _dirtyFlags; } void clearDirtyFlags(uint32_t mask = 0xffffffff) { _dirtyFlags &= ~mask; } diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 40faf2c3c3..8e925b2f79 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -77,7 +77,7 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setModelURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotationsSet, setJointRotationsSet); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotations, setJointRotations); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslationsSet, setJointTranslationsSet); @@ -145,7 +145,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, dataAt += bytesFromAnimation; } - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); if (animationPropertiesChanged) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; @@ -257,37 +257,54 @@ void ModelEntityItem::debugDump() const { qCDebug(entities) << " compound shape URL:" << getCompoundShapeURL(); } -void ModelEntityItem::updateShapeType(ShapeType type) { - // BEGIN_TEMPORARY_WORKAROUND - // we have allowed inconsistent ShapeType's to be stored in SVO files in the past (this was a bug) - // but we are now enforcing the entity properties to be consistent. To make the possible we're - // introducing a temporary workaround: we will ignore ShapeType updates that conflict with the - // _compoundShapeURL. - if (hasCompoundShapeURL()) { - type = SHAPE_TYPE_COMPOUND; - } - // END_TEMPORARY_WORKAROUND - +void ModelEntityItem::setShapeType(ShapeType type) { if (type != _shapeType) { + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic and STATIC_MESH are incompatible + // since the shape is being set here we clear the dynamic bit + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } _shapeType = type; _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } } -// virtual ShapeType ModelEntityItem::getShapeType() const { - if (_shapeType == SHAPE_TYPE_COMPOUND) { - return hasCompoundShapeURL() ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; - } else { - return _shapeType; + return computeTrueShapeType(); +} + +ShapeType ModelEntityItem::computeTrueShapeType() const { + ShapeType type = _shapeType; + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic is incompatible with STATIC_MESH + // shouldn't fall in here but just in case --> fall back to COMPOUND + type = SHAPE_TYPE_COMPOUND; + } + if (type == SHAPE_TYPE_COMPOUND && !hasCompoundShapeURL()) { + // no compoundURL set --> fall back to NONE + type = SHAPE_TYPE_NONE; + } + return type; +} + +void ModelEntityItem::setModelURL(const QString& url) { + if (_modelURL != url) { + _modelURL = url; + _parsedModelURL = QUrl(url); + if (_shapeType == SHAPE_TYPE_STATIC_MESH) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } } void ModelEntityItem::setCompoundShapeURL(const QString& url) { if (_compoundShapeURL != url) { + ShapeType oldType = computeTrueShapeType(); _compoundShapeURL = url; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - _shapeType = _compoundShapeURL.isEmpty() ? SHAPE_TYPE_NONE : SHAPE_TYPE_COMPOUND; + if (oldType != computeTrueShapeType()) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } } diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 29730bf4df..7b7edaf945 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -50,9 +50,10 @@ public: virtual bool needsToCallUpdate() const; virtual void debugDump() const; - void updateShapeType(ShapeType type); + void setShapeType(ShapeType type); virtual ShapeType getShapeType() const; + // TODO: Move these to subclasses, or other appropriate abstraction // getters/setters applicable to models and particles @@ -76,7 +77,7 @@ public: } // model related properties - virtual void setModelURL(const QString& url) { _modelURL = url; _parsedModelURL = QUrl(url); } + virtual void setModelURL(const QString& url); virtual void setCompoundShapeURL(const QString& url); // Animation related items... @@ -130,6 +131,7 @@ public: private: void setAnimationSettings(const QString& value); // only called for old bitstream format + ShapeType computeTrueShapeType() const; protected: // these are used: diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index a7bd0038e6..c501737146 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -342,7 +342,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(maxParticles, setMaxParticles); SET_ENTITY_PROPERTY_FROM_PROPERTIES(lifespan, setLifespan); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isEmitting, setIsEmitting); @@ -406,7 +406,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch READ_ENTITY_PROPERTY(PROP_EMITTING_PARTICLES, bool, setIsEmitting); } - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); READ_ENTITY_PROPERTY(PROP_MAX_PARTICLES, quint32, setMaxParticles); READ_ENTITY_PROPERTY(PROP_LIFESPAN, float, setLifespan); READ_ENTITY_PROPERTY(PROP_EMIT_RATE, float, setEmitRate); @@ -584,7 +584,7 @@ void ParticleEffectEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } -void ParticleEffectEntityItem::updateShapeType(ShapeType type) { +void ParticleEffectEntityItem::setShapeType(ShapeType type) { if (type != _shapeType) { _shapeType = type; _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 4538a1bb43..777f3b6cf6 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -95,8 +95,8 @@ public: void setAlphaSpread(float alphaSpread); float getAlphaSpread() const { return _alphaSpread; } - void updateShapeType(ShapeType type); - virtual ShapeType getShapeType() const { return _shapeType; } + void setShapeType(ShapeType type) override; + virtual ShapeType getShapeType() const override { return _shapeType; } virtual void debugDump() const; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index a28b8210c2..0b99d0377f 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -73,7 +73,7 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChangedInStage = _stageProperties.setProperties(properties); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(backgroundMode, setBackgroundMode); @@ -117,7 +117,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, bytesRead += bytesFromStage; dataAt += bytesFromStage; - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode); diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 56968aa9c9..599e70f83e 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -54,9 +54,9 @@ public: static bool getDrawZoneBoundaries() { return _drawZoneBoundaries; } static void setDrawZoneBoundaries(bool value) { _drawZoneBoundaries = value; } - virtual bool isReadyToComputeShape() { return false; } - void updateShapeType(ShapeType type) { _shapeType = type; } - virtual ShapeType getShapeType() const; + virtual bool isReadyToComputeShape() override { return false; } + void setShapeType(ShapeType type) override { _shapeType = type; } + virtual ShapeType getShapeType() const override; virtual bool hasCompoundShapeURL() const { return !_compoundShapeURL.isEmpty(); } const QString getCompoundShapeURL() const { return _compoundShapeURL; } From e5b89ebd17aaa10fcb7253c10fcaa4ea0576d9ae Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 22 Jun 2016 15:33:44 -0700 Subject: [PATCH 0714/1237] bump version number --- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6ca50420f3..c74b10820d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS; + return VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ae54450fee..e484a06502 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -180,6 +180,7 @@ const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; +const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH = 61; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, From f495f5e3b56173d15977159300db9802fa46843d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 22 Jun 2016 16:05:55 -0700 Subject: [PATCH 0715/1237] removed mac hydra support --- plugins/hifiSixense/src/SixenseManager.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 03028249a3..48d13a8eaf 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -66,14 +66,8 @@ const QString SHOW_DEBUG_RAW = "Debug Draw Raw Data"; const QString SHOW_DEBUG_CALIBRATED = "Debug Draw Calibrated Data"; bool SixenseManager::isSupported() const { -#ifdef HAVE_SIXENSE - -#if defined(Q_OS_OSX) - return QSysInfo::macVersion() <= QSysInfo::MV_MAVERICKS; -#else +#if defined(HAVE_SIXENSE) && !defined(Q_OS_OSX) return true; -#endif - #else return false; #endif From 3c9f3f392736b56a0641a6f6707c00077b2871bf Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Wed, 22 Jun 2016 16:22:14 -0700 Subject: [PATCH 0716/1237] Automatically enter first person when in HMD mode --- interface/src/Application.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d20a62fc7b..ddfa5dae8b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5350,7 +5350,13 @@ void Application::updateDisplayMode() { // reset the avatar, to set head and hand palms back to a reasonable default pose. getMyAvatar()->reset(false); - Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); + // go into first person when they are in HMD mode, since 3rd person HMD is dumb + if (isHMDMode() && !menu->isOptionChecked(MenuOption::FirstPerson)) { + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + cameraMenuChanged(); + } + + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } mat4 Application::getEyeProjection(int eye) const { From 581d87d6536b45867ea751369d5a3bd534948014 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 22 Jun 2016 16:40:10 -0700 Subject: [PATCH 0717/1237] feedback re magic numbers and variable name. --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 11 ++++++----- plugins/openvr/src/OpenVrHelpers.cpp | 3 ++- scripts/system/controllers/handControllerGrab.js | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index dbf264179e..f1aa1edc81 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -420,8 +420,9 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve } void HmdDisplayPlugin::compositeExtra() { - std::array handLasers; - std::array renderHandPoses; + const int NUMBER_OF_HANDS = 2; + std::array handLasers; + std::array renderHandPoses; Transform uiModelTransform; withPresentThreadLock([&] { handLasers = _handLasers; @@ -443,9 +444,9 @@ void HmdDisplayPlugin::compositeExtra() { using namespace oglplus; useProgram(_laserProgram); _laserGeometry->Use(); - std::array handLaserModelMatrices; + std::array handLaserModelMatrices; - for (int i = 0; i < 2; ++i) { + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { if (renderHandPoses[i] == identity) { continue; } @@ -485,7 +486,7 @@ void HmdDisplayPlugin::compositeExtra() { auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); auto view = glm::inverse(eyePose); const auto& projection = _eyeProjections[eye]; - for (int i = 0; i < 2; ++i) { + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { if (handLaserModelMatrices[i] == identity) { continue; } diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 436cd7fc30..e71c8942d6 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -194,7 +194,8 @@ void enableOpenVrKeyboard(PluginContainer* container) { _overlayMenuConnection = QObject::connect(action, &QAction::triggered, [action] { if (action->isChecked()) { _overlayRevealed = true; - QTimer::singleShot(100, [&] { _overlayRevealed = false; }); + const int KEYBOARD_DELAY_MS = 100; + QTimer::singleShot(KEYBOARD_DELAY_MS, [&] { _overlayRevealed = false; }); } }); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 39d85f1224..7706132c58 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -877,8 +877,8 @@ function MyController(hand) { // find entities near the avatar that might be equipable. var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); - var i, l = entities.length; - for (i = 0; i < l; i++) { + var i, length = entities.length; + for (i = 0; i < length; i++) { var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); // does this entity have an attach point? var wearableData = getEntityCustomData("wearable", entities[i], undefined); From 7e5b9db13a688100d9d587c1a4147e6a65f15e78 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Wed, 22 Jun 2016 16:50:47 -0700 Subject: [PATCH 0718/1237] Uses spaces, not tabs --- interface/src/Application.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ddfa5dae8b..53b9c18360 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5349,14 +5349,14 @@ void Application::updateDisplayMode() { // reset the avatar, to set head and hand palms back to a reasonable default pose. getMyAvatar()->reset(false); - - // go into first person when they are in HMD mode, since 3rd person HMD is dumb - if (isHMDMode() && !menu->isOptionChecked(MenuOption::FirstPerson)) { - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - cameraMenuChanged(); - } - - Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); + + // go into first person when they are in HMD mode, since 3rd person HMD is dumb + if (isHMDMode() && !menu->isOptionChecked(MenuOption::FirstPerson)) { + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + cameraMenuChanged(); + } + + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } mat4 Application::getEyeProjection(int eye) const { From a9ed0b1c8386c530e08ef9e23845ca21580f05f7 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 22 Jun 2016 16:56:50 -0700 Subject: [PATCH 0719/1237] completely destroyed sixense on macs --- cmake/externals/sixense/CMakeLists.txt | 25 +-- cmake/macros/TargetSixense.cmake | 12 +- plugins/hifiSixense/src/SixenseManager.cpp | 2 + plugins/hifiSixense/src/SixenseSupportOSX.cpp | 155 ------------------ 4 files changed, 10 insertions(+), 184 deletions(-) delete mode 100644 plugins/hifiSixense/src/SixenseSupportOSX.cpp diff --git a/cmake/externals/sixense/CMakeLists.txt b/cmake/externals/sixense/CMakeLists.txt index 16f2850449..bd0d042c0b 100644 --- a/cmake/externals/sixense/CMakeLists.txt +++ b/cmake/externals/sixense/CMakeLists.txt @@ -57,30 +57,7 @@ if (WIN32) elseif(APPLE) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/lib/osx_x64/release_dll/libsixense_x64.dylib CACHE TYPE INTERNAL) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${SOURCE_DIR}/lib/osx_x64/debug_dll/libsixensed_x64.dylib CACHE TYPE INTERNAL) - - set(_SIXENSE_LIB_DIR "${SOURCE_DIR}/lib/osx_x64") - ExternalProject_Add_Step( - ${EXTERNAL_NAME} - change-install-name-release - COMMENT "Calling install_name_tool on libraries to fix install name for dylib linking" - COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${_SIXENSE_LIB_DIR}/release_dll -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake - DEPENDEES install - WORKING_DIRECTORY - LOG 1 - ) - - set(_SIXENSE_LIB_DIR "${SOURCE_DIR}/lib/osx_x64") - ExternalProject_Add_Step( - ${EXTERNAL_NAME} - change-install-name-debug - COMMENT "Calling install_name_tool on libraries to fix install name for dylib linking" - COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${_SIXENSE_LIB_DIR}/debug_dll -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake - DEPENDEES install - WORKING_DIRECTORY - LOG 1 - ) + # We no longer support Sixense on Macs due to bugs in the Sixense DLL elseif(NOT ANDROID) diff --git a/cmake/macros/TargetSixense.cmake b/cmake/macros/TargetSixense.cmake index 6fd9cede1f..28128d8b79 100644 --- a/cmake/macros/TargetSixense.cmake +++ b/cmake/macros/TargetSixense.cmake @@ -6,9 +6,11 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # macro(TARGET_SIXENSE) - add_dependency_external_projects(sixense) - find_package(Sixense REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${SIXENSE_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${SIXENSE_LIBRARIES}) - add_definitions(-DHAVE_SIXENSE) + if(NOT APPLE) + add_dependency_external_projects(sixense) + find_package(Sixense REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${SIXENSE_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${SIXENSE_LIBRARIES}) + add_definitions(-DHAVE_SIXENSE) + endif() endmacro() diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 48d13a8eaf..ade643ec72 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -131,6 +131,7 @@ void SixenseManager::setSixenseFilter(bool filter) { void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { BAIL_IF_NOT_LOADED +#ifdef HAVE_SIXENSE static bool sixenseHasBeenConnected { false }; if (!sixenseHasBeenConnected && sixenseIsBaseConnected(0)) { sixenseHasBeenConnected = true; @@ -146,6 +147,7 @@ void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibr _container->requestReset(); _inputDevice->_requestReset = false; } +#endif } void SixenseManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { diff --git a/plugins/hifiSixense/src/SixenseSupportOSX.cpp b/plugins/hifiSixense/src/SixenseSupportOSX.cpp deleted file mode 100644 index fce2ea023b..0000000000 --- a/plugins/hifiSixense/src/SixenseSupportOSX.cpp +++ /dev/null @@ -1,155 +0,0 @@ -// -// SixenseSupportOSX.cpp -// libraries/input-plugins/src/input-plugins -// -// Created by Clement on 10/20/15. -// 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 -// - -// Mock implementation of sixense.h to hide dynamic linking on OS X -#if defined(__APPLE__) && defined(HAVE_SIXENSE) -#include - -#include - -#include -#include -#include - -#ifndef SIXENSE_LIB_FILENAME -#define SIXENSE_LIB_FILENAME QCoreApplication::applicationDirPath() + "/../Frameworks/libsixense_x64" -#endif - -using Library = std::unique_ptr; -static Library SIXENSE; - -struct Callable { - template - int operator() (Args&&... args){ - return reinterpret_cast(function)(std::forward(args)...); - } - QFunctionPointer function; -}; - -Callable resolve(const Library& library, const char* name) { - Q_ASSERT_X(library && library->isLoaded(), __FUNCTION__, "Sixense library not loaded"); - auto function = library->resolve(name); - Q_ASSERT_X(function, __FUNCTION__, std::string("Could not resolve ").append(name).c_str()); - return Callable { function }; -} -#define FORWARD resolve(SIXENSE, __FUNCTION__) - - -void loadSixense() { - Q_ASSERT_X(!(SIXENSE && SIXENSE->isLoaded()), __FUNCTION__, "Sixense library already loaded"); - SIXENSE.reset(new QLibrary(SIXENSE_LIB_FILENAME)); - Q_CHECK_PTR(SIXENSE); - - if (SIXENSE->load()){ - qDebug() << "Loaded sixense library for hydra support -" << SIXENSE->fileName(); - } else { - qDebug() << "Sixense library at" << SIXENSE->fileName() << "failed to load:" << SIXENSE->errorString(); - qDebug() << "Continuing without hydra support."; - } -} -void unloadSixense() { - SIXENSE->unload(); -} - - -// sixense.h wrapper for OSX dynamic linking -int sixenseInit() { - loadSixense(); - if (!SIXENSE || !SIXENSE->isLoaded()) { - return SIXENSE_FAILURE; - } - return FORWARD(); -} -int sixenseExit() { - auto returnCode = FORWARD(); - unloadSixense(); - return returnCode; -} - -int sixenseGetMaxBases() { - return FORWARD(); -} -int sixenseSetActiveBase(int i) { - return FORWARD(i); -} -int sixenseIsBaseConnected(int i) { - return FORWARD(i); -} - -int sixenseGetMaxControllers() { - return FORWARD(); -} -int sixenseIsControllerEnabled(int which) { - return FORWARD(which); -} -int sixenseGetNumActiveControllers() { - return FORWARD(); -} - -int sixenseGetHistorySize() { - return FORWARD(); -} - -int sixenseGetData(int which, int index_back, sixenseControllerData* data) { - return FORWARD(which, index_back, data); -} -int sixenseGetAllData(int index_back, sixenseAllControllerData* data) { - return FORWARD(index_back, data); -} -int sixenseGetNewestData(int which, sixenseControllerData* data) { - return FORWARD(which, data); -} -int sixenseGetAllNewestData(sixenseAllControllerData* data) { - return FORWARD(data); -} - -int sixenseSetHemisphereTrackingMode(int which_controller, int state) { - return FORWARD(which_controller, state); -} -int sixenseGetHemisphereTrackingMode(int which_controller, int* state) { - return FORWARD(which_controller, state); -} -int sixenseAutoEnableHemisphereTracking(int which_controller) { - return FORWARD(which_controller); -} - -int sixenseSetHighPriorityBindingEnabled(int on_or_off) { - return FORWARD(on_or_off); -} -int sixenseGetHighPriorityBindingEnabled(int* on_or_off) { - return FORWARD(on_or_off); -} - -int sixenseTriggerVibration(int controller_id, int duration_100ms, int pattern_id) { - return FORWARD(controller_id, duration_100ms, pattern_id); -} - -int sixenseSetFilterEnabled(int on_or_off) { - return FORWARD(on_or_off); -} -int sixenseGetFilterEnabled(int* on_or_off) { - return FORWARD(on_or_off); -} - -int sixenseSetFilterParams(float near_range, float near_val, float far_range, float far_val) { - return FORWARD(near_range, near_val, far_range, far_val); -} -int sixenseGetFilterParams(float* near_range, float* near_val, float* far_range, float* far_val) { - return FORWARD(near_range, near_val, far_range, far_val); -} - -int sixenseSetBaseColor(unsigned char red, unsigned char green, unsigned char blue) { - return FORWARD(red, green, blue); -} -int sixenseGetBaseColor(unsigned char* red, unsigned char* green, unsigned char* blue) { - return FORWARD(red, green, blue); -} -#endif From 645ae3c21e96fe299ceac3c2499b218b9fc87213 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 22 Jun 2016 17:02:02 -0700 Subject: [PATCH 0720/1237] remove tabs --- cmake/macros/TargetSixense.cmake | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cmake/macros/TargetSixense.cmake b/cmake/macros/TargetSixense.cmake index 28128d8b79..07dcfe67e4 100644 --- a/cmake/macros/TargetSixense.cmake +++ b/cmake/macros/TargetSixense.cmake @@ -6,11 +6,11 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # macro(TARGET_SIXENSE) - if(NOT APPLE) - add_dependency_external_projects(sixense) - find_package(Sixense REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${SIXENSE_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${SIXENSE_LIBRARIES}) - add_definitions(-DHAVE_SIXENSE) - endif() + if(NOT APPLE) + add_dependency_external_projects(sixense) + find_package(Sixense REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${SIXENSE_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${SIXENSE_LIBRARIES}) + add_definitions(-DHAVE_SIXENSE) + endif() endmacro() From cf44783550a906679324fa5f13847b617ec4dd3c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 22 Jun 2016 17:53:38 -0700 Subject: [PATCH 0721/1237] can't enter independent mode through edit.js in HMD mode --- scripts/system/libraries/entityCameraTool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index a8e9335956..63f7c43fdc 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -141,7 +141,7 @@ CameraManager = function() { }; that.enable = function() { - if (Camera.mode == "independent" || that.enabled) return; + if (Camera.mode == "independent" || that.enabled || HMD.active) return; for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.captureKeyEvents({ From 3ec14fd746a88abc3984397505a84a5aeea6ba47 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 22 Jun 2016 18:06:55 -0700 Subject: [PATCH 0722/1237] MErging and still trying to understand the curvature isssue --- libraries/fbx/src/FBXReader.h | 1 + libraries/fbx/src/FBXReader_Material.cpp | 4 + libraries/gpu-gl/src/gpu/gl/GLShared.cpp | 2 +- .../src/model-networking/ModelCache.cpp | 12 +++ .../src/model-networking/TextureCache.h | 1 + .../src/DeferredLightingEffect.cpp | 33 +++++++-- .../render-utils/src/DeferredLightingEffect.h | 46 +++++++++++- .../render-utils/src/FramebufferCache.cpp | 2 +- .../render-utils/src/MaterialTextures.slh | 21 +++++- .../render-utils/src/MeshPartPayload.cpp | 14 ++++ .../render-utils/src/RenderDeferredTask.cpp | 6 +- .../render-utils/src/model_normal_map.slf | 5 +- .../src/surfaceGeometry_makeCurvature.slf | 74 +++++++++++++------ libraries/render/src/render/ShapePipeline.cpp | 1 + libraries/render/src/render/ShapePipeline.h | 1 + .../utilities/render/surfaceGeometryPass.qml | 28 +------ 16 files changed, 186 insertions(+), 65 deletions(-) diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 483e6d9ba5..606c6832d0 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -167,6 +167,7 @@ public: FBXTexture metallicTexture; FBXTexture emissiveTexture; FBXTexture occlusionTexture; + FBXTexture scatteringTexture; FBXTexture lightmapTexture; glm::vec2 lightmapParams{ 0.0f, 1.0f }; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index 6ad12e4d9a..0517dcd4ec 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -255,6 +255,10 @@ void FBXReader::consolidateFBXMaterials() { if (material.name.contains("body_mat") || material.name.contains("skin")) { material._material->setScattering(1.0); + if (!material.emissiveTexture.isNull()) { + material.scatteringTexture = material.emissiveTexture; + material.emissiveTexture = FBXTexture(); + } } if (material.opacity <= 0.0f) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index 17152733d1..d4ecfe0b55 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -700,7 +700,7 @@ bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const s } qCWarning(gpugllogging) << "GLShader::compileShader - errors:"; qCWarning(gpugllogging) << temp; - delete[] temp; + delete[] temp; glDeleteShader(glshader); return false; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 40388e6123..971c5f927b 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -487,6 +487,11 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur setTextureMap(MapChannel::EMISSIVE_MAP, map); } + if (!material.scatteringTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.scatteringTexture, NetworkTexture::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + setTextureMap(MapChannel::SCATTERING_MAP, map); + } + if (!material.lightmapTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); _lightmapTransform = material.lightmapTexture.transform; @@ -507,6 +512,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { const auto& occlusionName = getTextureName(MapChannel::OCCLUSION_MAP); const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP); const auto& lightmapName = getTextureName(MapChannel::LIGHTMAP_MAP); + const auto& scatteringName = getTextureName(MapChannel::SCATTERING_MAP); if (!albedoName.isEmpty()) { auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); @@ -549,6 +555,12 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { setTextureMap(MapChannel::EMISSIVE_MAP, map); } + if (!scatteringName.isEmpty()) { + auto url = textureMap.contains(scatteringName) ? textureMap[scatteringName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, NetworkTexture::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + setTextureMap(MapChannel::SCATTERING_MAP, map); + } + if (!lightmapName.isEmpty()) { auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); auto map = fetchTextureMap(url, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index f3c6a86b49..0108a3dd6c 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -51,6 +51,7 @@ public: EMISSIVE_TEXTURE, CUBE_TEXTURE, OCCLUSION_TEXTURE, + SCATTERING_TEXTURE = OCCLUSION_TEXTURE, LIGHTMAP_TEXTURE, CUSTOM_TEXTURE }; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 3e2607413e..cefceb901e 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -450,7 +450,10 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c } -void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform) { +void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, bool points, bool spots) { + if (!points && !spots) { + return; + } auto args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { @@ -492,7 +495,7 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, batch.setViewTransform(monoViewTransform); // Splat Point lights - if (!deferredLightingEffect->_pointLights.empty()) { + if (points && !deferredLightingEffect->_pointLights.empty()) { // POint light pipeline batch.setPipeline(deferredLightingEffect->_pointLight); @@ -524,7 +527,7 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, } // Splat spot lights - if (!deferredLightingEffect->_spotLights.empty()) { + if (spots && !deferredLightingEffect->_spotLights.empty()) { // Spot light pipeline batch.setPipeline(deferredLightingEffect->_spotLight); @@ -603,16 +606,34 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, } } +RenderDeferred::RenderDeferred() : +_subsurfaceScatteringResource(std::make_shared()) +{ +} + + +void RenderDeferred::configure(const Config& config) { + + glm::vec4 bentInfo(config.bentRed, config.bentGreen, config.bentBlue, config.bentScale); + _subsurfaceScatteringResource->setBentNormalFactors(bentInfo); + + glm::vec2 curvatureInfo(config.curvatureOffset, config.curvatureScale); + _subsurfaceScatteringResource->setCurvatureFactors(curvatureInfo); + + + _enablePointLights = config.enablePointLights; + _enableSpotLights = config.enableSpotLights; +} void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFrameTransformPointer& deferredTransform) { - if (!_subsurfaceScatteringResource) { - _subsurfaceScatteringResource = std::make_shared(); + if (!_subsurfaceScatteringResource->getScatteringTable()) { _subsurfaceScatteringResource->generateScatteringTable(renderContext->args); } setupJob.run(sceneContext, renderContext, deferredTransform, _subsurfaceScatteringResource); - lightsJob.run(sceneContext, renderContext, deferredTransform); + + lightsJob.run(sceneContext, renderContext, deferredTransform, _enablePointLights, _enableSpotLights); cleanupJob.run(sceneContext, renderContext); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index da59a98e37..223b356d49 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -122,7 +122,7 @@ class RenderDeferredLocals { public: using JobModel = render::Job::ModelI; - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, bool points, bool spots); }; @@ -133,9 +133,47 @@ public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); }; + +class RenderDeferredConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(float bentRed MEMBER bentRed NOTIFY dirty) + Q_PROPERTY(float bentGreen MEMBER bentGreen NOTIFY dirty) + Q_PROPERTY(float bentBlue MEMBER bentBlue NOTIFY dirty) + Q_PROPERTY(float bentScale MEMBER bentScale NOTIFY dirty) + + Q_PROPERTY(float curvatureOffset MEMBER curvatureOffset NOTIFY dirty) + Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) + + + Q_PROPERTY(bool enablePointLights MEMBER enablePointLights NOTIFY dirty) + Q_PROPERTY(bool enableSpotLights MEMBER enableSpotLights NOTIFY dirty) +public: + RenderDeferredConfig() : render::Job::Config(true) {} + + float bentRed{ 1.5f }; + float bentGreen{ 0.8f }; + float bentBlue{ 0.3f }; + float bentScale{ 1.5f }; + + float curvatureOffset{ 0.08f }; + float curvatureScale{ 0.8f }; + + bool enablePointLights{ true }; + bool enableSpotLights{ true }; + +signals: + void dirty(); +}; + + class RenderDeferred { public: - using JobModel = render::Job::ModelI; + using Config = RenderDeferredConfig; + using JobModel = render::Job::ModelI; + + RenderDeferred(); + + void configure(const Config& config); void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); @@ -143,7 +181,11 @@ public: RenderDeferredLocals lightsJob; RenderDeferredCleanup cleanupJob; +protected: SubsurfaceScatteringResourcePointer _subsurfaceScatteringResource; + + bool _enablePointLights{ true }; + bool _enableSpotLights{ true }; }; #endif // hifi_DeferredLightingEffect_h diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 79a8af8eb7..21b0b4e052 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -107,7 +107,7 @@ void FramebufferCache::createPrimaryFramebuffer() { // For AO: auto pointMipSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT); - _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, pointMipSampler)); + _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); _depthPyramidFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _depthPyramidFramebuffer->setRenderBuffer(0, _depthPyramidTexture); _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 5cede16e13..97f0ed6e96 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -44,7 +44,7 @@ TexMapArray getTexMapArray() { <@endfunc@> -<@func declareMaterialTextures(withAlbedo, withRoughness, withNormal, withMetallic, withEmissive, withOcclusion)@> +<@func declareMaterialTextures(withAlbedo, withRoughness, withNormal, withMetallic, withEmissive, withOcclusion, withScattering)@> <@if withAlbedo@> uniform sampler2D albedoMap; @@ -87,10 +87,18 @@ float fetchOcclusionMap(vec2 uv) { return texture(occlusionMap, uv).r; } <@endif@> + +<@if withScattering@> +uniform sampler2D scatteringMap; +float fetchScatteringMap(vec2 uv) { + return step(0.5, texture(scatteringMap, uv).r); // boolean scattering for now +} +<@endif@> + <@endfunc@> -<@func fetchMaterialTexturesCoord0(matKey, texcoord0, albedo, roughness, normal, metallic, emissive)@> +<@func fetchMaterialTexturesCoord0(matKey, texcoord0, albedo, roughness, normal, metallic, emissive, scattering)@> <@if albedo@> vec4 <$albedo$> = (((<$matKey$> & (ALBEDO_MAP_BIT | OPACITY_MASK_MAP_BIT | OPACITY_TRANSLUCENT_MAP_BIT)) != 0) ? fetchAlbedoMap(<$texcoord0$>) : vec4(1.0)); <@endif@> @@ -106,6 +114,9 @@ float fetchOcclusionMap(vec2 uv) { <@if emissive@> vec3 <$emissive$> = (((<$matKey$> & EMISSIVE_MAP_BIT) != 0) ? fetchEmissiveMap(<$texcoord0$>) : vec3(0.0)); <@endif@> +<@if scattering@> + float <$scattering$> = (((<$matKey$> & SCATTERING_MAP_BIT) != 0) ? fetchScatteringMap(<$texcoord0$>) : 0.0); +<@endif@> <@endfunc@> <@func fetchMaterialTexturesCoord1(matKey, texcoord1, occlusion, lightmapVal)@> @@ -191,4 +202,10 @@ vec3 fetchLightmapMap(vec2 uv) { } <@endfunc@> +<@func evalMaterialScattering(fetchedScattering, materialScattering, matKey, scattering)@> +{ + <$scattering$> = (((<$matKey$> & SCATTERING_MAP_BIT) != 0) ? <$fetchedScattering$> : <$materialScattering$>); +} +<@endfunc@> + <@endif@> \ No newline at end of file diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 08c8dc23b4..f554e1aa39 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -217,6 +217,20 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, nullptr); } + // Scattering map + if (materialKey.isScatteringMap()) { + auto scatteringMap = textureMaps[model::MaterialKey::SCATTERING_MAP]; + if (scatteringMap && scatteringMap->isDefined()) { + batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, scatteringMap->getTextureView()); + + // texcoord are assumed to be the same has albedo + } else { + batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, textureCache->getWhiteTexture()); + } + } else { + batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, nullptr); + } + // Emissive / Lightmap if (materialKey.isLightmapMap()) { auto lightmapMap = textureMaps[model::MaterialKey::LIGHTMAP_MAP]; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 4d04814954..09129209c4 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -123,8 +123,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer)); - const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); + // const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer)); + // const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredFrameTransform); @@ -147,7 +147,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Debugging stages { // Debugging Deferred buffer job - const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(diffusedCurvatureFramebuffer, scatteringFramebuffer)); + const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(diffusedCurvatureFramebuffer, curvatureFramebuffer)); addJob("DebugDeferredBuffer", debugFramebuffers); // Scene Octree Debuging job diff --git a/libraries/render-utils/src/model_normal_map.slf b/libraries/render-utils/src/model_normal_map.slf index e6baac4e04..3acdedab2a 100755 --- a/libraries/render-utils/src/model_normal_map.slf +++ b/libraries/render-utils/src/model_normal_map.slf @@ -17,7 +17,7 @@ <@include model/Material.slh@> <@include MaterialTextures.slh@> -<$declareMaterialTextures(ALBEDO, ROUGHNESS, NORMAL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$> +<$declareMaterialTextures(ALBEDO, ROUGHNESS, NORMAL, _SCRIBE_NULL, EMISSIVE, OCCLUSION, SCATTERING)$> in vec4 _position; in vec2 _texCoord0; @@ -29,7 +29,7 @@ in vec3 _color; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); - <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, normalTex, _SCRIBE_NULL, emissiveTex)$> + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, normalTex, _SCRIBE_NULL, emissiveTex, scatteringTex)$> <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> float opacity = 1.0; @@ -50,6 +50,7 @@ void main(void) { <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> float scattering = getMaterialScattering(mat); + <$evalMaterialScattering(scatteringTex, scattering, matKey, scattering)$>; packDeferredFragment( viewNormal, diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index 5316ef4211..d4694b4c94 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -91,14 +91,10 @@ vec3 getWorldNormal(vec2 texcoord) { } vec3 getWorldNormalDiff(vec2 texcoord, vec2 delta) { - vec3 normal0 = getWorldNormal(texcoord - delta); - vec3 normal1 = getWorldNormal(texcoord + delta); - return normal1 - normal0; + return getWorldNormal(texcoord + delta) - getWorldNormal(texcoord - delta); } float getEyeDepthDiff(vec2 texcoord, vec2 delta) { - vec3 normal0 = getWorldNormal(texcoord - delta); - vec3 normal1 = getWorldNormal(texcoord + delta); return getZEyeLinear(texcoord + delta) - getZEyeLinear(texcoord - delta); } @@ -118,18 +114,19 @@ void main(void) { // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); - float nearPlaneScale = min(-Zeye / getCurvatureBasisScale(), 1.0); + // float nearPlaneScale = min(-Zeye / getCurvatureBasisScale(), 1.0); + float nearPlaneScale = 0.5 * getProjectionNear(); vec3 worldNormal = getWorldNormal(frameTexcoordPos); // The position of the pixel fragment in Eye space then in world space vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); - vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; + // vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; // Calculate the perspective scale. // Clamp to 0.5 - float perspectiveScale = max(0.5, (-getProjScaleEye() / Zeye)); - // float perspectiveScale = max(0.5, (-getProjectionNear() / Zeye)); + // float perspectiveScale = max(0.5, (-getProjScaleEye() / Zeye)); + float perspectiveScale = max(0.5, (-getCurvatureBasisScale() * getProjectionNear() / Zeye)); // Calculate dF/du and dF/dv vec2 viewportScale = perspectiveScale * getInvWidthHeight(); @@ -141,7 +138,7 @@ void main(void) { float threshold = getCurvatureDepthThreshold(); dFdu *= step(abs(dFdu.w), threshold); - dFdv *= step(abs(dFdv.w), threshold); + dFdv *= step(abs(dFdv.w), threshold); //outFragColor = vec4(du.x, du.y, 0.0, 1.0); // outFragColor = vec4(viewportScale, 0.0, 1.0); @@ -160,14 +157,15 @@ void main(void) { // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) // Eval px, py, pz world positions of the basis centered on the world pos of the fragment - float dist = getCurvatureBasisScale() * nearPlaneScale; - vec4 px = vec4(worldPos, 1.0) + vec4(dist, 0.0f, 0.0f, 0.0f); - vec4 py = vec4(worldPos, 1.0) + vec4(0.0f, dist, 0.0f, 0.0f); - vec4 pz = vec4(worldPos, 1.0) + vec4(0.0f, 0.0f, dist, 0.0f); + float axeLength = /*getCurvatureBasisScale() * */ nearPlaneScale; - px = frameTransform._view * px; - py = frameTransform._view * py; - pz = frameTransform._view * pz; + vec3 ax = (frameTransform._view[0].xyz * axeLength); + vec3 ay = (frameTransform._view[1].xyz * axeLength); + vec3 az = (frameTransform._view[2].xyz * axeLength); + + vec4 px = vec4(eyePos + ax, 0.0); + vec4 py = vec4(eyePos + ay, 0.0); + vec4 pz = vec4(eyePos + az, 0.0); /* if (texcoordPos.y > 0.5) { @@ -178,6 +176,33 @@ void main(void) { return; */ + float nearZ = -getProjectionNear(); + vec3 axeSign = vec3(1.0); + /* if (px.z >= nearZ) { + px = vec4(eyePos - ax, 0.0); + axeSign.x = -1.0; + } + if (py.z >= nearZ) { + py = vec4(eyePos - ay, 0.0); + axeSign.y = -1.0; + } + if (pz.z >= nearZ) { + pz = vec4(eyePos - az, 0.0); + axeSign.z = -1.0; + }*/ + + if (px.z >= -nearPlaneScale) { + outFragColor = vec4(1.0, 0.0, 0.0, 1.0); + return; + } else if (py.z >= -nearPlaneScale) { + outFragColor = vec4(0.0, 1.0, 0.0, 1.0); + return; + } else if (pz.z >= -nearPlaneScale) { + outFragColor = vec4(0.0, 0.0, 1.0, 1.0); + return; + } + + // Project px, py pz to homogeneous clip space mat4 viewProj = getProjection(stereoSide.x); px = viewProj * px; @@ -200,17 +225,24 @@ void main(void) { */ float pixPerspectiveScaleInv = 1.0 / (perspectiveScale * nearPlaneScale); - px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv; - py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv; - pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv; + px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv * axeSign.x; + py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv * axeSign.y; + pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv * axeSign.z; // Calculate dF/dx, dF/dy and dF/dz using chain rule vec4 dFdx = dFdu * px.x + dFdv * px.y; vec4 dFdy = dFdu * py.x + dFdv * py.y; vec4 dFdz = dFdu * pz.x + dFdv * pz.y; + vec3 trace = vec3(dFdx.x, dFdy.y, dFdz.z); + + if (dot(trace, trace) > params.curvatureInfo.w) { + outFragColor = vec4(dFdx.x, dFdy.y, dFdz.z, 1.0); + return; + } + // Calculate the mean curvature - float meanCurvature = ((dFdx.x + dFdy.y + dFdz.z) * 0.33333333333333333) * params.curvatureInfo.w; + float meanCurvature = ((trace.x + trace.y + trace.z) * 0.33333333333333333) * params.curvatureInfo.w; outFragColor = vec4(vec3(worldNormal + 1.0) * 0.5, (meanCurvature + 1.0) * 0.5); } diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 37c8db8364..5245992ec4 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -60,6 +60,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p slotBindings.insert(gpu::Shader::Binding(std::string("metallicMap"), Slot::MAP::METALLIC)); slotBindings.insert(gpu::Shader::Binding(std::string("emissiveMap"), Slot::MAP::EMISSIVE_LIGHTMAP)); slotBindings.insert(gpu::Shader::Binding(std::string("occlusionMap"), Slot::MAP::OCCLUSION)); + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringMap"), Slot::MAP::SCATTERING)); slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), Slot::BUFFER::LIGHT)); slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), Slot::MAP::LIGHT_AMBIENT)); slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), Slot::NORMAL_FITTING)); diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index bed3bd7c68..e65281fd33 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -209,6 +209,7 @@ public: EMISSIVE_LIGHTMAP, ROUGHNESS, OCCLUSION, + SCATTERING, LIGHT_AMBIENT, NORMAL_FITTING = 10, diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index b99605978c..7a061da65b 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -19,7 +19,7 @@ Column { Column{ Repeater { - model: [ "Depth Threshold:depthThreshold:1.0", "Basis Scale:basisScale:1.0", "Curvature Scale:curvatureScale:200.0" ] + model: [ "Depth Threshold:depthThreshold:0.1", "Basis Scale:basisScale:2.0", "Curvature Scale:curvatureScale:10.0" ] ConfigSlider { label: qsTr(modelData.split(":")[0]) integral: false @@ -44,31 +44,5 @@ Column { } } } - - Column{ - Repeater { - model: [ "Scattering Bent Red:Scattering:bentRed:2.0", - "Scattering Bent Green:Scattering:bentGreen:2.0", - "Scattering Bent Blue:Scattering:bentBlue:2.0", - "Scattering Bent Scale:Scattering:bentScale:2.0", - "Scattering Curvature Offset:Scattering:curvatureOffset:1.0", - "Scattering Curvature Scale:Scattering:curvatureScale:1.0", - ] - ConfigSlider { - label: qsTr(modelData.split(":")[0]) - integral: false - config: Render.getConfig(modelData.split(":")[1]) - property: modelData.split(":")[2] - max: modelData.split(":")[3] - min: 0.0 - } - } - } - CheckBox { - text: "Show scatteringLUT" - checked: false - onCheckedChanged: { Render.getConfig("Scattering").showLUT = checked } - } - } } From 7307bc6a1b6a80dfed4ec0245bca365d6009a3b0 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 15 Jun 2016 09:51:25 -0700 Subject: [PATCH 0723/1237] revert to domain connection refusal check with string --- libraries/networking/src/DomainHandler.cpp | 22 +++------------------- libraries/networking/src/DomainHandler.h | 2 +- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 170669dede..3213e7319a 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -407,25 +407,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 3ab583d597..be8ccbe50f 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -144,7 +144,7 @@ private: QString _pendingPath; QTimer _settingsTimer; - QList _domainConnectionRefusals; + QStringList _domainConnectionRefusals; bool _hasSignalledProtocolMismatch { false }; bool _hasCheckedForAccessToken { false }; int _connectionDenialsSinceKeypairRegen { 0 }; From f9b3165350bfee7243268e38ab91777538a5e4bf Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 23 Jun 2016 09:37:17 -0700 Subject: [PATCH 0724/1237] use a QSet instead of a QStringList --- libraries/networking/src/DomainHandler.cpp | 2 +- libraries/networking/src/DomainHandler.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 3213e7319a..c4b979e0b8 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -408,7 +408,7 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer _domainConnectionRefusals; bool _hasSignalledProtocolMismatch { false }; bool _hasCheckedForAccessToken { false }; int _connectionDenialsSinceKeypairRegen { 0 }; From c0be9aec6aedd683e12d7d61dfbf9ff210526d70 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 9 Jun 2016 13:28:25 -0700 Subject: [PATCH 0725/1237] cleanup the callers of the resamplers for mic to net and net to speaker, remove loopback resampler --- libraries/audio-client/src/AudioClient.cpp | 88 +++++++--------------- libraries/audio-client/src/AudioClient.h | 1 - 2 files changed, 26 insertions(+), 63 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0c7a79e2a3..ac055bb0f6 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -102,7 +102,6 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), - _loopbackResampler(NULL), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_receivedAudioStream, this), _stats(&_receivedAudioStream), @@ -315,54 +314,35 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, // FIXME: directly using 24khz has a bug somewhere that causes channels to be swapped. // Continue using our internal resampler, for now. - if (true || !audioDevice.isFormatSupported(desiredAudioFormat)) { - qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; - qCDebug(audioclient, "The desired audio format is not supported by this device"); + qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; - if (desiredAudioFormat.channelCount() == 1) { - adjustedAudioFormat = desiredAudioFormat; - adjustedAudioFormat.setChannelCount(2); - - if (false && audioDevice.isFormatSupported(adjustedAudioFormat)) { - return true; - } else { - adjustedAudioFormat.setChannelCount(1); - } - } - - const int FORTY_FOUR = 44100; - - adjustedAudioFormat = desiredAudioFormat; + const int FORTY_FOUR = 44100; + adjustedAudioFormat = desiredAudioFormat; #ifdef Q_OS_ANDROID - adjustedAudioFormat.setSampleRate(FORTY_FOUR); + adjustedAudioFormat.setSampleRate(FORTY_FOUR); #else - const int HALF_FORTY_FOUR = FORTY_FOUR / 2; + const int HALF_FORTY_FOUR = FORTY_FOUR / 2; - if (audioDevice.supportedSampleRates().contains(AudioConstants::SAMPLE_RATE * 2)) { - // use 48, which is a sample downsample, upsample - adjustedAudioFormat.setSampleRate(AudioConstants::SAMPLE_RATE * 2); - } else if (audioDevice.supportedSampleRates().contains(HALF_FORTY_FOUR)) { - // use 22050, resample but closer to 24 - adjustedAudioFormat.setSampleRate(HALF_FORTY_FOUR); - } else if (audioDevice.supportedSampleRates().contains(FORTY_FOUR)) { - // use 48000, resample - adjustedAudioFormat.setSampleRate(FORTY_FOUR); - } + if (audioDevice.supportedSampleRates().contains(AudioConstants::SAMPLE_RATE * 2)) { + // use 48, which is a sample downsample, upsample + adjustedAudioFormat.setSampleRate(AudioConstants::SAMPLE_RATE * 2); + } else if (audioDevice.supportedSampleRates().contains(HALF_FORTY_FOUR)) { + // use 22050, resample but closer to 24 + adjustedAudioFormat.setSampleRate(HALF_FORTY_FOUR); + } else if (audioDevice.supportedSampleRates().contains(FORTY_FOUR)) { + // use 48000, resample + adjustedAudioFormat.setSampleRate(FORTY_FOUR); + } #endif - if (adjustedAudioFormat != desiredAudioFormat) { - // return the nearest in case it needs 2 channels - adjustedAudioFormat = audioDevice.nearestFormat(adjustedAudioFormat); - return true; - } else { - return false; - } - } else { - // set the adjustedAudioFormat to the desiredAudioFormat, since it will work - adjustedAudioFormat = desiredAudioFormat; + if (adjustedAudioFormat != desiredAudioFormat) { + // return the nearest in case it needs 2 channels + adjustedAudioFormat = audioDevice.nearestFormat(adjustedAudioFormat); return true; + } else { + return false; } } @@ -467,11 +447,6 @@ void AudioClient::stop() { // "switch" to invalid devices in order to shut down the state switchInputToAudioDevice(QAudioDeviceInfo()); switchOutputToAudioDevice(QAudioDeviceInfo()); - - if (_loopbackResampler) { - delete _loopbackResampler; - _loopbackResampler = NULL; - } } void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointer message) { @@ -675,16 +650,10 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } } - // do we need to setup a resampler? - if (_inputFormat.sampleRate() != _outputFormat.sampleRate() && !_loopbackResampler) { - qCDebug(audioclient) << "Attemping to create a resampler for input format to output format for audio loopback."; - - assert(_inputFormat.sampleSize() == 16); - assert(_outputFormat.sampleSize() == 16); - int channelCount = (_inputFormat.channelCount() == 2 && _outputFormat.channelCount() == 2) ? 2 : 1; - - _loopbackResampler = new AudioSRC(_inputFormat.sampleRate(), _outputFormat.sampleRate(), channelCount); - } + // NOTE: we assume the inputFormat and the outputFormat are the same, since on any modern + // multimedia OS they should be. If there is a device that this is not true for, we can + // add back support to do resampling. + Q_ASSERT(_inputFormat.sampleRate() == _outputFormat.sampleRate()); static QByteArray loopBackByteArray; @@ -696,7 +665,8 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* inputSamples = reinterpret_cast(inputByteArray.data()); int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); - possibleResampling(_loopbackResampler, + auto NO_RESAMPLER = nullptr; + possibleResampling(NO_RESAMPLER, inputSamples, loopbackSamples, numInputSamples, numLoopbackSamples, _inputFormat, _outputFormat); @@ -1039,12 +1009,6 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _networkToOutputResampler = NULL; } - if (_loopbackResampler) { - // if we were using an input to output resample, delete it here - delete _loopbackResampler; - _loopbackResampler = NULL; - } - if (!outputDeviceInfo.isNull()) { qCDebug(audioclient) << "The audio output device " << outputDeviceInfo.deviceName() << "is available."; _outputAudioDeviceName = outputDeviceInfo.deviceName().trimmed(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3a14c878f6..dc46db5657 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -260,7 +260,6 @@ private: // possible streams needed for resample AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; - AudioSRC* _loopbackResampler; // Adds Reverb void configureReverb(); From 1d8d1240cad863ab853123b874d78d677ce26e07 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 23 Jun 2016 10:38:46 -0700 Subject: [PATCH 0726/1237] remove override to avoid new Jenkins OSX warnings --- libraries/entities/src/ParticleEffectEntityItem.h | 4 ++-- libraries/entities/src/ZoneEntityItem.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 777f3b6cf6..9ddda62c8b 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -95,8 +95,8 @@ public: void setAlphaSpread(float alphaSpread); float getAlphaSpread() const { return _alphaSpread; } - void setShapeType(ShapeType type) override; - virtual ShapeType getShapeType() const override { return _shapeType; } + void setShapeType(ShapeType type); + virtual ShapeType getShapeType() const { return _shapeType; } virtual void debugDump() const; diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 599e70f83e..f0f2a91d63 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -54,9 +54,9 @@ public: static bool getDrawZoneBoundaries() { return _drawZoneBoundaries; } static void setDrawZoneBoundaries(bool value) { _drawZoneBoundaries = value; } - virtual bool isReadyToComputeShape() override { return false; } - void setShapeType(ShapeType type) override { _shapeType = type; } - virtual ShapeType getShapeType() const override; + virtual bool isReadyToComputeShape() { return false; } + void setShapeType(ShapeType type) { _shapeType = type; } + virtual ShapeType getShapeType() const; virtual bool hasCompoundShapeURL() const { return !_compoundShapeURL.isEmpty(); } const QString getCompoundShapeURL() const { return _compoundShapeURL; } From be37921845d760e9052f68ec04891bf6d2dee237 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 9 Jun 2016 15:49:07 -0700 Subject: [PATCH 0727/1237] first cut at codec plugins --- interface/src/Application.cpp | 7 +++ libraries/audio-client/src/AudioClient.cpp | 20 ++++++--- libraries/plugins/src/plugins/CodecPlugin.h | 19 ++++++++ libraries/plugins/src/plugins/Forward.h | 4 ++ .../plugins/src/plugins/PluginManager.cpp | 30 +++++++++++++ libraries/plugins/src/plugins/PluginManager.h | 1 + libraries/plugins/src/plugins/RuntimePlugin.h | 9 ++++ plugins/pcmCodec/CMakeLists.txt | 11 +++++ plugins/pcmCodec/src/PCMCodecManager.cpp | 45 +++++++++++++++++++ plugins/pcmCodec/src/PCMCodecManager.h | 40 +++++++++++++++++ plugins/pcmCodec/src/PCMCodecProvider.cpp | 44 ++++++++++++++++++ plugins/pcmCodec/src/plugin.json | 1 + 12 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 libraries/plugins/src/plugins/CodecPlugin.h create mode 100644 plugins/pcmCodec/CMakeLists.txt create mode 100644 plugins/pcmCodec/src/PCMCodecManager.cpp create mode 100644 plugins/pcmCodec/src/PCMCodecManager.h create mode 100644 plugins/pcmCodec/src/PCMCodecProvider.cpp create mode 100644 plugins/pcmCodec/src/plugin.json diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 37c3b361bf..855cf2f313 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -85,6 +85,7 @@ #include #include #include +#include #include #include #include @@ -1236,7 +1237,13 @@ QString Application::getUserAgent() { userAgent += " " + formatPluginName(ip->getName()); } } + // for codecs, we include all of them, even if not active + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& cp : codecPlugins) { + userAgent += " " + formatPluginName(cp->getName()); + } + qDebug() << __FUNCTION__ << ":" << userAgent; return userAgent; } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0c7a79e2a3..2569f27aaa 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -813,19 +813,25 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { emitAudioPacket(audio.data(), audio.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho); } -void AudioClient::processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer) { - const int numNetworkOutputSamples = inputBuffer.size() / sizeof(int16_t); - const int numDeviceOutputSamples = numNetworkOutputSamples * (_outputFormat.sampleRate() * _outputFormat.channelCount()) - / (_desiredOutputFormat.sampleRate() * _desiredOutputFormat.channelCount()); +void AudioClient::processReceivedSamples(const QByteArray& networkBuffer, QByteArray& outputBuffer) { + + // TODO - codec decode goes here + QByteArray decodedBuffer = networkBuffer; + + const int numDecodecSamples = decodedBuffer.size() / sizeof(int16_t); + const int numDeviceOutputSamples = _outputFrameSize; + + Q_ASSERT(_outputFrameSize == numDecodecSamples * (_outputFormat.sampleRate() * _outputFormat.channelCount()) + / (_desiredOutputFormat.sampleRate() * _desiredOutputFormat.channelCount())); outputBuffer.resize(numDeviceOutputSamples * sizeof(int16_t)); - const int16_t* receivedSamples = reinterpret_cast(inputBuffer.data()); + const int16_t* decodedSamples = reinterpret_cast(decodedBuffer.data()); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); // copy the packet from the RB to the output - possibleResampling(_networkToOutputResampler, receivedSamples, outputSamples, - numNetworkOutputSamples, numDeviceOutputSamples, + possibleResampling(_networkToOutputResampler, decodedSamples, outputSamples, + numDecodecSamples, numDeviceOutputSamples, _desiredOutputFormat, _outputFormat); // apply stereo reverb at the listener, to the received audio diff --git a/libraries/plugins/src/plugins/CodecPlugin.h b/libraries/plugins/src/plugins/CodecPlugin.h new file mode 100644 index 0000000000..d9e1b947fe --- /dev/null +++ b/libraries/plugins/src/plugins/CodecPlugin.h @@ -0,0 +1,19 @@ +// +// CodecPlugin.h +// plugins/src/plugins +// +// Created by Brad Hefta-Gaub on 6/9/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#include "Plugin.h" + +class CodecPlugin : public Plugin { +public: + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; +}; + diff --git a/libraries/plugins/src/plugins/Forward.h b/libraries/plugins/src/plugins/Forward.h index 036b42f7d7..723c4f321e 100644 --- a/libraries/plugins/src/plugins/Forward.h +++ b/libraries/plugins/src/plugins/Forward.h @@ -13,10 +13,12 @@ enum class PluginType { DISPLAY_PLUGIN, INPUT_PLUGIN, + CODEC_PLUGIN, }; class DisplayPlugin; class InputPlugin; +class CodecPlugin; class Plugin; class PluginContainer; class PluginManager; @@ -25,4 +27,6 @@ using DisplayPluginPointer = std::shared_ptr; using DisplayPluginList = std::vector; using InputPluginPointer = std::shared_ptr; using InputPluginList = std::vector; +using CodecPluginPointer = std::shared_ptr; +using CodecPluginList = std::vector; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 29658eeb6b..fe34d2dbee 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -18,6 +18,7 @@ #include #include "RuntimePlugin.h" +#include "CodecPlugin.h" #include "DisplayPlugin.h" #include "InputPlugin.h" @@ -117,6 +118,7 @@ PluginManager::PluginManager() { // TODO migrate to a DLL model where plugins are discovered and loaded at runtime by the PluginManager class extern DisplayPluginList getDisplayPlugins(); extern InputPluginList getInputPlugins(); +extern CodecPluginList getCodecPlugins(); extern void saveInputPluginSettings(const InputPluginList& plugins); static DisplayPluginList displayPlugins; @@ -202,6 +204,34 @@ const InputPluginList& PluginManager::getInputPlugins() { return inputPlugins; } +const CodecPluginList& PluginManager::getCodecPlugins() { + static CodecPluginList codecPlugins; + static std::once_flag once; + std::call_once(once, [&] { + //codecPlugins = ::getCodecPlugins(); + + // Now grab the dynamic plugins + for (auto loader : getLoadedPlugins()) { + CodecProvider* codecProvider = qobject_cast(loader->instance()); + if (codecProvider) { + for (auto codecPlugin : codecProvider->getCodecPlugins()) { + if (codecPlugin->isSupported()) { + codecPlugins.push_back(codecPlugin); + } + } + } + } + + auto& container = PluginContainer::getInstance(); + for (auto plugin : codecPlugins) { + plugin->setContainer(&container); + plugin->init(); + } + }); + return codecPlugins; +} + + void PluginManager::setPreferredDisplayPlugins(const QStringList& displays) { preferredDisplayPlugins = displays; } diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 7903bdd724..30d52298da 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -18,6 +18,7 @@ public: const DisplayPluginList& getDisplayPlugins(); const InputPluginList& getInputPlugins(); + const CodecPluginList& getCodecPlugins(); DisplayPluginList getPreferredDisplayPlugins(); void setPreferredDisplayPlugins(const QStringList& displays); diff --git a/libraries/plugins/src/plugins/RuntimePlugin.h b/libraries/plugins/src/plugins/RuntimePlugin.h index d7bf31ea28..9bf15f344d 100644 --- a/libraries/plugins/src/plugins/RuntimePlugin.h +++ b/libraries/plugins/src/plugins/RuntimePlugin.h @@ -34,3 +34,12 @@ public: #define InputProvider_iid "com.highfidelity.plugins.input" Q_DECLARE_INTERFACE(InputProvider, InputProvider_iid) +class CodecProvider { +public: + virtual ~CodecProvider() {} + virtual CodecPluginList getCodecPlugins() = 0; +}; + +#define CodecProvider_iid "com.highfidelity.plugins.codec" +Q_DECLARE_INTERFACE(CodecProvider, CodecProvider_iid) + diff --git a/plugins/pcmCodec/CMakeLists.txt b/plugins/pcmCodec/CMakeLists.txt new file mode 100644 index 0000000000..162fe557cc --- /dev/null +++ b/plugins/pcmCodec/CMakeLists.txt @@ -0,0 +1,11 @@ +# +# Created by Brad Hefta-Gaub on 6/9/2016 +# Copyright 2016 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html +# + +set(TARGET_NAME pcmCodec) +setup_hifi_plugin() +link_hifi_libraries(shared plugins) diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp new file mode 100644 index 0000000000..264c139ac6 --- /dev/null +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -0,0 +1,45 @@ +// +// PCMCodecManager.cpp +// plugins/pcmCodec/src +// +// Created by Brad Hefta-Gaub on 6/9/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include + +#include "PCMCodecManager.h" + +const QString PCMCodecManager::NAME = "PCM Codec"; + +void PCMCodecManager::init() { +} + +void PCMCodecManager::deinit() { +} + +bool PCMCodecManager::activate() { + CodecPlugin::activate(); + return true; +} + +void PCMCodecManager::deactivate() { + CodecPlugin::deactivate(); +} + + +bool PCMCodecManager::isSupported() const { + return true; +} + + +void PCMCodecManager::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { + // this codec doesn't actually do anything.... + decodedBuffer = encodedBuffer; +} + diff --git a/plugins/pcmCodec/src/PCMCodecManager.h b/plugins/pcmCodec/src/PCMCodecManager.h new file mode 100644 index 0000000000..a8b3e49a2c --- /dev/null +++ b/plugins/pcmCodec/src/PCMCodecManager.h @@ -0,0 +1,40 @@ +// +// PCMCodecManager.h +// plugins/pcmCodec/src +// +// Created by Brad Hefta-Gaub on 6/9/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi__PCMCodecManager_h +#define hifi__PCMCodecManager_h + + +#include + +class PCMCodecManager : public CodecPlugin { + Q_OBJECT + +public: + // Plugin functions + bool isSupported() const override; + const QString& getName() const override { return NAME; } + + void init() override; + void deinit() override; + + /// Called when a plugin is being activated for use. May be called multiple times. + bool activate() override; + /// Called when a plugin is no longer being used. May be called multiple times. + void deactivate() override; + + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override; + +private: + static const QString NAME; +}; + +#endif // hifi__PCMCodecManager_h diff --git a/plugins/pcmCodec/src/PCMCodecProvider.cpp b/plugins/pcmCodec/src/PCMCodecProvider.cpp new file mode 100644 index 0000000000..732ed2d57d --- /dev/null +++ b/plugins/pcmCodec/src/PCMCodecProvider.cpp @@ -0,0 +1,44 @@ +// +// Created by Brad Hefta-Gaub on 6/9/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include +#include +#include + +#include +#include + +#include "PCMCodecManager.h" + +class PCMCodecProvider : public QObject, public CodecProvider { + Q_OBJECT + Q_PLUGIN_METADATA(IID CodecProvider_iid FILE "plugin.json") + Q_INTERFACES(CodecProvider) + +public: + PCMCodecProvider(QObject* parent = nullptr) : QObject(parent) {} + virtual ~PCMCodecProvider() {} + + virtual CodecPluginList getCodecPlugins() override { + static std::once_flag once; + std::call_once(once, [&] { + CodecPluginPointer plugin(new PCMCodecManager()); + if (plugin->isSupported()) { + _codecPlugins.push_back(plugin); + } + }); + return _codecPlugins; + } + +private: + CodecPluginList _codecPlugins; +}; + +#include "PCMCodecProvider.moc" diff --git a/plugins/pcmCodec/src/plugin.json b/plugins/pcmCodec/src/plugin.json new file mode 100644 index 0000000000..2d86251845 --- /dev/null +++ b/plugins/pcmCodec/src/plugin.json @@ -0,0 +1 @@ +{"name":"PCM Codec"} From a0bb55e4c8579679b7832542bb6b67459fb3c109 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 23 Jun 2016 10:48:29 -0700 Subject: [PATCH 0728/1237] coding standard fun --- scripts/system/libraries/entityCameraTool.js | 37 +++++++++++--------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index 63f7c43fdc..301b60f550 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -141,7 +141,9 @@ CameraManager = function() { }; that.enable = function() { - if (Camera.mode == "independent" || that.enabled || HMD.active) return; + if (Camera.mode == "independent" || that.enabled || HMD.active) { + return; + } for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.captureKeyEvents({ @@ -179,7 +181,9 @@ CameraManager = function() { } that.disable = function(ignoreCamera) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.releaseKeyEvents({ @@ -352,27 +356,21 @@ CameraManager = function() { that.mousePressEvent = function(event) { if (cameraTool.mousePressEvent(event)) { - return true; } - - if (!that.enabled) return; + if (!that.enabled) { + return; + } if (event.isRightButton || (event.isLeftButton && event.isControl && !event.isShifted)) { - that.mode = MODE_ORBIT; } else if (event.isMiddleButton || (event.isLeftButton && event.isControl && event.isShifted)) { - - that.mode = MODE_PAN; } if (that.mode !== MODE_INACTIVE) { - - hasDragged = false; - return true; } @@ -381,7 +379,9 @@ CameraManager = function() { that.mouseReleaseEvent = function(event) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } that.mode = MODE_INACTIVE; Reticle.setVisible(true); @@ -403,7 +403,9 @@ CameraManager = function() { }; that.wheelEvent = function(event) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } var dZoom = -event.delta * SCROLL_SENSITIVITY; @@ -459,8 +461,12 @@ CameraManager = function() { } function normalizeDegrees(degrees) { - while (degrees > 180) degrees -= 360; - while (degrees < -180) degrees += 360; + while (degrees > 180) { + degrees -= 360; + } + while (degrees < -180) { + degrees += 360; + } return degrees; } @@ -483,7 +489,6 @@ CameraManager = function() { that.targetZoomDistance = clamp(that.targetZoomDistance, MIN_ZOOM_DISTANCE, MAX_ZOOM_DISTANCE); } - if (easing) { easingTime = Math.min(EASE_TIME, easingTime + dt); } From 6111e4952afdfced12aca6ac0eb1c2d1906793d8 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 23 Jun 2016 10:59:26 -0700 Subject: [PATCH 0729/1237] fix comments --- libraries/audio-client/src/AudioClient.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index ac055bb0f6..7628c09748 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -326,13 +326,13 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, const int HALF_FORTY_FOUR = FORTY_FOUR / 2; if (audioDevice.supportedSampleRates().contains(AudioConstants::SAMPLE_RATE * 2)) { - // use 48, which is a sample downsample, upsample + // use 48, which is a simple downsample, upsample adjustedAudioFormat.setSampleRate(AudioConstants::SAMPLE_RATE * 2); } else if (audioDevice.supportedSampleRates().contains(HALF_FORTY_FOUR)) { // use 22050, resample but closer to 24 adjustedAudioFormat.setSampleRate(HALF_FORTY_FOUR); } else if (audioDevice.supportedSampleRates().contains(FORTY_FOUR)) { - // use 48000, resample + // use 44100, resample adjustedAudioFormat.setSampleRate(FORTY_FOUR); } #endif From 2fd38c1585f0f1d3b2a3da4df98777c5ca491050 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 23 Jun 2016 13:10:15 -0700 Subject: [PATCH 0730/1237] when adding or editing an entity from a script, make the _lastEdited in the transmitted properties be the same as the one in the Entity in the local tree --- libraries/entities/src/EntityScriptingInterface.cpp | 2 ++ libraries/entities/src/EntityTree.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index e0863041a1..55d93d5b5b 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -179,6 +179,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } entity->setLastBroadcast(usecTimestampNow()); + propertiesWithSimID.setLastEdited(entity->getLastEdited()); } else { qCDebug(entities) << "script failed to add new Entity to local Octree"; success = false; @@ -376,6 +377,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& properties.setQueryAACube(entity->getQueryAACube()); } entity->setLastBroadcast(usecTimestampNow()); + properties.setLastEdited(entity->getLastEdited()); // if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server // if they've changed. diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index ad0f929e59..7ebfecbe8e 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -898,7 +898,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c entityItemID, properties); endDecode = usecTimestampNow(); - const quint64 LAST_EDITED_SERVERSIDE_BUMP = 10000; // usec + const quint64 LAST_EDITED_SERVERSIDE_BUMP = 1; // usec if (!senderNode->getCanRez() && senderNode->getCanRezTmp()) { // this node is only allowed to rez temporary entities. if need be, cap the lifetime. if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || From 284fcc8a0b3118aa28f77ca8e3a951565febf6a8 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 23 Jun 2016 13:50:17 -0700 Subject: [PATCH 0731/1237] better check for oculus display in oculus legacy plugin because the sdk is buggy --- .../src/OculusLegacyDisplayPlugin.cpp | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index f1a803ee19..a994198957 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -66,18 +66,47 @@ bool OculusLegacyDisplayPlugin::isSupported() const { } auto hmd = ovrHmd_Create(0); + + // The Oculus SDK seems to have trouble finding the right screen sometimes, so we have to guess + // Guesses, in order of best match: + // - resolution and position match + // - resolution and one component of position match + // - resolution matches + // - position matches + QList matches({ -1, -1, -1, -1 }); if (hmd) { QPoint targetPosition{ hmd->WindowsPos.x, hmd->WindowsPos.y }; + QSize targetResolution{ hmd->Resolution.w, hmd->Resolution.h }; auto screens = qApp->screens(); for(int i = 0; i < screens.size(); ++i) { auto screen = screens[i]; QPoint position = screen->geometry().topLeft(); - if (position == targetPosition) { - _hmdScreen = i; - break; + QSize resolution = screen->geometry().size(); + + if (position == targetPosition && resolution == targetResolution) { + matches[0] = i; + } else if ((position.x() == targetPosition.x() || position.y() == targetPosition.y()) && + resolution == targetResolution) { + matches[1] = i; + } else if (resolution == targetResolution) { + matches[2] = i; + } else if (position == targetPosition) { + matches[3] = i; } } } + + for (int screen : matches) { + if (screen != -1) { + _hmdScreen = screen; + break; + } + } + + if (_hmdScreen == -1) { + qDebug() << "Could not find Rift screen"; + result = false; + } ovr_Shutdown(); return result; From fc42a3aef52759e8f8cda7a665deeede5377345e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 23 Jun 2016 15:36:47 -0700 Subject: [PATCH 0732/1237] Grab script hotspot work * Updated grab/equip logic to use sphere vs sphere tests, instead of sphere vs entity bounding box. * Added debug flag for visualizing grab spheres. * hotspot overlays are now updated as the objects they are attached to move. * You can now use the search beam to near grab large objects, as well as the sphere sphere test. * Optimized EntityPropertyCache to make a single call to Entities.getEntityProperties instead of three. * Moved grab script options from the "Developer > Hands" menu to the "Developer > Grab Script" menu. --- .../system/controllers/handControllerGrab.js | 312 +++++++++++------- 1 file changed, 200 insertions(+), 112 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 31f8ebd73f..1772e55015 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,10 +10,9 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, Xform */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ Script.include("/~/system/libraries/utils.js"); -Script.include("../libraries/Xform.js"); // // add lines where the hand ray picking is happening @@ -38,6 +37,9 @@ var THUMB_ON_VALUE = 0.5; var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. var PICK_WITH_HAND_RAY = true; + +var DRAW_GRAB_SPHERES = false; +var DRAW_HAND_SPHERES = false; var DROP_WITHOUT_SHAKE = false; // @@ -68,16 +70,20 @@ var LINE_ENTITY_DIMENSIONS = { var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray + // // near grabbing // -var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected +var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. + var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position -var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected + +var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. +var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. + var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed -var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds // @@ -254,7 +260,8 @@ function restore2DMode() { } } -// constructor +// EntityPropertiesCache is a helper class that contains a cache of entity properties. +// the hope is to prevent excess calls to Entity.getEntityProperties() function EntityPropertiesCache() { this.cache = {}; } @@ -265,34 +272,55 @@ EntityPropertiesCache.prototype.findEntities = function(position, radius) { var entities = Entities.findEntities(position, radius); var _this = this; entities.forEach(function (x) { - _this.addEntity(x); + _this.updateEntity(x); }); }; -EntityPropertiesCache.prototype.addEntity = function(entityID) { +EntityPropertiesCache.prototype.updateEntity = function(entityID) { var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); - var grabbableProps = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA); - var grabProps = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - var wearableProps = getEntityCustomData("wearable", entityID, {}); - this.cache[entityID] = { props: props, grabbableProps: grabbableProps, grabProps: grabProps, wearableProps: wearableProps }; + + // convert props.userData from a string to an object. + var userData = {}; + if (props.userData) { + try { + userData = JSON.parse(props.userData); + } catch(err) { + print("WARNING: malformed userData on " + entityID + ", name = " + props.name + ", error = " + err); + } + } + props.userData = userData; + + this.cache[entityID] = props; }; EntityPropertiesCache.prototype.getEntities = function() { return Object.keys(this.cache); } EntityPropertiesCache.prototype.getProps = function(entityID) { var obj = this.cache[entityID] - return obj ? obj.props : undefined; + return obj ? obj : undefined; }; EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { - var obj = this.cache[entityID] - return obj ? obj.grabbableProps : undefined; + var props = this.cache[entityID]; + if (props) { + return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA; + } else { + return undefined; + } }; EntityPropertiesCache.prototype.getGrabProps = function(entityID) { - var obj = this.cache[entityID] - return obj ? obj.grabProps : undefined; + var props = this.cache[entityID]; + if (props) { + return props.userData.grabKey ? props.userData.grabKey : {}; + } else { + return undefined; + } }; EntityPropertiesCache.prototype.getWearableProps = function(entityID) { - var obj = this.cache[entityID] - return obj ? obj.wearableProps : undefined; + var props = this.cache[entityID]; + if (props) { + return props.userData.wearable ? props.userData.wearable : {}; + } else { + return undefined; + } }; function MyController(hand) { @@ -323,7 +351,6 @@ function MyController(hand) { //for visualizations this.overlayLine = null; this.particleBeamObject = null; - this.grabSphere = null; //for lights this.spotlight = null; @@ -384,7 +411,7 @@ function MyController(hand) { } this.setState = function(newState, reason) { - this.grabSphereOff(); + if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); @@ -491,39 +518,6 @@ function MyController(hand) { } } - this.grabSphereOn = function() { - var color = {red: 0, green: 255, blue: 0}; - if (this.grabSphere === null) { - var sphereProperties = { - position: this.getHandPosition(), - size: GRAB_RADIUS*2, - color: color, - alpha: 0.1, - solid: true, - ignoreRayIntersection: true, - drawInFront: true, // Even when burried inside of something, show it. - visible: true - } - this.grabSphere = Overlays.addOverlay("sphere", sphereProperties); - } else { - Overlays.editOverlay(this.grabSphere, { - position: this.getHandPosition(), - size: GRAB_RADIUS*2, - color: color, - alpha: 0.1, - solid: true, - visible: true - }); - } - } - - this.grabSphereOff = function() { - if (this.grabSphere !== null) { - Overlays.deleteOverlay(this.grabSphere); - this.grabSphere = null; - } - }; - this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { @@ -873,51 +867,123 @@ function MyController(hand) { } }; - this.searchEnter = function() { - this.equipHotspotOverlays = []; + this.createHotspots = function () { + + var HAND_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var HAND_SPHERE_ALPHA = 0.7; + var HAND_SPHERE_RADIUS = 0.01; + + var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var EQUIP_SPHERE_ALPHA = 0.3; + + var NEAR_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; + var NEAR_SPHERE_ALPHA = 0.1; + + this.hotspotOverlays = []; + + if (DRAW_HAND_SPHERES) { + // add tiny spheres around the palm. + var handPosition = this.getHandPosition(); + var overlay = Overlays.addOverlay("sphere", { + position: handPosition, + size: HAND_SPHERE_RADIUS * 2, + color: HAND_SPHERE_COLOR, + alpha: HAND_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + this.hotspotOverlays.push({ + entityID: undefined, + overlay: overlay, + type: "hand" + }); + } // find entities near the avatar that might be equipable. - var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); - var i, l = entities.length; - for (i = 0; i < l; i++) { - var props = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); - // does this entity have an attach point? - var wearableData = getEntityCustomData("wearable", entities[i], undefined); - if (wearableData && wearableData.joints) { - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (wearableData.joints[handJointName]) { - var handOffsetPos = wearableData.joints[handJointName][0]; - var handOffsetRot = wearableData.joints[handJointName][1]; + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); - var handOffsetXform = new Xform(handOffsetRot, handOffsetPos); - var objectXform = new Xform(props.rotation, props.position); - var overlayXform = Xform.mul(objectXform, handOffsetXform.inv()); + var _this = this; + this.entityPropertyCache.getEntities().forEach(function (entityID) { + if (_this.entityIsEquippableWithoutDistanceCheck(entityID)) { + var props = _this.entityPropertyCache.getProps(entityID); - // draw the hotspot - this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { - rotation: overlayXform.rot, - position: overlayXform.pos, - size: 0.2, - color: { red: 90, green: 255, blue: 90 }, - alpha: 0.7, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - })); - } + overlay = Overlays.addOverlay("sphere", { + rotation: props.rotation, + position: props.position, + size: EQUIP_RADIUS * 2, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + _this.hotspotOverlays.push({ + entityID: entityID, + overlay: overlay, + type: "equip" + }); } - } + + if (DRAW_GRAB_SPHERES && _this.entityIsGrabbable(entityID)) { + var props = _this.entityPropertyCache.getProps(entityID); + + overlay = Overlays.addOverlay("sphere", { + rotation: props.rotation, + position: props.position, + size: NEAR_GRAB_RADIUS * 2, + color: NEAR_SPHERE_COLOR, + alpha: NEAR_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + _this.hotspotOverlays.push({ + entityID: entityID, + overlay: overlay, + type: "near" + }); + } + }); + }; + + this.updateHotspots = function() { + var _this = this; + this.hotspotOverlays.forEach(function (overlayInfo) { + if (overlayInfo.type === "hand") { + Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); + } else if (overlayInfo.type === "equip") { + _this.entityPropertyCache.updateEntity(overlayInfo.entityID); + var props = _this.entityPropertyCache.getProps(overlayInfo.entityID); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + } else if (overlayInfo.type === "near") { + _this.entityPropertyCache.updateEntity(overlayInfo.entityID); + var props = _this.entityPropertyCache.getProps(overlayInfo.entityID); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + } + }); + }; + + this.destroyHotspots = function() { + this.hotspotOverlays.forEach(function (overlayInfo) { + Overlays.deleteOverlay(overlayInfo.overlay); + }); + this.hotspotOverlays = []; + }; + + this.searchEnter = function() { + this.createHotspots(); }; this.searchExit = function() { - - // delete all equip hotspots - var i, l = this.equipHotspotOverlays.length; - for (i = 0; i < l; i++) { - Overlays.deleteOverlay(this.equipHotspotOverlays[i]); - } - this.equipHotspotOverlays = []; + this.destroyHotspots(); }; /// @@ -984,7 +1050,13 @@ function MyController(hand) { return grabbableProps && grabbableProps.wantsTrigger; }; - this.entityIsEquippable = function (entityID, handPosition) { + this.entityIsEquippableWithoutDistanceCheck = function (entityID) { + var props = this.entityPropertyCache.getProps(entityID); + var handPosition = props.position; + return this.entityIsEquippableWithDistanceCheck(entityID, handPosition); + }; + + this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition) { var props = this.entityPropertyCache.getProps(entityID); var distance = Vec3.distance(props.position, handPosition); var grabProps = this.entityPropertyCache.getGrabProps(entityID); @@ -998,9 +1070,9 @@ function MyController(hand) { return false; } - if (distance > NEAR_PICK_MAX_DISTANCE) { + if (distance > EQUIP_RADIUS) { if (debug) { - print("equip is skipping '" + props.name + "': too far away."); + print("equip is skipping '" + props.name + "': too far away, " + distance + " meters"); } return false; } @@ -1113,7 +1185,7 @@ function MyController(hand) { return true; }; - this.entityIsNearGrabbable = function(entityID, handPosition) { + this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { if (!this.entityIsGrabbable(entityID)) { return false; @@ -1123,7 +1195,7 @@ function MyController(hand) { var distance = Vec3.distance(props.position, handPosition); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - if (distance > NEAR_PICK_MAX_DISTANCE) { + if (distance > maxDistance) { // too far away, don't grab if (debug) { print(" grab is skipping '" + props.name + "': too far away."); @@ -1138,6 +1210,8 @@ function MyController(hand) { var _this = this; var name; + this.updateHotspots(); + this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; @@ -1151,16 +1225,12 @@ function MyController(hand) { var handPosition = this.getHandPosition(); - if (SHOW_GRAB_SPHERE) { - this.grabSphereOn(); - } - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS); + this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); var candidateEntities = this.entityPropertyCache.getEntities(); var equippableEntities = candidateEntities.filter(function (entity) { - return _this.entityIsEquippable(entity, handPosition); + return _this.entityIsEquippableWithDistanceCheck(entity, handPosition); }); var entity; @@ -1181,16 +1251,19 @@ function MyController(hand) { } } + var grabbableEntities = candidateEntities.filter(function (entity) { + return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_RADIUS); + }); + var rayPickInfo = this.calcRayPickInfo(this.hand); this.intersectionDistance = rayPickInfo.distance; - if (rayPickInfo.entityID) { - candidateEntities.push(rayPickInfo.entityID); - this.entityPropertyCache.addEntity(rayPickInfo.entityID); - } - var grabbableEntities = candidateEntities.filter(function (entity) { - return _this.entityIsNearGrabbable(entity, handPosition); - }); + if (rayPickInfo.entityID) { + this.entityPropertyCache.updateEntity(rayPickInfo.entityID); + if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { + grabbableEntities.push(rayPickInfo.entityID); + } + } if (grabbableEntities.length > 0) { // sort by distance @@ -1732,11 +1805,11 @@ function MyController(hand) { this.lastUnequipCheckTime = now; if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) { + Vec3.length(props.localPosition) > NEAR_GRAB_PICK_RADIUS * 2.0) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to autoequip -- we also // need to fail the findEntities test. - var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, EQUIP_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + @@ -2182,17 +2255,32 @@ function cleanup() { Script.scriptEnding.connect(cleanup); Script.update.connect(update); +if (!Menu.menuExists("Developer > Grab Script")) { + Menu.addMenu("Developer > Grab Script"); +} + Menu.addMenuItem({ - menuName: "Developer > Hands", + menuName: "Developer > Grab Script", menuItemName: "Drop Without Shake", isCheckable: true, isChecked: DROP_WITHOUT_SHAKE }); +Menu.addMenuItem({ + menuName: "Developer > Grab Script", + menuItemName: "Draw Grab Spheres", + isCheckable: true, + isChecked: DRAW_GRAB_SPHERES +}); + function handleMenuItemEvent(menuItem) { if (menuItem === "Drop Without Shake") { DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); } + if (menuItem === "Draw Grab Spheres") { + DRAW_GRAB_SPHERES = Menu.isOptionChecked("Draw Grab Spheres"); + DRAW_HAND_SPHERES = DRAW_GRAB_SPHERES; + } } Menu.menuItemEvent.connect(handleMenuItemEvent); From f8391f00629535b9849d928b0075c0f032dc61f2 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 23 Jun 2016 16:51:15 -0700 Subject: [PATCH 0733/1237] fix reticle bugs --- interface/src/Application.cpp | 26 +++++++++---------- interface/src/Application.h | 1 + libraries/ui/src/OffscreenUi.cpp | 5 ++-- .../controllers/handControllerPointer.js | 21 +++++++++++---- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 37c3b361bf..26da43fb58 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -921,17 +921,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : cycleCamera(); } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { if (!offscreenUi->navigationFocused()) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); + toggleMenuUnderReticle(); } } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); - } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { - if (!offscreenUi->navigationFocused()) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); - } + toggleMenuUnderReticle(); } else if (action == controller::toInt(controller::Action::RETICLE_X)) { auto oldPos = getApplicationCompositor().getReticlePosition(); getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); @@ -1240,7 +1233,16 @@ QString Application::getUserAgent() { return userAgent; } - +void Application::toggleMenuUnderReticle() const { + // In HMD, if the menu is near the mouse but not under it, the reticle can be at a significantly + // different depth. When you focus on the menu, the cursor can appear to your crossed eyes as both + // on the menu and off. + // Even in 2D, it is arguable whether the user would want the menu to be to the side. + const float X_LEFT_SHIFT = 50.0; + auto offscreenUi = DependencyManager::get(); + auto reticlePosition = getApplicationCompositor().getReticlePosition(); + offscreenUi->toggleMenu(QPoint(reticlePosition.x - X_LEFT_SHIFT, reticlePosition.y)); +} void Application::checkChangeCursor() { QMutexLocker locker(&_changeCursorLock); @@ -2462,9 +2464,7 @@ void Application::keyPressEvent(QKeyEvent* event) { void Application::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) { - auto offscreenUi = DependencyManager::get(); - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); + toggleMenuUnderReticle(); } _keysPressed.remove(event->key()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 5beaa5b455..6857ba2a3a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -408,6 +408,7 @@ private: static void dragEnterEvent(QDragEnterEvent* event); void maybeToggleMenuVisible(QMouseEvent* event) const; + void toggleMenuUnderReticle() const; MainWindow* _window; QElapsedTimer& _sessionRunTimer; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index fa1a31d196..1a7d4b2328 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -492,10 +492,9 @@ void OffscreenUi::unfocusWindows() { Q_ASSERT(invokeResult); } -void OffscreenUi::toggleMenu(const QPoint& screenPosition) { +void OffscreenUi::toggleMenu(const QPoint& screenPosition) { // caller should already have mapped using getReticlePosition emit showDesktop(); // we really only want to do this if you're showing the menu, but for now this works - auto virtualPos = mapToVirtualScreen(screenPosition, nullptr); - QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, virtualPos)); + QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, screenPosition)); } diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 374be0d1a1..a0f1f47b3c 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -208,9 +208,9 @@ function isShakingMouse() { // True if the person is waving the mouse around try return isShaking; } var NON_LINEAR_DIVISOR = 2; -var MINIMUM_SEEK_DISTANCE = 0.01; -function updateSeeking() { - if (!Reticle.visible || isShakingMouse()) { +var MINIMUM_SEEK_DISTANCE = 0.1; +function updateSeeking(doNotStartSeeking) { + if (!doNotStartSeeking && (!Reticle.visible || isShakingMouse())) { if (!isSeeking) { print('Start seeking mouse.'); isSeeking = true; @@ -224,8 +224,8 @@ function updateSeeking() { if (!lookAt2D) { // If this happens, something has gone terribly wrong. print('Cannot seek without lookAt position'); isSeeking = false; - return; - } // E.g., if parallel to location in HUD + return; // E.g., if parallel to location in HUD + } var copy = Reticle.position; function updateDimension(axis) { var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; @@ -353,6 +353,16 @@ clickMapping.from(rightTrigger.full).when(isPointingAtOverlayStartedNonFullTrigg clickMapping.from(leftTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(leftTrigger)).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); +clickMapping.from(Controller.Hardware.Keyboard.RightMouseClicked).peek().to(function () { + // Allow the reticle depth to be set correctly: + // Wait a tick for the context menu to be displayed, and then simulate a (non-hand-controller) mouse move + // so that the system updates qml state (Reticle.pointingAtSystemOverlay) before it gives us a mouseMove. + // We don't want the system code to always do this for us, because, e.g., we do not want to get a mouseMove + // after the Left/RightSecondaryThumb gives us a context menu. Only from the mouse. + Script.setTimeout(function () { + Reticle.setPosition(Reticle.position); + }, 0); +}); // Partial smoothed trigger is activation. clickMapping.from(rightTrigger.partial).to(makeToggleAction(Controller.Standard.RightHand)); clickMapping.from(leftTrigger.partial).to(makeToggleAction(Controller.Standard.LeftHand)); @@ -386,6 +396,7 @@ function update() { expireMouseCursor(); clearSystemLaser(); } + updateSeeking(true); if (!handControllerLockOut.expired(now)) { return off(); // Let them use mouse it in peace. } From 7bd553c09cea7784828354085901adf788f37a0c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 23 Jun 2016 17:03:15 -0700 Subject: [PATCH 0734/1237] near grab logic to uses sphere vs entity box instead of sphere vs sphere. Adjusted debug drawing accordingly. --- .../system/controllers/handControllerGrab.js | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 26f977f9b0..b1cf3927d9 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -38,7 +38,7 @@ var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only var PICK_WITH_HAND_RAY = true; -var DRAW_GRAB_SPHERES = false; +var DRAW_GRAB_BOXES = false; var DRAW_HAND_SPHERES = false; var DROP_WITHOUT_SHAKE = false; @@ -80,6 +80,8 @@ var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. +var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand + var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object @@ -870,26 +872,48 @@ function MyController(hand) { this.createHotspots = function () { var props, overlay; - var HAND_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; - var HAND_SPHERE_ALPHA = 0.7; - var HAND_SPHERE_RADIUS = 0.01; + var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var HAND_EQUIP_SPHERE_ALPHA = 0.7; + var HAND_EQUIP_SPHERE_RADIUS = 0.01; + + var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; + var HAND_GRAB_SPHERE_ALPHA = 0.3; + var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; var EQUIP_SPHERE_ALPHA = 0.3; - var NEAR_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; - var NEAR_SPHERE_ALPHA = 0.1; + var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; + var GRAB_BOX_ALPHA = 0.1; this.hotspotOverlays = []; if (DRAW_HAND_SPHERES) { - // add tiny spheres around the palm. + // add tiny green sphere around the palm. var handPosition = this.getHandPosition(); overlay = Overlays.addOverlay("sphere", { position: handPosition, - size: HAND_SPHERE_RADIUS * 2, - color: HAND_SPHERE_COLOR, - alpha: HAND_SPHERE_ALPHA, + size: HAND_EQUIP_SPHERE_RADIUS * 2, + color: HAND_EQUIP_SPHERE_COLOR, + alpha: HAND_EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + this.hotspotOverlays.push({ + entityID: undefined, + overlay: overlay, + type: "hand" + }); + + // add larger blue sphere around the palm. + overlay = Overlays.addOverlay("sphere", { + position: handPosition, + size: HAND_GRAB_SPHERE_RADIUS * 2, + color: HAND_GRAB_SPHERE_COLOR, + alpha: HAND_GRAB_SPHERE_ALPHA, solid: true, visible: true, ignoreRayIntersection: true, @@ -931,15 +955,15 @@ function MyController(hand) { }); } - if (DRAW_GRAB_SPHERES && _this.entityIsGrabbable(entityID)) { + if (DRAW_GRAB_BOXES && _this.entityIsGrabbable(entityID)) { props = _this.entityPropertyCache.getProps(entityID); - overlay = Overlays.addOverlay("sphere", { + overlay = Overlays.addOverlay("cube", { rotation: props.rotation, position: props.position, - size: NEAR_GRAB_RADIUS * 2, - color: NEAR_SPHERE_COLOR, - alpha: NEAR_SPHERE_ALPHA, + size: props.dimensions, //{x: props.dimensions.x, y: props.dimensions.y, z: props.dimensions.z}, + color: GRAB_BOX_COLOR, + alpha: GRAB_BOX_ALPHA, solid: true, visible: true, ignoreRayIntersection: true, @@ -1254,7 +1278,7 @@ function MyController(hand) { } var grabbableEntities = candidateEntities.filter(function (entity) { - return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_RADIUS); + return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); }); var rayPickInfo = this.calcRayPickInfo(this.hand); @@ -1807,11 +1831,11 @@ function MyController(hand) { this.lastUnequipCheckTime = now; if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > NEAR_GRAB_PICK_RADIUS * 2.0) { + Vec3.length(props.localPosition) > NEAR_GRAB_MAX_DISTANCE) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to autoequip -- we also // need to fail the findEntities test. - var nearPickedCandidateEntities = Entities.findEntities(handPosition, EQUIP_RADIUS); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + @@ -2270,18 +2294,18 @@ Menu.addMenuItem({ Menu.addMenuItem({ menuName: "Developer > Grab Script", - menuItemName: "Draw Grab Spheres", + menuItemName: "Draw Grab Boxes", isCheckable: true, - isChecked: DRAW_GRAB_SPHERES + isChecked: DRAW_GRAB_BOXES }); function handleMenuItemEvent(menuItem) { if (menuItem === "Drop Without Shake") { DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); } - if (menuItem === "Draw Grab Spheres") { - DRAW_GRAB_SPHERES = Menu.isOptionChecked("Draw Grab Spheres"); - DRAW_HAND_SPHERES = DRAW_GRAB_SPHERES; + if (menuItem === "Draw Grab Boxes") { + DRAW_GRAB_BOXES = Menu.isOptionChecked("Draw Grab Boxes"); + DRAW_HAND_SPHERES = DRAW_GRAB_BOXES; } } From 884b9211c620b3b1655a5c699036cc2f6f7c715c Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 23 Jun 2016 17:13:48 -0700 Subject: [PATCH 0735/1237] Curvature is looking correct now without artefact, moving on to the lighting --- .../render-utils/src/DeferredGlobalLight.slh | 45 +++++++++++++- .../src/DeferredLightingEffect.cpp | 15 +++-- .../render-utils/src/DeferredLightingEffect.h | 18 ++++-- .../render-utils/src/FramebufferCache.cpp | 2 +- .../render-utils/src/RenderDeferredTask.cpp | 4 +- .../render-utils/src/SubsurfaceScattering.cpp | 20 ++++++ .../render-utils/src/SubsurfaceScattering.h | 11 +++- .../render-utils/src/SubsurfaceScattering.slh | 14 ++++- .../render-utils/src/SurfaceGeometryPass.h | 13 +--- .../src/surfaceGeometry_makeCurvature.slf | 54 +++------------- libraries/render/src/render/BlurTask.h | 5 +- libraries/render/src/render/Task.h | 3 +- .../render/configSlider/ConfigSlider.qml | 6 +- .../utilities/render/debugDeferredLighting.js | 20 ++++++ .../utilities/render/deferredLighting.qml | 62 +++++++++++++++++++ .../utilities/render/surfaceGeometryPass.qml | 13 +++- 16 files changed, 224 insertions(+), 81 deletions(-) create mode 100644 scripts/developer/utilities/render/debugDeferredLighting.js create mode 100644 scripts/developer/utilities/render/deferredLighting.qml diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index bfab0fafb8..55d21dd47f 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -157,6 +157,7 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur <$declareDeferredCurvature()$> <@include SubsurfaceScattering.slh@> <$declareSubsurfaceScatteringResource()$> +!> vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { // prepareGlobalLight @@ -169,7 +170,7 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f // Get light Light light = getLight(); vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value - float metallic = 1.0; + float metallic = 0.0; vec3 fragLightDir = -normalize(getLightDirection(light)); @@ -184,14 +185,52 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); + + /* vec3 rN = normalize(mix(normal, bentNormalHigh, bendFactorSpectrum.x)); + vec3 gN = normalize(mix(normal, bentNormalHigh, bendFactorSpectrum.y)); + vec3 bN = normalize(mix(normal, bentNormalHigh, bendFactorSpectrum.z)); + */ vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); // --> Look up the pre-integrated curvature-dependent BDRF textures - vec3 bdrf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + vec3 brdf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + + // The position of the pixel fragment in Eye space then in world space + + float scatteringLevel = getScatteringLevel(); + + vec4 shading; + float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + { // Key Sun Lighting + // Diffuse Lighting + //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir,halfDir); + float power = specularDistribution(roughness, fragNormal, halfDir); + vec3 specular = power * fresnelColor * standardDiffuse; + + shading = vec4(specular, (1 - fresnelColor.x)); + } + + if (scatteringLevel < 0.1) { + brdf = vec3(standardDiffuse); + } + vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); - return vec3(bdrf); + // Diffuse from ambient + // color += albedo * evalSphericalLight(getLightAmbientSphere(light), bentNormalHigh).xyz *getLightAmbientIntensity(light); + + // Specular highlight from ambient + vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, 1.0); + // color += specularLighting; + + if ( showBRDF()) + return brdf; + return vec3(color); } <@endfunc@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index cefceb901e..e9715e6dfe 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -350,7 +350,10 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC } -void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource) { +void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, + const DeferredFrameTransformPointer& frameTransform, + const gpu::TexturePointer& diffusedCurvature2, + const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource) { auto args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { @@ -390,7 +393,7 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c // Subsurface scattering specific batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, framebufferCache->getCurvatureTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, framebufferCache->getCurvatureTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, diffusedCurvature2); batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, subsurfaceScatteringResource->getParametersBuffer()); @@ -621,17 +624,21 @@ void RenderDeferred::configure(const Config& config) { glm::vec2 curvatureInfo(config.curvatureOffset, config.curvatureScale); _subsurfaceScatteringResource->setCurvatureFactors(curvatureInfo); + _subsurfaceScatteringResource->setLevel((float)config.enableScattering); + _subsurfaceScatteringResource->setShowBRDF(config.showScatteringBRDF); _enablePointLights = config.enablePointLights; _enableSpotLights = config.enableSpotLights; } -void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFrameTransformPointer& deferredTransform) { +void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { if (!_subsurfaceScatteringResource->getScatteringTable()) { _subsurfaceScatteringResource->generateScatteringTable(renderContext->args); } - setupJob.run(sceneContext, renderContext, deferredTransform, _subsurfaceScatteringResource); + auto& deferredTransform = inputs.get0(); + auto& diffusedCurvature2 = inputs.get2()->getRenderBuffer(0); + setupJob.run(sceneContext, renderContext, deferredTransform, diffusedCurvature2, _subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform, _enablePointLights, _enableSpotLights); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 223b356d49..da1f4ddfd7 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -115,7 +115,9 @@ class RenderDeferredSetup { public: // using JobModel = render::Job::ModelI; - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, + const gpu::TexturePointer& diffusedCurvature2, + const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource); }; class RenderDeferredLocals { @@ -144,9 +146,13 @@ class RenderDeferredConfig : public render::Job::Config { Q_PROPERTY(float curvatureOffset MEMBER curvatureOffset NOTIFY dirty) Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) - + Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) + Q_PROPERTY(bool showScatteringBRDF MEMBER showScatteringBRDF NOTIFY dirty) + Q_PROPERTY(bool enablePointLights MEMBER enablePointLights NOTIFY dirty) Q_PROPERTY(bool enableSpotLights MEMBER enableSpotLights NOTIFY dirty) + + public: RenderDeferredConfig() : render::Job::Config(true) {} @@ -158,6 +164,9 @@ public: float curvatureOffset{ 0.08f }; float curvatureScale{ 0.8f }; + bool enableScattering{ true }; + bool showScatteringBRDF{ false }; + bool enablePointLights{ true }; bool enableSpotLights{ true }; @@ -168,14 +177,15 @@ signals: class RenderDeferred { public: + using Inputs = render::VaryingSet3 < DeferredFrameTransformPointer, gpu::FramebufferPointer, gpu::FramebufferPointer >; using Config = RenderDeferredConfig; - using JobModel = render::Job::ModelI; + using JobModel = render::Job::ModelI; RenderDeferred(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); RenderDeferredSetup setupJob; RenderDeferredLocals lightsJob; diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 21b0b4e052..fa95a66b0b 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -107,7 +107,7 @@ void FramebufferCache::createPrimaryFramebuffer() { // For AO: auto pointMipSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT); - _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _depthPyramidFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _depthPyramidFramebuffer->setRenderBuffer(0, _depthPyramidTexture); _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 09129209c4..3e74701246 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -123,11 +123,11 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - // const auto scatteringInputs = render::Varying(SubsurfaceScattering::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer)); + const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer)); // const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); // DeferredBuffer is complete, now let's shade it into the LightingBuffer - addJob("RenderDeferred", deferredFrameTransform); + addJob("RenderDeferred", deferredLightingInputs); // AA job to be revisited diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 25df2c388b..a164484eb4 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -64,6 +64,26 @@ glm::vec2 SubsurfaceScatteringResource::getCurvatureFactors() const { return _parametersBuffer.get().curvatureInfo; } + +void SubsurfaceScatteringResource::setLevel(float level) { + if (level != getLevel()) { + _parametersBuffer.edit().level = level; + } +} +float SubsurfaceScatteringResource::getLevel() const { + return _parametersBuffer.get().level; +} + +void SubsurfaceScatteringResource::setShowBRDF(bool show) { + if (show != isShowBRDF()) { + _parametersBuffer.edit().showBRDF = show; + } +} +bool SubsurfaceScatteringResource::isShowBRDF() const { + return (bool)_parametersBuffer.get().showBRDF; +} + + void SubsurfaceScatteringResource::generateScatteringTable(RenderArgs* args) { if (!_scatteringTable) { _scatteringTable = generatePreIntegratedScattering(args); diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index c0ce14cc8c..881be391b8 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -29,6 +29,14 @@ public: void setCurvatureFactors(const glm::vec2& sbCurvatureFactors); glm::vec2 getCurvatureFactors() const; + void setLevel(float level); + float getLevel() const; + + + void setShowBRDF(bool show); + bool isShowBRDF() const; + + UniformBufferView getParametersBuffer() const { return _parametersBuffer; } gpu::TexturePointer getScatteringTable() const { return _scatteringTable; } @@ -45,7 +53,8 @@ protected: public: glm::vec4 normalBentInfo{ 1.5f, 0.8f, 0.3f, 1.5f }; glm::vec2 curvatureInfo{ 0.08f, 0.8f }; - glm::vec2 spare{ 0.0f }; + float level{ 1.0f }; + float showBRDF{ 0.0f }; Parameters() {} }; diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index 985d3d5404..bcb16a583c 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -13,7 +13,7 @@ uniform sampler2D scatteringLUT; vec3 fetchBRDF(float LdotN, float curvature) { - return texture(scatteringLUT, vec2( LdotN * 0.5 + 0.5, curvature)).xyz; + return texture(scatteringLUT, vec2( clamp(LdotN * 0.5 + 0.5, 0.0, 1.0), curvature)).xyz; } vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { @@ -27,7 +27,7 @@ vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { // Subsurface Scattering parameters struct ScatteringParameters { vec4 normalBendInfo; // R, G, B, factor - vec4 curvatureInfo;// Offset, Scale + vec4 curvatureInfo;// Offset, Scale, level }; uniform subsurfaceScatteringParametersBuffer { @@ -38,8 +38,16 @@ vec3 getBendFactor() { return parameters.normalBendInfo.xyz * parameters.normalBendInfo.w; } +float getScatteringLevel() { + return parameters.curvatureInfo.z; +} + +bool showBRDF() { + return parameters.curvatureInfo.w > 0.0; +} + float unpackCurvature(float packedCurvature) { - return abs(packedCurvature * 2 - 1) * 0.5f * parameters.curvatureInfo.y + parameters.curvatureInfo.x; + return abs(packedCurvature * 2 - 1) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; } <@endfunc@> diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index b2d72891bd..c678ebba0f 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -28,7 +28,7 @@ public: float depthThreshold{ 0.1f }; float basisScale{ 1.0f }; - float curvatureScale{ 1.0f }; // Mean curvature value scaling (SI SI Dimension is [1/meters]) + float curvatureScale{ 10.0f }; double getGpuTime() { return gpuTime; } @@ -63,16 +63,7 @@ private: glm::vec4 resolutionInfo { -1.0f, 0.0f, 0.0f, 0.0f }; // Curvature algorithm glm::vec4 curvatureInfo{ 0.0f }; - // Dithering info - glm::vec4 ditheringInfo { 0.0f, 0.0f, 0.01f, 1.0f }; - // Sampling info - glm::vec4 sampleInfo { 11.0f, 1.0f/11.0f, 7.0f, 1.0f }; - // Blurring info - glm::vec4 blurInfo { 1.0f, 3.0f, 2.0f, 0.0f }; - // gaussian distribution coefficients first is the sampling radius (max is 6) - const static int GAUSSIAN_COEFS_LENGTH = 8; - float _gaussianCoefs[GAUSSIAN_COEFS_LENGTH]; - + Parameters() {} }; gpu::BufferView _parametersBuffer; diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index d4694b4c94..8ca4ddcd7a 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -17,14 +17,6 @@ struct SurfaceGeometryParams { vec4 resolutionInfo; // Curvature algorithm vec4 curvatureInfo; - // Dithering info - vec4 ditheringInfo; - // Sampling info - vec4 sampleInfo; - // Blurring info - vec4 blurInfo; - // gaussian distribution coefficients first is the sampling radius (max is 6) - vec4 _gaussianCoefs[2]; }; uniform surfaceGeometryParamsBuffer { @@ -114,14 +106,12 @@ void main(void) { // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); - // float nearPlaneScale = min(-Zeye / getCurvatureBasisScale(), 1.0); float nearPlaneScale = 0.5 * getProjectionNear(); vec3 worldNormal = getWorldNormal(frameTexcoordPos); // The position of the pixel fragment in Eye space then in world space vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); - // vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; // Calculate the perspective scale. // Clamp to 0.5 @@ -140,24 +130,9 @@ void main(void) { dFdu *= step(abs(dFdu.w), threshold); dFdv *= step(abs(dFdv.w), threshold); - //outFragColor = vec4(du.x, du.y, 0.0, 1.0); - // outFragColor = vec4(viewportScale, 0.0, 1.0); - /* if (perspectiveScale < getCurvatureBasisScale()) { - //outFragColor = vec4(0.0, 0.0, 4 * perspectiveScale, 1.0); - } else if (perspectiveScale < 0.5) { - outFragColor = vec4(0.0, 0.0, 2 * perspectiveScale, 1.0); - return; - } else if (perspectiveScale > 1.0) { - outFragColor = vec4(perspectiveScale, 0.0, 0.0, 1.0);s - return; - } else { - outFragColor = vec4(0.0, 0.5 * perspectiveScale, 0.0, 1.0); - return; - }*/ // Calculate ( du/dx, du/dy, du/dz ) and ( dv/dx, dv/dy, dv/dz ) - // Eval px, py, pz world positions of the basis centered on the world pos of the fragment - float axeLength = /*getCurvatureBasisScale() * */ nearPlaneScale; + float axeLength = nearPlaneScale; vec3 ax = (frameTransform._view[0].xyz * axeLength); vec3 ay = (frameTransform._view[1].xyz * axeLength); @@ -176,21 +151,7 @@ void main(void) { return; */ - float nearZ = -getProjectionNear(); - vec3 axeSign = vec3(1.0); - /* if (px.z >= nearZ) { - px = vec4(eyePos - ax, 0.0); - axeSign.x = -1.0; - } - if (py.z >= nearZ) { - py = vec4(eyePos - ay, 0.0); - axeSign.y = -1.0; - } - if (pz.z >= nearZ) { - pz = vec4(eyePos - az, 0.0); - axeSign.z = -1.0; - }*/ - + /* IN case the axis end point goes behind mid way near plane, this shouldn't happen if (px.z >= -nearPlaneScale) { outFragColor = vec4(1.0, 0.0, 0.0, 1.0); return; @@ -200,7 +161,7 @@ void main(void) { } else if (pz.z >= -nearPlaneScale) { outFragColor = vec4(0.0, 0.0, 1.0, 1.0); return; - } + }*/ // Project px, py pz to homogeneous clip space @@ -215,6 +176,7 @@ void main(void) { pz.xy /= pz.w; vec2 nclipPos = (texcoordPos - 0.5) * 2.0; + /* if (texcoordPos.y > 0.5) { outFragColor = vec4(px.xy * 0.5 + 0.5, 0.0, 1.0); @@ -224,10 +186,10 @@ void main(void) { return; */ - float pixPerspectiveScaleInv = 1.0 / (perspectiveScale * nearPlaneScale); - px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv * axeSign.x; - py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv * axeSign.y; - pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv * axeSign.z; + float pixPerspectiveScaleInv = 1.0 / (perspectiveScale); + px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv; + py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv; + pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv; // Calculate dF/dx, dF/dy and dF/dz using chain rule vec4 dFdx = dFdu * px.x + dFdv * px.y; diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index aea596cc2f..fe3537e301 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -73,10 +73,12 @@ public: class BlurGaussianConfig : public Job::Config { Q_OBJECT - Q_PROPERTY(bool enabled MEMBER enabled NOTIFY dirty) // expose enabled flag + Q_PROPERTY(bool enabled WRITE setEnabled READ isEnabled NOTIFY dirty) // expose enabled flag Q_PROPERTY(float filterScale MEMBER filterScale NOTIFY dirty) // expose enabled flag public: + BlurGaussianConfig() : Job::Config(true) {} + float filterScale{ 1.2f }; signals : void dirty(); @@ -112,6 +114,7 @@ class BlurGaussianDepthAwareConfig : public BlurGaussianConfig { Q_OBJECT Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) // expose enabled flag public: + BlurGaussianDepthAwareConfig() : BlurGaussianConfig() {} float depthThreshold{ 2.0f }; signals: diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index a496e1d876..74eefd86bc 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -216,6 +216,7 @@ public: JobConfig(bool enabled) : alwaysEnabled{ false }, enabled{ enabled } {} bool isEnabled() { return alwaysEnabled || enabled; } + void setEnabled(bool enable) { enabled = enable; } bool alwaysEnabled{ true }; bool enabled{ true }; @@ -344,7 +345,7 @@ public: void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { renderContext->jobConfig = std::static_pointer_cast(_config); - if (renderContext->jobConfig->alwaysEnabled || renderContext->jobConfig->enabled) { + if (renderContext->jobConfig->alwaysEnabled || renderContext->jobConfig->isEnabled()) { jobRun(_data, sceneContext, renderContext, _input.get(), _output.edit()); } renderContext->jobConfig.reset(); diff --git a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml index 5ee62dfe49..02135056f8 100644 --- a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml +++ b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml @@ -39,8 +39,8 @@ Item { Label { text: sliderControl.value.toFixed(root.integral ? 0 : 2) - anchors.left: root.left - anchors.leftMargin: 140 + anchors.left: root.labelControl.right + anchors.leftMargin: 8 anchors.top: root.top anchors.topMargin: 7 } @@ -56,7 +56,7 @@ Item { Slider { id: sliderControl stepSize: root.integral ? 1.0 : 0.0 - width: 192 + width: 150 height: 20 anchors.right: root.right anchors.rightMargin: 8 diff --git a/scripts/developer/utilities/render/debugDeferredLighting.js b/scripts/developer/utilities/render/debugDeferredLighting.js new file mode 100644 index 0000000000..f2fab61717 --- /dev/null +++ b/scripts/developer/utilities/render/debugDeferredLighting.js @@ -0,0 +1,20 @@ +// +// debugSurfaceGeometryPass.js +// +// Created by Sam Gateau on 6/6/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +// Set up the qml ui +var qml = Script.resolvePath('deferredLighting.qml'); +var window = new OverlayWindow({ + title: 'Deferred Lighting Pass', + source: qml, + width: 400, height: 400, +}); +window.setPosition(250, 750); +window.closed.connect(function() { Script.stop(); }); + diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml new file mode 100644 index 0000000000..3cb91f54ce --- /dev/null +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -0,0 +1,62 @@ +// +// deferredLighting.qml +// +// Created by Sam Gateau on 6/6/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "configSlider" + +Column { + spacing: 8 + Column { + id: deferredLighting + spacing: 10 + + CheckBox { + text: "Point Lights" + checked: true + onCheckedChanged: { Render.getConfig("RenderDeferred").enablePointLights = checked } + } + CheckBox { + text: "Spot Lights" + checked: true + onCheckedChanged: { Render.getConfig("RenderDeferred").enableSpotLights = checked } + } + + Column{ + CheckBox { + text: "Scattering" + checked: true + onCheckedChanged: { Render.getConfig("RenderDeferred").enableScattering = checked } + } + + CheckBox { + text: "Show Scattering BRDF" + checked: Render.getConfig("RenderDeferred").showScatteringBRDF + onCheckedChanged: { Render.getConfig("RenderDeferred").showScatteringBRDF = checked } + } + Repeater { + model: [ "Scattering Bent Red:RenderDeferred:bentRed:2.0", + "Scattering Bent Green:RenderDeferred:bentGreen:2.0", + "Scattering Bent Blue:RenderDeferred:bentBlue:2.0", + "Scattering Bent Scale:RenderDeferred:bentScale:5.0", + "Scattering Curvature Offset:RenderDeferred:curvatureOffset:1.0", + "Scattering Curvature Scale:RenderDeferred:curvatureScale:2.0", + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: false + config: Render.getConfig(modelData.split(":")[1]) + property: modelData.split(":")[2] + max: modelData.split(":")[3] + min: 0.0 + } + } + } + } +} diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index 7a061da65b..b163333e60 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -19,7 +19,7 @@ Column { Column{ Repeater { - model: [ "Depth Threshold:depthThreshold:0.1", "Basis Scale:basisScale:2.0", "Curvature Scale:curvatureScale:10.0" ] + model: [ "Depth Threshold:depthThreshold:0.1", "Basis Scale:basisScale:2.0", "Curvature Scale:curvatureScale:100.0" ] ConfigSlider { label: qsTr(modelData.split(":")[0]) integral: false @@ -32,6 +32,11 @@ Column { } Column{ + CheckBox { + text: "Diffuse Curvature 1" + checked: true + onCheckedChanged: { Render.getConfig("DiffuseCurvature").enabled = checked } + } Repeater { model: [ "Blur Scale:DiffuseCurvature:filterScale:2.0", "Blur Depth Threshold:DiffuseCurvature:depthThreshold:10.0", "Blur Scale2:DiffuseCurvature2:filterScale:2.0", "Blur Depth Threshold 2:DiffuseCurvature2:depthThreshold:10.0"] ConfigSlider { @@ -43,6 +48,12 @@ Column { min: 0.0 } } + + CheckBox { + text: "Diffuse Curvature 2" + checked: true + onCheckedChanged: { Render.getConfig("DiffuseCurvature2").enabled = checked } + } } } } From d7bffc3eabec8bb0f5f26a3eadf44aa7dfdc3a31 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 23 Jun 2016 17:14:41 -0700 Subject: [PATCH 0736/1237] first cut at negotiating codecs --- assignment-client/src/audio/AudioMixer.cpp | 26 +++++++++++ assignment-client/src/audio/AudioMixer.h | 1 + interface/src/Application.cpp | 4 +- libraries/audio-client/CMakeLists.txt | 2 +- libraries/audio-client/src/AudioClient.cpp | 46 +++++++++++++++++++ libraries/audio-client/src/AudioClient.h | 5 ++ libraries/networking/src/udt/PacketHeaders.h | 4 +- libraries/plugins/src/plugins/CodecPlugin.h | 1 + .../plugins/src/plugins/PluginManager.cpp | 5 +- plugins/pcmCodec/src/PCMCodecManager.cpp | 4 ++ plugins/pcmCodec/src/PCMCodecManager.h | 1 + 11 files changed, 94 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 03bb32cd53..95578e3998 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -90,6 +90,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::AudioStreamStats }, this, "handleNodeAudioPacket"); packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); + packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -446,6 +447,31 @@ void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer mes } } +void AudioMixer::handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode) { + qDebug() << __FUNCTION__; + + // read the codecs requested by the client + quint8 numberOfCodecs = 0; + message->readPrimitive(&numberOfCodecs); + QStringList codecList; + for (quint16 i = 0; i < numberOfCodecs; i++) { + QString requestedCodec = message->readString(); + qDebug() << "requestedCodec:" << requestedCodec; + codecList.append(requestedCodec); + } + qDebug() << "all requested codecs:" << codecList; + + auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); + + // write them to our packet + QString selectedCodec = codecList.front(); + qDebug() << "selectedCodec:" << selectedCodec; + replyPacket->writeString(selectedCodec); + + auto nodeList = DependencyManager::get(); + nodeList->sendPacket(std::move(replyPacket), *sendingNode); +} + void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { // enumerate the connected listeners to remove HRTF objects for the disconnected node auto nodeList = DependencyManager::get(); diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 24b4b39704..c90a918a5b 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -45,6 +45,7 @@ private slots: void broadcastMixes(); void handleNodeAudioPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleMuteEnvironmentPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); void removeHRTFsForFinishedInjector(const QUuid& streamID); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 855cf2f313..3c69f716ca 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1243,7 +1243,6 @@ QString Application::getUserAgent() { userAgent += " " + formatPluginName(cp->getName()); } - qDebug() << __FUNCTION__ << ":" << userAgent; return userAgent; } @@ -4440,6 +4439,9 @@ void Application::nodeActivated(SharedNodePointer node) { } } + if (node->getType() == NodeType::AudioMixer) { + DependencyManager::get()->negotiateAudioFormat(); + } } void Application::nodeKilled(SharedNodePointer node) { diff --git a/libraries/audio-client/CMakeLists.txt b/libraries/audio-client/CMakeLists.txt index 2c0fc0a9cd..9c298ce664 100644 --- a/libraries/audio-client/CMakeLists.txt +++ b/libraries/audio-client/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME audio-client) setup_hifi_library(Network Multimedia) -link_hifi_libraries(audio) +link_hifi_libraries(audio plugins) # append audio includes to our list of includes to bubble target_include_directories(${TARGET_NAME} PUBLIC "${HIFI_LIBRARY_DIR}/audio/src") diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 2569f27aaa..18db23dbf7 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -34,6 +34,8 @@ #include #include +#include +#include #include #include #include @@ -134,6 +136,7 @@ AudioClient::AudioClient() : packetReceiver.registerListener(PacketType::MixedAudio, this, "handleAudioDataPacket"); packetReceiver.registerListener(PacketType::NoisyMute, this, "handleNoisyMutePacket"); packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); + packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); } AudioClient::~AudioClient() { @@ -474,6 +477,32 @@ void AudioClient::stop() { } } +void AudioClient::negotiateAudioFormat() { + qDebug() << __FUNCTION__; + + auto nodeList = DependencyManager::get(); + + auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat); + + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + + quint8 numberOfCodecs = (quint8)codecPlugins.size(); + negotiateFormatPacket->writePrimitive(numberOfCodecs); + for (auto& plugin : codecPlugins) { + qDebug() << "Codec available:" << plugin->getName(); + negotiateFormatPacket->writeString(plugin->getName()); + } + + // grab our audio mixer from the NodeList, if it exists + SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); + + if (audioMixer) { + // send off this mute packet + nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer); + } +} + + void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointer message) { char bitset; @@ -528,6 +557,16 @@ void AudioClient::handleMuteEnvironmentPacket(QSharedPointer me emit muteEnvironmentRequested(position, radius); } +void AudioClient::handleSelectedAudioFormat(QSharedPointer message) { + qDebug() << __FUNCTION__; + + // write them to our packet + QString selectedCodec = message->readString(); + + qDebug() << "selectedCodec:" << selectedCodec; +} + + QString AudioClient::getDefaultDeviceName(QAudio::Mode mode) { QAudioDeviceInfo deviceInfo = defaultAudioDeviceForMode(mode); return deviceInfo.deviceName(); @@ -1227,6 +1266,13 @@ void AudioClient::loadSettings() { windowSecondsForDesiredCalcOnTooManyStarves.get()); _receivedAudioStream.setWindowSecondsForDesiredReduction(windowSecondsForDesiredReduction.get()); _receivedAudioStream.setRepetitionWithFade(repetitionWithFade.get()); + + qDebug() << "---- Initializing Audio Client ----"; + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& plugin : codecPlugins) { + qDebug() << "Codec available:" << plugin->getName(); + } + } void AudioClient::saveSettings() { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3a14c878f6..e05612f859 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -94,6 +94,8 @@ public: int _unfulfilledReads; }; + void negotiateAudioFormat(); + const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } @@ -139,6 +141,7 @@ public slots: void handleAudioDataPacket(QSharedPointer message); void handleNoisyMutePacket(QSharedPointer message); void handleMuteEnvironmentPacket(QSharedPointer message); + void handleSelectedAudioFormat(QSharedPointer message); void sendDownstreamAudioStatsPacket() { _stats.sendDownstreamAudioStatsPacket(); } void handleAudioInput(); @@ -292,6 +295,8 @@ private: void checkDevices(); bool _hasReceivedFirstPacket = false; + + //CodecPluginPointer _codec { nullptr }; }; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ae54450fee..b5d1be077f 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -95,7 +95,9 @@ public: AssetMappingOperation, AssetMappingOperationReply, ICEServerHeartbeatACK, - LAST_PACKET_TYPE = ICEServerHeartbeatACK + NegotiateAudioFormat, + SelectedAudioFormat, + LAST_PACKET_TYPE = SelectedAudioFormat }; }; diff --git a/libraries/plugins/src/plugins/CodecPlugin.h b/libraries/plugins/src/plugins/CodecPlugin.h index d9e1b947fe..6944d91bed 100644 --- a/libraries/plugins/src/plugins/CodecPlugin.h +++ b/libraries/plugins/src/plugins/CodecPlugin.h @@ -15,5 +15,6 @@ class CodecPlugin : public Plugin { public: virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) = 0; }; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index fe34d2dbee..0b4afe1be0 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -222,10 +222,11 @@ const CodecPluginList& PluginManager::getCodecPlugins() { } } - auto& container = PluginContainer::getInstance(); for (auto plugin : codecPlugins) { - plugin->setContainer(&container); + plugin->setContainer(_container); plugin->init(); + + qDebug() << "init codec:" << plugin->getName(); } }); return codecPlugins; diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index 264c139ac6..2401e99576 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -43,3 +43,7 @@ void PCMCodecManager::decode(const QByteArray& encodedBuffer, QByteArray& decode decodedBuffer = encodedBuffer; } +void PCMCodecManager::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { + // this codec doesn't actually do anything.... + encodedBuffer = decodedBuffer; +} diff --git a/plugins/pcmCodec/src/PCMCodecManager.h b/plugins/pcmCodec/src/PCMCodecManager.h index a8b3e49a2c..3b7ca36c02 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.h +++ b/plugins/pcmCodec/src/PCMCodecManager.h @@ -32,6 +32,7 @@ public: void deactivate() override; virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override; + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override; private: static const QString NAME; From 1de1c632afbb4f6f124108d32265e2aa3ec25c53 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 23 Jun 2016 17:16:42 -0700 Subject: [PATCH 0737/1237] use private_description instead of description for domain settings --- domain-server/resources/web/settings/js/settings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index c2cb2ecb80..4188e86548 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -555,7 +555,7 @@ function createNewDomainID(description, justConnected) { // get the JSON object ready that we'll use to create a new domain var domainJSON = { "domain": { - "description": description + "private_description": description }, "access_token": $(Settings.ACCESS_TOKEN_SELECTOR).val() } @@ -748,8 +748,8 @@ function chooseFromHighFidelityDomains(clickedButton) { _.each(data.data.domains, function(domain){ var domainString = ""; - if (domain.description) { - domainString += '"' + domain.description + '" - '; + if (domain.private_description) { + domainString += '"' + domain.private_description + '" - '; } domainString += domain.id; From 13310923c4440ec6b0a97dbf89627dc63fe832ad Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 23 Jun 2016 19:38:23 -0700 Subject: [PATCH 0738/1237] reset to temp domain after logout --- .../resources/web/settings/js/settings.js | 2 ++ domain-server/src/DomainServer.cpp | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 4188e86548..4f153d6190 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -457,6 +457,8 @@ function disonnectHighFidelityAccount() { }, function(){ // we need to post to settings to clear the access-token $(Settings.ACCESS_TOKEN_SELECTOR).val('').change(); + // reset the domain id to get a new temporary name + $(Settings.DOMAIN_ID_SELECTOR).val('').change(); saveSettings(); }); } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 192c0d26e6..ca0d7728dd 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -111,12 +111,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : return; } - // check for the temporary name parameter - const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; - if (args.contains(GET_TEMPORARY_NAME_SWITCH)) { - getTemporaryName(); - } - setupNodeListAndAssignments(); setupAutomaticNetworking(); if (!getID().isNull()) { @@ -125,6 +119,12 @@ DomainServer::DomainServer(int argc, char* argv[]) : sendHeartbeatToMetaverse(); } + // check for the temporary name parameter + const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; + if (args.contains(GET_TEMPORARY_NAME_SWITCH)) { + getTemporaryName(); + } + _gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request _metadata = new DomainMetadata(this); @@ -251,12 +251,13 @@ void DomainServer::getTemporaryName(bool force) { // check if we already have a domain ID const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); + qInfo() << "Requesting temporary domain name"; if (idValueVariant) { - qWarning() << "Temporary domain name requested but a domain ID is already present in domain-server settings."; + qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant->toString(); if (force) { - qWarning() << "Temporary domain name will be requested to replace it."; + qDebug() << "Requesting temporary domain name to replace current ID:" << getID(); } else { - qWarning() << "Temporary domain name will not be requested."; + qInfo() << "Abandoning request of temporary domain name."; return; } } From b1b378a91feb062e160cf794002e59c13e5ed11b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 23 Jun 2016 19:38:41 -0700 Subject: [PATCH 0739/1237] add back access token to domain-server --- domain-server/src/DomainServer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index ca0d7728dd..6e5c1fd361 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -470,6 +470,9 @@ bool DomainServer::resetAccountManagerAccessToken() { void DomainServer::setupAutomaticNetworking() { qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; + + resetAccountManagerAccessToken(); + _automaticNetworkingSetting = _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString(); From 1fcd7aa0c44d714fa4505b9b97fdad698ed9a674 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 23 Jun 2016 19:54:34 -0700 Subject: [PATCH 0740/1237] add build version to heartbeat --- domain-server/src/DomainServer.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 223cab61da..d421c6554b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1081,6 +1081,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // Setup the domain object to send to the data server QJsonObject domainObject; + // add the version + static const QString VERSION_KEY = "version"; + domainObject[VERSION_KEY] = BuildInfo::VERSION; + + // add networking if (!networkAddress.isEmpty()) { static const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; domainObject[PUBLIC_NETWORK_ADDRESS_KEY] = networkAddress; @@ -1089,10 +1094,10 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; - // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings - const QString RESTRICTED_ACCESS_FLAG = "restricted"; - // consider the domain to have restricted access if "anonymous" connections can't connect to the domain. + // add access level for anonymous connections + // consider the domain to be "restricted" if anonymous connections are disallowed + static const QString RESTRICTED_ACCESS_FLAG = "restricted"; NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain; From 6542604d1307234167e5a5e0ab67aadd37475773 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Fri, 24 Jun 2016 05:19:28 +0100 Subject: [PATCH 0741/1237] resolve conflicts with TouchscreenDevice updated to master --- interface/src/Application.cpp | 2 +- .../input-plugins/src/input-plugins/TouchscreenDevice.cpp | 8 +++++--- .../input-plugins/src/input-plugins/TouchscreenDevice.h | 5 ++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2d1dc64bcb..7858679583 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1593,7 +1593,7 @@ void Application::initializeUi() { if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); } - if (name == TouchscreenDevice::NAME) { + if (TouchscreenDevice::NAME == inputPlugin->getName()) { _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); } } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index f8da94e5e6..16b5947426 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -22,8 +22,10 @@ const QString TouchscreenDevice::NAME = "Touchscreen"; -void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { - _inputDevice->update(deltaTime, inputCalibrationData, jointsCaptured); +void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + auto userInputMapper = DependencyManager::get(); + userInputMapper->withLock([&, this]() { + _inputDevice->update(deltaTime, inputCalibrationData); // at DPI 100 use these arbitrary values to divide dragging distance static const float DPI_SCALE_X = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0), 1.0f, 10.0f) @@ -51,7 +53,7 @@ void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCal } } -void TouchscreenDevice::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) { +void TouchscreenDevice::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { _axisStateMap.clear(); } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index acd7d89333..772776348f 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -36,11 +36,10 @@ public: // Plugin functions virtual bool isSupported() const override { return true; } - virtual bool isJointController() const override { return false; } virtual const QString& getName() const override { return NAME; } virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } - virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void touchBeginEvent(const QTouchEvent* event); void touchEndEvent(const QTouchEvent* event); @@ -58,7 +57,7 @@ protected: // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, bool jointsCaptured) override; + virtual void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void focusOutEvent() override; controller::Input makeInput(TouchAxisChannel axis) const; From 8928854820c6f13302e094ac305a43b55dd066af Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Fri, 24 Jun 2016 05:20:47 +0100 Subject: [PATCH 0742/1237] lost change --- libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 16b5947426..9430f1d23a 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -26,6 +26,7 @@ void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCal auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); + }); // at DPI 100 use these arbitrary values to divide dragging distance static const float DPI_SCALE_X = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0), 1.0f, 10.0f) From 90cd335bdabcc1d3e7679da0b3d7a0ef0fc5868b Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Fri, 24 Jun 2016 10:48:25 +0100 Subject: [PATCH 0743/1237] missed a brace --- tests/controllers/src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index dbb17fd63f..1a4f8742e9 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -92,7 +92,7 @@ public: virtual QOpenGLContext* getPrimaryContext() override { return nullptr; } virtual ui::Menu* getPrimaryMenu() override { return nullptr; } virtual bool isForeground() const override { return true; } - virtual DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } + virtual DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } }; class MyControllerScriptingInterface : public controller::ScriptingInterface { @@ -144,6 +144,7 @@ int main(int argc, char** argv) { auto userInputMapper = DependencyManager::get(); if (name == KeyboardMouseDevice::NAME) { userInputMapper->registerDevice(std::dynamic_pointer_cast(inputPlugin)->getInputDevice()); + } if (name == TouchscreenDevice::NAME) { userInputMapper->registerDevice(std::dynamic_pointer_cast(inputPlugin)->getInputDevice()); } From e95a31f7601207f795a2403a483cabf29383de55 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 24 Jun 2016 09:18:04 -0700 Subject: [PATCH 0744/1237] CLeaning up and debugging the splotch --- .../render-utils/src/DeferredGlobalLight.slh | 19 ++++++++++--------- .../src/DeferredLightingEffect.cpp | 4 ++-- .../render-utils/src/RenderDeferredTask.cpp | 5 +++-- .../render-utils/src/SubsurfaceScattering.cpp | 16 ++++++++-------- .../render-utils/src/SubsurfaceScattering.h | 2 +- .../render-utils/src/SubsurfaceScattering.slh | 3 ++- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 55d21dd47f..7bb3c9f35d 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -178,21 +178,17 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f vec3 bentNormalLow = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); float curvature = unpackCurvature(diffusedCurvature.w); - vec3 rS = bentNormalHigh; vec3 bendFactorSpectrum = getBendFactor(); - vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); + // vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); + vec3 rN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.x)); vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); - - /* vec3 rN = normalize(mix(normal, bentNormalHigh, bendFactorSpectrum.x)); - vec3 gN = normalize(mix(normal, bentNormalHigh, bendFactorSpectrum.y)); - vec3 bN = normalize(mix(normal, bentNormalHigh, bendFactorSpectrum.z)); - */ vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); + //return 0.5 * (NdotLSpectrum + vec3(1.0)); // --> Look up the pre-integrated curvature-dependent BDRF textures vec3 brdf = fetchBRDFSpectrum(NdotLSpectrum, curvature); @@ -218,7 +214,8 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f if (scatteringLevel < 0.1) { brdf = vec3(standardDiffuse); } - vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); + + vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); // Diffuse from ambient @@ -230,7 +227,11 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f if ( showBRDF()) return brdf; - return vec3(color); + + vec3 debugNdotL = 0.5 * (NdotLSpectrum + vec3(1.0)); + return vec3(debugNdotL.z, curvature, 0.0 ); + + return vec3(color); } <@endfunc@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index e9715e6dfe..ff89a3fc46 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -636,8 +636,8 @@ void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderCo _subsurfaceScatteringResource->generateScatteringTable(renderContext->args); } - auto& deferredTransform = inputs.get0(); - auto& diffusedCurvature2 = inputs.get2()->getRenderBuffer(0); + auto deferredTransform = inputs.get0(); + auto diffusedCurvature2 = inputs.get2()->getRenderBuffer(0); setupJob.run(sceneContext, renderContext, deferredTransform, diffusedCurvature2, _subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform, _enablePointLights, _enableSpotLights); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 3e74701246..59a6ab6816 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -124,8 +124,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DrawLight", lights); const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer)); - // const auto scatteringFramebuffer = addJob("Scattering", scatteringInputs); - + // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredLightingInputs); @@ -136,6 +135,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Render transparent objects forward in LightingBuffer addJob("DrawTransparentDeferred", transparents, shapePlumber); + const auto scatteringFramebuffer = addJob("Scattering", deferredLightingInputs); + // Lighting Buffer ready for tone mapping addJob("ToneMapping"); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index a164484eb4..968cc1b42f 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -220,10 +220,10 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, auto framebufferCache = DependencyManager::get(); - if (!updateScatteringFramebuffer(curvatureFramebuffer, scatteringFramebuffer)) { + /* if (!updateScatteringFramebuffer(curvatureFramebuffer, scatteringFramebuffer)) { return; } - +*/ const auto theLight = DependencyManager::get()->getLightStage().lights[0]; gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { @@ -231,7 +231,7 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setViewportTransform(args->_viewport); - batch.setFramebuffer(_scatteringFramebuffer); + /* batch.setFramebuffer(_scatteringFramebuffer); batch.setPipeline(pipeline); @@ -251,7 +251,7 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.setResourceTexture(ScatteringTask_LinearMapSlot, framebufferCache->getDepthPyramidTexture()); batch.draw(gpu::TRIANGLE_STRIP, 4); - +*/ if (_showLUT) { auto viewportSize = std::min(args->_viewport.z, args->_viewport.w) >> 1; batch.setViewportTransform(glm::ivec4(0, 0, viewportSize, viewportSize)); @@ -359,9 +359,9 @@ void diffuseScatter(gpu::TexturePointer& lut) { vec3 val = integrate(x, y); // Convert to linear - val.x = sqrt(val.x); - val.y = sqrt(val.y); - val.z = sqrt(val.z); + // val.x = sqrt(val.x); + // val.y = sqrt(val.y); + // val.z = sqrt(val.z); // Convert to 24-bit image. unsigned char valI[3]; @@ -464,7 +464,7 @@ void diffuseProfile(gpu::TexturePointer& profile) { gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScattering(RenderArgs* args) { - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 128, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 256, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); diffuseProfile(profileMap); const int WIDTH = 128; diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 881be391b8..c1d9a609b9 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -92,7 +92,7 @@ public: float curvatureOffset{ 0.08f }; float curvatureScale{ 0.8f }; - bool showLUT{ false }; + bool showLUT{ true }; signals: void dirty(); diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index bcb16a583c..b002f2dcbd 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -13,7 +13,7 @@ uniform sampler2D scatteringLUT; vec3 fetchBRDF(float LdotN, float curvature) { - return texture(scatteringLUT, vec2( clamp(LdotN * 0.5 + 0.5, 0.0, 1.0), curvature)).xyz; + return texture(scatteringLUT, vec2( clamp(LdotN * 0.5 + 0.5, 0.0, 1.0), clamp(2 * curvature, 0.0, 1.0))).xyz; } vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { @@ -50,4 +50,5 @@ float unpackCurvature(float packedCurvature) { return abs(packedCurvature * 2 - 1) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; } + <@endfunc@> From 15fbff4ffb1ebc2225d7234ee820c2334978a755 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 10:09:52 -0700 Subject: [PATCH 0745/1237] Add eslintrc file --- scripts/.eslintrc.js | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 scripts/.eslintrc.js diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 0000000000..e866713b11 --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,72 @@ +module.exports = { + "root": true, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 5 + }, + "globals": { + "Account": false, + "AnimationCache": false, + "Assets": false, + "Audio": false, + "AudioDevice": false, + "AudioEffectOptions": false, + "AvatarList": false, + "AvatarManager": false, + "Camera": false, + "Clipboard": false, + "Controller": false, + "DialogsManager": false, + "Entities": false, + "FaceTracker": false, + "GlobalServices": false, + "HMD": false, + "LODManager": false, + "Mat4": false, + "Menu": false, + "Messages": false, + "ModelCache": false, + "MyAvatar": false, + "Overlays": false, + "Paths": false, + "Quat": false, + "Rates": false, + "Recording": false, + "Reticle": false, + "Scene": false, + "Script": false, + "ScriptDiscoveryService": false, + "Settings": false, + "SoundCache": false, + "Stats": false, + "TextureCache": false, + "Uuid": false, + "UndoStack": false, + "Vec3": false, + "WebSocket": false, + "WebWindow": false, + "Window": false, + "XMLHttpRequest": false, + "location": false, + "print": false + }, + "rules": { + "brace-style": ["error", "1tbs", { "allowSingleLine": false }], + "comma-dangle": ["error", "only-multiline"], + "camelcase": ["error"], + "curly": ["error", "all"], + "indent": ["error", 4, { "SwitchCase": 1 }], + "keyword-spacing": ["error", { "before": true, "after": true }], + "max-len": ["error", 128, 4], + "new-cap": ["error"], + //"no-magic-numbers": ["error", { "ignore": [0, 1], "ignoreArrayIndexes": true }], + "no-multiple-empty-lines": ["error"], + "no-multi-spaces": ["error"], + "no-unused-vars": ["error", { "args": "none", "vars": "local" }], + "semi": ["error", "always"], + "spaced-comment": ["error", "always", { + "line": { "markers": ["/"] } + }], + "space-before-function-paren": ["error", "never"] + } +}; From 88ad570024a927d80b24fbc036897fa7bb0b971f Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 10:16:26 -0700 Subject: [PATCH 0746/1237] Revert "Uses spaces, not tabs" This reverts commit 7e5b9db13a688100d9d587c1a4147e6a65f15e78. --- interface/src/Application.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 53b9c18360..ddfa5dae8b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5349,14 +5349,14 @@ void Application::updateDisplayMode() { // reset the avatar, to set head and hand palms back to a reasonable default pose. getMyAvatar()->reset(false); - - // go into first person when they are in HMD mode, since 3rd person HMD is dumb - if (isHMDMode() && !menu->isOptionChecked(MenuOption::FirstPerson)) { - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - cameraMenuChanged(); - } - - Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); + + // go into first person when they are in HMD mode, since 3rd person HMD is dumb + if (isHMDMode() && !menu->isOptionChecked(MenuOption::FirstPerson)) { + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + cameraMenuChanged(); + } + + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } mat4 Application::getEyeProjection(int eye) const { From 41d38ab63045405b7379a1516ebd7056460e97bf Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 10:16:40 -0700 Subject: [PATCH 0747/1237] Revert "Automatically enter first person when in HMD mode" This reverts commit 3c9f3f392736b56a0641a6f6707c00077b2871bf. --- interface/src/Application.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ddfa5dae8b..d20a62fc7b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5350,13 +5350,7 @@ void Application::updateDisplayMode() { // reset the avatar, to set head and hand palms back to a reasonable default pose. getMyAvatar()->reset(false); - // go into first person when they are in HMD mode, since 3rd person HMD is dumb - if (isHMDMode() && !menu->isOptionChecked(MenuOption::FirstPerson)) { - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - cameraMenuChanged(); - } - - Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } mat4 Application::getEyeProjection(int eye) const { From 39ff03cfb51acf0bd719fd5c39d1ff8caaf2a357 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 24 Jun 2016 10:53:46 -0700 Subject: [PATCH 0748/1237] reduce log spam in Vive plugins --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 4 +++- plugins/openvr/src/OpenVrHelpers.cpp | 28 ++++++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 5233ad644a..cfb374e3bd 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -96,7 +96,9 @@ bool OpenVrDisplayPlugin::internalActivate() { glm::vec3 uiPos(0.0f, UI_HEIGHT, UI_RADIUS - (0.5f * zSize) - UI_Z_OFFSET); _sensorResetMat = glm::inverse(createMatFromQuatAndPos(glm::quat(), uiPos)); } else { - qDebug() << "OpenVR: error could not get chaperone pointer"; + #if DEV_BUILD + qDebug() << "OpenVR: error could not get chaperone pointer"; + #endif } return Parent::internalActivate(); diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index e71c8942d6..399712d920 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -64,17 +64,25 @@ vr::IVRSystem* acquireOpenVrSystem() { if (hmdPresent) { Lock lock(mutex); if (!activeHmd) { - qCDebug(displayplugins) << "OpenVR: No vr::IVRSystem instance active, building"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: No vr::IVRSystem instance active, building"; + #endif vr::EVRInitError eError = vr::VRInitError_None; activeHmd = vr::VR_Init(&eError, vr::VRApplication_Scene); - qCDebug(displayplugins) << "OpenVR display: HMD is " << activeHmd << " error is " << eError; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR display: HMD is " << activeHmd << " error is " << eError; + #endif } if (activeHmd) { - qCDebug(displayplugins) << "OpenVR: incrementing refcount"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: incrementing refcount"; + #endif ++refCount; } } else { - qCDebug(displayplugins) << "OpenVR: no hmd present"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: no hmd present"; + #endif } return activeHmd; } @@ -82,10 +90,14 @@ vr::IVRSystem* acquireOpenVrSystem() { void releaseOpenVrSystem() { if (activeHmd) { Lock lock(mutex); - qCDebug(displayplugins) << "OpenVR: decrementing refcount"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: decrementing refcount"; + #endif --refCount; if (0 == refCount) { - qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; + #endif vr::VR_Shutdown(); activeHmd = nullptr; } @@ -261,7 +273,9 @@ void handleOpenVrEvents() { default: break; } - qDebug() << "OpenVR: Event " << activeHmd->GetEventTypeNameFromEnum((vr::EVREventType)event.eventType) << "(" << event.eventType << ")"; + #if DEV_BUILD + qDebug() << "OpenVR: Event " << activeHmd->GetEventTypeNameFromEnum((vr::EVREventType)event.eventType) << "(" << event.eventType << ")"; + #endif } } From a095da31fd8c1422058da1de0622554124f4f56c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 24 Jun 2016 11:48:55 -0700 Subject: [PATCH 0749/1237] Fix initial visibility of QML windows from scripts --- libraries/ui/src/QmlWindowClass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index b8834f0549..c0eba4abf3 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -118,7 +118,7 @@ void QmlWindowClass::initQml(QVariantMap properties) { } bool visible = !properties.contains(VISIBILE_PROPERTY) || properties[VISIBILE_PROPERTY].toBool(); - object->setProperty(VISIBILE_PROPERTY, visible); + object->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); object->setProperty(SOURCE_PROPERTY, _source); // Forward messages received from QML on to the script From d45694836fe3ffbfe21b8cc0b2642bbadd05e72c Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 11:57:12 -0700 Subject: [PATCH 0750/1237] Add signal for display mode change in HMD --- interface/src/scripting/HMDScriptingInterface.cpp | 3 +++ .../src/display-plugins/AbstractHMDScriptingInterface.h | 1 + 2 files changed, 4 insertions(+) diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 7bf1547a3c..9b633ef96d 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -19,6 +19,9 @@ #include "Application.h" HMDScriptingInterface::HMDScriptingInterface() { + connect(qApp, &Application::activeDisplayPluginChanged, [this]{ + emit displayModeChanged(isHMDMode()); + }); } glm::vec3 HMDScriptingInterface::calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const { diff --git a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h index 5df58ce677..f260fa959f 100644 --- a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h +++ b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h @@ -31,6 +31,7 @@ public: signals: void IPDScaleChanged(); + void displayModeChanged(bool isHMDMode); private: float _IPDScale{ 1.0 }; From 0c56af7ae8042982eb1f0969261c7908e513d5f7 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 11:57:50 -0700 Subject: [PATCH 0751/1237] Add default script to enter first person for HMD --- scripts/defaultScripts.js | 1 + scripts/system/firstPersonHMD.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 scripts/system/firstPersonHMD.js diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2a050d183e..f6d9d95ada 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,3 +21,4 @@ Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); Script.load("system/dialTone.js"); +Script.load("system/firstPersonHMD.js"); diff --git a/scripts/system/firstPersonHMD.js b/scripts/system/firstPersonHMD.js new file mode 100644 index 0000000000..082c6304be --- /dev/null +++ b/scripts/system/firstPersonHMD.js @@ -0,0 +1,17 @@ +// +// firstPersonHMD.js +// system +// +// Created by Zander Otavka on 6/24/16 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Automatically enter first person mode when entering HMD mode +HMD.displayModeChanged.connect(function(isHMDMode) { + if (isHMDMode) { + Camera.setModeString("first person"); + } +}); From 8bf72f28da84d177e67e920fa1e3f1b243a587b6 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 24 Jun 2016 12:06:02 -0700 Subject: [PATCH 0752/1237] Fix for grab script search ray length --- scripts/system/controllers/handControllerGrab.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 7706132c58..93d2269584 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1173,10 +1173,12 @@ function MyController(hand) { } var rayPickInfo = this.calcRayPickInfo(this.hand); - this.intersectionDistance = rayPickInfo.distance; if (rayPickInfo.entityID) { candidateEntities.push(rayPickInfo.entityID); this.entityPropertyCache.addEntity(rayPickInfo.entityID); + this.intersectionDistance = rayPickInfo.distance; + } else { + this.intersectionDistance = 0; } var grabbableEntities = candidateEntities.filter(function (entity) { From dd023e16fd53c500b6741fd6b7ff60ff05a61501 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 13:16:24 -0700 Subject: [PATCH 0753/1237] Tabs -> spaces --- interface/src/scripting/HMDScriptingInterface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 9b633ef96d..49c6635fb3 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -19,9 +19,9 @@ #include "Application.h" HMDScriptingInterface::HMDScriptingInterface() { - connect(qApp, &Application::activeDisplayPluginChanged, [this]{ - emit displayModeChanged(isHMDMode()); - }); + connect(qApp, &Application::activeDisplayPluginChanged, [this]{ + emit displayModeChanged(isHMDMode()); + }); } glm::vec3 HMDScriptingInterface::calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const { From 7da854d98c70cf4272c4d68c84f898789e6f28e3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 24 Jun 2016 13:35:47 -0700 Subject: [PATCH 0754/1237] do not reset hud from button --- interface/src/ui/OverlayConductor.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 6a99641ce4..7d0dc6c650 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -118,10 +118,12 @@ void OverlayConductor::update(float dt) { bool isDriving = updateAvatarHasDriveInput(); bool drivingChanged = prevDriving != isDriving; bool isAtRest = updateAvatarIsAtRest(); + bool shouldRecenter = false; if (_flags & SuppressedByDrive) { if (!isDriving) { _flags &= ~SuppressedByDrive; + shouldRecenter = true; } } else { if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { @@ -132,6 +134,7 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByHead) { if (isAtRest) { _flags &= ~SuppressedByHead; + shouldRecenter = true; } } else { if (_hmdMode && headOutsideOverlay()) { @@ -143,8 +146,8 @@ void OverlayConductor::update(float dt) { bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); if (targetVisible != currentVisible) { offscreenUi->setPinned(!targetVisible); - if (targetVisible && _hmdMode) { - centerUI(); - } + } + if (shouldRecenter) { + centerUI(); } } From 2ae0a7defc2bfbcdb4fc030d56153e1736363d37 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 24 Jun 2016 14:46:46 -0700 Subject: [PATCH 0755/1237] If there are TWO conditions holding things back, weight for them both to clear. --- interface/src/ui/OverlayConductor.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 7d0dc6c650..e18522cb2f 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -123,7 +123,9 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByDrive) { if (!isDriving) { _flags &= ~SuppressedByDrive; - shouldRecenter = true; + if (_flags & SuppressMask) { + shouldRecenter = true; + } } } else { if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { @@ -134,7 +136,9 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByHead) { if (isAtRest) { _flags &= ~SuppressedByHead; - shouldRecenter = true; + if (_flags & SuppressMask) { + shouldRecenter = true; + } } } else { if (_hmdMode && headOutsideOverlay()) { From 9ae3c386166a6834c8c571324189c65d1fe51b69 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 24 Jun 2016 15:26:24 -0700 Subject: [PATCH 0756/1237] doh! --- interface/src/ui/OverlayConductor.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index e18522cb2f..2ee106b6b3 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -123,9 +123,7 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByDrive) { if (!isDriving) { _flags &= ~SuppressedByDrive; - if (_flags & SuppressMask) { - shouldRecenter = true; - } + shouldRecenter = true; } } else { if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { @@ -136,9 +134,7 @@ void OverlayConductor::update(float dt) { if (_flags & SuppressedByHead) { if (isAtRest) { _flags &= ~SuppressedByHead; - if (_flags & SuppressMask) { - shouldRecenter = true; - } + shouldRecenter = true; } } else { if (_hmdMode && headOutsideOverlay()) { @@ -151,7 +147,7 @@ void OverlayConductor::update(float dt) { if (targetVisible != currentVisible) { offscreenUi->setPinned(!targetVisible); } - if (shouldRecenter) { + if (shouldRecenter && !_flags) { centerUI(); } } From 5e69af83c69c94e78ec813cd7aea4bfbe270e031 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 22 Jun 2016 17:53:38 -0700 Subject: [PATCH 0757/1237] can't enter independent mode through edit.js in HMD mode --- scripts/system/libraries/entityCameraTool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index a8e9335956..63f7c43fdc 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -141,7 +141,7 @@ CameraManager = function() { }; that.enable = function() { - if (Camera.mode == "independent" || that.enabled) return; + if (Camera.mode == "independent" || that.enabled || HMD.active) return; for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.captureKeyEvents({ From d5feac94f4cb3581f755cda07fbda4261f0a89bf Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 23 Jun 2016 10:48:29 -0700 Subject: [PATCH 0758/1237] coding standard fun --- scripts/system/libraries/entityCameraTool.js | 37 +++++++++++--------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index 63f7c43fdc..301b60f550 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -141,7 +141,9 @@ CameraManager = function() { }; that.enable = function() { - if (Camera.mode == "independent" || that.enabled || HMD.active) return; + if (Camera.mode == "independent" || that.enabled || HMD.active) { + return; + } for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.captureKeyEvents({ @@ -179,7 +181,9 @@ CameraManager = function() { } that.disable = function(ignoreCamera) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.releaseKeyEvents({ @@ -352,27 +356,21 @@ CameraManager = function() { that.mousePressEvent = function(event) { if (cameraTool.mousePressEvent(event)) { - return true; } - - if (!that.enabled) return; + if (!that.enabled) { + return; + } if (event.isRightButton || (event.isLeftButton && event.isControl && !event.isShifted)) { - that.mode = MODE_ORBIT; } else if (event.isMiddleButton || (event.isLeftButton && event.isControl && event.isShifted)) { - - that.mode = MODE_PAN; } if (that.mode !== MODE_INACTIVE) { - - hasDragged = false; - return true; } @@ -381,7 +379,9 @@ CameraManager = function() { that.mouseReleaseEvent = function(event) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } that.mode = MODE_INACTIVE; Reticle.setVisible(true); @@ -403,7 +403,9 @@ CameraManager = function() { }; that.wheelEvent = function(event) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } var dZoom = -event.delta * SCROLL_SENSITIVITY; @@ -459,8 +461,12 @@ CameraManager = function() { } function normalizeDegrees(degrees) { - while (degrees > 180) degrees -= 360; - while (degrees < -180) degrees += 360; + while (degrees > 180) { + degrees -= 360; + } + while (degrees < -180) { + degrees += 360; + } return degrees; } @@ -483,7 +489,6 @@ CameraManager = function() { that.targetZoomDistance = clamp(that.targetZoomDistance, MIN_ZOOM_DISTANCE, MAX_ZOOM_DISTANCE); } - if (easing) { easingTime = Math.min(EASE_TIME, easingTime + dt); } From f90a351400c5de2a89a30c25dc72a00fe25ca8ab Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 24 Jun 2016 16:31:24 -0700 Subject: [PATCH 0759/1237] Don't reposition windows prior to selecting the initial display plugin --- interface/resources/qml/desktop/Desktop.qml | 16 +++++++++++++++- interface/src/Application.cpp | 8 +++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 9f10cfc64a..27fa9692b9 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -24,6 +24,13 @@ FocusScope { readonly property int invalid_position: -9999; property rect recommendedRect: Qt.rect(0,0,0,0); property var expectedChildren; + property bool repositionLocked: true + + onRepositionLockedChanged: { + if (!repositionLocked) { + d.handleSizeChanged(); + } + } onHeightChanged: d.handleSizeChanged(); @@ -52,11 +59,14 @@ FocusScope { readonly property real menu: 8000 } - QtObject { id: d function handleSizeChanged() { + if (desktop.repositionLocked) { + return; + } + var oldRecommendedRect = recommendedRect; var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedOverlayRect(); var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, @@ -235,6 +245,10 @@ FocusScope { } function repositionAll() { + if (desktop.repositionLocked) { + return; + } + var oldRecommendedRect = recommendedRect; var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; var newRecommendedRect = Controller.getRecommendedOverlayRect(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 26da43fb58..858b86025f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -963,6 +963,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : updateHeartbeat(); loadSettings(); + + // Now that we've loaded the menu and thus switched to the previous display plugin + // we can unlock the desktop repositioning code, since all the positions will be + // relative to the desktop size for this plugin + auto offscreenUi = DependencyManager::get(); + offscreenUi->getDesktop()->setProperty("repositionLocked", false); + // Make sure we don't time out during slow operations at startup updateHeartbeat(); @@ -5348,7 +5355,6 @@ void Application::updateDisplayMode() { _displayPlugin = newDisplayPlugin; } - emit activeDisplayPluginChanged(); // reset the avatar, to set head and hand palms back to a reasonable default pose. From 2786061d71d96ce6157c2b98b59a59f292347736 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 16:34:54 -0700 Subject: [PATCH 0760/1237] Fix bug with entity adding w/o permission Entities could be added to the local tree, but be rejected by the server because of a lack of permissions. --- libraries/entities/src/EntityTree.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 7ebfecbe8e..21e5865c09 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -320,6 +320,11 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti return nullptr; } + if (!properties.getClientOnly() && getIsClient() && + !nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp()) { + return nullptr; + } + bool recordCreationTime = false; if (props.getCreated() == UNKNOWN_CREATED_TIME) { // the entity's creation time was not specified in properties, which means this is a NEW entity From a4da63c1ff33ad58f04ca35ed7d31647400b71bd Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jun 2016 16:39:57 -0700 Subject: [PATCH 0761/1237] Fix bug with Entities.addEntity returning bad id If addEntity failed for whatever reason (eg. lack of permission), it would return a non-null UUID. Fixes [FogBugz case #365](https://highfidelity.fogbugz.com/f/cases/356/Entities-addEntity-can-return-a-UUID-even-when-unsuccessful). --- libraries/entities/src/EntityScriptingInterface.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 55d93d5b5b..856e526b4c 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -191,9 +191,11 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties if (success) { emit debitEnergySource(cost); queueEntityMessage(PacketType::EntityAdd, id, propertiesWithSimID); - } - return id; + return id; + } else { + return QUuid(); + } } QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position) { From 8a5369fcb15e98cdf2143d690834d29fa58f1ec7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 12:03:36 +1200 Subject: [PATCH 0762/1237] Undo hide users.js when logged out --- scripts/system/users.js | 109 +++++++++++++++------------------------- 1 file changed, 41 insertions(+), 68 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 5b0ba42a45..54fa5951ac 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var PopUpMenu = function(properties) { +var PopUpMenu = function (properties) { var value = properties.value, promptOverlay, valueOverlay, @@ -217,7 +217,7 @@ var PopUpMenu = function(properties) { }; }; -var usersWindow = (function() { +var usersWindow = (function () { var baseURL = Script.resolvePath("assets/images/tools/"), WINDOW_WIDTH = 260, @@ -253,11 +253,7 @@ var usersWindow = (function() { WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_LEFT_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_RADIUS = 4, - WINDOW_BORDER_COLOR = { - red: 255, - green: 255, - blue: 255 - }, + WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, WINDOW_BORDER_ALPHA = 0.5, windowBorder, @@ -381,12 +377,9 @@ var usersWindow = (function() { isMirrorDisplay = false, isFullscreenMirror = false, - windowPosition = {}, // Bottom left corner of window pane. + windowPosition = {}, // Bottom left corner of window pane. isMovingWindow = false, - movingClickOffset = { - x: 0, - y: 0 - }, + movingClickOffset = { x: 0, y: 0 }, isUsingScrollbars = false, isMovingScrollbar = false, @@ -408,7 +401,9 @@ var usersWindow = (function() { } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + windowLineHeight + VISIBILITY_SPACER + windowLineHeight + WINDOW_BASE_MARGIN; + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + + windowLineHeight + VISIBILITY_SPACER + + windowLineHeight + WINDOW_BASE_MARGIN; // Limit window to height of viewport above window position minus VU meter and mirror if displayed windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; @@ -461,14 +456,17 @@ var usersWindow = (function() { x: scrollbarBackgroundPosition.x, y: scrollbarBackgroundPosition.y }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); Overlays.editOverlay(scrollbarBar, { x: scrollbarBackgroundPosition.x + 1, y: scrollbarBarPosition.y }); x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER - windowLineHeight - VISIBILITY_SPACER - windowLineHeight - WINDOW_BASE_MARGIN; + y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER + - windowLineHeight - VISIBILITY_SPACER + - windowLineHeight - WINDOW_BASE_MARGIN; Overlays.editOverlay(friendsButton, { x: x, y: y @@ -556,36 +554,9 @@ var usersWindow = (function() { usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); - checkLoggedIn(); } - var loggedIn = false; - - function checkLoggedIn() { - loggedIn = Account.isLoggedIn(); - if (loggedIn === false) { - Overlays.editOverlay(friendsButton, { - visible: false - }); - visibilityControl.setVisible(false); - displayControl.setVisible(false); - } else { - if (isMinimized === true) { - loggedIn = true; - return - } - Overlays.editOverlay(friendsButton, { - visible: true - }); - visibilityControl.setVisible(true); - displayControl.setVisible(true); - loggedIn = true; - - } - } - - - processUsers = function() { + processUsers = function () { var response, myUsername, user, @@ -638,7 +609,7 @@ var usersWindow = (function() { } }; - pollUsersTimedOut = function() { + pollUsersTimedOut = function () { print("Error: Request for users status timed out"); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; @@ -662,15 +633,11 @@ var usersWindow = (function() { Overlays.editOverlay(scrollbarBar, { visible: isVisible && isUsingScrollbars && !isMinimized }); - - if (loggedIn === true) { - Overlays.editOverlay(friendsButton, { - visible: isVisible && !isMinimized - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); - } - + Overlays.editOverlay(friendsButton, { + visible: isVisible && !isMinimized + }); + displayControl.setVisible(isVisible && !isMinimized); + visibilityControl.setVisible(isVisible && !isMinimized); } function setVisible(visible) { @@ -763,7 +730,9 @@ var usersWindow = (function() { userClicked = firstUserToDisplay + lineClicked; - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX + && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } @@ -831,8 +800,12 @@ var usersWindow = (function() { var isVisible; if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x + && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN + && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y + && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) + / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); updateOverlayPositions(); @@ -858,9 +831,13 @@ var usersWindow = (function() { isVisible = isBorderVisible; if (isVisible) { - isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x + && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH + && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y + && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; } else { - isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH + && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; } if (isVisible !== isBorderVisible) { isBorderVisible = isVisible; @@ -901,7 +878,8 @@ var usersWindow = (function() { isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay || isFullscreenMirror !== oldIsFullscreenMirror) { + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay + || isFullscreenMirror !== oldIsFullscreenMirror) { calculateWindowHeight(); updateUsersDisplay(); } @@ -951,8 +929,8 @@ var usersWindow = (function() { } else { hmdViewport = Controller.getRecommendedOverlayRect(); windowPosition = { - x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. - y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. + x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. + y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. }; } @@ -960,7 +938,7 @@ var usersWindow = (function() { windowBorder = Overlays.addOverlay("rectangle", { x: 0, - y: viewport.y, // Start up off-screen + y: viewport.y, // Start up off-screen width: WINDOW_BORDER_WIDTH, height: windowBorderHeight, radius: WINDOW_BORDER_RADIUS, @@ -1123,11 +1101,6 @@ var usersWindow = (function() { visible: isVisible && !isMinimized }); - - Script.setTimeout(function() { - checkLoggedIn() - }, 0); - Controller.mousePressEvent.connect(onMousePressEvent); Controller.mouseMoveEvent.connect(onMouseMoveEvent); Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); @@ -1170,4 +1143,4 @@ var usersWindow = (function() { setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}()); From 1c1d81ae25437e3b06aca39bbb973c0be634c5ee Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 12:18:03 +1200 Subject: [PATCH 0763/1237] Fix missing users.js menu item --- scripts/system/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 54fa5951ac..92a05b30fd 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -361,9 +361,9 @@ var usersWindow = (function () { myVisibility, - MENU_NAME = "Tools", + MENU_NAME = "View", MENU_ITEM = "Users Online", - MENU_ITEM_AFTER = "Chat...", + MENU_ITEM_AFTER = "Overlays", SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", SETINGS_USERS_WINDOW_OFFSET = "UsersWindow.Offset", From a619a142ed3ac771670ff27a2fef7743bd493276 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 24 Jun 2016 17:48:40 -0700 Subject: [PATCH 0764/1237] Friday night build, working on getting the LUT generated on the GPU --- libraries/fbx/src/FBXReader.cpp | 2 +- .../render-utils/src/DeferredGlobalLight.slh | 4 +- .../render-utils/src/DeferredLightingEffect.h | 2 +- .../render-utils/src/SubsurfaceScattering.cpp | 11 ++--- .../render-utils/src/SubsurfaceScattering.h | 2 +- .../render-utils/src/SubsurfaceScattering.slh | 1 + .../render-utils/src/SurfaceGeometryPass.h | 2 +- .../src/subsurfaceScattering_makeLUT.slf | 48 ++++++++++++++++--- .../src/surfaceGeometry_makeCurvature.slf | 2 +- libraries/ui/src/QmlWindowClass.cpp | 2 +- .../render/configSlider/ConfigSlider.qml | 4 +- .../utilities/render/deferredLighting.qml | 5 ++ 12 files changed, 62 insertions(+), 23 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 1fb67f1ace..8c9cdb2110 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -462,7 +462,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QVector blendshapes; QHash models; - QHash clusters; + QHash clusters; QHash animationCurves; QHash typeFlags; diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 7bb3c9f35d..a5396958ea 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -228,8 +228,8 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f if ( showBRDF()) return brdf; - vec3 debugNdotL = 0.5 * (NdotLSpectrum + vec3(1.0)); - return vec3(debugNdotL.z, curvature, 0.0 ); + //vec3 debugNdotL = 0.5 * (NdotLSpectrum + vec3(1.0)); + //return vec3(debugNdotL.z, curvature, 0.0 ); return vec3(color); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index da1f4ddfd7..85b9038046 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -165,7 +165,7 @@ public: float curvatureScale{ 0.8f }; bool enableScattering{ true }; - bool showScatteringBRDF{ false }; + bool showScatteringBRDF{ true }; bool enablePointLights{ true }; bool enableSpotLights{ true }; diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 968cc1b42f..adc6136cd2 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -401,10 +401,7 @@ void diffuseScatterGPU(gpu::TexturePointer& profileMap, gpu::TexturePointer& lut gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - // Stencil test the curvature pass for objects pixels only, not the background - // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - + makePipeline = gpu::Pipeline::create(program, state); } @@ -469,8 +466,8 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int WIDTH = 128; const int HEIGHT = 128; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, WIDTH, HEIGHT, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - diffuseScatter(scatteringLUT); - //diffuseScatterGPU(profileMap, scatteringLUT, args); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, WIDTH, HEIGHT, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + //diffuseScatter(scatteringLUT); + diffuseScatterGPU(profileMap, scatteringLUT, args); return scatteringLUT; } diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index c1d9a609b9..221cd96722 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -54,7 +54,7 @@ protected: glm::vec4 normalBentInfo{ 1.5f, 0.8f, 0.3f, 1.5f }; glm::vec2 curvatureInfo{ 0.08f, 0.8f }; float level{ 1.0f }; - float showBRDF{ 0.0f }; + float showBRDF{ 1.0f }; Parameters() {} }; diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index b002f2dcbd..ac759ab4a2 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -51,4 +51,5 @@ float unpackCurvature(float packedCurvature) { } + <@endfunc@> diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index c678ebba0f..697c93b745 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -26,7 +26,7 @@ class SurfaceGeometryPassConfig : public render::Job::Config { public: SurfaceGeometryPassConfig() : render::Job::Config(true) {} - float depthThreshold{ 0.1f }; + float depthThreshold{ 0.033f }; float basisScale{ 1.0f }; float curvatureScale{ 10.0f }; diff --git a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf index 90c7bfbf4e..41969640b5 100644 --- a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf +++ b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf @@ -14,9 +14,40 @@ const float _PI = 3.14159265358979523846; uniform sampler2D profileMap; +/* +vec3 scatter(float r) { + return texture(profileMap, vec2(2.0 * r, 0.5)).xyz; +} +*/ + +float gaussian(float v, float r) { + return (1.0 / sqrt(2.0 * _PI * v)) * exp(-(r*r) / (2.0 * v)); +} vec3 scatter(float r) { - return texture(profileMap, vec2(r * 0.5, 0.5)).xyz; + // Values from GPU Gems 3 "Advanced Skin Rendering". + // Originally taken from real life samples. + vec4 profile[6] = vec4[6]( + vec4(0.0064, 0.233, 0.455, 0.649), + vec4(0.0484, 0.100, 0.336, 0.344), + vec4(0.1870, 0.118, 0.198, 0.000), + vec4(0.5670, 0.113, 0.007, 0.007), + vec4(1.9900, 0.358, 0.004, 0.000), + vec4(7.4100, 0.078, 0.000, 0.000) + ); + //const int profileNum = 6; + + + + + vec3 ret(0.0); + for (int i = 0; i < 6; i++) { + float v = profile[i].x * 1.414; + float g = gaussian(v, r); + ret += g * profile[i].yzw; + } + + return ret; } @@ -32,15 +63,15 @@ vec3 integrate(float cosTheta, float skinRadius) { while (a <= (_PI)) { float sampleAngle = theta + a; - float diffuse = cos(sampleAngle); - if (diffuse < 0.0) diffuse = 0.0; - if (diffuse > 1.0) diffuse = 1.0; + float diffuse = clamp(cos(sampleAngle), 0.0, 1.0); + //if (diffuse < 0.0) diffuse = 0.0; + //if (diffuse > 1.0) diffuse = 1.0; // Distance. float sampleDist = abs(2.0 * skinRadius * sin(a * 0.5)); // Profile Weight. - vec3 weights = scatter(sampleDist); + vec3 weights = scatter( sampleDist); totalWeights += weights; totalLight += diffuse * weights; @@ -48,7 +79,8 @@ vec3 integrate(float cosTheta, float skinRadius) { } vec3 result = (totalLight / totalWeights); - + return clamp(result, vec3(0.0), vec3(1.0)); + return min(sqrt(result), vec3(1.0)); return scatter(skinRadius); @@ -59,6 +91,10 @@ out vec4 outFragColor; void main(void) { + // Lookup by: x: NDotL y: 1 / r + //float y = 2.0 * 1.0 / ((j + 1.0) / (double)height); + //float x = ((i / (double)width) * 2.0) - 1.0; + outFragColor = vec4(integrate(varTexCoord0.x * 2.0 - 1.0, 2.0 / varTexCoord0.y), 1.0); } diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index 8ca4ddcd7a..f896cb657d 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -120,7 +120,7 @@ void main(void) { // Calculate dF/du and dF/dv vec2 viewportScale = perspectiveScale * getInvWidthHeight(); - vec2 du = vec2( viewportScale.x, 0.0f ); + vec2 du = vec2( viewportScale.x * (stereoSide.w > 0.0 ? 0.5 : 1.0), 0.0f ); vec2 dv = vec2( 0.0f, viewportScale.y ); vec4 dFdu = vec4(getWorldNormalDiff(frameTexcoordPos, du), getEyeDepthDiff(frameTexcoordPos, du)); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index b8834f0549..c0eba4abf3 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -118,7 +118,7 @@ void QmlWindowClass::initQml(QVariantMap properties) { } bool visible = !properties.contains(VISIBILE_PROPERTY) || properties[VISIBILE_PROPERTY].toBool(); - object->setProperty(VISIBILE_PROPERTY, visible); + object->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); object->setProperty(SOURCE_PROPERTY, _source); // Forward messages received from QML on to the script diff --git a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml index 02135056f8..996cf4b34c 100644 --- a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml +++ b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml @@ -39,8 +39,8 @@ Item { Label { text: sliderControl.value.toFixed(root.integral ? 0 : 2) - anchors.left: root.labelControl.right - anchors.leftMargin: 8 + anchors.left: root.left + anchors.leftMargin: 200 anchors.top: root.top anchors.topMargin: 7 } diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 3cb91f54ce..1c9b10a0c3 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -57,6 +57,11 @@ Column { min: 0.0 } } + CheckBox { + text: "Scattering Table" + checked: Render.getConfig("Scattering").showLUT + onCheckedChanged: { Render.getConfig("Scattering").showLUT = checked } + } } } } From 9bc8441c735e5cee13a1c242236c11aa10b9cb6a Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 24 Jun 2016 17:50:43 -0700 Subject: [PATCH 0765/1237] Friday night build, working on getting the LUT generated on the GPU --- libraries/render-utils/src/subsurfaceScattering_makeLUT.slf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf index 41969640b5..463365ee36 100644 --- a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf +++ b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf @@ -40,7 +40,7 @@ vec3 scatter(float r) { - vec3 ret(0.0); + vec3 ret = vec3(0.0); for (int i = 0; i < 6; i++) { float v = profile[i].x * 1.414; float g = gaussian(v, r); From 8b0f59c9cfd032de2010d3d0cf1a9b93851f28a1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 13:12:16 +1200 Subject: [PATCH 0766/1237] Fix users window minimized state not being remembered --- scripts/system/users.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 92a05b30fd..afc3ac2de4 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -370,7 +370,7 @@ var usersWindow = (function () { // +ve x, y values are offset from left, top of screen; -ve from right, bottom. isVisible = true, - isMinimized = false, + isMinimized = true, isBorderVisible = false, viewport, @@ -388,6 +388,12 @@ var usersWindow = (function () { scrollbarBarClickedAt, // 0.0 .. 1.0 scrollbarValue = 0.0; // 0.0 .. 1.0 + function isValueTrue(value) { + // Work around Boolean Settings values being read as string when Interface starts up but as Booleans when re-read after + // Being written if refresh script. + return value === true || value === "true"; + } + function calculateWindowHeight() { var AUDIO_METER_HEIGHT = 52, MIRROR_HEIGHT = 220, @@ -664,6 +670,7 @@ var usersWindow = (function () { } }); updateOverlayVisibility(); + Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); } function onMenuItemEvent(event) { @@ -1121,12 +1128,10 @@ var usersWindow = (function () { pollUsers(); // Set minimized at end - setup code does not handle `minimized == false` correctly - setMinimized(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, false)); + setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, false))); } function tearDown() { - Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); - Menu.removeMenuItem(MENU_NAME, MENU_ITEM); Script.clearTimeout(usersTimer); From 0b6797acc354c44b89316fb429b90f83845d53c6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 13:48:43 +1200 Subject: [PATCH 0767/1237] Fix "show me" and "visible to" in users window not being remembered --- scripts/system/users.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index afc3ac2de4..b5fe7c4fad 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -359,14 +359,17 @@ var usersWindow = (function () { usersTimer = null, USERS_UPDATE_TIMEOUT = 5000, // ms = 5s + showMe, myVisibility, MENU_NAME = "View", MENU_ITEM = "Users Online", MENU_ITEM_AFTER = "Overlays", + SETTING_USERS_SHOW_ME = "UsersWindow.ShowMe", + SETTING_USERS_VISIBLE_TO = "UsersWindow.VisibleTo", SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", - SETINGS_USERS_WINDOW_OFFSET = "UsersWindow.Offset", + SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", // +ve x, y values are offset from left, top of screen; -ve from right, bottom. isVisible = true, @@ -550,7 +553,7 @@ var usersWindow = (function () { function pollUsers() { var url = API_URL; - if (displayControl.getValue() === DISPLAY_FRIENDS) { + if (showMe === DISPLAY_FRIENDS) { url += API_FRIENDS_FILTER; } @@ -681,9 +684,11 @@ var usersWindow = (function () { function onFindableByChanged(event) { if (VISIBILITY_VALUES.indexOf(event) !== -1) { + myVisibility = event; visibilityControl.setValue(event); + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); } else { - print("Error: Unrecognized onFindableByChanged value: " + myVisibility); + print("Error: Unrecognized onFindableByChanged value: " + event); } } @@ -713,11 +718,15 @@ var usersWindow = (function () { usersTimer = null; } pollUsers(); + showMe = displayControl.getValue(); + Settings.setValue(SETTING_USERS_SHOW_ME, showMe); return; } if (visibilityControl.handleClick(clickedOverlay)) { - GlobalServices.findableBy = visibilityControl.getValue(); + myVisibility = visibilityControl.getValue(); + GlobalServices.findableBy = myVisibility; + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); return; } @@ -869,7 +878,7 @@ var usersWindow = (function () { // Save offset of bottom of window to nearest edge of the window. offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? windowPosition.x : windowPosition.x - viewport.x; offset.y = (windowPosition.y < viewport.y / 2) ? windowPosition.y : windowPosition.y - viewport.y; - Settings.setValue(SETINGS_USERS_WINDOW_OFFSET, JSON.stringify(offset)); + Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); isMovingWindow = false; } } @@ -925,9 +934,9 @@ var usersWindow = (function () { viewport = Controller.getViewportDimensions(); - offsetSetting = Settings.getValue(SETINGS_USERS_WINDOW_OFFSET); + offsetSetting = Settings.getValue(SETTING_USERS_WINDOW_OFFSET); if (offsetSetting !== "") { - offset = JSON.parse(Settings.getValue(SETINGS_USERS_WINDOW_OFFSET)); + offset = JSON.parse(Settings.getValue(SETTING_USERS_WINDOW_OFFSET)); } if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; @@ -1048,9 +1057,14 @@ var usersWindow = (function () { alpha: FRIENDS_BUTTON_ALPHA }); + showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); + if (DISPLAY_VALUES.indexOf(showMe) === -1) { + showMe = DISPLAY_EVERYONE; + } + displayControl = new PopUpMenu({ prompt: DISPLAY_PROMPT, - value: DISPLAY_VALUES[0], + value: showMe, values: DISPLAY_VALUES, displayValues: DISPLAY_DISPLAY_VALUES, x: 0, @@ -1075,10 +1089,9 @@ var usersWindow = (function () { visible: isVisible && !isMinimized }); - myVisibility = GlobalServices.findableBy; + myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); if (VISIBILITY_VALUES.indexOf(myVisibility) === -1) { - print("Error: Unrecognized findableBy value: " + myVisibility); - myVisibility = VISIBILITY_ALL; + myVisibility = VISIBILITY_FRIENDS; } visibilityControl = new PopUpMenu({ From dc07368dc9717c71e212b4c46ef6f20b48201660 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 14:23:35 +1200 Subject: [PATCH 0768/1237] Hide users list when logged out --- scripts/system/users.js | 94 +++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index b5fe7c4fad..f23a0ea215 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -372,6 +372,7 @@ var usersWindow = (function () { SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", // +ve x, y values are offset from left, top of screen; -ve from right, bottom. + isLoggedIn = false, isVisible = true, isMinimized = true, isBorderVisible = false, @@ -526,13 +527,13 @@ var usersWindow = (function () { scrollbarBackgroundHeight = numUsersToDisplay * windowLineHeight - windowLineSpacing / 2; Overlays.editOverlay(scrollbarBackground, { height: scrollbarBackgroundHeight, - visible: isUsingScrollbars + visible: isLoggedIn && isUsingScrollbars }); scrollbarBarHeight = Math.max(numUsersToDisplay / linesOfUsers.length * scrollbarBackgroundHeight, SCROLLBAR_BAR_MIN_HEIGHT); Overlays.editOverlay(scrollbarBar, { height: scrollbarBarHeight, - visible: isUsingScrollbars + visible: isLoggedIn && isUsingScrollbars }); } @@ -550,6 +551,41 @@ var usersWindow = (function () { }); } + function updateOverlayVisibility() { + Overlays.editOverlay(windowBorder, { + visible: isLoggedIn && isVisible && isBorderVisible + }); + Overlays.editOverlay(windowPane, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(windowHeading, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(minimizeButton, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(scrollbarBackground, { + visible: isLoggedIn && isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(scrollbarBar, { + visible: isLoggedIn && isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(friendsButton, { + visible: isLoggedIn && isVisible && !isMinimized + }); + displayControl.setVisible(isLoggedIn && isVisible && !isMinimized); + visibilityControl.setVisible(isLoggedIn && isVisible && !isMinimized); + } + + function checkLoggedIn() { + var wasLoggedIn = isLoggedIn; + + isLoggedIn = Account.isLoggedIn(); + if (isLoggedIn !== wasLoggedIn) { + updateOverlayVisibility(); + } + } + function pollUsers() { var url = API_URL; @@ -563,6 +599,8 @@ var usersWindow = (function () { usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); + + checkLoggedIn(); } processUsers = function () { @@ -623,32 +661,6 @@ var usersWindow = (function () { usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; - function updateOverlayVisibility() { - Overlays.editOverlay(windowBorder, { - visible: isVisible && isBorderVisible - }); - Overlays.editOverlay(windowPane, { - visible: isVisible - }); - Overlays.editOverlay(windowHeading, { - visible: isVisible - }); - Overlays.editOverlay(minimizeButton, { - visible: isVisible - }); - Overlays.editOverlay(scrollbarBackground, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(scrollbarBar, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(friendsButton, { - visible: isVisible && !isMinimized - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); - } - function setVisible(visible) { isVisible = visible; @@ -662,7 +674,6 @@ var usersWindow = (function () { } updateOverlayVisibility(); - } function setMinimized(minimized) { @@ -876,8 +887,10 @@ var usersWindow = (function () { if (isMovingWindow) { // Save offset of bottom of window to nearest edge of the window. - offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? windowPosition.x : windowPosition.x - viewport.x; - offset.y = (windowPosition.y < viewport.y / 2) ? windowPosition.y : windowPosition.y - viewport.y; + offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) + ? windowPosition.x : windowPosition.x - viewport.x; + offset.y = (windowPosition.y < viewport.y / 2) + ? windowPosition.y : windowPosition.y - viewport.y; Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); isMovingWindow = false; } @@ -960,7 +973,7 @@ var usersWindow = (function () { radius: WINDOW_BORDER_RADIUS, color: WINDOW_BORDER_COLOR, alpha: WINDOW_BORDER_ALPHA, - visible: isVisible && isBorderVisible + visible: false }); windowPane = Overlays.addOverlay("text", { @@ -976,7 +989,7 @@ var usersWindow = (function () { backgroundAlpha: WINDOW_BACKGROUND_ALPHA, text: "", font: WINDOW_FONT, - visible: isVisible + visible: false }); windowHeading = Overlays.addOverlay("text", { @@ -991,7 +1004,7 @@ var usersWindow = (function () { backgroundAlpha: 0.0, text: "No users online", font: WINDOW_FONT, - visible: isVisible && !isMinimized + visible: false }); minimizeButton = Overlays.addOverlay("image", { @@ -1008,7 +1021,7 @@ var usersWindow = (function () { }, color: MIN_MAX_BUTTON_COLOR, alpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); scrollbarBackgroundPosition = { @@ -1023,7 +1036,7 @@ var usersWindow = (function () { backgroundColor: SCROLLBAR_BACKGROUND_COLOR, backgroundAlpha: SCROLLBAR_BACKGROUND_ALPHA, text: "", - visible: isVisible && isUsingScrollbars && !isMinimized + visible: false }); scrollbarBarPosition = { @@ -1038,7 +1051,7 @@ var usersWindow = (function () { backgroundColor: SCROLLBAR_BAR_COLOR, backgroundAlpha: SCROLLBAR_BAR_ALPHA, text: "", - visible: isVisible && isUsingScrollbars && !isMinimized + visible: false }); friendsButton = Overlays.addOverlay("image", { @@ -1054,7 +1067,8 @@ var usersWindow = (function () { height: FRIENDS_BUTTON_SVG_HEIGHT }, color: FRIENDS_BUTTON_COLOR, - alpha: FRIENDS_BUTTON_ALPHA + alpha: FRIENDS_BUTTON_ALPHA, + visible: false }); showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); @@ -1086,7 +1100,7 @@ var usersWindow = (function () { popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, buttonColor: MIN_MAX_BUTTON_COLOR, buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); @@ -1118,7 +1132,7 @@ var usersWindow = (function () { popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, buttonColor: MIN_MAX_BUTTON_COLOR, buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); Controller.mousePressEvent.connect(onMousePressEvent); From 17ce80ed69e97cdb075c27584751cccb78098cf5 Mon Sep 17 00:00:00 2001 From: Marko Kudjerski Date: Fri, 24 Jun 2016 19:28:31 -0700 Subject: [PATCH 0769/1237] sign executables with SHA256 --- cmake/macros/OptionalWinExecutableSigning.cmake | 2 +- cmake/templates/NSIS.template.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/macros/OptionalWinExecutableSigning.cmake b/cmake/macros/OptionalWinExecutableSigning.cmake index 784aae716f..41ca5762dc 100644 --- a/cmake/macros/OptionalWinExecutableSigning.cmake +++ b/cmake/macros/OptionalWinExecutableSigning.cmake @@ -22,7 +22,7 @@ macro(optional_win_executable_signing) # setup a post build command to sign the executable add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${SIGNTOOL_EXECUTABLE} sign /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 ${EXECUTABLE_PATH} + COMMAND ${SIGNTOOL_EXECUTABLE} sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 ${EXECUTABLE_PATH} ) else () message(FATAL_ERROR "HF_PFX_PASSPHRASE must be set for executables to be signed.") diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 0ea1199c09..4786b12743 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -64,7 +64,7 @@ ; The Inner invocation has written an uninstaller binary for us. ; We need to sign it if it's a production or PR build. !if @PRODUCTION_BUILD@ == 1 - !system '"@SIGNTOOL_EXECUTABLE@" sign /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 $%TEMP%\@UNINSTALLER_NAME@' = 0 + !system '"@SIGNTOOL_EXECUTABLE@" sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 $%TEMP%\@UNINSTALLER_NAME@' = 0 !endif ; Good. Now we can carry on writing the real installer. From bbe9b931bee9364581feb1ab983de329f7d0c301 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 26 Jun 2016 14:59:42 -0700 Subject: [PATCH 0770/1237] on Linux, don't mess with the menus from the hydra plugin because it makes Qt stop --- plugins/hifiSixense/src/SixenseManager.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index ade643ec72..baf13f1fae 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -77,6 +77,7 @@ bool SixenseManager::activate() { InputPlugin::activate(); #ifdef HAVE_SIXENSE + #if !defined(Q_OS_LINUX) _container->addMenu(MENU_PATH); _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, TOGGLE_SMOOTH, [this] (bool clicked) { setSixenseFilter(clicked); }, @@ -89,6 +90,7 @@ bool SixenseManager::activate() { _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, SHOW_DEBUG_CALIBRATED, [this] (bool clicked) { _inputDevice->setDebugDrawCalibrated(clicked); }, true, false); + #endif auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); @@ -106,8 +108,10 @@ void SixenseManager::deactivate() { InputPlugin::deactivate(); #ifdef HAVE_SIXENSE + #if !defined(Q_OS_LINUX) _container->removeMenuItem(MENU_NAME, TOGGLE_SMOOTH); _container->removeMenu(MENU_PATH); + #endif _inputDevice->_poseStateMap.clear(); From 55c2466249d65875f2a65e04a520de976edb4f34 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 27 Jun 2016 10:25:53 -0700 Subject: [PATCH 0771/1237] Move .eslintrc.js to project root This way, all project scripts are in scope, and the .eslintrc doesn't get pushed to the installer with the default scripts. --- scripts/.eslintrc.js => .eslintrc.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/.eslintrc.js => .eslintrc.js (100%) diff --git a/scripts/.eslintrc.js b/.eslintrc.js similarity index 100% rename from scripts/.eslintrc.js rename to .eslintrc.js From 0bfe3ea817ef96159d125c6cca45f53729030b2c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 27 Jun 2016 11:34:44 -0700 Subject: [PATCH 0772/1237] added support for disabling vsync on macs, but doesn't work with the rift --- .../display-plugins/OpenGLDisplayPlugin.cpp | 18 ++++++++++++++++-- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 3 +++ libraries/gl/src/gl/Config.h | 1 + libraries/gl/src/gl/GLWidget.cpp | 11 +++++++---- .../src/OculusLegacyDisplayPlugin.cpp | 1 + 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 4bca48aeb0..eb10d4ed5e 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -16,6 +16,9 @@ #include #include +#if defined(Q_OS_MAC) +#include +#endif #include #include #include @@ -597,8 +600,14 @@ void OpenGLDisplayPlugin::enableVsync(bool enable) { if (!_vsyncSupported) { return; } -#ifdef Q_OS_WIN +#if defined(Q_OS_WIN) wglSwapIntervalEXT(enable ? 1 : 0); +#elif defined(Q_OS_MAC) + GLint interval = enable ? 1 : 0; + CGLSetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &interval); +#else + // TODO: Fill in for linux + return; #endif } @@ -606,9 +615,14 @@ bool OpenGLDisplayPlugin::isVsyncEnabled() { if (!_vsyncSupported) { return true; } -#ifdef Q_OS_WIN +#if defined(Q_OS_WIN) return wglGetSwapIntervalEXT() != 0; +#elif defined(Q_OS_MAC) + GLint interval; + CGLGetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &interval); + return interval != 0; #else + // TODO: Fill in for linux return true; #endif } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index b29348f646..41af4ed543 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -200,7 +200,10 @@ static ProgramPtr getReprojectionProgram() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled + // On Mac, this won't work due to how the contexts are handled, so don't try +#if !defined(Q_OS_MAC) enableVsync(false); +#endif _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); diff --git a/libraries/gl/src/gl/Config.h b/libraries/gl/src/gl/Config.h index 593537a291..7947bd45df 100644 --- a/libraries/gl/src/gl/Config.h +++ b/libraries/gl/src/gl/Config.h @@ -20,6 +20,7 @@ #include #include +#include #endif diff --git a/libraries/gl/src/gl/GLWidget.cpp b/libraries/gl/src/gl/GLWidget.cpp index f113be1cfb..6fc9c41160 100644 --- a/libraries/gl/src/gl/GLWidget.cpp +++ b/libraries/gl/src/gl/GLWidget.cpp @@ -47,13 +47,16 @@ void GLWidget::initializeGL() { // Note, we *DO NOT* want Qt to automatically swap buffers for us. This results in the "ringing" bug mentioned in WL#19514 when we're throttling the framerate. setAutoBufferSwap(false); - // TODO: write the proper code for linux makeCurrent(); -#if defined(Q_OS_WIN) if (isValid() && context() && context()->contextHandle()) { - _vsyncSupported = context()->contextHandle()->hasExtension("WGL_EXT_swap_control");; - } +#if defined(Q_OS_WIN) + _vsyncSupported = context()->contextHandle()->hasExtension("WGL_EXT_swap_control"); +#elif defined(Q_OS_MAC) + _vsyncSupported = true; +#else + // TODO: write the proper code for linux #endif + } } void GLWidget::paintEvent(QPaintEvent* event) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index a994198957..e439b3fabf 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -73,6 +73,7 @@ bool OculusLegacyDisplayPlugin::isSupported() const { // - resolution and one component of position match // - resolution matches // - position matches + // If it still picks the wrong screen, you'll have to mess with your monitor configuration QList matches({ -1, -1, -1, -1 }); if (hmd) { QPoint targetPosition{ hmd->WindowsPos.x, hmd->WindowsPos.y }; From aae3555b63c964e6a484bdcd0dfcecac83b18b92 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Mon, 27 Jun 2016 21:01:06 +0100 Subject: [PATCH 0773/1237] update TouchscreenDevice * fix threading issue with zoom gesture * KeyboardMouseDevice touchpad disabled to prevent interference * device supported based on QTouchDevice::devices().count() --- interface/src/Application.cpp | 13 +- .../src/input-plugins/KeyboardMouseDevice.cpp | 117 +++++++++--------- .../src/input-plugins/KeyboardMouseDevice.h | 4 +- .../src/input-plugins/TouchscreenDevice.cpp | 21 ++-- .../src/input-plugins/TouchscreenDevice.h | 6 +- 5 files changed, 84 insertions(+), 77 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7858679583..7680112462 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -961,7 +961,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Setup the _keyboardMouseDevice, _touchscreenDevice and the user input mapper with the default bindings userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); - userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); + // if the _touchscreenDevice is not supported it will not be registered + if (_touchscreenDevice) { + userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); + } // force the model the look at the correct directory (weird order of operations issue) scriptEngines->setScriptsLocation(scriptEngines->getScriptsLocation()); @@ -2712,7 +2715,7 @@ void Application::touchUpdateEvent(QTouchEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchUpdateEvent(event); } - if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + if (_touchscreenDevice->isActive()) { _touchscreenDevice->touchUpdateEvent(event); } } @@ -2733,7 +2736,7 @@ void Application::touchBeginEvent(QTouchEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchBeginEvent(event); } - if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + if (_touchscreenDevice->isActive()) { _touchscreenDevice->touchBeginEvent(event); } @@ -2753,7 +2756,7 @@ void Application::touchEndEvent(QTouchEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchEndEvent(event); } - if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + if (_touchscreenDevice->isActive()) { _touchscreenDevice->touchEndEvent(event); } @@ -2761,7 +2764,7 @@ void Application::touchEndEvent(QTouchEvent* event) { } void Application::touchGestureEvent(QGestureEvent* event) { - if (Menu::getInstance()->isOptionChecked(TouchscreenDevice::NAME)) { + if (_touchscreenDevice->isActive()) { _touchscreenDevice->touchGestureEvent(event); } } diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index cc4117ffc0..550d127198 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -19,7 +19,7 @@ #include const QString KeyboardMouseDevice::NAME = "Keyboard/Mouse"; -bool KeyboardMouseDevice::_enableMouse = true; +bool KeyboardMouseDevice::_enableTouchpad = true; void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); @@ -62,32 +62,28 @@ void KeyboardMouseDevice::keyReleaseEvent(QKeyEvent* event) { } void KeyboardMouseDevice::mousePressEvent(QMouseEvent* event) { - if (_enableMouse) { - auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); - auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); - if (!result.second) { - // key pressed again ? without catching the release event ? - } - _lastCursor = event->pos(); - _mousePressTime = usecTimestampNow(); - _mouseMoved = false; - - eraseMouseClicked(); + auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); + auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); + if (!result.second) { + // key pressed again ? without catching the release event ? } + _lastCursor = event->pos(); + _mousePressTime = usecTimestampNow(); + _mouseMoved = false; + + eraseMouseClicked(); } void KeyboardMouseDevice::mouseReleaseEvent(QMouseEvent* event) { - if (_enableMouse) { - auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); - _inputDevice->_buttonPressedMap.erase(input.getChannel()); + auto input = _inputDevice->makeInput((Qt::MouseButton) event->button()); + _inputDevice->_buttonPressedMap.erase(input.getChannel()); - // if we pressed and released at the same location within a small time window, then create a "_CLICKED" - // input for this button we might want to add some small tolerance to this so if you do a small drag it - // still counts as a click. - static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click - if (!_mouseMoved && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) { - _inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel()); - } + // if we pressed and released at the same location within a small time window, then create a "_CLICKED" + // input for this button we might want to add some small tolerance to this so if you do a small drag it + // still counts as a click. + static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click + if (!_mouseMoved && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) { + _inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel()); } } @@ -98,24 +94,22 @@ void KeyboardMouseDevice::eraseMouseClicked() { } void KeyboardMouseDevice::mouseMoveEvent(QMouseEvent* event) { - if (_enableMouse) { - QPoint currentPos = event->pos(); - QPoint currentMove = currentPos - _lastCursor; + QPoint currentPos = event->pos(); + QPoint currentMove = currentPos - _lastCursor; - _inputDevice->_axisStateMap[MOUSE_AXIS_X_POS] = (currentMove.x() > 0 ? currentMove.x() : 0.0f); - _inputDevice->_axisStateMap[MOUSE_AXIS_X_NEG] = (currentMove.x() < 0 ? -currentMove.x() : 0.0f); - // Y mouse is inverted positive is pointing up the screen - _inputDevice->_axisStateMap[MOUSE_AXIS_Y_POS] = (currentMove.y() < 0 ? -currentMove.y() : 0.0f); - _inputDevice->_axisStateMap[MOUSE_AXIS_Y_NEG] = (currentMove.y() > 0 ? currentMove.y() : 0.0f); + _inputDevice->_axisStateMap[MOUSE_AXIS_X_POS] = (currentMove.x() > 0 ? currentMove.x() : 0.0f); + _inputDevice->_axisStateMap[MOUSE_AXIS_X_NEG] = (currentMove.x() < 0 ? -currentMove.x() : 0.0f); + // Y mouse is inverted positive is pointing up the screen + _inputDevice->_axisStateMap[MOUSE_AXIS_Y_POS] = (currentMove.y() < 0 ? -currentMove.y() : 0.0f); + _inputDevice->_axisStateMap[MOUSE_AXIS_Y_NEG] = (currentMove.y() > 0 ? currentMove.y() : 0.0f); - // FIXME - this has the characteristic that it will show large jumps when you move the cursor - // outside of the application window, because we don't get MouseEvents when the cursor is outside - // of the application window. - _lastCursor = currentPos; - _mouseMoved = true; + // FIXME - this has the characteristic that it will show large jumps when you move the cursor + // outside of the application window, because we don't get MouseEvents when the cursor is outside + // of the application window. + _lastCursor = currentPos; + _mouseMoved = true; - eraseMouseClicked(); - } + eraseMouseClicked(); } void KeyboardMouseDevice::wheelEvent(QWheelEvent* event) { @@ -139,34 +133,41 @@ glm::vec2 evalAverageTouchPoints(const QList& points) { } void KeyboardMouseDevice::touchBeginEvent(const QTouchEvent* event) { - _isTouching = event->touchPointStates().testFlag(Qt::TouchPointPressed); - _lastTouch = evalAverageTouchPoints(event->touchPoints()); - _lastTouchTime = _clock.now(); + if (_enableTouchpad) { + _isTouching = event->touchPointStates().testFlag(Qt::TouchPointPressed); + _lastTouch = evalAverageTouchPoints(event->touchPoints()); + _lastTouchTime = _clock.now(); + } } void KeyboardMouseDevice::touchEndEvent(const QTouchEvent* event) { - _isTouching = false; - _lastTouch = evalAverageTouchPoints(event->touchPoints()); - _lastTouchTime = _clock.now(); + if (_enableTouchpad) { + _isTouching = false; + _lastTouch = evalAverageTouchPoints(event->touchPoints()); + _lastTouchTime = _clock.now(); + } } void KeyboardMouseDevice::touchUpdateEvent(const QTouchEvent* event) { - auto currentPos = evalAverageTouchPoints(event->touchPoints()); - _lastTouchTime = _clock.now(); - - if (!_isTouching) { - _isTouching = event->touchPointStates().testFlag(Qt::TouchPointPressed); - } else { - auto currentMove = currentPos - _lastTouch; - - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_POS).getChannel()] = (currentMove.x > 0 ? currentMove.x : 0.0f); - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_NEG).getChannel()] = (currentMove.x < 0 ? -currentMove.x : 0.0f); - // Y mouse is inverted positive is pointing up the screen - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_POS).getChannel()] = (currentMove.y < 0 ? -currentMove.y : 0.0f); - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_NEG).getChannel()] = (currentMove.y > 0 ? currentMove.y : 0.0f); - } + if (_enableTouchpad) { + auto currentPos = evalAverageTouchPoints(event->touchPoints()); + _lastTouchTime = _clock.now(); - _lastTouch = currentPos; + if (!_isTouching) { + _isTouching = event->touchPointStates().testFlag(Qt::TouchPointPressed); + } + else { + auto currentMove = currentPos - _lastTouch; + + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_POS).getChannel()] = (currentMove.x > 0 ? currentMove.x : 0.0f); + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_NEG).getChannel()] = (currentMove.x < 0 ? -currentMove.x : 0.0f); + // Y mouse is inverted positive is pointing up the screen + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_POS).getChannel()] = (currentMove.y < 0 ? -currentMove.y : 0.0f); + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_NEG).getChannel()] = (currentMove.y > 0 ? currentMove.y : 0.0f); + } + + _lastTouch = currentPos; + } } controller::Input KeyboardMouseDevice::InputDevice::makeInput(Qt::Key code) const { diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h index fc2ad44a6d..d88c410ade 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h @@ -85,7 +85,7 @@ public: void wheelEvent(QWheelEvent* event); - static void enableMouse(bool enableMouse) { _enableMouse = enableMouse; } + static void enableTouchpad(bool enableTouchpad) { _enableTouchpad = enableTouchpad; } static const QString NAME; @@ -125,7 +125,7 @@ protected: std::chrono::high_resolution_clock _clock; std::chrono::high_resolution_clock::time_point _lastTouchTime; - static bool _enableMouse; + static bool _enableTouchpad; }; #endif // hifi_KeyboardMouseDevice_h diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 9430f1d23a..8e3eea4cf6 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -51,6 +51,13 @@ void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCal distanceScaleY = (_currentTouchVec.y - _firstTouchVec.y) / DPI_SCALE_Y; _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_NEG).getChannel()] = distanceScaleY; } + } else if (_touchPointCount == 2) { + if (_scaleFactor > _lastPinchScale && _scaleFactor != 0) { + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_POS).getChannel()] = 1.0f; + } else if (_scaleFactor != 0) { + _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_NEG).getChannel()] = 1.0f; + } + _lastPinchScale = _scaleFactor; } } @@ -64,12 +71,12 @@ void TouchscreenDevice::InputDevice::focusOutEvent() { void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); - KeyboardMouseDevice::enableMouse(false); + KeyboardMouseDevice::enableTouchpad(false); } void TouchscreenDevice::touchEndEvent(const QTouchEvent* event) { _touchPointCount = 0; - KeyboardMouseDevice::enableMouse(true); + KeyboardMouseDevice::enableTouchpad(true); } void TouchscreenDevice::touchUpdateEvent(const QTouchEvent* event) { @@ -81,13 +88,7 @@ void TouchscreenDevice::touchUpdateEvent(const QTouchEvent* event) { void TouchscreenDevice::touchGestureEvent(const QGestureEvent* event) { if (QGesture* gesture = event->gesture(Qt::PinchGesture)) { QPinchGesture* pinch = static_cast(gesture); - qreal scaleFactor = pinch->totalScaleFactor(); - if (scaleFactor > _lastPinchScale && scaleFactor != 0) { - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_POS).getChannel()] = 1.0f; - } else if (scaleFactor != 0) { - _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_NEG).getChannel()] = 1.0f; - } - _lastPinchScale = scaleFactor; + _scaleFactor = pinch->totalScaleFactor(); } } @@ -118,4 +119,4 @@ controller::Input::NamedVector TouchscreenDevice::InputDevice::getAvailableInput QString TouchscreenDevice::InputDevice::getDefaultMappingConfig() const { static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/touchscreen.json"; return MAPPING_JSON; -} \ No newline at end of file +} diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index 772776348f..53dba19b00 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -14,6 +14,7 @@ #include #include "InputPlugin.h" +#include class QTouchEvent; class QGestureEvent; @@ -32,10 +33,10 @@ public: enum TouchGestureAxisChannel { TOUCH_GESTURE_PINCH_POS = TOUCH_AXIS_Y_NEG + 1, TOUCH_GESTURE_PINCH_NEG, - }; + }; // Plugin functions - virtual bool isSupported() const override { return true; } + virtual bool isSupported() const override { return QTouchDevice::devices().count() > 0; } virtual const QString& getName() const override { return NAME; } virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } @@ -71,6 +72,7 @@ public: protected: qreal _lastPinchScale; + qreal _scaleFactor; glm::vec2 _firstTouchVec; glm::vec2 _currentTouchVec; int _touchPointCount; From 480b1a1263c83225496004cda781c1872f458b83 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Mon, 27 Jun 2016 21:06:06 +0100 Subject: [PATCH 0774/1237] extra line in KeyboardMouse Device there's 2 blank lines at the end of the file --- .../input-plugins/src/input-plugins/KeyboardMouseDevice.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index 550d127198..6edaddb24a 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -255,4 +255,3 @@ QString KeyboardMouseDevice::InputDevice::getDefaultMappingConfig() const { static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/keyboardMouse.json"; return MAPPING_JSON; } - From 235ee4bd463e69c3bcd1d808ece0244c1014926d Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 27 Jun 2016 13:06:12 -0700 Subject: [PATCH 0775/1237] Refactor Camera scripting interface a little get/setRotation has been removed, as it was a duplicate of get/setOrientation, but undocumented and barely used. get/setTransform and get/setPerspective have been changed to methods from slots, since glm::mat4 is not exposed to the scriping interface so they don't work in scripting. --- interface/src/Application.cpp | 26 +++++++++++++------------- interface/src/Camera.cpp | 16 ++++++++-------- interface/src/Camera.h | 27 ++++++++++++--------------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 26da43fb58..049356e298 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1717,22 +1717,22 @@ void Application::paintGL() { if (isHMDMode()) { mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setRotation(glm::quat_cast(camMat)); + _myCamera.setOrientation(glm::quat_cast(camMat)); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setRotation(myAvatar->getHead()->getCameraOrientation()); + _myCamera.setOrientation(myAvatar->getHead()->getCameraOrientation()); } } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { if (isHMDMode()) { auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setRotation(glm::normalize(glm::quat_cast(hmdWorldMat))); + _myCamera.setOrientation(glm::normalize(glm::quat_cast(hmdWorldMat))); _myCamera.setPosition(extractTranslation(hmdWorldMat) + myAvatar->getOrientation() * boomOffset); } else { - _myCamera.setRotation(myAvatar->getHead()->getOrientation()); + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getRotation() * boomOffset); + + _myCamera.getOrientation() * boomOffset); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition() + myAvatar->getOrientation() * boomOffset); @@ -1751,7 +1751,7 @@ void Application::paintGL() { glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - _myCamera.setRotation(worldMirrorRotation); + _myCamera.setOrientation(worldMirrorRotation); glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); // Mirror HMD lateral offsets @@ -1762,7 +1762,7 @@ void Application::paintGL() { + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + mirrorBodyOrientation * hmdOffset); } else { - _myCamera.setRotation(myAvatar->getWorldAlignedOrientation() + _myCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); _myCamera.setPosition(myAvatar->getDefaultEyePosition() + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) @@ -1775,11 +1775,11 @@ void Application::paintGL() { if (cameraEntity != nullptr) { if (isHMDMode()) { glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setRotation(cameraEntity->getRotation() * hmdRotation); + _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); } else { - _myCamera.setRotation(cameraEntity->getRotation()); + _myCamera.setOrientation(cameraEntity->getRotation()); _myCamera.setPosition(cameraEntity->getPosition()); } } @@ -3314,9 +3314,9 @@ void Application::updateMyAvatarLookAtPosition() { if (isLookingAtSomeone) { deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; } - lookAtSpot = origin + _myCamera.getRotation() * glm::quat(glm::radians(glm::vec3( + lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( eyePitch * deflection, eyeYaw * deflection, 0.0f))) * - glm::inverse(_myCamera.getRotation()) * (lookAtSpot - origin); + glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); } } @@ -4032,7 +4032,7 @@ void Application::loadViewFrustum(Camera& camera, ViewFrustum& viewFrustum) { // Set the viewFrustum up with the correct position and orientation of the camera viewFrustum.setPosition(camera.getPosition()); - viewFrustum.setOrientation(camera.getRotation()); + viewFrustum.setOrientation(camera.getOrientation()); // Ask the ViewFrustum class to calculate our corners viewFrustum.calculate(); @@ -4305,7 +4305,7 @@ void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& regi myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); } _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - _mirrorCamera.setRotation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); + _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); // set the bounds of rear mirror view diff --git a/interface/src/Camera.cpp b/interface/src/Camera.cpp index 53a3500bff..227bdadb97 100644 --- a/interface/src/Camera.cpp +++ b/interface/src/Camera.cpp @@ -62,14 +62,14 @@ void Camera::update(float deltaTime) { } void Camera::recompose() { - mat4 orientation = glm::mat4_cast(_rotation); + mat4 orientation = glm::mat4_cast(_orientation); mat4 translation = glm::translate(mat4(), _position); _transform = translation * orientation; } void Camera::decompose() { _position = vec3(_transform[3]); - _rotation = glm::quat_cast(_transform); + _orientation = glm::quat_cast(_transform); } void Camera::setTransform(const glm::mat4& transform) { @@ -85,8 +85,8 @@ void Camera::setPosition(const glm::vec3& position) { } } -void Camera::setRotation(const glm::quat& rotation) { - _rotation = rotation; +void Camera::setOrientation(const glm::quat& orientation) { + _orientation = orientation; recompose(); if (_isKeepLookingAt) { lookAt(_lookingAt); @@ -154,9 +154,9 @@ QString Camera::getModeString() const { void Camera::lookAt(const glm::vec3& lookAt) { glm::vec3 up = IDENTITY_UP; glm::mat4 lookAtMatrix = glm::lookAt(_position, lookAt, up); - glm::quat rotation = glm::quat_cast(lookAtMatrix); - rotation.w = -rotation.w; // Rosedale approved - _rotation = rotation; + glm::quat orientation = glm::quat_cast(lookAtMatrix); + orientation.w = -orientation.w; // Rosedale approved + _orientation = orientation; } void Camera::keepLookingAt(const glm::vec3& point) { @@ -171,7 +171,7 @@ void Camera::loadViewFrustum(ViewFrustum& frustum) const { // Set the viewFrustum up with the correct position and orientation of the camera frustum.setPosition(getPosition()); - frustum.setOrientation(getRotation()); + frustum.setOrientation(getOrientation()); // Ask the ViewFrustum class to calculate our corners frustum.calculate(); diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 486b98c100..46cad2efc8 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -45,7 +45,7 @@ class Camera : public QObject { public: Camera(); - void initialize(); // instantly put the camera at the ideal position and rotation. + void initialize(); // instantly put the camera at the ideal position and orientation. void update( float deltaTime ); @@ -57,25 +57,22 @@ public: EntityItemPointer getCameraEntityPointer() const { return _cameraEntity; } -public slots: - QString getModeString() const; - void setModeString(const QString& mode); - - glm::quat getRotation() const { return _rotation; } - void setRotation(const glm::quat& rotation); - - glm::vec3 getPosition() const { return _position; } - void setPosition(const glm::vec3& position); - - glm::quat getOrientation() const { return getRotation(); } - void setOrientation(const glm::quat& orientation) { setRotation(orientation); } - const glm::mat4& getTransform() const { return _transform; } void setTransform(const glm::mat4& transform); const glm::mat4& getProjection() const { return _projection; } void setProjection(const glm::mat4& projection); +public slots: + QString getModeString() const; + void setModeString(const QString& mode); + + glm::vec3 getPosition() const { return _position; } + void setPosition(const glm::vec3& position); + + glm::quat getOrientation() const { return _orientation; } + void setOrientation(const glm::quat& orientation); + QUuid getCameraEntity() const; void setCameraEntity(QUuid entityID); @@ -105,7 +102,7 @@ private: // derived glm::vec3 _position; - glm::quat _rotation; + glm::quat _orientation; bool _isKeepLookingAt{ false }; glm::vec3 _lookingAt; EntityItemPointer _cameraEntity; From 7a4b11ee9790e902d55e12292fe9b420c72478f3 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 27 Jun 2016 13:06:19 -0700 Subject: [PATCH 0776/1237] more work on codecs --- assignment-client/CMakeLists.txt | 2 +- assignment-client/src/audio/AudioMixer.cpp | 25 ++++++ libraries/audio-client/src/AudioClient.cpp | 94 +++++++++++++++------- libraries/audio-client/src/AudioClient.h | 5 +- plugins/pcmCodec/src/PCMCodecManager.cpp | 2 + 5 files changed, 95 insertions(+), 33 deletions(-) diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 1b5840c3c8..d0fd2c1176 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -6,7 +6,7 @@ setup_hifi_project(Core Gui Network Script Quick Widgets WebSockets) link_hifi_libraries( audio avatars octree gpu model fbx entities networking animation recording shared script-engine embedded-webserver - controllers physics + controllers physics plugins ) if (WIN32) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 95578e3998..27b0a092d0 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -47,6 +47,8 @@ #include #include #include +#include +#include #include #include #include @@ -447,6 +449,20 @@ void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer mes } } +DisplayPluginList getDisplayPlugins() { + DisplayPluginList result; + return result; +} + +InputPluginList getInputPlugins() { + InputPluginList result; + return result; +} + +void saveInputPluginSettings(const InputPluginList& plugins) { +} + + void AudioMixer::handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode) { qDebug() << __FUNCTION__; @@ -461,6 +477,15 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess } qDebug() << "all requested codecs:" << codecList; + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + if (codecPlugins.size() > 0) { + for (auto& plugin : codecPlugins) { + qDebug() << "Codec available:" << plugin->getName(); + } + } else { + qDebug() << "No Codecs available..."; + } + auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); // write them to our packet diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 18db23dbf7..7ef71087ce 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -477,32 +477,6 @@ void AudioClient::stop() { } } -void AudioClient::negotiateAudioFormat() { - qDebug() << __FUNCTION__; - - auto nodeList = DependencyManager::get(); - - auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat); - - auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); - - quint8 numberOfCodecs = (quint8)codecPlugins.size(); - negotiateFormatPacket->writePrimitive(numberOfCodecs); - for (auto& plugin : codecPlugins) { - qDebug() << "Codec available:" << plugin->getName(); - negotiateFormatPacket->writeString(plugin->getName()); - } - - // grab our audio mixer from the NodeList, if it exists - SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); - - if (audioMixer) { - // send off this mute packet - nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer); - } -} - - void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointer message) { char bitset; @@ -557,13 +531,47 @@ void AudioClient::handleMuteEnvironmentPacket(QSharedPointer me emit muteEnvironmentRequested(position, radius); } +void AudioClient::negotiateAudioFormat() { + qDebug() << __FUNCTION__; + + auto nodeList = DependencyManager::get(); + + auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat); + + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + + quint8 numberOfCodecs = (quint8)codecPlugins.size(); + negotiateFormatPacket->writePrimitive(numberOfCodecs); + for (auto& plugin : codecPlugins) { + qDebug() << "Codec available:" << plugin->getName(); + negotiateFormatPacket->writeString(plugin->getName()); + } + + // grab our audio mixer from the NodeList, if it exists + SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); + + if (audioMixer) { + // send off this mute packet + nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer); + } +} + void AudioClient::handleSelectedAudioFormat(QSharedPointer message) { qDebug() << __FUNCTION__; // write them to our packet - QString selectedCodec = message->readString(); + _selectedCodecName = message->readString(); + + qDebug() << "Selected Codec:" << _selectedCodecName; + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& plugin : codecPlugins) { + if (_selectedCodecName == plugin->getName()) { + _codec = plugin; + qDebug() << "Selected Codec Plugin:" << _codec.get(); + break; + } + } - qDebug() << "selectedCodec:" << selectedCodec; } @@ -839,7 +847,17 @@ void AudioClient::handleAudioInput() { audioTransform.setTranslation(_positionGetter()); audioTransform.setRotation(_orientationGetter()); // FIXME find a way to properly handle both playback audio and user audio concurrently - emitAudioPacket(networkAudioSamples, numNetworkBytes, _outgoingAvatarAudioSequenceNumber, audioTransform, packetType); + + // TODO - codec encode goes here + QByteArray decocedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + QByteArray encodedBuffer; + if (_codec) { + _codec->encode(decocedBuffer, encodedBuffer); + } else { + encodedBuffer = decocedBuffer; + } + + emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, packetType); _stats.sentPacket(); } } @@ -848,14 +866,28 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { Transform audioTransform; audioTransform.setTranslation(_positionGetter()); audioTransform.setRotation(_orientationGetter()); + + // TODO - codec decode goes here + QByteArray encodedBuffer; + if (_codec) { + _codec->encode(audio, encodedBuffer); + } else { + encodedBuffer = audio; + } + // FIXME check a flag to see if we should echo audio? - emitAudioPacket(audio.data(), audio.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho); + emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho); } void AudioClient::processReceivedSamples(const QByteArray& networkBuffer, QByteArray& outputBuffer) { // TODO - codec decode goes here - QByteArray decodedBuffer = networkBuffer; + QByteArray decodedBuffer; + if (_codec) { + _codec->decode(networkBuffer, decodedBuffer); + } else { + decodedBuffer = networkBuffer; + } const int numDecodecSamples = decodedBuffer.size() / sizeof(int16_t); const int numDeviceOutputSamples = _outputFrameSize; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index e05612f859..45b4a631b2 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -38,6 +38,8 @@ #include #include +#include + #include "AudioIOStats.h" #include "AudioNoiseGate.h" #include "AudioSRC.h" @@ -296,7 +298,8 @@ private: bool _hasReceivedFirstPacket = false; - //CodecPluginPointer _codec { nullptr }; + CodecPluginPointer _codec; + QString _selectedCodecName; }; diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index 2401e99576..eb2f4761b4 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -39,11 +39,13 @@ bool PCMCodecManager::isSupported() const { void PCMCodecManager::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { + qDebug() << __FUNCTION__ << "encodedBuffer:" << encodedBuffer.size(); // this codec doesn't actually do anything.... decodedBuffer = encodedBuffer; } void PCMCodecManager::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { + qDebug() << __FUNCTION__ << "decodedBuffer:" << decodedBuffer.size(); // this codec doesn't actually do anything.... encodedBuffer = decodedBuffer; } From 5aaf1f33929e755e9253ca80f1ef4275336f0cff Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 12:03:36 +1200 Subject: [PATCH 0777/1237] Undo hide users.js when logged out --- scripts/system/users.js | 109 +++++++++++++++------------------------- 1 file changed, 41 insertions(+), 68 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 5b0ba42a45..54fa5951ac 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var PopUpMenu = function(properties) { +var PopUpMenu = function (properties) { var value = properties.value, promptOverlay, valueOverlay, @@ -217,7 +217,7 @@ var PopUpMenu = function(properties) { }; }; -var usersWindow = (function() { +var usersWindow = (function () { var baseURL = Script.resolvePath("assets/images/tools/"), WINDOW_WIDTH = 260, @@ -253,11 +253,7 @@ var usersWindow = (function() { WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_LEFT_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_RADIUS = 4, - WINDOW_BORDER_COLOR = { - red: 255, - green: 255, - blue: 255 - }, + WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, WINDOW_BORDER_ALPHA = 0.5, windowBorder, @@ -381,12 +377,9 @@ var usersWindow = (function() { isMirrorDisplay = false, isFullscreenMirror = false, - windowPosition = {}, // Bottom left corner of window pane. + windowPosition = {}, // Bottom left corner of window pane. isMovingWindow = false, - movingClickOffset = { - x: 0, - y: 0 - }, + movingClickOffset = { x: 0, y: 0 }, isUsingScrollbars = false, isMovingScrollbar = false, @@ -408,7 +401,9 @@ var usersWindow = (function() { } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + windowLineHeight + VISIBILITY_SPACER + windowLineHeight + WINDOW_BASE_MARGIN; + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + + windowLineHeight + VISIBILITY_SPACER + + windowLineHeight + WINDOW_BASE_MARGIN; // Limit window to height of viewport above window position minus VU meter and mirror if displayed windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; @@ -461,14 +456,17 @@ var usersWindow = (function() { x: scrollbarBackgroundPosition.x, y: scrollbarBackgroundPosition.y }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); Overlays.editOverlay(scrollbarBar, { x: scrollbarBackgroundPosition.x + 1, y: scrollbarBarPosition.y }); x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER - windowLineHeight - VISIBILITY_SPACER - windowLineHeight - WINDOW_BASE_MARGIN; + y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER + - windowLineHeight - VISIBILITY_SPACER + - windowLineHeight - WINDOW_BASE_MARGIN; Overlays.editOverlay(friendsButton, { x: x, y: y @@ -556,36 +554,9 @@ var usersWindow = (function() { usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); - checkLoggedIn(); } - var loggedIn = false; - - function checkLoggedIn() { - loggedIn = Account.isLoggedIn(); - if (loggedIn === false) { - Overlays.editOverlay(friendsButton, { - visible: false - }); - visibilityControl.setVisible(false); - displayControl.setVisible(false); - } else { - if (isMinimized === true) { - loggedIn = true; - return - } - Overlays.editOverlay(friendsButton, { - visible: true - }); - visibilityControl.setVisible(true); - displayControl.setVisible(true); - loggedIn = true; - - } - } - - - processUsers = function() { + processUsers = function () { var response, myUsername, user, @@ -638,7 +609,7 @@ var usersWindow = (function() { } }; - pollUsersTimedOut = function() { + pollUsersTimedOut = function () { print("Error: Request for users status timed out"); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; @@ -662,15 +633,11 @@ var usersWindow = (function() { Overlays.editOverlay(scrollbarBar, { visible: isVisible && isUsingScrollbars && !isMinimized }); - - if (loggedIn === true) { - Overlays.editOverlay(friendsButton, { - visible: isVisible && !isMinimized - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); - } - + Overlays.editOverlay(friendsButton, { + visible: isVisible && !isMinimized + }); + displayControl.setVisible(isVisible && !isMinimized); + visibilityControl.setVisible(isVisible && !isMinimized); } function setVisible(visible) { @@ -763,7 +730,9 @@ var usersWindow = (function() { userClicked = firstUserToDisplay + lineClicked; - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX + && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } @@ -831,8 +800,12 @@ var usersWindow = (function() { var isVisible; if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x + && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN + && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y + && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) + / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); updateOverlayPositions(); @@ -858,9 +831,13 @@ var usersWindow = (function() { isVisible = isBorderVisible; if (isVisible) { - isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x + && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH + && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y + && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; } else { - isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH + && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; } if (isVisible !== isBorderVisible) { isBorderVisible = isVisible; @@ -901,7 +878,8 @@ var usersWindow = (function() { isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay || isFullscreenMirror !== oldIsFullscreenMirror) { + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay + || isFullscreenMirror !== oldIsFullscreenMirror) { calculateWindowHeight(); updateUsersDisplay(); } @@ -951,8 +929,8 @@ var usersWindow = (function() { } else { hmdViewport = Controller.getRecommendedOverlayRect(); windowPosition = { - x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. - y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. + x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. + y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. }; } @@ -960,7 +938,7 @@ var usersWindow = (function() { windowBorder = Overlays.addOverlay("rectangle", { x: 0, - y: viewport.y, // Start up off-screen + y: viewport.y, // Start up off-screen width: WINDOW_BORDER_WIDTH, height: windowBorderHeight, radius: WINDOW_BORDER_RADIUS, @@ -1123,11 +1101,6 @@ var usersWindow = (function() { visible: isVisible && !isMinimized }); - - Script.setTimeout(function() { - checkLoggedIn() - }, 0); - Controller.mousePressEvent.connect(onMousePressEvent); Controller.mouseMoveEvent.connect(onMouseMoveEvent); Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); @@ -1170,4 +1143,4 @@ var usersWindow = (function() { setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}()); From 35769b73a5435353f9a24db1609fb9858c26674d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 12:18:03 +1200 Subject: [PATCH 0778/1237] Fix missing users.js menu item --- scripts/system/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 54fa5951ac..92a05b30fd 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -361,9 +361,9 @@ var usersWindow = (function () { myVisibility, - MENU_NAME = "Tools", + MENU_NAME = "View", MENU_ITEM = "Users Online", - MENU_ITEM_AFTER = "Chat...", + MENU_ITEM_AFTER = "Overlays", SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", SETINGS_USERS_WINDOW_OFFSET = "UsersWindow.Offset", From 6a0c29f9936ab4be394dc30b86e732149f3a3c42 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 13:12:16 +1200 Subject: [PATCH 0779/1237] Fix users window minimized state not being remembered --- scripts/system/users.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 92a05b30fd..afc3ac2de4 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -370,7 +370,7 @@ var usersWindow = (function () { // +ve x, y values are offset from left, top of screen; -ve from right, bottom. isVisible = true, - isMinimized = false, + isMinimized = true, isBorderVisible = false, viewport, @@ -388,6 +388,12 @@ var usersWindow = (function () { scrollbarBarClickedAt, // 0.0 .. 1.0 scrollbarValue = 0.0; // 0.0 .. 1.0 + function isValueTrue(value) { + // Work around Boolean Settings values being read as string when Interface starts up but as Booleans when re-read after + // Being written if refresh script. + return value === true || value === "true"; + } + function calculateWindowHeight() { var AUDIO_METER_HEIGHT = 52, MIRROR_HEIGHT = 220, @@ -664,6 +670,7 @@ var usersWindow = (function () { } }); updateOverlayVisibility(); + Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); } function onMenuItemEvent(event) { @@ -1121,12 +1128,10 @@ var usersWindow = (function () { pollUsers(); // Set minimized at end - setup code does not handle `minimized == false` correctly - setMinimized(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, false)); + setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, false))); } function tearDown() { - Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); - Menu.removeMenuItem(MENU_NAME, MENU_ITEM); Script.clearTimeout(usersTimer); From 8f2e95cde2643928b8ab576702ee93368f81de9d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 13:48:43 +1200 Subject: [PATCH 0780/1237] Fix "show me" and "visible to" in users window not being remembered --- scripts/system/users.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index afc3ac2de4..b5fe7c4fad 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -359,14 +359,17 @@ var usersWindow = (function () { usersTimer = null, USERS_UPDATE_TIMEOUT = 5000, // ms = 5s + showMe, myVisibility, MENU_NAME = "View", MENU_ITEM = "Users Online", MENU_ITEM_AFTER = "Overlays", + SETTING_USERS_SHOW_ME = "UsersWindow.ShowMe", + SETTING_USERS_VISIBLE_TO = "UsersWindow.VisibleTo", SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", - SETINGS_USERS_WINDOW_OFFSET = "UsersWindow.Offset", + SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", // +ve x, y values are offset from left, top of screen; -ve from right, bottom. isVisible = true, @@ -550,7 +553,7 @@ var usersWindow = (function () { function pollUsers() { var url = API_URL; - if (displayControl.getValue() === DISPLAY_FRIENDS) { + if (showMe === DISPLAY_FRIENDS) { url += API_FRIENDS_FILTER; } @@ -681,9 +684,11 @@ var usersWindow = (function () { function onFindableByChanged(event) { if (VISIBILITY_VALUES.indexOf(event) !== -1) { + myVisibility = event; visibilityControl.setValue(event); + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); } else { - print("Error: Unrecognized onFindableByChanged value: " + myVisibility); + print("Error: Unrecognized onFindableByChanged value: " + event); } } @@ -713,11 +718,15 @@ var usersWindow = (function () { usersTimer = null; } pollUsers(); + showMe = displayControl.getValue(); + Settings.setValue(SETTING_USERS_SHOW_ME, showMe); return; } if (visibilityControl.handleClick(clickedOverlay)) { - GlobalServices.findableBy = visibilityControl.getValue(); + myVisibility = visibilityControl.getValue(); + GlobalServices.findableBy = myVisibility; + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); return; } @@ -869,7 +878,7 @@ var usersWindow = (function () { // Save offset of bottom of window to nearest edge of the window. offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? windowPosition.x : windowPosition.x - viewport.x; offset.y = (windowPosition.y < viewport.y / 2) ? windowPosition.y : windowPosition.y - viewport.y; - Settings.setValue(SETINGS_USERS_WINDOW_OFFSET, JSON.stringify(offset)); + Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); isMovingWindow = false; } } @@ -925,9 +934,9 @@ var usersWindow = (function () { viewport = Controller.getViewportDimensions(); - offsetSetting = Settings.getValue(SETINGS_USERS_WINDOW_OFFSET); + offsetSetting = Settings.getValue(SETTING_USERS_WINDOW_OFFSET); if (offsetSetting !== "") { - offset = JSON.parse(Settings.getValue(SETINGS_USERS_WINDOW_OFFSET)); + offset = JSON.parse(Settings.getValue(SETTING_USERS_WINDOW_OFFSET)); } if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; @@ -1048,9 +1057,14 @@ var usersWindow = (function () { alpha: FRIENDS_BUTTON_ALPHA }); + showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); + if (DISPLAY_VALUES.indexOf(showMe) === -1) { + showMe = DISPLAY_EVERYONE; + } + displayControl = new PopUpMenu({ prompt: DISPLAY_PROMPT, - value: DISPLAY_VALUES[0], + value: showMe, values: DISPLAY_VALUES, displayValues: DISPLAY_DISPLAY_VALUES, x: 0, @@ -1075,10 +1089,9 @@ var usersWindow = (function () { visible: isVisible && !isMinimized }); - myVisibility = GlobalServices.findableBy; + myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); if (VISIBILITY_VALUES.indexOf(myVisibility) === -1) { - print("Error: Unrecognized findableBy value: " + myVisibility); - myVisibility = VISIBILITY_ALL; + myVisibility = VISIBILITY_FRIENDS; } visibilityControl = new PopUpMenu({ From e62cbd8f44db9609471e756c5a19b2c61620a89f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 25 Jun 2016 14:23:35 +1200 Subject: [PATCH 0781/1237] Hide users list when logged out --- scripts/system/users.js | 94 +++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index b5fe7c4fad..f23a0ea215 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -372,6 +372,7 @@ var usersWindow = (function () { SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", // +ve x, y values are offset from left, top of screen; -ve from right, bottom. + isLoggedIn = false, isVisible = true, isMinimized = true, isBorderVisible = false, @@ -526,13 +527,13 @@ var usersWindow = (function () { scrollbarBackgroundHeight = numUsersToDisplay * windowLineHeight - windowLineSpacing / 2; Overlays.editOverlay(scrollbarBackground, { height: scrollbarBackgroundHeight, - visible: isUsingScrollbars + visible: isLoggedIn && isUsingScrollbars }); scrollbarBarHeight = Math.max(numUsersToDisplay / linesOfUsers.length * scrollbarBackgroundHeight, SCROLLBAR_BAR_MIN_HEIGHT); Overlays.editOverlay(scrollbarBar, { height: scrollbarBarHeight, - visible: isUsingScrollbars + visible: isLoggedIn && isUsingScrollbars }); } @@ -550,6 +551,41 @@ var usersWindow = (function () { }); } + function updateOverlayVisibility() { + Overlays.editOverlay(windowBorder, { + visible: isLoggedIn && isVisible && isBorderVisible + }); + Overlays.editOverlay(windowPane, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(windowHeading, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(minimizeButton, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(scrollbarBackground, { + visible: isLoggedIn && isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(scrollbarBar, { + visible: isLoggedIn && isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(friendsButton, { + visible: isLoggedIn && isVisible && !isMinimized + }); + displayControl.setVisible(isLoggedIn && isVisible && !isMinimized); + visibilityControl.setVisible(isLoggedIn && isVisible && !isMinimized); + } + + function checkLoggedIn() { + var wasLoggedIn = isLoggedIn; + + isLoggedIn = Account.isLoggedIn(); + if (isLoggedIn !== wasLoggedIn) { + updateOverlayVisibility(); + } + } + function pollUsers() { var url = API_URL; @@ -563,6 +599,8 @@ var usersWindow = (function () { usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); + + checkLoggedIn(); } processUsers = function () { @@ -623,32 +661,6 @@ var usersWindow = (function () { usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; - function updateOverlayVisibility() { - Overlays.editOverlay(windowBorder, { - visible: isVisible && isBorderVisible - }); - Overlays.editOverlay(windowPane, { - visible: isVisible - }); - Overlays.editOverlay(windowHeading, { - visible: isVisible - }); - Overlays.editOverlay(minimizeButton, { - visible: isVisible - }); - Overlays.editOverlay(scrollbarBackground, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(scrollbarBar, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(friendsButton, { - visible: isVisible && !isMinimized - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); - } - function setVisible(visible) { isVisible = visible; @@ -662,7 +674,6 @@ var usersWindow = (function () { } updateOverlayVisibility(); - } function setMinimized(minimized) { @@ -876,8 +887,10 @@ var usersWindow = (function () { if (isMovingWindow) { // Save offset of bottom of window to nearest edge of the window. - offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? windowPosition.x : windowPosition.x - viewport.x; - offset.y = (windowPosition.y < viewport.y / 2) ? windowPosition.y : windowPosition.y - viewport.y; + offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) + ? windowPosition.x : windowPosition.x - viewport.x; + offset.y = (windowPosition.y < viewport.y / 2) + ? windowPosition.y : windowPosition.y - viewport.y; Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); isMovingWindow = false; } @@ -960,7 +973,7 @@ var usersWindow = (function () { radius: WINDOW_BORDER_RADIUS, color: WINDOW_BORDER_COLOR, alpha: WINDOW_BORDER_ALPHA, - visible: isVisible && isBorderVisible + visible: false }); windowPane = Overlays.addOverlay("text", { @@ -976,7 +989,7 @@ var usersWindow = (function () { backgroundAlpha: WINDOW_BACKGROUND_ALPHA, text: "", font: WINDOW_FONT, - visible: isVisible + visible: false }); windowHeading = Overlays.addOverlay("text", { @@ -991,7 +1004,7 @@ var usersWindow = (function () { backgroundAlpha: 0.0, text: "No users online", font: WINDOW_FONT, - visible: isVisible && !isMinimized + visible: false }); minimizeButton = Overlays.addOverlay("image", { @@ -1008,7 +1021,7 @@ var usersWindow = (function () { }, color: MIN_MAX_BUTTON_COLOR, alpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); scrollbarBackgroundPosition = { @@ -1023,7 +1036,7 @@ var usersWindow = (function () { backgroundColor: SCROLLBAR_BACKGROUND_COLOR, backgroundAlpha: SCROLLBAR_BACKGROUND_ALPHA, text: "", - visible: isVisible && isUsingScrollbars && !isMinimized + visible: false }); scrollbarBarPosition = { @@ -1038,7 +1051,7 @@ var usersWindow = (function () { backgroundColor: SCROLLBAR_BAR_COLOR, backgroundAlpha: SCROLLBAR_BAR_ALPHA, text: "", - visible: isVisible && isUsingScrollbars && !isMinimized + visible: false }); friendsButton = Overlays.addOverlay("image", { @@ -1054,7 +1067,8 @@ var usersWindow = (function () { height: FRIENDS_BUTTON_SVG_HEIGHT }, color: FRIENDS_BUTTON_COLOR, - alpha: FRIENDS_BUTTON_ALPHA + alpha: FRIENDS_BUTTON_ALPHA, + visible: false }); showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); @@ -1086,7 +1100,7 @@ var usersWindow = (function () { popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, buttonColor: MIN_MAX_BUTTON_COLOR, buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); @@ -1118,7 +1132,7 @@ var usersWindow = (function () { popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, buttonColor: MIN_MAX_BUTTON_COLOR, buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); Controller.mousePressEvent.connect(onMousePressEvent); From ab52fc5b3cd244e87729aecb116f57ba127de6cb Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Mon, 27 Jun 2016 21:42:32 +0100 Subject: [PATCH 0782/1237] json indentation fix --- interface/resources/controllers/touchscreen.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/controllers/touchscreen.json b/interface/resources/controllers/touchscreen.json index 041259bd36..284a7a3914 100644 --- a/interface/resources/controllers/touchscreen.json +++ b/interface/resources/controllers/touchscreen.json @@ -2,8 +2,8 @@ "name": "Touchscreen to Actions", "channels": [ - { "from": "Touchscreen.GesturePinchOut", "to": "Actions.BoomOut", "filters": [ { "type": "scale", "scale": 0.02 } ]}, - { "from": "Touchscreen.GesturePinchIn", "to": "Actions.BoomIn", "filters": [ { "type": "scale", "scale": 0.02 } ]}, + { "from": "Touchscreen.GesturePinchOut", "to": "Actions.BoomOut", "filters": [ { "type": "scale", "scale": 0.02 } ] }, + { "from": "Touchscreen.GesturePinchIn", "to": "Actions.BoomIn", "filters": [ { "type": "scale", "scale": 0.02 } ] }, { "from": { "makeAxis" : [ [ "Touchscreen.DragLeft" ], @@ -13,7 +13,7 @@ "to": "Actions.Yaw" }, - { "from": { "makeAxis" : [ + { "from": { "makeAxis" : [ [ "Touchscreen.DragUp" ], [ "Touchscreen.DragDown" ] ] From 68be819ea25eecc159d1034869098a2f8e125b91 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 27 Jun 2016 14:07:13 -0700 Subject: [PATCH 0783/1237] fix crash when hmd context menu triggers a modal --- .../resources/qml/menus/MenuMouseHandler.qml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/menus/MenuMouseHandler.qml b/interface/resources/qml/menus/MenuMouseHandler.qml index 9ba158cb28..48574d41e5 100644 --- a/interface/resources/qml/menus/MenuMouseHandler.qml +++ b/interface/resources/qml/menus/MenuMouseHandler.qml @@ -39,6 +39,19 @@ Item { onSelected: d.handleSelection(subMenu, currentItem, item) } } + property var delay: Timer { // No setTimeout in QML. + property var menuItem: null; + interval: 0 + repeat: false + running: false + function trigger(item) { // Capture item and schedule asynchronous Timer. + menuItem = item; + start(); + } + onTriggered: { + menuItem.trigger(); // Now trigger the item. + } + } function toModel(items) { var result = modelMaker.createObject(desktop); @@ -128,7 +141,8 @@ Item { case MenuItemType.Item: console.log("Triggering " + item.text) - item.trigger(); + // Don't block waiting for modal dialogs and such that the menu might open. + delay.trigger(item); clearMenus(); break; } From 19b6a04175bf302ff2056a0db2917d232741bdcd Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Mon, 27 Jun 2016 23:33:12 +0100 Subject: [PATCH 0784/1237] indentation causing some issue is this resolved? Hard to tell. --- interface/resources/controllers/touchscreen.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/interface/resources/controllers/touchscreen.json b/interface/resources/controllers/touchscreen.json index 284a7a3914..1b4e9723ce 100644 --- a/interface/resources/controllers/touchscreen.json +++ b/interface/resources/controllers/touchscreen.json @@ -1,18 +1,17 @@ { "name": "Touchscreen to Actions", "channels": [ - { "from": "Touchscreen.GesturePinchOut", "to": "Actions.BoomOut", "filters": [ { "type": "scale", "scale": 0.02 } ] }, { "from": "Touchscreen.GesturePinchIn", "to": "Actions.BoomIn", "filters": [ { "type": "scale", "scale": 0.02 } ] }, - + { "from": { "makeAxis" : [ - [ "Touchscreen.DragLeft" ], - [ "Touchscreen.DragRight" ] - ] - }, + [ "Touchscreen.DragLeft" ], + [ "Touchscreen.DragRight" ] + ] + }, "to": "Actions.Yaw" }, - + { "from": { "makeAxis" : [ [ "Touchscreen.DragUp" ], [ "Touchscreen.DragDown" ] From 79cfd2e9dc4f67d0bd1167c86f7e9a501e551f66 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 28 Jun 2016 11:40:49 +1200 Subject: [PATCH 0785/1237] Reduce users window flicker when restart script --- scripts/system/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index f23a0ea215..d25e1b76b4 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -599,8 +599,6 @@ var usersWindow = (function () { usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); - - checkLoggedIn(); } processUsers = function () { @@ -646,6 +644,8 @@ var usersWindow = (function () { updateUsersDisplay(); updateOverlayPositions(); + checkLoggedIn(); + } else { print("Error: Request for users status returned " + usersRequest.status + " " + usersRequest.statusText); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. From 197d49fc03ddfff3b06f77638084233306bebc9c Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 27 Jun 2016 17:04:37 -0700 Subject: [PATCH 0786/1237] introducing the depth aware blur --- .../render-utils/src/DebugDeferredBuffer.cpp | 4 +- .../render-utils/src/RenderDeferredTask.cpp | 2 +- .../render-utils/src/SubsurfaceScattering.cpp | 158 ++++++++++++------ .../render-utils/src/SubsurfaceScattering.h | 8 +- .../render-utils/src/SubsurfaceScattering.slh | 89 ++++++++++ .../src/subsurfaceScattering_makeLUT.slf | 79 +-------- .../src/subsurfaceScattering_makeProfile.slf | 20 +++ libraries/render/src/render/BlurTask.cpp | 8 +- libraries/render/src/render/BlurTask.h | 2 +- libraries/render/src/render/BlurTask.slh | 10 +- .../utilities/render/deferredLighting.qml | 5 + .../utilities/render/surfaceGeometryPass.qml | 2 +- 12 files changed, 247 insertions(+), 140 deletions(-) create mode 100644 libraries/render-utils/src/subsurfaceScattering_makeProfile.slf diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 0e6697ae4c..7ae9502889 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -159,7 +159,7 @@ static const std::string DEFAULT_CURVATURE_SHADER{ static const std::string DEFAULT_NORMAL_CURVATURE_SHADER{ "vec4 getFragmentColor() {" //" return vec4(pow(vec3(texture(curvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" - " return vec4(pow(vec3(texture(curvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + " return vec4(vec3(texture(curvatureMap, uv).xyz), 1.0);" //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" " }" }; @@ -175,7 +175,7 @@ static const std::string DEFAULT_DIFFUSED_CURVATURE_SHADER{ static const std::string DEFAULT_DIFFUSED_NORMAL_CURVATURE_SHADER{ "vec4 getFragmentColor() {" //" return vec4(pow(vec3(texture(curvatureMap, uv).a), vec3(1.0 / 2.2)), 1.0);" - " return vec4(pow(vec3(texture(diffusedCurvatureMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" + " return vec4(vec3(texture(diffusedCurvatureMap, uv).xyz), 1.0);" //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" " }" }; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 59a6ab6816..57ff171a87 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -108,7 +108,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto theCurvatureVarying = curvatureFramebufferAndDepth[0]; -#define SIMPLE_BLUR 1 +//#define SIMPLE_BLUR 1 #if SIMPLE_BLUR const auto curvatureFramebuffer = addJob("DiffuseCurvature", curvatureFramebufferAndDepth.get().first); const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth.get().first, true); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index adc6136cd2..9a14a0ae5e 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -17,6 +17,7 @@ #include "DeferredLightingEffect.h" +#include "subsurfaceScattering_makeProfile_frag.h" #include "subsurfaceScattering_makeLUT_frag.h" #include "subsurfaceScattering_drawScattering_frag.h" @@ -85,8 +86,11 @@ bool SubsurfaceScatteringResource::isShowBRDF() const { void SubsurfaceScatteringResource::generateScatteringTable(RenderArgs* args) { + if (!_scatteringProfile) { + _scatteringProfile = generateScatteringProfile(args); + } if (!_scatteringTable) { - _scatteringTable = generatePreIntegratedScattering(args); + _scatteringTable = generatePreIntegratedScattering(_scatteringProfile, args); } } @@ -102,6 +106,7 @@ void SubsurfaceScattering::configure(const Config& config) { glm::vec2 curvatureInfo(config.curvatureOffset, config.curvatureScale); _scatteringResource->setCurvatureFactors(curvatureInfo); + _showProfile = config.showProfile; _showLUT = config.showLUT; } @@ -208,6 +213,7 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, RenderArgs* args = renderContext->args; _scatteringResource->generateScatteringTable(args); + auto scatteringProfile = _scatteringResource->getScatteringProfile(); auto scatteringTable = _scatteringResource->getScatteringTable(); @@ -227,7 +233,7 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const auto theLight = DependencyManager::get()->getLightStage().lights[0]; gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { - batch.enableStereo(false); + // batch.enableStereo(false); batch.setViewportTransform(args->_viewport); @@ -252,9 +258,18 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, batch.draw(gpu::TRIANGLE_STRIP, 4); */ + auto viewportSize = std::min(args->_viewport.z, args->_viewport.w) >> 1; + auto offsetViewport = viewportSize * 0.1; + + if (_showProfile) { + batch.setViewportTransform(glm::ivec4(0, 0, viewportSize, offsetViewport)); + batch.setPipeline(getShowLUTPipeline()); + batch.setResourceTexture(0, scatteringProfile); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + if (_showLUT) { - auto viewportSize = std::min(args->_viewport.z, args->_viewport.w) >> 1; - batch.setViewportTransform(glm::ivec4(0, 0, viewportSize, viewportSize)); + batch.setViewportTransform(glm::ivec4(0, offsetViewport * 1.5, viewportSize, viewportSize)); batch.setPipeline(getShowLUTPipeline()); batch.setResourceTexture(0, scatteringTable); batch.draw(gpu::TRIANGLE_STRIP, 4); @@ -267,7 +282,6 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, // Reference: http://www.altdevblogaday.com/2011/12/31/skin-shading-in-unity3d/ - #include #include #include @@ -276,7 +290,6 @@ void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, using namespace std; - double gaussian(float v, float r) { double g = (1.0 / sqrt(2.0 * _PI * v)) * exp(-(r*r) / (2.0 * v)); return g; @@ -385,45 +398,6 @@ void diffuseScatter(gpu::TexturePointer& lut) { } -void diffuseScatterGPU(gpu::TexturePointer& profileMap, gpu::TexturePointer& lut, RenderArgs* args) { - int width = lut->getWidth(); - int height = lut->getHeight(); - - gpu::PipelinePointer makePipeline; - { - auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_makeLUT_frag)); - gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("profileMap"), 0)); - // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); - gpu::Shader::makeProgram(*program, slotBindings); - - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - makePipeline = gpu::Pipeline::create(program, state); - } - - auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - makeFramebuffer->setRenderBuffer(0, lut); - - gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { - batch.enableStereo(false); - - batch.setViewportTransform(glm::ivec4(0, 0, width, height)); - - batch.setFramebuffer(makeFramebuffer); - batch.setPipeline(makePipeline); - batch.setResourceTexture(0, profileMap); - batch.draw(gpu::TRIANGLE_STRIP, 4); - batch.setResourceTexture(0, nullptr); - batch.setPipeline(nullptr); - batch.setFramebuffer(nullptr); - - }); -} - void diffuseProfile(gpu::TexturePointer& profile) { int width = profile->getWidth(); int height = profile->getHeight(); @@ -459,15 +433,95 @@ void diffuseProfile(gpu::TexturePointer& profile) { profile->assignStoredMip(0, gpu::Element::COLOR_RGBA_32, bytes.size(), bytes.data()); } -gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScattering(RenderArgs* args) { +void diffuseProfileGPU(gpu::TexturePointer& profileMap, RenderArgs* args) { + int width = profileMap->getWidth(); + int height = profileMap->getHeight(); - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 256, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - diffuseProfile(profileMap); + gpu::PipelinePointer makePipeline; + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_makeProfile_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); - const int WIDTH = 128; - const int HEIGHT = 128; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, WIDTH, HEIGHT, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + gpu::Shader::BindingSet slotBindings; + //slotBindings.insert(gpu::Shader::Binding(std::string("profileMap"), 0)); + // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + makePipeline = gpu::Pipeline::create(program, state); + } + + auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + makeFramebuffer->setRenderBuffer(0, profileMap); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + + batch.setViewportTransform(glm::ivec4(0, 0, width, height)); + + batch.setFramebuffer(makeFramebuffer); + batch.setPipeline(makePipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(0, nullptr); + batch.setPipeline(nullptr); + batch.setFramebuffer(nullptr); + }); +} + + +void diffuseScatterGPU(const gpu::TexturePointer& profileMap, gpu::TexturePointer& lut, RenderArgs* args) { + int width = lut->getWidth(); + int height = lut->getHeight(); + + gpu::PipelinePointer makePipeline; + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_makeLUT_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringProfile"), 0)); + // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + makePipeline = gpu::Pipeline::create(program, state); + } + + auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + makeFramebuffer->setRenderBuffer(0, lut); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + + batch.setViewportTransform(glm::ivec4(0, 0, width, height)); + + batch.setFramebuffer(makeFramebuffer); + batch.setPipeline(makePipeline); + batch.setResourceTexture(0, profileMap); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(0, nullptr); + batch.setPipeline(nullptr); + batch.setFramebuffer(nullptr); + + }); +} + +gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(RenderArgs* args) { + const int PROFILE_RESOLUTION = 512; + auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + diffuseProfileGPU(profileMap, args); + return profileMap; +} + +gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScattering(const gpu::TexturePointer& profile, RenderArgs* args) { + + const int TABLE_RESOLUTION = 512; + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); //diffuseScatter(scatteringLUT); - diffuseScatterGPU(profileMap, scatteringLUT, args); + diffuseScatterGPU(profile, scatteringLUT, args); return scatteringLUT; } diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 221cd96722..704f8cd421 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -39,10 +39,12 @@ public: UniformBufferView getParametersBuffer() const { return _parametersBuffer; } + gpu::TexturePointer getScatteringProfile() const { return _scatteringProfile; } gpu::TexturePointer getScatteringTable() const { return _scatteringTable; } void generateScatteringTable(RenderArgs* args); - static gpu::TexturePointer generatePreIntegratedScattering(RenderArgs* args); + static gpu::TexturePointer generateScatteringProfile(RenderArgs* args); + static gpu::TexturePointer generatePreIntegratedScattering(const gpu::TexturePointer& profile, RenderArgs* args); protected: @@ -62,6 +64,7 @@ protected: + gpu::TexturePointer _scatteringProfile; gpu::TexturePointer _scatteringTable; }; @@ -80,6 +83,7 @@ class SubsurfaceScatteringConfig : public render::Job::Config { Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) + Q_PROPERTY(bool showProfile MEMBER showProfile NOTIFY dirty) Q_PROPERTY(bool showLUT MEMBER showLUT NOTIFY dirty) public: SubsurfaceScatteringConfig() : render::Job::Config(true) {} @@ -92,6 +96,7 @@ public: float curvatureOffset{ 0.08f }; float curvatureScale{ 0.8f }; + bool showProfile{ true }; bool showLUT{ true }; signals: @@ -121,6 +126,7 @@ private: gpu::PipelinePointer _showLUTPipeline; gpu::PipelinePointer getShowLUTPipeline(); + bool _showProfile{ false }; bool _showLUT{ false }; }; diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index ac759ab4a2..b0a4596565 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -7,6 +7,95 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@func declareSubsurfaceScatteringProfileSource()@> + +float gaussian(float v, float r) { + const float _PI = 3.14159265358979523846; + return (1.0 / sqrt(2.0 * _PI * v)) * exp(-(r*r) / (2.0 * v)); +} + +vec3 scatter(float r) { + // r is the distance expressed in millimeter + // returns the scatter reflectance + // Values from GPU Gems 3 "Advanced Skin Rendering". + // Originally taken from real life samples. + const vec4 profile[6] = vec4[6]( + vec4(0.0064, 0.233, 0.455, 0.649), + vec4(0.0484, 0.100, 0.336, 0.344), + vec4(0.1870, 0.118, 0.198, 0.000), + vec4(0.5670, 0.113, 0.007, 0.007), + vec4(1.9900, 0.358, 0.004, 0.000), + vec4(7.4100, 0.078, 0.000, 0.000) + ); + const int profileNum = 6; + + vec3 ret = vec3(0.0); + for (int i = 0; i < profileNum; i++) { + float v = profile[i].x * 1.414; + float g = gaussian(v, r); + ret += g * profile[i].yzw; + } + + return ret; +} + +<@endfunc@> + +<@func declareSubsurfaceScatteringGenerateProfileMap()@> +<$declareSubsurfaceScatteringProfileSource()$> + +vec3 generateProfile(vec2 uv) { + return scatter(uv.x * 2.0); +} + +<@endfunc@> + +<@func declareSubsurfaceScatteringProfileMap()@> + +uniform sampler2D scatteringProfile; + +vec3 scatter(float r) { + return texture(scatteringProfile, vec2(r * 0.5, 0.5)).rgb; +} + +<@endfunc@> + +<@func declareSubsurfaceScatteringIntegrate(NumIntegrationSteps)@> + + +vec3 integrate(float cosTheta, float skinRadius) { + // Angle from lighting direction. + float theta = acos(cosTheta); + vec3 totalWeights = vec3(0.0); + vec3 totalLight = vec3(0.0); + + const float _PI = 3.14159265358979523846; + const float step = 2.0 * _PI / <$NumIntegrationSteps$>; + float a = -(_PI); + + + while (a <= (_PI)) { + float sampleAngle = theta + a; + float diffuse = clamp(cos(sampleAngle), 0.0, 1.0); + //if (diffuse < 0.0) diffuse = 0.0; + //if (diffuse > 1.0) diffuse = 1.0; + + // Distance. + float sampleDist = abs(2.0 * skinRadius * sin(a * 0.5)); + + // Profile Weight. + vec3 weights = scatter(sampleDist); + + totalWeights += weights; + totalLight += diffuse * weights; + a += step; + } + + vec3 result = (totalLight / totalWeights); + return clamp(result, vec3(0.0), vec3(1.0)); + +} +<@endfunc@> <@func declareSubsurfaceScatteringResource()@> diff --git a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf index 463365ee36..1cbf599558 100644 --- a/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf +++ b/libraries/render-utils/src/subsurfaceScattering_makeLUT.slf @@ -9,82 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - -const float _PI = 3.14159265358979523846; - - -uniform sampler2D profileMap; -/* -vec3 scatter(float r) { - return texture(profileMap, vec2(2.0 * r, 0.5)).xyz; -} -*/ - -float gaussian(float v, float r) { - return (1.0 / sqrt(2.0 * _PI * v)) * exp(-(r*r) / (2.0 * v)); -} - -vec3 scatter(float r) { - // Values from GPU Gems 3 "Advanced Skin Rendering". - // Originally taken from real life samples. - vec4 profile[6] = vec4[6]( - vec4(0.0064, 0.233, 0.455, 0.649), - vec4(0.0484, 0.100, 0.336, 0.344), - vec4(0.1870, 0.118, 0.198, 0.000), - vec4(0.5670, 0.113, 0.007, 0.007), - vec4(1.9900, 0.358, 0.004, 0.000), - vec4(7.4100, 0.078, 0.000, 0.000) - ); - //const int profileNum = 6; - - - - - vec3 ret = vec3(0.0); - for (int i = 0; i < 6; i++) { - float v = profile[i].x * 1.414; - float g = gaussian(v, r); - ret += g * profile[i].yzw; - } - - return ret; -} - - -vec3 integrate(float cosTheta, float skinRadius) { - // Angle from lighting direction. - float theta = acos(cosTheta); - vec3 totalWeights = vec3(0.0); - vec3 totalLight= vec3(0.0); - - float a = -(_PI); - - float inc = 0.1; - - while (a <= (_PI)) { - float sampleAngle = theta + a; - float diffuse = clamp(cos(sampleAngle), 0.0, 1.0); - //if (diffuse < 0.0) diffuse = 0.0; - //if (diffuse > 1.0) diffuse = 1.0; - - // Distance. - float sampleDist = abs(2.0 * skinRadius * sin(a * 0.5)); - - // Profile Weight. - vec3 weights = scatter( sampleDist); - - totalWeights += weights; - totalLight += diffuse * weights; - a += inc; - } - - vec3 result = (totalLight / totalWeights); - return clamp(result, vec3(0.0), vec3(1.0)); - - return min(sqrt(result), vec3(1.0)); - - return scatter(skinRadius); -} +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringProfileSource()$> +<$declareSubsurfaceScatteringIntegrate(2000)$> in vec2 varTexCoord0; out vec4 outFragColor; diff --git a/libraries/render-utils/src/subsurfaceScattering_makeProfile.slf b/libraries/render-utils/src/subsurfaceScattering_makeProfile.slf new file mode 100644 index 0000000000..ea4a864448 --- /dev/null +++ b/libraries/render-utils/src/subsurfaceScattering_makeProfile.slf @@ -0,0 +1,20 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/27/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringGenerateProfileMap()$> + +in vec2 varTexCoord0; +out vec4 outFragColor; + +void main(void) { + outFragColor = vec4(generateProfile(varTexCoord0.xy), 1.0); +} diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 1d5fe18f4a..d0fd52d7d4 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -263,7 +263,7 @@ gpu::PipelinePointer BlurGaussianDepthAware::getBlurVPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); // Stencil test the curvature pass for objects pixels only, not the background - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); _blurVPipeline = gpu::Pipeline::create(program, state); } @@ -286,7 +286,7 @@ gpu::PipelinePointer BlurGaussianDepthAware::getBlurHPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); // Stencil test the curvature pass for objects pixels only, not the background - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); _blurHPipeline = gpu::Pipeline::create(program, state); } @@ -339,6 +339,10 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const batch.draw(gpu::TRIANGLE_STRIP, 4); batch.setFramebuffer(blurringResources.finalFramebuffer); + if (_inOutResources._generateOutputFramebuffer) { + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + } + batch.setPipeline(blurHPipeline); batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.blurringTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index fe3537e301..b0a3e14e57 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -116,7 +116,7 @@ class BlurGaussianDepthAwareConfig : public BlurGaussianConfig { public: BlurGaussianDepthAwareConfig() : BlurGaussianConfig() {} - float depthThreshold{ 2.0f }; + float depthThreshold{ 1.0f }; signals: void dirty(); protected: diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index 2ed4021967..ece1bdf202 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -62,7 +62,7 @@ vec4 pixelShaderGaussian(vec2 texcoord, vec2 direction, vec2 pixelStep) { vec4 sampleCenter = texture(sourceMap, texcoord); - vec2 finalStep = parameters.filterInfo.x * direction * pixelStep; + vec2 finalStep = getFilterScale() * direction * pixelStep; vec4 srcBlurred = vec4(0.0); for(int i = 0; i < NUM_TAPS; i++) { @@ -99,9 +99,10 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep float filterScale = getFilterScale(); float scale = distanceToProjectionWindow / sampleDepth; - vec2 finalStep = filterScale * scale * direction * pixelStep; + vec2 finalStep = filterScale * scale * direction * pixelStep; // *1000.0; - vec4 srcBlurred = vec4(0.0); + // Accumulate the center sample + vec4 srcBlurred = gaussianDistributionCurve[0] * sampleCenter; for(int i = 1; i < NUM_TAPS; i++) { // Fetch color and depth for current sample. @@ -111,7 +112,8 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep // If the difference in depth is huge, we lerp color back. - float s = clamp(depthThreshold * distanceToProjectionWindow /* * filterScale*/ * abs(srcDepth - sampleDepth), 0.0, 1.0); + float s = clamp(/*depthThreshold */12.0 * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); + // float s = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); srcSample = mix(srcSample, sampleCenter, s); // Accumulate. diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 1c9b10a0c3..ec5fe07a7e 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -57,6 +57,11 @@ Column { min: 0.0 } } + CheckBox { + text: "Scattering Profile" + checked: Render.getConfig("Scattering").showProfile + onCheckedChanged: { Render.getConfig("Scattering").showProfile = checked } + } CheckBox { text: "Scattering Table" checked: Render.getConfig("Scattering").showLUT diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index b163333e60..dca84f6c1f 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -38,7 +38,7 @@ Column { onCheckedChanged: { Render.getConfig("DiffuseCurvature").enabled = checked } } Repeater { - model: [ "Blur Scale:DiffuseCurvature:filterScale:2.0", "Blur Depth Threshold:DiffuseCurvature:depthThreshold:10.0", "Blur Scale2:DiffuseCurvature2:filterScale:2.0", "Blur Depth Threshold 2:DiffuseCurvature2:depthThreshold:10.0"] + model: [ "Blur Scale:DiffuseCurvature:filterScale:2.0", "Blur Depth Threshold:DiffuseCurvature:depthThreshold:1.0", "Blur Scale2:DiffuseCurvature2:filterScale:2.0", "Blur Depth Threshold 2:DiffuseCurvature2:depthThreshold:1.0"] ConfigSlider { label: qsTr(modelData.split(":")[0]) integral: false From cc6dca853c98cd3e6136f2aeaef17ba2e24d4cda Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Tue, 28 Jun 2016 16:47:13 +0100 Subject: [PATCH 0787/1237] change TouchScreenDevice based on CR feedback * device support is based on detection of QTouchDevice::TouchScreen * DPI scale is calculated using the screen that generates the touch event --- .../src/input-plugins/TouchscreenDevice.cpp | 33 +++++++++++++------ .../src/input-plugins/TouchscreenDevice.h | 4 ++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 8e3eea4cf6..10bca75e0a 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -22,33 +23,36 @@ const QString TouchscreenDevice::NAME = "Touchscreen"; +bool TouchscreenDevice::isSupported() const { + for (auto touchDevice : QTouchDevice::devices()) { + if (touchDevice->type() == QTouchDevice::TouchScreen) { + return true; + } + } + return false; +} + void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); }); - // at DPI 100 use these arbitrary values to divide dragging distance - static const float DPI_SCALE_X = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0), 1.0f, 10.0f) - * 600.0f; - static const float DPI_SCALE_Y = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchY() / 100.0), 1.0f, 10.0f) - * 200.0f; - float distanceScaleX, distanceScaleY; if (_touchPointCount == 1) { if (_firstTouchVec.x < _currentTouchVec.x) { - distanceScaleX = (_currentTouchVec.x - _firstTouchVec.x) / DPI_SCALE_X; + distanceScaleX = (_currentTouchVec.x - _firstTouchVec.x) / _screenDPIScale.x; _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_POS).getChannel()] = distanceScaleX; } else if (_firstTouchVec.x > _currentTouchVec.x) { - distanceScaleX = (_firstTouchVec.x - _currentTouchVec.x) / DPI_SCALE_X; + distanceScaleX = (_firstTouchVec.x - _currentTouchVec.x) / _screenDPIScale.x; _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_NEG).getChannel()] = distanceScaleX; } // Y axis is inverted, positive is pointing up the screen if (_firstTouchVec.y > _currentTouchVec.y) { - distanceScaleY = (_firstTouchVec.y - _currentTouchVec.y) / DPI_SCALE_Y; + distanceScaleY = (_firstTouchVec.y - _currentTouchVec.y) / _screenDPIScale.y; _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_POS).getChannel()] = distanceScaleY; } else if (_firstTouchVec.y < _currentTouchVec.y) { - distanceScaleY = (_currentTouchVec.y - _firstTouchVec.y) / DPI_SCALE_Y; + distanceScaleY = (_currentTouchVec.y - _firstTouchVec.y) / _screenDPIScale.y; _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_NEG).getChannel()] = distanceScaleY; } } else if (_touchPointCount == 2) { @@ -72,6 +76,15 @@ void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); KeyboardMouseDevice::enableTouchpad(false); + if (_screenDPI != event->window()->screen()->physicalDotsPerInch()) { + // at DPI 100 use these arbitrary values to divide dragging distance + // the value is clamped from 1 to 10 so up to 1000 DPI would be supported atm + _screenDPIScale.x = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0), 1.0f, 10.0f) + * 600.0f; + _screenDPIScale.y = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchY() / 100.0), 1.0f, 10.0f) + * 200.0f; + _screenDPI = event->window()->screen()->physicalDotsPerInch(); + } } void TouchscreenDevice::touchEndEvent(const QTouchEvent* event) { diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index 53dba19b00..b354b7e9ec 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -36,7 +36,7 @@ public: }; // Plugin functions - virtual bool isSupported() const override { return QTouchDevice::devices().count() > 0; } + virtual bool isSupported() const override; virtual const QString& getName() const override { return NAME; } virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } @@ -73,6 +73,8 @@ public: protected: qreal _lastPinchScale; qreal _scaleFactor; + qreal _screenDPI; + glm::vec2 _screenDPIScale; glm::vec2 _firstTouchVec; glm::vec2 _currentTouchVec; int _touchPointCount; From 4e70e8ed42084e480e8be6cd95220d479a55560a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 09:55:49 -0700 Subject: [PATCH 0788/1237] js call to ray-pick against avatars --- interface/src/avatar/Avatar.cpp | 9 ++++ interface/src/avatar/Avatar.h | 1 + interface/src/avatar/AvatarManager.cpp | 48 ++++++++++++++++++++ interface/src/avatar/AvatarManager.h | 4 ++ libraries/avatars/src/AvatarData.cpp | 23 ++++++++++ libraries/avatars/src/AvatarData.h | 15 ++++++ libraries/script-engine/src/ScriptEngine.cpp | 1 + 7 files changed, 101 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 39bb7eac17..8fd330db19 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1085,6 +1085,15 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { shapeInfo.setOffset(uniformScale * _skeletonModel->getBoundingCapsuleOffset()); } +void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { + ShapeInfo shapeInfo; + computeShapeInfo(shapeInfo); + glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight + start = getPosition() - glm::vec3(0, halfExtents.y, 0); + end = getPosition() + glm::vec3(0, halfExtents.y, 0); + radius = halfExtents.x; +} + void Avatar::setMotionState(AvatarMotionState* motionState) { _motionState = motionState; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 064f0a9533..b9f44613c7 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -154,6 +154,7 @@ public: virtual void rebuildCollisionShape(); virtual void computeShapeInfo(ShapeInfo& shapeInfo); + void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); AvatarMotionState* getMotionState() { return _motionState; } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 68d2eca2c0..f08a75ffda 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -398,3 +398,51 @@ AvatarSharedPointer AvatarManager::getAvatarBySessionID(const QUuid& sessionID) return findAvatar(sessionID); } + +RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& ray, + const QScriptValue& avatarIdsToInclude, + const QScriptValue& avatarIdsToDiscard) { + if (QThread::currentThread() != thread()) { + RayToAvatarIntersectionResult result; + QMetaObject::invokeMethod(const_cast(this), "findRayIntersection", Qt::BlockingQueuedConnection, + Q_ARG(const QScriptValue&, avatarIdsToInclude), + Q_ARG(const QScriptValue&, avatarIdsToDiscard), + Q_RETURN_ARG(RayToAvatarIntersectionResult, result)); + return result; + } + + RayToAvatarIntersectionResult result; + + QVector avatarsToInclude = qVectorEntityItemIDFromScriptValue(avatarIdsToInclude); + QVector avatarsToDiscard = qVectorEntityItemIDFromScriptValue(avatarIdsToDiscard); + + glm::vec3 normDirection = glm::normalize(ray.direction); + + for (auto avatarData : _avatarHash) { + auto avatar = std::static_pointer_cast(avatarData); + if ((avatarsToInclude.size() > 0 && !avatarsToInclude.contains(avatar->getID())) || + (avatarsToDiscard.size() > 0 && avatarsToDiscard.contains(avatar->getID()))) { + continue; + } + + float distance; + + glm::vec3 start; + glm::vec3 end; + float radius; + avatar->getCapsule(start, end, radius); + + bool intersects = findRayCapsuleIntersection(ray.origin, normDirection, start, end, radius, distance); + if (intersects && (!result.intersects || distance < result.distance)) { + result.intersects = true; + result.avatarID = avatar->getID(); + result.distance = distance; + } + } + + if (result.intersects) { + result.intersection = ray.origin + normDirection * result.distance; + } + + return result; +} diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 9be186301d..c49d566aa8 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,6 +70,10 @@ public: void addAvatarToSimulation(Avatar* avatar); + Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersection(const PickRay& ray, + const QScriptValue& avatarIdsToInclude = QScriptValue(), + const QScriptValue& avatarIdsToDiscard = QScriptValue()); + public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } void updateAvatarRenderStatus(bool shouldRenderAvatars); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 709cc76d01..e73702cd95 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "AvatarLogging.h" @@ -1681,3 +1682,25 @@ AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { }); return result; } + +QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& value) { + QScriptValue obj = engine->newObject(); + obj.setProperty("intersects", value.intersects); + QScriptValue avatarIDValue = quuidToScriptValue(engine, value.avatarID); + obj.setProperty("avatarID", avatarIDValue); + obj.setProperty("distance", value.distance); + QScriptValue intersection = vec3toScriptValue(engine, value.intersection); + obj.setProperty("intersection", intersection); + return obj; +} + +void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, RayToAvatarIntersectionResult& value) { + value.intersects = object.property("intersects").toVariant().toBool(); + QScriptValue avatarIDValue = object.property("avatarID"); + quuidFromScriptValue(avatarIDValue, value.avatarID); + value.distance = object.property("distance").toVariant().toFloat(); + QScriptValue intersection = object.property("intersection"); + if (intersection.isValid()) { + vec3FromScriptValue(intersection, value.intersection); + } +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 2dd1079b49..14b4f07471 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -495,4 +495,19 @@ public: void registerAvatarTypes(QScriptEngine* engine); +class RayToAvatarIntersectionResult { +public: +RayToAvatarIntersectionResult() : intersects(false), avatarID(), distance(0) {} + bool intersects; + QUuid avatarID; + float distance; + glm::vec3 intersection; +}; + +Q_DECLARE_METATYPE(RayToAvatarIntersectionResult) + +QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& results); +void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, RayToAvatarIntersectionResult& results); + + #endif // hifi_AvatarData_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index a5e3be8a43..9642aaf588 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -465,6 +465,7 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly); qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue); qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue); + qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); From efdee523fbdb8e266e081de5c91b523697cb9304 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Tue, 28 Jun 2016 17:57:38 +0100 Subject: [PATCH 0789/1237] coding standard fix and renaming --- interface/resources/controllers/touchscreen.json | 2 +- .../src/input-plugins/KeyboardMouseDevice.cpp | 11 +++++------ .../src/input-plugins/KeyboardMouseDevice.h | 4 ++-- .../src/input-plugins/TouchscreenDevice.cpp | 12 ++++++------ .../src/input-plugins/TouchscreenDevice.h | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/interface/resources/controllers/touchscreen.json b/interface/resources/controllers/touchscreen.json index 1b4e9723ce..21362ce8fb 100644 --- a/interface/resources/controllers/touchscreen.json +++ b/interface/resources/controllers/touchscreen.json @@ -12,7 +12,7 @@ "to": "Actions.Yaw" }, - { "from": { "makeAxis" : [ + { "from": { "makeAxis" : [ [ "Touchscreen.DragUp" ], [ "Touchscreen.DragDown" ] ] diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index 6edaddb24a..56894efc4c 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -19,7 +19,7 @@ #include const QString KeyboardMouseDevice::NAME = "Keyboard/Mouse"; -bool KeyboardMouseDevice::_enableTouchpad = true; +bool KeyboardMouseDevice::_enableTouch = true; void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); @@ -133,7 +133,7 @@ glm::vec2 evalAverageTouchPoints(const QList& points) { } void KeyboardMouseDevice::touchBeginEvent(const QTouchEvent* event) { - if (_enableTouchpad) { + if (_enableTouch) { _isTouching = event->touchPointStates().testFlag(Qt::TouchPointPressed); _lastTouch = evalAverageTouchPoints(event->touchPoints()); _lastTouchTime = _clock.now(); @@ -141,7 +141,7 @@ void KeyboardMouseDevice::touchBeginEvent(const QTouchEvent* event) { } void KeyboardMouseDevice::touchEndEvent(const QTouchEvent* event) { - if (_enableTouchpad) { + if (_enableTouch) { _isTouching = false; _lastTouch = evalAverageTouchPoints(event->touchPoints()); _lastTouchTime = _clock.now(); @@ -149,14 +149,13 @@ void KeyboardMouseDevice::touchEndEvent(const QTouchEvent* event) { } void KeyboardMouseDevice::touchUpdateEvent(const QTouchEvent* event) { - if (_enableTouchpad) { + if (_enableTouch) { auto currentPos = evalAverageTouchPoints(event->touchPoints()); _lastTouchTime = _clock.now(); if (!_isTouching) { _isTouching = event->touchPointStates().testFlag(Qt::TouchPointPressed); - } - else { + } else { auto currentMove = currentPos - _lastTouch; _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_X_POS).getChannel()] = (currentMove.x > 0 ? currentMove.x : 0.0f); diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h index d88c410ade..2fdecf0bba 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h @@ -85,7 +85,7 @@ public: void wheelEvent(QWheelEvent* event); - static void enableTouchpad(bool enableTouchpad) { _enableTouchpad = enableTouchpad; } + static void enableTouch(bool enableTouch) { _enableTouch = enableTouch; } static const QString NAME; @@ -125,7 +125,7 @@ protected: std::chrono::high_resolution_clock _clock; std::chrono::high_resolution_clock::time_point _lastTouchTime; - static bool _enableTouchpad; + static bool _enableTouch; }; #endif // hifi_KeyboardMouseDevice_h diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 10bca75e0a..e9553c1785 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -56,12 +56,12 @@ void TouchscreenDevice::pluginUpdate(float deltaTime, const controller::InputCal _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_AXIS_Y_NEG).getChannel()] = distanceScaleY; } } else if (_touchPointCount == 2) { - if (_scaleFactor > _lastPinchScale && _scaleFactor != 0) { + if (_pinchScale > _lastPinchScale && _pinchScale != 0) { _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_POS).getChannel()] = 1.0f; - } else if (_scaleFactor != 0) { + } else if (_pinchScale != 0) { _inputDevice->_axisStateMap[_inputDevice->makeInput(TOUCH_GESTURE_PINCH_NEG).getChannel()] = 1.0f; } - _lastPinchScale = _scaleFactor; + _lastPinchScale = _pinchScale; } } @@ -75,7 +75,7 @@ void TouchscreenDevice::InputDevice::focusOutEvent() { void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); - KeyboardMouseDevice::enableTouchpad(false); + KeyboardMouseDevice::enableTouch(false); if (_screenDPI != event->window()->screen()->physicalDotsPerInch()) { // at DPI 100 use these arbitrary values to divide dragging distance // the value is clamped from 1 to 10 so up to 1000 DPI would be supported atm @@ -89,7 +89,7 @@ void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { void TouchscreenDevice::touchEndEvent(const QTouchEvent* event) { _touchPointCount = 0; - KeyboardMouseDevice::enableTouchpad(true); + KeyboardMouseDevice::enableTouch(true); } void TouchscreenDevice::touchUpdateEvent(const QTouchEvent* event) { @@ -101,7 +101,7 @@ void TouchscreenDevice::touchUpdateEvent(const QTouchEvent* event) { void TouchscreenDevice::touchGestureEvent(const QGestureEvent* event) { if (QGesture* gesture = event->gesture(Qt::PinchGesture)) { QPinchGesture* pinch = static_cast(gesture); - _scaleFactor = pinch->totalScaleFactor(); + _pinchScale = pinch->totalScaleFactor(); } } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index b354b7e9ec..f89f247ce8 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -72,7 +72,7 @@ public: protected: qreal _lastPinchScale; - qreal _scaleFactor; + qreal _pinchScale; qreal _screenDPI; glm::vec2 _screenDPIScale; glm::vec2 _firstTouchVec; From b36a39b2bcda94de87fdc831da884611f8a85eea Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 28 Jun 2016 09:59:59 -0700 Subject: [PATCH 0790/1237] Add version to launch user activity --- interface/src/Application.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1a0a11ccc3..1d8b3ae9d9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -773,6 +773,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : auto gpuIdent = GPUIdent::getInstance(); auto glContextData = getGLContextData(); QJsonObject properties = { + { "version", applicationVersion() }, { "previousSessionCrashed", _previousSessionCrashed }, { "previousSessionRuntime", sessionRunTime.get() }, { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, From 18a1f74c3752bd35a6d93f5135027453c0a9a671 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 28 Jun 2016 10:32:43 -0700 Subject: [PATCH 0791/1237] remove unused boolean for signalling protocol mismatch --- libraries/networking/src/DomainHandler.cpp | 1 - libraries/networking/src/DomainHandler.h | 1 - 2 files changed, 2 deletions(-) diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index c4b979e0b8..739c0f8f4a 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -117,7 +117,6 @@ void DomainHandler::hardReset() { _hostname = QString(); _sockAddr.clear(); - _hasSignalledProtocolMismatch = false; _domainConnectionRefusals.clear(); _hasCheckedForAccessToken = false; diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 0c041da47d..50639a4817 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -145,7 +145,6 @@ private: QTimer _settingsTimer; QSet _domainConnectionRefusals; - bool _hasSignalledProtocolMismatch { false }; bool _hasCheckedForAccessToken { false }; int _connectionDenialsSinceKeypairRegen { 0 }; From 8a0d58a0c2410f0fc43f69b4842591ae7143bc87 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 11:15:01 -0700 Subject: [PATCH 0792/1237] fix invokeMethod in AvatarManager::findRayIntersection --- interface/src/avatar/AvatarManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index f08a75ffda..c5a9efeecc 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -405,6 +405,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& if (QThread::currentThread() != thread()) { RayToAvatarIntersectionResult result; QMetaObject::invokeMethod(const_cast(this), "findRayIntersection", Qt::BlockingQueuedConnection, + Q_ARG(const PickRay&, ray), Q_ARG(const QScriptValue&, avatarIdsToInclude), Q_ARG(const QScriptValue&, avatarIdsToDiscard), Q_RETURN_ARG(RayToAvatarIntersectionResult, result)); From 0f9f4749e7b849e85246639245e8271679dd7de5 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 11:18:46 -0700 Subject: [PATCH 0793/1237] fix invokeMethod in AvatarManager::findRayIntersection --- interface/src/avatar/AvatarManager.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c5a9efeecc..0eba27ee78 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -402,18 +402,16 @@ AvatarSharedPointer AvatarManager::getAvatarBySessionID(const QUuid& sessionID) RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& ray, const QScriptValue& avatarIdsToInclude, const QScriptValue& avatarIdsToDiscard) { + RayToAvatarIntersectionResult result; if (QThread::currentThread() != thread()) { - RayToAvatarIntersectionResult result; QMetaObject::invokeMethod(const_cast(this), "findRayIntersection", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(RayToAvatarIntersectionResult, result), Q_ARG(const PickRay&, ray), Q_ARG(const QScriptValue&, avatarIdsToInclude), - Q_ARG(const QScriptValue&, avatarIdsToDiscard), - Q_RETURN_ARG(RayToAvatarIntersectionResult, result)); + Q_ARG(const QScriptValue&, avatarIdsToDiscard)); return result; } - RayToAvatarIntersectionResult result; - QVector avatarsToInclude = qVectorEntityItemIDFromScriptValue(avatarIdsToInclude); QVector avatarsToDiscard = qVectorEntityItemIDFromScriptValue(avatarIdsToDiscard); From 799686f29d46eb53c96742a53cea1bf9b4a7eea4 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 28 Jun 2016 11:36:08 -0700 Subject: [PATCH 0794/1237] Only update the (invisible) cursor position from hand controllers when the trigger is partially squeezed. --- .../controllers/handControllerPointer.js | 71 ++++++++++++------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index a0f1f47b3c..981600f0bd 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -187,6 +187,32 @@ function overlayFromWorldPoint(point) { return { x: horizontalPixels, y: verticalPixels }; } +function activeHudPoint2d(activeHand) { // if controller is valid, update reticle position and answer 2d point. Otherwise falsey. + var controllerPose = Controller.getPoseValue(activeHand); + // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) + if (!controllerPose.valid) { + return; // Controller is cradled. + } + var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), + MyAvatar.position); + // This gets point direction right, but if you want general quaternion it would be more complicated: + var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + + var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); + if (!hudPoint3d) { + if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here + print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. + } + return; + } + var hudPoint2d = overlayFromWorldPoint(hudPoint3d); + + // We don't know yet if we'll want to make the cursor or laser visble, but we need to move it to see if + // it's pointing at a QML tool (aka system overlay). + setReticlePosition(hudPoint2d); + return hudPoint2d; +} + // MOUSE ACTIVITY -------- // var isSeeking = false; @@ -351,8 +377,23 @@ function isPointingAtOverlayStartedNonFullTrigger(trigger) { } clickMapping.from(rightTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(rightTrigger)).to(Controller.Actions.ReticleClick); clickMapping.from(leftTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(leftTrigger)).to(Controller.Actions.ReticleClick); -clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); -clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); +// The following is essentially like Left and Right versions of +// clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); +// except that we first update the reticle position from the appropriate hand position, before invoking the ContextMenu. +var wantsMenu = 0; +clickMapping.from(function () { return wantsMenu; }).to(Controller.Actions.ContextMenu); +clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(function (clicked) { + if (clicked) { + activeHudPoint2d(Controller.Standard.RightHand); + } + wantsMenu = clicked; +}); +clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(function (clicked) { + if (clicked) { + activeHudPoint2d(Controller.Standard.LeftHand); + } + wantsMenu = clicked; +}); clickMapping.from(Controller.Hardware.Keyboard.RightMouseClicked).peek().to(function () { // Allow the reticle depth to be set correctly: // Wait a tick for the context menu to be displayed, and then simulate a (non-hand-controller) mouse move @@ -408,28 +449,13 @@ function update() { } leftTrigger.update(); rightTrigger.update(); - var controllerPose = Controller.getPoseValue(activeHand); - // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) - if (!controllerPose.valid) { - return off(); // Controller is cradled. + if (!activeTrigger.state) { + return off(); // No trigger } - var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), - MyAvatar.position); - // This gets point direction right, but if you want general quaternion it would be more complicated: - var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); - - var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); - if (!hudPoint3d) { - if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here - print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. - } + var hudPoint2d = activeHudPoint2d(activeHand); + if (!hudPoint2d) { return off(); } - var hudPoint2d = overlayFromWorldPoint(hudPoint3d); - - // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if - // it's pointing at a QML tool (aka system overlay). - setReticlePosition(hudPoint2d); // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { if (HMD.active) { @@ -446,9 +472,6 @@ function update() { return; } // We are not pointing at a HUD element (but it could be a 3d overlay). - if (!activeTrigger.state) { - return off(); // No trigger - } clearSystemLaser(); Reticle.visible = false; } From c62967a57bffd04e92bc0975180f06957b3de21b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 28 Jun 2016 12:38:00 -0700 Subject: [PATCH 0795/1237] add secs per hour --- libraries/shared/src/NumericalConstants.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/shared/src/NumericalConstants.h b/libraries/shared/src/NumericalConstants.h index ca18d8ad5e..d37e1e31c5 100644 --- a/libraries/shared/src/NumericalConstants.h +++ b/libraries/shared/src/NumericalConstants.h @@ -39,6 +39,9 @@ const quint64 NSECS_PER_MSEC = 1000000; const quint64 USECS_PER_MSEC = 1000; const quint64 MSECS_PER_SECOND = 1000; const quint64 USECS_PER_SECOND = USECS_PER_MSEC * MSECS_PER_SECOND; +const quint64 SECS_PER_MINUTE = 60; +const quint64 MINS_PER_HOUR = 60; +const quint64 SECS_PER_HOUR = SECS_PER_MINUTE * MINS_PER_HOUR; const int BITS_IN_BYTE = 8; const int BYTES_PER_KILOBYTE = 1000; From 56038a97a61fd9e70c09b752168ea24d272edec6 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 28 Jun 2016 12:38:46 -0700 Subject: [PATCH 0796/1237] add domain settings getter for descriptors, validates nested vals --- .../src/DomainServerSettingsManager.cpp | 62 ++++++++++++++----- .../src/DomainServerSettingsManager.h | 4 ++ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 543e61f485..262cc9d9ee 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "DomainServerSettingsManager.h" @@ -263,23 +264,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList if (oldVersion < 1.5) { // This was prior to operating hours, so add default hours - static const QString WEEKDAY_HOURS{ "descriptors.weekday_hours" }; - static const QString WEEKEND_HOURS{ "descriptors.weekend_hours" }; - static const QString UTC_OFFSET{ "descriptors.utc_offset" }; - - QVariant* weekdayHours = valueForKeyPath(_configMap.getUserConfig(), WEEKDAY_HOURS, true); - QVariant* weekendHours = valueForKeyPath(_configMap.getUserConfig(), WEEKEND_HOURS, true); - QVariant* utcOffset = valueForKeyPath(_configMap.getUserConfig(), UTC_OFFSET, true); - - *weekdayHours = QVariantList { QVariantMap{ { "open", QVariant("00:00") }, { "close", QVariant("23:59") } } }; - *weekendHours = QVariantList { QVariantMap{ { "open", QVariant("00:00") }, { "close", QVariant("23:59") } } }; - *utcOffset = QVariant(QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime()) / (float)3600); - - // write the new settings to file - persistToFile(); - - // reload the master and user config so the merged config is correct - _configMap.loadMasterAndUserConfig(_argumentList); + validateDescriptorsMap(); } } @@ -289,6 +274,49 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion); } +QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { + validateDescriptorsMap(); + + static const QString DESCRIPTORS{ "descriptors" }; + return *static_cast(getSettingsMap()[DESCRIPTORS].data()); +} + +void DomainServerSettingsManager::validateDescriptorsMap() { + static const QString WEEKDAY_HOURS{ "descriptors.weekday_hours" }; + static const QString WEEKEND_HOURS{ "descriptors.weekend_hours" }; + static const QString UTC_OFFSET{ "descriptors.utc_offset" }; + + QVariant* weekdayHours = valueForKeyPath(_configMap.getUserConfig(), WEEKDAY_HOURS, true); + QVariant* weekendHours = valueForKeyPath(_configMap.getUserConfig(), WEEKEND_HOURS, true); + QVariant* utcOffset = valueForKeyPath(_configMap.getUserConfig(), UTC_OFFSET, true); + + static const QString OPEN{ "open" }; + static const QString CLOSE{ "close" }; + static const QString DEFAULT_OPEN{ "00:00" }; + static const QString DEFAULT_CLOSE{ "23:59" }; + bool wasMalformed = false; + if (weekdayHours->isNull()) { + *weekdayHours = QVariantList{ QVariantMap{ { OPEN, QVariant(DEFAULT_OPEN) }, { CLOSE, QVariant(DEFAULT_CLOSE) } } }; + wasMalformed = true; + } + if (weekendHours->isNull()) { + *weekendHours = QVariantList{ QVariantMap{ { OPEN, QVariant(DEFAULT_OPEN) }, { CLOSE, QVariant(DEFAULT_CLOSE) } } }; + wasMalformed = true; + } + if (utcOffset->isNull()) { + *utcOffset = QVariant(QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime()) / (float)SECS_PER_HOUR); + wasMalformed = true; + } + + if (wasMalformed) { + // write the new settings to file + persistToFile(); + + // reload the master and user config so the merged config is correct + _configMap.loadMasterAndUserConfig(_argumentList); + } +} + void DomainServerSettingsManager::packPermissionsForMap(QString mapName, NodePermissionsMap& agentPermissions, QString keyPath) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index ec1d3b637d..66f1a83500 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -41,6 +41,8 @@ public: QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } + QVariantMap& getDescriptorsMap(); + bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); } bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); } NodePermissions getStandardPermissionsForName(const QString& name) const; @@ -72,6 +74,8 @@ private: friend class DomainServer; + void validateDescriptorsMap(); + void packPermissionsForMap(QString mapName, NodePermissionsMap& agentPermissions, QString keyPath); void packPermissions(); void unpackPermissions(); From c406538301a4a30c8937a2475ae1c144b76a4cbe Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 28 Jun 2016 12:40:25 -0700 Subject: [PATCH 0797/1237] properly parse domain metadata hours --- domain-server/src/DomainMetadata.cpp | 93 ++++++++++++++++------------ 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index c5048ea9d8..6a17bff4c0 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -60,6 +60,50 @@ const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; // // it is meant to be sent to and consumed by an external API +// merge delta into target +// target should be of the form [ OpenTime, CloseTime ], +// delta should be of the form [ { open: Time, close: Time } ] +void parseHours(QVariant delta, QVariant& target) { + using Hours = DomainMetadata::Descriptors::Hours; + + assert(target.canConvert()); + auto& targetList = *static_cast(target.data()); + + // if/when multiple ranges are allowed, this list will need to be iterated + assert(targetList[0].canConvert()); + auto& hours = *static_cast(targetList[0].data()); + + if (!delta.canConvert()) { + return; + } + + auto& deltaList = *static_cast(delta.data()); + if (deltaList.isEmpty()) { + return; + } + + auto& deltaHours = *static_cast(deltaList.first().data()); + if (deltaHours.isEmpty()) { + return; + } + + // merge delta into base + static const int OPEN_INDEX = 0; + static const int CLOSE_INDEX = 1; + auto open = deltaHours.find(Hours::OPEN); + if (open != deltaHours.end()) { + hours[OPEN_INDEX] = open.value(); + } + assert(hours[OPEN_INDEX].canConvert()); + + auto close = deltaHours.find(Hours::CLOSE); + if (close != deltaHours.end()) { + hours[CLOSE_INDEX] = close.value(); + } + assert(hours[CLOSE_INDEX].canConvert()); + +} + DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { // set up the structure necessary for casting during parsing (see parseHours, esp.) _metadata[USERS] = QVariantMap {}; @@ -100,45 +144,12 @@ QJsonObject DomainMetadata::get(const QString& group) { return QJsonObject::fromVariantMap(_metadata[group].toMap()); } -// merge delta into target -// target should be of the form [ OpenTime, CloseTime ], -// delta should be of the form [ { open: Time, close: Time } ] -void parseHours(QVariant delta, QVariant& target) { - using Hours = DomainMetadata::Descriptors::Hours; - - assert(target.canConvert()); - auto& targetList = *static_cast(target.data()); - - // if/when multiple ranges are allowed, this list will need to be iterated - assert(targetList[0].canConvert()); - auto& hours = *static_cast(targetList[0].data()); - - auto deltaHours = delta.toList()[0].toMap(); - if (deltaHours.isEmpty()) { - return; - } - - // merge delta into base - static const int OPEN_INDEX = 0; - static const int CLOSE_INDEX = 1; - auto open = deltaHours.find(Hours::OPEN); - if (open != deltaHours.end()) { - hours[OPEN_INDEX] = open.value(); - } - assert(hours[OPEN_INDEX].canConvert()); - auto close = deltaHours.find(Hours::CLOSE); - if (close != deltaHours.end()) { - hours[CLOSE_INDEX] = close.value(); - } - assert(hours[CLOSE_INDEX].canConvert()); -} - void DomainMetadata::descriptorsChanged() { // get descriptors assert(_metadata[DESCRIPTORS].canConvert()); auto& state = *static_cast(_metadata[DESCRIPTORS].data()); - auto settings = static_cast(parent())->_settingsManager.getSettingsMap(); - auto descriptors = settings[DESCRIPTORS].toMap(); + auto& settings = static_cast(parent())->_settingsManager.getSettingsMap(); + auto& descriptors = static_cast(parent())->_settingsManager.getDescriptorsMap(); // copy simple descriptors (description/maturity) state[Descriptors::DESCRIPTION] = descriptors[Descriptors::DESCRIPTION]; @@ -149,20 +160,20 @@ void DomainMetadata::descriptorsChanged() { state[Descriptors::TAGS] = descriptors[Descriptors::TAGS].toList(); // parse capacity - const QString CAPACITY = "security.maximum_user_capacity"; + static const QString CAPACITY = "security.maximum_user_capacity"; const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; state[Descriptors::CAPACITY] = capacity; // parse operating hours - const QString WEEKDAY_HOURS = "weekday_hours"; - const QString WEEKEND_HOURS = "weekend_hours"; - const QString UTC_OFFSET = "utc_offset"; + static const QString WEEKDAY_HOURS = "weekday_hours"; + static const QString WEEKEND_HOURS = "weekend_hours"; + static const QString UTC_OFFSET = "utc_offset"; assert(state[Descriptors::HOURS].canConvert()); auto& hours = *static_cast(state[Descriptors::HOURS].data()); - parseHours(descriptors.take(WEEKDAY_HOURS), hours[Descriptors::Hours::WEEKDAY]); - parseHours(descriptors.take(WEEKEND_HOURS), hours[Descriptors::Hours::WEEKEND]); hours[Descriptors::Hours::UTC_OFFSET] = descriptors.take(UTC_OFFSET); + parseHours(descriptors[WEEKDAY_HOURS], hours[Descriptors::Hours::WEEKDAY]); + parseHours(descriptors[WEEKEND_HOURS], hours[Descriptors::Hours::WEEKEND]); #if DEV_BUILD || PR_BUILD qDebug() << "Domain metadata descriptors set:" << QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); From c492d125e080a018d20b56b322a43a523ecb9e2e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 28 Jun 2016 13:19:03 -0700 Subject: [PATCH 0798/1237] limit hand controller actions to recommended area --- scripts/system/controllers/handControllerPointer.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index a0f1f47b3c..8e2edbe467 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -125,8 +125,18 @@ function ignoreMouseActivity() { weMovedReticle = false; return true; } +var reticleMinX, reticleMaxX, reticleMinY, reticleMaxY; +function updateRecommendedArea() { + var rectangle = Controller.getRecommendedOverlayRect(); + reticleMinX = rectangle.x; + reticleMaxX = rectangle.x + rectangle.width; + reticleMinY = rectangle.y; + reticleMaxY = rectangle.y + rectangle.height; +} var setReticlePosition = function (point2d) { weMovedReticle = true; + point2d.x = Math.max(reticleMinX, Math.min(point2d.x, reticleMaxX)); + point2d.y = Math.max(reticleMinY, Math.min(point2d.y, reticleMaxY)); Reticle.setPosition(point2d); }; @@ -463,6 +473,7 @@ Script.scriptEnding.connect(function () { var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds function checkSettings() { updateFieldOfView(); + updateRecommendedArea(); } checkSettings(); var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); From d128af48db49f07ef5575b2d93a11ea9e6224822 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 28 Jun 2016 13:47:55 -0700 Subject: [PATCH 0799/1237] Fix the curvature issue in stereo by evaluating in the mono clip space, not stereo. INtegrate the scattering in simple ambient without IBL --- .../src/DeferredFrameTransform.cpp | 7 +- .../render-utils/src/DeferredFrameTransform.h | 2 + .../render-utils/src/DeferredGlobalLight.slh | 103 +++++++++++++++++- .../src/DeferredLightingEffect.cpp | 2 + .../render-utils/src/DeferredLightingEffect.h | 8 +- .../render-utils/src/DeferredTransform.slh | 4 + .../render-utils/src/SubsurfaceScattering.cpp | 17 +++ .../render-utils/src/SubsurfaceScattering.h | 16 ++- .../render-utils/src/SubsurfaceScattering.slh | 11 ++ .../render-utils/src/SurfaceGeometryPass.h | 2 +- .../src/directional_ambient_light.slf | 18 +++ .../src/surfaceGeometry_makeCurvature.slf | 45 ++++++-- libraries/render/src/render/BlurTask.h | 2 +- libraries/render/src/render/BlurTask.slh | 4 +- .../utilities/render/debugDeferredLighting.js | 4 +- .../render/debugSurfaceGeometryPass.js | 4 +- .../utilities/render/deferredLighting.qml | 10 ++ 17 files changed, 231 insertions(+), 28 deletions(-) diff --git a/libraries/render-utils/src/DeferredFrameTransform.cpp b/libraries/render-utils/src/DeferredFrameTransform.cpp index c5345e24f9..2f9d7b016a 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.cpp +++ b/libraries/render-utils/src/DeferredFrameTransform.cpp @@ -36,13 +36,12 @@ void DeferredFrameTransform::update(RenderArgs* args) { cameraTransform.getMatrix(frameTransformBuffer.invView); cameraTransform.getInverseMatrix(frameTransformBuffer.view); + args->getViewFrustum().evalProjectionMatrix(frameTransformBuffer.projectionMono); + // Running in stero ? bool isStereo = args->_context->isStereo(); if (!isStereo) { - // Eval the mono projection - mat4 monoProjMat; - args->getViewFrustum().evalProjectionMatrix(monoProjMat); - frameTransformBuffer.projection[0] = monoProjMat; + frameTransformBuffer.projection[0] = frameTransformBuffer.projectionMono; frameTransformBuffer.stereoInfo = glm::vec4(0.0f, (float)args->_viewport.z, 0.0f, 0.0f); frameTransformBuffer.invpixelInfo = glm::vec4(1.0f / args->_viewport.z, 1.0f / args->_viewport.w, 0.0f, 0.0f); } else { diff --git a/libraries/render-utils/src/DeferredFrameTransform.h b/libraries/render-utils/src/DeferredFrameTransform.h index 82bc989028..70ac5fca92 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.h +++ b/libraries/render-utils/src/DeferredFrameTransform.h @@ -45,6 +45,8 @@ protected: glm::vec4 stereoInfo{ 0.0 }; // Mono proj matrix or Left and Right proj matrix going from Mono Eye space to side clip space glm::mat4 projection[2]; + // THe mono projection for sure + glm::mat4 projectionMono; // Inv View matrix from eye space (mono) to world space glm::mat4 invView; // View matrix from world space to eye space (mono) diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index a5396958ea..0a75760fb0 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -110,6 +110,99 @@ vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, floa } <@endfunc@> +<@func declareEvalAmbientSphereGlobalColorScattering()@> + +<$declareDeferredCurvature()$> +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringResource()$> +!> + +vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { + // prepareGlobalLight + + // Transform directions to worldspace + vec3 fragNormal = vec3((normal)); + vec3 fragEyeVector = vec3(invViewMat * vec4(-position, 0.0)); + vec3 fragEyeDir = normalize(fragEyeVector); + + // Get light + Light light = getLight(); + vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin + float metallic = 0.0; + + vec3 fragLightDir = -normalize(getLightDirection(light)); + + vec3 bentNormalHigh = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 bentNormalLow = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + float curvature = unpackCurvature(diffusedCurvature.w); + + if (showDiffusedNormal()) { + return diffusedCurvature.xyz; + return bentNormalLow * 0.5 + vec3(0.5); + } + if (showCurvature()) { + float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); + return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); + } + vec3 rS = bentNormalHigh; + + vec3 bendFactorSpectrum = getBendFactor(); + // vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); + vec3 rN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.x)); + vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); + vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); + + vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); + + //return 0.5 * (NdotLSpectrum + vec3(1.0)); + // --> Look up the pre-integrated curvature-dependent BDRF textures + vec3 brdf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + + // The position of the pixel fragment in Eye space then in world space + + float scatteringLevel = getScatteringLevel(); + + vec4 shading; + float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + { // Key Sun Lighting + // Diffuse Lighting + //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + float power = specularDistribution(roughness, fragNormal, halfDir); + vec3 specular = power * fresnelColor * standardDiffuse; + + shading = vec4(specular, (1 - fresnelColor.x)); + } + + if (scatteringLevel < 0.1) { + brdf = vec3(standardDiffuse); + } + + vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); + + + // Diffuse from ambient + // color += albedo * evalSphericalLight(getLightAmbientSphere(light), bentNormalHigh).xyz *getLightAmbientIntensity(light); + + // Specular highlight from ambient + vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, 1.0); + // color += specularLighting; + + if (showBRDF()) + return brdf; + + //vec3 debugNdotL = 0.5 * (NdotLSpectrum + vec3(1.0)); + //return vec3(debugNdotL.z, curvature, 0.0 ); + + return vec3(color); +} +<@endfunc@> + + + <@func declareEvalSkyboxGlobalColor()@> <$declareEvalGlobalSpecularIrradiance(0, 1, 0)$> @@ -169,7 +262,7 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f // Get light Light light = getLight(); - vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin float metallic = 0.0; vec3 fragLightDir = -normalize(getLightDirection(light)); @@ -178,6 +271,14 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f vec3 bentNormalLow = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); float curvature = unpackCurvature(diffusedCurvature.w); + if (showDiffusedNormal()) { + return diffusedCurvature.xyz; + return bentNormalLow * 0.5 + vec3(0.5); + } + if (showCurvature()) { + float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); + return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); + } vec3 rS = bentNormalHigh; vec3 bendFactorSpectrum = getBendFactor(); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index ff89a3fc46..217c6f726c 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -626,6 +626,8 @@ void RenderDeferred::configure(const Config& config) { _subsurfaceScatteringResource->setLevel((float)config.enableScattering); _subsurfaceScatteringResource->setShowBRDF(config.showScatteringBRDF); + _subsurfaceScatteringResource->setShowCurvature(config.showCurvature); + _subsurfaceScatteringResource->setShowDiffusedNormal(config.showDiffusedNormal); _enablePointLights = config.enablePointLights; _enableSpotLights = config.enableSpotLights; diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 85b9038046..4c64fdf93e 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -148,6 +148,8 @@ class RenderDeferredConfig : public render::Job::Config { Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) Q_PROPERTY(bool showScatteringBRDF MEMBER showScatteringBRDF NOTIFY dirty) + Q_PROPERTY(bool showCurvature MEMBER showCurvature NOTIFY dirty) + Q_PROPERTY(bool showDiffusedNormal MEMBER showDiffusedNormal NOTIFY dirty) Q_PROPERTY(bool enablePointLights MEMBER enablePointLights NOTIFY dirty) Q_PROPERTY(bool enableSpotLights MEMBER enableSpotLights NOTIFY dirty) @@ -162,10 +164,12 @@ public: float bentScale{ 1.5f }; float curvatureOffset{ 0.08f }; - float curvatureScale{ 0.8f }; + float curvatureScale{ 0.9f }; bool enableScattering{ true }; - bool showScatteringBRDF{ true }; + bool showScatteringBRDF{ false }; + bool showCurvature{ false }; + bool showDiffusedNormal{ false }; bool enablePointLights{ true }; bool enableSpotLights{ true }; diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 520ac77fde..814a59a407 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -19,6 +19,7 @@ struct DeferredFrameTransform { vec4 _depthInfo; vec4 _stereoInfo; mat4 _projection[2]; + mat4 _projectionMono; mat4 _viewInverse; mat4 _view; }; @@ -49,6 +50,9 @@ float getProjScale(int resolutionLevel) { mat4 getProjection(int side) { return frameTransform._projection[side]; } +mat4 getProjectionMono() { + return frameTransform._projectionMono; +} // positive near distance of the projection float getProjectionNear() { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 9a14a0ae5e..542f12b33e 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -84,6 +84,23 @@ bool SubsurfaceScatteringResource::isShowBRDF() const { return (bool)_parametersBuffer.get().showBRDF; } +void SubsurfaceScatteringResource::setShowCurvature(bool show) { + if (show != isShowCurvature()) { + _parametersBuffer.edit().showCurvature = show; + } +} +bool SubsurfaceScatteringResource::isShowCurvature() const { + return (bool)_parametersBuffer.get().showCurvature; +} + +void SubsurfaceScatteringResource::setShowDiffusedNormal(bool show) { + if (show != isShowDiffusedNormal()) { + _parametersBuffer.edit().showDiffusedNormal = show; + } +} +bool SubsurfaceScatteringResource::isShowDiffusedNormal() const { + return (bool)_parametersBuffer.get().showDiffusedNormal; +} void SubsurfaceScatteringResource::generateScatteringTable(RenderArgs* args) { if (!_scatteringProfile) { diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 704f8cd421..b4ad511bf3 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -35,7 +35,10 @@ public: void setShowBRDF(bool show); bool isShowBRDF() const; - + void setShowCurvature(bool show); + bool isShowCurvature() const; + void setShowDiffusedNormal(bool show); + bool isShowDiffusedNormal() const; UniformBufferView getParametersBuffer() const { return _parametersBuffer; } @@ -56,7 +59,12 @@ protected: glm::vec4 normalBentInfo{ 1.5f, 0.8f, 0.3f, 1.5f }; glm::vec2 curvatureInfo{ 0.08f, 0.8f }; float level{ 1.0f }; - float showBRDF{ 1.0f }; + float showBRDF{ 0.0f }; + float showCurvature{ 0.0f }; + float showDiffusedNormal{ 0.0f }; + float spare1{ 0.0f }; + float spare2{ 0.0f }; + Parameters() {} }; @@ -96,8 +104,8 @@ public: float curvatureOffset{ 0.08f }; float curvatureScale{ 0.8f }; - bool showProfile{ true }; - bool showLUT{ true }; + bool showProfile{ false }; + bool showLUT{ false }; signals: void dirty(); diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index b0a4596565..4aaaaff265 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -117,6 +117,7 @@ vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { struct ScatteringParameters { vec4 normalBendInfo; // R, G, B, factor vec4 curvatureInfo;// Offset, Scale, level + vec4 debugFlags; }; uniform subsurfaceScatteringParametersBuffer { @@ -135,6 +136,16 @@ bool showBRDF() { return parameters.curvatureInfo.w > 0.0; } +bool showCurvature() { + return parameters.debugFlags.x > 0.0; +} +bool showDiffusedNormal() { + return parameters.debugFlags.y > 0.0; +} +float unpackCurvatureSigned(float packedCurvature) { + return (packedCurvature * 2 - 1) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; +} + float unpackCurvature(float packedCurvature) { return abs(packedCurvature * 2 - 1) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 697c93b745..834b404429 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -26,7 +26,7 @@ class SurfaceGeometryPassConfig : public render::Job::Config { public: SurfaceGeometryPassConfig() : render::Job::Config(true) {} - float depthThreshold{ 0.033f }; + float depthThreshold{ 0.02f }; // meters float basisScale{ 1.0f }; float curvatureScale{ 10.0f }; diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index b1142ffe9e..932f798a73 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -17,6 +17,8 @@ <$declareEvalLightmappedColor()$> <$declareEvalAmbientSphereGlobalColor()$> +<$declareEvalAmbientSphereGlobalColorScattering()$> + in vec2 _texCoord0; out vec4 _fragColor; @@ -38,6 +40,22 @@ void main(void) { frag.diffuse, frag.specularVal.xyz); _fragColor = vec4(color, 1.0); + } else if (frag.mode == FRAG_MODE_SCATTERING) { + + vec4 blurredCurvature = fetchCurvature(_texCoord0); + vec4 diffusedCurvature = fetchDiffusedCurvature(_texCoord0); + + vec3 color = evalAmbientSphereGlobalColorScattering( + getViewInverse(), + shadowAttenuation, + frag.obscurance, + frag.position.xyz, + frag.normal, + frag.diffuse, + blurredCurvature, + diffusedCurvature, + frag.roughness); + _fragColor = vec4(color, 1.0); } else { vec3 color = evalAmbientSphereGlobalColor( getViewInverse(), diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index f896cb657d..6c19982c2c 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -112,6 +112,14 @@ void main(void) { // The position of the pixel fragment in Eye space then in world space vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); + vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; + + if (texcoordPos.y > 0.5) { + outFragColor = vec4(fract(10.0 * worldPos.xyz), 1.0); + } else { + outFragColor = vec4(fract(10.0 * eyePos.xyz), 1.0); + } + // return; // Calculate the perspective scale. // Clamp to 0.5 @@ -142,14 +150,14 @@ void main(void) { vec4 py = vec4(eyePos + ay, 0.0); vec4 pz = vec4(eyePos + az, 0.0); - /* + if (texcoordPos.y > 0.5) { outFragColor = vec4(fract(px.xyz), 1.0); } else { outFragColor = vec4(fract(eyePos.xyz), 1.0); } - return; - */ + // return; + /* IN case the axis end point goes behind mid way near plane, this shouldn't happen if (px.z >= -nearPlaneScale) { @@ -165,11 +173,13 @@ void main(void) { // Project px, py pz to homogeneous clip space - mat4 viewProj = getProjection(stereoSide.x); + // mat4 viewProj = getProjection(stereoSide.x); + mat4 viewProj = getProjectionMono(); px = viewProj * px; py = viewProj * py; pz = viewProj * pz; + // then to normalized clip space px.xy /= px.w; py.xy /= py.w; @@ -177,20 +187,37 @@ void main(void) { vec2 nclipPos = (texcoordPos - 0.5) * 2.0; -/* + + //vec4 clipPos = frameTransform._projection[stereoSide.x] * vec4(eyePos, 1.0); + vec4 clipPos = getProjectionMono() * vec4(eyePos, 1.0); + nclipPos = clipPos.xy / clipPos.w; + if (texcoordPos.y > 0.5) { - outFragColor = vec4(px.xy * 0.5 + 0.5, 0.0, 1.0); + // outFragColor = vec4(fract(10.0 * worldPos.xyz), 1.0); + outFragColor = vec4(fract(10.0 * (nclipPos)), 0.0, 1.0); + } else { - outFragColor = vec4(nclipPos * 0.5 + 0.5, 0.0, 1.0); + outFragColor = vec4(fract(10.0 * (clipPos.xy / clipPos.w)), 0.0, 1.0); + // outFragColor = vec4(nclipPos * 0.5 + 0.5, 0.0, 1.0); } - return; -*/ + //return; + float pixPerspectiveScaleInv = 1.0 / (perspectiveScale); px.xy = (px.xy - nclipPos) * pixPerspectiveScaleInv; py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv; pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv; + if (texcoordPos.y > 0.5) { + // outFragColor = vec4(fract(10.0 * worldPos.xyz), 1.0); + outFragColor = vec4(fract(10.0 * (px.xy)), 0.0, 1.0); + + } else { + outFragColor = vec4(fract(10.0 * (py.xy)), 0.0, 1.0); + // outFragColor = vec4(nclipPos * 0.5 + 0.5, 0.0, 1.0); + } + // return; + // Calculate dF/dx, dF/dy and dF/dz using chain rule vec4 dFdx = dFdu * px.x + dFdv * px.y; vec4 dFdy = dFdu * py.x + dFdv * py.y; diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index b0a3e14e57..6a0cd55a95 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -79,7 +79,7 @@ public: BlurGaussianConfig() : Job::Config(true) {} - float filterScale{ 1.2f }; + float filterScale{ 0.2f }; signals : void dirty(); diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index ece1bdf202..dd1a229599 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -99,7 +99,7 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep float filterScale = getFilterScale(); float scale = distanceToProjectionWindow / sampleDepth; - vec2 finalStep = filterScale * scale * direction * pixelStep; // *1000.0; + vec2 finalStep = filterScale * scale * direction * pixelStep; // Accumulate the center sample vec4 srcBlurred = gaussianDistributionCurve[0] * sampleCenter; @@ -112,7 +112,7 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep // If the difference in depth is huge, we lerp color back. - float s = clamp(/*depthThreshold */12.0 * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); + float s = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); // float s = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); srcSample = mix(srcSample, sampleCenter, s); diff --git a/scripts/developer/utilities/render/debugDeferredLighting.js b/scripts/developer/utilities/render/debugDeferredLighting.js index f2fab61717..9d48845ec9 100644 --- a/scripts/developer/utilities/render/debugDeferredLighting.js +++ b/scripts/developer/utilities/render/debugDeferredLighting.js @@ -13,8 +13,8 @@ var qml = Script.resolvePath('deferredLighting.qml'); var window = new OverlayWindow({ title: 'Deferred Lighting Pass', source: qml, - width: 400, height: 400, + width: 400, height: 350, }); -window.setPosition(250, 750); +window.setPosition(250, 800); window.closed.connect(function() { Script.stop(); }); diff --git a/scripts/developer/utilities/render/debugSurfaceGeometryPass.js b/scripts/developer/utilities/render/debugSurfaceGeometryPass.js index fed9ce4ef3..b28bb5269e 100644 --- a/scripts/developer/utilities/render/debugSurfaceGeometryPass.js +++ b/scripts/developer/utilities/render/debugSurfaceGeometryPass.js @@ -13,8 +13,8 @@ var qml = Script.resolvePath('surfaceGeometryPass.qml'); var window = new OverlayWindow({ title: 'Surface Geometry Pass', source: qml, - width: 400, height: 400, + width: 400, height: 300, }); -window.setPosition(250, 500); +window.setPosition(250, 400); window.closed.connect(function() { Script.stop(); }); diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index ec5fe07a7e..e82f142763 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -40,6 +40,16 @@ Column { checked: Render.getConfig("RenderDeferred").showScatteringBRDF onCheckedChanged: { Render.getConfig("RenderDeferred").showScatteringBRDF = checked } } + CheckBox { + text: "Show Curvature" + checked: Render.getConfig("RenderDeferred").showCurvature + onCheckedChanged: { Render.getConfig("RenderDeferred").showCurvature = checked } + } + CheckBox { + text: "Show Diffused Normal" + checked: Render.getConfig("RenderDeferred").showDiffusedNormal + onCheckedChanged: { Render.getConfig("RenderDeferred").showDiffusedNormal = checked } + } Repeater { model: [ "Scattering Bent Red:RenderDeferred:bentRed:2.0", "Scattering Bent Green:RenderDeferred:bentGreen:2.0", From 8f0d9668105fe9ae8f5434c10f097b4e0484a21a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 28 Jun 2016 13:53:57 -0700 Subject: [PATCH 0800/1237] working on enabling preview image --- libraries/display-plugins/CMakeLists.txt | 2 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 67 +++++++++++++++++-- .../display-plugins/hmd/HmdDisplayPlugin.h | 10 +++ 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index fe08647074..7d82b3b665 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME display-plugins) setup_hifi_library(OpenGL) -link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui) +link_hifi_libraries(shared plugins render-utils ui-plugins gl gpu-gl ui) target_opengl() diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 41af4ed543..5609a071a0 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -20,6 +20,10 @@ #include #include +#include +#include +#include + #include "../Logging.h" #include "../CompositorHelper.h" @@ -58,9 +62,38 @@ bool HmdDisplayPlugin::internalActivate() { _eyeInverseProjections[eye] = glm::inverse(_eyeProjections[eye]); }); + _firstPreview = true; + if (_previewTextureID == 0) { + const QString url("https://hifi-content.s3.amazonaws.com/samuel/preview.png"); + _previewTexture = DependencyManager::get()->getTexture(url); + +// const QString path("/Users/computer33/Documents/preview.png"); +// QImage previewTexture(path); +// if (!previewTexture.isNull()) { + if (_previewTexture && _previewTexture->isLoaded()) { +// previewTexture = previewTexture.mirrored(false, true); + glGenTextures(1, &_previewTextureID); + glBindTexture(GL_TEXTURE_2D, _previewTextureID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _previewTexture->getWidth(), _previewTexture->getHeight(), 0, + GL_BGRA, GL_UNSIGNED_BYTE, _previewTexture->getGPUTexture()->accessStoredMipFace(0)->readData()); + using namespace oglplus; + oglplus::Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); + oglplus::Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); + glBindTexture(GL_TEXTURE_2D, 0); + } + } + return Parent::internalActivate(); } +void HmdDisplayPlugin::internalDeactivate() { + if (_previewTextureID != 0) { + glDeleteTextures(1, &_previewTextureID); + _previewTextureID = 0; + } + Parent::internalDeactivate(); +} + static const char * REPROJECTION_VS = R"VS(#version 410 core in vec3 Position; @@ -196,6 +229,7 @@ static ProgramPtr getReprojectionProgram() { } #endif +static GLint PREVIEW_TEXTURE_LOCATION = -1; void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); @@ -206,9 +240,14 @@ void HmdDisplayPlugin::customizeContext() { #endif _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); - compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); using namespace oglplus; + if (!_enablePreview) { + compileProgram(_previewProgram, DrawUnitQuadTexcoord_vert, DrawTexture_frag); + PREVIEW_TEXTURE_LOCATION = Uniform(*_previewProgram, "colorMap").Location(); + } + + compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); REPROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "reprojection").Location(); INVERSE_PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "inverseProjections").Location(); PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "projections").Location(); @@ -217,6 +256,7 @@ void HmdDisplayPlugin::customizeContext() { void HmdDisplayPlugin::uncustomizeContext() { _sphereSection.reset(); _compositeFramebuffer.reset(); + _previewProgram.reset(); _reprojectionProgram.reset(); Parent::uncustomizeContext(); } @@ -241,8 +281,8 @@ void HmdDisplayPlugin::compositeScene() { useProgram(_reprojectionProgram); using namespace oglplus; - Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); - Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); + oglplus::Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); + oglplus::Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); Uniform(*_reprojectionProgram, REPROJECTION_MATRIX_LOCATION).Set(_currentPresentFrameInfo.presentReprojection); //Uniform(*_reprojectionProgram, PROJECTION_MATRIX_LOCATION).Set(_eyeProjections); //Uniform(*_reprojectionProgram, INVERSE_PROJECTION_MATRIX_LOCATION).Set(_eyeInverseProjections); @@ -312,12 +352,13 @@ void HmdDisplayPlugin::internalPresent() { hmdPresent(); // screen preview mirroring + auto window = _container->getPrimaryWidget(); + auto windowSize = toGlm(window->size()); + auto devicePixelRatio = window->devicePixelRatio(); if (_enablePreview) { - auto window = _container->getPrimaryWidget(); - auto windowSize = toGlm(window->size()); float windowAspect = aspect(windowSize); float sceneAspect = aspect(_renderTargetSize); - if (_monoPreview) { + if (_enablePreview && _monoPreview) { sceneAspect /= 2.0f; } float aspectRatio = sceneAspect / windowAspect; @@ -350,6 +391,20 @@ void HmdDisplayPlugin::internalPresent() { BufferSelectBit::ColorBuffer, BlitFilter::Nearest); }); swapBuffers(); + } else if (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio) { + if (_previewTexture) qDebug() << _previewTexture->getBytesReceived(); + if (PREVIEW_TEXTURE_LOCATION != -1 && _previewTextureID != 0) { + useProgram(_previewProgram); + glViewport(0, 0, windowSize.x, windowSize.y); + glUniform1i(PREVIEW_TEXTURE_LOCATION, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _previewTextureID); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + swapBuffers(); + _firstPreview = false; + _prevWindowSize = windowSize; + _prevDevicePixelRatio = devicePixelRatio; + } } postPreview(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index e6ceb7e376..7307c6968f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -13,6 +13,9 @@ #include "../OpenGLDisplayPlugin.h" +class NetworkTexture; +using NetworkTexturePointer = QSharedPointer; + class HmdDisplayPlugin : public OpenGLDisplayPlugin { using Parent = OpenGLDisplayPlugin; public: @@ -39,6 +42,7 @@ protected: virtual void updatePresentPose(); bool internalActivate() override; + void internalDeactivate() override; void compositeScene() override; void compositeOverlay() override; void compositePointer() override; @@ -73,6 +77,12 @@ private: bool _enablePreview { false }; bool _monoPreview { true }; bool _enableReprojection { true }; + bool _firstPreview { true }; + ProgramPtr _previewProgram; + GLuint _previewTextureID { 0 }; + NetworkTexturePointer _previewTexture { nullptr }; + glm::uvec2 _prevWindowSize { 0, 0 }; + qreal _prevDevicePixelRatio { 0 }; ShapeWrapperPtr _sphereSection; ProgramPtr _reprojectionProgram; }; From 630d5cfc825eef8026dd94145ae51623f1a6579e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 15:47:17 -0700 Subject: [PATCH 0801/1237] make trigger more sensitive, print out position and rotation when releasing something --- scripts/system/controllers/handControllerGrab.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f86ff158f6..2164340314 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -27,7 +27,7 @@ var WANT_DEBUG_SEARCH_NAME = null; var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.75; // Squeezed far enough to complete distant grab +var TRIGGER_GRAB_VALUE = 0.50; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; @@ -1994,6 +1994,12 @@ function MyController(hand) { joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); + + grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + print("adjusted position: " + vec3toStr(grabbedProperties.localPosition)); + print("adjusted rotation: " + quatToStr(grabbedProperties.localRotation)); + + this.grabbedEntity = null; if (this.triggerSmoothedGrab()) { From a82930cb7a07a74dc04f89b63906b182acc41c85 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 28 Jun 2016 15:59:58 -0700 Subject: [PATCH 0802/1237] cleanup --- libraries/display-plugins/CMakeLists.txt | 2 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 62 ++++++++++++------- .../display-plugins/hmd/HmdDisplayPlugin.h | 7 ++- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index 7d82b3b665..fe08647074 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME display-plugins) setup_hifi_library(OpenGL) -link_hifi_libraries(shared plugins render-utils ui-plugins gl gpu-gl ui) +link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui) target_opengl() diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 082fed787d..6ab6c71211 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -21,7 +21,9 @@ #include #include -#include +#include +#include +#include #include #include @@ -64,28 +66,39 @@ bool HmdDisplayPlugin::internalActivate() { _firstPreview = true; if (_previewTextureID == 0) { - const QString url("https://hifi-content.s3.amazonaws.com/samuel/preview.png"); - _previewTexture = DependencyManager::get()->getTexture(url); - -// const QString path("/Users/computer33/Documents/preview.png"); -// QImage previewTexture(path); -// if (!previewTexture.isNull()) { - if (_previewTexture && _previewTexture->isLoaded()) { -// previewTexture = previewTexture.mirrored(false, true); - glGenTextures(1, &_previewTextureID); - glBindTexture(GL_TEXTURE_2D, _previewTextureID); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _previewTexture->getWidth(), _previewTexture->getHeight(), 0, - GL_BGRA, GL_UNSIGNED_BYTE, _previewTexture->getGPUTexture()->accessStoredMipFace(0)->readData()); - using namespace oglplus; - oglplus::Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); - oglplus::Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); - glBindTexture(GL_TEXTURE_2D, 0); - } + const QUrl previewURL("https://hifi-content.s3.amazonaws.com/samuel/preview.png"); + QNetworkAccessManager& manager = NetworkAccessManager::getInstance(); + QNetworkRequest request(previewURL); + request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); +// connect(&manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadFinished(QNetworkReply*))); + manager.get(request); } return Parent::internalActivate(); } +void HmdDisplayPlugin::downloadFinished(QNetworkReply* reply) { + if (reply->error() != QNetworkReply::NetworkError::NoError) { + qDebug() << "HMDDisplayPlugin: error downloading preview image" << reply->errorString(); + return; + } + + QImage previewTexture; + previewTexture.loadFromData(reply->readAll()); + + if (!previewTexture.isNull()) { + previewTexture = previewTexture.mirrored(false, true); + glGenTextures(1, &_previewTextureID); + glBindTexture(GL_TEXTURE_2D, _previewTextureID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, previewTexture.width(), previewTexture.height(), 0, + GL_BGRA, GL_UNSIGNED_BYTE, previewTexture.bits()); + using namespace oglplus; + Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); + Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); + glBindTexture(GL_TEXTURE_2D, 0); + } +} + void HmdDisplayPlugin::internalDeactivate() { if (_previewTextureID != 0) { glDeleteTextures(1, &_previewTextureID); @@ -270,7 +283,8 @@ void HmdDisplayPlugin::customizeContext() { using namespace oglplus; if (!_enablePreview) { - compileProgram(_previewProgram, DrawUnitQuadTexcoord_vert, DrawTexture_frag); + std::string version("#version 410 core\n"); + compileProgram(_previewProgram, version + DrawUnitQuadTexcoord_vert, version + DrawTexture_frag); PREVIEW_TEXTURE_LOCATION = Uniform(*_previewProgram, "colorMap").Location(); } @@ -313,8 +327,8 @@ void HmdDisplayPlugin::compositeScene() { useProgram(_reprojectionProgram); using namespace oglplus; - oglplus::Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); - oglplus::Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); + Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); + Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); Uniform(*_reprojectionProgram, REPROJECTION_MATRIX_LOCATION).Set(_currentPresentFrameInfo.presentReprojection); //Uniform(*_reprojectionProgram, PROJECTION_MATRIX_LOCATION).Set(_eyeProjections); //Uniform(*_reprojectionProgram, INVERSE_PROJECTION_MATRIX_LOCATION).Set(_eyeInverseProjections); @@ -385,7 +399,7 @@ void HmdDisplayPlugin::internalPresent() { if (_enablePreview) { float windowAspect = aspect(windowSize); float sceneAspect = aspect(_renderTargetSize); - if (_enablePreview && _monoPreview) { + if (_monoPreview) { sceneAspect /= 2.0f; } float aspectRatio = sceneAspect / windowAspect; @@ -419,9 +433,9 @@ void HmdDisplayPlugin::internalPresent() { }); swapBuffers(); } else if (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio) { - if (_previewTexture) qDebug() << _previewTexture->getBytesReceived(); - if (PREVIEW_TEXTURE_LOCATION != -1 && _previewTextureID != 0) { + if (_previewTextureID != 0) { useProgram(_previewProgram); + windowSize *= devicePixelRatio; glViewport(0, 0, windowSize.x, windowSize.y); glUniform1i(PREVIEW_TEXTURE_LOCATION, 0); glActiveTexture(GL_TEXTURE0); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index ad2fc3509f..3be05ffbe6 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -14,8 +14,7 @@ #include "../OpenGLDisplayPlugin.h" -class NetworkTexture; -using NetworkTexturePointer = QSharedPointer; +class QNetworkReply; class HmdDisplayPlugin : public OpenGLDisplayPlugin { using Parent = OpenGLDisplayPlugin; @@ -89,6 +88,9 @@ protected: FrameInfo _currentPresentFrameInfo; FrameInfo _currentRenderFrameInfo; +public slots: + void downloadFinished(QNetworkReply* reply); + private: bool _enablePreview { false }; bool _monoPreview { true }; @@ -96,7 +98,6 @@ private: bool _firstPreview { true }; ProgramPtr _previewProgram; GLuint _previewTextureID { 0 }; - NetworkTexturePointer _previewTexture { nullptr }; glm::uvec2 _prevWindowSize { 0, 0 }; qreal _prevDevicePixelRatio { 0 }; ShapeWrapperPtr _sphereSection; From e538625fe07a062988351c35b77cdf07cbc01b69 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 28 Jun 2016 16:35:32 -0700 Subject: [PATCH 0803/1237] fix warning about convex hull vertex count cap --- libraries/physics/src/PhysicalEntitySimulation.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index cdf33a6edb..9714059e7c 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -218,10 +218,12 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re ShapeInfo shapeInfo; entity->computeShapeInfo(shapeInfo); int numPoints = shapeInfo.getLargestSubshapePointCount(); - if (numPoints > MAX_HULL_POINTS) { - qWarning() << "convex hull with" << numPoints - << "points for entity" << entity->getName() - << "at" << entity->getPosition() << " will be reduced"; + if (shapeInfo.getType() == SHAPE_TYPE_COMPOUND) { + if (numPoints > MAX_HULL_POINTS) { + qWarning() << "convex hull with" << numPoints + << "points for entity" << entity->getName() + << "at" << entity->getPosition() << " will be reduced"; + } } btCollisionShape* shape = ObjectMotionState::getShapeManager()->getShape(shapeInfo); if (shape) { From 74781c078638fbddd9a0fe5c5cbb6960a91526e9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 28 Jun 2016 16:36:10 -0700 Subject: [PATCH 0804/1237] boxify static meshes with too many vertices --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 366e365107..43fea75eac 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -696,6 +696,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { QVector localTransforms; const FBXGeometry& geometry = _model->getFBXGeometry(); int numberOfMeshes = geometry.meshes.size(); + int totalNumVertices = 0; for (int i = 0; i < numberOfMeshes; i++) { const FBXMesh& mesh = geometry.meshes.at(i); if (mesh.clusters.size() > 0) { @@ -706,6 +707,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::mat4 identity; localTransforms.push_back(identity); } + totalNumVertices += mesh.vertices.size(); + } + const int MAX_VERTICES_PER_STATIC_MESH = 1e6; + if (totalNumVertices > MAX_VERTICES_PER_STATIC_MESH) { + qWarning() << "model" << getModelURL() << "has too many vertices" << totalNumVertices << "and will collide as a box."; + info.setParams(SHAPE_TYPE_BOX, 0.5f * dimensions); + return; } updateModelBounds(); From b782ae667cc34c1495e28a091a6de775728aa6a6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 16:36:37 -0700 Subject: [PATCH 0805/1237] when one hand adjusts something equippd in the other, print out the new local position and rotation in case a content-creator wants to update userData --- .../system/controllers/handControllerGrab.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2164340314..8857d6c70c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1970,10 +1970,24 @@ function MyController(hand) { var noVelocity = false; if (this.grabbedEntity !== null) { + + // If this looks like the release after adjusting something still held in the other hand, print the position + // and rotation of the held thing to help content creators set the userData. + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); + if (grabData.refCount > 1) { + grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + if (grabbedProperties && grabbedProperties.localPosition && grabbedProperties.localRotation) { + print((this.hand === RIGHT_HAND ? '"LeftHand"' : '"RightHand"') + ":" + + '[{"x":' + grabbedProperties.localPosition.x + ', "y":' + grabbedProperties.localPosition.y + + ', "z":' + grabbedProperties.localPosition.z + '}, {"x":' + grabbedProperties.localRotation.x + + ', "y":' + grabbedProperties.localRotation.y + ', "z":' + grabbedProperties.localRotation.z + + ', "w":' + grabbedProperties.localRotation.w + '}]'); + } + } + if (this.actionID !== null) { Entities.deleteAction(this.grabbedEntity, this.actionID); // sometimes we want things to stay right where they are when we let go. - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); var releaseVelocityData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); if (releaseVelocityData.disableReleaseVelocity === true || // this next line allowed both: @@ -1994,12 +2008,6 @@ function MyController(hand) { joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); - - grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); - print("adjusted position: " + vec3toStr(grabbedProperties.localPosition)); - print("adjusted rotation: " + quatToStr(grabbedProperties.localRotation)); - - this.grabbedEntity = null; if (this.triggerSmoothedGrab()) { From 55c5d5364029c3c24712f096d9d802490760f0f3 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 28 Jun 2016 16:58:42 -0700 Subject: [PATCH 0806/1237] fixed signal --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 35 ++++++++++--------- .../display-plugins/hmd/HmdDisplayPlugin.h | 3 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 6ab6c71211..8ff527b023 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -70,14 +70,16 @@ bool HmdDisplayPlugin::internalActivate() { QNetworkAccessManager& manager = NetworkAccessManager::getInstance(); QNetworkRequest request(previewURL); request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); -// connect(&manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadFinished(QNetworkReply*))); - manager.get(request); + auto rep = manager.get(request); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); } return Parent::internalActivate(); } -void HmdDisplayPlugin::downloadFinished(QNetworkReply* reply) { +void HmdDisplayPlugin::downloadFinished() { + QNetworkReply* reply = static_cast(sender()); + if (reply->error() != QNetworkReply::NetworkError::NoError) { qDebug() << "HMDDisplayPlugin: error downloading preview image" << reply->errorString(); return; @@ -432,20 +434,19 @@ void HmdDisplayPlugin::internalPresent() { BufferSelectBit::ColorBuffer, BlitFilter::Nearest); }); swapBuffers(); - } else if (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio) { - if (_previewTextureID != 0) { - useProgram(_previewProgram); - windowSize *= devicePixelRatio; - glViewport(0, 0, windowSize.x, windowSize.y); - glUniform1i(PREVIEW_TEXTURE_LOCATION, 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, _previewTextureID); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - swapBuffers(); - _firstPreview = false; - _prevWindowSize = windowSize; - _prevDevicePixelRatio = devicePixelRatio; - } + } else if (_previewTextureID != 0 && (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio)) { + useProgram(_previewProgram); + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + glViewport(0, 0, windowSize.x * devicePixelRatio, windowSize.y * devicePixelRatio); + glUniform1i(PREVIEW_TEXTURE_LOCATION, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _previewTextureID); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + swapBuffers(); + _firstPreview = false; + _prevWindowSize = windowSize; + _prevDevicePixelRatio = devicePixelRatio; } postPreview(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 3be05ffbe6..374b41727b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -17,6 +17,7 @@ class QNetworkReply; class HmdDisplayPlugin : public OpenGLDisplayPlugin { + Q_OBJECT using Parent = OpenGLDisplayPlugin; public: bool isHmd() const override final { return true; } @@ -89,7 +90,7 @@ protected: FrameInfo _currentRenderFrameInfo; public slots: - void downloadFinished(QNetworkReply* reply); + void downloadFinished(); private: bool _enablePreview { false }; From 7f5e22d0eb18d698432310fd30b87fb8c0a0cde7 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 28 Jun 2016 17:20:24 -0700 Subject: [PATCH 0807/1237] Post tuesday review, clean up --- .../render-utils/src/DeferredGlobalLight.slh | 42 ++++++------------- .../render-utils/src/SubsurfaceScattering.slh | 12 ++++++ 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 0a75760fb0..9860ca79ee 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -132,31 +132,22 @@ vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenua vec3 fragLightDir = -normalize(getLightDirection(light)); - vec3 bentNormalHigh = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 bentNormalLow = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); float curvature = unpackCurvature(diffusedCurvature.w); if (showDiffusedNormal()) { return diffusedCurvature.xyz; - return bentNormalLow * 0.5 + vec3(0.5); + return lowNormal * 0.5 + vec3(0.5); } if (showCurvature()) { float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); } - vec3 rS = bentNormalHigh; - vec3 bendFactorSpectrum = getBendFactor(); - // vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); - vec3 rN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.x)); - vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); - vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); + vec3 bentNdotL = evalScatteringBentNdotL(fragNormal, midNormal, lowNormal, fragLightDir); - vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); - - //return 0.5 * (NdotLSpectrum + vec3(1.0)); - // --> Look up the pre-integrated curvature-dependent BDRF textures - vec3 brdf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + vec3 brdf = fetchBRDFSpectrum(bentNdotL, curvature); // The position of the pixel fragment in Eye space then in world space @@ -267,31 +258,22 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f vec3 fragLightDir = -normalize(getLightDirection(light)); - vec3 bentNormalHigh = normalize( (blurredCurvature.xyz - 0.5f) * 2.0f ); - vec3 bentNormalLow = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); float curvature = unpackCurvature(diffusedCurvature.w); if (showDiffusedNormal()) { return diffusedCurvature.xyz; - return bentNormalLow * 0.5 + vec3(0.5); + return lowNormal * 0.5 + vec3(0.5); } if (showCurvature()) { float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); } - vec3 rS = bentNormalHigh; - - vec3 bendFactorSpectrum = getBendFactor(); - // vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); - vec3 rN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.x)); - vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); - vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); - - vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); - //return 0.5 * (NdotLSpectrum + vec3(1.0)); - // --> Look up the pre-integrated curvature-dependent BDRF textures - vec3 brdf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + vec3 bentNdotL = evalScatteringBentNdotL(fragNormal, midNormal, lowNormal, fragLightDir); + + vec3 brdf = fetchBRDFSpectrum(bentNdotL, curvature); // The position of the pixel fragment in Eye space then in world space @@ -332,7 +314,7 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f //vec3 debugNdotL = 0.5 * (NdotLSpectrum + vec3(1.0)); //return vec3(debugNdotL.z, curvature, 0.0 ); - return vec3(color); + return vec3(color); } <@endfunc@> diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index 4aaaaff265..babd4e0a92 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -150,6 +150,18 @@ float unpackCurvature(float packedCurvature) { return abs(packedCurvature * 2 - 1) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; } +vec3 evalScatteringBentNdotL(vec3 normal, vec3 midNormal, vec3 lowNormal, vec3 lightDir) { + vec3 bendFactorSpectrum = getBendFactor(); + // vec3 rN = normalize(mix(normal, lowNormal, bendFactorSpectrum.x)); + vec3 rN = normalize(mix(midNormal, lowNormal, bendFactorSpectrum.x)); + vec3 gN = normalize(mix(midNormal, lowNormal, bendFactorSpectrum.y)); + vec3 bN = normalize(mix(midNormal, lowNormal, bendFactorSpectrum.z)); + + vec3 NdotLSpectrum = vec3(dot(rN, lightDir), dot(gN, lightDir), dot(bN, lightDir)); + + return NdotLSpectrum; +} + <@endfunc@> From a741ae5069d8fae41a4823d821236f2f30015a8c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 28 Jun 2016 17:43:15 -0700 Subject: [PATCH 0808/1237] moved _firstPreview after download --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 8ff527b023..8f8132926f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -64,7 +64,6 @@ bool HmdDisplayPlugin::internalActivate() { _eyeInverseProjections[eye] = glm::inverse(_eyeProjections[eye]); }); - _firstPreview = true; if (_previewTextureID == 0) { const QUrl previewURL("https://hifi-content.s3.amazonaws.com/samuel/preview.png"); QNetworkAccessManager& manager = NetworkAccessManager::getInstance(); @@ -98,6 +97,7 @@ void HmdDisplayPlugin::downloadFinished() { Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); glBindTexture(GL_TEXTURE_2D, 0); + _firstPreview = true; } } From d650c50a5bc5754cfbdc7f0a7d3cd1df62e90428 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 28 Jun 2016 17:44:30 -0700 Subject: [PATCH 0809/1237] Use normal Script.update (60hz) instead of a timer (@20hz). --- scripts/system/controllers/handControllerPointer.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index a0f1f47b3c..f7a85b2a43 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -398,7 +398,7 @@ function update() { } updateSeeking(true); if (!handControllerLockOut.expired(now)) { - return off(); // Let them use mouse it in peace. + return off(); // Let them use mouse in peace. } if (!Menu.isOptionChecked("First Person")) { return off(); // What to do? menus can be behind hand! @@ -406,13 +406,13 @@ function update() { if (!Window.hasFocus() || !Reticle.allowMouseCapture) { return off(); // Don't mess with other apps or paused mouse activity } - leftTrigger.update(); - rightTrigger.update(); var controllerPose = Controller.getPoseValue(activeHand); // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) if (!controllerPose.valid) { return off(); // Controller is cradled. } + leftTrigger.update(); + rightTrigger.update(); var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: @@ -452,12 +452,7 @@ function update() { clearSystemLaser(); Reticle.visible = false; } - -var UPDATE_INTERVAL = 50; // milliseconds. Script.update is too frequent. -var updater = Script.setInterval(update, UPDATE_INTERVAL); -Script.scriptEnding.connect(function () { - Script.clearInterval(updater); -}); +setupHandler(Script.update, update); // Check periodically for changes to setup. var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds From b551bc3c1ae20be28d312184e50fa2d0dd56461d Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 28 Jun 2016 18:01:09 -0700 Subject: [PATCH 0810/1237] Use the whole area, less a margin. --- scripts/system/controllers/handControllerPointer.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 8e2edbe467..00aca2af86 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -125,13 +125,12 @@ function ignoreMouseActivity() { weMovedReticle = false; return true; } -var reticleMinX, reticleMaxX, reticleMinY, reticleMaxY; +var MARGIN = 50; +var reticleMinX = MARGIN, reticleMaxX, reticleMinY = MARGIN, reticleMaxY; function updateRecommendedArea() { - var rectangle = Controller.getRecommendedOverlayRect(); - reticleMinX = rectangle.x; - reticleMaxX = rectangle.x + rectangle.width; - reticleMinY = rectangle.y; - reticleMaxY = rectangle.y + rectangle.height; + var dims = Controller.getViewportDimensions(); + reticleMaxX = dims.x - MARGIN; + reticleMaxY = dims.y - MARGIN; } var setReticlePosition = function (point2d) { weMovedReticle = true; From 962f9d5f22b1debaf131c1322742cc4e06edb582 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 28 Jun 2016 18:10:06 -0700 Subject: [PATCH 0811/1237] Don't update the shown state of the tool window when it's already invisible --- interface/resources/qml/ToolWindow.qml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index bbfc74493d..c957a37886 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -137,12 +137,15 @@ ScrollingWindow { } function updateVisiblity() { - for (var i = 0; i < tabView.count; ++i) { - if (tabView.getTab(i).enabled) { - return; + if (visible) { + for (var i = 0; i < tabView.count; ++i) { + if (tabView.getTab(i).enabled) { + console.log("QQQ tab is visible, returning early"); + return; + } } + shown = false; } - shown = false; } function findIndexForUrl(source) { From 21d2c977a36f656b0a1c3c56afa435c8c1c5098f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 28 Jun 2016 18:30:07 -0700 Subject: [PATCH 0812/1237] Remove roll from cursor in HMD --- .../src/display-plugins/CompositorHelper.cpp | 2 +- libraries/shared/src/GLMHelpers.cpp | 6 ++++++ libraries/shared/src/GLMHelpers.h | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 032350a07c..89ff2e0c8d 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -425,7 +425,7 @@ glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const d = glm::normalize(overlaySurfacePoint); } reticlePosition = headPosition + (d * getReticleDepth()); - quat reticleOrientation = glm::quat_cast(_currentDisplayPlugin->getHeadPose()); + quat reticleOrientation = cancelOutRoll(glm::quat_cast(_currentDisplayPlugin->getHeadPose())); vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize * getReticleDepth()); return glm::inverse(eyePose) * createMatFromScaleQuatAndPos(reticleScale, reticleOrientation, reticlePosition); } else { diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 556c313f95..8dbd0a146f 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -466,6 +466,12 @@ glm::mat4 createMatFromScaleQuatAndPos(const glm::vec3& scale, const glm::quat& glm::vec4(zAxis, 0.0f), glm::vec4(trans, 1.0f)); } +// cancel out roll +glm::quat cancelOutRoll(const glm::quat& q) { + glm::vec3 forward = q * glm::vec3(0.0f, 0.0f, -1.0f); + return glm::quat_cast(glm::inverse(glm::lookAt(Vectors::ZERO, forward, Vectors::UP))); +} + // cancel out roll and pitch glm::quat cancelOutRollAndPitch(const glm::quat& q) { glm::vec3 zAxis = q * glm::vec3(0.0f, 0.0f, 1.0f); diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index ae9ec25195..ef3bfeb674 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -221,6 +221,7 @@ glm::detail::tvec4 lerp(const glm::detail::tvec4& x, const glm::deta glm::mat4 createMatFromQuatAndPos(const glm::quat& q, const glm::vec3& p); glm::mat4 createMatFromScaleQuatAndPos(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans); +glm::quat cancelOutRoll(const glm::quat& q); glm::quat cancelOutRollAndPitch(const glm::quat& q); glm::mat4 cancelOutRollAndPitch(const glm::mat4& m); glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p); From b16812aa4d5d4bf497a97f2b55fe435efc98a2b2 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 19 May 2016 00:37:58 -0700 Subject: [PATCH 0813/1237] Adding GL 4.5 backend --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 11 +- .../gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp | 4 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp | 149 ++++++++++++++ libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 85 ++++++++ .../gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 48 +++++ .../gpu-gl/src/gpu/gl45/GL45BackendInput.cpp | 83 ++++++++ .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 145 ++++++++++++++ .../gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp | 38 ++++ .../src/gpu/gl45/GL45BackendTexture.cpp | 181 +++++++++++++++++ .../src/gpu/gl45/GL45BackendTransform.cpp | 71 +++++++ libraries/gpu/src/gpu/null/NullBackend.h | 54 +++++ tests/gpu-test/src/TestFbx.cpp | 187 ++++++++++++++++++ tests/gpu-test/src/TestFbx.h | 35 ++++ tests/gpu-test/src/TestFloorGrid.cpp | 54 +++++ tests/gpu-test/src/TestFloorGrid.h | 26 +++ tests/gpu-test/src/TestFloorTexture.cpp | 88 +++++++++ tests/gpu-test/src/TestFloorTexture.h | 22 +++ tests/gpu-test/src/main.cpp | 7 +- tests/gpu-test/src/unlit.slf | 28 --- tests/gpu-test/src/unlit.slv | 36 ---- 20 files changed, 1276 insertions(+), 76 deletions(-) create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45Backend.h create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp create mode 100644 libraries/gpu/src/gpu/null/NullBackend.h create mode 100644 tests/gpu-test/src/TestFbx.cpp create mode 100644 tests/gpu-test/src/TestFbx.h create mode 100644 tests/gpu-test/src/TestFloorGrid.cpp create mode 100644 tests/gpu-test/src/TestFloorGrid.h create mode 100644 tests/gpu-test/src/TestFloorTexture.cpp create mode 100644 tests/gpu-test/src/TestFloorTexture.h delete mode 100644 tests/gpu-test/src/unlit.slf delete mode 100644 tests/gpu-test/src/unlit.slv diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index e18b784018..32e063a4c6 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -17,6 +17,7 @@ #include #include "../gl41/GL41Backend.h" +#include "../gl45/GL45Backend.h" #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" @@ -31,25 +32,19 @@ using namespace gpu; using namespace gpu::gl; - static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); Backend* GLBackend::createBackend() { - -#if 0 // FIXME provide a mechanism to override the backend for testing // Where the gpuContext is initialized and where the TRUE Backend is created and assigned auto version = QOpenGLContextWrapper::currentContextVersion(); GLBackend* result; if (enableOpenGL45 && version >= 0x0405) { - result = new gpu::gl45::GLBackend; + result = new gpu::gl45::GL45Backend(); } else { - result = new gpu::gl41::GLBackend; + result = new gpu::gl41::GL41Backend(); } -#else - GLBackend* result = new gpu::gl41::GL41Backend; -#endif result->initInput(); result->initTransform(); gl::GLTexture::initTextureTransferHelper(); diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp index 3c6109bbdf..478d210535 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp @@ -16,7 +16,7 @@ using namespace gpu; using namespace gpu::gl41; class GL41Query : public gpu::gl::GLQuery { - using Parent = gpu::gl::GLBuffer; + using Parent = gpu::gl::GLQuery; public: static GLuint allocateQuery() { GLuint result; @@ -25,7 +25,7 @@ public: } GL41Query(const Query& query) - : gl::GLQuery(query, allocateQuery()) { } + : Parent(query, allocateQuery()) { } }; gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp new file mode 100644 index 0000000000..bb6ae67233 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -0,0 +1,149 @@ +// +// Created by Sam Gateau on 10/27/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" + +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") + +using namespace gpu; +using namespace gpu::gl45; + +void GL45Backend::do_draw(Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 1]._uint; + uint32 startVertex = batch._params[paramOffset + 0]._uint; + + if (isStereo()) { + setupStereoSide(0); + glDrawArrays(mode, startVertex, numVertices); + setupStereoSide(1); + glDrawArrays(mode, startVertex, numVertices); + + _stats._DSNumTriangles += 2 * numVertices / 3; + _stats._DSNumDrawcalls += 2; + + } else { + glDrawArrays(mode, startVertex, numVertices); + _stats._DSNumTriangles += numVertices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GL45Backend::do_drawIndexed(Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numIndices = batch._params[paramOffset + 1]._uint; + uint32 startIndex = batch._params[paramOffset + 0]._uint; + + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { + setupStereoSide(0); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + setupStereoSide(1); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + + _stats._DSNumTriangles += 2 * numIndices / 3; + _stats._DSNumDrawcalls += 2; + } else { + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + _stats._DSNumTriangles += numIndices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GL45Backend::do_drawInstanced(Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 2]._uint; + uint32 startVertex = batch._params[paramOffset + 1]._uint; + + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + + setupStereoSide(0); + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + setupStereoSide(1); + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + + _stats._DSNumTriangles += (trueNumInstances * numVertices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + _stats._DSNumTriangles += (numInstances * numVertices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GL45Backend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 3]._uint]; + uint32 numIndices = batch._params[paramOffset + 2]._uint; + uint32 startIndex = batch._params[paramOffset + 1]._uint; + uint32 startInstance = batch._params[paramOffset + 0]._uint; + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + setupStereoSide(0); + glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + setupStereoSide(1); + glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + _stats._DSNumTriangles += (trueNumInstances * numIndices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + _stats._DSNumTriangles += (numInstances * numIndices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + + _stats._DSNumAPIDrawcalls++; + + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + glMultiDrawArraysIndirect(mode, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + GLenum indexType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + glMultiDrawElementsIndirect(mode, indexType, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; + (void)CHECK_GL_ERROR(); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h new file mode 100644 index 0000000000..d0dfbd0e41 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -0,0 +1,85 @@ +// +// GL45Backend.h +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 10/27/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_45_GL45Backend_h +#define hifi_gpu_45_GL45Backend_h + +#include "../gl/GLBackend.h" +#include "../gl/GLTexture.h" + +namespace gpu { namespace gl45 { + +class GL45Backend : public gl::GLBackend { + using Parent = gl::GLBackend; + // Context Backend static interface required + friend class Context; + +public: + explicit GL45Backend(bool syncCache) : Parent(syncCache) {} + GL45Backend() : Parent() {} + + class GL45Texture : public gpu::gl::GLTexture { + using Parent = gpu::gl::GLTexture; + GLuint allocate(const Texture& texture); + public: + GL45Texture(const Texture& texture, bool transferrable); + GL45Texture(const Texture& texture, GLTexture* original); + + protected: + void transferMip(uint16_t mipLevel, uint8_t face = 0) const; + void allocateStorage() const override; + void updateSize() const override; + void transfer() const override; + void syncSampler() const override; + void generateMips() const override; + void withPreservedTexture(std::function f) const override; + }; + + +protected: + GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; + gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; + + GLuint getBufferID(const Buffer& buffer) override; + gl::GLBuffer* syncGPUObject(const Buffer& buffer) override; + + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + + GLuint getQueryID(const QueryPointer& query) override; + gl::GLQuery* syncGPUObject(const Query& query) override; + + // Draw Stage + void do_draw(Batch& batch, size_t paramOffset) override; + void do_drawIndexed(Batch& batch, size_t paramOffset) override; + void do_drawInstanced(Batch& batch, size_t paramOffset) override; + void do_drawIndexedInstanced(Batch& batch, size_t paramOffset) override; + void do_multiDrawIndirect(Batch& batch, size_t paramOffset) override; + void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) override; + + // Input Stage + void updateInput() override; + + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void transferTransformState(const Batch& batch) const override; + void initTransform() override; + void updateTransform(const Batch& batch); + void resetTransformStage(); + + // Output stage + void do_blit(Batch& batch, size_t paramOffset) override; +}; + +} } + +Q_DECLARE_LOGGING_CATEGORY(gpugl45logging) + + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp new file mode 100644 index 0000000000..b7bb04cbfb --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -0,0 +1,48 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" +#include "../gl/GLBuffer.h" + +using namespace gpu; +using namespace gpu::gl45; + +class GL45Buffer : public gl::GLBuffer { + using Parent = gpu::gl::GLBuffer; + static GLuint allocate() { + GLuint result; + glCreateBuffers(1, &result); + return result; + } + +public: + GL45Buffer(const Buffer& buffer, GLBuffer* original) : Parent(buffer, allocate()) { + glNamedBufferStorage(_buffer, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); + glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); + Backend::setGPUObject(buffer, this); + } + + void transfer() override { + Size offset; + Size size; + Size currentPage { 0 }; + auto data = _gpuObject.getSysmem().readData(); + while (_gpuObject.getNextTransferBlock(offset, size, currentPage)) { + glNamedBufferSubData(_buffer, (GLintptr)offset, (GLsizeiptr)size, data + offset); + } + (void)CHECK_GL_ERROR(); + _gpuObject._flags &= ~Buffer::DIRTY; + } +}; + +GLuint GL45Backend::getBufferID(const Buffer& buffer) { + return GL45Buffer::getId(buffer); +} + +gl::GLBuffer* GL45Backend::syncGPUObject(const Buffer& buffer) { + return GL45Buffer::sync(buffer); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp new file mode 100644 index 0000000000..5cefe40448 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp @@ -0,0 +1,83 @@ +// +// GL45BackendInput.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" + +using namespace gpu; +using namespace gpu::gl45; + +void GL45Backend::updateInput() { + if (_input._invalidFormat) { + + InputStageState::ActivationCache newActivation; + + // Assign the vertex format required + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; + GLuint offset = (GLuint)attrib._offset;; + GLboolean isNormalized = attrib._element.isNormalized(); + + GLenum perLocationSize = attrib._element.getLocationSize(); + + for (uint8_t locNum = 0; locNum < locationCount; ++locNum) { + newActivation.set(slot + locNum); + glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); + glVertexAttribBinding(slot + locNum, attrib._channel); + } + glVertexBindingDivisor(attrib._channel, attrib._frequency); + } + (void) CHECK_GL_ERROR(); + } + + // Manage Activation what was and what is expected now + for (uint32_t i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + _input._attributeActivation.flip(i); + } + } + (void) CHECK_GL_ERROR(); + + _input._invalidFormat = false; + _stats._ISNumFormatChanges++; + } + + if (_input._invalidBuffers.any()) { + auto numBuffers = _input._buffers.size(); + auto buffer = _input._buffers.data(); + auto vbo = _input._bufferVBOs.data(); + auto offset = _input._bufferOffsets.data(); + auto stride = _input._bufferStrides.data(); + + for (uint32_t bufferNum = 0; bufferNum < numBuffers; bufferNum++) { + if (_input._invalidBuffers.test(bufferNum)) { + glBindVertexBuffer(bufferNum, (*vbo), (GLuint)(*offset), (GLuint)(*stride)); + } + buffer++; + vbo++; + offset++; + stride++; + } + _input._invalidBuffers.reset(); + (void) CHECK_GL_ERROR(); + } +} + diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp new file mode 100644 index 0000000000..7f511be3e8 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -0,0 +1,145 @@ +// +// GL45BackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" +#include "../gl/GLFramebuffer.h" +#include "../gl/GLTexture.h" + +#include + +namespace gpu { namespace gl45 { + +class GL45Framebuffer : public gl::GLFramebuffer { + using Parent = gl::GLFramebuffer; + static GLuint allocate() { + GLuint result; + glCreateFramebuffers(1, &result); + return result; + } +public: + void update() override { + gl::GLTexture* gltexture = nullptr; + TexturePointer surface; + if (_gpuObject.getColorStamps() != _colorStamps) { + if (_gpuObject.hasColor()) { + _colorBuffers.clear(); + static const GLenum colorAttachments[] = { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2, + GL_COLOR_ATTACHMENT3, + GL_COLOR_ATTACHMENT4, + GL_COLOR_ATTACHMENT5, + GL_COLOR_ATTACHMENT6, + GL_COLOR_ATTACHMENT7, + GL_COLOR_ATTACHMENT8, + GL_COLOR_ATTACHMENT9, + GL_COLOR_ATTACHMENT10, + GL_COLOR_ATTACHMENT11, + GL_COLOR_ATTACHMENT12, + GL_COLOR_ATTACHMENT13, + GL_COLOR_ATTACHMENT14, + GL_COLOR_ATTACHMENT15 }; + + int unit = 0; + for (auto& b : _gpuObject.getRenderBuffers()) { + surface = b._texture; + if (surface) { + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + } else { + gltexture = nullptr; + } + + if (gltexture) { + glNamedFramebufferTexture(_id, colorAttachments[unit], gltexture->_texture, 0); + _colorBuffers.push_back(colorAttachments[unit]); + } else { + glNamedFramebufferTexture(_id, colorAttachments[unit], 0, 0); + } + unit++; + } + } + _colorStamps = _gpuObject.getColorStamps(); + } + + GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; + if (!_gpuObject.hasStencil()) { + attachement = GL_DEPTH_ATTACHMENT; + } else if (!_gpuObject.hasDepth()) { + attachement = GL_STENCIL_ATTACHMENT; + } + + if (_gpuObject.getDepthStamp() != _depthStamp) { + auto surface = _gpuObject.getDepthStencilBuffer(); + if (_gpuObject.hasDepthStencil() && surface) { + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + } + + if (gltexture) { + glNamedFramebufferTexture(_id, attachement, gltexture->_texture, 0); + } else { + glNamedFramebufferTexture(_id, attachement, 0, 0); + } + _depthStamp = _gpuObject.getDepthStamp(); + } + + + // Last but not least, define where we draw + if (!_colorBuffers.empty()) { + glDrawBuffers((GLsizei)_colorBuffers.size(), _colorBuffers.data()); + } else { + glDrawBuffer(GL_NONE); + } + + // Now check for completness + _status = glCheckNamedFramebufferStatus(_id, GL_DRAW_FRAMEBUFFER); + + // restore the current framebuffer + checkStatus(GL_DRAW_FRAMEBUFFER); + } + + +public: + GL45Framebuffer(const gpu::Framebuffer& framebuffer) + : Parent(framebuffer, allocate()) { } +}; + +gl::GLFramebuffer* GL45Backend::syncGPUObject(const Framebuffer& framebuffer) { + return gl::GLFramebuffer::sync(framebuffer); +} + +GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) { + return framebuffer ? gl::GLFramebuffer::getId(*framebuffer) : 0; +} + +void GL45Backend::do_blit(Batch& batch, size_t paramOffset) { + auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); + Vec4i srcvp; + for (auto i = 0; i < 4; ++i) { + srcvp[i] = batch._params[paramOffset + 1 + i]._int; + } + + auto dstframebuffer = batch._framebuffers.get(batch._params[paramOffset + 5]._uint); + Vec4i dstvp; + for (auto i = 0; i < 4; ++i) { + dstvp[i] = batch._params[paramOffset + 6 + i]._int; + } + + // Assign dest framebuffer if not bound already + auto destFbo = getFramebufferID(dstframebuffer); + auto srcFbo = getFramebufferID(srcframebuffer); + glBlitNamedFramebuffer(srcFbo, destFbo, + srcvp.x, srcvp.y, srcvp.z, srcvp.w, + dstvp.x, dstvp.y, dstvp.z, dstvp.w, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + (void) CHECK_GL_ERROR(); +} + +} } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp new file mode 100644 index 0000000000..a7a7c26bed --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp @@ -0,0 +1,38 @@ +// +// GL45BackendQuery.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 7/7/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 "GL45Backend.h" + +#include "../gl/GLQuery.h" + +namespace gpu { namespace gl45 { + +class GL45Query : public gpu::gl::GLQuery { + using Parent = gpu::gl::GLQuery; +public: + static GLuint allocateQuery() { + GLuint result; + glCreateQueries(GL_TIME_ELAPSED, 1, &result); + return result; + } + + GL45Query(const Query& query) + : Parent(query, allocateQuery()) { } +}; + +gl::GLQuery* GL45Backend::syncGPUObject(const Query& query) { + return GL45Query::sync(query); +} + +GLuint GL45Backend::getQueryID(const QueryPointer& query) { + return GL45Query::getId(query); +} + +} } \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp new file mode 100644 index 0000000000..36fb4bfde3 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -0,0 +1,181 @@ +// +// GL45BackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" + +#include +#include +#include + +#include "../gl/GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl45; + +using GLTexelFormat = gl::GLTexelFormat; +using GL45Texture = GL45Backend::GL45Texture; + +GLuint GL45Texture::allocate(const Texture& texture) { + Backend::incrementTextureGPUCount(); + GLuint result; + glCreateTextures(getGLTextureType(texture), 1, &result); + return result; +} + +GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { + return GL45Texture::getId(texture, transfer); +} + +gl::GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GL45Texture::sync(texture, transfer); +} + +GL45Backend::GL45Texture::GL45Texture(const Texture& texture, bool transferrable) + : gl::GLTexture(texture, allocate(texture), transferrable) {} + +GL45Backend::GL45Texture::GL45Texture(const Texture& texture, GLTexture* original) + : gl::GLTexture(texture, allocate(texture), original) {} + +void GL45Backend::GL45Texture::withPreservedTexture(std::function f) const { + f(); +} + +void GL45Backend::GL45Texture::generateMips() const { + glGenerateTextureMipmap(_id); + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::GL45Texture::allocateStorage() const { + gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); + } + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); + glTextureStorage2D(_id, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::GL45Texture::updateSize() const { + setSize(_virtualSize); + if (!_id) { + return; + } + + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); + } +} + +// Move content bits from the CPU to the GPU for a given mip / face +void GL45Backend::GL45Texture::transferMip(uint16_t mipLevel, uint8_t face) const { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + auto size = _gpuObject.evalMipDimensions(mipLevel); + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } else { + Q_ASSERT(false); + } + (void)CHECK_GL_ERROR(); +} + +// This should never happen on the main thread +// Move content bits from the CPU to the GPU +void GL45Backend::GL45Texture::transfer() const { + PROFILE_RANGE(__FUNCTION__); + //qDebug() << "Transferring texture: " << _privateTexture; + // Need to update the content of the GPU object from the source sysmem of the texture + if (_contentStamp >= _gpuObject.getDataStamp()) { + return; + } + + if (_downsampleSource._texture) { + GLuint fbo { 0 }; + glCreateFramebuffers(1, &fbo); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + // Find the distance between the old min mip and the new one + uint16 mipOffset = _minMip - _downsampleSource._minMip; + for (uint16 i = _minMip; i <= _maxMip; ++i) { + uint16 targetMip = i - _minMip; + uint16 sourceMip = targetMip + mipOffset; + Vec3u dimensions = _gpuObject.evalMipDimensions(i); + for (GLenum target : getFaceTargets(_target)) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, _downsampleSource._texture, sourceMip); + (void)CHECK_GL_ERROR(); + glCopyTextureSubImage2D(_id, targetMip, 0, 0, 0, 0, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); + } + } + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + } else { + // GO through the process of allocating the correct storage and/or update the content + switch (_gpuObject.getType()) { + case Texture::TEX_2D: + { + for (uint16_t i = _minMip; i <= _maxMip; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i)) { + transferMip(i); + } + } + } + break; + + case Texture::TEX_CUBE: + // transfer pixels from each faces + for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + transferMip(i, f); + } + } + } + break; + + default: + qCWarning(gpugl45logging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; + break; + } + } + if (_gpuObject.isAutogenerateMips()) { + glGenerateTextureMipmap(_id); + (void)CHECK_GL_ERROR(); + } +} + +void GL45Backend::GL45Texture::syncSampler() const { + const Sampler& sampler = _gpuObject.getSampler(); + + const auto& fm = FILTER_MODES[sampler.getFilter()]; + glTextureParameteri(_id, GL_TEXTURE_MIN_FILTER, fm.minFilter); + glTextureParameteri(_id, GL_TEXTURE_MAG_FILTER, fm.magFilter); + + if (sampler.doComparison()) { + glTextureParameteri(_id, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); + glTextureParameteri(_id, GL_TEXTURE_COMPARE_FUNC, gl::COMPARISON_TO_GL[sampler.getComparisonFunction()]); + } else { + glTextureParameteri(_id, GL_TEXTURE_COMPARE_MODE, GL_NONE); + } + + glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); + glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); + glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); + glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); +} + diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp new file mode 100644 index 0000000000..fd49729164 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp @@ -0,0 +1,71 @@ +// +// GL45BackendTransform.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" + +using namespace gpu; +using namespace gpu::gl45; + +void GL45Backend::initTransform() { + GLuint transformBuffers[3]; + glCreateBuffers(3, transformBuffers); + _transform._objectBuffer = transformBuffers[0]; + _transform._cameraBuffer = transformBuffers[1]; + _transform._drawCallInfoBuffer = transformBuffers[2]; + glCreateTextures(1, GL_TEXTURE_2D, &_transform._objectBufferTexture); + size_t cameraSize = sizeof(TransformStageState::CameraBufferElement); + while (_transform._cameraUboSize < cameraSize) { + _transform._cameraUboSize += _uboAlignment; + } +} + +void GL45Backend::transferTransformState(const Batch& batch) const { + // FIXME not thread safe + static std::vector bufferData; + if (!_transform._cameras.empty()) { + bufferData.resize(_transform._cameraUboSize * _transform._cameras.size()); + for (size_t i = 0; i < _transform._cameras.size(); ++i) { + memcpy(bufferData.data() + (_transform._cameraUboSize * i), &_transform._cameras[i], sizeof(TransformStageState::CameraBufferElement)); + } + glNamedBufferData(_transform._cameraBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + } + + if (!batch._objects.empty()) { + auto byteSize = batch._objects.size() * sizeof(Batch::TransformObject); + bufferData.resize(byteSize); + memcpy(bufferData.data(), batch._objects.data(), byteSize); + glNamedBufferData(_transform._objectBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + } + + if (!batch._namedData.empty()) { + bufferData.clear(); + for (auto& data : batch._namedData) { + auto currentSize = bufferData.size(); + auto bytesToCopy = data.second.drawCallInfos.size() * sizeof(Batch::DrawCallInfo); + bufferData.resize(currentSize + bytesToCopy); + memcpy(bufferData.data() + currentSize, data.second.drawCallInfos.data(), bytesToCopy); + _transform._drawCallInfoOffsets[data.first] = (GLvoid*)currentSize; + } + glNamedBufferData(_transform._drawCallInfoBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + } + +#ifdef GPU_SSBO_DRAW_CALL_INFO + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _transform._objectBuffer); +#else + glActiveTexture(GL_TEXTURE0 + TRANSFORM_OBJECT_SLOT); + glBindTexture(GL_TEXTURE_BUFFER, _transform._objectBufferTexture); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, _transform._objectBuffer); +#endif + + CHECK_GL_ERROR(); + + // Make sure the current Camera offset is unknown before render Draw + _transform._currentCameraOffset = INVALID_OFFSET; +} diff --git a/libraries/gpu/src/gpu/null/NullBackend.h b/libraries/gpu/src/gpu/null/NullBackend.h new file mode 100644 index 0000000000..097cee27e7 --- /dev/null +++ b/libraries/gpu/src/gpu/null/NullBackend.h @@ -0,0 +1,54 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_Null_Backend_h +#define hifi_gpu_Null_Backend_h + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../Context.h" + +namespace gpu { namespace null { + +class Backend : public gpu::Backend { + using Parent = gpu::Backend; + // Context Backend static interface required + friend class gpu::Context; + static void init() {} + static gpu::Backend* createBackend() { return new Backend(); } + static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { return true; } + +protected: + explicit Backend(bool syncCache) : Parent() { } + Backend() : Parent() { } +public: + ~Backend() { } + + void render(Batch& batch) final { } + + // This call synchronize the Full Backend cache with the current GLState + // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync + // the gpu::Backend state with the true gl state which has probably been messed up by these ugly naked gl calls + // Let's try to avoid to do that as much as possible! + void syncCache() final { } + + // This is the ugly "download the pixels to sysmem for taking a snapshot" + // Just avoid using it, it's ugly and will break performances + virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) final { } +}; + +} } + +#endif diff --git a/tests/gpu-test/src/TestFbx.cpp b/tests/gpu-test/src/TestFbx.cpp new file mode 100644 index 0000000000..cea356e125 --- /dev/null +++ b/tests/gpu-test/src/TestFbx.cpp @@ -0,0 +1,187 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestFbx.h" + +#include + +#include + +#include + +struct MyVertex { + vec3 position; + vec2 texCoords; + vec3 normal; + uint32_t color; + + static gpu::Stream::FormatPointer getVertexFormat() { + static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; + static const gpu::Element TEXTURE_ELEMENT { gpu::VEC2, gpu::FLOAT, gpu::UV }; + static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; + static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA }; + gpu::Stream::FormatPointer vertexFormat { std::make_shared() }; + vertexFormat->setAttribute(gpu::Stream::POSITION, 0, POSITION_ELEMENT, offsetof(MyVertex, position)); + vertexFormat->setAttribute(gpu::Stream::TEXCOORD, 0, TEXTURE_ELEMENT, offsetof(MyVertex, texCoords)); + vertexFormat->setAttribute(gpu::Stream::COLOR, 0, COLOR_ELEMENT, offsetof(MyVertex, color)); + vertexFormat->setAttribute(gpu::Stream::NORMAL, 0, NORMAL_ELEMENT, offsetof(MyVertex, normal)); + return vertexFormat; + } + +}; + +struct Part { + size_t baseVertex; + size_t baseIndex; + size_t materialId; +}; + +struct DrawElementsIndirectCommand { + uint count { 0 }; + uint instanceCount { 1 }; + uint firstIndex { 0 }; + uint baseVertex { 0 }; + uint baseInstance { 0 }; +}; + + +class FileDownloader : public QObject { + Q_OBJECT +public: + explicit FileDownloader(QUrl imageUrl, QObject *parent = 0) : QObject(parent) { + connect(&m_WebCtrl, SIGNAL(finished(QNetworkReply*)), this, SLOT(fileDownloaded(QNetworkReply*))); + QNetworkRequest request(imageUrl); + m_WebCtrl.get(request); + } + + virtual ~FileDownloader() {} + + const QByteArray& downloadedData() const { + return m_DownloadedData; + } + +signals: + void downloaded(); + +private slots: + void fileDownloaded(QNetworkReply* pReply) { + m_DownloadedData = pReply->readAll(); + pReply->deleteLater(); + emit downloaded(); + } + +private: + QNetworkAccessManager m_WebCtrl; + QByteArray m_DownloadedData; +}; + + +static const QUrl TEST_ASSET = QString("https://s3.amazonaws.com/DreamingContent/assets/models/tardis/console.fbx"); +static const mat4 TEST_ASSET_TRANSFORM = glm::translate(mat4(), vec3(0, -1.5f, 0)) * glm::scale(mat4(), vec3(0.01f)); +//static const QUrl TEST_ASSET = QString("https://s3.amazonaws.com/DreamingContent/assets/simple/SimpleMilitary/Models/Vehicles/tank_02_c.fbx"); +//static const mat4 TEST_ASSET_TRANSFORM = glm::translate(mat4(), vec3(0, -0.5f, 0)) * glm::scale(mat4(), vec3(0.1f)); + +TestFbx::TestFbx(const render::ShapePlumberPointer& shapePlumber) : _shapePlumber(shapePlumber) { + FileDownloader* downloader = new FileDownloader(TEST_ASSET, qApp); + QObject::connect(downloader, &FileDownloader::downloaded, [this, downloader] { + parseFbx(downloader->downloadedData()); + }); +} + +bool TestFbx::isReady() const { + return _partCount != 0; +} + +void TestFbx::parseFbx(const QByteArray& fbxData) { + QVariantHash mapping; + FBXGeometry* fbx = readFBX(fbxData, mapping); + size_t totalVertexCount = 0; + size_t totalIndexCount = 0; + size_t totalPartCount = 0; + size_t highestIndex = 0; + for (const auto& mesh : fbx->meshes) { + size_t vertexCount = mesh.vertices.size(); + totalVertexCount += mesh.vertices.size(); + highestIndex = std::max(highestIndex, vertexCount); + totalPartCount += mesh.parts.size(); + for (const auto& part : mesh.parts) { + totalIndexCount += part.quadTrianglesIndices.size(); + totalIndexCount += part.triangleIndices.size(); + } + } + size_t baseVertex = 0; + std::vector vertices; + vertices.reserve(totalVertexCount); + std::vector indices; + indices.reserve(totalIndexCount); + std::vector parts; + parts.reserve(totalPartCount); + _partCount = totalPartCount; + for (const auto& mesh : fbx->meshes) { + baseVertex = vertices.size(); + + vec3 color; + for (const auto& part : mesh.parts) { + DrawElementsIndirectCommand partIndirect; + partIndirect.baseVertex = (uint)baseVertex; + partIndirect.firstIndex = (uint)indices.size(); + partIndirect.baseInstance = (uint)parts.size(); + _partTransforms.push_back(mesh.modelTransform); + auto material = fbx->materials[part.materialID]; + color = material.diffuseColor; + for (auto index : part.quadTrianglesIndices) { + indices.push_back(index); + } + for (auto index : part.triangleIndices) { + indices.push_back(index); + } + size_t triangles = (indices.size() - partIndirect.firstIndex); + Q_ASSERT(0 == (triangles % 3)); + //triangles /= 3; + partIndirect.count = (uint)triangles; + parts.push_back(partIndirect); + } + + size_t vertexCount = mesh.vertices.size(); + for (size_t i = 0; i < vertexCount; ++i) { + MyVertex vertex; + vertex.position = mesh.vertices[(int)i]; + vec3 n = mesh.normals[(int)i]; + vertex.normal = n; + vertex.texCoords = mesh.texCoords[(int)i]; + vertex.color = toCompactColor(vec4(color, 1)); + vertices.push_back(vertex); + } + } + + _vertexBuffer->append(vertices); + _indexBuffer->append(indices); + _indirectBuffer->append(parts); + delete fbx; +} + +void TestFbx::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + //pipeline->pipeline + if (_partCount) { + for (size_t i = 0; i < _partCount; ++i) { + batch.setModelTransform(TEST_ASSET_TRANSFORM * _partTransforms[i]); + batch.setupNamedCalls(__FUNCTION__, [this](gpu::Batch& batch, gpu::Batch::NamedBatchData&) { + RenderArgs args; args._batch = &batch; + _shapePlumber->pickPipeline(&args, render::ShapeKey()); + batch.setInputBuffer(0, _vertexBuffer, 0, sizeof(MyVertex)); + batch.setIndexBuffer(gpu::UINT16, _indexBuffer, 0); + batch.setInputFormat(MyVertex::getVertexFormat()); + batch.setIndirectBuffer(_indirectBuffer, 0); + batch.multiDrawIndexedIndirect((uint)_partCount, gpu::TRIANGLES); + }); + } + } +} + +#include "TestFbx.moc" diff --git a/tests/gpu-test/src/TestFbx.h b/tests/gpu-test/src/TestFbx.h new file mode 100644 index 0000000000..1f3c0bb50e --- /dev/null +++ b/tests/gpu-test/src/TestFbx.h @@ -0,0 +1,35 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#include "TestHelpers.h" + +#include + +class FBXGeometry; + +class TestFbx : public GpuTestBase { + size_t _partCount { 0 }; + model::Material _material; + render::ShapeKey _shapeKey; + std::vector _partTransforms; + render::ShapePlumberPointer _shapePlumber; + gpu::Stream::FormatPointer _vertexFormat { std::make_shared() }; + gpu::BufferPointer _vertexBuffer { std::make_shared() }; + gpu::BufferPointer _indexBuffer { std::make_shared() }; + gpu::BufferPointer _indirectBuffer { std::make_shared() }; +public: + TestFbx(const render::ShapePlumberPointer& shapePlumber); + bool isReady() const override; + void renderTest(size_t test, RenderArgs* args) override; + +private: + void parseFbx(const QByteArray& fbxData); +}; + + diff --git a/tests/gpu-test/src/TestFloorGrid.cpp b/tests/gpu-test/src/TestFloorGrid.cpp new file mode 100644 index 0000000000..a24b4778d4 --- /dev/null +++ b/tests/gpu-test/src/TestFloorGrid.cpp @@ -0,0 +1,54 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestFloorGrid.h" + + +TestFloorGrid::TestFloorGrid() { + auto geometryCache = DependencyManager::get(); + // Render grid on xz plane (not the optimal way to do things, but w/e) + // Note: GeometryCache::renderGrid will *not* work, as it is apparenly unaffected by batch rotations and renders xy only + static const std::string GRID_INSTANCE = "Grid"; + static auto compactColor1 = toCompactColor(vec4 { 0.35f, 0.25f, 0.15f, 1.0f }); + static auto compactColor2 = toCompactColor(vec4 { 0.15f, 0.25f, 0.35f, 1.0f }); + static std::vector transforms; + static gpu::BufferPointer colorBuffer; + if (!transforms.empty()) { + transforms.reserve(200); + colorBuffer = std::make_shared(); + for (int i = 0; i < 100; ++i) { + { + glm::mat4 transform = glm::translate(mat4(), vec3(0, -1, -50 + i)); + transform = glm::scale(transform, vec3(100, 1, 1)); + transforms.push_back(transform); + colorBuffer->append(compactColor1); + } + + { + glm::mat4 transform = glm::mat4_cast(quat(vec3(0, PI / 2.0f, 0))); + transform = glm::translate(transform, vec3(0, -1, -50 + i)); + transform = glm::scale(transform, vec3(100, 1, 1)); + transforms.push_back(transform); + colorBuffer->append(compactColor2); + } + } + } + +} + +void TestFloorGrid::renderTest(size_t testId, RenderArgs* args) { + //gpu::Batch& batch = *(args->_batch); + //auto pipeline = geometryCache->getSimplePipeline(); + //for (auto& transform : transforms) { + // batch.setModelTransform(transform); + // batch.setupNamedCalls(GRID_INSTANCE, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + // batch.setPipeline(_pipeline); + // geometryCache->renderWireShapeInstances(batch, GeometryCache::Line, data.count(), colorBuffer); + // }); + //} +} diff --git a/tests/gpu-test/src/TestFloorGrid.h b/tests/gpu-test/src/TestFloorGrid.h new file mode 100644 index 0000000000..566c8f0968 --- /dev/null +++ b/tests/gpu-test/src/TestFloorGrid.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include +#include +#include + +#include +#include + +#include "TestHelpers.h" + +class TestFloorGrid : public GpuTestBase { +public: + TestFloorGrid(); + void renderTest(size_t testId, RenderArgs* args) override; +}; + + diff --git a/tests/gpu-test/src/TestFloorTexture.cpp b/tests/gpu-test/src/TestFloorTexture.cpp new file mode 100644 index 0000000000..884be5ec30 --- /dev/null +++ b/tests/gpu-test/src/TestFloorTexture.cpp @@ -0,0 +1,88 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestFloorTexture.h" + +struct Vertex { + vec4 position; + vec4 texture; + vec4 normal; + vec4 color; +}; +static const uint TEXTURE_OFFSET = offsetof(Vertex, texture); +static const uint NORMAL_OFFSET = offsetof(Vertex, normal); +static const uint POSITION_OFFSET = offsetof(Vertex, position); +static const uint COLOR_OFFSET = offsetof(Vertex, color); +static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element TEXTURE_ELEMENT { gpu::VEC2, gpu::FLOAT, gpu::UV }; +static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::FLOAT, gpu::RGBA }; + +FloorTextureTest::FloorTextureTest() { + auto geometryCache = DependencyManager::get(); + std::vector vertices; + const int MINX = -1000; + const int MAXX = 1000; + + vertices.push_back({ + vec4(MAXX, 0, MAXX, 1), + vec4(MAXX, MAXX, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertices.push_back({ + vec4(MAXX, 0, MINX, 1), + vec4(MAXX, 0, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertices.push_back({ + vec4(MINX, 0, MINX, 1), + vec4(0, 0, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertices.push_back({ + vec4(MINX, 0, MAXX, 1), + vec4(0, MAXX, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertexBuffer->append(vertices); + indexBuffer->append(std::vector({ 0, 1, 2, 2, 3, 0 })); + texture = DependencyManager::get()->getImageTexture("C:/Users/bdavis/Git/openvr/samples/bin/cube_texture.png"); + //texture = DependencyManager::get()->getImageTexture("H:/test.png"); + //texture = DependencyManager::get()->getImageTexture("H:/crate_blue.fbm/lambert8SG_Normal_OpenGL.png"); + vertexFormat->setAttribute(gpu::Stream::POSITION, 0, POSITION_ELEMENT, POSITION_OFFSET); + vertexFormat->setAttribute(gpu::Stream::TEXCOORD, 0, TEXTURE_ELEMENT, TEXTURE_OFFSET); + vertexFormat->setAttribute(gpu::Stream::COLOR, 0, COLOR_ELEMENT, COLOR_OFFSET); + vertexFormat->setAttribute(gpu::Stream::NORMAL, 0, NORMAL_ELEMENT, NORMAL_OFFSET); +} + +void FloorTextureTest::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + auto geometryCache = DependencyManager::get(); + static auto start = usecTimestampNow(); + auto now = usecTimestampNow(); + if ((now - start) > USECS_PER_SECOND * 1) { + start = now; + texture->incremementMinMip(); + } + + geometryCache->bindSimpleProgram(batch, true, true, true); + batch.setInputBuffer(0, vertexBuffer, 0, sizeof(Vertex)); + batch.setInputFormat(vertexFormat); + batch.setIndexBuffer(gpu::UINT16, indexBuffer, 0); + batch.setResourceTexture(0, texture); + batch.setModelTransform(glm::translate(glm::mat4(), vec3(0, -0.1, 0))); + batch.drawIndexed(gpu::TRIANGLES, 6, 0); +} diff --git a/tests/gpu-test/src/TestFloorTexture.h b/tests/gpu-test/src/TestFloorTexture.h new file mode 100644 index 0000000000..99eaf902b8 --- /dev/null +++ b/tests/gpu-test/src/TestFloorTexture.h @@ -0,0 +1,22 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#include "TestHelpers.h" + +class FloorTextureTest : public GpuTestBase { + gpu::BufferPointer vertexBuffer { std::make_shared() }; + gpu::BufferPointer indexBuffer { std::make_shared() }; + gpu::Stream::FormatPointer vertexFormat { std::make_shared() }; + gpu::TexturePointer texture; +public: + FloorTextureTest(); + void renderTest(size_t testId, RenderArgs* args) override; +}; + + diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index e672fe3c86..0f06546327 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -62,6 +62,9 @@ #include #include "TestWindow.h" +#include "TestFbx.h" +#include "TestFloorGrid.h" +#include "TestFloorTexture.h" #include "TestInstancedShapes.h" #include "TestShapes.h" @@ -90,8 +93,8 @@ class MyTestWindow : public TestWindow { #endif updateCamera(); _testBuilders = TestBuilders({ - //[this] { return new TestFbx(_shapePlumber); }, - [] { return new TestShapes(); }, + [this] { return new TestFbx(_shapePlumber); }, + [] { return new TestInstancedShapes(); }, }); } diff --git a/tests/gpu-test/src/unlit.slf b/tests/gpu-test/src/unlit.slf deleted file mode 100644 index f88fcb510b..0000000000 --- a/tests/gpu-test/src/unlit.slf +++ /dev/null @@ -1,28 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// simple.frag -// fragment shader -// -// Created by Andrzej Kapolka on 9/15/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include DeferredBufferWrite.slh@> -<@include model/Material.slh@> - -// the interpolated normal -in vec3 _normal; -in vec3 _color; - -void main(void) { - packDeferredFragment( - normalize(_normal.xyz), - 1.0, - _color.rgb, - DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION); -} diff --git a/tests/gpu-test/src/unlit.slv b/tests/gpu-test/src/unlit.slv deleted file mode 100644 index d51d817429..0000000000 --- a/tests/gpu-test/src/unlit.slv +++ /dev/null @@ -1,36 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// simple.vert -// vertex shader -// -// Created by Andrzej Kapolka on 9/15/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include gpu/Inputs.slh@> - -<@include gpu/Transform.slh@> - -<$declareStandardTransform()$> - -// the interpolated normal -out vec3 _normal; -out vec3 _color; -out vec2 _texCoord0; - -void main(void) { - _color = inColor.rgb; - _texCoord0 = inTexCoord0.st; - - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> - _normal = vec3(0.0, 0.0, 1.0); -} \ No newline at end of file From 8b56444ddeb0a78dba2258a92f7b8db413e62529 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 23 May 2016 14:47:16 -0700 Subject: [PATCH 0814/1237] Fix crash in GL45 buffer re-allocation --- libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp index b7bb04cbfb..1676b0ce1c 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -22,7 +22,9 @@ class GL45Buffer : public gl::GLBuffer { public: GL45Buffer(const Buffer& buffer, GLBuffer* original) : Parent(buffer, allocate()) { glNamedBufferStorage(_buffer, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); - glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); + if (original && original->_size) { + glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); + } Backend::setGPUObject(buffer, this); } From a41de3a60dc71932e4f1b9e3d6ab297fda5a0be8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 23 May 2016 15:46:47 -0700 Subject: [PATCH 0815/1237] Making GL 4.5 enabled by default, adding logging --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 32e063a4c6..60641c122d 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -32,17 +32,19 @@ using namespace gpu; using namespace gpu::gl; -static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); -bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +static const QString DEBUG_FLAG("HIFI_DISABLE_OPENGL_45"); +bool disableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); Backend* GLBackend::createBackend() { // FIXME provide a mechanism to override the backend for testing // Where the gpuContext is initialized and where the TRUE Backend is created and assigned auto version = QOpenGLContextWrapper::currentContextVersion(); GLBackend* result; - if (enableOpenGL45 && version >= 0x0405) { + if (!disableOpenGL45 && version >= 0x0405) { + qDebug() << "OpenGL 4.5 detected"; result = new gpu::gl45::GL45Backend(); } else { + qDebug() << "OpenGL 4.5 not detected / enabled, using compatibility backend"; result = new gpu::gl41::GL41Backend(); } result->initInput(); From ab7fed1af89a285e50e97cb7bd888800be522c0e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 31 May 2016 13:28:39 -0700 Subject: [PATCH 0816/1237] Switch back to disable GL 4.5 by default, add support for logging when enabled --- libraries/gl/src/gl/GLHelpers.cpp | 14 +++++++++++--- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 14 ++++++++++---- libraries/gl/src/gl/OffscreenGLCanvas.h | 3 --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 10 +++++----- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 79b39a2331..2781d5b9b0 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -6,6 +6,14 @@ #include #include #include +#include +#ifdef DEBUG +static bool enableDebug = true; +#else +static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); +static bool enableDebug = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +#endif + const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { static QSurfaceFormat format; @@ -15,9 +23,9 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS); format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS); setGLFormatVersion(format); -#ifdef DEBUG - format.setOption(QSurfaceFormat::DebugContext); -#endif + if (enableDebug) { + format.setOption(QSurfaceFormat::DebugContext); + } format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); QSurfaceFormat::setDefaultFormat(format); }); diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 90ff369cd6..eec3d2bf6b 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -12,23 +12,30 @@ #include "OffscreenGLCanvas.h" +#include #include #include #include #include "GLHelpers.h" +#ifdef DEBUG +static bool enableDebugLogger = true; +#else +static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); +static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +#endif + + OffscreenGLCanvas::OffscreenGLCanvas() : _context(new QOpenGLContext), _offscreenSurface(new QOffscreenSurface){ } OffscreenGLCanvas::~OffscreenGLCanvas() { -#ifdef DEBUG if (_logger) { makeCurrent(); delete _logger; _logger = nullptr; } -#endif _context->doneCurrent(); } @@ -60,7 +67,7 @@ bool OffscreenGLCanvas::makeCurrent() { qDebug() << "GL Renderer: " << QString((const char*) glGetString(GL_RENDERER)); }); -#ifdef DEBUG + if (result && !_logger) { _logger = new QOpenGLDebugLogger(this); if (_logger->initialize()) { @@ -71,7 +78,6 @@ bool OffscreenGLCanvas::makeCurrent() { _logger->startLogging(QOpenGLDebugLogger::LoggingMode::SynchronousLogging); } } -#endif return result; } diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h index 387804bf56..69210f638d 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.h +++ b/libraries/gl/src/gl/OffscreenGLCanvas.h @@ -36,10 +36,7 @@ protected: std::once_flag _reportOnce; QOpenGLContext* _context; QOffscreenSurface* _offscreenSurface; -#ifdef DEBUG QOpenGLDebugLogger* _logger{ nullptr }; -#endif - }; #endif // hifi_OffscreenGLCanvas_h diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 60641c122d..ce2f4c8d66 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -32,19 +32,19 @@ using namespace gpu; using namespace gpu::gl; -static const QString DEBUG_FLAG("HIFI_DISABLE_OPENGL_45"); -bool disableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); +static bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); Backend* GLBackend::createBackend() { // FIXME provide a mechanism to override the backend for testing // Where the gpuContext is initialized and where the TRUE Backend is created and assigned auto version = QOpenGLContextWrapper::currentContextVersion(); GLBackend* result; - if (!disableOpenGL45 && version >= 0x0405) { - qDebug() << "OpenGL 4.5 detected"; + if (enableOpenGL45 && version >= 0x0405) { + qDebug() << "Using OpenGL 4.5 backend"; result = new gpu::gl45::GL45Backend(); } else { - qDebug() << "OpenGL 4.5 not detected / enabled, using compatibility backend"; + qDebug() << "Using OpenGL 4.1 backend"; result = new gpu::gl41::GL41Backend(); } result->initInput(); From 542601fd9dafe4f219d9f3939f8a78cb9b7ea931 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 1 Jun 2016 13:58:54 -0700 Subject: [PATCH 0817/1237] Fix invalid GL 4.5 calls --- libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 4 ++-- libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index 7f511be3e8..b846dd4df3 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -93,9 +93,9 @@ public: // Last but not least, define where we draw if (!_colorBuffers.empty()) { - glDrawBuffers((GLsizei)_colorBuffers.size(), _colorBuffers.data()); + glNamedFramebufferDrawBuffers(_id, (GLsizei)_colorBuffers.size(), _colorBuffers.data()); } else { - glDrawBuffer(GL_NONE); + glNamedFramebufferDrawBuffer(_id, GL_NONE); } // Now check for completness diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp index fd49729164..96afb4cc71 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp @@ -19,7 +19,7 @@ void GL45Backend::initTransform() { _transform._objectBuffer = transformBuffers[0]; _transform._cameraBuffer = transformBuffers[1]; _transform._drawCallInfoBuffer = transformBuffers[2]; - glCreateTextures(1, GL_TEXTURE_2D, &_transform._objectBufferTexture); + glCreateTextures(GL_TEXTURE_BUFFER, 1, &_transform._objectBufferTexture); size_t cameraSize = sizeof(TransformStageState::CameraBufferElement); while (_transform._cameraUboSize < cameraSize) { _transform._cameraUboSize += _uboAlignment; @@ -59,9 +59,9 @@ void GL45Backend::transferTransformState(const Batch& batch) const { #ifdef GPU_SSBO_DRAW_CALL_INFO glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _transform._objectBuffer); #else + glTextureBuffer(_transform._objectBufferTexture, GL_RGBA32F, _transform._objectBuffer); glActiveTexture(GL_TEXTURE0 + TRANSFORM_OBJECT_SLOT); glBindTexture(GL_TEXTURE_BUFFER, _transform._objectBufferTexture); - glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, _transform._objectBuffer); #endif CHECK_GL_ERROR(); From f93a91a97ff442781b8d274256d022fcda058ec4 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 28 Jun 2016 19:18:16 -0700 Subject: [PATCH 0818/1237] Abandon seperate vertex formats for now --- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 2 +- .../gpu-gl/src/gpu/gl/GLBackendInput.cpp | 173 ++++++++++++++++++ .../gpu-gl/src/gpu/gl41/GL41BackendInput.cpp | 168 +---------------- .../gpu-gl/src/gpu/gl45/GL45BackendInput.cpp | 67 +------ 4 files changed, 176 insertions(+), 234 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 5255a6cb25..d27ec3808b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -186,7 +186,7 @@ protected: virtual void killInput() final; virtual void syncInputStateCache() final; virtual void resetInputStage() final; - virtual void updateInput() = 0; + virtual void updateInput(); struct InputStageState { bool _invalidFormat { true }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp index 448cc508eb..99c1ff0438 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp @@ -150,3 +150,176 @@ void GLBackend::do_setIndirectBuffer(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } + + +// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding +// Core 43 does :) +// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat +// Once resolved, break this up into the GL 4.1 and 4.5 backends +#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) +#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT +#else +#define SUPPORT_VERTEX_ATTRIB_FORMAT +#endif + +void GLBackend::updateInput() { +#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) + if (_input._invalidFormat) { + + InputStageState::ActivationCache newActivation; + + // Assign the vertex format required + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = _elementTypeToGL41Type[attrib._element.getType()]; + GLuint offset = attrib._offset;; + GLboolean isNormalized = attrib._element.isNormalized(); + + GLenum perLocationSize = attrib._element.getLocationSize(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + newActivation.set(slot + locNum); + glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); + glVertexAttribBinding(slot + locNum, attrib._channel); + } + glVertexBindingDivisor(attrib._channel, attrib._frequency); + } + (void)CHECK_GL_ERROR(); + } + + // Manage Activation what was and what is expected now + for (size_t i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + _input._attributeActivation.flip(i); + } + } + (void)CHECK_GL_ERROR(); + + _input._invalidFormat = false; + _stats._ISNumFormatChanges++; + } + + if (_input._invalidBuffers.any()) { + int numBuffers = _input._buffers.size(); + auto buffer = _input._buffers.data(); + auto vbo = _input._bufferVBOs.data(); + auto offset = _input._bufferOffsets.data(); + auto stride = _input._bufferStrides.data(); + + for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { + if (_input._invalidBuffers.test(bufferNum)) { + glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); + } + buffer++; + vbo++; + offset++; + stride++; + } + _input._invalidBuffers.reset(); + (void)CHECK_GL_ERROR(); + } +#else + if (_input._invalidFormat || _input._invalidBuffers.any()) { + + if (_input._invalidFormat) { + InputStageState::ActivationCache newActivation; + + _stats._ISNumFormatChanges++; + + // Check expected activation + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + uint8_t locationCount = attrib._element.getLocationCount(); + for (int i = 0; i < locationCount; ++i) { + newActivation.set(attrib._slot + i); + } + } + } + + // Manage Activation what was and what is expected now + for (unsigned int i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + (void)CHECK_GL_ERROR(); + + _input._attributeActivation.flip(i); + } + } + } + + // now we need to bind the buffers and assign the attrib pointers + if (_input._format) { + const Buffers& buffers = _input._buffers; + const Offsets& offsets = _input._bufferOffsets; + const Offsets& strides = _input._bufferStrides; + + const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); + auto& inputChannels = _input._format->getChannels(); + _stats._ISNumInputBufferChanges++; + + GLuint boundVBO = 0; + for (auto& channelIt : inputChannels) { + const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; + if ((channelIt).first < buffers.size()) { + int bufferNum = (channelIt).first; + + if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { + // GLuint vbo = gpu::GL41Backend::getBufferID((*buffers[bufferNum])); + GLuint vbo = _input._bufferVBOs[bufferNum]; + if (boundVBO != vbo) { + glBindBuffer(GL_ARRAY_BUFFER, vbo); + (void)CHECK_GL_ERROR(); + boundVBO = vbo; + } + _input._invalidBuffers[bufferNum] = false; + + for (unsigned int i = 0; i < channel._slots.size(); i++) { + const Stream::Attribute& attrib = attributes.at(channel._slots[i]); + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; + // GLenum perLocationStride = strides[bufferNum]; + GLenum perLocationStride = attrib._element.getLocationSize(); + GLuint stride = (GLuint)strides[bufferNum]; + GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); + GLboolean isNormalized = attrib._element.isNormalized(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, + reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); + glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); + } + + // TODO: Support properly the IAttrib version + + (void)CHECK_GL_ERROR(); + } + } + } + } + } + // everything format related should be in sync now + _input._invalidFormat = false; + } +#endif +} + diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp index 1b0caa7345..d3e86de606 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp @@ -13,173 +13,7 @@ using namespace gpu; using namespace gpu::gl41; -// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding -// Core 43 does :) -// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat -#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) -#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT -#else -#define SUPPORT_VERTEX_ATTRIB_FORMAT -#endif - void GL41Backend::updateInput() { -#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) - if (_input._invalidFormat) { - - InputStageState::ActivationCache newActivation; - - // Assign the vertex format required - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGL41Type[attrib._element.getType()]; - GLuint offset = attrib._offset;; - GLboolean isNormalized = attrib._element.isNormalized(); - - GLenum perLocationSize = attrib._element.getLocationSize(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - newActivation.set(slot + locNum); - glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); - glVertexAttribBinding(slot + locNum, attrib._channel); - } - glVertexBindingDivisor(attrib._channel, attrib._frequency); - } - (void) CHECK_GL_ERROR(); - } - - // Manage Activation what was and what is expected now - for (size_t i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - _input._attributeActivation.flip(i); - } - } - (void) CHECK_GL_ERROR(); - - _input._invalidFormat = false; - _stats._ISNumFormatChanges++; - } - - if (_input._invalidBuffers.any()) { - int numBuffers = _input._buffers.size(); - auto buffer = _input._buffers.data(); - auto vbo = _input._bufferVBOs.data(); - auto offset = _input._bufferOffsets.data(); - auto stride = _input._bufferStrides.data(); - - for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { - if (_input._invalidBuffers.test(bufferNum)) { - glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); - } - buffer++; - vbo++; - offset++; - stride++; - } - _input._invalidBuffers.reset(); - (void) CHECK_GL_ERROR(); - } -#else - if (_input._invalidFormat || _input._invalidBuffers.any()) { - - if (_input._invalidFormat) { - InputStageState::ActivationCache newActivation; - - _stats._ISNumFormatChanges++; - - // Check expected activation - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - uint8_t locationCount = attrib._element.getLocationCount(); - for (int i = 0; i < locationCount; ++i) { - newActivation.set(attrib._slot + i); - } - } - } - - // Manage Activation what was and what is expected now - for (unsigned int i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - (void) CHECK_GL_ERROR(); - - _input._attributeActivation.flip(i); - } - } - } - - // now we need to bind the buffers and assign the attrib pointers - if (_input._format) { - const Buffers& buffers = _input._buffers; - const Offsets& offsets = _input._bufferOffsets; - const Offsets& strides = _input._bufferStrides; - - const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); - auto& inputChannels = _input._format->getChannels(); - _stats._ISNumInputBufferChanges++; - - GLuint boundVBO = 0; - for (auto& channelIt : inputChannels) { - const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; - if ((channelIt).first < buffers.size()) { - int bufferNum = (channelIt).first; - - if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { - // GLuint vbo = gpu::GL41Backend::getBufferID((*buffers[bufferNum])); - GLuint vbo = _input._bufferVBOs[bufferNum]; - if (boundVBO != vbo) { - glBindBuffer(GL_ARRAY_BUFFER, vbo); - (void) CHECK_GL_ERROR(); - boundVBO = vbo; - } - _input._invalidBuffers[bufferNum] = false; - - for (unsigned int i = 0; i < channel._slots.size(); i++) { - const Stream::Attribute& attrib = attributes.at(channel._slots[i]); - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; - // GLenum perLocationStride = strides[bufferNum]; - GLenum perLocationStride = attrib._element.getLocationSize(); - GLuint stride = (GLuint)strides[bufferNum]; - GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); - GLboolean isNormalized = attrib._element.isNormalized(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, - reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); - glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); - } - - // TODO: Support properly the IAttrib version - - (void) CHECK_GL_ERROR(); - } - } - } - } - } - // everything format related should be in sync now - _input._invalidFormat = false; - } -#endif + Parent::updateInput(); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp index 5cefe40448..b578d7d7a8 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp @@ -14,70 +14,5 @@ using namespace gpu; using namespace gpu::gl45; void GL45Backend::updateInput() { - if (_input._invalidFormat) { - - InputStageState::ActivationCache newActivation; - - // Assign the vertex format required - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; - GLuint offset = (GLuint)attrib._offset;; - GLboolean isNormalized = attrib._element.isNormalized(); - - GLenum perLocationSize = attrib._element.getLocationSize(); - - for (uint8_t locNum = 0; locNum < locationCount; ++locNum) { - newActivation.set(slot + locNum); - glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); - glVertexAttribBinding(slot + locNum, attrib._channel); - } - glVertexBindingDivisor(attrib._channel, attrib._frequency); - } - (void) CHECK_GL_ERROR(); - } - - // Manage Activation what was and what is expected now - for (uint32_t i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - _input._attributeActivation.flip(i); - } - } - (void) CHECK_GL_ERROR(); - - _input._invalidFormat = false; - _stats._ISNumFormatChanges++; - } - - if (_input._invalidBuffers.any()) { - auto numBuffers = _input._buffers.size(); - auto buffer = _input._buffers.data(); - auto vbo = _input._bufferVBOs.data(); - auto offset = _input._bufferOffsets.data(); - auto stride = _input._bufferStrides.data(); - - for (uint32_t bufferNum = 0; bufferNum < numBuffers; bufferNum++) { - if (_input._invalidBuffers.test(bufferNum)) { - glBindVertexBuffer(bufferNum, (*vbo), (GLuint)(*offset), (GLuint)(*stride)); - } - buffer++; - vbo++; - offset++; - stride++; - } - _input._invalidBuffers.reset(); - (void) CHECK_GL_ERROR(); - } + Parent::updateInput(); } - From 52d017ac743a6f3eeba49e6d8c9c904a19814494 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 19:22:19 -0700 Subject: [PATCH 0819/1237] undo previous change --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 8857d6c70c..c92fb88f54 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -27,7 +27,7 @@ var WANT_DEBUG_SEARCH_NAME = null; var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.50; // Squeezed far enough to complete distant grab +var TRIGGER_GRAB_VALUE = 0.75; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; From 97e90ed798625bb9b06a81cefc32453a70cd310f Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Wed, 29 Jun 2016 03:45:54 +0100 Subject: [PATCH 0820/1237] TouchscreenDevice DPI scaling now handled via JSON mapping do the arbitrary scaling in the mapping file --- interface/resources/controllers/touchscreen.json | 4 ++-- .../input-plugins/src/input-plugins/TouchscreenDevice.cpp | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/interface/resources/controllers/touchscreen.json b/interface/resources/controllers/touchscreen.json index 21362ce8fb..5b2ff62a8d 100644 --- a/interface/resources/controllers/touchscreen.json +++ b/interface/resources/controllers/touchscreen.json @@ -9,7 +9,7 @@ [ "Touchscreen.DragRight" ] ] }, - "to": "Actions.Yaw" + "to": "Actions.Yaw", "filters": [ { "type": "scale", "scale": 0.12 } ] }, { "from": { "makeAxis" : [ @@ -17,7 +17,7 @@ [ "Touchscreen.DragDown" ] ] }, - "to": "Actions.Pitch" + "to": "Actions.Pitch", "filters": [ { "type": "scale", "scale": 0.04 } ] } ] } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index e9553c1785..271a3bb732 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -77,12 +77,6 @@ void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); KeyboardMouseDevice::enableTouch(false); if (_screenDPI != event->window()->screen()->physicalDotsPerInch()) { - // at DPI 100 use these arbitrary values to divide dragging distance - // the value is clamped from 1 to 10 so up to 1000 DPI would be supported atm - _screenDPIScale.x = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchX() / 100.0), 1.0f, 10.0f) - * 600.0f; - _screenDPIScale.y = glm::clamp((float)(qApp->primaryScreen()->physicalDotsPerInchY() / 100.0), 1.0f, 10.0f) - * 200.0f; _screenDPI = event->window()->screen()->physicalDotsPerInch(); } } From 2c56d29a68e508020941794f8a5bb17b65013d14 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Wed, 29 Jun 2016 03:47:20 +0100 Subject: [PATCH 0821/1237] git add seemed to miss a change use event window, not primaryScreen --- libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 271a3bb732..12e990757d 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -77,6 +77,8 @@ void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); KeyboardMouseDevice::enableTouch(false); if (_screenDPI != event->window()->screen()->physicalDotsPerInch()) { + _screenDPIScale.x = (float)event->window()->screen()->physicalDotsPerInchX(); + _screenDPIScale.y = (float)event->window()->screen()->physicalDotsPerInchY(); _screenDPI = event->window()->screen()->physicalDotsPerInch(); } } From 9b993b266528bee8a4b39c7e93b791ceb417ebff Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Wed, 29 Jun 2016 03:58:17 +0100 Subject: [PATCH 0822/1237] store pointer to event->window()->screen() save the planet! --- .../src/input-plugins/TouchscreenDevice.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 12e990757d..64f02b5df3 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -76,10 +76,11 @@ void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); KeyboardMouseDevice::enableTouch(false); - if (_screenDPI != event->window()->screen()->physicalDotsPerInch()) { - _screenDPIScale.x = (float)event->window()->screen()->physicalDotsPerInchX(); - _screenDPIScale.y = (float)event->window()->screen()->physicalDotsPerInchY(); - _screenDPI = event->window()->screen()->physicalDotsPerInch(); + QScreen* eventScreen = event->window()->screen(); + if (_screenDPI != eventScreen->physicalDotsPerInch()) { + _screenDPIScale.x = (float)eventScreen->physicalDotsPerInchX(); + _screenDPIScale.y = (float)eventScreen->physicalDotsPerInchY(); + _screenDPI = eventScreen->physicalDotsPerInch(); } } From b649b30777a86733c98a596c3899a2c27506c850 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Jun 2016 08:20:47 -0700 Subject: [PATCH 0823/1237] Reduce margin. --- scripts/system/controllers/handControllerPointer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 00aca2af86..36442e92d1 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -125,7 +125,7 @@ function ignoreMouseActivity() { weMovedReticle = false; return true; } -var MARGIN = 50; +var MARGIN = 25; var reticleMinX = MARGIN, reticleMaxX, reticleMinY = MARGIN, reticleMaxY; function updateRecommendedArea() { var dims = Controller.getViewportDimensions(); From 03ed36cf3303b41c63d94955b2532da28bb477f4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 09:13:52 -0700 Subject: [PATCH 0824/1237] take shape offset into account when getting avatar's capsule --- interface/src/avatar/Avatar.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 8fd330db19..4d9481f002 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1089,8 +1089,8 @@ void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { ShapeInfo shapeInfo; computeShapeInfo(shapeInfo); glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight - start = getPosition() - glm::vec3(0, halfExtents.y, 0); - end = getPosition() + glm::vec3(0, halfExtents.y, 0); + start = getPosition() - glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); + end = getPosition() + glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); radius = halfExtents.x; } From c3dbe5d9c43e23a84392442de5a036a4c0030b8a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 09:45:09 -0700 Subject: [PATCH 0825/1237] ray pick against avatar meshes rather than capsules --- interface/src/avatar/AvatarManager.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 0eba27ee78..e1fe03ae2e 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -425,13 +425,20 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& } float distance; + BoxFace face; + glm::vec3 surfaceNormal; - glm::vec3 start; - glm::vec3 end; - float radius; - avatar->getCapsule(start, end, radius); + SkeletonModelPointer avatarModel = avatar->getSkeletonModel(); + AABox avatarBounds = avatarModel->getRenderableMeshBound(); + if (!avatarBounds.findRayIntersection(ray.origin, normDirection, distance, face, surfaceNormal)) { + // ray doesn't intersect avatar's bounding-box + continue; + } + + QString extraInfo; + bool intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, + distance, face, surfaceNormal, extraInfo, true); - bool intersects = findRayCapsuleIntersection(ray.origin, normDirection, start, end, radius, distance); if (intersects && (!result.intersects || distance < result.distance)) { result.intersects = true; result.avatarID = avatar->getID(); From 9114ebb548aa2384fb16584c197704a272483511 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 09:53:26 -0700 Subject: [PATCH 0826/1237] remove unused code --- interface/src/avatar/Avatar.cpp | 9 --------- interface/src/avatar/Avatar.h | 1 - 2 files changed, 10 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 4d9481f002..39bb7eac17 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1085,15 +1085,6 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { shapeInfo.setOffset(uniformScale * _skeletonModel->getBoundingCapsuleOffset()); } -void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { - ShapeInfo shapeInfo; - computeShapeInfo(shapeInfo); - glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight - start = getPosition() - glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); - end = getPosition() + glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); - radius = halfExtents.x; -} - void Avatar::setMotionState(AvatarMotionState* motionState) { _motionState = motionState; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index b9f44613c7..064f0a9533 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -154,7 +154,6 @@ public: virtual void rebuildCollisionShape(); virtual void computeShapeInfo(ShapeInfo& shapeInfo); - void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); AvatarMotionState* getMotionState() { return _motionState; } From 5a9be817df1fc30898ca988efb5607150d8c93ce Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 29 Jun 2016 10:28:55 -0700 Subject: [PATCH 0827/1237] PR comments --- interface/resources/qml/ToolWindow.qml | 1 - libraries/shared/src/GLMHelpers.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index c957a37886..f6a4600e06 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -140,7 +140,6 @@ ScrollingWindow { if (visible) { for (var i = 0; i < tabView.count; ++i) { if (tabView.getTab(i).enabled) { - console.log("QQQ tab is visible, returning early"); return; } } diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 8dbd0a146f..0c02cd5b03 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -468,7 +468,7 @@ glm::mat4 createMatFromScaleQuatAndPos(const glm::vec3& scale, const glm::quat& // cancel out roll glm::quat cancelOutRoll(const glm::quat& q) { - glm::vec3 forward = q * glm::vec3(0.0f, 0.0f, -1.0f); + glm::vec3 forward = q * Vectors::FRONT; return glm::quat_cast(glm::inverse(glm::lookAt(Vectors::ZERO, forward, Vectors::UP))); } From fcb6e7869dce8583dca423fc94a0acafd44b757e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 29 Jun 2016 10:54:24 -0700 Subject: [PATCH 0828/1237] logging --- .../DomainContent/Toybox/bow/bow.js | 5 ++- .../DomainContent/Toybox/bow/createBow.js | 41 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/unpublishedScripts/DomainContent/Toybox/bow/bow.js b/unpublishedScripts/DomainContent/Toybox/bow/bow.js index 2430b68076..02854e4de4 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/bow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/bow.js @@ -484,8 +484,9 @@ Entities.editEntity(this.arrow, arrowProperties); this.playShootArrowSound(); - var whichHand = this.hand === 'left' ? 0 : 1; - Controller.triggerShortHapticPulse(whichHand, 2); + var backHand = this.hand === 'left' ? 1 : 0; + var haptic = Controller.triggerShortHapticPulse(1, backHand); + print('JBP TRIGGERED HAPTIC ' + haptic +" hand: "+ backHand) //clear the strings back to only the single straight one this.deleteStrings(); diff --git a/unpublishedScripts/DomainContent/Toybox/bow/createBow.js b/unpublishedScripts/DomainContent/Toybox/bow/createBow.js index 5deec3f6bc..4daeba320a 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/createBow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/createBow.js @@ -62,24 +62,35 @@ function makeBow() { shapeType: 'compound', compoundShapeURL: COLLISION_HULL_URL, script: SCRIPT_URL, + collidesWith: 'dynamic,kinetmatic,static', userData: JSON.stringify({ grabbableKey: { invertSolidWhileHeld: true }, - wearable:{joints:{RightHand:[{x:0.03960523009300232, - y:0.01979270577430725, - z:0.03294898942112923}, - {x:-0.7257906794548035, - y:-0.4611682891845703, - z:0.4436084032058716, - w:-0.25251442193984985}], - LeftHand:[{x:0.0055799782276153564, - y:0.04354757443070412, - z:0.05119767785072327}, - {x:-0.14914104342460632, - y:0.6448180079460144, - z:-0.2888556718826294, - w:-0.6917579770088196}]}} + wearable: { + joints: { + RightHand: [{ + x: 0.03960523009300232, + y: 0.01979270577430725, + z: 0.03294898942112923 + }, { + x: -0.7257906794548035, + y: -0.4611682891845703, + z: 0.4436084032058716, + w: -0.25251442193984985 + }], + LeftHand: [{ + x: 0.0055799782276153564, + y: 0.04354757443070412, + z: 0.05119767785072327 + }, { + x: -0.14914104342460632, + y: 0.6448180079460144, + z: -0.2888556718826294, + w: -0.6917579770088196 + }] + } + } }) }; @@ -148,4 +159,4 @@ function cleanup() { Entities.deleteEntity(preNotchString); } -Script.scriptEnding.connect(cleanup); +Script.scriptEnding.connect(cleanup); \ No newline at end of file From af2a0ed924865686546f24f8e4da84aa5055eeac Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 29 Jun 2016 11:02:43 -0700 Subject: [PATCH 0829/1237] preserve preview aspect ratio, fix device pixel ratio scaling --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 48 ++++++++++--------- .../display-plugins/hmd/HmdDisplayPlugin.h | 1 + 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 8f8132926f..cbd0026d54 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -97,6 +97,7 @@ void HmdDisplayPlugin::downloadFinished() { Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); glBindTexture(GL_TEXTURE_2D, 0); + _previewAspect = ((float)previewTexture.width())/((float)previewTexture.height()); _firstPreview = true; } } @@ -396,30 +397,31 @@ void HmdDisplayPlugin::internalPresent() { // screen preview mirroring auto window = _container->getPrimaryWidget(); - auto windowSize = toGlm(window->size()); auto devicePixelRatio = window->devicePixelRatio(); + auto windowSize = toGlm(window->size()); + windowSize *= devicePixelRatio; + float windowAspect = aspect(windowSize); + float sceneAspect = _enablePreview ? aspect(_renderTargetSize) : _previewAspect; + if (_enablePreview && _monoPreview) { + sceneAspect /= 2.0f; + } + float aspectRatio = sceneAspect / windowAspect; + + uvec2 targetViewportSize = windowSize; + if (aspectRatio < 1.0f) { + targetViewportSize.x *= aspectRatio; + } else { + targetViewportSize.y /= aspectRatio; + } + + uvec2 targetViewportPosition; + if (targetViewportSize.x < windowSize.x) { + targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; + } else if (targetViewportSize.y < windowSize.y) { + targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; + } + if (_enablePreview) { - float windowAspect = aspect(windowSize); - float sceneAspect = aspect(_renderTargetSize); - if (_monoPreview) { - sceneAspect /= 2.0f; - } - float aspectRatio = sceneAspect / windowAspect; - - uvec2 targetViewportSize = windowSize; - if (aspectRatio < 1.0f) { - targetViewportSize.x *= aspectRatio; - } else { - targetViewportSize.y /= aspectRatio; - } - - uvec2 targetViewportPosition; - if (targetViewportSize.x < windowSize.x) { - targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; - } else if (targetViewportSize.y < windowSize.y) { - targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; - } - using namespace oglplus; Context::Clear().ColorBuffer(); auto sourceSize = _compositeFramebuffer->size; @@ -438,7 +440,7 @@ void HmdDisplayPlugin::internalPresent() { useProgram(_previewProgram); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); - glViewport(0, 0, windowSize.x * devicePixelRatio, windowSize.y * devicePixelRatio); + glViewport(targetViewportPosition.x, targetViewportPosition.y, targetViewportSize.x, targetViewportSize.y); glUniform1i(PREVIEW_TEXTURE_LOCATION, 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _previewTextureID); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 374b41727b..dfab3e8d84 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -101,6 +101,7 @@ private: GLuint _previewTextureID { 0 }; glm::uvec2 _prevWindowSize { 0, 0 }; qreal _prevDevicePixelRatio { 0 }; + float _previewAspect { 0 }; ShapeWrapperPtr _sphereSection; ProgramPtr _reprojectionProgram; ProgramPtr _laserProgram; From 0e3b54c1303b26b8663b27eb32574b25fb25e2c9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 29 Jun 2016 11:07:48 -0700 Subject: [PATCH 0830/1237] remove logging --- unpublishedScripts/DomainContent/Toybox/bow/bow.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/unpublishedScripts/DomainContent/Toybox/bow/bow.js b/unpublishedScripts/DomainContent/Toybox/bow/bow.js index 02854e4de4..89a3d166d6 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/bow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/bow.js @@ -143,7 +143,6 @@ //disable the opposite hand in handControllerGrab.js by message var handToDisable = this.hand === 'right' ? 'left' : 'right'; - print("disabling hand: " + handToDisable); Messages.sendMessage('Hifi-Hand-Disabler', handToDisable); var data = getEntityCustomData('grabbableKey', this.entityID, {}); @@ -202,7 +201,7 @@ }, createArrow: function() { - print('create arrow') + // print('create arrow') this.playArrowNotchSound(); var arrow = Entities.addEntity({ @@ -486,7 +485,6 @@ var backHand = this.hand === 'left' ? 1 : 0; var haptic = Controller.triggerShortHapticPulse(1, backHand); - print('JBP TRIGGERED HAPTIC ' + haptic +" hand: "+ backHand) //clear the strings back to only the single straight one this.deleteStrings(); From 6d3129899beadd857582a00ee395be3733ca9142 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 29 Jun 2016 11:47:41 -0700 Subject: [PATCH 0831/1237] remove prints, fix typo, formatting --- .../DomainContent/Toybox/bow/bow.js | 43 ++++++++----------- .../DomainContent/Toybox/bow/createBow.js | 2 +- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/unpublishedScripts/DomainContent/Toybox/bow/bow.js b/unpublishedScripts/DomainContent/Toybox/bow/bow.js index 89a3d166d6..0c16bcbc7b 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/bow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/bow.js @@ -183,7 +183,6 @@ }, releaseEquip: function(entityID, args) { - // print('RELEASE GRAB EVENT') Messages.sendMessage('Hifi-Hand-Disabler', "none") this.stringDrawn = false; @@ -201,7 +200,6 @@ }, createArrow: function() { - // print('create arrow') this.playArrowNotchSound(); var arrow = Entities.addEntity({ @@ -226,25 +224,24 @@ var makeArrowStick = function(entityA, entityB, collision) { Entities.editEntity(entityA, { - angularVelocity: { - x: 0, - y: 0, - z: 0 - }, - velocity: { - x: 0, - y: 0, - z: 0 - }, - gravity: { - x: 0, - y: 0, - z: 0 - }, - position: collision.contactPoint, - dynamic: false - }) - // print('ARROW COLLIDED WITH::' + entityB); + angularVelocity: { + x: 0, + y: 0, + z: 0 + }, + velocity: { + x: 0, + y: 0, + z: 0 + }, + gravity: { + x: 0, + y: 0, + z: 0 + }, + position: collision.contactPoint, + dynamic: false + }) Script.removeEventHandler(arrow, "collisionWithEntity", makeArrowStick) } @@ -300,7 +297,6 @@ }, updateStringPositions: function() { - // print('update string positions!!!') var upVector = Quat.getUp(this.bowProperties.rotation); var upOffset = Vec3.multiply(upVector, TOP_NOTCH_OFFSET); var downVector = Vec3.multiply(-1, Quat.getUp(this.bowProperties.rotation)); @@ -371,7 +367,6 @@ if (this.triggerValue < DRAW_STRING_THRESHOLD && this.stringDrawn === true) { // firing the arrow - // print('HIT RELEASE LOOP IN CHECK'); this.drawStrings(); this.hasArrowNotched = false; @@ -381,7 +376,6 @@ } else if (this.triggerValue > DRAW_STRING_THRESHOLD && this.stringDrawn === true) { - // print('HIT CONTINUE LOOP IN CHECK') //continuing to aim the arrow this.aiming = true; @@ -389,7 +383,6 @@ this.updateArrowPositionInNotch(); } else if (this.triggerValue > DRAW_STRING_THRESHOLD && this.stringDrawn === false) { - // print('HIT START LOOP IN CHECK'); this.arrow = this.createArrow(); this.playStringPullSound(); diff --git a/unpublishedScripts/DomainContent/Toybox/bow/createBow.js b/unpublishedScripts/DomainContent/Toybox/bow/createBow.js index 4daeba320a..f1ed9eb263 100644 --- a/unpublishedScripts/DomainContent/Toybox/bow/createBow.js +++ b/unpublishedScripts/DomainContent/Toybox/bow/createBow.js @@ -62,7 +62,7 @@ function makeBow() { shapeType: 'compound', compoundShapeURL: COLLISION_HULL_URL, script: SCRIPT_URL, - collidesWith: 'dynamic,kinetmatic,static', + collidesWith: 'dynamic,kinematic,static', userData: JSON.stringify({ grabbableKey: { invertSolidWhileHeld: true From d53c7ae5d8021ffc11145fceb684539f2f789257 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 12:39:04 -0700 Subject: [PATCH 0832/1237] experimenting --- libraries/render-utils/src/Model.cpp | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 0470a238fc..f947474aac 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -456,6 +456,15 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { if (pickAgainstTriangles) { QVector thisMeshTriangles; + + glm::mat4 meshTransform; + if (mesh.clusters.size() > 0) { + int jointIndex = mesh.clusters[0].jointIndex; + meshTransform = _rig->getJointTransform(jointIndex); + } else { + meshTransform = mesh.modelTransform; + } + for (int j = 0; j < mesh.parts.size(); j++) { const FBXMeshPart& part = mesh.parts.at(j); @@ -474,10 +483,10 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 mv3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); // track the mesh parts in model space if (!atLeastOnePointInBounds) { @@ -517,9 +526,9 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); // track the mesh parts in model space if (!atLeastOnePointInBounds) { From 80fceccb7e6042ab0fdc5c9c06a9cc51516569a6 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 29 Jun 2016 12:43:39 -0700 Subject: [PATCH 0833/1237] simplify hours parsing in domain metadata --- domain-server/src/DomainMetadata.cpp | 43 +++++++++++----------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 6a17bff4c0..3f91c118e9 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -50,8 +50,8 @@ const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; // "tags": [ String ], // capped list of tags // "hours": { // "utc_offset": Number, -// "weekday": [ { "open": Time, "close": Time } ], -// "weekend": [ { "open": Time, "close": Time } ], +// "weekday": [ [ Time, Time ] ], +// "weekend": [ [ Time, Time ] ], // } // } @@ -65,13 +65,10 @@ const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; // delta should be of the form [ { open: Time, close: Time } ] void parseHours(QVariant delta, QVariant& target) { using Hours = DomainMetadata::Descriptors::Hours; - - assert(target.canConvert()); - auto& targetList = *static_cast(target.data()); - - // if/when multiple ranges are allowed, this list will need to be iterated - assert(targetList[0].canConvert()); - auto& hours = *static_cast(targetList[0].data()); + static const QVariantList DEFAULT_HOURS{ + QVariantList{ "00:00", "23:59" } + }; + target.setValue(DEFAULT_HOURS); if (!delta.canConvert()) { return; @@ -83,25 +80,21 @@ void parseHours(QVariant delta, QVariant& target) { } auto& deltaHours = *static_cast(deltaList.first().data()); - if (deltaHours.isEmpty()) { + auto open = deltaHours.find(Hours::OPEN); + auto close = deltaHours.find(Hours::CLOSE); + if (open == deltaHours.end() || close == deltaHours.end()) { return; } - // merge delta into base + // merge delta into new hours static const int OPEN_INDEX = 0; static const int CLOSE_INDEX = 1; - auto open = deltaHours.find(Hours::OPEN); - if (open != deltaHours.end()) { - hours[OPEN_INDEX] = open.value(); - } + auto& hours = *static_cast(static_cast(target.data())->first().data()); + hours[OPEN_INDEX] = open.value(); + hours[CLOSE_INDEX] = close.value(); + assert(hours[OPEN_INDEX].canConvert()); - - auto close = deltaHours.find(Hours::CLOSE); - if (close != deltaHours.end()) { - hours[CLOSE_INDEX] = close.value(); - } assert(hours[CLOSE_INDEX].canConvert()); - } DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { @@ -109,12 +102,8 @@ DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { _metadata[USERS] = QVariantMap {}; _metadata[DESCRIPTORS] = QVariantMap { { Descriptors::HOURS, QVariantMap { - { Descriptors::Hours::WEEKDAY, QVariantList { - QVariantList{ QVariant{}, QVariant{} } } - }, - { Descriptors::Hours::WEEKEND, QVariantList { - QVariantList{ QVariant{}, QVariant{} } } - } + { Descriptors::Hours::WEEKDAY, QVariant{} }, + { Descriptors::Hours::WEEKEND, QVariant{} } } } }; From 1b87e902de0c66dd2aea0eaef336a95abc50c720 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 29 Jun 2016 13:23:52 -0700 Subject: [PATCH 0834/1237] fix initialization of DEFAULT_HOURS in DomainMetadata --- domain-server/src/DomainMetadata.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 3f91c118e9..d2762e788b 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -66,7 +66,7 @@ const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; void parseHours(QVariant delta, QVariant& target) { using Hours = DomainMetadata::Descriptors::Hours; static const QVariantList DEFAULT_HOURS{ - QVariantList{ "00:00", "23:59" } + { QVariantList{ "00:00", "23:59" } } }; target.setValue(DEFAULT_HOURS); From 15d09bb4fa4aecc8c8ed1b92bc2f1fb6064167fe Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 29 Jun 2016 13:33:33 -0700 Subject: [PATCH 0835/1237] cleanup, fixed texture corruption bug --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 32 +++++++++---------- .../display-plugins/hmd/HmdDisplayPlugin.h | 10 +++--- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index cbd0026d54..ca6a03f93b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -84,20 +84,10 @@ void HmdDisplayPlugin::downloadFinished() { return; } - QImage previewTexture; - previewTexture.loadFromData(reply->readAll()); + _previewTexture.loadFromData(reply->readAll()); - if (!previewTexture.isNull()) { - previewTexture = previewTexture.mirrored(false, true); - glGenTextures(1, &_previewTextureID); - glBindTexture(GL_TEXTURE_2D, _previewTextureID); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, previewTexture.width(), previewTexture.height(), 0, - GL_BGRA, GL_UNSIGNED_BYTE, previewTexture.bits()); - using namespace oglplus; - Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); - Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); - glBindTexture(GL_TEXTURE_2D, 0); - _previewAspect = ((float)previewTexture.width())/((float)previewTexture.height()); + if (!_previewTexture.isNull()) { + _previewAspect = ((float)_previewTexture.width())/((float)_previewTexture.height()); _firstPreview = true; } } @@ -286,7 +276,7 @@ void HmdDisplayPlugin::customizeContext() { using namespace oglplus; if (!_enablePreview) { - std::string version("#version 410 core\n"); + const std::string version("#version 410 core\n"); compileProgram(_previewProgram, version + DrawUnitQuadTexcoord_vert, version + DrawTexture_frag); PREVIEW_TEXTURE_LOCATION = Uniform(*_previewProgram, "colorMap").Location(); } @@ -436,7 +426,18 @@ void HmdDisplayPlugin::internalPresent() { BufferSelectBit::ColorBuffer, BlitFilter::Nearest); }); swapBuffers(); - } else if (_previewTextureID != 0 && (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio)) { + } else if (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio) { + if (_firstPreview) { + glGenTextures(1, &_previewTextureID); + glBindTexture(GL_TEXTURE_2D, _previewTextureID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _previewTexture.width(), _previewTexture.height(), 0, + GL_BGRA, GL_UNSIGNED_BYTE, _previewTexture.mirrored(false, true).bits()); + using namespace oglplus; + Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); + Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); + glBindTexture(GL_TEXTURE_2D, 0); + _firstPreview = false; + } useProgram(_previewProgram); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -446,7 +447,6 @@ void HmdDisplayPlugin::internalPresent() { glBindTexture(GL_TEXTURE_2D, _previewTextureID); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); swapBuffers(); - _firstPreview = false; _prevWindowSize = windowSize; _prevDevicePixelRatio = devicePixelRatio; } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index dfab3e8d84..61b352b17b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -14,8 +14,6 @@ #include "../OpenGLDisplayPlugin.h" -class QNetworkReply; - class HmdDisplayPlugin : public OpenGLDisplayPlugin { Q_OBJECT using Parent = OpenGLDisplayPlugin; @@ -97,13 +95,17 @@ private: bool _monoPreview { true }; bool _enableReprojection { true }; bool _firstPreview { true }; + ProgramPtr _previewProgram; + QImage _previewTexture; + float _previewAspect { 0 }; GLuint _previewTextureID { 0 }; glm::uvec2 _prevWindowSize { 0, 0 }; qreal _prevDevicePixelRatio { 0 }; - float _previewAspect { 0 }; - ShapeWrapperPtr _sphereSection; + ProgramPtr _reprojectionProgram; + ShapeWrapperPtr _sphereSection; + ProgramPtr _laserProgram; ShapeWrapperPtr _laserGeometry; }; From b0422106373e13dcf22ca181fde8853c0a5edf3e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 24 Jun 2016 15:08:55 -0700 Subject: [PATCH 0836/1237] Addition of equipHotspots --- .../system/controllers/handControllerGrab.js | 103 ++++++++++++++---- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f86ff158f6..e971450aff 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -324,6 +324,14 @@ EntityPropertiesCache.prototype.getWearableProps = function(entityID) { return undefined; } }; +EntityPropertiesCache.prototype.getEquipHotspotsProps = function(entityID) { + var props = this.cache[entityID]; + if (props) { + return props.userData.equipHotspots ? props.userData.equipHotspots : {}; + } else { + return undefined; + } +}; function MyController(hand) { this.hand = hand; @@ -1083,8 +1091,8 @@ function MyController(hand) { }; this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition) { + var distance, handJointName; var props = this.entityPropertyCache.getProps(entityID); - var distance = Vec3.distance(props.position, handPosition); var grabProps = this.entityPropertyCache.getGrabProps(entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1096,30 +1104,81 @@ function MyController(hand) { return false; } - if (distance > EQUIP_RADIUS) { - if (debug) { - print("equip is skipping '" + props.name + "': too far away, " + distance + " meters"); - } - return false; - } + var equipHotspotsProps = this.entityPropertyCache.getEquipHotspotsProps(entityID); + if (equipHotspotsProps && equipHotspotsProps.length > 0) { + var i, length = equipHotspotsProps.length; + for (i = 0; i < length; i++) { + var hotspot = equipHotspotsProps[i]; + if (!hotspot.position) { + if (debug) { + print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing a position"); + } + continue; + } - var wearableProps = this.entityPropertyCache.getWearableProps(entityID); - if (!wearableProps || !wearableProps.joints) { - if (debug) { - print("equip is skipping '" + props.name + "': no wearable attach-point"); - } - return false; - } + if (!hotspot.radius) { + if (debug) { + print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing a radius"); + } + continue; + } - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (!wearableProps.joints[handJointName]) { - if (debug) { - print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName); - } - return false; - } + distance = Vec3.distance(hotspot.position, handPosition); + if (distance > hotspot.radius) { + if (debug) { + print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is too far away, " + distance + " meters"); + } + continue; + } - return true; + if (!hotspot.joints) { + if (debug) { + print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing joints"); + } + continue; + } + + handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (!hotspot.joints[handJointName]) { + if (debug) { + print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing joint." + handJointName); + } + continue; + } + + return true; + } + + return false; + + } else { + + distance = Vec3.distance(props.position, handPosition); + if (distance > EQUIP_RADIUS) { + if (debug) { + print("equip is skipping '" + props.name + "': too far away, " + distance + " meters"); + } + return false; + } + + var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + if (!wearableProps || !wearableProps.joints) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable attach-point"); + } + return false; + } + + handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (!wearableProps.joints[handJointName]) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName); + } + return false; + } + + return true; + } }; this.entityIsGrabbable = function (entityID) { From 1b98c7347324c78aa069004c6dc47105e282ea0b Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 24 Jun 2016 18:08:25 -0700 Subject: [PATCH 0837/1237] WIP, equip-points work but they don't use the proper attach point. --- .../system/controllers/handControllerGrab.js | 177 ++++++++---------- scripts/system/libraries/Xform.js | 8 + 2 files changed, 82 insertions(+), 103 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index e971450aff..c814b4396f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,9 +10,10 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, Xform */ Script.include("/~/system/libraries/utils.js"); +Script.include("../libraries/Xform.js"); // // add lines where the hand ray picking is happening @@ -931,7 +932,8 @@ function MyController(hand) { this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, - type: "hand" + type: "hand", + localPosition: {x: 0, y: 0, z: 0} }); } @@ -941,35 +943,38 @@ function MyController(hand) { var _this = this; this.entityPropertyCache.getEntities().forEach(function (entityID) { + var props = _this.entityPropertyCache.getProps(entityID); if (_this.entityIsEquippableWithoutDistanceCheck(entityID)) { - props = _this.entityPropertyCache.getProps(entityID); + var equipHotspots = _this.collectEquipHotspots(entityID); + var entityXform = new Xform(props.rotation, props.position); + var i, length = equipHotspots.length; + for (i = 0; i < length; i++) { + var hotspot = equipHotspots[i]; + overlay = Overlays.addOverlay("sphere", { + position: entityXform.xformPoint(hotspot.position), + size: hotspot.radius * 2, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); - overlay = Overlays.addOverlay("sphere", { - rotation: props.rotation, - position: props.position, - size: EQUIP_RADIUS * 2, - color: EQUIP_SPHERE_COLOR, - alpha: EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - - _this.hotspotOverlays.push({ - entityID: entityID, - overlay: overlay, - type: "equip" - }); + _this.hotspotOverlays.push({ + entityID: entityID, + overlay: overlay, + type: "equip", + localPosition: hotspot.position + }); + } } if (DRAW_GRAB_BOXES && _this.entityIsGrabbable(entityID)) { - props = _this.entityPropertyCache.getProps(entityID); - overlay = Overlays.addOverlay("cube", { rotation: props.rotation, position: props.position, - size: props.dimensions, //{x: props.dimensions.x, y: props.dimensions.y, z: props.dimensions.z}, + size: props.dimensions, color: GRAB_BOX_COLOR, alpha: GRAB_BOX_ALPHA, solid: true, @@ -981,7 +986,8 @@ function MyController(hand) { _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, - type: "near" + type: "near", + localPosition: {x: 0, y: 0, z: 0} }); } }); @@ -996,7 +1002,8 @@ function MyController(hand) { } else if (overlayInfo.type === "equip") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + var entityXform = new Xform(props.rotation, props.position); + Overlays.editOverlay(overlayInfo.overlay, { position: entityXform.xformPoint(overlayInfo.localPosition), rotation: props.rotation }); } else if (overlayInfo.type === "near") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); @@ -1084,17 +1091,41 @@ function MyController(hand) { return grabbableProps && grabbableProps.wantsTrigger; }; - this.entityIsEquippableWithoutDistanceCheck = function (entityID) { - var props = this.entityPropertyCache.getProps(entityID); - var handPosition = props.position; - return this.entityIsEquippableWithDistanceCheck(entityID, handPosition); + this.collectEquipHotspots = function (entityID) { + var result = []; + var equipHotspotsProps = this.entityPropertyCache.getEquipHotspotsProps(entityID); + if (equipHotspotsProps && equipHotspotsProps.length > 0) { + var i, length = equipHotspotsProps.length; + for (i = 0; i < length; i++) { + var hotspot = equipHotspotsProps[i]; + if (hotspot.position && hotspot.radius && hotspot.joints) { + result.push({ + position: hotspot.position, + radius: hotspot.radius, + joints: hotspot.joints + }); + } + } + } else { + var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + if (wearableProps && wearableProps.joints) { + result.push({ + position: {x: 0, y: 0, z: 0}, + radius: EQUIP_RADIUS, + joints: wearableProps.joints + }); + } + } + return result; }; - this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition) { - var distance, handJointName; + this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition, skipDistanceCheck) { + var distance; + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; var props = this.entityPropertyCache.getProps(entityID); var grabProps = this.entityPropertyCache.getGrabProps(entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + var entityXform = new Xform(props.rotation, props.position); var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; if (refCount > 0) { @@ -1104,81 +1135,21 @@ function MyController(hand) { return false; } - var equipHotspotsProps = this.entityPropertyCache.getEquipHotspotsProps(entityID); - if (equipHotspotsProps && equipHotspotsProps.length > 0) { - var i, length = equipHotspotsProps.length; - for (i = 0; i < length; i++) { - var hotspot = equipHotspotsProps[i]; - if (!hotspot.position) { - if (debug) { - print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing a position"); - } - continue; - } - - if (!hotspot.radius) { - if (debug) { - print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing a radius"); - } - continue; - } - - distance = Vec3.distance(hotspot.position, handPosition); - if (distance > hotspot.radius) { - if (debug) { - print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is too far away, " + distance + " meters"); - } - continue; - } - - if (!hotspot.joints) { - if (debug) { - print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing joints"); - } - continue; - } - - handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (!hotspot.joints[handJointName]) { - if (debug) { - print("equip is skipping '" + props.name + "': equip hotspot[" + i + "] is missing joint." + handJointName); - } - continue; - } - + var equipHotspots = this.collectEquipHotspots(entityID); + var i, length = equipHotspots.length; + for (i = 0; i < length; i++) { + var hotspot = equipHotspots[i]; + distance = Vec3.distance(entityXform.xformPoint(hotspot.position), handPosition); + if ((skipDistanceCheck || distance < hotspot.radius) && hotspot.joints[handJointName]) { return true; } - - return false; - - } else { - - distance = Vec3.distance(props.position, handPosition); - if (distance > EQUIP_RADIUS) { - if (debug) { - print("equip is skipping '" + props.name + "': too far away, " + distance + " meters"); - } - return false; - } - - var wearableProps = this.entityPropertyCache.getWearableProps(entityID); - if (!wearableProps || !wearableProps.joints) { - if (debug) { - print("equip is skipping '" + props.name + "': no wearable attach-point"); - } - return false; - } - - handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (!wearableProps.joints[handJointName]) { - if (debug) { - print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName); - } - return false; - } - - return true; } + return false; + }; + + this.entityIsEquippableWithoutDistanceCheck = function (entityID) { + var handPosition = this.getHandPosition(); + return this.entityIsEquippableWithDistanceCheck(entityID, handPosition, true); }; this.entityIsGrabbable = function (entityID) { diff --git a/scripts/system/libraries/Xform.js b/scripts/system/libraries/Xform.js index 75051c4983..1aa43cf30b 100644 --- a/scripts/system/libraries/Xform.js +++ b/scripts/system/libraries/Xform.js @@ -33,6 +33,14 @@ Xform.prototype.mirrorX = function() { {x: -this.pos.x, y: this.pos.y, z: this.pos.z}); }; +Xform.prototype.xformVector = function (vector) { + return Vec3.multiplyQbyV(this.rot, vector); +} + +Xform.prototype.xformPoint = function (point) { + return Vec3.sum(Vec3.multiplyQbyV(this.rot, point), this.pos); +} + Xform.prototype.toString = function() { var rot = this.rot; var pos = this.pos; From 43d4dba4c0323d40bed0186269a6456995607ad9 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 27 Jun 2016 18:05:45 -0700 Subject: [PATCH 0838/1237] iterate over hotspots not entities. This makes it possible to render multiple hotspots per entity. Also, it will use the same logic to decide how to deal with overlapping entity equip hotspots. --- .../system/controllers/handControllerGrab.js | 145 +++++++++--------- scripts/system/libraries/utils.js | 8 +- 2 files changed, 78 insertions(+), 75 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index c814b4396f..fe95dda596 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,7 +10,7 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, Xform */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, flatten, Xform */ Script.include("/~/system/libraries/utils.js"); Script.include("../libraries/Xform.js"); @@ -879,7 +879,7 @@ function MyController(hand) { }; this.createHotspots = function () { - var props, overlay; + var _this = this; var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; var HAND_EQUIP_SPHERE_ALPHA = 0.7; @@ -897,6 +897,7 @@ function MyController(hand) { this.hotspotOverlays = []; + var overlay; if (DRAW_HAND_SPHERES) { // add tiny green sphere around the palm. var handPosition = this.getHandPosition(); @@ -910,7 +911,6 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, @@ -928,7 +928,6 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, @@ -941,55 +940,54 @@ function MyController(hand) { this.entityPropertyCache.clear(); this.entityPropertyCache.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); - var _this = this; - this.entityPropertyCache.getEntities().forEach(function (entityID) { - var props = _this.entityPropertyCache.getProps(entityID); - if (_this.entityIsEquippableWithoutDistanceCheck(entityID)) { - var equipHotspots = _this.collectEquipHotspots(entityID); - var entityXform = new Xform(props.rotation, props.position); - var i, length = equipHotspots.length; - for (i = 0; i < length; i++) { - var hotspot = equipHotspots[i]; - overlay = Overlays.addOverlay("sphere", { - position: entityXform.xformPoint(hotspot.position), - size: hotspot.radius * 2, - color: EQUIP_SPHERE_COLOR, - alpha: EQUIP_SPHERE_ALPHA, + if (DRAW_GRAB_BOXES) { + // add blue box overlays for grabbable entities. + this.entityPropertyCache.getEntities().forEach(function (entityID) { + var props = _this.entityPropertyCache.getProps(entityID); + if (_this.entityIsGrabbable(entityID)) { + var overlay = Overlays.addOverlay("cube", { + rotation: props.rotation, + position: props.position, + size: props.dimensions, + color: GRAB_BOX_COLOR, + alpha: GRAB_BOX_ALPHA, solid: true, visible: true, ignoreRayIntersection: true, drawInFront: false }); - _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, - type: "equip", - localPosition: hotspot.position + type: "near", + localPosition: {x: 0, y: 0, z: 0} }); } - } + }); + } - if (DRAW_GRAB_BOXES && _this.entityIsGrabbable(entityID)) { - overlay = Overlays.addOverlay("cube", { - rotation: props.rotation, - position: props.position, - size: props.dimensions, - color: GRAB_BOX_COLOR, - alpha: GRAB_BOX_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - - _this.hotspotOverlays.push({ - entityID: entityID, - overlay: overlay, - type: "near", - localPosition: {x: 0, y: 0, z: 0} - }); - } + // add green spheres for each equippable hotspot. + flatten(this.entityPropertyCache.getEntities().map(function (entityID) { + return _this.collectEquipHotspots(entityID); + })).filter(function (hotspot) { + return _this.hotspotIsEquippable(hotspot); + }).forEach(function (hotspot) { + var overlay = Overlays.addOverlay("sphere", { + position: hotspot.worldPosition, + size: hotspot.radius * 2, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + _this.hotspotOverlays.push({ + entityID: hotspot.entityID, + overlay: overlay, + type: "equip", + localPosition: hotspot.localPosition + }); }); }; @@ -1091,8 +1089,18 @@ function MyController(hand) { return grabbableProps && grabbableProps.wantsTrigger; }; + /// @param {UUID} entityID + // @returns {Object[]} array of objects with the following fields. + // * entityID {UUID} + // * localPosition {Vec3} position of the hotspot in object space. + // * worldPosition {vec3} position of the hotspot in world space. + // * radius {number} radius of equip hotspot + // * joints {Object} keys are joint names values are arrays of two elements: + // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. this.collectEquipHotspots = function (entityID) { var result = []; + var props = this.entityPropertyCache.getProps(entityID); + var entityXform = new Xform(props.rotation, props.position); var equipHotspotsProps = this.entityPropertyCache.getEquipHotspotsProps(entityID); if (equipHotspotsProps && equipHotspotsProps.length > 0) { var i, length = equipHotspotsProps.length; @@ -1100,7 +1108,9 @@ function MyController(hand) { var hotspot = equipHotspotsProps[i]; if (hotspot.position && hotspot.radius && hotspot.joints) { result.push({ - position: hotspot.position, + entityID: entityID, + localPosition: hotspot.position, + worldPosition: entityXform.xformPoint(hotspot.position), radius: hotspot.radius, joints: hotspot.joints }); @@ -1110,7 +1120,9 @@ function MyController(hand) { var wearableProps = this.entityPropertyCache.getWearableProps(entityID); if (wearableProps && wearableProps.joints) { result.push({ - position: {x: 0, y: 0, z: 0}, + entityID: entityID, + localPosition: {x: 0, y: 0, z: 0}, + worldPosition: entityXform.pos, radius: EQUIP_RADIUS, joints: wearableProps.joints }); @@ -1119,13 +1131,10 @@ function MyController(hand) { return result; }; - this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition, skipDistanceCheck) { - var distance; - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - var props = this.entityPropertyCache.getProps(entityID); - var grabProps = this.entityPropertyCache.getGrabProps(entityID); + this.hotspotIsEquippable = function (hotspot) { + var props = this.entityPropertyCache.getProps(hotspot.entityID); + var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - var entityXform = new Xform(props.rotation, props.position); var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; if (refCount > 0) { @@ -1135,21 +1144,7 @@ function MyController(hand) { return false; } - var equipHotspots = this.collectEquipHotspots(entityID); - var i, length = equipHotspots.length; - for (i = 0; i < length; i++) { - var hotspot = equipHotspots[i]; - distance = Vec3.distance(entityXform.xformPoint(hotspot.position), handPosition); - if ((skipDistanceCheck || distance < hotspot.radius) && hotspot.joints[handJointName]) { - return true; - } - } - return false; - }; - - this.entityIsEquippableWithoutDistanceCheck = function (entityID) { - var handPosition = this.getHandPosition(); - return this.entityIsEquippableWithDistanceCheck(entityID, handPosition, true); + return true; }; this.entityIsGrabbable = function (entityID) { @@ -1285,22 +1280,24 @@ function MyController(hand) { this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); var candidateEntities = this.entityPropertyCache.getEntities(); - var equippableEntities = candidateEntities.filter(function (entity) { - return _this.entityIsEquippableWithDistanceCheck(entity, handPosition); + var equippableHotspots = flatten(candidateEntities.map(function (entityID) { + return _this.collectEquipHotspots(entityID); + })).filter(function (hotspot) { + return _this.hotspotIsEquippable(hotspot) && Vec3.distance(hotspot.worldPosition, handPosition) < hotspot.radius; }); var entity; - if (equippableEntities.length > 0) { + if (equippableHotspots.length > 0) { // sort by distance - equippableEntities.sort(function (a, b) { - var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); - var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + equippableHotspots.sort(function (a, b) { + var aDistance = Vec3.distance(a.worldPosition, handPosition); + var bDistance = Vec3.distance(b.worldPosition, handPosition); return aDistance - bDistance; }); - entity = equippableEntities[0]; if (this.triggerSmoothedGrab()) { - this.grabbedEntity = entity; - this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(entity).name + "'"); + this.grabbedHotspot = equippableHotspots[0]; + this.grabbedEntity = equippableHotspots[0].entityID; + this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(this.grabbedEntity).name + "'"); return; } else { // TODO: highlight the equippable object? diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index f39f4d7913..b7de9b012c 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -309,5 +309,11 @@ calculateHandSizeRatio = function() { clamp = function(val, min, max){ return Math.max(min, Math.min(max, val)) - } +} +// flattens an array of arrays into a single array +// example: flatten([[1], [3, 4], []) => [1, 3, 4] +// NOTE: only does one level of flattening, it is not recursive. +flatten = function(array) { + return [].concat.apply([], array); +} From fd062d09dc46a9c09ccd149ed6997fbeaef6f363 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 29 Jun 2016 15:59:51 -0700 Subject: [PATCH 0839/1237] initial notes and research --- scripts/system/controllers/teleport.js | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 scripts/system/controllers/teleport.js diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js new file mode 100644 index 0000000000..fe7ab67ad9 --- /dev/null +++ b/scripts/system/controllers/teleport.js @@ -0,0 +1,63 @@ +//check if trigger is down +//if trigger is down, check if thumb is down +//if both are down, enter 'teleport mode' +//aim controller to change landing spot +//visualize aim + spot (line + circle) +//release trigger to teleport then exit teleport mode +//if thumb is release, exit teleport mode + + +//v2: show room boundaries when choosing a place to teleport +//v2: smooth fade screen in/out? +//v2: haptic feedback +//v2: particles for parabolic arc to landing spot + +//parabola: point count, point spacing, graphic thickness + + + +//Standard.LeftPrimaryThumb +//Standard.LT + + +//create a controller mapping and make sure to disable it when the script is stopped +var teleportMapping = Controller.newMapping('Hifi-Teleporter-Dev-' + Math.random()); +Script.scriptEnding.connect(teleportMapping.disable); + +// Gather the trigger data for smoothing. +clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); +clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); + + + +function thumbPad() { + +} + +thumbPad.prototype = { + down: function() { + return this.padValue === 0 ? true : false; + } +} + +function trigger() { + +} + +trigger.prototype = { + triggerPress: function(value) { + this.triggerValue = value; + }, + down: function() { + return this.triggerValue === 0 ? true : false; + } +} + + +teleportMapping.from(thumbPad.down).when(trigger.down).to(teleport); + +function teleport() { + +} + +function() {} \ No newline at end of file From 335ed667bb1958e6f310cfeb64bcd3156d60978b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 29 Jun 2016 16:05:17 -0700 Subject: [PATCH 0840/1237] dk2 snapshot takes picture of HMD window --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 14 ++++++++++++++ .../src/display-plugins/hmd/HmdDisplayPlugin.h | 2 ++ 2 files changed, 16 insertions(+) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index f1aa1edc81..38fb86dc28 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -38,6 +38,20 @@ QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { return CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; } +QImage HmdDisplayPlugin::getScreenshot() const { + using namespace oglplus; + QImage screenshot(_compositeFramebuffer->size.x, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); + auto windowSize = toGlm(_container->getPrimaryWidget()->size()); + _compositeFramebuffer->Bound(Framebuffer::Target::Read, [&] { + Context::BlitFramebuffer( + 0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, + 0, 0, windowSize.x, windowSize.y, + BufferSelectBit::ColorBuffer, BlitFilter::Nearest); + Context::ReadPixels(0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); + }); + return screenshot.mirrored(false, true); +} + bool HmdDisplayPlugin::internalActivate() { _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index fada15d864..9a8d89a2c8 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -27,6 +27,8 @@ public: void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) override final; bool isDisplayVisible() const override { return isHmdMounted(); } + QImage getScreenshot() const override; + QRect getRecommendedOverlayRect() const override final; virtual glm::mat4 getHeadPose() const override; From 5f9c7b6ea5cb5df7392777a9976a53b0e9e2480a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 17:02:49 -0700 Subject: [PATCH 0841/1237] trying to take joint information into account when raypicking against models --- interface/src/avatar/AvatarManager.cpp | 3 + libraries/render-utils/src/Model.cpp | 161 +++++++++++++++++++------ libraries/render-utils/src/Model.h | 5 + 3 files changed, 135 insertions(+), 34 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index e1fe03ae2e..a5a02bdaff 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -435,6 +435,9 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& continue; } + avatarModel->invalidCalculatedMeshBoxes(); + avatarModel->recalculateMeshBoxes(true); + QString extraInfo; bool intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, distance, face, surfaceNormal, extraInfo, true); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index f947474aac..4969585af4 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "AbstractViewStateInterface.h" #include "MeshPartPayload.h" @@ -441,32 +442,31 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { PROFILE_RANGE(__FUNCTION__); bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; - if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { + if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || + (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { const FBXGeometry& geometry = getFBXGeometry(); int numberOfMeshes = geometry.meshes.size(); _calculatedMeshBoxes.resize(numberOfMeshes); _calculatedMeshTriangles.clear(); _calculatedMeshTriangles.resize(numberOfMeshes); _calculatedMeshPartBoxes.clear(); - for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& mesh = geometry.meshes.at(i); + + + + int okCount = 0; + int notOkCount = 0; + + + for (int meshIndex = 0; meshIndex < numberOfMeshes; meshIndex++) { + const FBXMesh& mesh = geometry.meshes.at(meshIndex); Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents, _translation, _rotation); - _calculatedMeshBoxes[i] = AABox(scaledMeshExtents); + _calculatedMeshBoxes[meshIndex] = AABox(scaledMeshExtents); if (pickAgainstTriangles) { QVector thisMeshTriangles; - - glm::mat4 meshTransform; - if (mesh.clusters.size() > 0) { - int jointIndex = mesh.clusters[0].jointIndex; - meshTransform = _rig->getJointTransform(jointIndex); - } else { - meshTransform = mesh.modelTransform; - } - - for (int j = 0; j < mesh.parts.size(); j++) { - const FBXMeshPart& part = mesh.parts.at(j); + for (int partIndex = 0; partIndex < mesh.parts.size(); partIndex++) { + const FBXMeshPart& part = mesh.parts.at(partIndex); bool atLeastOnePointInBounds = false; AABox thisPartBounds; @@ -483,10 +483,62 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - glm::vec3 mv3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + glm::vec3 v[3]; + int ok = 0; + if (meshIndex < _meshStates.size()) { + int quadPointIndexes[ 4 ] = {i0, i1, i2, i3}; + for (int pointInQuadIndex = 0; pointInQuadIndex < 4; pointInQuadIndex++) { + int vertexIndex = quadPointIndexes[pointInQuadIndex]; + glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; + glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; + + bool vSet = false; + for (int ci = 0; ci < 4; ci++) { + int clusterIndex = (int) clusterIndices[ci]; + float clusterWeight = clusterWeights[ci]; + if (clusterIndex < 0 || + clusterIndex >= mesh.clusters.size() || + clusterWeight == 0.0f) { + continue; + } + const FBXCluster& cluster = mesh.clusters.at(clusterIndex); + auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; + glm::vec3 meshVertex = mesh.vertices[vertexIndex]; + glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); + glm::vec3 tpoint = clusterWeight * (_translation + fuh); + v[pointInQuadIndex] += tpoint; + vSet = true; + } + if (vSet) { + ok++; + } + } + } + + + glm::vec3 v0; + glm::vec3 v1; + glm::vec3 v2; + glm::vec3 v3; + + glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + + if (ok == 4) { + okCount++; + v0 = v[0]; + v1 = v[1]; + v2 = v[2]; + v3 = v[3]; + } else { + notOkCount++; + v0 = calculateScaledOffsetPoint(mv0); + v1 = calculateScaledOffsetPoint(mv1); + v2 = calculateScaledOffsetPoint(mv2); + v3 = calculateScaledOffsetPoint(mv3); + } // track the mesh parts in model space if (!atLeastOnePointInBounds) { @@ -499,11 +551,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv2; thisPartBounds += mv3; - glm::vec3 v0 = calculateScaledOffsetPoint(mv0); - glm::vec3 v1 = calculateScaledOffsetPoint(mv1); - glm::vec3 v2 = calculateScaledOffsetPoint(mv2); - glm::vec3 v3 = calculateScaledOffsetPoint(mv3); - // Sam's recommended triangle slices Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; @@ -514,7 +561,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisMeshTriangles.push_back(tri1); thisMeshTriangles.push_back(tri2); - } } @@ -526,9 +572,57 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 v[3]; + int ok = 0; + if (meshIndex < _meshStates.size()) { + int trianglePointIndexes[ 3 ] = {i0, i1, i2}; + for (int pointInTriIndex = 0; pointInTriIndex < 3; pointInTriIndex++) { + int vertexIndex = trianglePointIndexes[pointInTriIndex]; + glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; + glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; + + bool vSet = false; + for (int ci = 0; ci < 4; ci++) { + int clusterIndex = (int) clusterIndices[ci]; + float clusterWeight = clusterWeights[ci]; + if (clusterIndex < 0 || + clusterIndex >= mesh.clusters.size() || + clusterWeight == 0.0f) { + continue; + } + const FBXCluster& cluster = mesh.clusters.at(clusterIndex); + auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; + glm::vec3 meshVertex = mesh.vertices[vertexIndex]; + glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); + glm::vec3 tpoint = clusterWeight * (_translation + fuh); + v[pointInTriIndex] += tpoint; + vSet = true; + } + if (vSet) { + ok++; + } + } + } + + glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + + glm::vec3 v0; + glm::vec3 v1; + glm::vec3 v2; + + if (ok == 3) { + okCount++; + v0 = v[0]; + v1 = v[1]; + v2 = v[2]; + } else { + notOkCount++; + v0 = calculateScaledOffsetPoint(mv0); + v1 = calculateScaledOffsetPoint(mv1); + v2 = calculateScaledOffsetPoint(mv2); + } // track the mesh parts in model space if (!atLeastOnePointInBounds) { @@ -540,21 +634,20 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv1; thisPartBounds += mv2; - glm::vec3 v0 = calculateScaledOffsetPoint(mv0); - glm::vec3 v1 = calculateScaledOffsetPoint(mv1); - glm::vec3 v2 = calculateScaledOffsetPoint(mv2); - Triangle tri = { v0, v1, v2 }; thisMeshTriangles.push_back(tri); } } - _calculatedMeshPartBoxes[QPair(i, j)] = thisPartBounds; + _calculatedMeshPartBoxes[QPair(meshIndex, partIndex)] = thisPartBounds; } - _calculatedMeshTriangles[i] = thisMeshTriangles; + _calculatedMeshTriangles[meshIndex] = thisMeshTriangles; _calculatedMeshPartBoxesValid = true; } } + + qDebug() << "ok = " << okCount << " not-ok =" << notOkCount; + _calculatedMeshBoxesValid = true; _calculatedMeshTrianglesValid = pickAgainstTriangles; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 6a7c9ec560..66c5eb019e 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -316,11 +316,14 @@ protected: float getLimbLength(int jointIndex) const; /// Allow sub classes to force invalidating the bboxes +public: void invalidCalculatedMeshBoxes() { _calculatedMeshBoxesValid = false; _calculatedMeshPartBoxesValid = false; _calculatedMeshTrianglesValid = false; } +protected: + // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; @@ -357,7 +360,9 @@ protected: bool _calculatedMeshTrianglesValid; QMutex _mutex; +public: void recalculateMeshBoxes(bool pickAgainstTriangles = false); +protected: void segregateMeshGroups(); // used to calculate our list of translucent vs opaque meshes static model::MaterialPointer _collisionHullMaterial; From 6072487c9c388374e707a17fdb69b7d629e7f37b Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 29 Jun 2016 17:28:53 -0700 Subject: [PATCH 0842/1237] Equip-points attachments work Also made handControllerGrab.js eslint clean. --- .../system/controllers/handControllerGrab.js | 197 ++++++++---------- 1 file changed, 82 insertions(+), 115 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index fe95dda596..d37602f8e6 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,7 +10,7 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, flatten, Xform */ +/* global setEntityCustomData, getEntityCustomData, vec3toStr, flatten, Xform */ Script.include("/~/system/libraries/utils.js"); Script.include("../libraries/Xform.js"); @@ -139,7 +139,7 @@ var DEFAULT_GRABBABLE_DATA = { var USE_BLACKLIST = true; var blacklist = []; -//we've created various ways of visualizing looking for and moving distant objects +// we've created various ways of visualizing looking for and moving distant objects var USE_ENTITY_LINES_FOR_SEARCHING = false; var USE_OVERLAY_LINES_FOR_SEARCHING = true; @@ -255,7 +255,8 @@ function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, // because other scripts (such as handControllerPointer) might be using some other visualization // instead of setting Reticle.visible. - return EXTERNALLY_MANAGED_2D_MINOR_MODE && (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position)); + return (EXTERNALLY_MANAGED_2D_MINOR_MODE && + (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); } function restore2DMode() { if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { @@ -274,7 +275,7 @@ EntityPropertiesCache.prototype.clear = function() { EntityPropertiesCache.prototype.findEntities = function(position, radius) { var entities = Entities.findEntities(position, radius); var _this = this; - entities.forEach(function (x) { + entities.forEach(function(x) { _this.updateEntity(x); }); }; @@ -286,7 +287,7 @@ EntityPropertiesCache.prototype.updateEntity = function(entityID) { if (props.userData) { try { userData = JSON.parse(props.userData); - } catch(err) { + } catch (err) { print("WARNING: malformed userData on " + entityID + ", name = " + props.name + ", error = " + err); } } @@ -296,9 +297,9 @@ EntityPropertiesCache.prototype.updateEntity = function(entityID) { }; EntityPropertiesCache.prototype.getEntities = function() { return Object.keys(this.cache); -} +}; EntityPropertiesCache.prototype.getProps = function(entityID) { - var obj = this.cache[entityID] + var obj = this.cache[entityID]; return obj ? obj : undefined; }; EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { @@ -346,7 +347,7 @@ function MyController(hand) { this.getHandRotation = function() { var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; return Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).rotation); - } + }; this.actionID = null; // action this script created... this.grabbedEntity = null; // on this entity. @@ -359,11 +360,11 @@ function MyController(hand) { this.rawSecondaryValue = 0; this.rawThumbValue = 0; - //for visualizations + // for visualizations this.overlayLine = null; this.particleBeamObject = null; - //for lights + // for lights this.spotlight = null; this.pointlight = null; this.overlayLine = null; @@ -388,7 +389,7 @@ function MyController(hand) { var _this = this; var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; - this.ignoreInput = function () { + this.ignoreInput = function() { // We've made the decision to use 'this' for new code, even though it is fragile, // in order to keep/ the code uniform without making any no-op line changes. return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); @@ -419,7 +420,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed = function(entityMethodName) { var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); - } + }; this.setState = function(newState, reason) { @@ -517,7 +518,7 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: true, // Even when burried inside of something, show it. visible: true - } + }; this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); } else { Overlays.editOverlay(this.searchSphere, { @@ -527,7 +528,7 @@ function MyController(hand) { visible: true }); } - } + }; this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { @@ -576,7 +577,7 @@ function MyController(hand) { this.overlayLineOn(handPosition, searchSphereLocation, (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } - } + }; this.handleDistantParticleBeam = function(handPosition, objectPosition, color) { @@ -643,7 +644,7 @@ function MyController(hand) { "alphaFinish": 1, "additiveBlending": 0, "textures": "https://hifi-content.s3.amazonaws.com/alan/dev/textures/grabsprite-3.png" - } + }; this.particleBeamObject = Entities.addEntity(particleBeamPropertiesObject); }; @@ -657,7 +658,7 @@ function MyController(hand) { emitSpeed: speed, speedSpread: spread, lifespan: lifespan - }) + }); }; this.evalLightWorldTransform = function(modelPos, modelRot) { @@ -711,9 +712,9 @@ function MyController(hand) { this.spotlight = Entities.addEntity(lightProperties); } else { Entities.editEntity(this.spotlight, { - //without this, this light would maintain rotation with its parent + // without this, this light would maintain rotation with its parent rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0) - }) + }); } }; @@ -777,7 +778,7 @@ function MyController(hand) { Entities.deleteEntity(this.particleBeamObject); this.particleBeamObject = null; } - } + }; this.turnLightsOff = function() { if (this.spotlight !== null) { @@ -853,7 +854,7 @@ function MyController(hand) { this.thumbPress = function(value) { _this.rawThumbValue = value; - } + }; this.thumbPressed = function() { return _this.rawThumbValue > THUMB_ON_VALUE; @@ -878,7 +879,7 @@ function MyController(hand) { } }; - this.createHotspots = function () { + this.createHotspots = function() { var _this = this; var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; @@ -942,7 +943,7 @@ function MyController(hand) { if (DRAW_GRAB_BOXES) { // add blue box overlays for grabbable entities. - this.entityPropertyCache.getEntities().forEach(function (entityID) { + this.entityPropertyCache.getEntities().forEach(function(entityID) { var props = _this.entityPropertyCache.getProps(entityID); if (_this.entityIsGrabbable(entityID)) { var overlay = Overlays.addOverlay("cube", { @@ -967,11 +968,11 @@ function MyController(hand) { } // add green spheres for each equippable hotspot. - flatten(this.entityPropertyCache.getEntities().map(function (entityID) { + flatten(this.entityPropertyCache.getEntities().map(function(entityID) { return _this.collectEquipHotspots(entityID); - })).filter(function (hotspot) { + })).filter(function(hotspot) { return _this.hotspotIsEquippable(hotspot); - }).forEach(function (hotspot) { + }).forEach(function(hotspot) { var overlay = Overlays.addOverlay("sphere", { position: hotspot.worldPosition, size: hotspot.radius * 2, @@ -994,14 +995,17 @@ function MyController(hand) { this.updateHotspots = function() { var _this = this; var props; - this.hotspotOverlays.forEach(function (overlayInfo) { + this.hotspotOverlays.forEach(function(overlayInfo) { if (overlayInfo.type === "hand") { Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); } else if (overlayInfo.type === "equip") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); var entityXform = new Xform(props.rotation, props.position); - Overlays.editOverlay(overlayInfo.overlay, { position: entityXform.xformPoint(overlayInfo.localPosition), rotation: props.rotation }); + Overlays.editOverlay(overlayInfo.overlay, { + position: entityXform.xformPoint(overlayInfo.localPosition), + rotation: props.rotation + }); } else if (overlayInfo.type === "near") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); @@ -1011,7 +1015,7 @@ function MyController(hand) { }; this.destroyHotspots = function() { - this.hotspotOverlays.forEach(function (overlayInfo) { + this.hotspotOverlays.forEach(function(overlayInfo) { Overlays.deleteOverlay(overlayInfo.overlay); }); this.hotspotOverlays = []; @@ -1025,14 +1029,14 @@ function MyController(hand) { this.destroyHotspots(); }; - /// // Performs ray pick test from the hand controller into the world // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND // @returns {object} returns object with two keys entityID and distance // this.calcRayPickInfo = function(hand) { - var pose = Controller.getPoseValue((hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var standardControllerValue = (hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + var pose = Controller.getPoseValue(standardControllerValue); var worldHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position); var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); @@ -1071,33 +1075,37 @@ function MyController(hand) { } var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); - if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { + if (!intersection.intersects || + (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { intersection = overlayIntersection; } if (intersection.intersects) { - return { entityID: intersection.entityID, - searchRay: pickRay, - distance: Vec3.distance(pickRay.origin, intersection.intersection) } + return { + entityID: intersection.entityID, + searchRay: pickRay, + distance: Vec3.distance(pickRay.origin, intersection.intersection) + }; } else { return result; } }; - this.entityWantsTrigger = function (entityID) { + this.entityWantsTrigger = function(entityID) { var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); return grabbableProps && grabbableProps.wantsTrigger; }; - /// @param {UUID} entityID - // @returns {Object[]} array of objects with the following fields. + // returns a list of all equip-hotspots assosiated with this entity. + // @param {UUID} entityID + // @returns {Object[]} array of objects with the following fields. // * entityID {UUID} // * localPosition {Vec3} position of the hotspot in object space. // * worldPosition {vec3} position of the hotspot in world space. // * radius {number} radius of equip hotspot // * joints {Object} keys are joint names values are arrays of two elements: // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. - this.collectEquipHotspots = function (entityID) { + this.collectEquipHotspots = function(entityID) { var result = []; var props = this.entityPropertyCache.getProps(entityID); var entityXform = new Xform(props.rotation, props.position); @@ -1131,7 +1139,7 @@ function MyController(hand) { return result; }; - this.hotspotIsEquippable = function (hotspot) { + this.hotspotIsEquippable = function(hotspot) { var props = this.entityPropertyCache.getProps(hotspot.entityID); var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1147,7 +1155,7 @@ function MyController(hand) { return true; }; - this.entityIsGrabbable = function (entityID) { + this.entityIsGrabbable = function(entityID) { var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); var grabProps = this.entityPropertyCache.getGrabProps(entityID); var props = this.entityPropertyCache.getProps(entityID); @@ -1280,16 +1288,16 @@ function MyController(hand) { this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); var candidateEntities = this.entityPropertyCache.getEntities(); - var equippableHotspots = flatten(candidateEntities.map(function (entityID) { + var equippableHotspots = flatten(candidateEntities.map(function(entityID) { return _this.collectEquipHotspots(entityID); - })).filter(function (hotspot) { + })).filter(function(hotspot) { return _this.hotspotIsEquippable(hotspot) && Vec3.distance(hotspot.worldPosition, handPosition) < hotspot.radius; }); var entity; if (equippableHotspots.length > 0) { // sort by distance - equippableHotspots.sort(function (a, b) { + equippableHotspots.sort(function(a, b) { var aDistance = Vec3.distance(a.worldPosition, handPosition); var bDistance = Vec3.distance(b.worldPosition, handPosition); return aDistance - bDistance; @@ -1304,7 +1312,7 @@ function MyController(hand) { } } - var grabbableEntities = candidateEntities.filter(function (entity) { + var grabbableEntities = candidateEntities.filter(function(entity) { return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); }); @@ -1321,7 +1329,7 @@ function MyController(hand) { if (grabbableEntities.length > 0) { // sort by distance - grabbableEntities.sort(function (a, b) { + grabbableEntities.sort(function(a, b) { var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); return aDistance - bDistance; @@ -1381,9 +1389,11 @@ function MyController(hand) { } } - //search line visualizations + // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { - this.lineOn(rayPickInfo.searchRay.origin, Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + this.lineOn(rayPickInfo.searchRay.origin, + Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), + NO_INTERSECT_COLOR); } this.searchIndicatorOn(rayPickInfo.searchRay); @@ -1398,11 +1408,11 @@ function MyController(hand) { timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; } return timeScale; - } + }; this.getMass = function(dimensions, density) { return (dimensions.x * dimensions.y * dimensions.z) * density; - } + }; this.distanceHoldingEnter = function() { @@ -1529,7 +1539,8 @@ function MyController(hand) { var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * + this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1557,30 +1568,9 @@ function MyController(hand) { } } - // var defaultConstraintData = { - // axisStart: false, - // axisEnd: false - // } - // - // var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); - // var clampedVector; - // var targetPosition; - // if (constraintData.axisStart !== false) { - // clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, - // constraintData.axisStart, - // constraintData.axisEnd); - // targetPosition = clampedVector; - // } else { - // targetPosition = { - // x: this.currentObjectPosition.x, - // y: this.currentObjectPosition.y, - // z: this.currentObjectPosition.z - // } - // } - var handPosition = this.getHandPosition(); - //visualizations + // visualizations if (USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); } @@ -1588,7 +1578,7 @@ function MyController(hand) { this.overlayLineOn(handPosition, grabbedProperties.position, INTERSECT_COLOR); } if (USE_PARTICLE_BEAM_FOR_MOVING === true) { - this.handleDistantParticleBeam(handPosition, grabbedProperties.position, INTERSECT_COLOR) + this.handleDistantParticleBeam(handPosition, grabbedProperties.position, INTERSECT_COLOR); } if (USE_POINTLIGHT === true) { this.handlePointLight(this.grabbedEntity); @@ -1648,46 +1638,17 @@ function MyController(hand) { scalar = 1; } var projection = Vec3.sum(axisStart, Vec3.multiply(scalar, Vec3.normalize(bPrime))); - return projection + return projection; }; - this.hasPresetOffsets = function() { - var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}}); - if ("joints" in wearableData) { - var allowedJoints = wearableData.joints; - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (handJointName in allowedJoints) { - return true; - } - } - return false; - } - - this.getPresetPosition = function() { - var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}}); - var allowedJoints = wearableData.joints; - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (handJointName in allowedJoints) { - return allowedJoints[handJointName][0]; - } - } - - this.getPresetRotation = function() { - var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}}); - var allowedJoints = wearableData.joints; - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (handJointName in allowedJoints) { - return allowedJoints[handJointName][1]; - } - } - this.dropGestureReset = function() { this.fastHandMoveDetected = false; this.fastHandMoveTimer = 0; }; this.dropGestureProcess = function(deltaTime) { - var pose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var standardControllerValue = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + var pose = Controller.getPoseValue(standardControllerValue); var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); @@ -1745,13 +1706,17 @@ function MyController(hand) { var handPosition = this.getHandPosition(); var hasPresetPosition = false; - if (this.state == STATE_HOLD && this.hasPresetOffsets()) { + if (this.state == STATE_HOLD && this.grabbedHotspot) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; - this.offsetPosition = this.getPresetPosition(); - this.offsetRotation = this.getPresetRotation(); - hasPresetPosition = true; + + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (this.grabbedHotspot.joints[handJointName]) { + this.offsetPosition = this.grabbedHotspot.joints[handJointName][0]; + this.offsetRotation = this.grabbedHotspot.joints[handJointName][1]; + hasPresetPosition = true; + } } else { this.ignoreIK = false; @@ -1784,7 +1749,7 @@ function MyController(hand) { var reparentProps = { parentID: MyAvatar.sessionUUID, parentJointIndex: handJointIndex - } + }; if (hasPresetPosition) { reparentProps["localPosition"] = this.offsetPosition; reparentProps["localRotation"] = this.offsetRotation; @@ -2022,6 +1987,7 @@ function MyController(hand) { })); this.grabbedEntity = null; + this.grabbedHotspot = null; if (this.triggerSmoothedGrab()) { this.waitForTriggerRelease = true; @@ -2124,7 +2090,7 @@ function MyController(hand) { print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); Entities.editEntity(childID, {parentID: NULL_UUID}); }); - } + }; this.deactivateEntity = function(entityID, noVelocity) { var deactiveProps; @@ -2149,7 +2115,7 @@ function MyController(hand) { // things that are held by parenting and dropped with no velocity will end up as "static" in bullet. If // it looks like the dropped thing should fall, give it a little velocity. - var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]) + var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]); var parentID = props.parentID; var doSetVelocity = false; @@ -2231,7 +2197,8 @@ mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController. Controller.enableMapping(MAPPING_NAME); -//the section below allows the grab script to listen for messages that disable either one or both hands. useful for two handed items +// the section below allows the grab script to listen for messages +// that disable either one or both hands. useful for two handed items var handToDisable = 'none'; function update(deltaTime) { @@ -2294,7 +2261,7 @@ var handleHandMessages = function(channel, message, sender) { } } } -} +}; Messages.messageReceived.connect(handleHandMessages); From 40d584021a47e2c0d1960f13a07ac98115263cf1 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 29 Jun 2016 18:09:46 -0700 Subject: [PATCH 0843/1237] replace old code completely, removed unnecessary blit --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 10 ++++++---- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 14 -------------- .../src/display-plugins/hmd/HmdDisplayPlugin.h | 2 -- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index b72f52351f..8650c086e3 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -647,12 +647,14 @@ void OpenGLDisplayPlugin::withMainThreadContext(std::function f) const { } QImage OpenGLDisplayPlugin::getScreenshot() const { - QImage result; + using namespace oglplus; + QImage screenshot(_compositeFramebuffer->size.x, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); withMainThreadContext([&] { - static auto widget = _container->getPrimaryWidget(); - result = widget->grabFrameBuffer(); + auto windowSize = toGlm(_container->getPrimaryWidget()->size()); + Framebuffer::Bind(Framebuffer::Target::Read, _compositeFramebuffer->fbo); + Context::ReadPixels(0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); }); - return result; + return screenshot.mirrored(false, true); } uint32_t OpenGLDisplayPlugin::getSceneTextureId() const { diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 38fb86dc28..f1aa1edc81 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -38,20 +38,6 @@ QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { return CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; } -QImage HmdDisplayPlugin::getScreenshot() const { - using namespace oglplus; - QImage screenshot(_compositeFramebuffer->size.x, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); - auto windowSize = toGlm(_container->getPrimaryWidget()->size()); - _compositeFramebuffer->Bound(Framebuffer::Target::Read, [&] { - Context::BlitFramebuffer( - 0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, - 0, 0, windowSize.x, windowSize.y, - BufferSelectBit::ColorBuffer, BlitFilter::Nearest); - Context::ReadPixels(0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); - }); - return screenshot.mirrored(false, true); -} - bool HmdDisplayPlugin::internalActivate() { _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 9a8d89a2c8..fada15d864 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -27,8 +27,6 @@ public: void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) override final; bool isDisplayVisible() const override { return isHmdMounted(); } - QImage getScreenshot() const override; - QRect getRecommendedOverlayRect() const override final; virtual glm::mat4 getHeadPose() const override; From 4b4e38a57cc8e9773965701610b44de4b4f35ab3 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 29 Jun 2016 18:28:39 -0700 Subject: [PATCH 0844/1237] end of day --- scripts/system/controllers/teleport.js | 129 ++++++++++++++++++++----- 1 file changed, 103 insertions(+), 26 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index fe7ab67ad9..e2d0dcff8d 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -14,8 +14,6 @@ //parabola: point count, point spacing, graphic thickness - - //Standard.LeftPrimaryThumb //Standard.LT @@ -24,40 +22,119 @@ var teleportMapping = Controller.newMapping('Hifi-Teleporter-Dev-' + Math.random()); Script.scriptEnding.connect(teleportMapping.disable); -// Gather the trigger data for smoothing. -clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); -clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); +// peek at the trigger and thumbs to store their values +teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); +teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); +teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); +teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - -function thumbPad() { +function ThumbPad() { } -thumbPad.prototype = { - down: function() { - return this.padValue === 0 ? true : false; - } -} - -function trigger() { - -} - -trigger.prototype = { - triggerPress: function(value) { - this.triggerValue = value; +ThumbPad.prototype = { + buttonPress: function(value) { + print('pad press: ' + value) + this.buttonValue = value; }, down: function() { - return this.triggerValue === 0 ? true : false; + return this.buttonValue === 0 ? true : false; } } - -teleportMapping.from(thumbPad.down).when(trigger.down).to(teleport); - -function teleport() { +function Trigger() { } -function() {} \ No newline at end of file +Trigger.prototype = { + buttonPress: function(value) { + print('trigger press: ' + value) + this.buttonValue = value; + }, + down: function() { + return this.buttonValue === 0 ? true : false; + } +} + +teleportMapping.from(leftPad.down).when(leftTrigger.down).to(teleporter.enterTeleportMode); +teleportMapping.from(rightPad.down).when(rightTrigger.down).to(teleporter.enterTeleportMode); + + +function Teleporter() { + +} + +Teleporter.prototype = { + enterTeleportMode: function(value) { + print('value on enter: ' + value); + }, + exitTeleportMode: function(value) { + print('value on exit: ' + value); + }, + teleport: function(value) { + //todo + //get the y position of the teleport landing spot + print('value on teleport: ' + value) + var properties = Entities.getEntityProperties(_this.entityID); + var offset = getAvatarFootOffset(); + properties.position.y += offset; + + print('OFFSET IS::: ' + JSON.stringify(offset)) + print('TELEPORT POSITION IS:: ' + JSON.stringify(properties.position)); + MyAvatar.position = properties.position; + } +} + +function getAvatarFootOffset() { + var data = getJointData(); + var upperLeg, lowerLeg, foot, toe, toeTop; + data.forEach(function(d) { + + var jointName = d.joint; + if (jointName === "RightUpLeg") { + upperLeg = d.translation.y; + } + if (jointName === "RightLeg") { + lowerLeg = d.translation.y; + } + if (jointName === "RightFoot") { + foot = d.translation.y; + } + if (jointName === "RightToeBase") { + toe = d.translation.y; + } + if (jointName === "RightToe_End") { + toeTop = d.translation.y + } + }) + + var myPosition = MyAvatar.position; + var offset = upperLeg + lowerLeg + foot + toe + toeTop; + offset = offset / 100; + return offset +}; + +function getJointData() { + var allJointData = []; + var jointNames = MyAvatar.jointNames; + jointNames.forEach(function(joint, index) { + var translation = MyAvatar.getJointTranslation(index); + var rotation = MyAvatar.getJointRotation(index) + allJointData.push({ + joint: joint, + index: index, + translation: translation, + rotation: rotation + }); + }); + + return allJointData; +} + + +var leftPad = new ThumbPad(); +var rightPad = new ThumbPad(); +var leftTrigger = new Trigger(); +var rightTrigger = new Trigger(); +var teleporter = new Teleporter(); \ No newline at end of file From ab21a5a5d5a260765ab886d016c3972a44d9b56a Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 29 Jun 2016 18:59:06 -0700 Subject: [PATCH 0845/1237] FIxing the bad brdf and adding better debug tool and better Pipeline architecture --- .../src/DeferredLightingEffect.cpp | 23 +- .../render-utils/src/DeferredLightingEffect.h | 34 +- .../render-utils/src/RenderDeferredTask.cpp | 12 +- .../render-utils/src/SubsurfaceScattering.cpp | 326 +++++++++--------- .../render-utils/src/SubsurfaceScattering.h | 58 +++- .../render-utils/src/SubsurfaceScattering.slh | 3 +- .../subsurfaceScattering_drawScattering.slf | 215 ++++-------- libraries/render/src/render/Task.h | 24 +- .../utilities/render/debugDeferredLighting.js | 4 +- .../render/debugSubsurfaceScattering.js | 37 ++ .../utilities/render/deferredLighting.qml | 51 --- .../utilities/render/subsurfaceScattering.qml | 76 ++++ 12 files changed, 434 insertions(+), 429 deletions(-) create mode 100644 scripts/developer/utilities/render/debugSubsurfaceScattering.js create mode 100644 scripts/developer/utilities/render/subsurfaceScattering.qml diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 217c6f726c..0609191262 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -609,38 +609,23 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, } } -RenderDeferred::RenderDeferred() : -_subsurfaceScatteringResource(std::make_shared()) -{ +RenderDeferred::RenderDeferred() { } void RenderDeferred::configure(const Config& config) { - glm::vec4 bentInfo(config.bentRed, config.bentGreen, config.bentBlue, config.bentScale); - _subsurfaceScatteringResource->setBentNormalFactors(bentInfo); - - glm::vec2 curvatureInfo(config.curvatureOffset, config.curvatureScale); - _subsurfaceScatteringResource->setCurvatureFactors(curvatureInfo); - - _subsurfaceScatteringResource->setLevel((float)config.enableScattering); - _subsurfaceScatteringResource->setShowBRDF(config.showScatteringBRDF); - _subsurfaceScatteringResource->setShowCurvature(config.showCurvature); - _subsurfaceScatteringResource->setShowDiffusedNormal(config.showDiffusedNormal); - _enablePointLights = config.enablePointLights; _enableSpotLights = config.enableSpotLights; } void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { - if (!_subsurfaceScatteringResource->getScatteringTable()) { - _subsurfaceScatteringResource->generateScatteringTable(renderContext->args); - } - auto deferredTransform = inputs.get0(); auto diffusedCurvature2 = inputs.get2()->getRenderBuffer(0); - setupJob.run(sceneContext, renderContext, deferredTransform, diffusedCurvature2, _subsurfaceScatteringResource); + auto subsurfaceScatteringResource = inputs.get3(); + + setupJob.run(sceneContext, renderContext, deferredTransform, diffusedCurvature2, subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform, _enablePointLights, _enableSpotLights); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 4c64fdf93e..7538cba97c 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -138,39 +138,12 @@ public: class RenderDeferredConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(float bentRed MEMBER bentRed NOTIFY dirty) - Q_PROPERTY(float bentGreen MEMBER bentGreen NOTIFY dirty) - Q_PROPERTY(float bentBlue MEMBER bentBlue NOTIFY dirty) - Q_PROPERTY(float bentScale MEMBER bentScale NOTIFY dirty) + Q_PROPERTY(bool enablePointLights MEMBER enablePointLights NOTIFY dirty) + Q_PROPERTY(bool enableSpotLights MEMBER enableSpotLights NOTIFY dirty) - Q_PROPERTY(float curvatureOffset MEMBER curvatureOffset NOTIFY dirty) - Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) - - Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) - Q_PROPERTY(bool showScatteringBRDF MEMBER showScatteringBRDF NOTIFY dirty) - Q_PROPERTY(bool showCurvature MEMBER showCurvature NOTIFY dirty) - Q_PROPERTY(bool showDiffusedNormal MEMBER showDiffusedNormal NOTIFY dirty) - - Q_PROPERTY(bool enablePointLights MEMBER enablePointLights NOTIFY dirty) - Q_PROPERTY(bool enableSpotLights MEMBER enableSpotLights NOTIFY dirty) - - public: RenderDeferredConfig() : render::Job::Config(true) {} - float bentRed{ 1.5f }; - float bentGreen{ 0.8f }; - float bentBlue{ 0.3f }; - float bentScale{ 1.5f }; - - float curvatureOffset{ 0.08f }; - float curvatureScale{ 0.9f }; - - bool enableScattering{ true }; - bool showScatteringBRDF{ false }; - bool showCurvature{ false }; - bool showDiffusedNormal{ false }; - bool enablePointLights{ true }; bool enableSpotLights{ true }; @@ -181,7 +154,7 @@ signals: class RenderDeferred { public: - using Inputs = render::VaryingSet3 < DeferredFrameTransformPointer, gpu::FramebufferPointer, gpu::FramebufferPointer >; + using Inputs = render::VaryingSet4 < DeferredFrameTransformPointer, gpu::FramebufferPointer, gpu::FramebufferPointer, SubsurfaceScatteringResourcePointer>; using Config = RenderDeferredConfig; using JobModel = render::Job::ModelI; @@ -196,7 +169,6 @@ public: RenderDeferredCleanup cleanupJob; protected: - SubsurfaceScatteringResourcePointer _subsurfaceScatteringResource; bool _enablePointLights{ true }; bool _enableSpotLights{ true }; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 57ff171a87..d359de0942 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -117,13 +117,15 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth, true); #endif + const auto scatteringResource = addJob("Scattering"); + // AO job addJob("AmbientOcclusion"); // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer)); + const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer, scatteringResource)); // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredLightingInputs); @@ -134,8 +136,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Render transparent objects forward in LightingBuffer addJob("DrawTransparentDeferred", transparents, shapePlumber); - - const auto scatteringFramebuffer = addJob("Scattering", deferredLightingInputs); + // Lighting Buffer ready for tone mapping addJob("ToneMapping"); @@ -147,6 +148,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Debugging stages { + addJob("DebugScattering", deferredLightingInputs); + // Debugging Deferred buffer job const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(diffusedCurvatureFramebuffer, curvatureFramebuffer)); addJob("DebugDeferredBuffer", debugFramebuffers); @@ -166,9 +169,6 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { } } - // FIXME: Hit effect is never used, let's hide it for now, probably a more generic way to add custom post process effects - // addJob("HitEffect"); - // Blit! addJob("Blit"); } diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 542f12b33e..30b8b3f7bd 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -123,180 +123,26 @@ void SubsurfaceScattering::configure(const Config& config) { glm::vec2 curvatureInfo(config.curvatureOffset, config.curvatureScale); _scatteringResource->setCurvatureFactors(curvatureInfo); - _showProfile = config.showProfile; - _showLUT = config.showLUT; + _scatteringResource->setLevel((float)config.enableScattering); + _scatteringResource->setShowBRDF(config.showScatteringBRDF); + _scatteringResource->setShowCurvature(config.showCurvature); + _scatteringResource->setShowDiffusedNormal(config.showDiffusedNormal); } - - -gpu::PipelinePointer SubsurfaceScattering::getScatteringPipeline() { - if (!_scatteringPipeline) { - auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - // auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); - auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_drawScattering_frag)); - gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ScatteringTask_FrameTransformSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("scatteringParamsBuffer"), ScatteringTask_ParamSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), ScatteringTask_LightSlot)); - - slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), ScatteringTask_ScatteringTableSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), ScatteringTask_CurvatureMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("diffusedCurvatureMap"), ScatteringTask_DiffusedCurvatureMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), ScatteringTask_NormalMapSlot)); - - slotBindings.insert(gpu::Shader::Binding(std::string("albedoMap"), ScatteringTask_AlbedoMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), ScatteringTask_LinearMapSlot)); - - slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), ScatteringTask_IBLMapSlot)); - - gpu::Shader::makeProgram(*program, slotBindings); - - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - // Stencil test the curvature pass for objects pixels only, not the background - // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - - _scatteringPipeline = gpu::Pipeline::create(program, state); - } - - return _scatteringPipeline; -} - - -gpu::PipelinePointer _showLUTPipeline; -gpu::PipelinePointer getShowLUTPipeline(); -gpu::PipelinePointer SubsurfaceScattering::getShowLUTPipeline() { - if (!_showLUTPipeline) { - auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); - gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); - - gpu::Shader::BindingSet slotBindings; - // slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SubsurfaceScattering_FrameTransformSlot)); - // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); - gpu::Shader::makeProgram(*program, slotBindings); - - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - // Stencil test the curvature pass for objects pixels only, not the background - // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - - _showLUTPipeline = gpu::Pipeline::create(program, state); - } - - return _showLUTPipeline; -} - -bool SubsurfaceScattering::updateScatteringFramebuffer(const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& scatteringFramebuffer) { - if (!sourceFramebuffer) { - return false; - } - - if (!_scatteringFramebuffer) { - _scatteringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - - // attach depthStencil if present in source - if (sourceFramebuffer->hasDepthStencil()) { - // _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); - _scatteringFramebuffer->setRenderBuffer(0, blurringTarget); - } else { - // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so - if ((_scatteringFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_scatteringFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { - _scatteringFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); - if (sourceFramebuffer->hasDepthStencil()) { - // _scatteringFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } - } - } - - if (!scatteringFramebuffer) { - scatteringFramebuffer = _scatteringFramebuffer; - } - - return true; - -} - - -void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, gpu::FramebufferPointer& scatteringFramebuffer) { +void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, Outputs& outputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; - _scatteringResource->generateScatteringTable(args); - auto scatteringProfile = _scatteringResource->getScatteringProfile(); - auto scatteringTable = _scatteringResource->getScatteringTable(); - - - auto pipeline = getScatteringPipeline(); - - auto& frameTransform = inputs.get0(); - auto& curvatureFramebuffer = inputs.get1(); - auto& diffusedFramebuffer = inputs.get2(); - - - auto framebufferCache = DependencyManager::get(); - - /* if (!updateScatteringFramebuffer(curvatureFramebuffer, scatteringFramebuffer)) { - return; + if (!_scatteringResource->getScatteringTable()) { + _scatteringResource->generateScatteringTable(renderContext->args); } -*/ - const auto theLight = DependencyManager::get()->getLightStage().lights[0]; - - gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { - // batch.enableStereo(false); - batch.setViewportTransform(args->_viewport); - - /* batch.setFramebuffer(_scatteringFramebuffer); - - batch.setPipeline(pipeline); - - batch.setUniformBuffer(ScatteringTask_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); - batch.setUniformBuffer(ScatteringTask_ParamSlot, _scatteringResource->getParametersBuffer()); - if (theLight->light) { - batch.setUniformBuffer(ScatteringTask_LightSlot, theLight->light->getSchemaBuffer()); - if (theLight->light->getAmbientMap()) { - batch.setResourceTexture(ScatteringTask_IBLMapSlot, theLight->light->getAmbientMap()); - } - } - batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, scatteringTable); - batch.setResourceTexture(ScatteringTask_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); - batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); - batch.setResourceTexture(ScatteringTask_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); - batch.setResourceTexture(ScatteringTask_AlbedoMapSlot, framebufferCache->getDeferredColorTexture()); - batch.setResourceTexture(ScatteringTask_LinearMapSlot, framebufferCache->getDepthPyramidTexture()); - - batch.draw(gpu::TRIANGLE_STRIP, 4); -*/ - auto viewportSize = std::min(args->_viewport.z, args->_viewport.w) >> 1; - auto offsetViewport = viewportSize * 0.1; - - if (_showProfile) { - batch.setViewportTransform(glm::ivec4(0, 0, viewportSize, offsetViewport)); - batch.setPipeline(getShowLUTPipeline()); - batch.setResourceTexture(0, scatteringProfile); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - - if (_showLUT) { - batch.setViewportTransform(glm::ivec4(0, offsetViewport * 1.5, viewportSize, viewportSize)); - batch.setPipeline(getShowLUTPipeline()); - batch.setResourceTexture(0, scatteringTable); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - }); + outputs = _scatteringResource; } - - - +#ifdef GENERATE_SCATTERING_RESOURCE_ON_CPU // Reference: http://www.altdevblogaday.com/2011/12/31/skin-shading-in-unity3d/ #include @@ -450,6 +296,8 @@ void diffuseProfile(gpu::TexturePointer& profile) { profile->assignStoredMip(0, gpu::Element::COLOR_RGBA_32, bytes.size(), bytes.data()); } +#endif + void diffuseProfileGPU(gpu::TexturePointer& profileMap, RenderArgs* args) { int width = profileMap->getWidth(); int height = profileMap->getHeight(); @@ -461,8 +309,6 @@ void diffuseProfileGPU(gpu::TexturePointer& profileMap, RenderArgs* args) { gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; - //slotBindings.insert(gpu::Shader::Binding(std::string("profileMap"), 0)); - // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -500,7 +346,6 @@ void diffuseScatterGPU(const gpu::TexturePointer& profileMap, gpu::TexturePointe gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("scatteringProfile"), 0)); - // slotBindings.insert(gpu::Shader::Binding(std::string("sourceMap"), BlurTask_SourceSlot)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -529,7 +374,7 @@ void diffuseScatterGPU(const gpu::TexturePointer& profileMap, gpu::TexturePointe gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(RenderArgs* args) { const int PROFILE_RESOLUTION = 512; - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); diffuseProfileGPU(profileMap, args); return profileMap; } @@ -537,8 +382,153 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScattering(const gpu::TexturePointer& profile, RenderArgs* args) { const int TABLE_RESOLUTION = 512; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); diffuseScatterGPU(profile, scatteringLUT, args); return scatteringLUT; } + + + +DebugSubsurfaceScattering::DebugSubsurfaceScattering() { +} + +void DebugSubsurfaceScattering::configure(const Config& config) { + + _showProfile = config.showProfile; + _showLUT = config.showLUT; + _showCursorPixel = config.showCursorPixel; + _debugCursorTexcoord = config.debugCursorTexcoord; +} + + + +gpu::PipelinePointer DebugSubsurfaceScattering::getScatteringPipeline() { + if (!_scatteringPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + // auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_drawScattering_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ScatteringTask_FrameTransformSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringParamsBuffer"), ScatteringTask_ParamSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), ScatteringTask_LightSlot)); + + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), ScatteringTask_ScatteringTableSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), ScatteringTask_CurvatureMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("diffusedCurvatureMap"), ScatteringTask_DiffusedCurvatureMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), ScatteringTask_NormalMapSlot)); + + slotBindings.insert(gpu::Shader::Binding(std::string("albedoMap"), ScatteringTask_AlbedoMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), ScatteringTask_LinearMapSlot)); + + slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), ScatteringTask_IBLMapSlot)); + + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + _scatteringPipeline = gpu::Pipeline::create(program, state); + } + + return _scatteringPipeline; +} + + +gpu::PipelinePointer _showLUTPipeline; +gpu::PipelinePointer getShowLUTPipeline(); +gpu::PipelinePointer DebugSubsurfaceScattering::getShowLUTPipeline() { + if (!_showLUTPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + _showLUTPipeline = gpu::Pipeline::create(program, state); + } + + return _showLUTPipeline; +} + + +void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + + RenderArgs* args = renderContext->args; + + + auto& frameTransform = inputs.get0(); + auto& curvatureFramebuffer = inputs.get1(); + auto& diffusedFramebuffer = inputs.get2(); + auto& scatteringResource = inputs.get3(); + + if (!scatteringResource) { + return; + } + auto scatteringProfile = scatteringResource->getScatteringProfile(); + auto scatteringTable = scatteringResource->getScatteringTable(); + + auto framebufferCache = DependencyManager::get(); + + + + const auto theLight = DependencyManager::get()->getLightStage().lights[0]; + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + + + auto viewportSize = std::min(args->_viewport.z, args->_viewport.w) >> 1; + auto offsetViewport = viewportSize * 0.1; + + if (_showProfile) { + batch.setViewportTransform(glm::ivec4(0, 0, viewportSize, offsetViewport)); + batch.setPipeline(getShowLUTPipeline()); + batch.setResourceTexture(0, scatteringProfile); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + + if (_showLUT) { + batch.setViewportTransform(glm::ivec4(0, offsetViewport * 1.5, viewportSize, viewportSize)); + batch.setPipeline(getShowLUTPipeline()); + batch.setResourceTexture(0, scatteringTable); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + if (_showCursorPixel) { + + auto debugScatteringPipeline = getScatteringPipeline(); + batch.setPipeline(debugScatteringPipeline); + + Transform model; + model.setTranslation(glm::vec3(0.0, offsetViewport * 1.5 / args->_viewport.w, 0.0)); + model.setScale(glm::vec3(viewportSize / (float)args->_viewport.z, viewportSize / (float)args->_viewport.w, 1.0)); + batch.setModelTransform(model); + + batch.setUniformBuffer(ScatteringTask_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); + batch.setUniformBuffer(ScatteringTask_ParamSlot, scatteringResource->getParametersBuffer()); + if (theLight->light) { + batch.setUniformBuffer(ScatteringTask_LightSlot, theLight->light->getSchemaBuffer()); + } + batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, scatteringTable); + batch.setResourceTexture(ScatteringTask_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); + batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); + batch.setResourceTexture(ScatteringTask_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); + batch.setResourceTexture(ScatteringTask_AlbedoMapSlot, framebufferCache->getDeferredColorTexture()); + batch.setResourceTexture(ScatteringTask_LinearMapSlot, framebufferCache->getDepthPyramidTexture()); + + + batch._glUniform2f(debugScatteringPipeline->getProgram()->getUniforms().findLocation("uniformCursorTexcoord"), _debugCursorTexcoord.x, _debugCursorTexcoord.y); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + + batch.setViewportTransform(args->_viewport); + + }); +} diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index b4ad511bf3..3446324df3 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -90,9 +90,11 @@ class SubsurfaceScatteringConfig : public render::Job::Config { Q_PROPERTY(float curvatureOffset MEMBER curvatureOffset NOTIFY dirty) Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) + Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) + Q_PROPERTY(bool showScatteringBRDF MEMBER showScatteringBRDF NOTIFY dirty) + Q_PROPERTY(bool showCurvature MEMBER showCurvature NOTIFY dirty) + Q_PROPERTY(bool showDiffusedNormal MEMBER showDiffusedNormal NOTIFY dirty) - Q_PROPERTY(bool showProfile MEMBER showProfile NOTIFY dirty) - Q_PROPERTY(bool showLUT MEMBER showLUT NOTIFY dirty) public: SubsurfaceScatteringConfig() : render::Job::Config(true) {} @@ -102,10 +104,12 @@ public: float bentScale{ 1.5f }; float curvatureOffset{ 0.08f }; - float curvatureScale{ 0.8f }; + float curvatureScale{ 0.9f }; - bool showProfile{ false }; - bool showLUT{ false }; + bool enableScattering{ true }; + bool showScatteringBRDF{ false }; + bool showCurvature{ false }; + bool showDiffusedNormal{ false }; signals: void dirty(); @@ -113,22 +117,54 @@ signals: class SubsurfaceScattering { public: - using Inputs = render::VaryingSet3; + //using Inputs = render::VaryingSet4; + using Outputs = SubsurfaceScatteringResourcePointer; using Config = SubsurfaceScatteringConfig; - using JobModel = render::Job::ModelIO; + using JobModel = render::Job::ModelO; SubsurfaceScattering(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, gpu::FramebufferPointer& scatteringFramebuffer); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, Outputs& outputs); private: SubsurfaceScatteringResourcePointer _scatteringResource; +}; - bool updateScatteringFramebuffer(const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& scatteringFramebuffer); - gpu::FramebufferPointer _scatteringFramebuffer; +class DebugSubsurfaceScatteringConfig : public render::Job::Config { + Q_OBJECT + + Q_PROPERTY(bool showProfile MEMBER showProfile NOTIFY dirty) + Q_PROPERTY(bool showLUT MEMBER showLUT NOTIFY dirty) + Q_PROPERTY(bool showCursorPixel MEMBER showCursorPixel NOTIFY dirty) + Q_PROPERTY(glm::vec2 debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) +public: + DebugSubsurfaceScatteringConfig() : render::Job::Config(true) {} + + bool showProfile{ false }; + bool showLUT{ false }; + bool showCursorPixel{ false }; + glm::vec2 debugCursorTexcoord{ 0.5, 0.5 }; + +signals: + void dirty(); +}; + +class DebugSubsurfaceScattering { +public: + using Inputs = render::VaryingSet4; + using Config = DebugSubsurfaceScatteringConfig; + using JobModel = render::Job::ModelI; + + DebugSubsurfaceScattering(); + + void configure(const Config& config); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); + +private: + gpu::PipelinePointer _scatteringPipeline; gpu::PipelinePointer getScatteringPipeline(); @@ -136,6 +172,8 @@ private: gpu::PipelinePointer getShowLUTPipeline(); bool _showProfile{ false }; bool _showLUT{ false }; + bool _showCursorPixel{ false }; + glm::vec2 _debugCursorTexcoord; }; #endif // hifi_SubsurfaceScattering_h diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index babd4e0a92..1c2a2ace55 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -109,8 +109,7 @@ vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { return vec3( fetchBRDF(LdotNSpectrum.r, curvature).r, fetchBRDF(LdotNSpectrum.g, curvature).g, - fetchBRDF(LdotNSpectrum.b, curvature).b - ); + fetchBRDF(LdotNSpectrum.b, curvature).b); } // Subsurface Scattering parameters diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index 6151cfbd2e..0b881e8e29 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -10,175 +10,112 @@ // - -<$declareDeferredFrameTransform()$> -!> <@include DeferredBufferRead.slh@> +<@include model/Light.slh@> -<@include DeferredGlobalLight.slh@> - -<$declareEvalGlobalSpecularIrradiance(0, 1, 0)$> - -uniform sampler2D linearDepthMap; -float getZEye(ivec2 pixel) { - return -texelFetch(linearDepthMap, pixel, 0).x; -} -float getZEyeLinear(vec2 texcoord) { - return -texture(linearDepthMap, texcoord).x; -} - - -vec2 sideToFrameTexcoord(vec2 side, vec2 texcoordPos) { - return vec2((texcoordPos.x + side.x) * side.y, texcoordPos.y); -} - -// the curvature texture -uniform sampler2D curvatureMap; - -vec4 fetchCurvature(vec2 texcoord) { - return texture(curvatureMap, texcoord); -} - -// the curvature texture -uniform sampler2D diffusedCurvatureMap; - -vec4 fetchDiffusedCurvature(vec2 texcoord) { - return texture(diffusedCurvatureMap, texcoord); -} - - -uniform sampler2D scatteringLUT; - -vec3 fetchBRDF(float LdotN, float curvature) { - return texture(scatteringLUT, vec2( LdotN * 0.5 + 0.5, curvature)).xyz; -} - -vec3 fetchBRDFSpectrum(vec3 LdotNSpectrum, float curvature) { - return vec3( - fetchBRDF(LdotNSpectrum.r, curvature).r, - fetchBRDF(LdotNSpectrum.g, curvature).g, - fetchBRDF(LdotNSpectrum.b, curvature).b - ); -} - -// Scattering parameters - -struct ScatteringParameters { - vec4 normalBendInfo; // R, G, B, factor - vec4 curvatureInfo;// Offset, Scale -}; - -uniform scatteringParamsBuffer { - ScatteringParameters parameters; -}; - -vec3 getBendFactor() { - return parameters.normalBendInfo.xyz * parameters.normalBendInfo.w; -} - -float unpackCurvature(float packedCurvature) { - return abs(packedCurvature * 2 - 1) * 0.5f * parameters.curvatureInfo.y + parameters.curvatureInfo.x; -} +<$declareDeferredCurvature()$> +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringResource()$> in vec2 varTexCoord0; out vec4 _fragColor; +uniform vec2 uniformCursorTexcoord = vec2(0.5); + //uniform vec3 uniformLightVector = vec3(1.0); -void main(void) { - - DeferredFragment fragment = unpackDeferredFragmentNoPosition(varTexCoord0); +vec3 evalScatteringBRDF(vec2 texcoord) { + DeferredFragment fragment = unpackDeferredFragmentNoPosition(texcoord); vec3 normal = fragment.normal; // .getWorldNormal(varTexCoord0); - vec4 blurredCurvature = fetchCurvature(varTexCoord0); - vec4 diffusedCurvature = fetchDiffusedCurvature(varTexCoord0); + vec4 blurredCurvature = fetchCurvature(texcoord); + vec4 diffusedCurvature = fetchDiffusedCurvature(texcoord); - // --> Get curvature data - vec3 bentNormalHigh = normalize( (blurredCurvature.xyz - 0.5f) * 2.0f ); - vec3 bentNormalLow = normalize( (diffusedCurvature.xyz - 0.5f) * 2.0f ); + + // Transform directions to worldspace + vec3 fragNormal = vec3((normal)); + + // Get light + Light light = getLight(); + vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin + float metallic = 0.0; + + vec3 fragLightDir = -normalize(getLightDirection(light)); + + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); float curvature = unpackCurvature(diffusedCurvature.w); + if (showDiffusedNormal()) { + return diffusedCurvature.xyz; + return lowNormal * 0.5 + vec3(0.5); + } + if (showCurvature()) { + float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); + return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); + } - // _fragColor = vec4(vec3(diffusedCurvature.xyz), 1.0); + vec3 bentNdotL = evalScatteringBentNdotL(fragNormal, midNormal, lowNormal, fragLightDir); - // --> Calculate the light vector. - + vec3 brdf = fetchBRDFSpectrum(bentNdotL, curvature); + + return brdf; +} + +vec3 drawScatteringTableUV(vec2 cursor, vec2 texcoord) { + DeferredFragment fragment = unpackDeferredFragmentNoPosition(cursor); + + vec3 normal = fragment.normal; // .getWorldNormal(varTexCoord0); + vec4 blurredCurvature = fetchCurvature(cursor); + vec4 diffusedCurvature = fetchDiffusedCurvature(cursor); + + + // Get light Light light = getLight(); - vec3 fragLightDir = -getLightDirection(light); //normalize(uniformLightVector); //normalize(lightPos - sourcePos.xyz); - - // _fragColor = vec4(fetchBRDF(dot(bentNormalR, fragLightDir), abs(diffusedCurvature.w * 2 - 1)), 1.0); - // _fragColor = vec4(vec3(abs(dot(bentNormalR, fragLightDir))), 1.0); - // _fragColor = vec4(vec3(varTexCoord0, 0.0), 1.0); - // _fragColor = vec4(vec3(bentNormalR * 0.5 + 0.5), 1.0); + vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin + vec3 fragLightDir = -normalize(getLightDirection(light)); - vec3 rS = bentNormalHigh; - - vec3 bendFactorSpectrum = getBendFactor(); - vec3 rN = normalize(mix(normal, bentNormalLow, bendFactorSpectrum.x)); - vec3 gN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.y)); - vec3 bN = normalize(mix(bentNormalHigh, bentNormalLow, bendFactorSpectrum.z)); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + float curvature = unpackCurvature(diffusedCurvature.w); - vec3 NdotLSpectrum = vec3(dot(rN, fragLightDir), dot(gN, fragLightDir), dot(bN, fragLightDir)); + vec3 bentNdotL = evalScatteringBentNdotL(normal, midNormal, lowNormal, fragLightDir); - // --> Look up the pre-integrated curvature-dependent BDRF textures - vec3 bdrf = fetchBRDFSpectrum(NdotLSpectrum, curvature); + // return clamp(bentNdotL * 0.5 + 0.5, 0.0, 1.0); - - // Pixel being shaded - ivec2 pixelPos; - vec2 texcoordPos; - ivec4 stereoSide; - ivec2 framePixelPos = getPixelPosTexcoordPosAndSide(gl_FragCoord.xy, pixelPos, texcoordPos, stereoSide); - vec2 stereoSideClip = vec2(stereoSide.x, (isStereo() ? 0.5 : 1.0)); - vec2 frameTexcoordPos = sideToFrameTexcoord(stereoSideClip, texcoordPos); - - // Fetch the z under the pixel (stereo or not) - float Zeye = getZEye(framePixelPos); - - vec3 worldNormal = fragment.normal; - - // The position of the pixel fragment in Eye space then in world space - vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); - vec3 fragEyeDir = -(frameTransform._viewInverse * vec4(normalize(eyePos), 0.0)).xyz; - vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value - if (fragment.metallic > 0.5) { - fresnel = fragment.diffuse; - fragment.metallic = 1.0; - } - vec3 albedo = fragment.diffuse; - vec3 fragNormal = fragment.normal; - - vec4 shading; - - { // Key Sun Lighting - // Diffuse Lighting - float diffuse = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); - - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir,halfDir); - float power = specularDistribution(fragment.roughness, fragNormal, halfDir); - vec3 specular = power * fresnelColor * diffuse; - - shading = vec4(specular, (1.0 - fragment.metallic) * diffuse * (1 - fresnelColor.x)); + vec3 distance = vec3(0.0); + for (int c = 0; c < 3; c++) { + vec2 BRDFuv = vec2(clamp(bentNdotL[c] * 0.5 + 0.5, 0.0, 1.0), clamp(2 * curvature, 0.0, 1.0)); + vec2 delta = BRDFuv - texcoord; + distance[c] = 1.0 - dot(delta, delta); } - vec3 color = vec3(albedo * vec3(bdrf.xyz) + shading.rgb) * getLightColor(light) * getLightIntensity(light); + distance *= distance; + float threshold = 0.999; + vec3 color = vec3(0.0); + bool keep = false; + for (int c = 0; c < 3; c++) { + if (distance[c] > threshold) { + keep = true; + color[c] += 1.0; + } + } - // Diffuse from ambient - color += (1 - fragment.metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), bentNormalHigh).xyz * 1.0 * getLightAmbientIntensity(light); + if (!keep) + discard; - // Specular highlight from ambient - vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, fragment.roughness, fresnel, 1.0); - color += specularLighting; + return color; +} - //_fragColor = vec4(evalSkyboxLight(rS, 0.0).rgb, 1.0); +void main(void) { + // _fragColor = vec4(evalScatteringBRDF(varTexCoord0), 1.0); + // _fragColor = vec4(uniformCursorTexcoord, 0.0, 1.0); - _fragColor = vec4(color, 1.0); + _fragColor = vec4(drawScatteringTableUV(uniformCursorTexcoord, varTexCoord0), 1.0); } diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 74eefd86bc..b7ea70a370 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -113,7 +113,7 @@ public: using Parent = std::tuple; VaryingSet3() : Parent(Varying(T0()), Varying(T1()), Varying(T2())) {} - VaryingSet3(const VaryingSet3& trio) : Parent(std::get<0>(trio), std::get<1>(trio), std::get<2>(trio)) {} + VaryingSet3(const VaryingSet3& src) : Parent(std::get<0>(src), std::get<1>(src), std::get<2>(src)) {} VaryingSet3(const Varying& first, const Varying& second, const Varying& third) : Parent(first, second, third) {} const T0& get0() const { return std::get<0>((*this)).template get(); } @@ -126,6 +126,28 @@ public: T2& edit2() { return std::get<2>((*this)).template edit(); } }; +template +class VaryingSet4 : public std::tuple{ +public: + using Parent = std::tuple; + + VaryingSet4() : Parent(Varying(T0()), Varying(T1()), Varying(T2()), Varying(T3())) {} + VaryingSet4(const VaryingSet4& src) : Parent(std::get<0>(src), std::get<1>(src), std::get<2>(src), std::get<3>(src)) {} + VaryingSet4(const Varying& first, const Varying& second, const Varying& third, const Varying& fourth) : Parent(first, second, third, fourth) {} + + const T0& get0() const { return std::get<0>((*this)).template get(); } + T0& edit0() { return std::get<0>((*this)).template edit(); } + + const T1& get1() const { return std::get<1>((*this)).template get(); } + T1& edit1() { return std::get<1>((*this)).template edit(); } + + const T2& get2() const { return std::get<2>((*this)).template get(); } + T2& edit2() { return std::get<2>((*this)).template edit(); } + + const T3& get3() const { return std::get<3>((*this)).template get(); } + T3& edit3() { return std::get<3>((*this)).template edit(); } +}; + template < class T, int NUM > class VaryingArray : public std::array { public: diff --git a/scripts/developer/utilities/render/debugDeferredLighting.js b/scripts/developer/utilities/render/debugDeferredLighting.js index 9d48845ec9..710f08d401 100644 --- a/scripts/developer/utilities/render/debugDeferredLighting.js +++ b/scripts/developer/utilities/render/debugDeferredLighting.js @@ -1,5 +1,5 @@ // -// debugSurfaceGeometryPass.js +// debugDeferredLighting.js // // Created by Sam Gateau on 6/6/2016 // Copyright 2016 High Fidelity, Inc. @@ -13,7 +13,7 @@ var qml = Script.resolvePath('deferredLighting.qml'); var window = new OverlayWindow({ title: 'Deferred Lighting Pass', source: qml, - width: 400, height: 350, + width: 400, height: 100, }); window.setPosition(250, 800); window.closed.connect(function() { Script.stop(); }); diff --git a/scripts/developer/utilities/render/debugSubsurfaceScattering.js b/scripts/developer/utilities/render/debugSubsurfaceScattering.js new file mode 100644 index 0000000000..72b15546e0 --- /dev/null +++ b/scripts/developer/utilities/render/debugSubsurfaceScattering.js @@ -0,0 +1,37 @@ +// +// debugSubsirfaceScattering.js +// +// Created by Sam Gateau on 6/6/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +// Set up the qml ui +var qml = Script.resolvePath('subsurfaceScattering.qml'); +var window = new OverlayWindow({ + title: 'Subsurface Scattering', + source: qml, + width: 400, height: 350, +}); +window.setPosition(250, 950); +window.closed.connect(function() { Script.stop(); }); + +var moveDebugCursor = false; +Controller.mousePressEvent.connect(function (e) { + if (e.isMiddleButton) { + moveDebugCursor = true; + setDebugCursor(e.x, e.y); + } +}); +Controller.mouseReleaseEvent.connect(function() { moveDebugCursor = false; }); +Controller.mouseMoveEvent.connect(function (e) { if (moveDebugCursor) setDebugCursor(e.x, e.y); }); + + +function setDebugCursor(x, y) { + nx = (x / Window.innerWidth); + ny = 1.0 - ((y) / (Window.innerHeight - 32)); + + Render.getConfig("DebugScattering").debugCursorTexcoord = { x: nx, y: ny }; +} diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index e82f142763..02dd01e4c5 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -27,56 +27,5 @@ Column { checked: true onCheckedChanged: { Render.getConfig("RenderDeferred").enableSpotLights = checked } } - - Column{ - CheckBox { - text: "Scattering" - checked: true - onCheckedChanged: { Render.getConfig("RenderDeferred").enableScattering = checked } - } - - CheckBox { - text: "Show Scattering BRDF" - checked: Render.getConfig("RenderDeferred").showScatteringBRDF - onCheckedChanged: { Render.getConfig("RenderDeferred").showScatteringBRDF = checked } - } - CheckBox { - text: "Show Curvature" - checked: Render.getConfig("RenderDeferred").showCurvature - onCheckedChanged: { Render.getConfig("RenderDeferred").showCurvature = checked } - } - CheckBox { - text: "Show Diffused Normal" - checked: Render.getConfig("RenderDeferred").showDiffusedNormal - onCheckedChanged: { Render.getConfig("RenderDeferred").showDiffusedNormal = checked } - } - Repeater { - model: [ "Scattering Bent Red:RenderDeferred:bentRed:2.0", - "Scattering Bent Green:RenderDeferred:bentGreen:2.0", - "Scattering Bent Blue:RenderDeferred:bentBlue:2.0", - "Scattering Bent Scale:RenderDeferred:bentScale:5.0", - "Scattering Curvature Offset:RenderDeferred:curvatureOffset:1.0", - "Scattering Curvature Scale:RenderDeferred:curvatureScale:2.0", - ] - ConfigSlider { - label: qsTr(modelData.split(":")[0]) - integral: false - config: Render.getConfig(modelData.split(":")[1]) - property: modelData.split(":")[2] - max: modelData.split(":")[3] - min: 0.0 - } - } - CheckBox { - text: "Scattering Profile" - checked: Render.getConfig("Scattering").showProfile - onCheckedChanged: { Render.getConfig("Scattering").showProfile = checked } - } - CheckBox { - text: "Scattering Table" - checked: Render.getConfig("Scattering").showLUT - onCheckedChanged: { Render.getConfig("Scattering").showLUT = checked } - } - } } } diff --git a/scripts/developer/utilities/render/subsurfaceScattering.qml b/scripts/developer/utilities/render/subsurfaceScattering.qml new file mode 100644 index 0000000000..db0c2b798c --- /dev/null +++ b/scripts/developer/utilities/render/subsurfaceScattering.qml @@ -0,0 +1,76 @@ +// +// subsurfaceScattering.qml +// +// Created by Sam Gateau on 6/6/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "configSlider" + +Column { + spacing: 8 + Column { + id: scattering + spacing: 10 + + Column{ + CheckBox { + text: "Scattering" + checked: Render.getConfig("Scattering").enableScattering + onCheckedChanged: { Render.getConfig("Scattering").enableScattering = checked } + } + + CheckBox { + text: "Show Scattering BRDF" + checked: Render.getConfig("Scattering").showScatteringBRDF + onCheckedChanged: { Render.getConfig("Scattering").showScatteringBRDF = checked } + } + CheckBox { + text: "Show Curvature" + checked: Render.getConfig("Scattering").showCurvature + onCheckedChanged: { Render.getConfig("Scattering").showCurvature = checked } + } + CheckBox { + text: "Show Diffused Normal" + checked: Render.getConfig("Scattering").showDiffusedNormal + onCheckedChanged: { Render.getConfig("Scattering").showDiffusedNormal = checked } + } + Repeater { + model: [ "Scattering Bent Red:Scattering:bentRed:2.0", + "Scattering Bent Green:Scattering:bentGreen:2.0", + "Scattering Bent Blue:Scattering:bentBlue:2.0", + "Scattering Bent Scale:Scattering:bentScale:5.0", + "Scattering Curvature Offset:Scattering:curvatureOffset:1.0", + "Scattering Curvature Scale:Scattering:curvatureScale:2.0", + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: false + config: Render.getConfig(modelData.split(":")[1]) + property: modelData.split(":")[2] + max: modelData.split(":")[3] + min: 0.0 + } + } + CheckBox { + text: "Scattering Profile" + checked: Render.getConfig("DebugScattering").showProfile + onCheckedChanged: { Render.getConfig("DebugScattering").showProfile = checked } + } + CheckBox { + text: "Scattering Table" + checked: Render.getConfig("DebugScattering").showLUT + onCheckedChanged: { Render.getConfig("DebugScattering").showLUT = checked } + } + CheckBox { + text: "Cursor Pixel" + checked: Render.getConfig("DebugScattering").showCursorPixel + onCheckedChanged: { Render.getConfig("DebugScattering").showCursorPixel = checked } + } + } + } +} From 759b3065fc8693bca06b89134b7a91a87cdccec4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 30 Jun 2016 20:48:19 +1200 Subject: [PATCH 0846/1237] Distinguish between file name and directory as prompt in file chooser --- interface/resources/qml/dialogs/FileDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 5372028da5..fa5be18cd3 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -603,7 +603,7 @@ ModalWindow { TextField { id: currentSelection - label: "Path:" + label: selectDirectory ? "Directory:" : "File name:" anchors { left: parent.left right: selectionType.visible ? selectionType.left: parent.right From be102ba9265a8915ec947414bae05fb43933704f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 30 Jun 2016 10:05:57 -0700 Subject: [PATCH 0847/1237] remove unused variable --- .../display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 8650c086e3..18f39cd3df 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -650,7 +650,6 @@ QImage OpenGLDisplayPlugin::getScreenshot() const { using namespace oglplus; QImage screenshot(_compositeFramebuffer->size.x, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); withMainThreadContext([&] { - auto windowSize = toGlm(_container->getPrimaryWidget()->size()); Framebuffer::Bind(Framebuffer::Target::Read, _compositeFramebuffer->fbo); Context::ReadPixels(0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); }); From 057fc8adcecfc059ebfc9898da3cc9b174e4c1aa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 30 Jun 2016 10:25:49 -0700 Subject: [PATCH 0848/1237] endable blending --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ca6a03f93b..f1f300c72b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -439,6 +439,8 @@ void HmdDisplayPlugin::internalPresent() { _firstPreview = false; } useProgram(_previewProgram); + glEnable (GL_BLEND); + glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); glViewport(targetViewportPosition.x, targetViewportPosition.y, targetViewportSize.x, targetViewportSize.y); From 1f451269cd7b67bd94554a21f85830fefd92e546 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 13:22:01 -0700 Subject: [PATCH 0849/1237] Allow scripts to receive a visibility changed signal from their windows --- libraries/ui/src/QmlWindowClass.cpp | 1 + libraries/ui/src/QmlWindowClass.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index c0eba4abf3..7d56e51495 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -123,6 +123,7 @@ void QmlWindowClass::initQml(QVariantMap properties) { // Forward messages received from QML on to the script connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection); + connect(_qmlWindow, SIGNAL(visibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); }); } Q_ASSERT(_qmlWindow); diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index e1865b133a..c30027df6e 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -24,7 +24,7 @@ class QmlWindowClass : public QObject { Q_OBJECT Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize NOTIFY sizeChanged) - Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); @@ -52,7 +52,7 @@ public slots: void sendToQml(const QVariant& message); signals: - void visibilityChanged(bool visible); // Tool window + void visibleChanged(); void positionChanged(); void sizeChanged(); void moved(glm::vec2 position); From 82b1cba81dcba949e1eaf3095c110a72ad90176e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 30 Jun 2016 13:58:44 -0700 Subject: [PATCH 0850/1237] equip improvements Allow the user to equip an object while near or far grabbing it from the other hand. --- scripts/system/controllers/handControllerGrab.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index d37602f8e6..7a0bfc85cf 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1144,10 +1144,11 @@ function MyController(hand) { var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + // Controller.Standard.LeftHand var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; - if (refCount > 0) { + if (refCount > 0 && this.getOtherHandController().grabbedEntity != hotspot.entityID) { if (debug) { - print("equip is skipping '" + props.name + "': it is already grabbed"); + print("equip is skipping '" + props.name + "': grabbed by someone else"); } return false; } @@ -1698,6 +1699,12 @@ function MyController(hand) { this.grabbedEntity = saveGrabbedID; } + var otherHandController = this.getOtherHandController(); + if (otherHandController.grabbedEntity == this.grabbedEntity && + (otherHandController.state == STATE_NEAR_GRABBING || otherHandController.state == STATE_DISTANCE_HOLDING)) { + otherHandController.setState(STATE_OFF, "other hand grabbed this entity"); + } + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); this.activateEntity(this.grabbedEntity, grabbedProperties, false); @@ -1818,7 +1825,6 @@ function MyController(hand) { return; } - var now = Date.now(); if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * CHECK_TOO_FAR_UNEQUIP_TIME) { this.lastUnequipCheckTime = now; @@ -2176,6 +2182,10 @@ function MyController(hand) { } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; + + this.getOtherHandController = function() { + return (this.hand === RIGHT_HAND) ? leftController : rightController; + }; } var rightController = new MyController(RIGHT_HAND); From 5b343c9b82e322f6452c4e3f48244fe55272457e Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Thu, 30 Jun 2016 14:18:07 -0700 Subject: [PATCH 0851/1237] Fix eslint descrepencies Prohibit trailing commas, and fix function spacing rule to be more in line with the coding standard. Also prohibit leading decimal points, also specified in coding standard. --- .eslintrc.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e866713b11..a7f4291257 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,13 +52,14 @@ module.exports = { }, "rules": { "brace-style": ["error", "1tbs", { "allowSingleLine": false }], - "comma-dangle": ["error", "only-multiline"], + "comma-dangle": ["error", "never"], "camelcase": ["error"], "curly": ["error", "all"], "indent": ["error", 4, { "SwitchCase": 1 }], "keyword-spacing": ["error", { "before": true, "after": true }], "max-len": ["error", 128, 4], "new-cap": ["error"], + "no-floating-decimal": ["error"], //"no-magic-numbers": ["error", { "ignore": [0, 1], "ignoreArrayIndexes": true }], "no-multiple-empty-lines": ["error"], "no-multi-spaces": ["error"], @@ -67,6 +68,6 @@ module.exports = { "spaced-comment": ["error", "always", { "line": { "markers": ["/"] } }], - "space-before-function-paren": ["error", "never"] + "space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}] } }; From dfb6cb1aa340f04d4116efc8c621fa3d08f08aca Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 14:25:36 -0700 Subject: [PATCH 0852/1237] Updating entity list --- scripts/system/libraries/entityList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index e9baeac86c..448293a2ac 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -111,8 +111,8 @@ EntityListTool = function(opts) { } }); - webView.visibilityChanged.connect(function (visible) { - if (visible) { + webView.visibleChanged.connect(function () { + if (webView.visible) { that.sendUpdate(); } }); From 38776fe007220cc7b57fdc68e2ac975a359ae22e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 30 Jun 2016 14:35:27 -0700 Subject: [PATCH 0853/1237] Fix multiple sessions being created for some users If a user didn't have send-data activated they don't have their session id sent in the header of requests, but instead send it inside the request body. The session id being send in the request included curly braces which the server considers invalid. --- interface/src/DiscoverabilityManager.cpp | 6 ++---- libraries/networking/src/AccountManager.h | 3 +++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index c4d985419e..6550edff18 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -80,8 +80,7 @@ void DiscoverabilityManager::updateLocation() { locationObject.insert(FRIENDS_ONLY_KEY_IN_LOCATION, (_mode.get() == Discoverability::Friends)); // if we have a session ID add it now, otherwise add a null value - auto sessionID = accountManager->getSessionID(); - rootObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); + rootObject[SESSION_ID_KEY] = accountManager->getSessionIDWithoutCurlyBraces(); JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = this; @@ -111,8 +110,7 @@ void DiscoverabilityManager::updateLocation() { callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; QJsonObject heartbeatObject; - auto sessionID = accountManager->getSessionID(); - heartbeatObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); + heartbeatObject[SESSION_ID_KEY] = accountManager->getSessionIDWithoutCurlyBraces(); accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index c12f663d3e..7a8d2a72fa 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -24,6 +24,8 @@ #include +#include "UUID.h" + class JSONCallbackParameters { public: JSONCallbackParameters(QObject* jsonCallbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), @@ -86,6 +88,7 @@ public: static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); + QString getSessionIDWithoutCurlyBraces() const { return uuidStringWithoutCurlyBraces(_sessionID); } QUuid getSessionID() const { return _sessionID; } void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } From 49da7d9a61beae00f125694ca86dcc45c8a6b526 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 30 Jun 2016 14:43:27 -0700 Subject: [PATCH 0854/1237] Remove sending of session id in request body in favor of header --- interface/src/DiscoverabilityManager.cpp | 9 +-------- libraries/networking/src/AccountManager.cpp | 8 ++------ libraries/networking/src/AccountManager.h | 1 - 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 6550edff18..4051bd8a1e 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -79,9 +79,6 @@ void DiscoverabilityManager::updateLocation() { const QString FRIENDS_ONLY_KEY_IN_LOCATION = "friends_only"; locationObject.insert(FRIENDS_ONLY_KEY_IN_LOCATION, (_mode.get() == Discoverability::Friends)); - // if we have a session ID add it now, otherwise add a null value - rootObject[SESSION_ID_KEY] = accountManager->getSessionIDWithoutCurlyBraces(); - JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = this; callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; @@ -109,12 +106,8 @@ void DiscoverabilityManager::updateLocation() { callbackParameters.jsonCallbackReceiver = this; callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; - QJsonObject heartbeatObject; - heartbeatObject[SESSION_ID_KEY] = accountManager->getSessionIDWithoutCurlyBraces(); - accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, - QNetworkAccessManager::PutOperation, callbackParameters, - QJsonDocument(heartbeatObject).toJson()); + QNetworkAccessManager::PutOperation, callbackParameters); } } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index e49bf5d5a5..ed325b8d69 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -220,12 +220,8 @@ void AccountManager::sendRequest(const QString& path, networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); - // if we're allowed to send usage data, include whatever the current session ID is with this request - auto& activityLogger = UserActivityLogger::getInstance(); - if (activityLogger.isEnabled()) { - networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER, - uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); - } + networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER, + uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); QUrl requestURL = _authURL; diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 7a8d2a72fa..cdf7e3f1cd 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -88,7 +88,6 @@ public: static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); - QString getSessionIDWithoutCurlyBraces() const { return uuidStringWithoutCurlyBraces(_sessionID); } QUuid getSessionID() const { return _sessionID; } void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } From dd73f0c333116a1b78c8553206b8cc6262b15dec Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 30 Jun 2016 14:50:29 -0700 Subject: [PATCH 0855/1237] Remove unused header from AccountManager.h --- libraries/networking/src/AccountManager.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index cdf7e3f1cd..c12f663d3e 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -24,8 +24,6 @@ #include -#include "UUID.h" - class JSONCallbackParameters { public: JSONCallbackParameters(QObject* jsonCallbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), From 86f8b578412bf0f905aa9df05f10bf794a72dc9b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 15:16:28 -0700 Subject: [PATCH 0856/1237] Highlight controls based on activeFocus, not focus --- interface/resources/qml/controls-uit/SpinBox.qml | 12 ++++++------ interface/resources/qml/controls-uit/TextField.qml | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controls-uit/SpinBox.qml index 5d78cfc5a8..a1237d4bc7 100755 --- a/interface/resources/qml/controls-uit/SpinBox.qml +++ b/interface/resources/qml/controls-uit/SpinBox.qml @@ -36,15 +36,15 @@ SpinBox { id: spinStyle background: Rectangle { color: isLightColorScheme - ? (spinBox.focus ? hifi.colors.white : hifi.colors.lightGray) - : (spinBox.focus ? hifi.colors.black : hifi.colors.baseGrayShadow) + ? (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGray) + : (spinBox.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow) border.color: spinBoxLabelInside.visible ? spinBoxLabelInside.color : hifi.colors.primaryHighlight - border.width: spinBox.focus ? spinBoxLabelInside.visible ? 2 : 1 : 0 + border.width: spinBox.activeFocus ? spinBoxLabelInside.visible ? 2 : 1 : 0 } textColor: isLightColorScheme - ? (spinBox.focus ? hifi.colors.black : hifi.colors.lightGray) - : (spinBox.focus ? hifi.colors.white : hifi.colors.lightGrayText) + ? (spinBox.activeFocus ? hifi.colors.black : hifi.colors.lightGray) + : (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGrayText) selectedTextColor: hifi.colors.black selectionColor: hifi.colors.primaryHighlight @@ -96,7 +96,7 @@ SpinBox { anchors.fill: parent propagateComposedEvents: true onWheel: { - if(spinBox.focus) + if(spinBox.activeFocus) wheel.accepted = false else wheel.accepted = true diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index 79027cdc3b..65fab00700 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -36,14 +36,14 @@ TextField { style: TextFieldStyle { textColor: isLightColorScheme - ? (textField.focus ? hifi.colors.black : hifi.colors.lightGray) - : (textField.focus ? hifi.colors.white : hifi.colors.lightGrayText) + ? (textField.activeFocus ? hifi.colors.black : hifi.colors.lightGray) + : (textField.activeFocus ? hifi.colors.white : hifi.colors.lightGrayText) background: Rectangle { color: isLightColorScheme - ? (textField.focus ? hifi.colors.white : hifi.colors.textFieldLightBackground) - : (textField.focus ? hifi.colors.black : hifi.colors.baseGrayShadow) + ? (textField.activeFocus ? hifi.colors.white : hifi.colors.textFieldLightBackground) + : (textField.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow) border.color: hifi.colors.primaryHighlight - border.width: textField.focus ? 1 : 0 + border.width: textField.activeFocus ? 1 : 0 radius: isSearchField ? textField.height / 2 : 0 HiFiGlyphs { From ce54f1b1d12dda0ff2b9ea14fff0d6f9d2a01311 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 15:18:14 -0700 Subject: [PATCH 0857/1237] Improve desktop 'unfocus' functionality --- interface/resources/qml/desktop/Desktop.qml | 26 ++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 27fa9692b9..59d5b435ba 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -30,6 +30,7 @@ FocusScope { if (!repositionLocked) { d.handleSizeChanged(); } + } onHeightChanged: d.handleSizeChanged(); @@ -66,7 +67,6 @@ FocusScope { if (desktop.repositionLocked) { return; } - var oldRecommendedRect = recommendedRect; var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedOverlayRect(); var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, @@ -311,8 +311,8 @@ FocusScope { onPinnedChanged: { if (pinned) { - nullFocus.focus = true; - nullFocus.forceActiveFocus(); + desktop.focus = true; + desktop.forceActiveFocus(); // recalculate our non-pinned children hiddenChildren = d.findMatchingChildren(desktop, function(child){ @@ -486,17 +486,31 @@ FocusScope { } function unfocusWindows() { + // First find the active focus item, and unfocus it, all the way + // up the parent chain to the window + var currentFocus = offscreenWindow.activeFocusItem; + var targetWindow = d.getDesktopWindow(currentFocus); + while (currentFocus) { + if (currentFocus === targetWindow) { + break; + } + currentFocus.focus = false; + currentFocus = currentFocus.parent; + } + + // Unfocus all windows var windows = d.getTopLevelWindows(); for (var i = 0; i < windows.length; ++i) { windows[i].focus = false; } + + // For the desktop to have active focus desktop.focus = true; + desktop.forceActiveFocus(); } FocusHack { id: focusHack; } - FocusScope { id: nullFocus; } - Rectangle { id: focusDebugger; objectName: "focusDebugger" @@ -510,5 +524,5 @@ FocusScope { enabled: DebugQML onTriggered: focusDebugger.visible = !focusDebugger.visible } - + } From 167cb72a0ef03aadef3ba528f15202ee09ed4b14 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 15:18:59 -0700 Subject: [PATCH 0858/1237] Don't auto-focus the filter field in running scripts --- interface/resources/qml/hifi/dialogs/RunningScripts.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 5457caccf1..3f05a140ae 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -53,11 +53,6 @@ ScrollingWindow { updateRunningScripts(); } - function setDefaultFocus() { - // Work around FocusScope of scrollable window. - filterEdit.forceActiveFocus(); - } - function updateRunningScripts() { var runningScripts = ScriptDiscoveryService.getRunning(); runningScriptsModel.clear() @@ -276,7 +271,6 @@ ScrollingWindow { isSearchField: true anchors.left: parent.left anchors.right: parent.right - focus: true colorScheme: hifi.colorSchemes.dark placeholderText: "Filter" onTextChanged: scriptsModel.filterRegExp = new RegExp("^.*" + text + ".*$", "i") From 14784a60888b4584d31db40d9c794ae9f653ee21 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 15:22:13 -0700 Subject: [PATCH 0859/1237] Remove default focus on text fields --- scripts/developer/tests/playaPerformanceTest.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/developer/tests/playaPerformanceTest.qml b/scripts/developer/tests/playaPerformanceTest.qml index cd8171a0f6..028cfc9e1a 100644 --- a/scripts/developer/tests/playaPerformanceTest.qml +++ b/scripts/developer/tests/playaPerformanceTest.qml @@ -125,7 +125,6 @@ Rectangle { TextField { id: addressLine - focus: true anchors { left: parent.left; right: parent.right; top: row.bottom; margins: 16; From b9f9b180838a94d86c821b8d0bd6fb00b1e2ba25 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 30 Jun 2016 15:41:58 -0700 Subject: [PATCH 0860/1237] if an edit packet doesn't include a change to lifetime, don't attempt to cap the lifetime --- libraries/entities/src/EntityTree.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 21e5865c09..ef0401ceaf 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -904,7 +904,9 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endDecode = usecTimestampNow(); const quint64 LAST_EDITED_SERVERSIDE_BUMP = 1; // usec - if (!senderNode->getCanRez() && senderNode->getCanRezTmp()) { + if ((message.getType() == PacketType::EntityAdd || + (message.getType() == PacketType::EntityEdit && properties.lifetimeChanged())) && + !senderNode->getCanRez() && senderNode->getCanRezTmp()) { // this node is only allowed to rez temporary entities. if need be, cap the lifetime. if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || properties.getLifetime() > _maxTmpEntityLifetime) { From 69ffd63ba702417de9edc2b33bd24c05e9dee125 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 30 Jun 2016 15:42:48 -0700 Subject: [PATCH 0861/1237] fix snapshot sharing window --- interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml | 2 +- interface/src/Application.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml index 3dacb3b39c..f99b770a78 100644 --- a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml +++ b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml @@ -7,7 +7,7 @@ import "../../windows" import "../../js/Utils.js" as Utils import "../models" -ScrollingWindow { +Window { id: root resizable: true width: 516 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5978461dcf..1c9ec94dc4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4998,7 +4998,6 @@ void Application::takeSnapshot() { DependencyManager::get()->load("hifi/dialogs/SnapshotShareDialog.qml", [=](QQmlContext*, QObject* dialog) { dialog->setProperty("source", QUrl::fromLocalFile(fileName)); - connect(dialog, SIGNAL(uploadSnapshot(const QString& snapshot)), this, SLOT(uploadSnapshot(const QString& snapshot))); }); } From d8e5e3cbe4b1c7af668193d738e6ef0f941de7fa Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 30 Jun 2016 16:00:18 -0700 Subject: [PATCH 0862/1237] equip bug fix you should not be able to equip an object that is already equipped, with your other hand. --- scripts/system/controllers/handControllerGrab.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 7a0bfc85cf..ecece8c4f7 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1144,9 +1144,11 @@ function MyController(hand) { var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - // Controller.Standard.LeftHand var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; - if (refCount > 0 && this.getOtherHandController().grabbedEntity != hotspot.entityID) { + var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || + this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && + this.getOtherHandController().grabbedEntity == hotspot.entityID); + if (refCount > 0 && !okToEquipFromOtherHand) { if (debug) { print("equip is skipping '" + props.name + "': grabbed by someone else"); } From 82e074de0fe1c1dc08eb55170ab19b3a23b2508d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 30 Jun 2016 16:05:44 -0700 Subject: [PATCH 0863/1237] working teleporter --- scripts/system/controllers/teleport.js | 359 +++++++++++++++++++++---- 1 file changed, 300 insertions(+), 59 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index e2d0dcff8d..5ad09d53a6 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -6,86 +6,295 @@ //release trigger to teleport then exit teleport mode //if thumb is release, exit teleport mode - //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? -//v2: haptic feedback -//v2: particles for parabolic arc to landing spot +//v2: haptic feedbackasd -//parabola: point count, point spacing, graphic thickness +var inTeleportMode = false; -//Standard.LeftPrimaryThumb -//Standard.LT +function ThumbPad(hand) { + this.hand = hand; + var _this = this; + this.buttonPress = function(value) { + print('jbp pad press: ' + value + " on: " + _this.hand) + _this.buttonValue = value; + }; -//create a controller mapping and make sure to disable it when the script is stopped -var teleportMapping = Controller.newMapping('Hifi-Teleporter-Dev-' + Math.random()); -Script.scriptEnding.connect(teleportMapping.disable); - -// peek at the trigger and thumbs to store their values -teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); -teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); -teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); -teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - - -function ThumbPad() { + this.down = function() { + return _this.buttonValue === 1 ? 1.0 : 0.0; + }; } -ThumbPad.prototype = { - buttonPress: function(value) { - print('pad press: ' + value) - this.buttonValue = value; - }, - down: function() { - return this.buttonValue === 0 ? true : false; - } +function Trigger(hand) { + this.hand = hand; + var _this = this; + + this.buttonPress = function(value) { + print('jbp trigger press: ' + value + " on: " + _this.hand) + _this.buttonValue = value; + + }; + + this.down = function() { + return _this.buttonValue === 1 ? 1.0 : 0.0; + }; } -function Trigger() { - -} - -Trigger.prototype = { - buttonPress: function(value) { - print('trigger press: ' + value) - this.buttonValue = value; - }, - down: function() { - return this.buttonValue === 0 ? true : false; - } -} - -teleportMapping.from(leftPad.down).when(leftTrigger.down).to(teleporter.enterTeleportMode); -teleportMapping.from(rightPad.down).when(rightTrigger.down).to(teleporter.enterTeleportMode); - function Teleporter() { + var _this = this; + this.targetProps = null; + this.rightOverlayLine = null; + this.leftOverlayLine = null; + this.initialize = function() { + print('jbp initialize') + this.createMappings(); + this.disableGrab(); + _this.targetEntity = Entities.addEntity({ + name: 'Hifi-Teleporter-Target-Entity', + position: MyAvatar.position, + type: 'Sphere', + dimensions: { + x: 0.2, + y: 0.2, + z: 0.2 + }, + color: { + red: 255, + green: 255, + blue: 255 + }, + collisionless: true, + collidesWith: '', + visible: true + }); + }; -} -Teleporter.prototype = { - enterTeleportMode: function(value) { - print('value on enter: ' + value); - }, - exitTeleportMode: function(value) { - print('value on exit: ' + value); - }, - teleport: function(value) { + this.createMappings = function() { + print('jbp create mappings internal'); + // peek at the trigger and thumbs to store their values + teleporter.telporterMappingInternalName='Hifi-Teleporter-Internal-Dev-' + Math.random(); + teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); + + Controller.enableMapping(teleporter.telporterMappingInternalName); + }; + + this.disableMappings = function() { + print('jbp disable mappings internal') + Controller.disableMapping(teleporter.telporterMappingInternalName); + }; + + this.enterTeleportMode = function(hand) { + if (inTeleportMode === true) { + return + } + print('jbp hand on entering teleport mode: ' + hand); + inTeleportMode = true; + this.teleportHand = hand; + this.initialize(); + this.updateConnected = true; + Script.update.connect(this.update); + }; + + this.exitTeleportMode = function(value) { + print('jbp value on exit: ' + value); + this.disableMappings(); + this.rightOverlayOff(); + this.leftOverlayOff(); + Entities.deleteEntity(_this.targetEntity); + this.enableGrab(); + Script.update.disconnect(this.update); + this.updateConnected = false; + inTeleportMode = false; + + }; + + this.update = function() { + //print('in teleporter update') + if (rightPad.buttonValue === 0 || leftPad.buttonValue === 0) { + print('JBP THUMB RELEASED SHOULD EXIT') + _this.exitTeleportMode(); + return; + } + + if (teleporter.teleportHand === 'left') { + teleporter.leftRay(); + if (leftTrigger.buttonValue === 0) { + _this.teleport(); + } + } else { + teleporter.rightRay(); + if (rightTrigger.buttonValue === 0) { + _this.teleport(); + } + } + + + }; + + this.rightRay = function() { + + var rightPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).translation), MyAvatar.position); + + var rightRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).rotation) + + var rightPickRay = { + origin: rightPosition, + direction: Quat.getUp(rightRotation), + }; + + this.rightPickRay = rightPickRay; + + var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); + this.rightLineOn(rightPickRay.origin, location, { + red: 255, + green: 0, + blue: 0 + }); + var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); + + if (rightIntersection.intersects) { + this.updateTargetEntity(rightIntersection); + }; + }; + + this.leftRay = function() { + var leftPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).translation), MyAvatar.position); + + + var leftRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).rotation) + var leftPickRay = { + origin: leftPosition, + direction: Quat.getUp(leftRotation), + }; + + this.leftPickRay = leftPickRay; + + var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); + this.leftLineOn(leftPickRay.origin, location, { + red: 0, + green: 255, + blue: 0 + }); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); + + if (leftIntersection.intersects) { + this.updateTargetEntity(leftIntersection); + }; + }; + + this.rightLineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.rightOverlayLine === null) { + var lineProperties = { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + ignoreRayIntersection: true, // always ignore this + visible: true, + alpha: 1 + }; + + this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.rightOverlayLine, { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + visible: true, + ignoreRayIntersection: true, // always ignore this + alpha: 1 + }); + } + }; + + this.leftLineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.leftOverlayLine === null) { + var lineProperties = { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + ignoreRayIntersection: true, // always ignore this + visible: true, + alpha: 1 + }; + + this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.leftOverlayLine, { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + visible: true, + ignoreRayIntersection: true, // always ignore this + alpha: 1 + }); + } + }; + + this.rightOverlayOff = function() { + if (this.rightOverlayLine !== null) { + Overlays.deleteOverlay(this.rightOverlayLine); + this.rightOverlayLine=null; + } + }; + + this.leftOverlayOff = function() { + if (this.leftOverlayLine !== null) { + Overlays.deleteOverlay(this.leftOverlayLine); + this.leftOverlayLine=null; + } + }; + + this.updateTargetEntity = function(intersection) { + var targetProps = Entities.getEntityProperties(this.targetEntity); + var position = { + x: intersection.intersection.x, + y: intersection.intersection.y + targetProps.dimensions.y / 2, + z: intersection.intersection.z + } + Entities.editEntity(this.targetEntity, { + position: position + }); + + + }; + + this.disableGrab = function() { + Messages.sendLocalMessage('Hifi-Hand-Disabler', 'both'); + }; + + this.enableGrab = function() { + Messages.sendLocalMessage('Hifi-Hand-Disabler', 'none'); + }; + + this.teleport = function(value) { //todo //get the y position of the teleport landing spot print('value on teleport: ' + value) - var properties = Entities.getEntityProperties(_this.entityID); + var properties = Entities.getEntityProperties(teleporter.targetEntity); var offset = getAvatarFootOffset(); properties.position.y += offset; print('OFFSET IS::: ' + JSON.stringify(offset)) print('TELEPORT POSITION IS:: ' + JSON.stringify(properties.position)); MyAvatar.position = properties.position; - } + + this.exitTeleportMode(); + }; } +//related to repositioning the avatar after you teleport function getAvatarFootOffset() { var data = getJointData(); var upperLeg, lowerLeg, foot, toe, toeTop; @@ -133,8 +342,40 @@ function getJointData() { } -var leftPad = new ThumbPad(); -var rightPad = new ThumbPad(); -var leftTrigger = new Trigger(); -var rightTrigger = new Trigger(); -var teleporter = new Teleporter(); \ No newline at end of file +var leftPad = new ThumbPad('left'); +var rightPad = new ThumbPad('right'); +var leftTrigger = new Trigger('left'); +var rightTrigger = new Trigger('right'); + +//create a controller mapping and make sure to disable it when the script is stopped + +var mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); +var teleportMapping = Controller.newMapping(mappingName); +teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); +teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); +teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); +teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + +teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { + teleporter.enterTeleportMode('left') +}); +teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { + teleporter.enterTeleportMode('right') +}); + +var teleporter = new Teleporter(); + +Controller.enableMapping(mappingName); + +Script.scriptEnding.connect(cleanup); + +function cleanup() { + teleportMapping.disable(); + teleporter.disableMappings(); + teleporter.rightOverlayOff(); + teleporter.leftOverlayOff(); + Entities.deleteEntity(teleporter.targetEntity); + if (teleporter.updateConnected !== null) { + Script.update.disconnect(teleporter.update); + } +} \ No newline at end of file From b7926b8582829bc3ac444b401b5548f7872c21d8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 16:11:05 -0700 Subject: [PATCH 0864/1237] Don't render wire geometry with lighting effects --- interface/src/ui/overlays/Circle3DOverlay.cpp | 7 +++++-- interface/src/ui/overlays/Cube3DOverlay.cpp | 6 +++--- interface/src/ui/overlays/Grid3DOverlay.cpp | 5 +++-- interface/src/ui/overlays/Image3DOverlay.cpp | 8 +++++--- interface/src/ui/overlays/Line3DOverlay.cpp | 9 ++++++--- interface/src/ui/overlays/Rectangle3DOverlay.cpp | 8 +++++--- interface/src/ui/overlays/Sphere3DOverlay.cpp | 4 ++-- interface/src/ui/overlays/Web3DOverlay.cpp | 4 +++- libraries/render-utils/src/GeometryCache.cpp | 5 +++++ libraries/render-utils/src/GeometryCache.h | 8 +++++--- 10 files changed, 42 insertions(+), 22 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 8fb3a36919..1abcb0f5c0 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -97,7 +97,7 @@ void Circle3DOverlay::render(RenderArgs* args) { _lastColor = colorX; auto geometryCache = DependencyManager::get(); - + Q_ASSERT(args->_batch); auto& batch = *args->_batch; @@ -149,6 +149,7 @@ void Circle3DOverlay::render(RenderArgs* args) { geometryCache->updateVertices(_quadVerticesID, points, color); } + geometryCache->bindSimpleProgram(batch); geometryCache->renderVertices(batch, gpu::TRIANGLES, _quadVerticesID); } else { @@ -187,6 +188,8 @@ void Circle3DOverlay::render(RenderArgs* args) { geometryCache->updateVertices(_lineVerticesID, points, color); } + // Render unlit + geometryCache->bindSimpleProgram(batch, false, false, true); if (getIsDashedLine()) { geometryCache->renderVertices(batch, gpu::LINES, _lineVerticesID); } else { @@ -279,7 +282,7 @@ void Circle3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Circle3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace(); + auto builder = render::ShapeKey::Builder().withOwnPipeline(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index 38650f9fda..17894aa091 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -48,7 +48,7 @@ void Cube3DOverlay::render(RenderArgs* args) { auto geometryCache = DependencyManager::get(); auto pipeline = args->_pipeline; if (!pipeline) { - pipeline = geometryCache->getShapePipeline(); + pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline(); } if (_isSolid) { @@ -56,7 +56,7 @@ void Cube3DOverlay::render(RenderArgs* args) { batch->setModelTransform(transform); geometryCache->renderSolidCubeInstance(*batch, cubeColor, pipeline); } else { - + geometryCache->bindSimpleProgram(*batch, false, false, true, true); if (getIsDashedLine()) { transform.setScale(1.0f); batch->setModelTransform(transform); @@ -97,7 +97,7 @@ void Cube3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Cube3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder(); + auto builder = render::ShapeKey::Builder().withOwnPipeline(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } diff --git a/interface/src/ui/overlays/Grid3DOverlay.cpp b/interface/src/ui/overlays/Grid3DOverlay.cpp index edc27e35f2..7d86395937 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.cpp +++ b/interface/src/ui/overlays/Grid3DOverlay.cpp @@ -75,10 +75,11 @@ void Grid3DOverlay::render(RenderArgs* args) { transform.setScale(glm::vec3(getDimensions(), 1.0f)); transform.setTranslation(position); batch->setModelTransform(transform); - + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(*batch, false, false, true, true); const float MINOR_GRID_EDGE = 0.0025f; const float MAJOR_GRID_EDGE = 0.005f; - DependencyManager::get()->renderGrid(*batch, minCorner, maxCorner, + geometryCache->renderGrid(*batch, minCorner, maxCorner, _minorGridRowDivisions, _minorGridColDivisions, MINOR_GRID_EDGE, _majorGridRowDivisions, _majorGridColDivisions, MAJOR_GRID_EDGE, gridColor, _drawInFront); diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index fd0d2bcedf..72ae246859 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -92,8 +92,10 @@ void Image3DOverlay::render(RenderArgs* args) { batch->setModelTransform(transform); batch->setResourceTexture(0, _texture->getGPUTexture()); - - DependencyManager::get()->renderQuad( + + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(*batch, true, false); + geometryCache->renderQuad( *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) ); @@ -102,7 +104,7 @@ void Image3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Image3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace().withDepthBias(); + auto builder = render::ShapeKey::Builder().withOwnPipeline(); if (_emissive) { builder.withUnlit(); } diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 3971e91211..d53b287f76 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -54,17 +54,20 @@ void Line3DOverlay::render(RenderArgs* args) { if (batch) { batch->setModelTransform(_transform); + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(*batch, false, false, true, true); if (getIsDashedLine()) { // TODO: add support for color to renderDashedLine() - DependencyManager::get()->renderDashedLine(*batch, _start, _end, colorv4, _geometryCacheID); + geometryCache->renderDashedLine(*batch, _start, _end, colorv4, _geometryCacheID); } else { - DependencyManager::get()->renderLine(*batch, _start, _end, colorv4, _geometryCacheID); + + geometryCache->renderLine(*batch, _start, _end, colorv4, _geometryCacheID); } } } const render::ShapeKey Line3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace(); + auto builder = render::ShapeKey::Builder().withOwnPipeline(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.cpp b/interface/src/ui/overlays/Rectangle3DOverlay.cpp index 5a541fd58a..75d7ec565c 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.cpp +++ b/interface/src/ui/overlays/Rectangle3DOverlay.cpp @@ -53,13 +53,15 @@ void Rectangle3DOverlay::render(RenderArgs* args) { transform.setRotation(rotation); batch->setModelTransform(transform); + auto geometryCache = DependencyManager::get(); if (getIsSolid()) { glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, 0.0f); glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, 0.0f); - DependencyManager::get()->renderQuad(*batch, topLeft, bottomRight, rectangleColor); + geometryCache->bindSimpleProgram(*batch); + geometryCache->renderQuad(*batch, topLeft, bottomRight, rectangleColor); } else { - auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(*batch, false, false, true, true); if (getIsDashedLine()) { glm::vec3 point1(-halfDimensions.x, -halfDimensions.y, 0.0f); glm::vec3 point2(halfDimensions.x, -halfDimensions.y, 0.0f); @@ -89,7 +91,7 @@ void Rectangle3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Rectangle3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder(); + auto builder = render::ShapeKey::Builder().withOwnPipeline(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index d50f2f7285..1dd61611f2 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -46,7 +46,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { auto geometryCache = DependencyManager::get(); auto pipeline = args->_pipeline; if (!pipeline) { - pipeline = geometryCache->getShapePipeline(); + pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline(); } if (_isSolid) { @@ -58,7 +58,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Sphere3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder(); + auto builder = render::ShapeKey::Builder().withOwnPipeline(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index f5baecd96a..c9c24d3ab6 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -100,7 +100,9 @@ void Web3DOverlay::render(RenderArgs* args) { } batch.setModelTransform(transform); - DependencyManager::get()->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color); + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(batch, true, false, true, false); + geometryCache->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color); batch.setResourceTexture(0, args->_whiteTexture); // restore default white color after me } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 1154f27ee0..9ea4bd9905 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -309,6 +309,7 @@ gpu::Stream::FormatPointer& getInstancedSolidStreamFormat() { } render::ShapePipelinePointer GeometryCache::_simplePipeline; +render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; GeometryCache::GeometryCache() : _nextID(0) @@ -324,6 +325,10 @@ GeometryCache::GeometryCache() : DependencyManager::get()->getNormalFittingTexture()); } ); + GeometryCache::_simpleWirePipeline = + std::make_shared(getSimplePipeline(false, false, true, true), nullptr, + [](const render::ShapePipeline&, gpu::Batch& batch) { } + ); } GeometryCache::~GeometryCache() { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index c46a9bb084..9f18f1644c 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -157,7 +157,8 @@ public: gpu::PipelinePointer getSimplePipeline(bool textured = false, bool culled = true, bool unlit = false, bool depthBias = false); render::ShapePipelinePointer getShapePipeline() { return GeometryCache::_simplePipeline; } - + render::ShapePipelinePointer getWireShapePipeline() { return GeometryCache::_simpleWirePipeline; } + // Static (instanced) geometry void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); @@ -179,7 +180,7 @@ public: void renderWireSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simplePipeline); void renderWireSphereInstance(gpu::Batch& batch, const glm::vec3& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline) { + const render::ShapePipelinePointer& pipeline = _simpleWirePipeline) { renderWireSphereInstance(batch, glm::vec4(color, 1.0f), pipeline); } @@ -193,7 +194,7 @@ public: void renderWireCubeInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simplePipeline); void renderWireCubeInstance(gpu::Batch& batch, const glm::vec3& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline) { + const render::ShapePipelinePointer& pipeline = _simpleWirePipeline) { renderWireCubeInstance(batch, glm::vec4(color, 1.0f), pipeline); } @@ -401,6 +402,7 @@ private: gpu::ShaderPointer _simpleShader; gpu::ShaderPointer _unlitShader; static render::ShapePipelinePointer _simplePipeline; + static render::ShapePipelinePointer _simpleWirePipeline; QHash _simplePrograms; }; From ff55644fe859a72da6e16e5b15d92f1e5da171b8 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 30 Jun 2016 16:24:11 -0700 Subject: [PATCH 0865/1237] stateful hud toolbar buttons --- interface/resources/qml/AddressBarDialog.qml | 1 + .../DialogsManagerScriptingInterface.cpp | 2 ++ .../DialogsManagerScriptingInterface.h | 1 + interface/src/ui/AddressBarDialog.cpp | 4 ++++ interface/src/ui/AddressBarDialog.h | 1 + interface/src/ui/DialogsManager.h | 2 ++ scripts/system/examples.js | 18 +++++++++++------- scripts/system/goto.js | 16 +++++++++++----- scripts/system/hmd.js | 19 ++++++++++++------- scripts/system/mute.js | 19 ++++++++++++++----- 10 files changed, 59 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index a48804faba..dc060b70e1 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -30,6 +30,7 @@ Window { width: addressBarDialog.implicitWidth height: addressBarDialog.implicitHeight + onShownChanged: addressBarDialog.observeShownChanged(shown); Component.onCompleted: { root.parentChanged.connect(center); center(); diff --git a/interface/src/scripting/DialogsManagerScriptingInterface.cpp b/interface/src/scripting/DialogsManagerScriptingInterface.cpp index 80a8b4ac7c..cbca7ff4ff 100644 --- a/interface/src/scripting/DialogsManagerScriptingInterface.cpp +++ b/interface/src/scripting/DialogsManagerScriptingInterface.cpp @@ -18,6 +18,8 @@ DialogsManagerScriptingInterface::DialogsManagerScriptingInterface() { connect(DependencyManager::get().data(), &DialogsManager::addressBarToggled, this, &DialogsManagerScriptingInterface::addressBarToggled); + connect(DependencyManager::get().data(), &DialogsManager::addressBarShown, + this, &DialogsManagerScriptingInterface::addressBarShown); } void DialogsManagerScriptingInterface::toggleAddressBar() { diff --git a/interface/src/scripting/DialogsManagerScriptingInterface.h b/interface/src/scripting/DialogsManagerScriptingInterface.h index ef44e20d61..075b89f0e5 100644 --- a/interface/src/scripting/DialogsManagerScriptingInterface.h +++ b/interface/src/scripting/DialogsManagerScriptingInterface.h @@ -24,6 +24,7 @@ public slots: signals: void addressBarToggled(); + void addressBarShown(bool visible); }; #endif diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index ba0cf18d32..6fb437e312 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -16,6 +16,7 @@ #include "DependencyManager.h" #include "AddressManager.h" +#include "DialogsManager.h" HIFI_QML_DEF(AddressBarDialog) @@ -74,3 +75,6 @@ void AddressBarDialog::displayAddressNotFoundMessage() { OffscreenUi::critical("", "There is no address information for that user or place"); } +void AddressBarDialog::observeShownChanged(bool visible) { + DependencyManager::get()->emitAddressBarShown(visible); +} diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index b2751860cc..bbce52c67c 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -38,6 +38,7 @@ protected: Q_INVOKABLE void loadHome(); Q_INVOKABLE void loadBack(); Q_INVOKABLE void loadForward(); + Q_INVOKABLE void observeShownChanged(bool visible); bool _backEnabled; bool _forwardEnabled; diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index b8fa22ec83..c48c6df0e6 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -40,6 +40,7 @@ public: QPointer getHMDToolsDialog() const { return _hmdToolsDialog; } QPointer getLodToolsDialog() const { return _lodToolsDialog; } QPointer getOctreeStatsDialog() const { return _octreeStatsDialog; } + void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } public slots: void toggleAddressBar(); @@ -60,6 +61,7 @@ public slots: signals: void addressBarToggled(); + void addressBarShown(bool visible); private slots: void hmdToolsClosed(); diff --git a/scripts/system/examples.js b/scripts/system/examples.js index a948f9e563..4d838bc8af 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -55,16 +55,20 @@ var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var browseExamplesButton = toolBar.addButton({ imageURL: toolIconUrl + "examples-01.svg", objectName: "examples", - yOffset: 50, - alpha: 0.9, + buttonState: 1, + alpha: 0.9 }); -var browseExamplesButtonDown = false; - -browseExamplesButton.clicked.connect(function(){ +function onExamplesWindowVisibilityChanged() { + browseExamplesButton.writeProperty('buttonState', examplesWindow.visible ? 0 : 1); +} +function onClick() { toggleExamples(); -}); +} +browseExamplesButton.clicked.connect(onClick); +examplesWindow.visibleChanged.connect(onExamplesWindowVisibilityChanged); Script.scriptEnding.connect(function () { - browseExamplesButton.clicked.disconnect(); + browseExamplesButton.clicked.disconnect(onClick); + examplesWindow.visibleChanged.disconnect(onExamplesWindowVisibilityChanged); }); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index a2ade02a78..4650e72dad 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -16,14 +16,20 @@ var button = toolBar.addButton({ objectName: "goto", imageURL: Script.resolvePath("assets/images/tools/directory-01.svg"), visible: true, - yOffset: 50, + buttonState: 1, alpha: 0.9, }); - -button.clicked.connect(function(){ + +function onAddressBarShown(visible) { + button.writeProperty('buttonState', visible ? 0 : 1); +} +function onClicked(){ DialogsManager.toggleAddressBar(); -}); +} +button.clicked.connect(onClicked); +DialogsManager.addressBarShown.connect(onAddressBarShown); Script.scriptEnding.connect(function () { - button.clicked.disconnect(); + button.clicked.disconnect(onClicked); + DialogsManager.addressBarShown.disconnect(onAddressBarShown); }); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 2965c0d254..0d6a273975 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -20,23 +20,28 @@ var desktopMenuItemName = "Desktop"; var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var button; - +function onHmdChanged(isHmd) { + button.writeProperty('buttonState', isHmd ? 0 : 1); +} +function onClicked(){ + var isDesktop = Menu.isOptionChecked(desktopMenuItemName); + Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); +} if (headset) { button = toolBar.addButton({ objectName: "hmdToggle", imageURL: Script.resolvePath("assets/images/tools/hmd-switch-01.svg"), visible: true, - yOffset: 50, alpha: 0.9, }); + onHmdChanged(HMD.active); - button.clicked.connect(function(){ - var isDesktop = Menu.isOptionChecked(desktopMenuItemName); - Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); - }); + button.clicked.connect(onClicked); + HMD.displayModeChanged.connect(onHmdChanged); Script.scriptEnding.connect(function () { - button.clicked.disconnect(); + button.clicked.disconnect(onClicked); + HMD.displayModeChanged.disconnect(onHmdChanged); }); } diff --git a/scripts/system/mute.js b/scripts/system/mute.js index f66b6852ea..f91ecbafae 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -14,16 +14,25 @@ var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var button = toolBar.addButton({ objectName: "mute", - imageURL: Script.resolvePath("assets/images/tools/microphone.svg"), + imageURL: Script.resolvePath("assets/images/tools/mic-01.svg"), visible: true, alpha: 0.9, }); - -button.clicked.connect(function(){ + +function onMuteToggled() { + // We could just toggle state, but we're less likely to get out of wack if we read the AudioDevice. + // muted => "on" button state => buttonState 1 + button.writeProperty('buttonState', AudioDevice.getMuted() ? 1 : 0); +} +onMuteToggled(); +function onClicked(){ var menuItem = "Mute Microphone"; Menu.setIsOptionChecked(menuItem, !Menu.isOptionChecked(menuItem)); -}); +} +button.clicked.connect(onClicked); +AudioDevice.muteToggled.connect(onMuteToggled); Script.scriptEnding.connect(function () { - button.clicked.disconnect(); + button.clicked.disconnect(onClicked); + AudioDevice.muteToggled.disconnect(onMuteToggled); }); From f03f5f3e683a807c989a569272d080c843021362 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 30 Jun 2016 16:47:10 -0700 Subject: [PATCH 0866/1237] new icons --- interface/resources/icons/hud-01.svg | 105 ----- interface/resources/icons/hud.svg | 258 ++++++++++++ interface/resources/qml/hifi/Desktop.qml | 2 +- .../assets/images/tools/directory-01.svg | 194 ---------- .../system/assets/images/tools/directory.svg | 366 ++++++++++++++++++ .../system/assets/images/tools/edit-01.svg | 94 ----- scripts/system/assets/images/tools/edit.svg | 234 +++++++++++ .../assets/images/tools/examples-01.svg | 306 --------------- .../assets/images/tools/hmd-switch-01.svg | 124 ------ scripts/system/assets/images/tools/market.svg | 258 ++++++++++++ scripts/system/assets/images/tools/mic.svg | 164 ++++++++ scripts/system/assets/images/tools/switch.svg | 230 +++++++++++ scripts/system/edit.js | 2 +- scripts/system/examples.js | 2 +- scripts/system/goto.js | 2 +- scripts/system/hmd.js | 2 +- scripts/system/mute.js | 2 +- 17 files changed, 1516 insertions(+), 829 deletions(-) delete mode 100644 interface/resources/icons/hud-01.svg create mode 100644 interface/resources/icons/hud.svg delete mode 100644 scripts/system/assets/images/tools/directory-01.svg create mode 100644 scripts/system/assets/images/tools/directory.svg delete mode 100644 scripts/system/assets/images/tools/edit-01.svg create mode 100644 scripts/system/assets/images/tools/edit.svg delete mode 100644 scripts/system/assets/images/tools/examples-01.svg delete mode 100644 scripts/system/assets/images/tools/hmd-switch-01.svg create mode 100644 scripts/system/assets/images/tools/market.svg create mode 100644 scripts/system/assets/images/tools/mic.svg create mode 100644 scripts/system/assets/images/tools/switch.svg diff --git a/interface/resources/icons/hud-01.svg b/interface/resources/icons/hud-01.svg deleted file mode 100644 index 4929389268..0000000000 --- a/interface/resources/icons/hud-01.svg +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/hud.svg b/interface/resources/icons/hud.svg new file mode 100644 index 0000000000..9e77aa6040 --- /dev/null +++ b/interface/resources/icons/hud.svg @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 169542c0f0..7a7a6b63df 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -56,7 +56,7 @@ OriginalDesktop.Desktop { var sysToolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); var toggleHudButton = sysToolbar.addButton({ objectName: "hudToggle", - imageURL: "../../../icons/hud-01.svg", + imageURL: "../../../icons/hud.svg", visible: true, pinned: true, }); diff --git a/scripts/system/assets/images/tools/directory-01.svg b/scripts/system/assets/images/tools/directory-01.svg deleted file mode 100644 index d038611d69..0000000000 --- a/scripts/system/assets/images/tools/directory-01.svg +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/tools/directory.svg b/scripts/system/assets/images/tools/directory.svg new file mode 100644 index 0000000000..912243c52e --- /dev/null +++ b/scripts/system/assets/images/tools/directory.svg @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/tools/edit-01.svg b/scripts/system/assets/images/tools/edit-01.svg deleted file mode 100644 index c661c6f678..0000000000 --- a/scripts/system/assets/images/tools/edit-01.svg +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/tools/edit.svg b/scripts/system/assets/images/tools/edit.svg new file mode 100644 index 0000000000..13389a2b14 --- /dev/null +++ b/scripts/system/assets/images/tools/edit.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/tools/examples-01.svg b/scripts/system/assets/images/tools/examples-01.svg deleted file mode 100644 index ec4758dcde..0000000000 --- a/scripts/system/assets/images/tools/examples-01.svg +++ /dev/null @@ -1,306 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/tools/hmd-switch-01.svg b/scripts/system/assets/images/tools/hmd-switch-01.svg deleted file mode 100644 index 31389d355c..0000000000 --- a/scripts/system/assets/images/tools/hmd-switch-01.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/tools/market.svg b/scripts/system/assets/images/tools/market.svg new file mode 100644 index 0000000000..fa4a3d7256 --- /dev/null +++ b/scripts/system/assets/images/tools/market.svg @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/tools/mic.svg b/scripts/system/assets/images/tools/mic.svg new file mode 100644 index 0000000000..f3afccca70 --- /dev/null +++ b/scripts/system/assets/images/tools/mic.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MUTE + + + + + + + + + + + + MUTE + + UNMUTE + + + + + + + + + + + + + + + + + + UNMUTE + + diff --git a/scripts/system/assets/images/tools/switch.svg b/scripts/system/assets/images/tools/switch.svg new file mode 100644 index 0000000000..23655d747d --- /dev/null +++ b/scripts/system/assets/images/tools/switch.svg @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 9d5585e353..6810bae590 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -192,7 +192,7 @@ var toolBar = (function() { }); activeButton = toolBar.addTool({ - imageURL: toolIconUrl + "edit-01.svg", + imageURL: toolIconUrl + "edit.svg", subImage: { x: 0, y: Tool.IMAGE_WIDTH, diff --git a/scripts/system/examples.js b/scripts/system/examples.js index 4d838bc8af..0ec2644f9c 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -53,7 +53,7 @@ function toggleExamples() { var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var browseExamplesButton = toolBar.addButton({ - imageURL: toolIconUrl + "examples-01.svg", + imageURL: toolIconUrl + "market.svg", objectName: "examples", buttonState: 1, alpha: 0.9 diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 4650e72dad..24c402ab85 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -14,7 +14,7 @@ var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var button = toolBar.addButton({ objectName: "goto", - imageURL: Script.resolvePath("assets/images/tools/directory-01.svg"), + imageURL: Script.resolvePath("assets/images/tools/directory.svg"), visible: true, buttonState: 1, alpha: 0.9, diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 0d6a273975..305557b60c 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -30,7 +30,7 @@ function onClicked(){ if (headset) { button = toolBar.addButton({ objectName: "hmdToggle", - imageURL: Script.resolvePath("assets/images/tools/hmd-switch-01.svg"), + imageURL: Script.resolvePath("assets/images/tools/switch.svg"), visible: true, alpha: 0.9, }); diff --git a/scripts/system/mute.js b/scripts/system/mute.js index f91ecbafae..cd29e8faae 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -14,7 +14,7 @@ var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var button = toolBar.addButton({ objectName: "mute", - imageURL: Script.resolvePath("assets/images/tools/mic-01.svg"), + imageURL: Script.resolvePath("assets/images/tools/mic.svg"), visible: true, alpha: 0.9, }); From 104ee63a772f4907da558215162610754219acc6 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 30 Jun 2016 17:05:16 -0700 Subject: [PATCH 0867/1237] notes --- scripts/system/controllers/teleport.js | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5ad09d53a6..728f34faea 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,14 +1,25 @@ -//check if trigger is down -//if trigger is down, check if thumb is down -//if both are down, enter 'teleport mode' -//aim controller to change landing spot -//visualize aim + spot (line + circle) -//release trigger to teleport then exit teleport mode -//if thumb is release, exit teleport mode +//v1 +//check if trigger is down xxx +//if trigger is down, check if thumb is down xxx +//if both are down, enter 'teleport mode' xxx +//aim controller to change landing spot xxx +//visualize aim + spot (line + circle) xxx +//release trigger to teleport then exit teleport mode xxx +//if thumb is release, exit teleport mode xxx //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? -//v2: haptic feedbackasd +//v2: haptic feedback + + +// alternate notes for philip: +// try just thumb to teleport +// cancel if destination is within ~1m of current location + + +//try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) + + var inTeleportMode = false; From 9a604d0eded421fc25eb8bf8c1576a703bbf928d Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 30 Jun 2016 19:03:24 -0700 Subject: [PATCH 0868/1237] Bringing Beckmann specular to the lighting --- .../render-utils/src/DebugDeferredBuffer.cpp | 2 +- .../render-utils/src/DeferredBufferRead.slh | 13 ++++- .../render-utils/src/DeferredBufferWrite.slh | 2 +- .../render-utils/src/DeferredGlobalLight.slh | 55 +++++++++++++----- .../src/DeferredLightingEffect.cpp | 4 ++ .../render-utils/src/MaterialTextures.slh | 3 +- .../render-utils/src/SubsurfaceScattering.cpp | 56 ++++++++++++++++++- .../render-utils/src/SubsurfaceScattering.h | 7 +++ .../render-utils/src/SubsurfaceScattering.slh | 33 +++++++++++ .../src/directional_ambient_light.slf | 1 + .../src/directional_skybox_light.slf | 1 + ...surfaceScattering_makeSpecularBeckmann.slf | 26 +++++++++ .../utilities/render/debugToneMapping.js | 20 +++++++ .../utilities/render/subsurfaceScattering.qml | 5 ++ 14 files changed, 208 insertions(+), 20 deletions(-) create mode 100644 libraries/render-utils/src/subsurfaceScattering_makeSpecularBeckmann.slf create mode 100644 scripts/developer/utilities/render/debugToneMapping.js diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 7ae9502889..ba7997c60e 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -114,7 +114,7 @@ static const std::string DEFAULT_LIGHTMAP_SHADER{ static const std::string DEFAULT_SCATTERING_SHADER{ "vec4 getFragmentColor() {" " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" - " return (frag.mode == FRAG_MODE_SCATTERING ? vec4(vec3(1.0), 1.0) : vec4(vec3(0.0), 1.0));" + " return (frag.mode == FRAG_MODE_SCATTERING ? vec4(vec3(pow(frag.scattering, 1.0 / 2.2)), 1.0) : vec4(vec3(0.0), 1.0));" " }" }; diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index cabfe001c7..7d6aead855 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -45,6 +45,7 @@ struct DeferredFragment { float roughness; vec3 emissive; int mode; + float scattering; float depthVal; }; @@ -63,8 +64,17 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { // Diffuse color and unpack the mode and the metallicness frag.diffuse = frag.diffuseVal.xyz; + frag.scattering = 0.0; unpackModeMetallic(frag.diffuseVal.w, frag.mode, frag.metallic); + frag.emissive = frag.specularVal.xyz; + frag.obscurance = min(frag.specularVal.w, frag.obscurance); + + + if (frag.mode == FRAG_MODE_SCATTERING) { + frag.scattering = frag.emissive.x; + frag.emissive = vec3(0.0); + } if (frag.metallic <= 0.5) { frag.metallic = 0.0; @@ -74,9 +84,6 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { frag.metallic = 1.0; } - frag.emissive = frag.specularVal.xyz; - frag.obscurance = min(frag.specularVal.w, frag.obscurance); - return frag; } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 71236c71ad..ee90f26346 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -39,7 +39,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness } _fragColor0 = vec4(albedo, ((scattering > 0.0) ? packScatteringMetallic(metallic) : packShadedMetallic(metallic))); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); - _fragColor2 = vec4(emissive, occlusion); + _fragColor2 = vec4(((scattering > 0.0) ? vec3(scattering) : emissive), occlusion); } diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 9860ca79ee..90abb9709a 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -117,7 +117,9 @@ vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, floa <$declareSubsurfaceScatteringResource()$> !> -vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { +<$declareSkinSpecularLighting()$> + +vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { // prepareGlobalLight // Transform directions to worldspace @@ -161,16 +163,22 @@ vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenua // Specular Lighting vec3 halfDir = normalize(fragEyeDir + fragLightDir); + + float specular = skinSpecular(fragNormal, fragLightDir, fragEyeDir, roughness, 1.0); + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); float power = specularDistribution(roughness, fragNormal, halfDir); - vec3 specular = power * fresnelColor * standardDiffuse; + //vec3 specular = power * fresnelColor * standardDiffuse; - shading = vec4(specular, (1 - fresnelColor.x)); + shading = vec4(vec3(specular), (1 - fresnelColor.x)); } + if (scatteringLevel < 0.1) { brdf = vec3(standardDiffuse); } + brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); + vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); @@ -243,7 +251,12 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur <$declareSubsurfaceScatteringResource()$> !> -vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { +<$declareSkinSpecularLighting()$> + +float curvatureAO(in float k) { + return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369; +} +vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { // prepareGlobalLight // Transform directions to worldspace @@ -266,10 +279,10 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f return diffusedCurvature.xyz; return lowNormal * 0.5 + vec3(0.5); } - if (showCurvature()) { + /* if (showCurvature()) { float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); - } + }*/ vec3 bentNdotL = evalScatteringBentNdotL(fragNormal, midNormal, lowNormal, fragLightDir); @@ -287,25 +300,41 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f // Specular Lighting vec3 halfDir = normalize(fragEyeDir + fragLightDir); - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir,halfDir); - float power = specularDistribution(roughness, fragNormal, halfDir); - vec3 specular = power * fresnelColor * standardDiffuse; - shading = vec4(specular, (1 - fresnelColor.x)); + float specular = skinSpecular(fragNormal, fragLightDir, fragEyeDir, roughness, 1.0); + + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + float power = specularDistribution(roughness, fragNormal, halfDir); + //vec3 specular = power * fresnelColor * standardDiffuse; + + shading = vec4(vec3(specular), (1 - fresnelColor.x)); + + // shading = vec4(specular, (1 - fresnelColor.x)); } if (scatteringLevel < 0.1) { brdf = vec3(standardDiffuse); } - vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); + brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); + vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); + + //vec3 color = vec3(shading.rgb) * getLightColor(light) * getLightIntensity(light); + + float ambientOcclusion = curvatureAO((diffusedCurvature.w * 2 - 1) * 20.0f) * 0.5f; + float ambientOcclusionHF = curvatureAO((diffusedCurvature.w * 2 - 1) * 8.0f) * 0.5f; + ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF); + + if (showCurvature()) { + return vec3(ambientOcclusion); + } // Diffuse from ambient - // color += albedo * evalSphericalLight(getLightAmbientSphere(light), bentNormalHigh).xyz *getLightAmbientIntensity(light); + color += ambientOcclusion * albedo * evalSphericalLight(getLightAmbientSphere(light), lowNormal).xyz *getLightAmbientIntensity(light); // Specular highlight from ambient - vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, 1.0); + // vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, 1.0); // color += specularLighting; if ( showBRDF()) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 0609191262..369466275b 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -62,6 +62,7 @@ enum DeferredShader_MapSlot { DEFERRED_BUFFER_CURVATURE_UNIT, DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, SCATTERING_LUT_UNIT, + SCATTERING_SPECULAR_UNIT, }; enum DeferredShader_BufferSlot { DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 0, @@ -175,6 +176,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), DEFERRED_BUFFER_CURVATURE_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("diffusedCurvatureMap"), DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), SCATTERING_LUT_UNIT)); + slotBindings.insert(gpu::Shader::Binding(std::string("scatteringSpecularBeckmann"), SCATTERING_SPECULAR_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT)); @@ -399,6 +401,7 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c batch.setResourceTexture(SCATTERING_LUT_UNIT, subsurfaceScatteringResource->getScatteringTable()); + batch.setResourceTexture(SCATTERING_SPECULAR_UNIT, subsurfaceScatteringResource->getScatteringSpecular()); // Global directional light and ambient pass @@ -593,6 +596,7 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, nullptr); batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, nullptr); batch.setResourceTexture(SCATTERING_LUT_UNIT, nullptr); + batch.setResourceTexture(SCATTERING_SPECULAR_UNIT, nullptr); batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, nullptr); batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, nullptr); diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 97f0ed6e96..ddebd16c3a 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -91,7 +91,8 @@ float fetchOcclusionMap(vec2 uv) { <@if withScattering@> uniform sampler2D scatteringMap; float fetchScatteringMap(vec2 uv) { - return step(0.5, texture(scatteringMap, uv).r); // boolean scattering for now + //return step(0.5, texture(scatteringMap, uv).r); // boolean scattering for now + return texture(scatteringMap, uv).r; // boolean scattering for now } <@endif@> diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 30b8b3f7bd..3535732f2e 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -19,6 +19,8 @@ #include "subsurfaceScattering_makeProfile_frag.h" #include "subsurfaceScattering_makeLUT_frag.h" +#include "subsurfaceScattering_makeSpecularBeckmann_frag.h" + #include "subsurfaceScattering_drawScattering_frag.h" enum ScatteringShaderBufferSlots { @@ -109,6 +111,9 @@ void SubsurfaceScatteringResource::generateScatteringTable(RenderArgs* args) { if (!_scatteringTable) { _scatteringTable = generatePreIntegratedScattering(_scatteringProfile, args); } + if (!_scatteringSpecular) { + _scatteringSpecular = generateScatteringSpecularBeckmann(args); + } } SubsurfaceScattering::SubsurfaceScattering() { @@ -372,6 +377,41 @@ void diffuseScatterGPU(const gpu::TexturePointer& profileMap, gpu::TexturePointe }); } +void computeSpecularBeckmannGPU(gpu::TexturePointer& beckmannMap, RenderArgs* args) { + int width = beckmannMap->getWidth(); + int height = beckmannMap->getHeight(); + + gpu::PipelinePointer makePipeline; + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_makeSpecularBeckmann_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + makePipeline = gpu::Pipeline::create(program, state); + } + + auto makeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + makeFramebuffer->setRenderBuffer(0, beckmannMap); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + batch.enableStereo(false); + + batch.setViewportTransform(glm::ivec4(0, 0, width, height)); + + batch.setFramebuffer(makeFramebuffer); + batch.setPipeline(makePipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(0, nullptr); + batch.setPipeline(nullptr); + batch.setFramebuffer(nullptr); + }); +} + gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(RenderArgs* args) { const int PROFILE_RESOLUTION = 512; auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); @@ -388,7 +428,12 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin return scatteringLUT; } - +gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { + const int SPECULAR_RESOLUTION = 256; + auto beckmannMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + computeSpecularBeckmannGPU(beckmannMap, args); + return beckmannMap; +} DebugSubsurfaceScattering::DebugSubsurfaceScattering() { } @@ -397,6 +442,7 @@ void DebugSubsurfaceScattering::configure(const Config& config) { _showProfile = config.showProfile; _showLUT = config.showLUT; + _showSpecularTable = config.showSpecularTable; _showCursorPixel = config.showCursorPixel; _debugCursorTexcoord = config.debugCursorTexcoord; } @@ -473,6 +519,7 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont } auto scatteringProfile = scatteringResource->getScatteringProfile(); auto scatteringTable = scatteringResource->getScatteringTable(); + auto scatteringSpecular = scatteringResource->getScatteringSpecular(); auto framebufferCache = DependencyManager::get(); @@ -528,6 +575,13 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont } } + if (_showSpecularTable) { + batch.setViewportTransform(glm::ivec4(viewportSize + offsetViewport * 0.5, 0, viewportSize * 0.5, viewportSize * 0.5)); + batch.setPipeline(getShowLUTPipeline()); + batch.setResourceTexture(0, scatteringSpecular); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + batch.setViewportTransform(args->_viewport); }); diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 3446324df3..1442feeb1f 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -44,10 +44,13 @@ public: gpu::TexturePointer getScatteringProfile() const { return _scatteringProfile; } gpu::TexturePointer getScatteringTable() const { return _scatteringTable; } + gpu::TexturePointer getScatteringSpecular() const { return _scatteringSpecular; } void generateScatteringTable(RenderArgs* args); + static gpu::TexturePointer generateScatteringProfile(RenderArgs* args); static gpu::TexturePointer generatePreIntegratedScattering(const gpu::TexturePointer& profile, RenderArgs* args); + static gpu::TexturePointer generateScatteringSpecularBeckmann(RenderArgs* args); protected: @@ -74,6 +77,7 @@ protected: gpu::TexturePointer _scatteringProfile; gpu::TexturePointer _scatteringTable; + gpu::TexturePointer _scatteringSpecular; }; using SubsurfaceScatteringResourcePointer = std::shared_ptr; @@ -138,6 +142,7 @@ class DebugSubsurfaceScatteringConfig : public render::Job::Config { Q_PROPERTY(bool showProfile MEMBER showProfile NOTIFY dirty) Q_PROPERTY(bool showLUT MEMBER showLUT NOTIFY dirty) + Q_PROPERTY(bool showSpecularTable MEMBER showSpecularTable NOTIFY dirty) Q_PROPERTY(bool showCursorPixel MEMBER showCursorPixel NOTIFY dirty) Q_PROPERTY(glm::vec2 debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) public: @@ -145,6 +150,7 @@ public: bool showProfile{ false }; bool showLUT{ false }; + bool showSpecularTable{ false }; bool showCursorPixel{ false }; glm::vec2 debugCursorTexcoord{ 0.5, 0.5 }; @@ -172,6 +178,7 @@ private: gpu::PipelinePointer getShowLUTPipeline(); bool _showProfile{ false }; bool _showLUT{ false }; + bool _showSpecularTable{ false }; bool _showCursorPixel{ false }; glm::vec2 _debugCursorTexcoord; }; diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index 1c2a2ace55..8b801427df 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -60,6 +60,39 @@ vec3 scatter(float r) { <@endfunc@> + +<@func declareSkinSpecularLighting()@> + +uniform sampler2D scatteringSpecularBeckmann; + +float fetchSpecularBeckmann(float ndoth, float roughness) { + return pow( 2.0 * texture(scatteringSpecularBeckmann, vec2(ndoth, roughness)).r, 10.0); +} + +float fresnelReflectance(vec3 H, vec3 V, float Fo) { + float base = 1.0 - dot(V, H); + float exponential = pow(base, 5.0); + return exponential + Fo * (1.0 - exponential); +} + +float skinSpecular(vec3 N, vec3 L, vec3 V, float roughness, float intensity) { + float result = 0.0; + float ndotl = dot(N, L); + if (ndotl > 0.0) { + vec3 h = L + V; + vec3 H = normalize(h); + float ndoth = dot(N, H); + float PH = fetchSpecularBeckmann(ndoth, roughness); + float F = fresnelReflectance(H, V, 0.028); + float frSpec = max(PH * F / dot(h, h), 0.0); + result = ndotl * intensity * frSpec; + } + + return result; +} + +<@endfunc@> + <@func declareSubsurfaceScatteringIntegrate(NumIntegrationSteps)@> diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index 932f798a73..156ff44f69 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -52,6 +52,7 @@ void main(void) { frag.position.xyz, frag.normal, frag.diffuse, + frag.scattering, blurredCurvature, diffusedCurvature, frag.roughness); diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index 544271d20d..d019e29866 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -52,6 +52,7 @@ void main(void) { frag.position.xyz, frag.normal, frag.diffuse, + frag.scattering, blurredCurvature, diffusedCurvature, frag.roughness); diff --git a/libraries/render-utils/src/subsurfaceScattering_makeSpecularBeckmann.slf b/libraries/render-utils/src/subsurfaceScattering_makeSpecularBeckmann.slf new file mode 100644 index 0000000000..f10d287c35 --- /dev/null +++ b/libraries/render-utils/src/subsurfaceScattering_makeSpecularBeckmann.slf @@ -0,0 +1,26 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/30/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + + +in vec2 varTexCoord0; +out vec4 outFragColor; + +float specularBeckmann(float ndoth, float roughness) { + float alpha = acos(ndoth); + float ta = tan(alpha); + float val = 1.0 / (roughness * roughness * pow(ndoth, 4.0)) * exp(-(ta * ta) / (roughness * roughness)); + return val; +} + +void main(void) { + outFragColor = vec4(vec3(0.5 * pow( specularBeckmann(varTexCoord0.x, varTexCoord0.y), 0.1)), 1.0); +} diff --git a/scripts/developer/utilities/render/debugToneMapping.js b/scripts/developer/utilities/render/debugToneMapping.js new file mode 100644 index 0000000000..ef14c24fb7 --- /dev/null +++ b/scripts/developer/utilities/render/debugToneMapping.js @@ -0,0 +1,20 @@ +// +// debugToneMapping.js +// +// Created by Sam Gateau on 6/30/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +// Set up the qml ui +var qml = Script.resolvePath('toneMapping.qml'); +var window = new OverlayWindow({ + title: 'Tone Mapping', + source: qml, + width: 400, height: 200, +}); +window.setPosition(250, 1000); +window.closed.connect(function() { Script.stop(); }); + diff --git a/scripts/developer/utilities/render/subsurfaceScattering.qml b/scripts/developer/utilities/render/subsurfaceScattering.qml index db0c2b798c..47b960c98b 100644 --- a/scripts/developer/utilities/render/subsurfaceScattering.qml +++ b/scripts/developer/utilities/render/subsurfaceScattering.qml @@ -71,6 +71,11 @@ Column { checked: Render.getConfig("DebugScattering").showCursorPixel onCheckedChanged: { Render.getConfig("DebugScattering").showCursorPixel = checked } } + CheckBox { + text: "Skin Specular Beckmann" + checked: Render.getConfig("DebugScattering").showSpecularTable + onCheckedChanged: { Render.getConfig("DebugScattering").showSpecularTable = checked } + } } } } From 5288d983763ea57e4e1d33f37cc21384a82c5ba5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 30 Jun 2016 20:52:41 -0700 Subject: [PATCH 0869/1237] Using pipelines properly where appropriate --- interface/src/ui/overlays/Circle3DOverlay.cpp | 8 ++++---- interface/src/ui/overlays/Cube3DOverlay.cpp | 6 ++++-- interface/src/ui/overlays/Grid3DOverlay.cpp | 6 ++---- interface/src/ui/overlays/Image3DOverlay.cpp | 6 ++---- interface/src/ui/overlays/Sphere3DOverlay.cpp | 3 +++ 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 1abcb0f5c0..2896ce711e 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -149,7 +149,6 @@ void Circle3DOverlay::render(RenderArgs* args) { geometryCache->updateVertices(_quadVerticesID, points, color); } - geometryCache->bindSimpleProgram(batch); geometryCache->renderVertices(batch, gpu::TRIANGLES, _quadVerticesID); } else { @@ -188,8 +187,6 @@ void Circle3DOverlay::render(RenderArgs* args) { geometryCache->updateVertices(_lineVerticesID, points, color); } - // Render unlit - geometryCache->bindSimpleProgram(batch, false, false, true); if (getIsDashedLine()) { geometryCache->renderVertices(batch, gpu::LINES, _lineVerticesID); } else { @@ -282,10 +279,13 @@ void Circle3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Circle3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withOwnPipeline(); + auto builder = render::ShapeKey::Builder().withoutCullFace(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } + if (!getIsSolid()) { + builder.withUnlit().withDepthBias(); + } return builder.build(); } diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index 17894aa091..f72fb8d920 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -44,7 +44,6 @@ void Cube3DOverlay::render(RenderArgs* args) { Transform transform; transform.setTranslation(position); transform.setRotation(rotation); - auto geometryCache = DependencyManager::get(); auto pipeline = args->_pipeline; if (!pipeline) { @@ -97,10 +96,13 @@ void Cube3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Cube3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withOwnPipeline(); + auto builder = render::ShapeKey::Builder(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } + if (!getIsSolid()) { + builder.withUnlit().withDepthBias(); + } return builder.build(); } diff --git a/interface/src/ui/overlays/Grid3DOverlay.cpp b/interface/src/ui/overlays/Grid3DOverlay.cpp index 7d86395937..e9bbcddf59 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.cpp +++ b/interface/src/ui/overlays/Grid3DOverlay.cpp @@ -75,11 +75,9 @@ void Grid3DOverlay::render(RenderArgs* args) { transform.setScale(glm::vec3(getDimensions(), 1.0f)); transform.setTranslation(position); batch->setModelTransform(transform); - auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(*batch, false, false, true, true); const float MINOR_GRID_EDGE = 0.0025f; const float MAJOR_GRID_EDGE = 0.005f; - geometryCache->renderGrid(*batch, minCorner, maxCorner, + DependencyManager::get()->renderGrid(*batch, minCorner, maxCorner, _minorGridRowDivisions, _minorGridColDivisions, MINOR_GRID_EDGE, _majorGridRowDivisions, _majorGridColDivisions, MAJOR_GRID_EDGE, gridColor, _drawInFront); @@ -87,7 +85,7 @@ void Grid3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Grid3DOverlay::getShapeKey() { - return render::ShapeKey::Builder().withOwnPipeline(); + return render::ShapeKey::Builder().withOwnPipeline().withUnlit().withDepthBias(); } void Grid3DOverlay::setProperties(const QVariantMap& properties) { diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index 72ae246859..d59e552779 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -93,9 +93,7 @@ void Image3DOverlay::render(RenderArgs* args) { batch->setModelTransform(transform); batch->setResourceTexture(0, _texture->getGPUTexture()); - auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(*batch, true, false); - geometryCache->renderQuad( + DependencyManager::get()->renderQuad( *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) ); @@ -104,7 +102,7 @@ void Image3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Image3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withOwnPipeline(); + auto builder = render::ShapeKey::Builder().withoutCullFace().withDepthBias(); if (_emissive) { builder.withUnlit(); } diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 1dd61611f2..85530d1376 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -62,6 +62,9 @@ const render::ShapeKey Sphere3DOverlay::getShapeKey() { if (getAlpha() != 1.0f) { builder.withTranslucent(); } + if (!getIsSolid()) { + builder.withUnlit().withDepthBias(); + } return builder.build(); } From b2e407a8e186b093d3721fada0f098d25eabafc1 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 00:48:29 -0700 Subject: [PATCH 0870/1237] fix vive bug --- scripts/system/controllers/teleport.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 728f34faea..42ffd2dac9 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -87,9 +87,9 @@ function Teleporter() { this.createMappings = function() { print('jbp create mappings internal'); // peek at the trigger and thumbs to store their values - teleporter.telporterMappingInternalName='Hifi-Teleporter-Internal-Dev-' + Math.random(); - teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); - + teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); + teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); + Controller.enableMapping(teleporter.telporterMappingInternalName); }; @@ -124,20 +124,24 @@ function Teleporter() { }; this.update = function() { - //print('in teleporter update') - if (rightPad.buttonValue === 0 || leftPad.buttonValue === 0) { - print('JBP THUMB RELEASED SHOULD EXIT') - _this.exitTeleportMode(); - return; - } + //print('in teleporter update') + if (teleporter.teleportHand === 'left') { teleporter.leftRay(); + if (leftPad.buttonValue === 0) { + _this.exitTeleportMode(); + return; + } if (leftTrigger.buttonValue === 0) { _this.teleport(); } } else { teleporter.rightRay(); + if (rightPad.buttonValue === 0) { + _this.exitTeleportMode(); + return; + } if (rightTrigger.buttonValue === 0) { _this.teleport(); } @@ -256,14 +260,14 @@ function Teleporter() { this.rightOverlayOff = function() { if (this.rightOverlayLine !== null) { Overlays.deleteOverlay(this.rightOverlayLine); - this.rightOverlayLine=null; + this.rightOverlayLine = null; } }; this.leftOverlayOff = function() { if (this.leftOverlayLine !== null) { Overlays.deleteOverlay(this.leftOverlayLine); - this.leftOverlayLine=null; + this.leftOverlayLine = null; } }; From 804a187a7b81928a386b29270d9b1d7fe0af86f7 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 1 Jul 2016 10:17:17 -0700 Subject: [PATCH 0871/1237] Mute changes. --- scripts/system/assets/images/tools/mic.svg | 121 +++++++++++------- .../system/assets/images/tools/microphone.svg | 13 -- scripts/system/mute.js | 7 +- 3 files changed, 80 insertions(+), 61 deletions(-) delete mode 100644 scripts/system/assets/images/tools/microphone.svg diff --git a/scripts/system/assets/images/tools/mic.svg b/scripts/system/assets/images/tools/mic.svg index f3afccca70..126bb539fc 100644 --- a/scripts/system/assets/images/tools/mic.svg +++ b/scripts/system/assets/images/tools/mic.svg @@ -10,12 +10,7 @@ .st4{filter:url(#Adobe_OpacityMaskFilter_1_);} .st5{mask:url(#SVGID_2_);} .st6{fill:#1E1E1E;} - .st7{fill:none;stroke:#75FF48;stroke-width:0.25;stroke-miterlimit:10;} - .st8{fill:none;} - .st9{fill:#FFFFFF;} - .st10{font-family:'Raleway-Bold';} - .st11{font-size:9px;} - .st12{fill:#333333;} + .st7{fill:#FFFFFF;} @@ -95,9 +90,7 @@ fIzDR8cN4+ibinDd+4E+1lB91Xov/9k=" transform="matrix(1 0 0 1 -2 148)"> - - - + @@ -106,59 +99,97 @@ fIzDR8cN4+ibinDd+4E+1lB91Xov/9k=" transform="matrix(1 0 0 1 -2 148)"> - - MUTE - - - - - + + + + + + + + + - - - - - + + + + + - - MUTE - - UNMUTE + h-2.3l0-1.8C28.9,78.5,31,76.3,31,73.7z"/> + + + + + + + + + + + + + + - - + + c0.4-0.4,0.4-1.1,0-1.5L26.5,13.9z"/> - - + + h-2.3l0-1.8C28.8,28.6,30.9,26.5,30.9,23.8z"/> - - + + c0.4,0.4,1.1,0.4,1.5,0c0.4-0.4,0.4-1.1,0-1.5L26.5,163.8z"/> - - + + h-2.3l0-1.8C28.8,178.6,30.9,176.4,30.9,173.7z"/> + + + + + + + + - - UNMUTE diff --git a/scripts/system/assets/images/tools/microphone.svg b/scripts/system/assets/images/tools/microphone.svg deleted file mode 100644 index bd5e8afac7..0000000000 --- a/scripts/system/assets/images/tools/microphone.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - image/svg+xml - - - Layer 1 - - - - Mute - - - \ No newline at end of file diff --git a/scripts/system/mute.js b/scripts/system/mute.js index cd29e8faae..1a575efa01 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -16,13 +16,14 @@ var button = toolBar.addButton({ objectName: "mute", imageURL: Script.resolvePath("assets/images/tools/mic.svg"), visible: true, - alpha: 0.9, + buttonState: 1, + alpha: 0.9 }); function onMuteToggled() { // We could just toggle state, but we're less likely to get out of wack if we read the AudioDevice. - // muted => "on" button state => buttonState 1 - button.writeProperty('buttonState', AudioDevice.getMuted() ? 1 : 0); + // muted => button "on" state => 1. go figure. + button.writeProperty('buttonState', AudioDevice.getMuted() ? 0 : 1); } onMuteToggled(); function onClicked(){ From 0c60d87d3be8bfd5b5298e7db6f5f3e2b0f43405 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 10:32:37 -0700 Subject: [PATCH 0872/1237] fix some touch mappings --- interface/resources/controllers/oculus_touch.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index b59bf54e5b..3def439221 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -1,10 +1,10 @@ { "name": "Oculus Touch to Standard", "channels": [ - { "from": "OculusTouch.A", "to": "Standard.A" }, - { "from": "OculusTouch.B", "to": "Standard.B" }, - { "from": "OculusTouch.X", "to": "Standard.X" }, - { "from": "OculusTouch.Y", "to": "Standard.Y" }, + { "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb" }, + { "from": "OculusTouch.B", "to": "Standard.RightSecondaryThumb" }, + { "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb" }, + { "from": "OculusTouch.Y", "to": "Standard.LeftSecondaryThumb" }, { "from": "OculusTouch.LY", "filters": "invert", "to": "Standard.LY" }, { "from": "OculusTouch.LX", "to": "Standard.LX" }, From 43c7fd1e27093ca5577c542919b439d14d6373cf Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 1 Jul 2016 10:34:13 -0700 Subject: [PATCH 0873/1237] New art. --- interface/resources/icons/hud.svg | 309 ++++--------- .../system/assets/images/tools/directory.svg | 427 +++++++----------- scripts/system/assets/images/tools/edit.svg | 243 +++------- scripts/system/assets/images/tools/market.svg | 249 +++------- scripts/system/assets/images/tools/mic.svg | 221 ++++----- scripts/system/assets/images/tools/switch.svg | 295 +++++------- 6 files changed, 583 insertions(+), 1161 deletions(-) diff --git a/interface/resources/icons/hud.svg b/interface/resources/icons/hud.svg index 9e77aa6040..acfd21b909 100644 --- a/interface/resources/icons/hud.svg +++ b/interface/resources/icons/hud.svg @@ -3,140 +3,29 @@ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -144,115 +33,113 @@ vaK3kSYV8RW8iTCviK3kSYV8RW8iTCiPaa0X/wAJMKhq3ZXV/pkbI0eRJfe0h26v/9k=" transform= - - + + - - + - - - + + + - - + + - - + - - - + + + - - + + - - + - - + + + + + + + + + + + + - - - - - - - - - - - - + c-0.4-0.2-0.7-0.4-0.9-0.7c-0.2-0.3-0.4-0.6-0.5-1c-0.1-0.4-0.2-0.8-0.2-1.2v-3.4h1.3v3.4c0,0.3,0,0.5,0.1,0.8 + c0.1,0.3,0.1,0.5,0.3,0.7c0.1,0.2,0.3,0.4,0.5,0.5C25,191.7,25.3,191.8,25.6,191.8z"/> + diff --git a/scripts/system/assets/images/tools/directory.svg b/scripts/system/assets/images/tools/directory.svg index 912243c52e..09f3c14bf0 100644 --- a/scripts/system/assets/images/tools/directory.svg +++ b/scripts/system/assets/images/tools/directory.svg @@ -3,364 +3,253 @@ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - + + + c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.2,0.2-0.3,0.4-0.3,0.7C33.7,38.5,33.7,38.8,33.7,39.1z"/> - + C16.6,17.2,16.5,17.1,16.6,17z M16,18.2L16,18.2c0-0.2,0.1-0.2,0.2-0.2l3.3,0.8c0.1,0,0.1,0.1,0.1,0.2l0.1,4.7 + c0,0.1,0,0.1-0.1,0.1h-0.1l0,0h-0.1h-0.1l0,0c-0.1,0-0.3-0.1-0.5-0.1c-1.1-0.3-1.9-0.6-2.6-1.1c-0.4-0.3-0.6-0.6-0.6-1 + C15.6,20.5,15.7,19.3,16,18.2z"/> - + - - + - - + + + c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.2,0.2-0.3,0.4-0.3,0.7C33.7,88.5,33.7,88.8,33.7,89.1z"/> - + C16.6,67.2,16.5,67.1,16.6,67z M16,68.2L16,68.2c0-0.2,0.1-0.2,0.2-0.2l3.3,0.8c0.1,0,0.1,0.1,0.1,0.2l0.1,4.7 + c0,0.1,0,0.1-0.1,0.1h-0.1l0,0h-0.1h-0.1l0,0c-0.1,0-0.3-0.1-0.5-0.1c-1.1-0.3-1.9-0.6-2.6-1.1c-0.4-0.3-0.6-0.6-0.6-1 + C15.6,70.5,15.7,69.3,16,68.2z"/> - + - - - - + C18.9,138.6,18.9,138.9,18.9,139.2z"/> + + - + c-0.1,0-0.1-0.1-0.1-0.2L25,119.5L25,119.5z M25,125.6c0-0.1,0.1-0.2,0.2-0.2h0.2l3.4-0.2c0,0,0.1,0,0.1,0.1c0,0,0.1,0.1,0,0.1 + v0.1l0,0c0,0,0,0,0,0.1v0.1v0.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.1,0.3-0.2,0.4l-0.1,0.2c-0.2,0.6-0.5,1.2-0.7,1.8 + c-0.2,0.5-0.6,0.9-0.9,1.3c-0.3,0.3-0.9,0.6-1.5,0.6h-0.2l0,0c0,0-0.1,0-0.1-0.1v-0.1L25,125.6L25,125.6z M21.5,129.5l-0.1,0.1 + l0,0l-0.2-0.1l0,0c-1.8-0.4-4.2-2.6-5.1-5.2v-0.1c0-0.1,0-0.1,0-0.2c0,0,0.1-0.1,0.2,0l0.9,0.3c0.8,0.2,1.6,0.5,2.4,0.7 + c0.2,0.1,0.4,0.1,0.4,0.4c0.3,1.6,0.8,2.8,1.5,3.8l0.1,0.1C21.6,129.4,21.6,129.4,21.5,129.5z M24.2,130.1c0,0.1-0.1,0.1-0.1,0.1 + H24c0,0,0,0-0.1,0c0,0-0.9-0.3-1.3-0.8c-1-1.3-1.5-2.7-1.9-4l0,0v-0.1c0-0.1,0-0.1,0-0.1l0.1-0.1h0.1l1.9,0.2l1.2,0.1 + c0.1,0,0.2,0.1,0.2,0.2L24.2,130.1L24.2,130.1z M22.8,119.3l1.2,0.1c0.1,0,0.2,0.1,0.2,0.2v5c0,0,0,0.1-0.1,0.1H24l0,0 + c0,0-0.9-0.1-1.4-0.1c-0.6,0-1.2-0.1-1.7-0.2c-0.2,0-0.4-0.3-0.4-0.4c-0.1-1.2-0.1-2.3-0.2-3.1c0-0.3,0-1.4,0-1.5s0-0.1,0.1-0.1 + c0,0,0.1-0.1,0.1,0H22.8z M16.4,117.1L16.4,117.1c0.8-2.1,2.8-3.9,5-4.7l0.2-0.1c0.1,0,0.2,0,0.2,0.1s0,0.1,0,0.2l-0.1,0.2 + c-1.2,1.6-1.7,3.5-2,5.3v0.1c0,0,0,0.1-0.1,0.1h-0.1c0,0,0,0-0.1,0h-0.1c-0.4-0.1-0.7-0.2-1.1-0.3c-0.6-0.1-1.2-0.3-1.8-0.5h-0.1 + C16.4,117.3,16.3,117.2,16.4,117.1z M15.8,118.3L15.8,118.3c0-0.2,0.1-0.2,0.2-0.2l3.3,0.8c0.1,0,0.1,0.1,0.1,0.2l0.1,4.7 + c0,0.1,0,0.1-0.1,0.1h-0.1l0,0h-0.1h-0.1l0,0c-0.1,0-0.3-0.1-0.5-0.1c-1.1-0.3-1.9-0.6-2.6-1.1c-0.4-0.3-0.6-0.6-0.6-1 + C15.4,120.6,15.5,119.4,15.8,118.3z"/> - + - - - - + C18.9,188.7,18.9,189,18.9,189.3z"/> + + - + C30,174.1,30,174.1,30,173.9L30,173.9z M30.1,175.1c0.8-0.2,1.5-0.4,2.3-0.6l0.9-0.3c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0.1,0,0.2v0.1 + c-0.5,1.2-1.1,2.2-2,3.1c-0.8,0.9-1.9,1.5-3.1,2.1h-0.1c0,0,0,0-0.1,0h-0.1c-0.1,0-0.1-0.1-0.1-0.2v-0.1l0,0c0,0,0-0.1,0.1-0.2 + c0.4-0.6,1-1.6,1.5-3.7C29.7,175.3,29.8,175.1,30.1,175.1z M25,169.6c0-0.1,0.1-0.2,0.2-0.2h0.2l2.7-0.1l0.9-0.1c0,0,0,0,0.1,0 + l0,0h0.1h0.1c0.1,0,0.1,0.1,0.1,0.2c0,0.1,0,0.2,0,0.4c0,0.1,0,0.3,0,0.4v0.3c0,0.5,0,1,0,1.4v0.2c0,0.4,0,0.8-0.1,1.1 + c0,0.1,0,0.2,0,0.3s0,0.2,0,0.3s0,0.4-0.4,0.5c-0.7,0.1-1.3,0.1-2,0.2h-0.2c-0.2,0-0.5,0-0.8,0.1c-0.4,0-0.5,0.1-0.6,0.1h-0.1 + l-0.2-0.1c-0.1,0-0.1-0.1-0.1-0.2L25,169.6L25,169.6z M25,175.7c0-0.1,0.1-0.2,0.2-0.2h0.2l3.4-0.2c0,0,0.1,0,0.1,0.1 + c0,0,0.1,0.1,0,0.1v0.1l0,0c0,0,0,0,0,0.1v0.1v0.1c0,0.1,0,0.2-0.1,0.3s-0.1,0.3-0.2,0.4l-0.1,0.2c-0.2,0.6-0.5,1.2-0.7,1.8 + c-0.2,0.5-0.6,0.9-0.9,1.3c-0.3,0.3-0.9,0.6-1.5,0.6h-0.2l0,0c0,0-0.1,0-0.1-0.1v-0.1L25,175.7L25,175.7z M21.5,179.6l-0.1,0.1 + l0,0l-0.2-0.1l0,0c-1.8-0.4-4.2-2.6-5.1-5.2v-0.1c0-0.1,0-0.1,0-0.2c0,0,0.1-0.1,0.2,0l0.9,0.3c0.8,0.2,1.6,0.5,2.4,0.7 + c0.2,0.1,0.4,0.1,0.4,0.4c0.3,1.6,0.8,2.8,1.5,3.8l0.1,0.1C21.6,179.5,21.6,179.5,21.5,179.6z M24.2,180.2c0,0.1-0.1,0.1-0.1,0.1 + H24c0,0,0,0-0.1,0c0,0-0.9-0.3-1.3-0.8c-1-1.3-1.5-2.7-1.9-4l0,0v-0.1c0-0.1,0-0.1,0-0.1l0.1-0.1h0.1l1.9,0.2l1.2,0.1 + c0.1,0,0.2,0.1,0.2,0.2L24.2,180.2L24.2,180.2z M22.8,169.4l1.2,0.1c0.1,0,0.2,0.1,0.2,0.2v5c0,0,0,0.1-0.1,0.1H24l0,0 + c0,0-0.9-0.1-1.4-0.1c-0.6,0-1.2-0.1-1.7-0.2c-0.2,0-0.4-0.3-0.4-0.4c-0.1-1.2-0.1-2.3-0.2-3.1c0-0.3,0-1.4,0-1.5 + c0-0.1,0-0.1,0.1-0.1c0,0,0.1-0.1,0.1,0H22.8z M16.4,167.2L16.4,167.2c0.8-2.1,2.8-3.9,5-4.7l0.2-0.1c0.1,0,0.2,0,0.2,0.1 + c0,0.1,0,0.1,0,0.2l-0.1,0.2c-1.2,1.6-1.7,3.5-2,5.3v0.1c0,0,0,0.1-0.1,0.1h-0.1c0,0,0,0-0.1,0h-0.1c-0.4-0.1-0.7-0.2-1.1-0.3 + c-0.6-0.1-1.2-0.3-1.8-0.5h-0.1C16.4,167.4,16.3,167.3,16.4,167.2z M15.8,168.4L15.8,168.4c0-0.2,0.1-0.2,0.2-0.2l3.3,0.8 + c0.1,0,0.1,0.1,0.1,0.2l0.1,4.7c0,0.1,0,0.1-0.1,0.1h-0.1l0,0h-0.1h-0.1l0,0c-0.1,0-0.3-0.1-0.5-0.1c-1.1-0.3-1.9-0.6-2.6-1.1 + c-0.4-0.3-0.6-0.6-0.6-1C15.4,170.7,15.5,169.5,15.8,168.4z"/> - + diff --git a/scripts/system/assets/images/tools/edit.svg b/scripts/system/assets/images/tools/edit.svg index 13389a2b14..f65e0cd84d 100644 --- a/scripts/system/assets/images/tools/edit.svg +++ b/scripts/system/assets/images/tools/edit.svg @@ -3,232 +3,121 @@ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + c0,0,0.1,0,0.1,0c0.1-0.4,0.2-0.8,0.2-1.3C37.2,21.7,33.7,18.9,30.2,19.9z M17.5,11.5c-0.4,0-0.7-0.3-0.7-0.7 + c0-0.4,0.3-0.8,0.7-0.8c0.4,0,0.8,0.4,0.8,0.7C18.3,11.1,17.9,11.5,17.5,11.5z M27.9,13.8c0.3,0.3,0.6,0.7,1,1 + c-0.7,0.7-1.4,1.4-2.1,2.1c-0.3-0.3-0.7-0.7-1-1C26.5,15.2,27.2,14.5,27.9,13.8z"/> - - + - - + c0.2-0.1,0.4-0.3,0.6-0.4s0.3-0.4,0.4-0.7C24.2,39.8,24.2,39.5,24.2,39.2z"/> + + - - - + + + c0,0,0.1,0,0.1,0c0.1-0.4,0.2-0.8,0.2-1.3C37.2,71.7,33.8,68.9,30.3,69.9z M17.6,61.5c-0.4,0-0.7-0.3-0.7-0.7 + c0-0.4,0.3-0.8,0.7-0.8c0.4,0,0.8,0.4,0.8,0.7C18.3,61.1,18,61.5,17.6,61.5z M28,63.8c0.3,0.3,0.6,0.7,1,1 + c-0.7,0.7-1.4,1.4-2.1,2.1c-0.3-0.3-0.7-0.7-1-1C26.6,65.2,27.3,64.5,28,63.8z"/> - - + - - + c0.2-0.1,0.4-0.3,0.6-0.4s0.3-0.4,0.4-0.7C24.2,89.8,24.2,89.5,24.2,89.2z"/> + + - - - + + + c0,0,0.1,0,0.1,0c0.1-0.4,0.2-0.8,0.2-1.3C37.1,121.8,33.7,119,30.2,120z M17.5,111.5c-0.4,0-0.7-0.3-0.7-0.7 + c0-0.4,0.3-0.8,0.7-0.8c0.4,0,0.8,0.4,0.8,0.7C18.2,111.2,17.9,111.5,17.5,111.5z M27.9,113.9c0.3,0.3,0.6,0.7,1,1 + c-0.7,0.7-1.4,1.4-2.1,2.1c-0.3-0.3-0.7-0.7-1-1C26.5,115.3,27.2,114.6,27.9,113.9z"/> - - - - + + + + - - - + + + c0,0,0.1,0,0.1,0c0.1-0.4,0.2-0.8,0.2-1.3C37.1,171.9,33.7,169,30.2,170.1z M17.4,161.6c-0.4,0-0.7-0.3-0.7-0.7 + c0-0.4,0.3-0.8,0.7-0.8c0.4,0,0.8,0.4,0.8,0.7C18.2,161.2,17.8,161.6,17.4,161.6z M27.8,164c0.3,0.3,0.6,0.7,1,1 + c-0.7,0.7-1.4,1.4-2.1,2.1c-0.3-0.3-0.7-0.7-1-1C26.4,165.4,27.1,164.7,27.8,164z"/> - - - - + + + + diff --git a/scripts/system/assets/images/tools/market.svg b/scripts/system/assets/images/tools/market.svg index fa4a3d7256..0cec030933 100644 --- a/scripts/system/assets/images/tools/market.svg +++ b/scripts/system/assets/images/tools/market.svg @@ -3,158 +3,47 @@ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + - - - + c-0.1-0.1-0.2-0.2-0.3-0.2C24,37.1,23.9,37,23.8,37h-1.5V39.1z"/> + + + - - - + c-0.1-0.2-0.1-0.4-0.2-0.6c-0.1-0.2-0.1-0.4-0.2-0.7l-0.2-0.6H32.1z"/> + + - - - + + - - - + c-0.1-0.1-0.2-0.2-0.3-0.2C24,87.1,23.9,87,23.8,87h-1.5V89.1z"/> + + + - - - + c-0.1-0.2-0.1-0.4-0.2-0.6c-0.1-0.2-0.1-0.4-0.2-0.7l-0.2-0.6H32.1z"/> + + - - - + + - - - + c-0.1-0.1-0.2-0.2-0.3-0.2c-0.1-0.1-0.2-0.1-0.3-0.1h-1.5V139.2z"/> + + + - - - + c-0.1-0.6-0.3-1.3-0.5-2c-0.1-0.2-0.1-0.4-0.2-0.6c-0.1-0.2-0.1-0.4-0.2-0.7l-0.2-0.6H32z"/> + + - - - - - - + + + + + + - - - + c-0.1-0.6-0.3-1.3-0.5-2c-0.1-0.2-0.1-0.4-0.2-0.6c-0.1-0.2-0.1-0.4-0.2-0.7l-0.2-0.6H32z"/> + + diff --git a/scripts/system/assets/images/tools/mic.svg b/scripts/system/assets/images/tools/mic.svg index 126bb539fc..0424f3eb49 100644 --- a/scripts/system/assets/images/tools/mic.svg +++ b/scripts/system/assets/images/tools/mic.svg @@ -3,193 +3,124 @@ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - + + + - + - - + - - + c-0.4-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.4-0.6-0.5-1c-0.1-0.4-0.1-0.8-0.1-1.2v-3.3H22v3.3c0,0.3,0,0.5,0.1,0.8 + c0.1,0.2,0.1,0.5,0.3,0.7c0.1,0.2,0.3,0.3,0.5,0.5S23.2,191.8,23.5,191.8z"/> + + - - - - - + + + + - - - - - + + + + + + h-2.3l0-1.8C28.6,79,30.7,76.9,30.7,74.2z"/> - - + + + + + + + + + - - - - - - - - - - + c0.1,0.2,0.1,0.5,0.3,0.7s0.3,0.3,0.5,0.5S30.1,41.9,30.4,41.9z"/> + + - - + + - - + + c0,2.6,2.1,4.8,4.8,5.2l0,1.8h-2.2c-0.5,0-0.9,0.4-0.9,0.9c0,0.5,0.4,0.9,0.9,0.9H28c0.5,0,0.9-0.4,0.9-0.9c0-0.5-0.4-0.9-0.9-0.9 + h-2.3l0-1.8C28.5,29.2,30.6,27,30.6,24.4z"/> - - + + c0.4,0.4,1.1,0.4,1.5,0c0.4-0.4,0.4-1.1,0-1.5L26.1,164.3z"/> - - + + c0,2.6,2.1,4.8,4.8,5.2l0,1.8h-2.2c-0.5,0-0.9,0.4-0.9,0.9c0,0.5,0.4,0.9,0.9,0.9H28c0.5,0,0.9-0.4,0.9-0.9c0-0.5-0.4-0.9-0.9-0.9 + h-2.3l0-1.8C28.5,179.1,30.6,176.9,30.6,174.3z"/> - - - - + + + - - + s-0.6-0.4-0.9-0.7c-0.2-0.3-0.4-0.6-0.5-1c-0.1-0.4-0.1-0.8-0.1-1.2v-3.3h1.2v3.3c0,0.3,0,0.5,0.1,0.8c0.1,0.2,0.1,0.5,0.3,0.7 + c0.1,0.2,0.3,0.3,0.5,0.5S30,141.9,30.3,141.9z"/> + + diff --git a/scripts/system/assets/images/tools/switch.svg b/scripts/system/assets/images/tools/switch.svg index 23655d747d..e67a9aac04 100644 --- a/scripts/system/assets/images/tools/switch.svg +++ b/scripts/system/assets/images/tools/switch.svg @@ -3,228 +3,165 @@ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - + - + - - - - - - - - - - - - - - - - - + + + - - - - + + + + + + + + + + + + + + + - - + + - - - + + + - - - - + + + + - - + + - - - + + + - - - - + + + + - + C31.3,190.2,31.3,189.8,31.3,189.4z"/> + + + From 218b26b5213cd1bc8af041064b4d5e82644e7334 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 1 Jul 2016 10:58:55 -0700 Subject: [PATCH 0874/1237] removed snapshot share dialog, ctrl s shouldn't leave you moving --- .../qml/hifi/dialogs/SnapshotShareDialog.qml | 117 ------------------ interface/src/Application.cpp | 12 +- interface/src/ui/Snapshot.cpp | 115 ----------------- interface/src/ui/Snapshot.h | 8 -- interface/src/ui/SnapshotShareDialog.cpp | 47 ------- .../src/input-plugins/KeyboardMouseDevice.cpp | 8 +- 6 files changed, 6 insertions(+), 301 deletions(-) delete mode 100644 interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml delete mode 100644 interface/src/ui/SnapshotShareDialog.cpp diff --git a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml deleted file mode 100644 index f99b770a78..0000000000 --- a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml +++ /dev/null @@ -1,117 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtQuick.XmlListModel 2.0 - -import "../../windows" -import "../../js/Utils.js" as Utils -import "../models" - -Window { - id: root - resizable: true - width: 516 - height: 616 - minSize: Qt.vector2d(500, 600); - maxSize: Qt.vector2d(1000, 800); - - property alias source: image.source - - Rectangle { - anchors.fill: parent - color: "white" - - Item { - anchors { fill: parent; margins: 8 } - - Image { - id: image - anchors { top: parent.top; left: parent.left; right: parent.right; bottom: notesLabel.top; bottomMargin: 8 } - fillMode: Image.PreserveAspectFit - } - - Text { - id: notesLabel - anchors { left: parent.left; bottom: notes.top; bottomMargin: 8; } - text: "Notes about this image" - font.pointSize: 14 - font.bold: true - color: "#666" - } - - TextArea { - id: notes - anchors { left: parent.left; bottom: parent.bottom; right: shareButton.left; rightMargin: 8 } - height: 60 - } - - Button { - id: shareButton - anchors { verticalCenter: notes.verticalCenter; right: parent.right; } - width: 120; height: 50 - text: "Share" - - style: ButtonStyle { - background: Rectangle { - implicitWidth: 120 - implicitHeight: 50 - border.width: control.activeFocus ? 2 : 1 - color: "#333" - radius: 9 - } - label: Text { - color: shareButton.enabled ? "white" : "gray" - font.pixelSize: 18 - font.bold: true - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - anchors.fill: parent - text: shareButton.text - } - } - - onClicked: { - enabled = false; - uploadTimer.start(); - } - - Timer { - id: uploadTimer - running: false - interval: 5 - repeat: false - onTriggered: { - var uploaded = SnapshotUploader.uploadSnapshot(root.source.toString()) - console.log("Uploaded result " + uploaded) - if (!uploaded) { - console.log("Upload failed "); - } - } - } - } - } - - Action { - id: shareAction - text: qsTr("OK") - enabled: root.result ? true : false - shortcut: Qt.Key_Return - onTriggered: { - root.destroy(); - } - } - - Action { - id: cancelAction - text: qsTr("Cancel") - shortcut: Qt.Key_Escape - onTriggered: { - root.destroy(); - } - } - } -} - - - - diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1c9ec94dc4..9d177d724d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1517,7 +1517,6 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" - rootContext->setContextProperty("SnapshotUploader", new SnapshotUploader()); rootContext->setContextProperty("ApplicationInterface", this); rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("Controller", DependencyManager::get().data()); @@ -4989,16 +4988,7 @@ void Application::takeSnapshot() { player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - QString fileName = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); - - auto accountManager = DependencyManager::get(); - if (!accountManager->isLoggedIn()) { - return; - } - - DependencyManager::get()->load("hifi/dialogs/SnapshotShareDialog.qml", [=](QQmlContext*, QObject* dialog) { - dialog->setProperty("source", QUrl::fromLocalFile(fileName)); - }); + Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); } float Application::getRenderResolutionScale() const { diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index b8be2bb8c4..a3af742f92 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -133,118 +133,3 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { return imageTempFile; } } - -const QString FORUM_URL = "https://alphas.highfidelity.io"; -const QString FORUM_UPLOADS_URL = FORUM_URL + "/uploads"; -const QString FORUM_POST_URL = FORUM_URL + "/posts"; -const QString FORUM_REPLY_TO_TOPIC = "244"; -const QString FORUM_POST_TEMPLATE = "

%2

"; -const QString SHARE_DEFAULT_ERROR = "The server isn't responding. Please try again in a few minutes."; -const QString SUCCESS_LABEL_TEMPLATE = "Success!!! Go check out your image ...
%1"; - - -QString SnapshotUploader::uploadSnapshot(const QUrl& fileUrl) { - auto accountManager = DependencyManager::get(); - if (accountManager->getAccountInfo().getDiscourseApiKey().isEmpty()) { - OffscreenUi::warning(nullptr, "", "Your Discourse API key is missing, you cannot share snapshots. Please try to relog."); - return QString(); - } - - QHttpPart apiKeyPart; - apiKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"api_key\"")); - apiKeyPart.setBody(accountManager->getAccountInfo().getDiscourseApiKey().toLatin1()); - - QString filename = fileUrl.toLocalFile(); - qDebug() << filename; - QFile* file = new QFile(filename); - Q_ASSERT(file->exists()); - file->open(QIODevice::ReadOnly); - - QHttpPart imagePart; - imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); - imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"file\"; filename=\"" + file->fileName() + "\"")); - imagePart.setBodyDevice(file); - - QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - file->setParent(multiPart); // we cannot delete the file now, so delete it with the multiPart - multiPart->append(apiKeyPart); - multiPart->append(imagePart); - - QUrl url(FORUM_UPLOADS_URL); - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - - QString result; - QEventLoop loop; - - QSharedPointer reply(NetworkAccessManager::getInstance().post(request, multiPart)); - QObject::connect(reply.data(), &QNetworkReply::finished, [&] { - loop.quit(); - - qDebug() << reply->errorString(); - for (const auto& header : reply->rawHeaderList()) { - qDebug() << "Header " << QString(header); - } - auto replyResult = reply->readAll(); - qDebug() << QString(replyResult); - QJsonDocument jsonResponse = QJsonDocument::fromJson(replyResult); - const QJsonObject& responseObject = jsonResponse.object(); - if (!responseObject.contains("url")) { - OffscreenUi::warning(this, "", SHARE_DEFAULT_ERROR); - return; - } - result = responseObject["url"].toString(); - }); - loop.exec(); - return result; -} - -QString SnapshotUploader::sendForumPost(const QString& snapshotPath, const QString& notes) { - // post to Discourse forum - QNetworkRequest request; - request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QUrl forumUrl(FORUM_POST_URL); - - QUrlQuery query; - query.addQueryItem("api_key", DependencyManager::get()->getAccountInfo().getDiscourseApiKey()); - query.addQueryItem("topic_id", FORUM_REPLY_TO_TOPIC); - query.addQueryItem("raw", FORUM_POST_TEMPLATE.arg(snapshotPath, notes)); - forumUrl.setQuery(query); - - QByteArray postData = forumUrl.toEncoded(QUrl::RemoveFragment); - request.setUrl(forumUrl); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - QNetworkReply* requestReply = NetworkAccessManager::getInstance().post(request, postData); - - QEventLoop loop; - QString result; - connect(requestReply, &QNetworkReply::finished, [&] { - loop.quit(); - QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); - requestReply->deleteLater(); - const QJsonObject& responseObject = jsonResponse.object(); - - if (!responseObject.contains("id")) { - QString errorMessage(SHARE_DEFAULT_ERROR); - if (responseObject.contains("errors")) { - QJsonArray errorArray = responseObject["errors"].toArray(); - if (!errorArray.first().toString().isEmpty()) { - errorMessage = errorArray.first().toString(); - } - } - OffscreenUi::warning(this, "", errorMessage); - return; - } - - const QString urlTemplate = "%1/t/%2/%3/%4"; - result = urlTemplate.arg(FORUM_URL, - responseObject["topic_slug"].toString(), - QString::number(responseObject["topic_id"].toDouble()), - QString::number(responseObject["post_number"].toDouble())); - }); - loop.exec(); - return result; -} - diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index d87a70255f..5856743141 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -43,12 +43,4 @@ private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; -class SnapshotUploader : public QObject{ - Q_OBJECT -public: - SnapshotUploader(QObject* parent = nullptr) : QObject(parent) {} - Q_INVOKABLE QString uploadSnapshot(const QUrl& fileUrl); - Q_INVOKABLE QString sendForumPost(const QString& snapshotPath, const QString& notes); -}; - #endif // hifi_Snapshot_h diff --git a/interface/src/ui/SnapshotShareDialog.cpp b/interface/src/ui/SnapshotShareDialog.cpp deleted file mode 100644 index 94f89641e2..0000000000 --- a/interface/src/ui/SnapshotShareDialog.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// -// SnapshotShareDialog.cpp -// interface/src/ui -// -// Created by Stojce Slavkovski on 2/16/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#if 0 - - -#include - -const int NARROW_SNAPSHOT_DIALOG_SIZE = 500; -const int WIDE_SNAPSHOT_DIALOG_WIDTH = 650; -const int SUCCESS_LABEL_HEIGHT = 140; - -const QString SHARE_BUTTON_STYLE = "border-width:0;border-radius:9px;border-radius:9px;font-family:Arial;font-size:18px;" - "font-weight:100;color:#FFFFFF;width: 120px;height: 50px;"; -const QString SHARE_BUTTON_ENABLED_STYLE = "background-color: #333;"; -const QString SHARE_BUTTON_DISABLED_STYLE = "background-color: #999;"; - -Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) - -SnapshotShareDialog::SnapshotShareDialog(QString fileName, QWidget* parent) : - QDialog(parent), - _fileName(fileName) -{ - - - _ui.snapshotWidget->setPixmap(snaphsotPixmap); - _ui.snapshotWidget->adjustSize(); -} - -void SnapshotShareDialog::accept() { - // prevent multiple clicks on share button - _ui.shareButton->setEnabled(false); - // gray out share button - _ui.shareButton->setStyleSheet(SHARE_BUTTON_STYLE + SHARE_BUTTON_DISABLED_STYLE); - uploadSnapshot(); -} - - -#endif diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index 915ec1db87..ebe80f12cf 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -49,9 +49,11 @@ void KeyboardMouseDevice::InputDevice::focusOutEvent() { void KeyboardMouseDevice::keyPressEvent(QKeyEvent* event) { auto input = _inputDevice->makeInput((Qt::Key) event->key()); - auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); - if (!result.second) { - // key pressed again ? without catching the release event ? + if (!(event->modifiers() & Qt::KeyboardModifier::ControlModifier)) { + auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); + if (result.second) { + // key pressed again ? without catching the release event ? + } } } From 4762091aaf60c9c2b478952e60bd28b825de3642 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 11:12:20 -0700 Subject: [PATCH 0875/1237] add easy mode --- scripts/system/controllers/teleport.js | 85 ++++++++++++++++++++------ 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 42ffd2dac9..f26261d0ff 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -19,10 +19,10 @@ //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) - - var inTeleportMode = false; +var easyMode = true; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -53,7 +53,6 @@ function Trigger(hand) { }; } - function Teleporter() { var _this = this; this.targetProps = null; @@ -107,7 +106,13 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - Script.update.connect(this.update); + if (easyMode !== true) { + Script.update.connect(this.update); + + } else { + Script.update.connect(this.updateEasy); + + } }; this.exitTeleportMode = function(value) { @@ -117,7 +122,11 @@ function Teleporter() { this.leftOverlayOff(); Entities.deleteEntity(_this.targetEntity); this.enableGrab(); - Script.update.disconnect(this.update); + if (easyMode !== true) { + Script.update.disconnect(this.update); + } else { + Script.update.disconnect(this.updateEasy); + } this.updateConnected = false; inTeleportMode = false; @@ -126,7 +135,6 @@ function Teleporter() { this.update = function() { //print('in teleporter update') - if (teleporter.teleportHand === 'left') { teleporter.leftRay(); if (leftPad.buttonValue === 0) { @@ -150,6 +158,25 @@ function Teleporter() { }; + this.updateEasy = function() { + + if (teleporter.teleportHand === 'left') { + teleporter.leftRay(); + if (leftPad.buttonValue === 0) { + _this.teleport(); + return; + } + + } else { + teleporter.rightRay(); + if (rightPad.buttonValue === 0) { + _this.teleport(); + return; + } + } + + }; + this.rightRay = function() { var rightPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).translation), MyAvatar.position); @@ -357,6 +384,7 @@ function getJointData() { } + var leftPad = new ThumbPad('left'); var rightPad = new ThumbPad('right'); var leftTrigger = new Trigger('left'); @@ -364,20 +392,41 @@ var rightTrigger = new Trigger('right'); //create a controller mapping and make sure to disable it when the script is stopped -var mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); -var teleportMapping = Controller.newMapping(mappingName); -teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); -teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); -teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); -teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); +var mappingName, teleportMapping; -teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { - teleporter.enterTeleportMode('left') -}); -teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { - teleporter.enterTeleportMode('right') -}); +function registerMappings() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); + teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { + teleporter.enterTeleportMode('right') + }); + +} + +function registerMappingsEasy() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + + teleportMapping.from(leftPad.down).to(function() { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightPad.down).to(function() { + teleporter.enterTeleportMode('right') + }); +} + +registerMappingsEasy(); var teleporter = new Teleporter(); Controller.enableMapping(mappingName); From 02cc6cc567150909587d9f09c39882b354f5c4a3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 1 Jul 2016 12:18:26 -0700 Subject: [PATCH 0876/1237] hover states --- .../qml/hifi/toolbars/ToolbarButton.qml | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index a3be4533d2..4356b18253 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -8,6 +8,7 @@ Item { property var subImage; property int yOffset: 0 property int buttonState: 0 + property int hoverOffset: 0 property var toolbar; property real size: 50 // toolbar ? toolbar.buttonSize : 50 width: size; height: size @@ -36,9 +37,15 @@ Item { } } - + function updateOffset() { + yOffset = size * (buttonState + hoverOffset); + } onButtonStateChanged: { - yOffset = size * buttonState + hoverOffset = 0; // subtle: show the new state without hover. don't wait for mouse to be moved away + // The above is per UX design, but ALSO avoid a subtle issue that would be a problem because + // the hand controllers don't move the mouse when not triggered, so releasing the trigger would + // never show unhovered. + updateOffset(); } Component.onCompleted: { @@ -58,8 +65,18 @@ Item { } MouseArea { + id: mouseArea + hoverEnabled: true anchors.fill: parent onClicked: button.clicked(); + onEntered: { + hoverOffset = 2; + updateOffset(); + } + onExited: { + hoverOffset = 0; + updateOffset(); + } } } From 1ad9cfde99c3a882537f3d39c4f1821939fde12f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 1 Jul 2016 13:54:00 -0700 Subject: [PATCH 0877/1237] make hud button participate in buttonState just like other toolbar chicklets --- interface/resources/qml/hifi/Desktop.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 7a7a6b63df..561bd722f2 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -61,8 +61,8 @@ OriginalDesktop.Desktop { pinned: true, }); - toggleHudButton.yOffset = Qt.binding(function(){ - return desktop.pinned ? 50 : 0 + toggleHudButton.buttonState = Qt.binding(function(){ + return desktop.pinned ? 1 : 0 }); toggleHudButton.clicked.connect(function(){ console.log("Clicked on hud button") From ab98614dd47887d85f8c204eacbe3e0c617b27a4 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 1 Jul 2016 14:39:32 -0700 Subject: [PATCH 0878/1237] Fixing the basic to compile the TestWindow --- tests/gpu-test/src/TestWindow.cpp | 13 ++++++++++--- tests/gpu-test/src/TestWindow.h | 9 +++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index 4fe25e989d..c601e8c077 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -98,7 +98,8 @@ void TestWindow::beginFrame() { #ifdef DEFERRED_LIGHTING auto deferredLightingEffect = DependencyManager::get(); - deferredLightingEffect->prepare(_renderArgs); + + _prepareDeferred.run(_sceneContext, _renderContext); #else gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 0.0f, 0.1f, 0.2f, 1.0f }); @@ -128,8 +129,14 @@ void TestWindow::endFrame() { batch.setResourceTexture(0, nullptr); }); - auto deferredLightingEffect = DependencyManager::get(); - deferredLightingEffect->render(_renderContext); + DeferredFrameTransformPointer frameTransform; + _generateDeferredFrameTransform.run(_sceneContext, _renderContext, frameTransform); + + // auto deferredLightingEffect = DependencyManager::get(); + // deferredLightingEffect->render(_renderContext); + RenderDeferred::Inputs deferredInputs; + deferredInputs.edit0() = frameTransform; + _renderDeferred.run(_sceneContext, _renderContext, deferredInputs); gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { PROFILE_RANGE_BATCH(batch, "blit"); diff --git a/tests/gpu-test/src/TestWindow.h b/tests/gpu-test/src/TestWindow.h index 5b6b205721..d6c11b7459 100644 --- a/tests/gpu-test/src/TestWindow.h +++ b/tests/gpu-test/src/TestWindow.h @@ -19,7 +19,7 @@ #include #include -//#define DEFERRED_LIGHTING +#define DEFERRED_LIGHTING class TestWindow : public QWindow { protected: @@ -31,9 +31,14 @@ protected: #ifdef DEFERRED_LIGHTING // Prepare the ShapePipelines render::ShapePlumberPointer _shapePlumber { std::make_shared() }; - render::RenderContextPointer _renderContext { std::make_shared() }; + render::SceneContextPointer _sceneContext{ std::make_shared() }; + render::RenderContextPointer _renderContext{ std::make_shared() }; gpu::PipelinePointer _opaquePipeline; model::LightPointer _light { std::make_shared() }; + + GenerateDeferredFrameTransform _generateDeferredFrameTransform; + PrepareDeferred _prepareDeferred; + RenderDeferred _renderDeferred; #endif RenderArgs* _renderArgs { new RenderArgs() }; From 7f6c441b22a21fe70d13b2034abf941decbdab2d Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 1 Jul 2016 15:05:52 -0700 Subject: [PATCH 0879/1237] COmpiling the test --- tests/gpu-test/src/TestWindow.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index c601e8c077..104755c1b6 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -132,8 +132,6 @@ void TestWindow::endFrame() { DeferredFrameTransformPointer frameTransform; _generateDeferredFrameTransform.run(_sceneContext, _renderContext, frameTransform); - // auto deferredLightingEffect = DependencyManager::get(); - // deferredLightingEffect->render(_renderContext); RenderDeferred::Inputs deferredInputs; deferredInputs.edit0() = frameTransform; _renderDeferred.run(_sceneContext, _renderContext, deferredInputs); From ffbfc89e99565277a2a1c05475c458df576471db Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 15:19:37 -0700 Subject: [PATCH 0880/1237] teleportr --- scripts/system/controllers/teleport.js | 129 ++++++++++--------------- 1 file changed, 51 insertions(+), 78 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index f26261d0ff..1486f8390f 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -13,7 +13,7 @@ // alternate notes for philip: -// try just thumb to teleport +// try just thumb to teleport xxx // cancel if destination is within ~1m of current location @@ -23,6 +23,13 @@ var inTeleportMode = false; var easyMode = true; +var TARGET_MODEL_URL = 'http://hifi-production.s3.amazonaws.com/DomainContent/Toybox/potted_plant/potted_plant.fbx'; +var TARGET_MODEL_DIMENSIONS = { + x: 1.1005, + y: 2.1773, + z: 1.0739 +}; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -62,24 +69,22 @@ function Teleporter() { print('jbp initialize') this.createMappings(); this.disableGrab(); - _this.targetEntity = Entities.addEntity({ - name: 'Hifi-Teleporter-Target-Entity', - position: MyAvatar.position, - type: 'Sphere', - dimensions: { - x: 0.2, - y: 0.2, - z: 0.2 - }, - color: { - red: 255, - green: 255, - blue: 255 - }, - collisionless: true, - collidesWith: '', - visible: true + + var cameraEuler = Quat.safeEulerAngles(Camera.orientation); + var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { + x: 0, + y: 1, + z: 0 }); + + var targetOverlayProps = { + url: TARGET_MODEL_URL, + position: MyAvatar.position, + rotation: towardsMe, + dimensions: TARGET_MODEL_DIMENSIONS + }; + + _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); }; @@ -106,59 +111,28 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - if (easyMode !== true) { - Script.update.connect(this.update); + Script.update.connect(this.update); - } else { - Script.update.connect(this.updateEasy); - - } }; this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); + Script.update.disconnect(this.update); this.disableMappings(); this.rightOverlayOff(); this.leftOverlayOff(); - Entities.deleteEntity(_this.targetEntity); + // Entities.deleteEntity(_this.targetEntity); + Overlays.deleteOverlay(_this.targetOverlay); this.enableGrab(); - if (easyMode !== true) { - Script.update.disconnect(this.update); - } else { - Script.update.disconnect(this.updateEasy); - } + this.updateConnected = false; - inTeleportMode = false; - + Script.setTimeout(function() { + inTeleportMode = false; + }, 100); }; + this.update = function() { - //print('in teleporter update') - - if (teleporter.teleportHand === 'left') { - teleporter.leftRay(); - if (leftPad.buttonValue === 0) { - _this.exitTeleportMode(); - return; - } - if (leftTrigger.buttonValue === 0) { - _this.teleport(); - } - } else { - teleporter.rightRay(); - if (rightPad.buttonValue === 0) { - _this.exitTeleportMode(); - return; - } - if (rightTrigger.buttonValue === 0) { - _this.teleport(); - } - } - - - }; - - this.updateEasy = function() { if (teleporter.teleportHand === 'left') { teleporter.leftRay(); @@ -199,7 +173,8 @@ function Teleporter() { var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - this.updateTargetEntity(rightIntersection); + this.updateTargetOverlay(rightIntersection); + }; }; @@ -221,10 +196,10 @@ function Teleporter() { green: 255, blue: 0 }); - var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); if (leftIntersection.intersects) { - this.updateTargetEntity(leftIntersection); + this.updateTargetOverlay(leftIntersection); }; }; @@ -298,18 +273,17 @@ function Teleporter() { } }; - this.updateTargetEntity = function(intersection) { - var targetProps = Entities.getEntityProperties(this.targetEntity); + this.updateTargetOverlay = function(intersection) { + this.intersection=intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y + targetProps.dimensions.y / 2, + y: intersection.intersection.y+TARGET_MODEL_DIMENSIONS.y, z: intersection.intersection.z } - Entities.editEntity(this.targetEntity, { + Overlays.editOverlay(this.targetOverlay, { position: position }); - - + }; this.disableGrab = function() { @@ -321,17 +295,16 @@ function Teleporter() { }; this.teleport = function(value) { - //todo - //get the y position of the teleport landing spot - print('value on teleport: ' + value) - var properties = Entities.getEntityProperties(teleporter.targetEntity); + + print('TELEPORT CALLED'); + var offset = getAvatarFootOffset(); - properties.position.y += offset; - - print('OFFSET IS::: ' + JSON.stringify(offset)) - print('TELEPORT POSITION IS:: ' + JSON.stringify(properties.position)); - MyAvatar.position = properties.position; - + + // _this.intersectionPosition.y+=offset; + // print('OFFSET IS::: ' + JSON.stringify(offset)) + // print('TELEPORT POSITION IS:: ' + JSON.stringify(_this.intersectionPosition)); + _this.intersection.intersection.y+=offset; + MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); }; } @@ -438,7 +411,7 @@ function cleanup() { teleporter.disableMappings(); teleporter.rightOverlayOff(); teleporter.leftOverlayOff(); - Entities.deleteEntity(teleporter.targetEntity); + Overlays.deleteOverlay(teleporter.targetOverlay); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From 9afc4d0c26fd3982c677678e03cadcc979ea6c95 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 15:19:53 -0700 Subject: [PATCH 0881/1237] cleanup --- scripts/system/controllers/teleport.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 1486f8390f..65c3274684 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -124,7 +124,7 @@ function Teleporter() { // Entities.deleteEntity(_this.targetEntity); Overlays.deleteOverlay(_this.targetOverlay); this.enableGrab(); - + this.updateConnected = false; Script.setTimeout(function() { inTeleportMode = false; @@ -274,16 +274,16 @@ function Teleporter() { }; this.updateTargetOverlay = function(intersection) { - this.intersection=intersection; + this.intersection = intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y+TARGET_MODEL_DIMENSIONS.y, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { position: position }); - + }; this.disableGrab = function() { @@ -299,11 +299,8 @@ function Teleporter() { print('TELEPORT CALLED'); var offset = getAvatarFootOffset(); - - // _this.intersectionPosition.y+=offset; - // print('OFFSET IS::: ' + JSON.stringify(offset)) - // print('TELEPORT POSITION IS:: ' + JSON.stringify(_this.intersectionPosition)); - _this.intersection.intersection.y+=offset; + + _this.intersection.intersection.y += offset; MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); }; From 00ce75ef485b70fba81c58f3c12d441382a5cc92 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 15:30:21 -0700 Subject: [PATCH 0882/1237] more notes --- scripts/system/controllers/teleport.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 65c3274684..4b8c31c918 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -7,18 +7,19 @@ //release trigger to teleport then exit teleport mode xxx //if thumb is release, exit teleport mode xxx -//v2: show room boundaries when choosing a place to teleport -//v2: smooth fade screen in/out? -//v2: haptic feedback - -// alternate notes for philip: // try just thumb to teleport xxx -// cancel if destination is within ~1m of current location - //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) + +//terminate the line when there is an intersection +//when there's not an intersection, set a fixed distance? + + +//v2: show room boundaries when choosing a place to teleport +//v2: smooth fade screen in/out? +//v2: haptic feedback var inTeleportMode = false; var easyMode = true; From 88d28120b683160a65a291884f09704cc6894593 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 1 Jul 2016 15:48:58 -0700 Subject: [PATCH 0883/1237] Adding missed file --- plugins/openvr/src/OpenVrHelpers.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 399712d920..c93a2178b5 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -268,6 +268,7 @@ void handleOpenVrEvents() { case vr::VREvent_KeyboardClosed: _keyboardFocusObject = nullptr; _keyboardShown = false; + DependencyManager::get()->unfocusWindows(); break; default: From 760f4366e13e357a66603d81e64c2e2a98087f84 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 2 Jul 2016 11:25:04 +1200 Subject: [PATCH 0884/1237] Don't show users border when hover mouse if logged out --- scripts/system/users.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/system/users.js b/scripts/system/users.js index d25e1b76b4..2ff2689bad 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -826,6 +826,10 @@ var usersWindow = (function () { function onMouseMoveEvent(event) { var isVisible; + if (!isLoggedIn) { + return; + } + if (isMovingScrollbar) { if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN From 9b822f97c03cd62b741a7743531f14ac0603427c Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 17:26:52 -0700 Subject: [PATCH 0885/1237] end of day --- scripts/system/controllers/teleport.js | 56 ++++++++++++++++---------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 4b8c31c918..7a826e2ca7 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,3 +1,5 @@ +// by james b. pollack @imgntn on 7/2/2016 + //v1 //check if trigger is down xxx //if trigger is down, check if thumb is down xxx @@ -16,19 +18,18 @@ //terminate the line when there is an intersection //when there's not an intersection, set a fixed distance? - //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? //v2: haptic feedback + var inTeleportMode = false; -var easyMode = true; +var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; -var TARGET_MODEL_URL = 'http://hifi-production.s3.amazonaws.com/DomainContent/Toybox/potted_plant/potted_plant.fbx'; var TARGET_MODEL_DIMENSIONS = { - x: 1.1005, - y: 2.1773, - z: 1.0739 + x: 1.15, + y: 0.5 + z: 1.15 }; function ThumbPad(hand) { @@ -167,16 +168,17 @@ function Teleporter() { var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); this.rightLineOn(rightPickRay.origin, location, { - red: 255, - green: 0, - blue: 0 + red: 7, + green: 36, + blue: 44 }); var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { this.updateTargetOverlay(rightIntersection); - - }; + } else { + this.noIntersection() + } }; this.leftRay = function() { @@ -193,22 +195,24 @@ function Teleporter() { var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); this.leftLineOn(leftPickRay.origin, location, { - red: 0, - green: 255, - blue: 0 + red: 7, + green: 36, + blue: 44 }); var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); if (leftIntersection.intersects) { this.updateTargetOverlay(leftIntersection); - }; + } else { + this.noIntersection() + } }; this.rightLineOn = function(closePoint, farPoint, color) { // draw a line if (this.rightOverlayLine === null) { var lineProperties = { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -221,7 +225,7 @@ function Teleporter() { } else { var success = Overlays.editOverlay(this.rightOverlayLine, { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -236,7 +240,7 @@ function Teleporter() { // draw a line if (this.leftOverlayLine === null) { var lineProperties = { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -249,7 +253,7 @@ function Teleporter() { } else { var success = Overlays.editOverlay(this.leftOverlayLine, { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -274,6 +278,13 @@ function Teleporter() { } }; + this.noIntersection = function() { + print('no intersection' + teleporter.targetOverlay); + Overlays.editOverlay(teleporter.targetOverlay, { + visible: false, + }); + }; + this.updateTargetOverlay = function(intersection) { this.intersection = intersection; var position = { @@ -282,7 +293,8 @@ function Teleporter() { z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { - position: position + position: position, + visible: true }); }; @@ -382,7 +394,7 @@ function registerMappings() { } -function registerMappingsEasy() { +function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); @@ -397,7 +409,7 @@ function registerMappingsEasy() { }); } -registerMappingsEasy(); +registerMappings(); var teleporter = new Teleporter(); Controller.enableMapping(mappingName); From 2a52f1db7f3881b7cb3ec2a91febddac70f4ddd3 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 1 Jul 2016 18:36:51 -0700 Subject: [PATCH 0886/1237] Adding the lighting model class to configure accross the jobs the lighting --- .../render-utils/src/DeferredGlobalLight.slh | 10 +- .../src/DeferredLightingEffect.cpp | 17 ++- .../render-utils/src/DeferredLightingEffect.h | 16 +- libraries/render-utils/src/LightingModel.cpp | 134 +++++++++++++++++ libraries/render-utils/src/LightingModel.h | 142 ++++++++++++++++++ libraries/render-utils/src/LightingModel.slh | 64 ++++++++ .../render-utils/src/MaterialTextures.slh | 3 +- .../render-utils/src/RenderDeferredTask.cpp | 7 +- .../render-utils/src/SubsurfaceScattering.cpp | 7 +- .../render-utils/src/SubsurfaceScattering.h | 3 +- libraries/render/src/render/Task.h | 26 ++++ .../utilities/render/deferredLighting.qml | 29 ++-- 12 files changed, 421 insertions(+), 37 deletions(-) create mode 100644 libraries/render-utils/src/LightingModel.cpp create mode 100644 libraries/render-utils/src/LightingModel.h create mode 100644 libraries/render-utils/src/LightingModel.slh diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 90abb9709a..6791a90829 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -14,6 +14,9 @@ <@include model/Light.slh@> <@include DeferredLighting.slh@> +<@include LightingModel.slh@> +<$declareLightingModel()$> + <@func declareSkyboxMap()@> // declareSkyboxMap uniform samplerCube skyboxMap; @@ -79,8 +82,9 @@ vec3 evalGlobalSpecularIrradiance(Light light, vec3 fragEyeDir, vec3 fragNormal, metallic = 1.0; } vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, metallic, fresnel, roughness); - vec3 color = vec3(albedo * shading.w + shading.rgb) * min(shadowAttenuation, obscurance) * getLightColor(light) * getLightIntensity(light); - color += emissive; + vec3 color = vec3(0.0); + color += vec3(albedo * shading.w * isDiffuseEnabled() + shading.rgb * isSpecularEnabled()) * min(shadowAttenuation, obscurance) * getLightColor(light) * getLightIntensity(light) * isDirectionalEnabled(); + color += emissive * isEmissiveEnabled(); <@endfunc@> @@ -180,7 +184,7 @@ vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenua brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); - vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); + vec3 color = vec3(albedo * vec3(brdf.xyz) * isDiffuseEnabled() * shading.w + shading.rgb * isSpecularEnabled()) * getLightColor(light) * getLightIntensity(light) * isDirectionalEnabled(); // Diffuse from ambient diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 369466275b..d7e9b311e3 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -66,6 +66,7 @@ enum DeferredShader_MapSlot { }; enum DeferredShader_BufferSlot { DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 0, + LIGHTING_MODEL_BUFFER_SLOT, SCATTERING_PARAMETERS_BUFFER_SLOT, LIGHT_GPU_SLOT, }; @@ -180,6 +181,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), LIGHTING_MODEL_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("subsurfaceScatteringParametersBuffer"), SCATTERING_PARAMETERS_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); @@ -354,6 +356,7 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, + const LightingModelPointer& lightingModel, const gpu::TexturePointer& diffusedCurvature2, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource) { @@ -393,6 +396,9 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c // The Deferred Frame Transform buffer batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, frameTransform->getFrameTransformBuffer()); + // THe lighting model + batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, lightingModel->getParametersBuffer()); + // Subsurface scattering specific batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, framebufferCache->getCurvatureTexture()); batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, diffusedCurvature2); @@ -599,6 +605,7 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, batch.setResourceTexture(SCATTERING_SPECULAR_UNIT, nullptr); batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, nullptr); + batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, nullptr); batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, nullptr); }); @@ -619,19 +626,17 @@ RenderDeferred::RenderDeferred() { void RenderDeferred::configure(const Config& config) { - - _enablePointLights = config.enablePointLights; - _enableSpotLights = config.enableSpotLights; } void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { auto deferredTransform = inputs.get0(); + auto lightingModel = inputs.get1(); auto diffusedCurvature2 = inputs.get2()->getRenderBuffer(0); - auto subsurfaceScatteringResource = inputs.get3(); + auto subsurfaceScatteringResource = inputs.get4(); - setupJob.run(sceneContext, renderContext, deferredTransform, diffusedCurvature2, subsurfaceScatteringResource); + setupJob.run(sceneContext, renderContext, deferredTransform, lightingModel, diffusedCurvature2, subsurfaceScatteringResource); - lightsJob.run(sceneContext, renderContext, deferredTransform, _enablePointLights, _enableSpotLights); + lightsJob.run(sceneContext, renderContext, deferredTransform, lightingModel->isPointLightEnabled(), lightingModel->isSpotLightEnabled()); cleanupJob.run(sceneContext, renderContext); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 7538cba97c..9fb28919d6 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -24,6 +24,7 @@ #include #include "DeferredFrameTransform.h" +#include "LightingModel.h" #include "LightStage.h" @@ -115,7 +116,9 @@ class RenderDeferredSetup { public: // using JobModel = render::Job::ModelI; - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, + const DeferredFrameTransformPointer& frameTransform, + const LightingModelPointer& lightingModel, const gpu::TexturePointer& diffusedCurvature2, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource); }; @@ -138,15 +141,9 @@ public: class RenderDeferredConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(bool enablePointLights MEMBER enablePointLights NOTIFY dirty) - Q_PROPERTY(bool enableSpotLights MEMBER enableSpotLights NOTIFY dirty) - public: RenderDeferredConfig() : render::Job::Config(true) {} - bool enablePointLights{ true }; - bool enableSpotLights{ true }; - signals: void dirty(); }; @@ -154,7 +151,7 @@ signals: class RenderDeferred { public: - using Inputs = render::VaryingSet4 < DeferredFrameTransformPointer, gpu::FramebufferPointer, gpu::FramebufferPointer, SubsurfaceScatteringResourcePointer>; + using Inputs = render::VaryingSet5 < DeferredFrameTransformPointer, LightingModelPointer, gpu::FramebufferPointer, gpu::FramebufferPointer, SubsurfaceScatteringResourcePointer>; using Config = RenderDeferredConfig; using JobModel = render::Job::ModelI; @@ -169,9 +166,6 @@ public: RenderDeferredCleanup cleanupJob; protected: - - bool _enablePointLights{ true }; - bool _enableSpotLights{ true }; }; #endif // hifi_DeferredLightingEffect_h diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp new file mode 100644 index 0000000000..af546f54dd --- /dev/null +++ b/libraries/render-utils/src/LightingModel.cpp @@ -0,0 +1,134 @@ +// +// LightingModel.cpp +// libraries/render-utils/src/ +// +// Created by Sam Gateau 7/1/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "LightingModel.h" + +LightingModel::LightingModel() { + Parameters parameters; + _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); +} + +void LightingModel::setUnlit(bool enable) { + if (enable != isUnlitEnabled()) { + _parametersBuffer.edit().enableUnlit = (float) enable; + } +} +bool LightingModel::isUnlitEnabled() const { + return (bool)_parametersBuffer.get().enableUnlit; +} + +void LightingModel::setShaded(bool enable) { + if (enable != isShadedEnabled()) { + _parametersBuffer.edit().enableShaded = (float)enable; + } +} +bool LightingModel::isShadedEnabled() const { + return (bool)_parametersBuffer.get().enableShaded; +} + +void LightingModel::setEmissive(bool enable) { + if (enable != isEmissiveEnabled()) { + _parametersBuffer.edit().enableEmissive = (float)enable; + } +} +bool LightingModel::isEmissiveEnabled() const { + return (bool)_parametersBuffer.get().enableEmissive; +} +void LightingModel::setLightmap(bool enable) { + if (enable != isLightmapEnabled()) { + _parametersBuffer.edit().enableLightmap = (float)enable; + } +} +bool LightingModel::isLightmapEnabled() const { + return (bool)_parametersBuffer.get().enableLightmap; +} + +void LightingModel::setScattering(bool enable) { + if (enable != isScatteringEnabled()) { + _parametersBuffer.edit().enableScattering = (float)enable; + } +} +bool LightingModel::isScatteringEnabled() const { + return (bool)_parametersBuffer.get().enableScattering; +} + +void LightingModel::setDiffuse(bool enable) { + if (enable != isDiffuseEnabled()) { + _parametersBuffer.edit().enableDiffuse = (float)enable; + } +} +bool LightingModel::isDiffuseEnabled() const { + return (bool)_parametersBuffer.get().enableDiffuse; +} +void LightingModel::setSpecular(bool enable) { + if (enable != isSpecularEnabled()) { + _parametersBuffer.edit().enableSpecular = (float)enable; + } +} +bool LightingModel::isSpecularEnabled() const { + return (bool)_parametersBuffer.get().enableSpecular; +} + +void LightingModel::setAmbientLight(bool enable) { + if (enable != isAmbientLightEnabled()) { + _parametersBuffer.edit().enableAmbientLight = (float)enable; + } +} +bool LightingModel::isAmbientLightEnabled() const { + return (bool)_parametersBuffer.get().enableAmbientLight; +} +void LightingModel::setDirectionalLight(bool enable) { + if (enable != isDirectionalLightEnabled()) { + _parametersBuffer.edit().enableDirectionalLight = (float)enable; + } +} +bool LightingModel::isDirectionalLightEnabled() const { + return (bool)_parametersBuffer.get().enableDirectionalLight; +} +void LightingModel::setPointLight(bool enable) { + if (enable != isPointLightEnabled()) { + _parametersBuffer.edit().enablePointLight = (float)enable; + } +} +bool LightingModel::isPointLightEnabled() const { + return (bool)_parametersBuffer.get().enablePointLight; +} +void LightingModel::setSpotLight(bool enable) { + if (enable != isSpotLightEnabled()) { + _parametersBuffer.edit().enableSpotLight = (float)enable; + } +} +bool LightingModel::isSpotLightEnabled() const { + return (bool)_parametersBuffer.get().enableSpotLight; +} + + +MakeLightingModel::MakeLightingModel() { + _lightingModel = std::make_shared(); +} + +void MakeLightingModel::configure(const Config& config) { + _lightingModel->setUnlit(config.enableUnlit); + _lightingModel->setShaded(config.enableShaded); + _lightingModel->setEmissive(config.enableEmissive); + _lightingModel->setLightmap(config.enableLightmap); + _lightingModel->setScattering(config.enableScattering); + _lightingModel->setDiffuse(config.enableDiffuse); + _lightingModel->setSpecular(config.enableSpecular); + _lightingModel->setAmbientLight(config.enableAmbientLight); + _lightingModel->setDirectionalLight(config.enableDirectionalLight); + _lightingModel->setPointLight(config.enablePointLight); + _lightingModel->setSpotLight(config.enableSpotLight); +} + +void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { + + lightingModel = _lightingModel; +} \ No newline at end of file diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h new file mode 100644 index 0000000000..384110bc70 --- /dev/null +++ b/libraries/render-utils/src/LightingModel.h @@ -0,0 +1,142 @@ +// +// LightingModel.h +// libraries/render-utils/src/ +// +// Created by Sam Gateau 7/1/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_LightingModel_h +#define hifi_LightingModel_h + +#include "gpu/Resource.h" +#include "render/DrawTask.h" + +class RenderArgs; + +// LightingModel is a helper class gathering in one place the flags to enable the lighting contributions +class LightingModel { +public: + using UniformBufferView = gpu::BufferView; + + LightingModel(); + + + void setUnlit(bool enable); + bool isUnlitEnabled() const; + void setShaded(bool enable); + bool isShadedEnabled() const; + + void setEmissive(bool enable); + bool isEmissiveEnabled() const; + void setLightmap(bool enable); + bool isLightmapEnabled() const; + + void setScattering(bool enable); + bool isScatteringEnabled() const; + + void setDiffuse(bool enable); + bool isDiffuseEnabled() const; + void setSpecular(bool enable); + bool isSpecularEnabled() const; + + void setAmbientLight(bool enable); + bool isAmbientLightEnabled() const; + void setDirectionalLight(bool enable); + bool isDirectionalLightEnabled() const; + void setPointLight(bool enable); + bool isPointLightEnabled() const; + void setSpotLight(bool enable); + bool isSpotLightEnabled() const; + + + UniformBufferView getParametersBuffer() const { return _parametersBuffer; } + +protected: + + + // Class describing the uniform buffer with the transform info common to the AO shaders + // It s changing every frame + class Parameters { + public: + float enableUnlit{ 1.0f }; + float enableShaded{ 1.0f }; + float enableEmissive{ 1.0f }; + float enableLightmap{ 1.0f }; + + float enableScattering{ 1.0f }; + float enableDiffuse{ 1.0f }; + float enableSpecular{ 1.0f }; + float spare; + + float enableAmbientLight{ 1.0f }; + float enableDirectionalLight{ 1.0f }; + float enablePointLight{ 1.0f }; + float enableSpotLight{ 1.0f }; + + Parameters() {} + }; + UniformBufferView _parametersBuffer; +}; + +using LightingModelPointer = std::shared_ptr; + + + + +class MakeLightingModelConfig : public render::Job::Config { + Q_OBJECT + + Q_PROPERTY(bool enableUnlit MEMBER enableUnlit NOTIFY dirty) + Q_PROPERTY(bool enableShaded MEMBER enableShaded NOTIFY dirty) + Q_PROPERTY(bool enableEmissive MEMBER enableEmissive NOTIFY dirty) + Q_PROPERTY(bool enableLightmap MEMBER enableLightmap NOTIFY dirty) + + Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) + Q_PROPERTY(bool enableDiffuse MEMBER enableDiffuse NOTIFY dirty) + Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty) + + Q_PROPERTY(bool enableAmbientLight MEMBER enableAmbientLight NOTIFY dirty) + Q_PROPERTY(bool enableDirectionalLight MEMBER enableDirectionalLight NOTIFY dirty) + Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) + Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty) + +public: + MakeLightingModelConfig() : render::Job::Config() {} // Make Lighting Model is always on + + bool enableUnlit{ true }; + bool enableShaded{ true }; + bool enableEmissive{ true }; + bool enableLightmap{ true }; + + bool enableScattering{ true }; + bool enableDiffuse{ true }; + bool enableSpecular{ true }; + + bool enableAmbientLight{ true }; + bool enableDirectionalLight{ true }; + bool enablePointLight{ true }; + bool enableSpotLight{ true }; + +signals: + void dirty(); +}; + +class MakeLightingModel { +public: + using Config = MakeLightingModelConfig; + using JobModel = render::Job::ModelO; + + MakeLightingModel(); + + void configure(const Config& config); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel); + +private: + LightingModelPointer _lightingModel; +}; + +#endif // hifi_SurfaceGeometryPass_h diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh new file mode 100644 index 0000000000..cd25353f60 --- /dev/null +++ b/libraries/render-utils/src/LightingModel.slh @@ -0,0 +1,64 @@ + +<@if not LIGHTING_MODEL_SLH@> +<@def LIGHTING_MODEL_SLH@> + +<@func declareLightingModel()@> + +struct LightingModel { + vec4 _UnlitShadedEmissiveLightmap; + vec4 _ScatteringDiffuseSpecular; + vec4 _AmbientDirectionalPointSpot; +}; + +uniform lightingModelBuffer { + LightingModel lightingModel; +}; + +float isUnlitEnabled() { + return lightingModel._UnlitShadedEmissiveLightmap.x; +} +float isShadedEnabled() { + return lightingModel._UnlitShadedEmissiveLightmap.y; +} +float isEmissiveEnabled() { + return lightingModel._UnlitShadedEmissiveLightmap.z; +} +float isLightmapEnabled() { + return lightingModel._UnlitShadedEmissiveLightmap.w; +} + +float isScatteringEnabled() { + return lightingModel._ScatteringDiffuseSpecular.x; +} +float isDiffuseEnabled() { + return lightingModel._ScatteringDiffuseSpecular.y; +} +float isSpecularEnabled() { + return lightingModel._ScatteringDiffuseSpecular.z; +} + +float isAmbientEnabled() { + return lightingModel._AmbientDirectionalPointSpot.x; +} +float isDirectionalEnabled() { + return lightingModel._AmbientDirectionalPointSpot.y; +} +float isPointEnabled() { + return lightingModel._AmbientDirectionalPointSpot.z; +} +float isSpotEnabled() { + return lightingModel._AmbientDirectionalPointSpot.w; +} + +<@endfunc@> + +<@endif@> diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index ddebd16c3a..7313d87d62 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -91,7 +91,8 @@ float fetchOcclusionMap(vec2 uv) { <@if withScattering@> uniform sampler2D scatteringMap; float fetchScatteringMap(vec2 uv) { - //return step(0.5, texture(scatteringMap, uv).r); // boolean scattering for now + float scattering = texture(scatteringMap, uv).r; // boolean scattering for now + return max(((scattering - 0.1) / 0.9), 0.0); return texture(scatteringMap, uv).r; // boolean scattering for now } <@endif@> diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d359de0942..006279909e 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -25,6 +25,7 @@ #include #include +#include "LightingModel.h" #include "DebugDeferredBuffer.h" #include "DeferredLightingEffect.h" #include "SurfaceGeometryPass.h" @@ -88,7 +89,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET]; // Prepare deferred, generate the shared Deferred Frame Transform - const auto deferredFrameTransform = addJob("EvalDeferredFrameTransform"); + const auto deferredFrameTransform = addJob("DeferredFrameTransform"); + const auto lightingModel = addJob("LightingModel"); + // GPU jobs: Start preparing the deferred and lighting buffer addJob("PrepareDeferred"); @@ -125,7 +128,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, curvatureFramebuffer, diffusedCurvatureFramebuffer, scatteringResource)); + const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, lightingModel, curvatureFramebuffer, diffusedCurvatureFramebuffer, scatteringResource)); // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredLightingInputs); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 3535732f2e..8ec80b11c3 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -510,9 +510,10 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont auto& frameTransform = inputs.get0(); - auto& curvatureFramebuffer = inputs.get1(); - auto& diffusedFramebuffer = inputs.get2(); - auto& scatteringResource = inputs.get3(); + auto& lightingModel = inputs.get1(); + auto& curvatureFramebuffer = inputs.get2(); + auto& diffusedFramebuffer = inputs.get3(); + auto& scatteringResource = inputs.get4(); if (!scatteringResource) { return; diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 1442feeb1f..905e30d74e 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -16,6 +16,7 @@ #include "render/DrawTask.h" #include "DeferredFrameTransform.h" +#include "LightingModel.h" class SubsurfaceScatteringResource { public: @@ -160,7 +161,7 @@ signals: class DebugSubsurfaceScattering { public: - using Inputs = render::VaryingSet4; + using Inputs = render::VaryingSet5; using Config = DebugSubsurfaceScatteringConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index b7ea70a370..409ff7b77f 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -148,6 +148,32 @@ public: T3& edit3() { return std::get<3>((*this)).template edit(); } }; + +template +class VaryingSet5 : public std::tuple{ +public: + using Parent = std::tuple; + + VaryingSet5() : Parent(Varying(T0()), Varying(T1()), Varying(T2()), Varying(T3()), Varying(T4())) {} + VaryingSet5(const VaryingSet5& src) : Parent(std::get<0>(src), std::get<1>(src), std::get<2>(src), std::get<3>(src), std::get<4>(src)) {} + VaryingSet5(const Varying& first, const Varying& second, const Varying& third, const Varying& fourth, const Varying& fifth) : Parent(first, second, third, fourth, fifth) {} + + const T0& get0() const { return std::get<0>((*this)).template get(); } + T0& edit0() { return std::get<0>((*this)).template edit(); } + + const T1& get1() const { return std::get<1>((*this)).template get(); } + T1& edit1() { return std::get<1>((*this)).template edit(); } + + const T2& get2() const { return std::get<2>((*this)).template get(); } + T2& edit2() { return std::get<2>((*this)).template edit(); } + + const T3& get3() const { return std::get<3>((*this)).template get(); } + T3& edit3() { return std::get<3>((*this)).template edit(); } + + const T4& get4() const { return std::get<4>((*this)).template get(); } + T4& edit4() { return std::get<4>((*this)).template edit(); } +}; + template < class T, int NUM > class VaryingArray : public std::array { public: diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 02dd01e4c5..8c3ff66c17 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -16,16 +16,25 @@ Column { Column { id: deferredLighting spacing: 10 - - CheckBox { - text: "Point Lights" - checked: true - onCheckedChanged: { Render.getConfig("RenderDeferred").enablePointLights = checked } - } - CheckBox { - text: "Spot Lights" - checked: true - onCheckedChanged: { Render.getConfig("RenderDeferred").enableSpotLights = checked } + Repeater { + model: [ + "Unlit:LightingModel:enableUnlit", + "Shaded:LightingModel:enableShaded", + "Emissive:LightingModel:enableEmissive", + "Lightmap:LightingModel:enableLightmap", + "Scattering:LightingModel:enableScattering", + "Diffuse:LightingModel:enableDiffuse", + "Specular:LightingModel:enableSpecular", + "Ambient:LightingModel:enableAmbientLight", + "Directional:LightingModel:enableDirectionalLight", + "Point:LightingModel:enablePointLight", + "Spot:LightingModel:enableSpotLight" + ] + CheckBox { + text: modelData.split(":")[0] + checked: Render.getConfig(modelData.split(":")[1]) + onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } } } } From d3c006b5daec2769120f3fed9e1b0696291dd69a Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 1 Jul 2016 18:50:53 -0700 Subject: [PATCH 0887/1237] Added floating-point audio resampler --- libraries/audio/src/AudioSRC.cpp | 126 +++++++++++++++++++++++++++++-- libraries/audio/src/AudioSRC.h | 4 + 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index 98de36e655..688e36c777 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -539,6 +539,58 @@ void AudioSRC::convertOutputToInt16(float** inputs, int16_t* output, int numFram } } +// deinterleave stereo +void AudioSRC::convertInputFromFloat(const float* input, float** outputs, int numFrames) { + + if (_numChannels == 1) { + + memcpy(outputs[0], input, numFrames * sizeof(float)); + + } else if (_numChannels == 2) { + + int i = 0; + for (; i < numFrames - 3; i += 4) { + __m128 f0 = _mm_loadu_ps(&input[2*i + 0]); + __m128 f1 = _mm_loadu_ps(&input[2*i + 4]); + + // deinterleave + _mm_storeu_ps(&outputs[0][i], _mm_shuffle_ps(f0, f1, _MM_SHUFFLE(2,0,2,0))); + _mm_storeu_ps(&outputs[1][i], _mm_shuffle_ps(f0, f1, _MM_SHUFFLE(3,1,3,1))); + } + for (; i < numFrames; i++) { + // deinterleave + outputs[0][i] = input[2*i + 0]; + outputs[1][i] = input[2*i + 1]; + } + } +} + +// interleave stereo +void AudioSRC::convertOutputToFloat(float** inputs, float* output, int numFrames) { + + if (_numChannels == 1) { + + memcpy(output, inputs[0], numFrames * sizeof(float)); + + } else if (_numChannels == 2) { + + int i = 0; + for (; i < numFrames - 3; i += 4) { + __m128 f0 = _mm_loadu_ps(&inputs[0][i]); + __m128 f1 = _mm_loadu_ps(&inputs[1][i]); + + // interleave + _mm_storeu_ps(&output[2*i + 0], _mm_unpacklo_ps(f0, f1)); + _mm_storeu_ps(&output[2*i + 4], _mm_unpackhi_ps(f0, f1)); + } + for (; i < numFrames; i++) { + // interleave + output[2*i + 0] = inputs[0][i]; + output[2*i + 1] = inputs[1][i]; + } + } +} + #else int AudioSRC::multirateFilter1(const float* input0, float* output0, int inputFrames) { @@ -738,6 +790,38 @@ void AudioSRC::convertOutputToInt16(float** inputs, int16_t* output, int numFram } } +// deinterleave stereo +void AudioSRC::convertInputFromFloat(const float* input, float** outputs, int numFrames) { + + if (_numChannels == 1) { + + memcpy(outputs[0], input, numFrames * sizeof(float)); + + } else if (_numChannels == 2) { + for (int i = 0; i < numFrames; i++) { + // deinterleave + outputs[0][i] = input[2*i + 0]; + outputs[1][i] = input[2*i + 1]; + } + } +} + +// interleave stereo +void AudioSRC::convertOutputToFloat(float** inputs, float* output, int numFrames) { + + if (_numChannels == 1) { + + memcpy(output, inputs[0], numFrames * sizeof(float)); + + } else if (_numChannels == 2) { + for (int i = 0; i < numFrames; i++) { + // interleave + output[2*i + 0] = inputs[0][i]; + output[2*i + 1] = inputs[1][i]; + } + } +} + #endif int AudioSRC::processFloat(float** inputs, float** outputs, int inputFrames) { @@ -749,19 +833,19 @@ int AudioSRC::processFloat(float** inputs, float** outputs, int inputFrames) { if (_numChannels == 1) { // refill history buffers - memcpy(_history[0] + _numHistory, _inputs[0], nh * sizeof(float)); + memcpy(_history[0] + _numHistory, inputs[0], nh * sizeof(float)); // process history buffer - outputFrames += multirateFilter1(_history[0], _outputs[0], nh); + outputFrames += multirateFilter1(_history[0], outputs[0], nh); // process remaining input if (ni) { - outputFrames += multirateFilter1(_inputs[0], _outputs[0] + outputFrames, ni); + outputFrames += multirateFilter1(inputs[0], outputs[0] + outputFrames, ni); } // shift history buffers if (ni) { - memcpy(_history[0], _inputs[0] + ni, _numHistory * sizeof(float)); + memcpy(_history[0], inputs[0] + ni, _numHistory * sizeof(float)); } else { memmove(_history[0], _history[0] + nh, _numHistory * sizeof(float)); } @@ -769,15 +853,15 @@ int AudioSRC::processFloat(float** inputs, float** outputs, int inputFrames) { } else if (_numChannels == 2) { // refill history buffers - memcpy(_history[0] + _numHistory, _inputs[0], nh * sizeof(float)); - memcpy(_history[1] + _numHistory, _inputs[1], nh * sizeof(float)); + memcpy(_history[0] + _numHistory, inputs[0], nh * sizeof(float)); + memcpy(_history[1] + _numHistory, inputs[1], nh * sizeof(float)); // process history buffer - outputFrames += multirateFilter2(_history[0], _history[1], _outputs[0], _outputs[1], nh); + outputFrames += multirateFilter2(_history[0], _history[1], outputs[0], outputs[1], nh); // process remaining input if (ni) { - outputFrames += multirateFilter2(_inputs[0], _inputs[1], _outputs[0] + outputFrames, _outputs[1] + outputFrames, ni); + outputFrames += multirateFilter2(inputs[0], inputs[1], outputs[0] + outputFrames, outputs[1] + outputFrames, ni); } // shift history buffers @@ -885,6 +969,32 @@ int AudioSRC::render(const int16_t* input, int16_t* output, int inputFrames) { return outputFrames; } +// +// This version handles input/output as interleaved float +// +int AudioSRC::render(const float* input, float* output, int inputFrames) { + int outputFrames = 0; + + while (inputFrames) { + + int ni = std::min(inputFrames, _inputBlock); + + convertInputFromFloat(input, _inputs, ni); + + int no = processFloat(_inputs, _outputs, ni); + assert(no <= SRC_BLOCK); + + convertOutputToFloat(_outputs, output, no); + + input += _numChannels * ni; + output += _numChannels * no; + inputFrames -= ni; + outputFrames += no; + } + + return outputFrames; +} + // the min output frames that will be produced by inputFrames int AudioSRC::getMinOutput(int inputFrames) { if (_step == 0) { diff --git a/libraries/audio/src/AudioSRC.h b/libraries/audio/src/AudioSRC.h index a4aefc5ed7..409507f68e 100644 --- a/libraries/audio/src/AudioSRC.h +++ b/libraries/audio/src/AudioSRC.h @@ -35,6 +35,7 @@ public: ~AudioSRC(); int render(const int16_t* input, int16_t* output, int inputFrames); + int render(const float* input, float* output, int inputFrames); int getMinOutput(int inputFrames); int getMaxOutput(int inputFrames); @@ -78,6 +79,9 @@ private: void convertInputFromInt16(const int16_t* input, float** outputs, int numFrames); void convertOutputToInt16(float** inputs, int16_t* output, int numFrames); + void convertInputFromFloat(const float* input, float** outputs, int numFrames); + void convertOutputToFloat(float** inputs, float* output, int numFrames); + int processFloat(float** inputs, float** outputs, int inputFrames); }; From ad8f17a1425b2cf11de348d8daa250dbb4d8111b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 1 Jul 2016 19:29:43 -0700 Subject: [PATCH 0888/1237] Add support for deinterleaved float-point audio (native format) --- libraries/audio/src/AudioSRC.cpp | 6 +++--- libraries/audio/src/AudioSRC.h | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index 688e36c777..185ad948e7 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -824,7 +824,7 @@ void AudioSRC::convertOutputToFloat(float** inputs, float* output, int numFrames #endif -int AudioSRC::processFloat(float** inputs, float** outputs, int inputFrames) { +int AudioSRC::render(float** inputs, float** outputs, int inputFrames) { int outputFrames = 0; int nh = std::min(_numHistory, inputFrames); // number of frames from history buffer @@ -955,7 +955,7 @@ int AudioSRC::render(const int16_t* input, int16_t* output, int inputFrames) { convertInputFromInt16(input, _inputs, ni); - int no = processFloat(_inputs, _outputs, ni); + int no = render(_inputs, _outputs, ni); assert(no <= SRC_BLOCK); convertOutputToInt16(_outputs, output, no); @@ -981,7 +981,7 @@ int AudioSRC::render(const float* input, float* output, int inputFrames) { convertInputFromFloat(input, _inputs, ni); - int no = processFloat(_inputs, _outputs, ni); + int no = render(_inputs, _outputs, ni); assert(no <= SRC_BLOCK); convertOutputToFloat(_outputs, output, no); diff --git a/libraries/audio/src/AudioSRC.h b/libraries/audio/src/AudioSRC.h index 409507f68e..e8c8f4370f 100644 --- a/libraries/audio/src/AudioSRC.h +++ b/libraries/audio/src/AudioSRC.h @@ -34,7 +34,13 @@ public: AudioSRC(int inputSampleRate, int outputSampleRate, int numChannels); ~AudioSRC(); + // deinterleaved float input/output (native format) + int render(float** inputs, float** outputs, int inputFrames); + + // interleaved int16_t input/output int render(const int16_t* input, int16_t* output, int inputFrames); + + // interleaved float input/output int render(const float* input, float* output, int inputFrames); int getMinOutput(int inputFrames); @@ -81,8 +87,6 @@ private: void convertInputFromFloat(const float* input, float** outputs, int numFrames); void convertOutputToFloat(float** inputs, float* output, int numFrames); - - int processFloat(float** inputs, float** outputs, int inputFrames); }; #endif // AudioSRC_h From 941067b9d01d6269da07cc6da87082c3734a0811 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 2 Jul 2016 07:25:04 -0700 Subject: [PATCH 0889/1237] Added floating-point audio reverb --- libraries/audio/src/AudioReverb.cpp | 81 ++++++++++++++++++++++++++++- libraries/audio/src/AudioReverb.h | 7 +++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/libraries/audio/src/AudioReverb.cpp b/libraries/audio/src/AudioReverb.cpp index 5c57e92ce5..5f1da56b4c 100644 --- a/libraries/audio/src/AudioReverb.cpp +++ b/libraries/audio/src/AudioReverb.cpp @@ -1725,7 +1725,7 @@ void ReverbImpl::reset() { // Public API // -static const int REVERB_BLOCK = 1024; +static const int REVERB_BLOCK = 256; AudioReverb::AudioReverb(float sampleRate) { @@ -1898,6 +1898,44 @@ void AudioReverb::convertOutputToInt16(float** inputs, int16_t* output, int numF } } +// deinterleave stereo +void AudioReverb::convertInputFromFloat(const float* input, float** outputs, int numFrames) { + + int i = 0; + for (; i < numFrames - 3; i += 4) { + __m128 f0 = _mm_loadu_ps(&input[2*i + 0]); + __m128 f1 = _mm_loadu_ps(&input[2*i + 4]); + + // deinterleave + _mm_storeu_ps(&outputs[0][i], _mm_shuffle_ps(f0, f1, _MM_SHUFFLE(2,0,2,0))); + _mm_storeu_ps(&outputs[1][i], _mm_shuffle_ps(f0, f1, _MM_SHUFFLE(3,1,3,1))); + } + for (; i < numFrames; i++) { + // deinterleave + outputs[0][i] = input[2*i + 0]; + outputs[1][i] = input[2*i + 1]; + } +} + +// interleave stereo +void AudioReverb::convertOutputToFloat(float** inputs, float* output, int numFrames) { + + int i = 0; + for(; i < numFrames - 3; i += 4) { + __m128 f0 = _mm_loadu_ps(&inputs[0][i]); + __m128 f1 = _mm_loadu_ps(&inputs[1][i]); + + // interleave + _mm_storeu_ps(&output[2*i + 0],_mm_unpacklo_ps(f0,f1)); + _mm_storeu_ps(&output[2*i + 4],_mm_unpackhi_ps(f0,f1)); + } + for(; i < numFrames; i++) { + // interleave + output[2*i + 0] = inputs[0][i]; + output[2*i + 1] = inputs[1][i]; + } +} + #else // convert int16_t to float, deinterleave stereo @@ -1944,6 +1982,26 @@ void AudioReverb::convertOutputToInt16(float** inputs, int16_t* output, int numF } } +// deinterleave stereo +void AudioReverb::convertInputFromFloat(const float* input, float** outputs, int numFrames) { + + for (int i = 0; i < numFrames; i++) { + // deinterleave + outputs[0][i] = input[2*i + 0]; + outputs[1][i] = input[2*i + 1]; + } +} + +// interleave stereo +void AudioReverb::convertOutputToFloat(float** inputs, float* output, int numFrames) { + + for (int i = 0; i < numFrames; i++) { + // interleave + output[2*i + 0] = inputs[0][i]; + output[2*i + 1] = inputs[1][i]; + } +} + #endif // @@ -1966,3 +2024,24 @@ void AudioReverb::render(const int16_t* input, int16_t* output, int numFrames) { numFrames -= n; } } + +// +// This version handles input/output as interleaved float +// +void AudioReverb::render(const float* input, float* output, int numFrames) { + + while (numFrames) { + + int n = MIN(numFrames, REVERB_BLOCK); + + convertInputFromFloat(input, _inout, n); + + _impl->process(_inout, _inout, n); + + convertOutputToFloat(_inout, output, n); + + input += 2 * n; + output += 2 * n; + numFrames -= n; + } +} diff --git a/libraries/audio/src/AudioReverb.h b/libraries/audio/src/AudioReverb.h index 639d62d8ec..ee163698ea 100644 --- a/libraries/audio/src/AudioReverb.h +++ b/libraries/audio/src/AudioReverb.h @@ -64,13 +64,20 @@ public: // interleaved int16_t input/output void render(const int16_t* input, int16_t* output, int numFrames); + // interleaved float input/output + void render(const float* input, float* output, int numFrames); + private: ReverbImpl *_impl; ReverbParameters _params; float* _inout[2]; + void convertInputFromInt16(const int16_t* input, float** outputs, int numFrames); void convertOutputToInt16(float** inputs, int16_t* output, int numFrames); + + void convertInputFromFloat(const float* input, float** outputs, int numFrames); + void convertOutputToFloat(float** inputs, float* output, int numFrames); }; #endif // hifi_AudioReverb_h From b7874116b9377bf6029ac6842c99b15f3221e595 Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 2 Jul 2016 23:36:08 -0400 Subject: [PATCH 0890/1237] fix TypedArray byte ordering and .subarray end indexing --- .../script-engine/src/TypedArrayPrototype.cpp | 4 +- libraries/script-engine/src/TypedArrays.cpp | 12 +++- .../diagnostics/typedArraysUnitTest.js | 58 ++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/libraries/script-engine/src/TypedArrayPrototype.cpp b/libraries/script-engine/src/TypedArrayPrototype.cpp index bb612b393f..4de948e806 100644 --- a/libraries/script-engine/src/TypedArrayPrototype.cpp +++ b/libraries/script-engine/src/TypedArrayPrototype.cpp @@ -71,8 +71,10 @@ QScriptValue TypedArrayPrototype::subarray(qint32 begin, qint32 end) { end = (end < 0) ? length + end : end; // here we clamp the indices to fit the array + // note: begin offset is *inclusive* while end offset is *exclusive* + // (see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray#Parameters) begin = glm::clamp(begin, 0, (length - 1)); - end = glm::clamp(end, 0, (length - 1)); + end = glm::clamp(end, 0, length); byteOffset += begin * bytesPerElement; length = (end - begin > 0) ? end - begin : 0; diff --git a/libraries/script-engine/src/TypedArrays.cpp b/libraries/script-engine/src/TypedArrays.cpp index c1c4117f76..4d5181ff33 100644 --- a/libraries/script-engine/src/TypedArrays.cpp +++ b/libraries/script-engine/src/TypedArrays.cpp @@ -88,7 +88,11 @@ QScriptValue TypedArray::construct(QScriptContext* context, QScriptEngine* engin if (arrayBuffer) { if (context->argumentCount() == 1) { // Case for entire ArrayBuffer - newObject = cls->newInstance(bufferArg, 0, arrayBuffer->size()); + if (arrayBuffer->size() % cls->_bytesPerElement != 0) { + engine->evaluate("throw \"RangeError: byteLength is not a multiple of BYTES_PER_ELEMENT\""); + } + quint32 length = arrayBuffer->size() / cls->_bytesPerElement; + newObject = cls->newInstance(bufferArg, 0, length); } else { QScriptValue byteOffsetArg = context->argument(1); if (!byteOffsetArg.isNumber()) { @@ -206,6 +210,7 @@ QScriptValue propertyHelper(const QByteArray* arrayBuffer, const QScriptString& QDataStream stream(*arrayBuffer); stream.skipRawData(id); + stream.setByteOrder(QDataStream::LittleEndian); T result; stream >> result; return result; @@ -218,6 +223,7 @@ void setPropertyHelper(QByteArray* arrayBuffer, const QScriptString& name, uint if (arrayBuffer && value.isNumber()) { QDataStream stream(arrayBuffer, QIODevice::ReadWrite); stream.skipRawData(id); + stream.setByteOrder(QDataStream::LittleEndian); stream << (T)value.toNumber(); } @@ -357,6 +363,7 @@ QScriptValue Float32ArrayClass::property(const QScriptValue& object, const QScri if (ok && arrayBuffer) { QDataStream stream(*arrayBuffer); stream.skipRawData(id); + stream.setByteOrder(QDataStream::LittleEndian); stream.setFloatingPointPrecision(QDataStream::SinglePrecision); float result; @@ -375,6 +382,7 @@ void Float32ArrayClass::setProperty(QScriptValue& object, const QScriptString& n if (ba && value.isNumber()) { QDataStream stream(ba, QIODevice::ReadWrite); stream.skipRawData(id); + stream.setByteOrder(QDataStream::LittleEndian); stream.setFloatingPointPrecision(QDataStream::SinglePrecision); stream << (float)value.toNumber(); @@ -392,6 +400,7 @@ QScriptValue Float64ArrayClass::property(const QScriptValue& object, const QScri if (ok && arrayBuffer) { QDataStream stream(*arrayBuffer); stream.skipRawData(id); + stream.setByteOrder(QDataStream::LittleEndian); stream.setFloatingPointPrecision(QDataStream::DoublePrecision); double result; @@ -410,6 +419,7 @@ void Float64ArrayClass::setProperty(QScriptValue& object, const QScriptString& n if (ba && value.isNumber()) { QDataStream stream(ba, QIODevice::ReadWrite); stream.skipRawData(id); + stream.setByteOrder(QDataStream::LittleEndian); stream.setFloatingPointPrecision(QDataStream::DoublePrecision); stream << (double)value.toNumber(); diff --git a/scripts/developer/utilities/diagnostics/typedArraysUnitTest.js b/scripts/developer/utilities/diagnostics/typedArraysUnitTest.js index a50a40e43e..c61865c8d6 100644 --- a/scripts/developer/utilities/diagnostics/typedArraysUnitTest.js +++ b/scripts/developer/utilities/diagnostics/typedArraysUnitTest.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include("../../libraries/unitTest.js"); +Script.include("../../../../script-archive/libraries/unitTest.js"); // e.g. extractbits([0xff, 0x80, 0x00, 0x00], 23, 30); inclusive function extractbits(bytes, lo, hi) { @@ -551,6 +551,20 @@ test('TypedArray.subarray', function () { this.arrayEqual(a.subarray(-1, -4), []); this.arrayEqual(a.subarray(1).subarray(1), [3, 4, 5]); this.arrayEqual(a.subarray(1, 4).subarray(1, 2), [3]); + + var a = new Float32Array(16); + a[0] = -1; + a[15] = 1/8; + this.assertEquals(a.length, 16); + this.assertEquals(a.byteLength, a.length * a.BYTES_PER_ELEMENT); + + this.assertEquals(a.subarray(-12).length, 12, '[-12,)'); + this.arrayEqual(a.subarray(-16), a, '[-16,)'); + this.arrayEqual(a.subarray(12, 16), [0,0,0,1/8], '[12,16)'); + this.arrayEqual(a.subarray(0, 4), [-1,0,0,0],'[0,4)'); + this.arrayEqual(a.subarray(-16, -12), [-1,0,0,0],'[-16,-12)'); + this.assertEquals(a.subarray(0, -12).length, 4,'[0,-12)'); + this.arrayEqual(a.subarray(-10), a.subarray(6),'[-10,)'); }); @@ -706,3 +720,45 @@ test('Regression Tests', function() { this.assertEquals(truncated, -minFloat32, 'smallest 32 bit float should not truncate to zero'); }); +test('new TypedArray(ArrayBuffer).length Tests', function() { + var uint8s = new Uint8Array([0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff]), + buffer = uint8s.buffer; + + this.assertEquals(buffer.byteLength, 8, 'buffer.length'); + + var _this = this; + [ + 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array', + 'Float32Array', 'Float64Array', 'Uint8ClampedArray' + ].forEach(function(typeArrayName) { + var typeArray = eval(typeArrayName); + var a = new typeArray(buffer); + _this.assertEquals(a.BYTES_PER_ELEMENT, typeArrayName.match(/\d+/)[0]/8, typeArrayName+'.BYTES_PER_ELEMENT'); + _this.assertEquals(a.byteLength, buffer.byteLength, typeArrayName+'.byteLength'); + _this.assertEquals(a.length, buffer.byteLength / typeArray.BYTES_PER_ELEMENT, typeArrayName+'.length'); + }); +}); + +test('Native endianness check', function() { + var buffer = ArrayBuffer(4); + new Uint8Array(buffer).set([0xaa, 0xbb, 0xcc, 0xdd]); + var endian = { aabbccdd: 'big', ddccbbaa: 'little' }[ + new Uint32Array(buffer)[0].toString(16) + ]; + this.assertEquals(endian, 'little'); +}); + +test('TypeArray byte order tests', function() { + var uint8s = new Uint8Array([0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff]), + buffer = uint8s.buffer; + + this.arrayEqual(new Uint8Array(buffer), [0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff], "Uint8Array"); + this.arrayEqual(new Uint16Array(buffer), [0x00ff, 0x0000, 0x0000, 0xff00], "Uint16Array"); + this.arrayEqual(new Uint32Array(buffer), [0x000000ff, 0xff000000], "Uint32Array"); + + this.arrayEqual(new Int8Array(buffer), [-1,0,0,0,0,0,0,-1], "Int8Array"); + this.arrayEqual(new Int16Array(buffer), [255, 0, 0, -256], "Int16Array"); + + this.arrayEqual(new Float32Array(buffer), [3.5733110840282835e-43, -1.7014118346046923e+38], "Float32Array"); + this.arrayEqual(new Float64Array(buffer), [-5.486124068793999e+303], "Float64Array"); +}); From f7c9957bb1d391a33c096485a9fb1cc637c6600e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sun, 3 Jul 2016 11:33:05 -0700 Subject: [PATCH 0891/1237] PR feedback: better naming of convertInput() and convertOutput() --- libraries/audio/src/AudioSRC.cpp | 28 ++++++++++++++-------------- libraries/audio/src/AudioSRC.h | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index 185ad948e7..5dba63b349 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -389,7 +389,7 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* } // convert int16_t to float, deinterleave stereo -void AudioSRC::convertInputFromInt16(const int16_t* input, float** outputs, int numFrames) { +void AudioSRC::convertInput(const int16_t* input, float** outputs, int numFrames) { __m128 scale = _mm_set1_ps(1/32768.0f); if (_numChannels == 1) { @@ -467,8 +467,8 @@ static inline __m128 dither4() { return _mm_mul_ps(d0, _mm_set1_ps(1/65536.0f)); } -// convert float to int16_t, interleave stereo -void AudioSRC::convertOutputToInt16(float** inputs, int16_t* output, int numFrames) { +// convert float to int16_t with dither, interleave stereo +void AudioSRC::convertOutput(float** inputs, int16_t* output, int numFrames) { __m128 scale = _mm_set1_ps(32768.0f); if (_numChannels == 1) { @@ -540,7 +540,7 @@ void AudioSRC::convertOutputToInt16(float** inputs, int16_t* output, int numFram } // deinterleave stereo -void AudioSRC::convertInputFromFloat(const float* input, float** outputs, int numFrames) { +void AudioSRC::convertInput(const float* input, float** outputs, int numFrames) { if (_numChannels == 1) { @@ -566,7 +566,7 @@ void AudioSRC::convertInputFromFloat(const float* input, float** outputs, int nu } // interleave stereo -void AudioSRC::convertOutputToFloat(float** inputs, float* output, int numFrames) { +void AudioSRC::convertOutput(float** inputs, float* output, int numFrames) { if (_numChannels == 1) { @@ -726,7 +726,7 @@ int AudioSRC::multirateFilter2(const float* input0, const float* input1, float* } // convert int16_t to float, deinterleave stereo -void AudioSRC::convertInputFromInt16(const int16_t* input, float** outputs, int numFrames) { +void AudioSRC::convertInput(const int16_t* input, float** outputs, int numFrames) { const float scale = 1/32768.0f; if (_numChannels == 1) { @@ -750,8 +750,8 @@ static inline float dither() { return (int32_t)(r0 - r1) * (1/65536.0f); } -// convert float to int16_t, interleave stereo -void AudioSRC::convertOutputToInt16(float** inputs, int16_t* output, int numFrames) { +// convert float to int16_t with dither, interleave stereo +void AudioSRC::convertOutput(float** inputs, int16_t* output, int numFrames) { const float scale = 32768.0f; if (_numChannels == 1) { @@ -791,7 +791,7 @@ void AudioSRC::convertOutputToInt16(float** inputs, int16_t* output, int numFram } // deinterleave stereo -void AudioSRC::convertInputFromFloat(const float* input, float** outputs, int numFrames) { +void AudioSRC::convertInput(const float* input, float** outputs, int numFrames) { if (_numChannels == 1) { @@ -807,7 +807,7 @@ void AudioSRC::convertInputFromFloat(const float* input, float** outputs, int nu } // interleave stereo -void AudioSRC::convertOutputToFloat(float** inputs, float* output, int numFrames) { +void AudioSRC::convertOutput(float** inputs, float* output, int numFrames) { if (_numChannels == 1) { @@ -953,12 +953,12 @@ int AudioSRC::render(const int16_t* input, int16_t* output, int inputFrames) { int ni = std::min(inputFrames, _inputBlock); - convertInputFromInt16(input, _inputs, ni); + convertInput(input, _inputs, ni); int no = render(_inputs, _outputs, ni); assert(no <= SRC_BLOCK); - convertOutputToInt16(_outputs, output, no); + convertOutput(_outputs, output, no); input += _numChannels * ni; output += _numChannels * no; @@ -979,12 +979,12 @@ int AudioSRC::render(const float* input, float* output, int inputFrames) { int ni = std::min(inputFrames, _inputBlock); - convertInputFromFloat(input, _inputs, ni); + convertInput(input, _inputs, ni); int no = render(_inputs, _outputs, ni); assert(no <= SRC_BLOCK); - convertOutputToFloat(_outputs, output, no); + convertOutput(_outputs, output, no); input += _numChannels * ni; output += _numChannels * no; diff --git a/libraries/audio/src/AudioSRC.h b/libraries/audio/src/AudioSRC.h index e8c8f4370f..f7becc22c3 100644 --- a/libraries/audio/src/AudioSRC.h +++ b/libraries/audio/src/AudioSRC.h @@ -82,11 +82,11 @@ private: int multirateFilter1_AVX2(const float* input0, float* output0, int inputFrames); int multirateFilter2_AVX2(const float* input0, const float* input1, float* output0, float* output1, int inputFrames); - void convertInputFromInt16(const int16_t* input, float** outputs, int numFrames); - void convertOutputToInt16(float** inputs, int16_t* output, int numFrames); + void convertInput(const int16_t* input, float** outputs, int numFrames); + void convertOutput(float** inputs, int16_t* output, int numFrames); - void convertInputFromFloat(const float* input, float** outputs, int numFrames); - void convertOutputToFloat(float** inputs, float* output, int numFrames); + void convertInput(const float* input, float** outputs, int numFrames); + void convertOutput(float** inputs, float* output, int numFrames); }; #endif // AudioSRC_h From 014f6fa259f5b12f9492fd534c39fa28a3617f18 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sun, 3 Jul 2016 11:40:32 -0700 Subject: [PATCH 0892/1237] PR feedback: better naming of convertInput() and convertOutput() --- libraries/audio/src/AudioReverb.cpp | 28 ++++++++++++++-------------- libraries/audio/src/AudioReverb.h | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/libraries/audio/src/AudioReverb.cpp b/libraries/audio/src/AudioReverb.cpp index 5f1da56b4c..652f7ed58d 100644 --- a/libraries/audio/src/AudioReverb.cpp +++ b/libraries/audio/src/AudioReverb.cpp @@ -1804,7 +1804,7 @@ void AudioReverb::render(float** inputs, float** outputs, int numFrames) { #include // convert int16_t to float, deinterleave stereo -void AudioReverb::convertInputFromInt16(const int16_t* input, float** outputs, int numFrames) { +void AudioReverb::convertInput(const int16_t* input, float** outputs, int numFrames) { __m128 scale = _mm_set1_ps(1/32768.0f); int i = 0; @@ -1855,8 +1855,8 @@ static inline __m128 dither4() { return _mm_mul_ps(d0, _mm_set1_ps(1/65536.0f)); } -// convert float to int16_t, interleave stereo -void AudioReverb::convertOutputToInt16(float** inputs, int16_t* output, int numFrames) { +// convert float to int16_t with dither, interleave stereo +void AudioReverb::convertOutput(float** inputs, int16_t* output, int numFrames) { __m128 scale = _mm_set1_ps(32768.0f); int i = 0; @@ -1899,7 +1899,7 @@ void AudioReverb::convertOutputToInt16(float** inputs, int16_t* output, int numF } // deinterleave stereo -void AudioReverb::convertInputFromFloat(const float* input, float** outputs, int numFrames) { +void AudioReverb::convertInput(const float* input, float** outputs, int numFrames) { int i = 0; for (; i < numFrames - 3; i += 4) { @@ -1918,7 +1918,7 @@ void AudioReverb::convertInputFromFloat(const float* input, float** outputs, int } // interleave stereo -void AudioReverb::convertOutputToFloat(float** inputs, float* output, int numFrames) { +void AudioReverb::convertOutput(float** inputs, float* output, int numFrames) { int i = 0; for(; i < numFrames - 3; i += 4) { @@ -1939,7 +1939,7 @@ void AudioReverb::convertOutputToFloat(float** inputs, float* output, int numFra #else // convert int16_t to float, deinterleave stereo -void AudioReverb::convertInputFromInt16(const int16_t* input, float** outputs, int numFrames) { +void AudioReverb::convertInput(const int16_t* input, float** outputs, int numFrames) { const float scale = 1/32768.0f; for (int i = 0; i < numFrames; i++) { @@ -1957,8 +1957,8 @@ static inline float dither() { return (int32_t)(r0 - r1) * (1/65536.0f); } -// convert float to int16_t, interleave stereo -void AudioReverb::convertOutputToInt16(float** inputs, int16_t* output, int numFrames) { +// convert float to int16_t with dither, interleave stereo +void AudioReverb::convertOutput(float** inputs, int16_t* output, int numFrames) { const float scale = 32768.0f; for (int i = 0; i < numFrames; i++) { @@ -1983,7 +1983,7 @@ void AudioReverb::convertOutputToInt16(float** inputs, int16_t* output, int numF } // deinterleave stereo -void AudioReverb::convertInputFromFloat(const float* input, float** outputs, int numFrames) { +void AudioReverb::convertInput(const float* input, float** outputs, int numFrames) { for (int i = 0; i < numFrames; i++) { // deinterleave @@ -1993,7 +1993,7 @@ void AudioReverb::convertInputFromFloat(const float* input, float** outputs, int } // interleave stereo -void AudioReverb::convertOutputToFloat(float** inputs, float* output, int numFrames) { +void AudioReverb::convertOutput(float** inputs, float* output, int numFrames) { for (int i = 0; i < numFrames; i++) { // interleave @@ -2013,11 +2013,11 @@ void AudioReverb::render(const int16_t* input, int16_t* output, int numFrames) { int n = MIN(numFrames, REVERB_BLOCK); - convertInputFromInt16(input, _inout, n); + convertInput(input, _inout, n); _impl->process(_inout, _inout, n); - convertOutputToInt16(_inout, output, n); + convertOutput(_inout, output, n); input += 2 * n; output += 2 * n; @@ -2034,11 +2034,11 @@ void AudioReverb::render(const float* input, float* output, int numFrames) { int n = MIN(numFrames, REVERB_BLOCK); - convertInputFromFloat(input, _inout, n); + convertInput(input, _inout, n); _impl->process(_inout, _inout, n); - convertOutputToFloat(_inout, output, n); + convertOutput(_inout, output, n); input += 2 * n; output += 2 * n; diff --git a/libraries/audio/src/AudioReverb.h b/libraries/audio/src/AudioReverb.h index ee163698ea..adb8890f6a 100644 --- a/libraries/audio/src/AudioReverb.h +++ b/libraries/audio/src/AudioReverb.h @@ -73,11 +73,11 @@ private: float* _inout[2]; - void convertInputFromInt16(const int16_t* input, float** outputs, int numFrames); - void convertOutputToInt16(float** inputs, int16_t* output, int numFrames); + void convertInput(const int16_t* input, float** outputs, int numFrames); + void convertOutput(float** inputs, int16_t* output, int numFrames); - void convertInputFromFloat(const float* input, float** outputs, int numFrames); - void convertOutputToFloat(float** inputs, float* output, int numFrames); + void convertInput(const float* input, float** outputs, int numFrames); + void convertOutput(float** inputs, float* output, int numFrames); }; #endif // hifi_AudioReverb_h From f43c6447e1abd6c3f3ec8934eef82a217ce0c698 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sun, 3 Jul 2016 11:46:23 -0700 Subject: [PATCH 0893/1237] remove unneeded #pragma --- libraries/audio/src/AudioReverb.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio/src/AudioReverb.cpp b/libraries/audio/src/AudioReverb.cpp index 652f7ed58d..c561231376 100644 --- a/libraries/audio/src/AudioReverb.cpp +++ b/libraries/audio/src/AudioReverb.cpp @@ -13,13 +13,13 @@ #include "AudioReverb.h" #ifdef _MSC_VER -#pragma warning(disable : 4351) // new behavior: elements of array will be default initialized #include inline static int MULHI(int a, int b) { long long c = __emul(a, b); return ((int*)&c)[1]; } + #else #define MULHI(a,b) (int)(((long long)(a) * (b)) >> 32) From 0ad7aa72fb09f7b67da3399a2ff0cb83d33e05d2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 4 Jul 2016 14:34:38 +1200 Subject: [PATCH 0894/1237] Prevent users window from being dragged offscreen --- scripts/system/users.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 2ff2689bad..853067b90b 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -344,6 +344,7 @@ var usersWindow = (function () { windowTextHeight, windowLineSpacing, windowLineHeight, // = windowTextHeight + windowLineSpacing + windowMinimumHeight, usersOnline, // Raw users data linesOfUsers = [], // Array of indexes pointing into usersOnline @@ -435,6 +436,11 @@ var usersWindow = (function () { } } + function saturateWindowPosition() { + windowPosition.x = Math.max(0, Math.min(viewport.x - WINDOW_WIDTH, windowPosition.x)); + windowPosition.y = Math.max(windowMinimumHeight, Math.min(viewport.y, windowPosition.y)); + } + function updateOverlayPositions() { // Overlay positions are all relative to windowPosition; windowPosition is the position of the windowPane overlay. var windowLeft = windowPosition.x, @@ -854,6 +860,8 @@ var usersWindow = (function () { x: event.x - movingClickOffset.x, y: event.y - movingClickOffset.y }; + + saturateWindowPosition(); calculateWindowHeight(); updateOverlayPositions(); updateUsersDisplay(); @@ -947,6 +955,7 @@ var usersWindow = (function () { windowTextHeight = Math.floor(Overlays.textSize(textSizeOverlay, "1").height); windowLineSpacing = Math.floor(Overlays.textSize(textSizeOverlay, "1\n2").height - 2 * windowTextHeight); windowLineHeight = windowTextHeight + windowLineSpacing; + windowMinimumHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; Overlays.deleteOverlay(textSizeOverlay); viewport = Controller.getViewportDimensions(); @@ -958,7 +967,6 @@ var usersWindow = (function () { if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; windowPosition.y = offset.y <= 0 ? viewport.y + offset.y : offset.y; - } else { hmdViewport = Controller.getRecommendedOverlayRect(); windowPosition = { @@ -967,6 +975,7 @@ var usersWindow = (function () { }; } + saturateWindowPosition(); calculateWindowHeight(); windowBorder = Overlays.addOverlay("rectangle", { From f4d409f1e16b56bc08558edca6fa1928647b11c1 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 09:22:19 -0700 Subject: [PATCH 0895/1237] fix typo --- scripts/system/controllers/teleport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 7a826e2ca7..2988136603 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -28,7 +28,7 @@ var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Te var TARGET_MODEL_DIMENSIONS = { x: 1.15, - y: 0.5 + y: 0.5, z: 1.15 }; From ff745a345e17e3ba44dc5e7925338f252d9538fe Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 09:44:51 -0700 Subject: [PATCH 0896/1237] remove dupe --- scripts/system/controllers/teleport.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 2988136603..50a673272b 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -377,23 +377,6 @@ var rightTrigger = new Trigger('right'); var mappingName, teleportMapping; -function registerMappings() { - mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); - teleportMapping = Controller.newMapping(mappingName); - teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); - teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); - teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); - teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - - teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { - teleporter.enterTeleportMode('left') - }); - teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { - teleporter.enterTeleportMode('right') - }); - -} - function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); From 056e9e3a720851dd0635e3315df2be861481b3fb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 10:31:47 -0700 Subject: [PATCH 0897/1237] add teleport models etc --- scripts/system/assets/models/teleportBeam.fbx | Bin 0 -> 40172 bytes .../assets/models/teleportDestination.fbx | Bin 0 -> 188700 bytes scripts/system/controllers/teleport.js | 92 +++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 scripts/system/assets/models/teleportBeam.fbx create mode 100644 scripts/system/assets/models/teleportDestination.fbx diff --git a/scripts/system/assets/models/teleportBeam.fbx b/scripts/system/assets/models/teleportBeam.fbx new file mode 100644 index 0000000000000000000000000000000000000000..21b63d5843ac14a12ea7601d1bf8475c696cac93 GIT binary patch literal 40172 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 bNQV9Ysr(37w%}RY!{p|7QjO6owI%;QsNJ!J literal 0 HcmV?d00001 diff --git a/scripts/system/assets/models/teleportDestination.fbx b/scripts/system/assets/models/teleportDestination.fbx new file mode 100644 index 0000000000000000000000000000000000000000..c850982f563417e960d9079dbc0da29408ed01c9 GIT binary patch literal 188700 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℞lN$ndk^!g{~r9Z~3GWN2eQLZJ8~+EUl9 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 z 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 RE9bu=_IC)^{WEw8{ue`kmstP+ literal 0 HcmV?d00001 diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 50a673272b..32c88174c9 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -15,8 +15,8 @@ //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) -//terminate the line when there is an intersection -//when there's not an intersection, set a fixed distance? +//terminate the line when there is an intersection (moving away from lines so...) +//when there's not an intersection, set a fixed distance? (no) //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? @@ -24,6 +24,13 @@ var inTeleportMode = false; +var currentFadeSphereOpacity = 1; +var fadeSphereInterval = null; +//milliseconds between fading one-tenth -- so this is a one second fade total +var FADE_IN_INTERVAL = 100; +var FADE_OUT_INTERVAL = 100; +var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; + var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -117,6 +124,87 @@ function Teleporter() { }; + this.createStretchyBeam = function() { + + var beamProps = { + url: BEAM_MODEL_URL, + position: MyAvatar.position, + rotation: towardsMe, + dimensions: TARGET_MODEL_DIMENSIONS + }; + + _this.stretchyBeam = Overlays.addOverlay("model", beamProps); + }; + + this.createFadeSphere = function(avatarHead) { + var sphereProps = { + // rotation: props.rotation, + position: avatarHead, + size: 0.25, + color: { + red: 0, + green: 0, + blue: 0, + }, + alpha: 1, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: true + }; + + currentFadeSphereOpacity = 1; + + _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); + }; + + this.fadeSphereOut = function() { + + fadeSphereInterval = Script.setInterval(function() { + if (currentFadeSphereOpacity === 0) { + Script.clearInterval(fadeSphereInterval); + fadeSphereInterval = null; + return; + } + if (currentFadeSphereOpacity > 0) { + currentFadeSphereOpacity -= 0.1; + } + Overlays.editOverlay(_this.fadeSphere, { + opacity: currentFadeSphereOpacity + }) + + }, FADE_OUT_INTERVAL) + }; + + this.fadeSphereIn = function() { + fadeSphereInterval = Script.setInterval(function() { + if (currentFadeSphereOpacity === 1) { + Script.clearInterval(fadeSphereInterval); + fadeSphereInterval = null; + return; + } + if (currentFadeSphereOpacity < 1) { + currentFadeSphereOpacity += 0.1; + } + Overlays.editOverlay(_this.fadeSphere, { + opacity: currentFadeSphereOpacity + }) + + }, FADE_IN_INTERVAL); + }; + + this.deleteFadeSphere = function() { + Overlays.deleteOverlay(_this.fadeSphere); + }; + + this.updateStretchyBeam = function() { + + }; + + this.deleteStretchyBeam = function() { + Overlays.deleteOverlay(_this.stretchyBeam); + }; + this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); From d6250d4c5af23014da55b30a54380bce448de65a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 4 Jul 2016 21:19:14 -0700 Subject: [PATCH 0898/1237] Fix export entities by region --- interface/src/Application.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1c9ec94dc4..82cbaf93c2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3049,17 +3049,20 @@ bool Application::exportEntities(const QString& filename, const QVector entities; QVector ids; auto entityTree = getEntities()->getTree(); entityTree->withReadLock([&] { - entityTree->findEntities(AACube(offset, scale), entities); + entityTree->findEntities(boundingCube, entities); foreach(EntityItemPointer entity, entities) { ids << entity->getEntityItemID(); } }); - return exportEntities(filename, ids, &offset); + return exportEntities(filename, ids, ¢er); } void Application::loadSettings() { From cbd1f8df88d83d8ea3c703b9d50b214b2967d751 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 13:11:14 -0700 Subject: [PATCH 0899/1237] add stuff for fading in/out and also for a nice stretchy beam --- scripts/system/controllers/teleport.js | 127 ++++++++++++++++++++----- 1 file changed, 102 insertions(+), 25 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 32c88174c9..92e53ed9f8 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -10,8 +10,16 @@ //if thumb is release, exit teleport mode xxx +//v2 // try just thumb to teleport xxx +//v2 +//fade in/out +//stretchy beam instead of GL line + +//v3 +// + //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) @@ -26,10 +34,13 @@ var inTeleportMode = false; var currentFadeSphereOpacity = 1; var fadeSphereInterval = null; -//milliseconds between fading one-tenth -- so this is a one second fade total -var FADE_IN_INTERVAL = 100; -var FADE_OUT_INTERVAL = 100; +//milliseconds between fading one-tenth -- so this is a half second fade total +var FADE_IN_INTERVAL = 50; +var FADE_OUT_INTERVAL = 50; + var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; +var STRETCHY_BEAM_DIMENSIONS_X = 0.07; +var STRETCHY_BEAM_DIMENSIONS_Y = 0.07; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; @@ -59,7 +70,6 @@ function Trigger(hand) { var _this = this; this.buttonPress = function(value) { - print('jbp trigger press: ' + value + " on: " + _this.hand) _this.buttonValue = value; }; @@ -74,6 +84,7 @@ function Teleporter() { this.targetProps = null; this.rightOverlayLine = null; this.leftOverlayLine = null; + this.stretchyBeam = null; this.initialize = function() { print('jbp initialize') this.createMappings(); @@ -113,8 +124,9 @@ function Teleporter() { this.enterTeleportMode = function(hand) { if (inTeleportMode === true) { - return - } + return; + }; + print('jbp hand on entering teleport mode: ' + hand); inTeleportMode = true; this.teleportHand = hand; @@ -124,21 +136,61 @@ function Teleporter() { }; - this.createStretchyBeam = function() { + this.drawStretchyBeamWithoutIntersection = function() { + + }; + + this.findMidpoint = function(handPosition, intersection) { + var xy = Vec3.sum(handPosition, intersection.intersection); + var midpoint = Vec3.multiply(0.5, xy); + print('midpoint point is ' + JSON.stringify(midpoint)); + return midpoint + }; + + + this.createStretchyBeam = function(handPosition, intersection, rotation) { var beamProps = { url: BEAM_MODEL_URL, - position: MyAvatar.position, - rotation: towardsMe, - dimensions: TARGET_MODEL_DIMENSIONS + position: _this.findMidpoint(handPosition, intersection), + dimensions: { + x: STRETCHY_BEAM_DIMENSIONS_X, + y: STRETCHY_BEAM_DIMENSIONS_Y, + z: 0.1 + }, + ignoreRayIntersection: true, }; _this.stretchyBeam = Overlays.addOverlay("model", beamProps); + Script.update.connect(_this.updateStretchyBeam); }; + + this.updateStretchyBeam = function(handPosition, intersection, rotation) { + var dimensions = { + x: STRETCHY_BEAM_DIMENSIONS_X, + y: STRETCHY_BEAM_DIMENSIONS_Y, + z: Vec3.distance(handPosition, intersection.intersection) / 2 + }; + + var position = _this.findMidpoint(handPosition, intersection); + + Overlays.editOverlay(_this.stretchyBeam, { + dimensions: dimensions, + position: position, + + }) + + }; + + this.deleteStretchyBeam = function() { + Overlays.deleteOverlay(_this.stretchyBeam); + Script.update.disconnect(_this.updateStretchyBeam); + }; + + this.createFadeSphere = function(avatarHead) { var sphereProps = { - // rotation: props.rotation, position: avatarHead, size: 0.25, color: { @@ -156,6 +208,8 @@ function Teleporter() { currentFadeSphereOpacity = 1; _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); + + Script.update.connect(_this.updateFadeSphere); }; this.fadeSphereOut = function() { @@ -163,6 +217,7 @@ function Teleporter() { fadeSphereInterval = Script.setInterval(function() { if (currentFadeSphereOpacity === 0) { Script.clearInterval(fadeSphereInterval); + _this.deleteFadeSphere(); fadeSphereInterval = null; return; } @@ -170,16 +225,17 @@ function Teleporter() { currentFadeSphereOpacity -= 0.1; } Overlays.editOverlay(_this.fadeSphere, { - opacity: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity }) - }, FADE_OUT_INTERVAL) + }, FADE_OUT_INTERVAL); }; this.fadeSphereIn = function() { fadeSphereInterval = Script.setInterval(function() { if (currentFadeSphereOpacity === 1) { Script.clearInterval(fadeSphereInterval); + _this.deleteFadeSphere(); fadeSphereInterval = null; return; } @@ -187,34 +243,32 @@ function Teleporter() { currentFadeSphereOpacity += 0.1; } Overlays.editOverlay(_this.fadeSphere, { - opacity: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity }) }, FADE_IN_INTERVAL); }; + this.updateFadeSphere = function() { + var headPosition = MyAvatar.getHeadPosition(); + Overlays.editOverlay(_this.fadeSphere, { + position: headPosition + }) + }; + this.deleteFadeSphere = function() { + Script.update.disconnect(_this.updateFadeSphere); Overlays.deleteOverlay(_this.fadeSphere); }; - this.updateStretchyBeam = function() { - - }; - - this.deleteStretchyBeam = function() { - Overlays.deleteOverlay(_this.stretchyBeam); - }; - this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); this.disableMappings(); this.rightOverlayOff(); this.leftOverlayOff(); - // Entities.deleteEntity(_this.targetEntity); Overlays.deleteOverlay(_this.targetOverlay); this.enableGrab(); - this.updateConnected = false; Script.setTimeout(function() { inTeleportMode = false; @@ -264,8 +318,14 @@ function Teleporter() { if (rightIntersection.intersects) { this.updateTargetOverlay(rightIntersection); + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + } else { + this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + } } else { this.noIntersection() + this.noIntersectionStretchyBeam(); } }; @@ -282,17 +342,27 @@ function Teleporter() { this.leftPickRay = leftPickRay; var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); + this.leftLineOn(leftPickRay.origin, location, { red: 7, green: 36, blue: 44 }); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); if (leftIntersection.intersects) { this.updateTargetOverlay(leftIntersection); + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + } else { + this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + + } + } else { - this.noIntersection() + this.noIntersection(); + this.noIntersectionStretchyBeam(); } }; @@ -373,6 +443,13 @@ function Teleporter() { }); }; + + this.noIntersectionStretchyBeam = function() { + print('no intersection'); + + }; + + this.updateTargetOverlay = function(intersection) { this.intersection = intersection; var position = { From b8e6572ebfe490cd587922b2a7617b2575915f4c Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 5 Jul 2016 17:00:54 -0700 Subject: [PATCH 0900/1237] basic hand-controller editing --- .../system/controllers/handControllerGrab.js | 31 +++-- scripts/system/edit.js | 8 ++ .../system/libraries/entitySelectionTool.js | 115 +++++++++++++----- 3 files changed, 112 insertions(+), 42 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ecece8c4f7..dc6b78de8e 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -221,6 +221,14 @@ function entityHasActions(entityID) { return Entities.getActionIDs(entityID).length > 0; } +function findRayIntersection(pickRay, precise, include, exclude) { + var entities = Entities.findRayIntersection(pickRay, precise, include, exclude); + var overlays = Overlays.findRayIntersection(pickRay); + if (!overlays.intersects || (entities.distance <= overlays.distance)) { + return entities; + } + return overlays; +} function entityIsGrabbedByOther(entityID) { // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*. var actionIDs = Entities.getActionIDs(entityID); @@ -251,6 +259,10 @@ function propsArePhysical(props) { // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; +var EDIT_SETTING = "io.highfidelity.isEditting"; +function isEditing() { + return EXTERNALLY_MANAGED_2D_MINOR_MODE && Settings.getValue(EDIT_SETTING); +} function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, // because other scripts (such as handControllerPointer) might be using some other visualization @@ -1069,20 +1081,15 @@ function MyController(hand) { var intersection; if (USE_BLACKLIST === true && blacklist.length !== 0) { - intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); + intersection = findRayIntersection(pickRayBacked, true, [], blacklist); } else { - intersection = Entities.findRayIntersection(pickRayBacked, true); - } - - var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); - if (!intersection.intersects || - (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { - intersection = overlayIntersection; + intersection = findRayIntersection(pickRayBacked, true); } if (intersection.intersects) { return { entityID: intersection.entityID, + overlayID: intersection.overlayID, searchRay: pickRay, distance: Vec3.distance(pickRay.origin, intersection.intersection) }; @@ -1326,6 +1333,8 @@ function MyController(hand) { if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { grabbableEntities.push(rayPickInfo.entityID); } + } else if (rayPickInfo.overlayID) { + this.intersectionDistance = rayPickInfo.distance; } else { this.intersectionDistance = 0; } @@ -1382,7 +1391,7 @@ function MyController(hand) { // TODO: highlight the far-triggerable object? } } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { - if (this.triggerSmoothedGrab()) { + if (this.triggerSmoothedGrab() && !isEditing()) { this.grabbedEntity = entity; this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); return; @@ -1942,8 +1951,8 @@ function MyController(hand) { var now = Date.now(); if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { - var intersection = Entities.findRayIntersection(pickRay, true); - if (intersection.accurate) { + var intersection = findRayIntersection(pickRay, true); + if (intersection.accurate || intersection.overlayID) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { this.callEntityMethodOnGrabbed("stopFarTrigger"); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 9d5585e353..f746fa2885 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -332,6 +332,8 @@ var toolBar = (function() { entityListTool.clearEntityList(); }; + var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts + Settings.setValue(EDIT_SETTING, false); that.setActive = function(active) { if (active != isActive) { if (active && !Entities.canRez() && !Entities.canRezTmp()) { @@ -341,6 +343,7 @@ var toolBar = (function() { enabled: active })); isActive = active; + Settings.setValue(EDIT_SETTING, active); if (!isActive) { entityListTool.setVisible(false); gridTool.setVisible(false); @@ -348,6 +351,7 @@ var toolBar = (function() { propertiesTool.setVisible(false); selectionManager.clearSelections(); cameraManager.disable(); + selectionDisplay.triggerMapping.disable(); } else { UserActivityLogger.enabledEdit(); hasShownPropertiesTool = false; @@ -356,6 +360,7 @@ var toolBar = (function() { grid.setEnabled(true); propertiesTool.setVisible(true); // Not sure what the following was meant to accomplish, but it currently causes + selectionDisplay.triggerMapping.enable(); // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); } @@ -1438,6 +1443,9 @@ function pushCommandForSelections(createdEntityData, deletedEntityData) { var entityID = SelectionManager.selections[i]; var initialProperties = SelectionManager.savedProperties[entityID]; var currentProperties = Entities.getEntityProperties(entityID); + if (!initialProperties) { + continue; + } undoData.setProperties.push({ entityID: entityID, properties: { diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index e46306ca31..2003df3652 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1005,6 +1005,56 @@ SelectionDisplay = (function() { var activeTool = null; var grabberTools = {}; + // We get mouseMoveEvents from the handControllers, via handControllerPointer. + // But we dont' get mousePressEvents. + that.triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); + Script.scriptEnding.connect(that.triggerMapping.disable); + that.TRIGGER_GRAB_VALUE = 0.85; // From handControllerGrab/Pointer.js. Should refactor. + that.TRIGGER_ON_VALUE = 0.4; + that.TRIGGER_OFF_VALUE = 0.15; + that.triggered = false; + var activeHand = Controller.Standard.RightHand; + function makeTriggerHandler(hand) { + return function (value) { + if (!that.triggered && (value > that.TRIGGER_GRAB_VALUE)) { // should we smooth? + that.triggered = true; + if (activeHand !== hand) { + // No switching while the other is already triggered, so no need to release. + activeHand = (activeHand === Controller.Standard.RightHand) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + } + var eventResult = that.mousePressEvent({}); + if (!eventResult || (eventResult === 'selectionBox')) { + var pickRay = controllerComputePickRay(); + if (pickRay) { + var entityIntersection = Entities.findRayIntersection(pickRay, true); + var overlayIntersection = Overlays.findRayIntersection(pickRay); + if (entityIntersection.intersects && + (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { + selectionManager.setSelections([entityIntersection.entityID]); + } + } + } + } else if (that.triggered && (value < that.TRIGGER_OFF_VALUE)) { + that.triggered = false; + that.mouseReleaseEvent({}); + } + }; + } + that.triggerMapping.from(Controller.Standard.RT).peek().to(makeTriggerHandler(Controller.Standard.RightHand)); + that.triggerMapping.from(Controller.Standard.LT).peek().to(makeTriggerHandler(Controller.Standard.LeftHand)); + function controllerComputePickRay() { + var controllerPose = Controller.getPoseValue(activeHand); + if (controllerPose.valid && that.triggered) { + var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), + MyAvatar.position); + // This gets point direction right, but if you want general quaternion it would be more complicated: + var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + return {origin: controllerPosition, direction: controllerDirection}; + } + } + function generalComputePickRay(x, y) { + return controllerComputePickRay() || Camera.computePickRay(x, y); + } function addGrabberTool(overlay, tool) { grabberTools[overlay] = { mode: tool.mode, @@ -1047,7 +1097,7 @@ SelectionDisplay = (function() { lastCameraOrientation = Camera.getOrientation(); if (event !== false) { - pickRay = Camera.computePickRay(event.x, event.y); + pickRay = generalComputePickRay(event.x, event.y); var wantDebug = false; if (wantDebug) { @@ -2269,7 +2319,7 @@ SelectionDisplay = (function() { startPosition = SelectionManager.worldPosition; var dimensions = SelectionManager.worldDimensions; - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); initialXZPick = rayPlaneIntersection(pickRay, translateXZTool.pickPlanePosition, { x: 0, y: 1, @@ -2312,7 +2362,7 @@ SelectionDisplay = (function() { }, onMove: function(event) { var wantDebug = false; - pickRay = Camera.computePickRay(event.x, event.y); + pickRay = generalComputePickRay(event.x, event.y); var pick = rayPlaneIntersection2(pickRay, translateXZTool.pickPlanePosition, { x: 0, @@ -2422,6 +2472,9 @@ SelectionDisplay = (function() { for (var i = 0; i < SelectionManager.selections.length; i++) { var properties = SelectionManager.savedProperties[SelectionManager.selections[i]]; + if (!properties) { + continue; + } var newPosition = Vec3.sum(properties.position, { x: vector.x, y: 0, @@ -2448,7 +2501,7 @@ SelectionDisplay = (function() { addGrabberTool(grabberMoveUp, { mode: "TRANSLATE_UP_DOWN", onBegin: function(event) { - pickRay = Camera.computePickRay(event.x, event.y); + pickRay = generalComputePickRay(event.x, event.y); upDownPickNormal = Quat.getFront(lastCameraOrientation); // Remove y component so the y-axis lies along the plane we picking on - this will @@ -2481,7 +2534,7 @@ SelectionDisplay = (function() { pushCommandForSelections(duplicatedEntityIDs); }, onMove: function(event) { - pickRay = Camera.computePickRay(event.x, event.y); + pickRay = generalComputePickRay(event.x, event.y); // translate mode left/right based on view toward entity var newIntersection = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, upDownPickNormal); @@ -2690,7 +2743,7 @@ SelectionDisplay = (function() { } } planeNormal = Vec3.multiplyQbyV(rotation, planeNormal); - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); lastPick = rayPlaneIntersection(pickRay, pickRayPosition, planeNormal); @@ -2726,7 +2779,7 @@ SelectionDisplay = (function() { rotation = SelectionManager.worldRotation; } - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); newPick = rayPlaneIntersection(pickRay, pickRayPosition, planeNormal); @@ -2782,10 +2835,10 @@ SelectionDisplay = (function() { var wantDebug = false; if (wantDebug) { print(stretchMode); - Vec3.print(" newIntersection:", newIntersection); + //Vec3.print(" newIntersection:", newIntersection); Vec3.print(" vector:", vector); - Vec3.print(" oldPOS:", oldPOS); - Vec3.print(" newPOS:", newPOS); + //Vec3.print(" oldPOS:", oldPOS); + //Vec3.print(" newPOS:", newPOS); Vec3.print(" changeInDimensions:", changeInDimensions); Vec3.print(" newDimensions:", newDimensions); @@ -3350,7 +3403,7 @@ SelectionDisplay = (function() { pushCommandForSelections(); }, onMove: function(event) { - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); Overlays.editOverlay(selectionBox, { ignoreRayIntersection: true, visible: false @@ -3520,7 +3573,7 @@ SelectionDisplay = (function() { pushCommandForSelections(); }, onMove: function(event) { - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); Overlays.editOverlay(selectionBox, { ignoreRayIntersection: true, visible: false @@ -3682,7 +3735,7 @@ SelectionDisplay = (function() { pushCommandForSelections(); }, onMove: function(event) { - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); Overlays.editOverlay(selectionBox, { ignoreRayIntersection: true, visible: false @@ -3797,13 +3850,13 @@ SelectionDisplay = (function() { that.mousePressEvent = function(event) { var wantDebug = false; - if (!event.isLeftButton) { + if (!event.isLeftButton && !that.triggered) { // if another mouse button than left is pressed ignore it return false; } var somethingClicked = false; - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); // before we do a ray test for grabbers, disable the ray intersection for our selection box Overlays.editOverlay(selectionBox, { @@ -3837,7 +3890,7 @@ SelectionDisplay = (function() { if (tool) { activeTool = tool; mode = tool.mode; - somethingClicked = true; + somethingClicked = 'tool'; if (activeTool && activeTool.onBegin) { activeTool.onBegin(event); } @@ -3845,7 +3898,7 @@ SelectionDisplay = (function() { switch (result.overlayID) { case grabberMoveUp: mode = "TRANSLATE_UP_DOWN"; - somethingClicked = true; + somethingClicked = mode; // in translate mode, we hide our stretch handles... for (var i = 0; i < stretchHandles.length; i++) { @@ -3860,34 +3913,34 @@ SelectionDisplay = (function() { case grabberEdgeTN: // TODO: maybe this should be TOP+NEAR stretching? case grabberEdgeBN: // TODO: maybe this should be BOTTOM+FAR stretching? mode = "STRETCH_NEAR"; - somethingClicked = true; + somethingClicked = mode; break; case grabberFAR: case grabberEdgeTF: // TODO: maybe this should be TOP+FAR stretching? case grabberEdgeBF: // TODO: maybe this should be BOTTOM+FAR stretching? mode = "STRETCH_FAR"; - somethingClicked = true; + somethingClicked = mode; break; case grabberTOP: mode = "STRETCH_TOP"; - somethingClicked = true; + somethingClicked = mode; break; case grabberBOTTOM: mode = "STRETCH_BOTTOM"; - somethingClicked = true; + somethingClicked = mode; break; case grabberRIGHT: case grabberEdgeTR: // TODO: maybe this should be TOP+RIGHT stretching? case grabberEdgeBR: // TODO: maybe this should be BOTTOM+RIGHT stretching? mode = "STRETCH_RIGHT"; - somethingClicked = true; + somethingClicked = mode; break; case grabberLEFT: case grabberEdgeTL: // TODO: maybe this should be TOP+LEFT stretching? case grabberEdgeBL: // TODO: maybe this should be BOTTOM+LEFT stretching? mode = "STRETCH_LEFT"; - somethingClicked = true; + somethingClicked = mode; break; default: @@ -3955,7 +4008,7 @@ SelectionDisplay = (function() { if (tool) { activeTool = tool; mode = tool.mode; - somethingClicked = true; + somethingClicked = 'tool'; if (activeTool && activeTool.onBegin) { activeTool.onBegin(event); } @@ -3963,7 +4016,7 @@ SelectionDisplay = (function() { switch (result.overlayID) { case yawHandle: mode = "ROTATE_YAW"; - somethingClicked = true; + somethingClicked = mode; overlayOrientation = yawHandleRotation; overlayCenter = yawCenter; yawZero = result.intersection; @@ -3973,7 +4026,7 @@ SelectionDisplay = (function() { case pitchHandle: mode = "ROTATE_PITCH"; initialPosition = SelectionManager.worldPosition; - somethingClicked = true; + somethingClicked = mode; overlayOrientation = pitchHandleRotation; overlayCenter = pitchCenter; pitchZero = result.intersection; @@ -3982,7 +4035,7 @@ SelectionDisplay = (function() { case rollHandle: mode = "ROTATE_ROLL"; - somethingClicked = true; + somethingClicked = mode; overlayOrientation = rollHandleRotation; overlayCenter = rollCenter; rollZero = result.intersection; @@ -4156,7 +4209,7 @@ SelectionDisplay = (function() { mode = translateXZTool.mode; activeTool.onBegin(event); - somethingClicked = true; + somethingClicked = 'selectionBox'; break; default: if (wantDebug) { @@ -4169,7 +4222,7 @@ SelectionDisplay = (function() { } if (somethingClicked) { - pickRay = Camera.computePickRay(event.x, event.y); + pickRay = generalComputePickRay(event.x, event.y); if (wantDebug) { print("mousePressEvent()...... " + overlayNames[result.overlayID]); } @@ -4201,7 +4254,7 @@ SelectionDisplay = (function() { } // if no tool is active, then just look for handles to highlight... - var pickRay = Camera.computePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); var result = Overlays.findRayIntersection(pickRay); var pickedColor; var pickedAlpha; @@ -4320,7 +4373,7 @@ SelectionDisplay = (function() { that.updateHandleSizes = function() { if (selectionManager.hasSelection()) { var diff = Vec3.subtract(selectionManager.worldPosition, Camera.getPosition()); - var grabberSize = Vec3.length(diff) * GRABBER_DISTANCE_TO_SIZE_RATIO * 2; + var grabberSize = Vec3.length(diff) * GRABBER_DISTANCE_TO_SIZE_RATIO * 5; var dimensions = SelectionManager.worldDimensions; var avgDimension = (dimensions.x + dimensions.y + dimensions.z) / 3; grabberSize = Math.min(grabberSize, avgDimension / 10); From 04184ee1fba636a42e6844334d6b7e11980c5cc4 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 17:21:57 -0700 Subject: [PATCH 0901/1237] added snapshot notification in desktop mode --- interface/src/Application.cpp | 4 +- .../src/scripting/WindowScriptingInterface.h | 1 + scripts/system/notifications.js | 83 +++++++++++-------- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9d177d724d..d00c966da1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4988,7 +4988,9 @@ void Application::takeSnapshot() { player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); + QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); + + emit DependencyManager::get()->snapshotTaken(path); } float Application::getRenderResolutionScale() const { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index b92114c1bf..4f26ccd057 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -46,6 +46,7 @@ signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode); + void snapshotTaken(const QString& path); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 7d97470b8a..667d87271d 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -49,10 +49,10 @@ // 2. Declare a text string. // 3. Call createNotifications(text, NotificationType) parsing the text. // example: -// if (key.text === "s") { +// if (key.text === "o") { // if (ctrlIsPressed === true) { -// noteString = "Snapshot taken."; -// createNotification(noteString, NotificationType.SNAPSHOT); +// noteString = "Open script"; +// createNotification(noteString, NotificationType.OPEN_SCRIPT); // } // } @@ -233,8 +233,9 @@ function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { // Pushes data to each array and sets up data for 2nd dimension array // to handle auxiliary data not carried by the overlay class // specifically notification "heights", "times" of creation, and . -function notify(notice, button, height) { - var noticeWidth, +function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, noticeHeight, positions, last; @@ -269,9 +270,13 @@ function notify(notice, button, height) { height: noticeHeight }); } else { - var notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - buttons.push((Overlays.addOverlay("image", button))); + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push((notificationText)); + } else { + notifications.push(Overlays.addOverlay("image", notice)); + } + buttons.push(Overlays.addOverlay("image", button)); } height = height + 1.0; @@ -281,11 +286,24 @@ function notify(notice, button, height) { last = notifications.length - 1; createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); fadeIn(notifications[last], buttons[last]); + + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + imageURL: imageProperties.path + }; + notify(notice, button, imageHeight, imageProperties, true); + } + return notificationText; } // This function creates and sizes the overlays -function createNotification(text, notificationType) { +function createNotification(text, notificationType, imageProperties) { var count = (text.match(/\n/g) || []).length, breakPoint = 43.0, // length when new line is added extraLine = 0, @@ -308,6 +326,11 @@ function createNotification(text, notificationType) { level = (stack + 20.0); height = height + extraLine; + + if (imageProperties && imageProperties.path) { + imageProperties.path = Script.resolvePath(imageProperties.path); + } + noticeProperties = { x: overlayLocationX, y: level, @@ -336,12 +359,11 @@ function createNotification(text, notificationType) { }; if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) - { + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { randomSounds.playRandom(); } - return notify(noticeProperties, buttonProperties, height); + return notify(noticeProperties, buttonProperties, height, imageProperties); } function deleteNotification(index) { @@ -362,21 +384,12 @@ function deleteNotification(index) { // wraps whole word to newline function stringDivider(str, slotWidth, spaceReplacer) { - var p, - left, - right; + var left, right; - if (str.length > slotWidth) { - p = slotWidth; - while (p > 0 && str[p] !== ' ') { - p -= 1; - } - - if (p > 0) { - left = str.substring(0, p); - right = str.substring(p + 1); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); - } + if (str.length > slotWidth && slotWidth > 0) { + left = str.substring(0, slotWidth); + right = str.substring(slotWidth + 1); + return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); } return str; } @@ -504,7 +517,15 @@ function onMuteStateChanged() { } function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED ); + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); +} + +function onSnapshotTaken(path) { + var imageProperties = { + path: path, + aspectRatio: Window.innerWidth / Window.innerHeight + } + createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); } // handles mouse clicks on buttons @@ -541,13 +562,6 @@ function keyPressEvent(key) { if (key.key === 16777249) { ctrlIsPressed = true; } - - if (key.text === "s") { - if (ctrlIsPressed === true) { - noteString = "Snapshot taken."; - createNotification(noteString, NotificationType.SNAPSHOT); - } - } } function setup() { @@ -615,5 +629,6 @@ Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); Menu.menuItemEvent.connect(menuItemEvent); Window.domainConnectionRefused.connect(onDomainConnectionRefused); +Window.snapshotTaken.connect(onSnapshotTaken); setup(); From c78dbe26b65fe19c96644e00221a09c8889dccb2 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 17:25:21 -0700 Subject: [PATCH 0902/1237] working initial beam, still trying to clear up no intersection beam issues --- scripts/system/controllers/teleport.js | 329 ++++++++++++++----------- 1 file changed, 187 insertions(+), 142 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 92e53ed9f8..8c67811529 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -39,17 +39,30 @@ var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; -var STRETCHY_BEAM_DIMENSIONS_X = 0.07; -var STRETCHY_BEAM_DIMENSIONS_Y = 0.07; +var BEAM_MODEL_URL_NO_INTERSECTION = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam2.fbx"; + + +var STRETCHY_BEAM_DIMENSIONS_X = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 10; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; - var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, z: 1.15 }; + +//swirly thing, not sure we'll get to use it +// var TARGET_MODEL_URL='http://hifi-content.s3.amazonaws.com/alan/dev/Cyclone-4.fbx'; + +// var TARGET_MODEL_DIMENSIONS = { +// x: 0.93, +// y: 0.93, +// z: 1.22 +// }; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -62,7 +75,6 @@ function ThumbPad(hand) { this.down = function() { return _this.buttonValue === 1 ? 1.0 : 0.0; }; - } function Trigger(hand) { @@ -81,15 +93,21 @@ function Trigger(hand) { function Teleporter() { var _this = this; + this.intersection = null; this.targetProps = null; - this.rightOverlayLine = null; - this.leftOverlayLine = null; + this.targetOverlay = null; this.stretchyBeam = null; + this.noIntersectionStretchyBeam = null; + this.updateConnected = null; + this.initialize = function() { print('jbp initialize') this.createMappings(); this.disableGrab(); + }; + this.createTargetOverlay = function() { + print('creating target overlay') var cameraEuler = Quat.safeEulerAngles(Camera.orientation); var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { x: 0, @@ -101,7 +119,8 @@ function Teleporter() { url: TARGET_MODEL_URL, position: MyAvatar.position, rotation: towardsMe, - dimensions: TARGET_MODEL_DIMENSIONS + dimensions: TARGET_MODEL_DIMENSIONS, + visible: true, }; _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); @@ -125,7 +144,7 @@ function Teleporter() { this.enterTeleportMode = function(hand) { if (inTeleportMode === true) { return; - }; + } print('jbp hand on entering teleport mode: ' + hand); inTeleportMode = true; @@ -136,14 +155,9 @@ function Teleporter() { }; - this.drawStretchyBeamWithoutIntersection = function() { - - }; - this.findMidpoint = function(handPosition, intersection) { var xy = Vec3.sum(handPosition, intersection.intersection); var midpoint = Vec3.multiply(0.5, xy); - print('midpoint point is ' + JSON.stringify(midpoint)); return midpoint }; @@ -162,7 +176,6 @@ function Teleporter() { }; _this.stretchyBeam = Overlays.addOverlay("model", beamProps); - Script.update.connect(_this.updateStretchyBeam); }; @@ -170,24 +183,105 @@ function Teleporter() { var dimensions = { x: STRETCHY_BEAM_DIMENSIONS_X, y: STRETCHY_BEAM_DIMENSIONS_Y, - z: Vec3.distance(handPosition, intersection.intersection) / 2 + z: Vec3.distance(handPosition, intersection.intersection) }; + print('dimensions in update:: ' + JSON.stringify(dimensions)); var position = _this.findMidpoint(handPosition, intersection); - Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, - + rotation: Quat.multiply(rotation, Quat.angleAxis(180, { + x: 0, + y: 1, + z: 0 + })) }) }; this.deleteStretchyBeam = function() { - Overlays.deleteOverlay(_this.stretchyBeam); - Script.update.disconnect(_this.updateStretchyBeam); + if (_this.stretchyBeam !== null) { + Overlays.deleteOverlay(_this.stretchyBeam); + _this.stretchyBeam = null; + } }; + this.createNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { + + var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; + + var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); + + var midpoint = this.findMidpoint(handPosition, { + intersection: ahead + }); + + var dimensions = { + x: 0.04, + y: 0.04, + z: Vec3.distance(handPosition, ahead) + }; + + + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { + x: 0, + y: 1, + z: 0 + })); + + var beamProps = { + dimensions: dimensions, + url: BEAM_MODEL_URL_NO_INTERSECTION, + position: midpoint, + rotation: finalRotation, + ignoreRayIntersection: true, + }; + + this.noIntersectionStretchyBeam = Overlays.addOverlay("model", beamProps); + + + }; + + this.updateNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { + + var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; + + var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); + + var midpoint = this.findMidpoint(handPosition, { + intersection: ahead + }); + + var dimensions = { + x: STRETCHY_BEAM_DIMENSIONS_X, + y: STRETCHY_BEAM_DIMENSIONS_Y, + z: Vec3.distance(handPosition, ahead) + }; + + print('dimensions in update:: ' + JSON.stringify(dimensions)); + + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { + x: 0, + y: 1, + z: 0 + })); + + var goodEdit = Overlays.editOverlay(_this.noIntersectionStretchyBeam, { + dimensions: dimensions, + position: midpoint, + rotation: rotation + }) + + }; + + + this.deleteNoIntersectionStretchyBeam = function() { + if (_this.noIntersectionStretchyBeam !== null) { + Overlays.deleteOverlay(_this.noIntersectionStretchyBeam); + _this.noIntersectionStretchyBeam = null; + } + }; this.createFadeSphere = function(avatarHead) { var sphereProps = { @@ -261,15 +355,21 @@ function Teleporter() { Overlays.deleteOverlay(_this.fadeSphere); }; + this.deleteTargetOverlay = function() { + Overlays.deleteOverlay(this.targetOverlay); + this.intersection = null; + this.targetOverlay = null; + } + this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); + this.updateConnected = null; this.disableMappings(); - this.rightOverlayOff(); - this.leftOverlayOff(); - Overlays.deleteOverlay(_this.targetOverlay); + this.deleteStretchyBeam(); + this.deleteNoIntersectionStretchyBeam(); + this.deleteTargetOverlay(); this.enableGrab(); - this.updateConnected = false; Script.setTimeout(function() { inTeleportMode = false; }, 100); @@ -301,160 +401,105 @@ function Teleporter() { var rightRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).rotation) + var rightPickRay = { origin: rightPosition, direction: Quat.getUp(rightRotation), }; + var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(90, { + x: 1, + y: 0, + z: 0 + })); + this.rightPickRay = rightPickRay; var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); - this.rightLineOn(rightPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); + var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - this.updateTargetOverlay(rightIntersection); - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.deleteNoIntersectionStretchyBeam(); + + if (this.targetOverlay !== null) { + this.updateTargetOverlay(rightIntersection); } else { - this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.createTargetOverlay(); + } + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); + } else { + this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); } } else { - this.noIntersection() - this.noIntersectionStretchyBeam(); + this.deleteTargetOverlay(); + this.deleteStretchyBeam(); + if (this.noIntersectionStretchyBeam !== null) { + this.updateNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); + + } else { + this.createNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); + } } - }; + } + this.leftRay = function() { var leftPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).translation), MyAvatar.position); - var leftRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).rotation) + var leftPickRay = { origin: leftPosition, direction: Quat.getUp(leftRotation), }; + + var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(90, { + x: 1, + y: 0, + z: 0 + })); + this.leftPickRay = leftPickRay; var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); - this.leftLineOn(leftPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); - - var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { - this.updateTargetOverlay(leftIntersection); - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.deleteNoIntersectionStretchyBeam(); + if (this.targetOverlay !== null) { + this.updateTargetOverlay(leftIntersection); } else { - this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.createTargetOverlay(); + } + + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); + } else { + this.createStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); } } else { - this.noIntersection(); - this.noIntersectionStretchyBeam(); + this.deleteTargetOverlay(); + this.deleteStretchyBeam(); + if (this.noIntersectionStretchyBeam !== null) { + this.updateNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); + } else { + this.createNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); + } } }; - this.rightLineOn = function(closePoint, farPoint, color) { - // draw a line - if (this.rightOverlayLine === null) { - var lineProperties = { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - ignoreRayIntersection: true, // always ignore this - visible: true, - alpha: 1 - }; - - this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - var success = Overlays.editOverlay(this.rightOverlayLine, { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - visible: true, - ignoreRayIntersection: true, // always ignore this - alpha: 1 - }); - } - }; - - this.leftLineOn = function(closePoint, farPoint, color) { - // draw a line - if (this.leftOverlayLine === null) { - var lineProperties = { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - ignoreRayIntersection: true, // always ignore this - visible: true, - alpha: 1 - }; - - this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - var success = Overlays.editOverlay(this.leftOverlayLine, { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - visible: true, - ignoreRayIntersection: true, // always ignore this - alpha: 1 - }); - } - }; - - this.rightOverlayOff = function() { - if (this.rightOverlayLine !== null) { - Overlays.deleteOverlay(this.rightOverlayLine); - this.rightOverlayLine = null; - } - }; - - this.leftOverlayOff = function() { - if (this.leftOverlayLine !== null) { - Overlays.deleteOverlay(this.leftOverlayLine); - this.leftOverlayLine = null; - } - }; - - this.noIntersection = function() { - print('no intersection' + teleporter.targetOverlay); - Overlays.editOverlay(teleporter.targetOverlay, { - visible: false, - }); - }; - - - this.noIntersectionStretchyBeam = function() { - print('no intersection'); - - }; - this.updateTargetOverlay = function(intersection) { this.intersection = intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y + 0.1, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { @@ -477,13 +522,15 @@ function Teleporter() { print('TELEPORT CALLED'); var offset = getAvatarFootOffset(); - - _this.intersection.intersection.y += offset; - MyAvatar.position = _this.intersection.intersection; + if (_this.intersection !== null) { + _this.intersection.intersection.y += offset; + MyAvatar.position = _this.intersection.intersection; + } this.exitTeleportMode(); }; } + //related to repositioning the avatar after you teleport function getAvatarFootOffset() { var data = getJointData(); @@ -529,9 +576,7 @@ function getJointData() { }); return allJointData; -} - - +}; var leftPad = new ThumbPad('left'); var rightPad = new ThumbPad('right'); @@ -567,9 +612,9 @@ Script.scriptEnding.connect(cleanup); function cleanup() { teleportMapping.disable(); teleporter.disableMappings(); - teleporter.rightOverlayOff(); - teleporter.leftOverlayOff(); - Overlays.deleteOverlay(teleporter.targetOverlay); + teleporter.deleteStretchyBeam(); + teleporter.deleteTargetOverlay(); + teleporter.deleteNoIntersectionStretchyBeam(); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From 26950eaaa6efed9de1a7a9ce4495d943108210fa Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 17:30:17 -0700 Subject: [PATCH 0903/1237] working second model adjustment with hack but wtf --- scripts/system/controllers/teleport.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 8c67811529..b724ac3cfd 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -44,7 +44,7 @@ var BEAM_MODEL_URL_NO_INTERSECTION = "http://hifi-content.s3.amazonaws.com/james var STRETCHY_BEAM_DIMENSIONS_X = 0.04; var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 10; +var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -185,7 +185,7 @@ function Teleporter() { y: STRETCHY_BEAM_DIMENSIONS_Y, z: Vec3.distance(handPosition, intersection.intersection) }; - print('dimensions in update:: ' + JSON.stringify(dimensions)); + var position = _this.findMidpoint(handPosition, intersection); Overlays.editOverlay(_this.stretchyBeam, { @@ -220,7 +220,7 @@ function Teleporter() { var dimensions = { x: 0.04, y: 0.04, - z: Vec3.distance(handPosition, ahead) + z:0.1 }; @@ -258,8 +258,9 @@ function Teleporter() { y: STRETCHY_BEAM_DIMENSIONS_Y, z: Vec3.distance(handPosition, ahead) }; - - print('dimensions in update:: ' + JSON.stringify(dimensions)); + dimensions=Vec3.multiply(10,dimensions) + print('dimensions in update:: ' + JSON.stringify(dimensions)); + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, From 4eb94f8ccbd48d6b86f2ca23945c34fea2122d8e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 17:40:33 -0700 Subject: [PATCH 0904/1237] added support for snapshot notification in HMD, although 3d image overlays seem to be broken in master --- scripts/system/notifications.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 667d87271d..988d2998d7 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -251,16 +251,23 @@ function notify(notice, button, height, imageProperties, image) { notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; notice.bottomMargin = 0; notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + if (!image) { + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); + } button.url = button.imageURL; button.scale = button.width * NOTIFICATION_3D_SCALE; button.isFacingAvatar = false; - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notifications.push((Overlays.addOverlay("text3d", notice))); buttons.push((Overlays.addOverlay("image3d", button))); overlay3DDetails.push({ notificationOrientation: positions.notificationOrientation, @@ -294,6 +301,8 @@ function notify(notice, button, height, imageProperties, image) { y: notice.y + height, width: notice.width, height: imageHeight, + topMargin: 0, + leftMargin: 0, imageURL: imageProperties.path }; notify(notice, button, imageHeight, imageProperties, true); From b0494ec9d53d194637b78dcdf172c85507196434 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 17:48:30 -0700 Subject: [PATCH 0905/1237] bug notes --- scripts/system/controllers/teleport.js | 30 +++++++++++--------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b724ac3cfd..4630b234bc 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,5 +1,8 @@ // by james b. pollack @imgntn on 7/2/2016 + +///BUG: there is currently something going on when you try to adjust the dimensions of a second overlay model, it seems to be off by a factor of 10 or already changed or something weird. + //v1 //check if trigger is down xxx //if trigger is down, check if thumb is down xxx @@ -13,22 +16,15 @@ //v2 // try just thumb to teleport xxx -//v2 -//fade in/out -//stretchy beam instead of GL line - //v3 -// +//fade in/out +//stretchy beam instead of GL line xxx + //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) - -//terminate the line when there is an intersection (moving away from lines so...) -//when there's not an intersection, set a fixed distance? (no) - -//v2: show room boundaries when choosing a place to teleport -//v2: smooth fade screen in/out? -//v2: haptic feedback +//v?: haptic feedback +//v?: show room boundaries when choosing a place to teleport var inTeleportMode = false; @@ -220,7 +216,7 @@ function Teleporter() { var dimensions = { x: 0.04, y: 0.04, - z:0.1 + z: 0.1 }; @@ -231,7 +227,7 @@ function Teleporter() { })); var beamProps = { - dimensions: dimensions, + // dimensions: dimensions, url: BEAM_MODEL_URL_NO_INTERSECTION, position: midpoint, rotation: finalRotation, @@ -258,9 +254,9 @@ function Teleporter() { y: STRETCHY_BEAM_DIMENSIONS_Y, z: Vec3.distance(handPosition, ahead) }; - dimensions=Vec3.multiply(10,dimensions) - print('dimensions in update:: ' + JSON.stringify(dimensions)); - + dimensions = Vec3.multiply(10, dimensions) + print('dimensions in update:: ' + JSON.stringify(dimensions)); + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, From 66b65391a46502d6186533f57b946108c7cf7cb4 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 5 Jul 2016 18:21:49 -0700 Subject: [PATCH 0906/1237] Cleaning up the lighting equations --- .../render-utils/src/DeferredGlobalLight.slh | 380 ++++++------------ libraries/render-utils/src/LightAmbient.slh | 111 +++++ .../render-utils/src/LightDirectional.slh | 73 ++++ .../render-utils/src/SubsurfaceScattering.slh | 33 +- .../src/directional_ambient_light.slf | 7 +- .../src/directional_skybox_light.slf | 7 +- .../subsurfaceScattering_drawScattering.slf | 29 +- 7 files changed, 355 insertions(+), 285 deletions(-) create mode 100644 libraries/render-utils/src/LightAmbient.slh create mode 100644 libraries/render-utils/src/LightDirectional.slh diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 6791a90829..d76c333206 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -17,74 +17,34 @@ <@include LightingModel.slh@> <$declareLightingModel()$> -<@func declareSkyboxMap()@> -// declareSkyboxMap -uniform samplerCube skyboxMap; +<@include LightAmbient.slh@> +<@include LightDirectional.slh@> -vec4 evalSkyboxLight(vec3 direction, float lod) { - // textureQueryLevels is not available until #430, so we require explicit lod - // float mipmapLevel = lod * textureQueryLevels(skyboxMap); - return textureLod(skyboxMap, direction, lod); -} -<@endfunc@> -<@func declareEvalGlobalSpecularIrradiance(supportAmbientSphere, supportAmbientMap, supportIfAmbientMapElseAmbientSphere)@> - -vec3 fresnelSchlickAmbient(vec3 fresnelColor, vec3 lightDir, vec3 halfDir, float gloss) { - return fresnelColor + (max(vec3(gloss), fresnelColor) - fresnelColor) * pow(1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0), 5); -} - -<@if supportAmbientMap@> - <$declareSkyboxMap()$> -<@endif@> - -vec3 evalGlobalSpecularIrradiance(Light light, vec3 fragEyeDir, vec3 fragNormal, float roughness, vec3 fresnel, float obscurance) { - vec3 direction = -reflect(fragEyeDir, fragNormal); - vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, fragEyeDir, fragNormal, 1 - roughness); - vec3 specularLight; - <@if supportIfAmbientMapElseAmbientSphere@> - if (getLightHasAmbientMap(light)) - <@endif@> - <@if supportAmbientMap@> - { - float levels = getLightAmbientMapNumMips(light); - float lod = min(floor((roughness) * levels), levels); - specularLight = evalSkyboxLight(direction, lod).xyz; - } - <@endif@> - <@if supportIfAmbientMapElseAmbientSphere@> - else - <@endif@> - <@if supportAmbientSphere@> - { - specularLight = evalSphericalLight(getLightAmbientSphere(light), direction).xyz; - } - <@endif@> - - return specularLight * ambientFresnel * getLightAmbientIntensity(light); -} -<@endfunc@> - -<@func prepareGlobalLight()@> +<@func prepareGlobalLight(isScattering)@> // prepareGlobalLight - // Transform directions to worldspace - // vec3 fragNormal = vec3(invViewMat * vec4(normal, 0.0)); vec3 fragNormal = vec3((normal)); vec3 fragEyeVector = vec3(invViewMat * vec4(-position, 0.0)); vec3 fragEyeDir = normalize(fragEyeVector); // Get light Light light = getLight(); + + vec3 color = vec3(0.0); + +<@if isScattering@> + vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin + float metallic = 0.0; +<@else@> vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value if (metallic > 0.5) { fresnel = albedo; metallic = 1.0; } - vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, metallic, fresnel, roughness); - vec3 color = vec3(0.0); - color += vec3(albedo * shading.w * isDiffuseEnabled() + shading.rgb * isSpecularEnabled()) * min(shadowAttenuation, obscurance) * getLightColor(light) * getLightIntensity(light) * isDirectionalEnabled(); color += emissive * isEmissiveEnabled(); +<@endif@> + <@endfunc@> @@ -96,131 +56,138 @@ vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obsc } <@endfunc@> -<@func declareEvalAmbientSphereGlobalColor()@> -<$declareEvalGlobalSpecularIrradiance(1, 0, 0)$> +<@func declareEvalAmbientSphereGlobalColor(supportScattering)@> +<$declareLightingAmbient(1, 0, 0, supportScattering)$> +<$declareLightingDirectional(supportScattering)$> vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness) { + <$prepareGlobalLight()$> - // Diffuse from ambient - color += (1 - metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), fragNormal).xyz * obscurance * getLightAmbientIntensity(light); + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); + color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); + color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); - // Specular highlight from ambient - vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, obscurance); - color += specularLighting; + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); + color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); return color; } -<@endfunc@> -<@func declareEvalAmbientSphereGlobalColorScattering()@> +<@if supportScattering@> <$declareDeferredCurvature()$> -<@include SubsurfaceScattering.slh@> -<$declareSubsurfaceScatteringResource()$> -!> -<$declareSkinSpecularLighting()$> +vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { -vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { - // prepareGlobalLight + <$prepareGlobalLight(1)$> - // Transform directions to worldspace - vec3 fragNormal = vec3((normal)); - vec3 fragEyeVector = vec3(invViewMat * vec4(-position, 0.0)); - vec3 fragEyeDir = normalize(fragEyeVector); - - // Get light - Light light = getLight(); - vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin - float metallic = 0.0; - - vec3 fragLightDir = -normalize(getLightDirection(light)); - - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float curvature = unpackCurvature(diffusedCurvature.w); + float highCurvature = unpackCurvature(blurredCurvature.w); + float lowCurvature = unpackCurvature(diffusedCurvature.w); - if (showDiffusedNormal()) { - return diffusedCurvature.xyz; - return lowNormal * 0.5 + vec3(0.5); - } - if (showCurvature()) { - float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); - return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); - } - - vec3 bentNdotL = evalScatteringBentNdotL(fragNormal, midNormal, lowNormal, fragLightDir); - - vec3 brdf = fetchBRDFSpectrum(bentNdotL, curvature); - - // The position of the pixel fragment in Eye space then in world space - - float scatteringLevel = getScatteringLevel(); - - vec4 shading; - float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - { // Key Sun Lighting - // Diffuse Lighting - //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); - - float specular = skinSpecular(fragNormal, fragLightDir, fragEyeDir, roughness, 1.0); - - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); - float power = specularDistribution(roughness, fragNormal, halfDir); - //vec3 specular = power * fresnelColor * standardDiffuse; - - shading = vec4(vec3(specular), (1 - fresnelColor.x)); - } + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbientScattering(ambientDiffuse, ambientSpecular, light, + fragEyeDir, fragNormal, roughness, + metallic, fresnel, albedo, obscurance, + isScatteringEnabled() * scattering, lowNormal, highCurvature, lowCurvature); + color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); + color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); - if (scatteringLevel < 0.1) { - brdf = vec3(standardDiffuse); - } - brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectionalScattering(directionalDiffuse, directionalSpecular, light, + fragEyeDir, fragNormal, roughness, + metallic, fresnel, albedo, shadowAttenuation, + isScatteringEnabled() * scattering, midNormal, lowNormal, lowCurvature); + color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); + color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); - - vec3 color = vec3(albedo * vec3(brdf.xyz) * isDiffuseEnabled() * shading.w + shading.rgb * isSpecularEnabled()) * getLightColor(light) * getLightIntensity(light) * isDirectionalEnabled(); - - - // Diffuse from ambient - // color += albedo * evalSphericalLight(getLightAmbientSphere(light), bentNormalHigh).xyz *getLightAmbientIntensity(light); - - // Specular highlight from ambient - vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, 1.0); - // color += specularLighting; - - if (showBRDF()) - return brdf; - - //vec3 debugNdotL = 0.5 * (NdotLSpectrum + vec3(1.0)); - //return vec3(debugNdotL.z, curvature, 0.0 ); - - return vec3(color); + return color; } + +<@endif@> + <@endfunc@> +<@func declareEvalSkyboxGlobalColor(supportScattering)@> -<@func declareEvalSkyboxGlobalColor()@> -<$declareEvalGlobalSpecularIrradiance(0, 1, 0)$> +<$declareLightingAmbient(0, 1, 0, supportScattering)$> +<$declareLightingDirectional(supportScattering)$> vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness) { <$prepareGlobalLight()$> - // Diffuse from ambient - color += (1 - metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), fragNormal).xyz * obscurance * getLightAmbientIntensity(light); + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); + color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); + color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); - // Specular highlight from ambient - vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, obscurance); - color += specularLighting; + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); + color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); return color; } + +<@if supportScattering@> + +<$declareDeferredCurvature()$> + +vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { + <$prepareGlobalLight(1)$> + + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + float highCurvature = unpackCurvature(blurredCurvature.w); + float lowCurvature = unpackCurvature(diffusedCurvature.w); + + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbientScattering(ambientDiffuse, ambientSpecular, light, + fragEyeDir, fragNormal, roughness, + metallic, fresnel, albedo, obscurance, + isScatteringEnabled() * scattering, lowNormal, highCurvature, lowCurvature); + color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); + color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectionalScattering(directionalDiffuse, directionalSpecular, light, + fragEyeDir, fragNormal, roughness, + metallic, fresnel, albedo, shadowAttenuation, + isScatteringEnabled() * scattering, midNormal, lowNormal, lowCurvature); + color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); + color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); + + return vec3(color); +} + +<@endif@> + <@endfunc@> <@func declareEvalLightmappedColor()@> @@ -243,128 +210,35 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur // Ambient light is the lightmap when in shadow vec3 ambientLight = (1 - lightAttenuation) * lightmap * getLightAmbientIntensity(light); - return obscurance * albedo * (diffuseLight + ambientLight); + return isLightmapEnabled() * obscurance * albedo * (diffuseLight + ambientLight); } <@endfunc@> -<@func declareEvalSkyboxGlobalColorScattering()@> - -<$declareDeferredCurvature()$> -<@include SubsurfaceScattering.slh@> -<$declareSubsurfaceScatteringResource()$> -!> - -<$declareSkinSpecularLighting()$> - -float curvatureAO(in float k) { - return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369; -} -vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature, float roughness) { - // prepareGlobalLight - - // Transform directions to worldspace - vec3 fragNormal = vec3((normal)); - vec3 fragEyeVector = vec3(invViewMat * vec4(-position, 0.0)); - vec3 fragEyeDir = normalize(fragEyeVector); - - // Get light - Light light = getLight(); - vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin - float metallic = 0.0; - - vec3 fragLightDir = -normalize(getLightDirection(light)); - - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float curvature = unpackCurvature(diffusedCurvature.w); - - if (showDiffusedNormal()) { - return diffusedCurvature.xyz; - return lowNormal * 0.5 + vec3(0.5); - } - /* if (showCurvature()) { - float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); - return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); - }*/ - - vec3 bentNdotL = evalScatteringBentNdotL(fragNormal, midNormal, lowNormal, fragLightDir); - - vec3 brdf = fetchBRDFSpectrum(bentNdotL, curvature); - - // The position of the pixel fragment in Eye space then in world space - - float scatteringLevel = getScatteringLevel(); - - vec4 shading; - float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - { // Key Sun Lighting - // Diffuse Lighting - //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); - - float specular = skinSpecular(fragNormal, fragLightDir, fragEyeDir, roughness, 1.0); - - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); - float power = specularDistribution(roughness, fragNormal, halfDir); - //vec3 specular = power * fresnelColor * standardDiffuse; - - shading = vec4(vec3(specular), (1 - fresnelColor.x)); - - // shading = vec4(specular, (1 - fresnelColor.x)); - } - - if (scatteringLevel < 0.1) { - brdf = vec3(standardDiffuse); - } - - brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); - vec3 color = vec3(albedo * vec3(brdf.xyz) * shading.w + shading.rgb) * getLightColor(light) * getLightIntensity(light); - - //vec3 color = vec3(shading.rgb) * getLightColor(light) * getLightIntensity(light); - - float ambientOcclusion = curvatureAO((diffusedCurvature.w * 2 - 1) * 20.0f) * 0.5f; - float ambientOcclusionHF = curvatureAO((diffusedCurvature.w * 2 - 1) * 8.0f) * 0.5f; - ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF); - - if (showCurvature()) { - return vec3(ambientOcclusion); - } - - - // Diffuse from ambient - color += ambientOcclusion * albedo * evalSphericalLight(getLightAmbientSphere(light), lowNormal).xyz *getLightAmbientIntensity(light); - - // Specular highlight from ambient - // vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, 1.0); - // color += specularLighting; - - if ( showBRDF()) - return brdf; - - //vec3 debugNdotL = 0.5 * (NdotLSpectrum + vec3(1.0)); - //return vec3(debugNdotL.z, curvature, 0.0 ); - - return vec3(color); -} -<@endfunc@> <@func declareEvalGlobalLightingAlphaBlended()@> -<$declareEvalGlobalSpecularIrradiance(1, 1, 1)$> +<$declareLightingAmbient(1, 1, 1)$> +<$declareLightingDirectional()$> vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float opacity) { <$prepareGlobalLight()$> - // Diffuse from ambient - color += (1 - metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), fragNormal).xyz * obscurance * getLightAmbientIntensity(light); + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); + color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); + color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled() / opacity; - // Specular highlight from ambient - vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel, obscurance); - color += specularLighting / opacity; + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); + color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled() / opacity; return color; } diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh new file mode 100644 index 0000000000..e677eac6f8 --- /dev/null +++ b/libraries/render-utils/src/LightAmbient.slh @@ -0,0 +1,111 @@ +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 7/5/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + + +<@func declareSkyboxMap()@> +// declareSkyboxMap +uniform samplerCube skyboxMap; + +vec4 evalSkyboxLight(vec3 direction, float lod) { + // textureQueryLevels is not available until #430, so we require explicit lod + // float mipmapLevel = lod * textureQueryLevels(skyboxMap); + return textureLod(skyboxMap, direction, lod); +} +<@endfunc@> + +<@func declareEvalAmbientSpecularIrradiance(supportAmbientSphere, supportAmbientMap, supportIfAmbientMapElseAmbientSphere)@> + +vec3 fresnelSchlickAmbient(vec3 fresnelColor, vec3 lightDir, vec3 halfDir, float gloss) { + return fresnelColor + (max(vec3(gloss), fresnelColor) - fresnelColor) * pow(1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0), 5); +} + +<@if supportAmbientMap@> +<$declareSkyboxMap()$> +<@endif@> + +vec3 evalAmbientSpecularIrradiance(Light light, vec3 fragEyeDir, vec3 fragNormal, float roughness, vec3 fresnel) { + vec3 direction = -reflect(fragEyeDir, fragNormal); + vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, fragEyeDir, fragNormal, 1 - roughness); + vec3 specularLight; + <@if supportIfAmbientMapElseAmbientSphere@> + if (getLightHasAmbientMap(light)) + <@endif@> + <@if supportAmbientMap@> + { + float levels = getLightAmbientMapNumMips(light); + float lod = min(floor((roughness)* levels), levels); + specularLight = evalSkyboxLight(direction, lod).xyz; + } + <@endif@> + <@if supportIfAmbientMapElseAmbientSphere@> + else + <@endif@> + <@if supportAmbientSphere@> + { + specularLight = evalSphericalLight(getLightAmbientSphere(light), direction).xyz; + } + <@endif@> + + return specularLight * ambientFresnel; +} +<@endfunc@> + +<@func declareLightingAmbient(supportAmbientSphere, supportAmbientMap, supportIfAmbientMapElseAmbientSphere, supportScattering)@> + +<$declareEvalAmbientSpecularIrradiance(supportAmbientSphere, supportAmbientMap, supportIfAmbientMapElseAmbientSphere)$> + +void evalLightingAmbient(out vec3 diffuse, out vec3 specular, Light light, vec3 eyeDir, vec3 normal, float roughness, float metallic, vec3 fresnel, vec3 albedo, float obscurance) { + // Diffuse from ambient + diffuse = (1 - metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), normal).xyz * obscurance * getLightAmbientIntensity(light); + + // Specular highlight from ambient + specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); +} + +<@if supportScattering@> + +!> + +float curvatureAO(in float k) { + return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369; +} + +void evalLightingAmbientScattering(out vec3 diffuse, out vec3 specular, Light light, + vec3 eyeDir, vec3 normal, float roughness, + float metallic, vec3 fresnel, vec3 albedo, float obscurance, + float scatttering, vec3 lowNormal, float highCurvature, float lowCurvature) { + + float ambientOcclusion = curvatureAO(lowCurvature * 20.0f) * 0.5f; + float ambientOcclusionHF = curvatureAO(highCurvature * 8.0f) * 0.5f; + ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF); + + /* if (showCurvature()) { + diffuse = vec3(ambientOcclusion); + specular = vec3(0.0); + return; + }*/ + + + // Diffuse from ambient + diffuse = ambientOcclusion * albedo * evalSphericalLight(getLightAmbientSphere(light), lowNormal).xyz *getLightAmbientIntensity(light); + + // Specular highlight from ambient + // vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel); + // color += specularLighting; + + + // Specular highlight from ambient +// specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); + specular = vec3(0.0); +} + +<@endif@> + +<@endfunc@> diff --git a/libraries/render-utils/src/LightDirectional.slh b/libraries/render-utils/src/LightDirectional.slh new file mode 100644 index 0000000000..63ee8b3eda --- /dev/null +++ b/libraries/render-utils/src/LightDirectional.slh @@ -0,0 +1,73 @@ +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 7/5/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + + +<@func declareLightingDirectional(supportScattering)@> +<@include DeferredLighting.slh@> + +void evalLightingDirectional(out vec3 diffuse, out vec3 specular, Light light, + vec3 eyeDir, vec3 normal, float roughness, + float metallic, vec3 fresnel, vec3 albedo, float shadow) { + + vec4 shading = evalFragShading(normal, -getLightDirection(light), eyeDir, metallic, fresnel, roughness); + + diffuse = albedo * shading.w * shadow * getLightColor(light) * getLightIntensity(light); + + specular = shading.rgb * shadow * getLightColor(light) * getLightIntensity(light); +} + +<@if supportScattering@> + +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringBRDF()$> +<$declareSkinSpecularLighting()$> + +void evalLightingDirectionalScattering(out vec3 diffuse, out vec3 specular, Light light, + vec3 eyeDir, vec3 normal, float roughness, + float metallic, vec3 fresnel, vec3 albedo, float shadow, + float scattering, vec3 midNormal, vec3 lowNormal, float curvature) { + + vec3 fragLightDir = -normalize(getLightDirection(light)); + vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); + float scatteringLevel = getScatteringLevel(); + vec4 shading; + float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + { // Key Sun Lighting + // Diffuse Lighting + //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(eyeDir + fragLightDir); + + float specular = skinSpecular(normal, fragLightDir, eyeDir, roughness, 1.0); + + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + float power = specularDistribution(roughness, normal, halfDir); + //vec3 specular = power * fresnelColor * standardDiffuse; + + shading = vec4(vec3(specular), (1 - fresnelColor.x)); + } + + + if (scatteringLevel < 0.1) { + brdf = vec3(standardDiffuse); + } + brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); + + + diffuse = albedo * brdf.xyz * shadow * getLightColor(light) * getLightIntensity(light); + + specular = shading.rgb * shadow * getLightColor(light) * getLightIntensity(light); +} + +<@endif@> + +<@endfunc@> + diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index 8b801427df..6d5dd01d8f 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -6,6 +6,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@if not SUBSURFACE_SCATTERING_SLH@> +<@def SUBSURFACE_SCATTERING_SLH@> <@func declareSubsurfaceScatteringProfileSource()@> @@ -174,12 +176,14 @@ bool showCurvature() { bool showDiffusedNormal() { return parameters.debugFlags.y > 0.0; } -float unpackCurvatureSigned(float packedCurvature) { - return (packedCurvature * 2 - 1) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; + + +float tuneCurvatureUnsigned(float curvature) { + return abs(curvature) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; } float unpackCurvature(float packedCurvature) { - return abs(packedCurvature * 2 - 1) * parameters.curvatureInfo.y + parameters.curvatureInfo.x; + return (packedCurvature * 2 - 1); } vec3 evalScatteringBentNdotL(vec3 normal, vec3 midNormal, vec3 lowNormal, vec3 lightDir) { @@ -197,3 +201,26 @@ vec3 evalScatteringBentNdotL(vec3 normal, vec3 midNormal, vec3 lowNormal, vec3 l <@endfunc@> + +<@func declareSubsurfaceScatteringBRDF()@> +<$declareSubsurfaceScatteringResource()$> + +vec3 evalSkinBRDF(vec3 lightDir, vec3 normal, vec3 midNormal, vec3 lowNormal, float curvature) { + if (showDiffusedNormal()) { + return lowNormal * 0.5 + vec3(0.5); + } + if (showCurvature()) { + return (curvature > 0 ? vec3(curvature, 0.0, 0.0) : vec3(0.0, 0.0, -curvature)); + } + + vec3 bentNdotL = evalScatteringBentNdotL(normal, midNormal, lowNormal, lightDir); + + float tunedCurvature = tuneCurvatureUnsigned(curvature); + + vec3 brdf = fetchBRDFSpectrum(bentNdotL, tunedCurvature); + return brdf; +} + +<@endfunc@> + +<@endif@> \ No newline at end of file diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index 156ff44f69..89663dbfa4 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -16,8 +16,7 @@ <@include DeferredGlobalLight.slh@> <$declareEvalLightmappedColor()$> -<$declareEvalAmbientSphereGlobalColor()$> -<$declareEvalAmbientSphereGlobalColorScattering()$> +<$declareEvalAmbientSphereGlobalColor(supportScattering)$> in vec2 _texCoord0; @@ -52,10 +51,10 @@ void main(void) { frag.position.xyz, frag.normal, frag.diffuse, + frag.roughness, frag.scattering, blurredCurvature, - diffusedCurvature, - frag.roughness); + diffusedCurvature); _fragColor = vec4(color, 1.0); } else { vec3 color = evalAmbientSphereGlobalColor( diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index d019e29866..e2183887a5 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -16,8 +16,7 @@ <@include DeferredGlobalLight.slh@> <$declareEvalLightmappedColor()$> -<$declareEvalSkyboxGlobalColor()$> -<$declareEvalSkyboxGlobalColorScattering()$> +<$declareEvalSkyboxGlobalColor(supportScattering)$> in vec2 _texCoord0; out vec4 _fragColor; @@ -52,10 +51,10 @@ void main(void) { frag.position.xyz, frag.normal, frag.diffuse, + frag.roughness, frag.scattering, blurredCurvature, - diffusedCurvature, - frag.roughness); + diffusedCurvature); _fragColor = vec4(color, 1.0); } else { vec3 color = evalSkyboxGlobalColor( diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index 0b881e8e29..7ee65e27e3 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -17,7 +17,7 @@ <$declareDeferredCurvature()$> <@include SubsurfaceScattering.slh@> -<$declareSubsurfaceScatteringResource()$> +<$declareSubsurfaceScatteringBRDF()$> in vec2 varTexCoord0; out vec4 _fragColor; @@ -32,6 +32,9 @@ vec3 evalScatteringBRDF(vec2 texcoord) { vec3 normal = fragment.normal; // .getWorldNormal(varTexCoord0); vec4 blurredCurvature = fetchCurvature(texcoord); vec4 diffusedCurvature = fetchDiffusedCurvature(texcoord); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + float curvature = unpackCurvature(diffusedCurvature.w); // Transform directions to worldspace @@ -44,22 +47,8 @@ vec3 evalScatteringBRDF(vec2 texcoord) { vec3 fragLightDir = -normalize(getLightDirection(light)); - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float curvature = unpackCurvature(diffusedCurvature.w); - if (showDiffusedNormal()) { - return diffusedCurvature.xyz; - return lowNormal * 0.5 + vec3(0.5); - } - if (showCurvature()) { - float curvatureSigned = unpackCurvatureSigned(diffusedCurvature.w); - return (curvatureSigned > 0 ? vec3(curvatureSigned, 0.0, 0.0) : vec3(0.0, 0.0, -curvatureSigned)); - } - - vec3 bentNdotL = evalScatteringBentNdotL(fragNormal, midNormal, lowNormal, fragLightDir); - - vec3 brdf = fetchBRDFSpectrum(bentNdotL, curvature); + vec3 brdf = evalSkinBRDF(fragLightDir, fragNormal, midNormal, lowNormal, curvature); return brdf; } @@ -70,7 +59,9 @@ vec3 drawScatteringTableUV(vec2 cursor, vec2 texcoord) { vec3 normal = fragment.normal; // .getWorldNormal(varTexCoord0); vec4 blurredCurvature = fetchCurvature(cursor); vec4 diffusedCurvature = fetchDiffusedCurvature(cursor); - + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + float curvature = unpackCurvature(diffusedCurvature.w); // Get light Light light = getLight(); @@ -78,10 +69,6 @@ vec3 drawScatteringTableUV(vec2 cursor, vec2 texcoord) { vec3 fragLightDir = -normalize(getLightDirection(light)); - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float curvature = unpackCurvature(diffusedCurvature.w); - vec3 bentNdotL = evalScatteringBentNdotL(normal, midNormal, lowNormal, fragLightDir); // return clamp(bentNdotL * 0.5 + 0.5, 0.0, 1.0); From b3513b543c50899edc63c9fda74bd386e4916e27 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 18:23:59 -0700 Subject: [PATCH 0907/1237] fixed snapshot path setting --- .../hifi/dialogs/AvatarPreferencesDialog.qml | 2 +- interface/src/ui/PreferencesDialog.cpp | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml index 86f195612c..45414cfaf8 100644 --- a/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml @@ -7,7 +7,7 @@ PreferencesDialog { id: root objectName: "AvatarPreferencesDialog" title: "Avatar Settings" - showCategories: [ "Avatar Basics", "Avatar Tuning", "Avatar Camera" ] + showCategories: [ "Avatar Basics", "Snapshots", "Avatar Tuning", "Avatar Camera" ] property var settings: Settings { category: root.objectName property alias x: root.x diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index c1705da206..146df549dd 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -35,7 +35,7 @@ void setupPreferences() { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); static const QString AVATAR_BASICS { "Avatar Basics" }; { - auto getter = [=]()->QString {return myAvatar->getDisplayName(); }; + auto getter = [=]()->QString { return myAvatar->getDisplayName(); }; auto setter = [=](const QString& value) { myAvatar->setDisplayName(value); }; auto preference = new EditPreference(AVATAR_BASICS, "Avatar display name (optional)", getter, setter); preference->setPlaceholderText("Not showing a name"); @@ -43,7 +43,7 @@ void setupPreferences() { } { - auto getter = [=]()->QString {return myAvatar->getCollisionSoundURL(); }; + auto getter = [=]()->QString { return myAvatar->getCollisionSoundURL(); }; auto setter = [=](const QString& value) { myAvatar->setCollisionSoundURL(value); }; auto preference = new EditPreference(AVATAR_BASICS, "Avatar collision sound URL (optional)", getter, setter); preference->setPlaceholderText("Enter the URL of a sound to play when you bump into something"); @@ -56,20 +56,24 @@ void setupPreferences() { auto preference = new AvatarPreference(AVATAR_BASICS, "Appearance", getter, setter); preferences->addPreference(preference); } + { - auto getter = [=]()->bool {return myAvatar->getSnapTurn(); }; + auto getter = [=]()->bool { return myAvatar->getSnapTurn(); }; auto setter = [=](bool value) { myAvatar->setSnapTurn(value); }; preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Snap turn when in HMD", getter, setter)); } { - auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenMoving(); }; + auto getter = [=]()->bool { return myAvatar->getClearOverlayWhenMoving(); }; auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } + + // Snapshots + static const QString SNAPSHOTS { "Snapshots" }; { - auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; - auto setter = [](const QString& value) { Snapshot::snapshotsLocation.set(value); }; - auto preference = new BrowsePreference("Snapshots", "Put my snapshots here", getter, setter); + auto getter = [=]()->QString { return Snapshot::snapshotsLocation.get(); }; + auto setter = [=](const QString& value) { Snapshot::snapshotsLocation.set(value); }; + auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } @@ -85,7 +89,7 @@ void setupPreferences() { })); { - auto getter = []()->bool {return !Menu::getInstance()->isOptionChecked(MenuOption::DisableActivityLogger); }; + auto getter = []()->bool { return !Menu::getInstance()->isOptionChecked(MenuOption::DisableActivityLogger); }; auto setter = [](bool value) { Menu::getInstance()->setIsOptionChecked(MenuOption::DisableActivityLogger, !value); }; preferences->addPreference(new CheckPreference("Privacy", "Send data", getter, setter)); } @@ -184,7 +188,7 @@ void setupPreferences() { static const QString AUDIO("Audio"); { - auto getter = []()->bool {return DependencyManager::get()->getReceivedAudioStream().getDynamicJitterBuffers(); }; + auto getter = []()->bool { return DependencyManager::get()->getReceivedAudioStream().getDynamicJitterBuffers(); }; auto setter = [](bool value) { DependencyManager::get()->getReceivedAudioStream().setDynamicJitterBuffers(value); }; preferences->addPreference(new CheckPreference(AUDIO, "Enable dynamic jitter buffers", getter, setter)); } @@ -207,7 +211,7 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = []()->bool {return DependencyManager::get()->getReceivedAudioStream().getUseStDevForJitterCalc(); }; + auto getter = []()->bool { return DependencyManager::get()->getReceivedAudioStream().getUseStDevForJitterCalc(); }; auto setter = [](bool value) { DependencyManager::get()->getReceivedAudioStream().setUseStDevForJitterCalc(value); }; preferences->addPreference(new CheckPreference(AUDIO, "Use standard deviation for dynamic jitter calc", getter, setter)); } @@ -236,7 +240,7 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = []()->bool {return DependencyManager::get()->getReceivedAudioStream().getRepetitionWithFade(); }; + auto getter = []()->bool { return DependencyManager::get()->getReceivedAudioStream().getRepetitionWithFade(); }; auto setter = [](bool value) { DependencyManager::get()->getReceivedAudioStream().setRepetitionWithFade(value); }; preferences->addPreference(new CheckPreference(AUDIO, "Repetition with fade", getter, setter)); } @@ -250,7 +254,7 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = []()->bool {return DependencyManager::get()->getOutputStarveDetectionEnabled(); }; + auto getter = []()->bool { return DependencyManager::get()->getOutputStarveDetectionEnabled(); }; auto setter = [](bool value) { DependencyManager::get()->setOutputStarveDetectionEnabled(value); }; auto preference = new CheckPreference(AUDIO, "Output starve detection (automatic buffer size increase)", getter, setter); preferences->addPreference(preference); From 79864df08ea91abf042bbf20ffc02068d91c9693 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 18:53:39 -0700 Subject: [PATCH 0908/1237] didn't mean to do that --- interface/src/ui/PreferencesDialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 146df549dd..4007c940c3 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -71,8 +71,8 @@ void setupPreferences() { // Snapshots static const QString SNAPSHOTS { "Snapshots" }; { - auto getter = [=]()->QString { return Snapshot::snapshotsLocation.get(); }; - auto setter = [=](const QString& value) { Snapshot::snapshotsLocation.set(value); }; + auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; + auto setter = [](const QString& value) { Snapshot::snapshotsLocation.set(value); }; auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } From 431043e1957e78ccce079e78fe8945e9f3fb3886 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 10:50:51 -0700 Subject: [PATCH 0909/1237] declare new shape types --- libraries/entities/src/EntityItemProperties.cpp | 4 ++++ libraries/shared/src/ShapeInfo.h | 2 ++ 2 files changed, 6 insertions(+) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index a62f4b182a..dcd7e25bc1 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -101,6 +101,8 @@ const char* shapeTypeNames[] = { "hull", "plane", "compound", + "simple-hull", + "simple-compound", "static-mesh" }; @@ -123,6 +125,8 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_HULL); addShapeType(SHAPE_TYPE_PLANE); addShapeType(SHAPE_TYPE_COMPOUND); + addShapeType(SHAPE_TYPE_SIMPLE_HULL); + addShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); addShapeType(SHAPE_TYPE_STATIC_MESH); } diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 96132a4b23..7bf145412a 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -39,6 +39,8 @@ enum ShapeType { SHAPE_TYPE_HULL, SHAPE_TYPE_PLANE, SHAPE_TYPE_COMPOUND, + SHAPE_TYPE_SIMPLE_HULL, + SHAPE_TYPE_SIMPLE_COMPOUND, SHAPE_TYPE_STATIC_MESH }; From 33d732c446771cc7a9cf575926e324d715075581 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 10:51:15 -0700 Subject: [PATCH 0910/1237] construct geometry for new shape types --- .../src/RenderableModelEntityItem.cpp | 144 ++++++++++-------- 1 file changed, 83 insertions(+), 61 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 43fea75eac..e1c944c8dd 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -423,7 +423,8 @@ void RenderableModelEntityItem::render(RenderArgs* args) { // check to see if when we added our models to the scene they were ready, if they were not ready, then // fix them up in the scene - bool shouldShowCollisionHull = (args->_debugFlags & (int)RenderArgs::RENDER_DEBUG_HULLS) > 0; + bool shouldShowCollisionHull = (args->_debugFlags & (int)RenderArgs::RENDER_DEBUG_HULLS) > 0 + && getShapeType() == SHAPE_TYPE_COMPOUND; if (_model->needsFixupInScene() || _showCollisionHull != shouldShowCollisionHull) { _showCollisionHull = shouldShowCollisionHull; render::PendingChanges pendingChanges; @@ -691,7 +692,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } } info.setParams(type, dimensions, _compoundShapeURL); - } else if (type == SHAPE_TYPE_STATIC_MESH) { + assert(pointCollection.size() > 0); // adebug + } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { + updateModelBounds(); + + // should never fall in here when model not fully loaded + assert(_model->isLoaded()); + // compute meshPart local transforms QVector localTransforms; const FBXGeometry& geometry = _model->getFBXGeometry(); @@ -716,30 +723,42 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { return; } - updateModelBounds(); - - // should never fall in here when collision model not fully loaded - assert(_model->isLoaded()); + auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); + int32_t numMeshes = (int32_t)(meshes.size()); ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); pointCollection.clear(); - - ShapeInfo::PointList points; - ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); - auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); + if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { + pointCollection.resize(numMeshes); + } else { + pointCollection.resize(1); + } Extents extents; int meshCount = 0; + int pointListIndex = 0; for (auto& mesh : meshes) { const gpu::BufferView& vertices = mesh->getVertexBuffer(); const gpu::BufferView& indices = mesh->getIndexBuffer(); const gpu::BufferView& parts = mesh->getPartBuffer(); + ShapeInfo::PointList& points = pointCollection[pointListIndex]; + + // reserve room + int32_t sizeToReserve = (int32_t)(vertices.getNumElements()); + if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { + // a list of points for each mesh + pointListIndex++; + } else { + // only one list of points + sizeToReserve += (int32_t)((gpu::Size)points.size()); + } + points.reserve(sizeToReserve); + // copy points - const glm::mat4& localTransform = localTransforms[meshCount]; uint32_t meshIndexOffset = (uint32_t)points.size(); + const glm::mat4& localTransform = localTransforms[meshCount]; gpu::BufferView::Iterator vertexItr = vertices.cbegin(); - points.reserve((int32_t)((gpu::Size)points.size() + vertices.getNumElements())); while (vertexItr != vertices.cend()) { glm::vec3 point = extractTranslation(localTransform * glm::translate(*vertexItr)); points.push_back(point); @@ -747,55 +766,57 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ++vertexItr; } - // copy triangleIndices - triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); - gpu::BufferView::Iterator partItr = parts.cbegin(); - while (partItr != parts.cend()) { - - if (partItr->_topology == model::Mesh::TRIANGLES) { - assert(partItr->_numIndices % 3 == 0); - auto indexItr = indices.cbegin() + partItr->_startIndex; - auto indexEnd = indexItr + partItr->_numIndices; - while (indexItr != indexEnd) { - triangleIndices.push_back(*indexItr + meshIndexOffset); - ++indexItr; - } - } else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) { - assert(partItr->_numIndices > 2); - uint32_t approxNumIndices = 3 * partItr->_numIndices; - if (approxNumIndices > (uint32_t)(triangleIndices.capacity() - triangleIndices.size())) { - // we underestimated the final size of triangleIndices so we pre-emptively expand it - triangleIndices.reserve(triangleIndices.size() + approxNumIndices); - } - - auto indexItr = indices.cbegin() + partItr->_startIndex; - auto indexEnd = indexItr + (partItr->_numIndices - 2); - - // first triangle uses the first three indices - triangleIndices.push_back(*(indexItr++) + meshIndexOffset); - triangleIndices.push_back(*(indexItr++) + meshIndexOffset); - triangleIndices.push_back(*(indexItr++) + meshIndexOffset); - - // the rest use previous and next index - uint32_t triangleCount = 1; - while (indexItr != indexEnd) { - if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) { - if (triangleCount % 2 == 0) { - // even triangles use first two indices in order - triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); - triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); - } else { - // odd triangles swap order of first two indices - triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); - triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); - } + if (type == SHAPE_TYPE_STATIC_MESH) { + // copy into triangleIndices + ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); + gpu::BufferView::Iterator partItr = parts.cbegin(); + while (partItr != parts.cend()) { + if (partItr->_topology == model::Mesh::TRIANGLES) { + assert(partItr->_numIndices % 3 == 0); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + partItr->_numIndices; + while (indexItr != indexEnd) { triangleIndices.push_back(*indexItr + meshIndexOffset); - ++triangleCount; + ++indexItr; + } + } else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) { + assert(partItr->_numIndices > 2); + uint32_t approxNumIndices = 3 * partItr->_numIndices; + if (approxNumIndices > (uint32_t)(triangleIndices.capacity() - triangleIndices.size())) { + // we underestimated the final size of triangleIndices so we pre-emptively expand it + triangleIndices.reserve(triangleIndices.size() + approxNumIndices); + } + + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + (partItr->_numIndices - 2); + + // first triangle uses the first three indices + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + + // the rest use previous and next index + uint32_t triangleCount = 1; + while (indexItr != indexEnd) { + if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) { + if (triangleCount % 2 == 0) { + // even triangles use first two indices in order + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + } else { + // odd triangles swap order of first two indices + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + } + triangleIndices.push_back(*indexItr + meshIndexOffset); + ++triangleCount; + } + ++indexItr; } - ++indexItr; } + ++partItr; } - ++partItr; } ++meshCount; } @@ -808,12 +829,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { scaleToFit[i] = 1.0f; } } - for (int i = 0; i < points.size(); ++i) { - points[i] = (points[i] * scaleToFit); + for (auto points : pointCollection) { + for (int i = 0; i < points.size(); ++i) { + points[i] = (points[i] * scaleToFit); + } } - pointCollection.push_back(points); - info.setParams(SHAPE_TYPE_STATIC_MESH, 0.5f * dimensions, _modelURL); + info.setParams(type, 0.5f * dimensions, _modelURL); } else { ModelEntityItem::computeShapeInfo(info); info.setParams(type, 0.5f * dimensions); From 97fc5ac3b8aab8a5b21f9762e11d69c3e141b86c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 10:51:28 -0700 Subject: [PATCH 0911/1237] construct Bullet shapes for new shape types --- libraries/physics/src/ShapeFactory.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 3afc170a8c..f71711eccd 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -204,7 +204,7 @@ btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info) { if (numIndices < INT16_MAX) { int16_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); for (int32_t i = 0; i < numIndices; ++i) { - indices[i] = triangleIndices[i]; + indices[i] = (int16_t)triangleIndices[i]; } } else { int32_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); @@ -257,7 +257,9 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { shape = new btCapsuleShape(radius, height); } break; - case SHAPE_TYPE_COMPOUND: { + case SHAPE_TYPE_COMPOUND: + case SHAPE_TYPE_SIMPLE_HULL: + case SHAPE_TYPE_SIMPLE_COMPOUND: { const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); uint32_t numSubShapes = info.getNumSubShapes(); if (numSubShapes == 1) { From 76cb80112e9672f2619eedfd6a36a578d658f5c6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 10:51:43 -0700 Subject: [PATCH 0912/1237] support new shape types in edit.js --- scripts/system/html/entityProperties.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 121e38c340..54c79b1d9f 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1646,6 +1646,8 @@ + +
From 92af84920420a03b5591a2d6786e01e40aa0175c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 10:52:01 -0700 Subject: [PATCH 0913/1237] bump version for new shape types --- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index c74b10820d..6359ad0aff 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH; + return VERSION_MODEL_ENTITIES_SUPPORT_SIMPLE_HULLS; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e484a06502..9140cf8738 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -181,6 +181,7 @@ const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH = 61; +const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_SIMPLE_HULLS = 62; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, From 6cbc566a771ad3fd8b9c1fdb8a0c0617fe188f73 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 11:53:26 -0700 Subject: [PATCH 0914/1237] remove debug cruft --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index e1c944c8dd..840eace27e 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -692,7 +692,6 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } } info.setParams(type, dimensions, _compoundShapeURL); - assert(pointCollection.size() > 0); // adebug } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { updateModelBounds(); From 7d803b7513d4b177d92177e1e52eae7d3e914698 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 6 Jul 2016 11:54:39 -0700 Subject: [PATCH 0915/1237] Fix model overlays not correctly scaling --- interface/src/ui/overlays/ModelOverlay.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index adf08934f0..6b81ad7ff3 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -45,7 +45,7 @@ void ModelOverlay::update(float deltatime) { _updateModel = false; _model->setSnapModelToCenter(true); - _model->setScale(getDimensions()); + _model->setScaleToFit(true, getDimensions()); _model->setRotation(getRotation()); _model->setTranslation(getPosition()); _model->setURL(_url); @@ -100,7 +100,6 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { if (newScale.x <= 0 || newScale.y <= 0 || newScale.z <= 0) { setDimensions(scale); } else { - _model->setScaleToFit(true, getDimensions()); _updateModel = true; } } From 6a44ea76e541b22bd9bfdaec392f4a5e193bdf95 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 6 Jul 2016 11:57:29 -0700 Subject: [PATCH 0916/1237] fixed snapshot preview on windows --- scripts/system/notifications.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 988d2998d7..98a31d708c 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -247,14 +247,14 @@ function notify(notice, button, height, imageProperties, image) { noticeHeight = notice.height * NOTIFICATION_3D_SCALE; notice.size = { x: noticeWidth, y: noticeHeight }; - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; notice.isFacingAvatar = false; @@ -289,8 +289,8 @@ function notify(notice, button, height, imageProperties, image) { height = height + 1.0; heights.push(height); times.push(new Date().getTime() / 1000); - myAlpha.push(0); last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); fadeIn(notifications[last], buttons[last]); @@ -301,9 +301,11 @@ function notify(notice, button, height, imageProperties, image) { y: notice.y + height, width: notice.width, height: imageHeight, - topMargin: 0, - leftMargin: 0, - imageURL: imageProperties.path + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255}, + visible: true, + imageURL: imageProperties.path, + alpha: backgroundAlpha }; notify(notice, button, imageHeight, imageProperties, true); } @@ -336,10 +338,6 @@ function createNotification(text, notificationType, imageProperties) { level = (stack + 20.0); height = height + extraLine; - if (imageProperties && imageProperties.path) { - imageProperties.path = Script.resolvePath(imageProperties.path); - } - noticeProperties = { x: overlayLocationX, y: level, @@ -397,7 +395,7 @@ function stringDivider(str, slotWidth, spaceReplacer) { if (str.length > slotWidth && slotWidth > 0) { left = str.substring(0, slotWidth); - right = str.substring(slotWidth + 1); + right = str.substring(slotWidth); return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); } return str; @@ -531,7 +529,7 @@ function onDomainConnectionRefused(reason) { function onSnapshotTaken(path) { var imageProperties = { - path: path, + path: Script.resolvePath("file:///" + path), aspectRatio: Window.innerWidth / Window.innerHeight } createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); From 890b22426c44b5773257c483d17e00e86809aab9 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 6 Jul 2016 12:15:34 -0700 Subject: [PATCH 0917/1237] added folder selection dialog on first snapshot --- interface/src/ui/Snapshot.cpp | 10 +++++++++- interface/src/ui/Snapshot.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index a3af742f92..aaf11d14a4 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -44,6 +44,7 @@ const QString URL = "highfidelity_url"; Setting::Handle Snapshot::snapshotsLocation("snapshotsLocation", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); +Setting::Handle Snapshot::hasSetSnapshotsLocation("hasSetSnapshotsLocation", false); SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) { @@ -103,7 +104,14 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { const int IMAGE_QUALITY = 100; if (!isTemporary) { - QString snapshotFullPath = snapshotsLocation.get(); + QString snapshotFullPath; + if (!hasSetSnapshotsLocation.get()) { + snapshotFullPath = QFileDialog::getExistingDirectory(nullptr, "Choose Snapshots Directory", snapshotsLocation.get()); + hasSetSnapshotsLocation.set(true); + snapshotsLocation.set(snapshotFullPath); + } else { + snapshotFullPath = snapshotsLocation.get(); + } if (!snapshotFullPath.endsWith(QDir::separator())) { snapshotFullPath.append(QDir::separator()); diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 5856743141..2e7986a5c0 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -39,6 +39,7 @@ public: static SnapshotMetaData* parseSnapshotData(QString snapshotPath); static Setting::Handle snapshotsLocation; + static Setting::Handle hasSetSnapshotsLocation; private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; From 2d073cc99c2b50b042c69af331989efd82bbc774 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 12:31:03 -0700 Subject: [PATCH 0918/1237] working teleporter in huffmans build --- scripts/system/controllers/teleport.js | 179 +++++++------------------ 1 file changed, 45 insertions(+), 134 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 4630b234bc..d4ad82721d 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -104,17 +104,10 @@ function Teleporter() { this.createTargetOverlay = function() { print('creating target overlay') - var cameraEuler = Quat.safeEulerAngles(Camera.orientation); - var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { - x: 0, - y: 1, - z: 0 - }); + var targetOverlayProps = { url: TARGET_MODEL_URL, - position: MyAvatar.position, - rotation: towardsMe, dimensions: TARGET_MODEL_DIMENSIONS, visible: true, }; @@ -122,6 +115,15 @@ function Teleporter() { _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); }; + this.rotateTargetTorwardMe = function() { + var cameraEuler = Quat.safeEulerAngles(Camera.orientation); + var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { + x: 0, + y: 1, + z: 0 + }); + } + this.createMappings = function() { print('jbp create mappings internal'); @@ -157,37 +159,45 @@ function Teleporter() { return midpoint }; + this.updateOrCreateStretchyBeam = function(handPosition, intersection, rotation, direction, noIntersection) { - this.createStretchyBeam = function(handPosition, intersection, rotation) { + if (_this.stretchyBeam === null) { + var beamProps = { + url: BEAM_MODEL_URL, + position: MyAvatar.position, - var beamProps = { - url: BEAM_MODEL_URL, - position: _this.findMidpoint(handPosition, intersection), - dimensions: { - x: STRETCHY_BEAM_DIMENSIONS_X, - y: STRETCHY_BEAM_DIMENSIONS_Y, - z: 0.1 - }, - ignoreRayIntersection: true, - }; + }; - _this.stretchyBeam = Overlays.addOverlay("model", beamProps); - }; + _this.stretchyBeam = Overlays.addOverlay("model", beamProps); + } - - this.updateStretchyBeam = function(handPosition, intersection, rotation) { var dimensions = { x: STRETCHY_BEAM_DIMENSIONS_X, y: STRETCHY_BEAM_DIMENSIONS_Y, - z: Vec3.distance(handPosition, intersection.intersection) + z: intersection !== null ? Vec3.distance(handPosition, intersection.intersection) : STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION }; + var position; + if (noIntersection === true) { + this.deleteTargetOverlay(); + print('no intersection') + position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION/2 , direction)); + } else { + print('intersection, find midpoint') + position = _this.findMidpoint(handPosition, intersection); + } + + + print('rotation: ' + JSON.stringify(rotation)); + print('position: ' + JSON.stringify(position)); + print('dimensions: ' + JSON.stringify(dimensions)); + + - var position = _this.findMidpoint(handPosition, intersection); Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, - rotation: Quat.multiply(rotation, Quat.angleAxis(180, { + rotation: Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, y: 1, z: 0 @@ -203,82 +213,6 @@ function Teleporter() { } }; - this.createNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { - - var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; - - var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); - - var midpoint = this.findMidpoint(handPosition, { - intersection: ahead - }); - - var dimensions = { - x: 0.04, - y: 0.04, - z: 0.1 - }; - - - var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { - x: 0, - y: 1, - z: 0 - })); - - var beamProps = { - // dimensions: dimensions, - url: BEAM_MODEL_URL_NO_INTERSECTION, - position: midpoint, - rotation: finalRotation, - ignoreRayIntersection: true, - }; - - this.noIntersectionStretchyBeam = Overlays.addOverlay("model", beamProps); - - - }; - - this.updateNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { - - var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; - - var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); - - var midpoint = this.findMidpoint(handPosition, { - intersection: ahead - }); - - var dimensions = { - x: STRETCHY_BEAM_DIMENSIONS_X, - y: STRETCHY_BEAM_DIMENSIONS_Y, - z: Vec3.distance(handPosition, ahead) - }; - dimensions = Vec3.multiply(10, dimensions) - print('dimensions in update:: ' + JSON.stringify(dimensions)); - - - var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { - x: 0, - y: 1, - z: 0 - })); - - var goodEdit = Overlays.editOverlay(_this.noIntersectionStretchyBeam, { - dimensions: dimensions, - position: midpoint, - rotation: rotation - }) - - }; - - - this.deleteNoIntersectionStretchyBeam = function() { - if (_this.noIntersectionStretchyBeam !== null) { - Overlays.deleteOverlay(_this.noIntersectionStretchyBeam); - _this.noIntersectionStretchyBeam = null; - } - }; this.createFadeSphere = function(avatarHead) { var sphereProps = { @@ -364,7 +298,6 @@ function Teleporter() { this.updateConnected = null; this.disableMappings(); this.deleteStretchyBeam(); - this.deleteNoIntersectionStretchyBeam(); this.deleteTargetOverlay(); this.enableGrab(); Script.setTimeout(function() { @@ -417,27 +350,17 @@ function Teleporter() { var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - this.deleteNoIntersectionStretchyBeam(); if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { this.createTargetOverlay(); } - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); - } else { - this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); - } + this.updateOrCreateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal, rightPickRay.direction, false); } else { - this.deleteTargetOverlay(); - this.deleteStretchyBeam(); - if (this.noIntersectionStretchyBeam !== null) { - this.updateNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); - } else { - this.createNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); - } + this.updateOrCreateStretchyBeam(rightPickRay.origin, null, rightFinal, rightPickRay.direction, true); + } } @@ -452,7 +375,6 @@ function Teleporter() { direction: Quat.getUp(leftRotation), }; - var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(90, { x: 1, y: 0, @@ -466,28 +388,18 @@ function Teleporter() { var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { - this.deleteNoIntersectionStretchyBeam(); if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { this.createTargetOverlay(); } - - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); - } else { - this.createStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); - - } + this.updateOrCreateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal, leftPickRay.direction, false); } else { - this.deleteTargetOverlay(); - this.deleteStretchyBeam(); - if (this.noIntersectionStretchyBeam !== null) { - this.updateNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); - } else { - this.createNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); - } + + this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); + + } }; @@ -496,7 +408,7 @@ function Teleporter() { this.intersection = intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y + 0.1, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y/2, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { @@ -611,7 +523,6 @@ function cleanup() { teleporter.disableMappings(); teleporter.deleteStretchyBeam(); teleporter.deleteTargetOverlay(); - teleporter.deleteNoIntersectionStretchyBeam(); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From 5659d57ac3f6271bfae79cf395c9f815ff229ba5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 13:00:57 -0700 Subject: [PATCH 0919/1237] fix shape generation for SIMPLE_COMPOUND --- .../src/RenderableModelEntityItem.cpp | 10 +++++----- libraries/shared/src/ShapeInfo.cpp | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 840eace27e..7eb7d87566 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -608,7 +608,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // should never fall in here when collision model not fully loaded // hence we assert that all geometries exist and are loaded - assert(_model->isLoaded() && _model->isCollisionLoaded()); + assert(_model && _model->isLoaded() && _model->isCollisionLoaded()); const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry(); ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); @@ -696,15 +696,15 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { updateModelBounds(); // should never fall in here when model not fully loaded - assert(_model->isLoaded()); + assert(_model && _model->isLoaded()); // compute meshPart local transforms QVector localTransforms; - const FBXGeometry& geometry = _model->getFBXGeometry(); - int numberOfMeshes = geometry.meshes.size(); + const FBXGeometry& fbxGeometry = _model->getFBXGeometry(); + int numberOfMeshes = fbxGeometry.meshes.size(); int totalNumVertices = 0; for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& mesh = geometry.meshes.at(i); + const FBXMesh& mesh = fbxGeometry.meshes.at(i); if (mesh.clusters.size() > 0) { const FBXCluster& cluster = mesh.clusters.at(0); auto jointMatrix = _model->getRig()->getJointTransform(cluster.jointIndex); diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index e0f4cc18b2..424c2bfa22 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -83,12 +83,19 @@ void ShapeInfo::setOffset(const glm::vec3& offset) { } uint32_t ShapeInfo::getNumSubShapes() const { - if (_type == SHAPE_TYPE_NONE) { - return 0; - } else if (_type == SHAPE_TYPE_COMPOUND) { - return _pointCollection.size(); + switch (_type) { + case SHAPE_TYPE_NONE: + return 0; + case SHAPE_TYPE_COMPOUND: + case SHAPE_TYPE_SIMPLE_COMPOUND: + return _pointCollection.size(); + case SHAPE_TYPE_SIMPLE_HULL: + case SHAPE_TYPE_STATIC_MESH: + assert(_pointCollection.size() == 1); + // yes fall through to default + default: + return 1; } - return 1; } int ShapeInfo::getLargestSubshapePointCount() const { From b2ef491b973ff704567180bbfae22ef94805a027 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 13:20:42 -0700 Subject: [PATCH 0920/1237] rotate target toward you --- scripts/system/controllers/teleport.js | 73 ++++++++++++++------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index d4ad82721d..e45b2f3127 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -26,28 +26,6 @@ //v?: haptic feedback //v?: show room boundaries when choosing a place to teleport -var inTeleportMode = false; - -var currentFadeSphereOpacity = 1; -var fadeSphereInterval = null; -//milliseconds between fading one-tenth -- so this is a half second fade total -var FADE_IN_INTERVAL = 50; -var FADE_OUT_INTERVAL = 50; - -var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; -var BEAM_MODEL_URL_NO_INTERSECTION = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam2.fbx"; - - -var STRETCHY_BEAM_DIMENSIONS_X = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; - -var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; -var TARGET_MODEL_DIMENSIONS = { - x: 1.15, - y: 0.5, - z: 1.15 -}; //swirly thing, not sure we'll get to use it @@ -59,6 +37,29 @@ var TARGET_MODEL_DIMENSIONS = { // z: 1.22 // }; + +var inTeleportMode = false; + +var currentFadeSphereOpacity = 1; +var fadeSphereInterval = null; +//milliseconds between fading one-tenth -- so this is a half second fade total +var FADE_IN_INTERVAL = 50; +var FADE_OUT_INTERVAL = 50; + +var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; + +var STRETCHY_BEAM_DIMENSIONS_X = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; + +var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; +var TARGET_MODEL_DIMENSIONS = { + x: 1.15, + y: 0.5, + z: 1.15 + +}; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -117,11 +118,15 @@ function Teleporter() { this.rotateTargetTorwardMe = function() { var cameraEuler = Quat.safeEulerAngles(Camera.orientation); + print('camera euler' + JSON.stringify(cameraEuler)) var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { x: 0, y: 1, z: 0 }); + + print('towardsMe' + JSON.stringify(towardsMe)) + return towardsMe } @@ -179,25 +184,18 @@ function Teleporter() { var position; if (noIntersection === true) { - this.deleteTargetOverlay(); + this.deleteTargetOverlay(); print('no intersection') - position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION/2 , direction)); + position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION / 2, direction)); } else { print('intersection, find midpoint') position = _this.findMidpoint(handPosition, intersection); } - - print('rotation: ' + JSON.stringify(rotation)); - print('position: ' + JSON.stringify(position)); - print('dimensions: ' + JSON.stringify(dimensions)); - - - Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, - rotation: Quat.multiply(rotation, Quat.angleAxis(180, { + rotation: Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, y: 1, z: 0 @@ -406,14 +404,21 @@ function Teleporter() { this.updateTargetOverlay = function(intersection) { this.intersection = intersection; + print('intersection is: ' + JSON.stringify(intersection)) + + // var rotation = Quat.getUp(intersection.intersection.surfaceNormal); + + var rotation = Quat.lookAt(intersection.intersection,MyAvatar.position,Vec3.UP) + var euler = Quat.safeEulerAngles(rotation) var position = { x: intersection.intersection.x, - y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y/2, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { position: position, - visible: true + rotation:Quat.fromPitchYawRollDegrees(0,euler.y,0), + // rotation: this.rotateTargetTorwardMe() }); }; From 8ef9f48221fae9a8978882140319d15e722c73b7 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 6 Jul 2016 13:42:46 -0700 Subject: [PATCH 0921/1237] An objective test of basic performance on struggling machines. --- scripts/developer/tests/loadedMachine.js | 163 +++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 scripts/developer/tests/loadedMachine.js diff --git a/scripts/developer/tests/loadedMachine.js b/scripts/developer/tests/loadedMachine.js new file mode 100644 index 0000000000..c222eb7379 --- /dev/null +++ b/scripts/developer/tests/loadedMachine.js @@ -0,0 +1,163 @@ +"use strict"; +/*jslint vars: true, plusplus: true*/ +/*globals Script, MyAvatar, Quat, Render, ScriptDiscoveryService, Window, LODManager, Entities, print*/ +// +// loadedMachine.js +// scripts/developer/tests/ +// +// Created by Howard Stearns on 6/6/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Characterises the performance of heavily loaded or struggling machines. +// There is no point in running this on a machine that producing the target 60 or 90 hz rendering rate. +// +// The script examines several source of load, including each running script and a couple of Entity types. +// It turns all of these off, as well as LOD management, and twirls in place to get a baseline render rate. +// Then it turns each load on, one at a time, and records a new render rate. +// At the end, it reports the ordered results (in a popup), restores the original loads and LOD management, and quits. +// +// Small performance changes are not meaningful. +// If you think you find something that is significant, you should probably run again to make sure that it isn't random variation. +// You can also compare (e.g., the baseline) for different conditions such as domain, version, server settings, etc. +// This does not profile scripts as they are used -- it merely measures the effect of having the script loaded and running quietly. + +var NUMBER_OF_TWIRLS_PER_LOAD = 10; +var MILLISECONDS_PER_SECOND = 1000; +var WAIT_TIME_MILLISECONDS = MILLISECONDS_PER_SECOND; +var accumulatedRotation = 0; +var start; +var frames = 0; +var config = Render.getConfig("Stats"); +var thisPath = Script.resolvePath(''); +var scriptData = ScriptDiscoveryService.getRunning(); +var scripts = scriptData.filter(function (datum) { return datum.name !== 'defaultScripts.js'; }).map(function (script) { return script.path; }); +// If defaultScripts.js is running, we leave it running, because restarting it safely is a mess. +var otherScripts = scripts.filter(function (path) { return path !== thisPath; }); +var numberLeftRunning = scriptData.length - otherScripts.length; +print('initially running', otherScripts.length, 'scripts. Leaving', numberLeftRunning, 'and stopping', otherScripts); +var typedEntities = {Light: [], ParticleEffect: []}; +var interestingTypes = Object.keys(typedEntities); +var loads = ['ignore', 'baseline'].concat(otherScripts, interestingTypes); +var loadIndex = 0, nLoads = loads.length, load; +var results = []; +var initialLodIsAutomatic = LODManager.getAutomaticLODAdjust(); +var SEARCH_RADIUS = 17000; +var DEFAULT_LOD = 32768 * 400; +LODManager.setAutomaticLODAdjust(false); +LODManager.setOctreeSizeScale(DEFAULT_LOD); + +// Fill the typedEnties with the entityIDs that are already visible. It would be nice if this were more efficient. +var allEntities = Entities.findEntities(MyAvatar.position, SEARCH_RADIUS); +print('Searching', allEntities.length, 'entities for', interestingTypes); +allEntities.forEach(function (entityID) { + var properties = Entities.getEntityProperties(entityID, ['type', 'visible']); + if (properties.visible && (interestingTypes.indexOf(properties.type) >= 0)) { + typedEntities[properties.type].push(entityID); + } +}); +interestingTypes.forEach(function (type) { + print('There are', typedEntities[type].length, type, 'entities.'); +}); +function toggleVisibility(type, on) { + typedEntities[type].forEach(function (entityID) { + Entities.editEntity(entityID, {visible: on}); + }); +} +function restoreOneTest(load) { + print('restore', load); + switch (load) { + case 'baseline': + case 'ignore': // ignore is used to do a twirl to make sure everything is loaded. + break; + case 'Light': + case 'ParticleEffect': + toggleVisibility(load, true); + break; + default: + Script.load(load); + } +} +function undoOneTest(load) { + print('stop', load); + switch (load) { + case 'baseline': + case 'ignore': + break; + case 'Light': + case 'ParticleEffect': + toggleVisibility(load, false); + break; + default: + ScriptDiscoveryService.stopScript(load); + } +} +loads.forEach(undoOneTest); + +function finalReport() { + var baseline = results[0]; + results.forEach(function (result) { + result.ratio = (baseline.fps / result.fps); + }); + results.sort(function (a, b) { return b.ratio - a.ratio; }); + var report = 'Performance Report:\nBaseline: ' + baseline.fps.toFixed(1) + ' fps.'; + results.forEach(function (result) { + report += '\n' + result.ratio.toFixed(2) + ' (' + result.fps.toFixed(1) + ' fps over ' + result.elapsed + ' seconds) for ' + result.name; + }); + Window.alert(report); + LODManager.setAutomaticLODAdjust(initialLodIsAutomatic); + loads.forEach(restoreOneTest); + Script.stop(); +} + +function onNewStats() { // Accumulates frames on signal during load test + frames++; +} +var DEGREES_PER_FULL_TWIRL = 360; +var DEGREES_PER_UPDATE = 6; +function onUpdate() { // Spins on update signal during load test + MyAvatar.orientation = Quat.fromPitchYawRollDegrees(0, accumulatedRotation, 0); + accumulatedRotation += DEGREES_PER_UPDATE; + if (accumulatedRotation >= (DEGREES_PER_FULL_TWIRL * NUMBER_OF_TWIRLS_PER_LOAD)) { + tearDown(); + } +} +function startTest() { + print('start', load); + accumulatedRotation = frames = 0; + start = Date.now(); + Script.update.connect(onUpdate); + config.newStats.connect(onNewStats); +} + +function setup() { + if (loadIndex >= nLoads) { + return finalReport(); + } + load = loads[loadIndex]; + restoreOneTest(load); + Script.setTimeout(startTest, WAIT_TIME_MILLISECONDS); +} +function tearDown() { + var now = Date.now(); + var elapsed = (now - start) / MILLISECONDS_PER_SECOND; + Script.update.disconnect(onUpdate); + config.newStats.disconnect(onNewStats); + if (load !== 'ignore') { + results.push({name: load, fps: frames / elapsed, elapsed: elapsed}); + } + undoOneTest(load); + loadIndex++; + setup(); +} +function waitUntilStopped() { // Wait until all the scripts have stopped + var count = ScriptDiscoveryService.getRunning().length; + if (count === numberLeftRunning) { + return setup(); + } + print('Still', count, 'scripts running'); + Script.setTimeout(waitUntilStopped, WAIT_TIME_MILLISECONDS); +} +waitUntilStopped(); From e993a695195aca298f1898449d2cd0ed0a16073b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 14:06:53 -0700 Subject: [PATCH 0922/1237] last commit for stretched beams -- doesnt work at long distances due to lack of precision --- scripts/system/controllers/teleport.js | 115 +++++++++++-------------- 1 file changed, 48 insertions(+), 67 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index e45b2f3127..0f7403fcc3 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,42 +1,12 @@ -// by james b. pollack @imgntn on 7/2/2016 +// Created by james b. pollack @imgntn on 7/2/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Creates a beam and target and then teleports you there when you let go of the activation button. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -///BUG: there is currently something going on when you try to adjust the dimensions of a second overlay model, it seems to be off by a factor of 10 or already changed or something weird. - -//v1 -//check if trigger is down xxx -//if trigger is down, check if thumb is down xxx -//if both are down, enter 'teleport mode' xxx -//aim controller to change landing spot xxx -//visualize aim + spot (line + circle) xxx -//release trigger to teleport then exit teleport mode xxx -//if thumb is release, exit teleport mode xxx - - -//v2 -// try just thumb to teleport xxx - -//v3 -//fade in/out -//stretchy beam instead of GL line xxx - - -//try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) - -//v?: haptic feedback -//v?: show room boundaries when choosing a place to teleport - - - -//swirly thing, not sure we'll get to use it -// var TARGET_MODEL_URL='http://hifi-content.s3.amazonaws.com/alan/dev/Cyclone-4.fbx'; - -// var TARGET_MODEL_DIMENSIONS = { -// x: 0.93, -// y: 0.93, -// z: 1.22 -// }; - var inTeleportMode = false; @@ -116,20 +86,6 @@ function Teleporter() { _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); }; - this.rotateTargetTorwardMe = function() { - var cameraEuler = Quat.safeEulerAngles(Camera.orientation); - print('camera euler' + JSON.stringify(cameraEuler)) - var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { - x: 0, - y: 1, - z: 0 - }); - - print('towardsMe' + JSON.stringify(towardsMe)) - return towardsMe - } - - this.createMappings = function() { print('jbp create mappings internal'); // peek at the trigger and thumbs to store their values @@ -192,6 +148,14 @@ function Teleporter() { position = _this.findMidpoint(handPosition, intersection); } + + print('AT UPDATE DIMENSIONS: ' + JSON.stringify(dimensions)) + print('AT UPDATE POSITION: ' + JSON.stringify(position)) + print('AT UPDATE ROTATION: ' + JSON.stringify(rotation)) + if (noIntersection === false) { + print('AT UPDATE NORMAL: ' + JSON.stringify(intersection.surfaceNormal)) + + } Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, @@ -325,15 +289,13 @@ function Teleporter() { this.rightRay = function() { + var rightPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).translation), MyAvatar.position); - var rightRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).rotation) + var rightControllerRotation = Controller.getPoseValue(Controller.Standard.RightHand).rotation; + var rightRotation = Quat.multiply(MyAvatar.orientation, rightControllerRotation) - var rightPickRay = { - origin: rightPosition, - direction: Quat.getUp(rightRotation), - }; var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(90, { x: 1, @@ -341,6 +303,11 @@ function Teleporter() { z: 0 })); + var rightPickRay = { + origin: rightPosition, + direction: Quat.getUp(rightRotation), + }; + this.rightPickRay = rightPickRay; var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); @@ -349,6 +316,10 @@ function Teleporter() { if (rightIntersection.intersects) { + if (rightIntersection.distance > 250) { + print('VERY FAR INTERSECTION') + + } if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { @@ -368,10 +339,6 @@ function Teleporter() { var leftRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).rotation) - var leftPickRay = { - origin: leftPosition, - direction: Quat.getUp(leftRotation), - }; var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(90, { x: 1, @@ -379,6 +346,12 @@ function Teleporter() { z: 0 })); + + var leftPickRay = { + origin: leftPosition, + direction: Quat.getUp(leftRotation), + }; + this.leftPickRay = leftPickRay; var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); @@ -386,6 +359,11 @@ function Teleporter() { var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { + if (leftIntersection.distance > 250) { + print('VERY FAR INTERSECTION') + this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); + return + } if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { @@ -404,11 +382,8 @@ function Teleporter() { this.updateTargetOverlay = function(intersection) { this.intersection = intersection; - print('intersection is: ' + JSON.stringify(intersection)) - // var rotation = Quat.getUp(intersection.intersection.surfaceNormal); - - var rotation = Quat.lookAt(intersection.intersection,MyAvatar.position,Vec3.UP) + var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP) var euler = Quat.safeEulerAngles(rotation) var position = { x: intersection.intersection.x, @@ -417,8 +392,7 @@ function Teleporter() { } Overlays.editOverlay(this.targetOverlay, { position: position, - rotation:Quat.fromPitchYawRollDegrees(0,euler.y,0), - // rotation: this.rotateTargetTorwardMe() + rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), }); }; @@ -431,15 +405,22 @@ function Teleporter() { Messages.sendLocalMessage('Hifi-Hand-Disabler', 'none'); }; + this.triggerHaptics = function() { + var hand = this.hand === 'left' ? 0 : 1; + var haptic = Controller.triggerShortHapticPulse(0.2, hand); + }; + this.teleport = function(value) { print('TELEPORT CALLED'); - var offset = getAvatarFootOffset(); if (_this.intersection !== null) { + var offset = getAvatarFootOffset(); _this.intersection.intersection.y += offset; MyAvatar.position = _this.intersection.intersection; } + + this.triggerHaptics(); this.exitTeleportMode(); }; } From a07970f05357fcf563845578cd5b2c4d99fe13aa Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 6 Jul 2016 14:09:55 -0700 Subject: [PATCH 0923/1237] Fix for one-frame glitch when changing dimensions of a model entity. This was due to Model::setScaleToFit being called on the script thread instead of the main thread. --- interface/src/ui/overlays/ModelOverlay.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index adf08934f0..b07e8bb48a 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -100,7 +100,9 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { if (newScale.x <= 0 || newScale.y <= 0 || newScale.z <= 0) { setDimensions(scale); } else { - _model->setScaleToFit(true, getDimensions()); + QMetaObject::invokeMethod(_model.get(), "setScaleToFit", Qt::AutoConnection, + Q_ARG(const bool&, true), + Q_ARG(const glm::vec3&, getDimensions())); _updateModel = true; } } From 11ef2456713e869f462b24100d56179fba06111f Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 14:28:09 -0700 Subject: [PATCH 0924/1237] temp hack for debugging --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7eb7d87566..9a37a8f7b7 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -603,6 +603,12 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType type = getShapeType(); glm::vec3 dimensions = getDimensions(); + + // BEGIN temp HACK + int numSubMeshes = _model->getFBXGeometry().meshes.size(); + qDebug() << "HACK ModeEntity" << getName() << "numSubMeshes =" << numSubMeshes; + // END temp HACK + if (type == SHAPE_TYPE_COMPOUND) { updateModelBounds(); From 301a9f4434e8b2b73af2d5d3af8aba4a343942bc Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 14:31:43 -0700 Subject: [PATCH 0925/1237] improved hack logging --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 9a37a8f7b7..87e89aa9dc 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -606,7 +606,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // BEGIN temp HACK int numSubMeshes = _model->getFBXGeometry().meshes.size(); - qDebug() << "HACK ModeEntity" << getName() << "numSubMeshes =" << numSubMeshes; + if (numSubMeshes > 1) { + qDebug() << "HACK entity name =" << getName() << " modelURL =" << " pos =" << getPosition() << " numSubMeshes =" << numSubMeshes; + } // END temp HACK if (type == SHAPE_TYPE_COMPOUND) { From 15ff7bc7319307b6cabbca3b48ec16ff3a4886c7 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 14:35:57 -0700 Subject: [PATCH 0926/1237] final hack tweak --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 87e89aa9dc..827b1b895b 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -607,7 +607,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // BEGIN temp HACK int numSubMeshes = _model->getFBXGeometry().meshes.size(); if (numSubMeshes > 1) { - qDebug() << "HACK entity name =" << getName() << " modelURL =" << " pos =" << getPosition() << " numSubMeshes =" << numSubMeshes; + qDebug() << "HACK entity name =" << getName() << " modelURL =" << getModelURL() << " pos =" << getPosition() << " numSubMeshes = " << numSubMeshes; } // END temp HACK From 110f5e9e2adaab0353cc8f3df3ecb01b5aacfe78 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 6 Jul 2016 15:08:57 -0700 Subject: [PATCH 0927/1237] Don't compare distance when no intersection. --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index dc6b78de8e..373f203e80 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -224,7 +224,7 @@ function entityHasActions(entityID) { function findRayIntersection(pickRay, precise, include, exclude) { var entities = Entities.findRayIntersection(pickRay, precise, include, exclude); var overlays = Overlays.findRayIntersection(pickRay); - if (!overlays.intersects || (entities.distance <= overlays.distance)) { + if (!overlays.intersects || (entities.intersects && (entities.distance <= overlays.distance))) { return entities; } return overlays; From dc8e21d76ca24fad26982e29972a86e7bae4da48 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 15:16:31 -0700 Subject: [PATCH 0928/1237] cleanup stretchy stuff --- scripts/system/controllers/teleport.js | 182 +++++++++++++------------ 1 file changed, 98 insertions(+), 84 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 0f7403fcc3..4f0782da0c 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,12 +16,6 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; -var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; - -var STRETCHY_BEAM_DIMENSIONS_X = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; - var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -62,9 +56,9 @@ function Teleporter() { var _this = this; this.intersection = null; this.targetProps = null; + this.rightOverlayLine = null; + this.leftOverlayLine = null; this.targetOverlay = null; - this.stretchyBeam = null; - this.noIntersectionStretchyBeam = null; this.updateConnected = null; this.initialize = function() { @@ -120,61 +114,6 @@ function Teleporter() { return midpoint }; - this.updateOrCreateStretchyBeam = function(handPosition, intersection, rotation, direction, noIntersection) { - - if (_this.stretchyBeam === null) { - var beamProps = { - url: BEAM_MODEL_URL, - position: MyAvatar.position, - - }; - - _this.stretchyBeam = Overlays.addOverlay("model", beamProps); - } - - var dimensions = { - x: STRETCHY_BEAM_DIMENSIONS_X, - y: STRETCHY_BEAM_DIMENSIONS_Y, - z: intersection !== null ? Vec3.distance(handPosition, intersection.intersection) : STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION - }; - - var position; - if (noIntersection === true) { - this.deleteTargetOverlay(); - print('no intersection') - position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION / 2, direction)); - } else { - print('intersection, find midpoint') - position = _this.findMidpoint(handPosition, intersection); - } - - - print('AT UPDATE DIMENSIONS: ' + JSON.stringify(dimensions)) - print('AT UPDATE POSITION: ' + JSON.stringify(position)) - print('AT UPDATE ROTATION: ' + JSON.stringify(rotation)) - if (noIntersection === false) { - print('AT UPDATE NORMAL: ' + JSON.stringify(intersection.surfaceNormal)) - - } - Overlays.editOverlay(_this.stretchyBeam, { - dimensions: dimensions, - position: position, - rotation: Quat.multiply(rotation, Quat.angleAxis(180, { - x: 0, - y: 1, - z: 0 - })) - }) - - }; - - this.deleteStretchyBeam = function() { - if (_this.stretchyBeam !== null) { - Overlays.deleteOverlay(_this.stretchyBeam); - _this.stretchyBeam = null; - } - }; - this.createFadeSphere = function(avatarHead) { var sphereProps = { @@ -254,12 +193,17 @@ function Teleporter() { this.targetOverlay = null; } + this.turnOffOverlayBeams = function() { + this.rightOverlayOff(); + this.leftOverlayOff(); + } + this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); this.updateConnected = null; this.disableMappings(); - this.deleteStretchyBeam(); + this.turnOffOverlayBeams(); this.deleteTargetOverlay(); this.enableGrab(); Script.setTimeout(function() { @@ -312,24 +256,24 @@ function Teleporter() { var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); + this.rightLineOn(rightPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); + var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - if (rightIntersection.distance > 250) { - print('VERY FAR INTERSECTION') - - } if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { this.createTargetOverlay(); } - this.updateOrCreateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal, rightPickRay.direction, false); + } else { - - this.updateOrCreateStretchyBeam(rightPickRay.origin, null, rightFinal, rightPickRay.direction, true); - + this.deleteTargetOverlay(); } } @@ -354,31 +298,101 @@ function Teleporter() { this.leftPickRay = leftPickRay; - var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); + var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 500)); + + this.leftLineOn(leftPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { - if (leftIntersection.distance > 250) { - print('VERY FAR INTERSECTION') - this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); - return - } + if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { this.createTargetOverlay(); } - this.updateOrCreateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal, leftPickRay.direction, false); + } else { - - this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); - - + this.deleteTargetOverlay(); } }; + this.rightLineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.rightOverlayLine === null) { + var lineProperties = { + start: closePoint, + end: farPoint, + color: color, + ignoreRayIntersection: true, // always ignore this + visible: true, + alpha: 1, + solid: true, + glow: 1.0 + }; + + this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.rightOverlayLine, { + lineWidth: 50, + start: closePoint, + end: farPoint, + color: color, + visible: true, + ignoreRayIntersection: true, // always ignore this + alpha: 1 + }); + } + }; + + this.leftLineOn = function(closePoint, farPoint, color) { + // draw a line + print('COLOR ON LINE : ' + JSON.stringify(color)) + if (this.leftOverlayLine === null) { + var lineProperties = { + ignoreRayIntersection: true, // always ignore this + start: closePoint, + end: farPoint, + color: color, + visible: true, + alpha: 1, + solid: true, + glow: 1.0 + }; + + this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.leftOverlayLine, { + start: closePoint, + end: farPoint, + color: color, + visible: true, + alpha: 1, + solid: true, + glow: 1.0 + }); + } + }; + this.rightOverlayOff = function() { + if (this.rightOverlayLine !== null) { + Overlays.deleteOverlay(this.rightOverlayLine); + this.rightOverlayLine = null; + } + }; + + this.leftOverlayOff = function() { + if (this.leftOverlayLine !== null) { + Overlays.deleteOverlay(this.leftOverlayLine); + this.leftOverlayLine = null; + } + }; this.updateTargetOverlay = function(intersection) { this.intersection = intersection; @@ -507,8 +521,8 @@ Script.scriptEnding.connect(cleanup); function cleanup() { teleportMapping.disable(); teleporter.disableMappings(); - teleporter.deleteStretchyBeam(); teleporter.deleteTargetOverlay(); + teleporter.turnOffOverlayBeams(); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From 696169ed3731f93d95b104c862e34313d259e488 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 15:19:10 -0700 Subject: [PATCH 0929/1237] terminate lines at intersection --- scripts/system/controllers/teleport.js | 33 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 4f0782da0c..caa898dd76 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -256,16 +256,15 @@ function Teleporter() { var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); - this.rightLineOn(rightPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - + this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, { + red: 7, + green: 36, + blue: 44 + }); if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { @@ -273,6 +272,12 @@ function Teleporter() { } } else { + + this.rightLineOn(rightPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); this.deleteTargetOverlay(); } } @@ -300,16 +305,16 @@ function Teleporter() { var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 500)); - this.leftLineOn(leftPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { + this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, { + red: 7, + green: 36, + blue: 44 + }); if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { @@ -318,6 +323,12 @@ function Teleporter() { } else { + + this.leftLineOn(leftPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); this.deleteTargetOverlay(); } }; From 5cb585aec34bdc36245fadf8e39906ef8481fc1a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 15:25:01 -0700 Subject: [PATCH 0930/1237] remove debug prints --- scripts/system/controllers/teleport.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index caa898dd76..2345ac7384 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -29,7 +29,6 @@ function ThumbPad(hand) { var _this = this; this.buttonPress = function(value) { - print('jbp pad press: ' + value + " on: " + _this.hand) _this.buttonValue = value; }; @@ -62,14 +61,11 @@ function Teleporter() { this.updateConnected = null; this.initialize = function() { - print('jbp initialize') this.createMappings(); this.disableGrab(); }; this.createTargetOverlay = function() { - print('creating target overlay') - var targetOverlayProps = { url: TARGET_MODEL_URL, @@ -81,7 +77,6 @@ function Teleporter() { }; this.createMappings = function() { - print('jbp create mappings internal'); // peek at the trigger and thumbs to store their values teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); @@ -90,7 +85,6 @@ function Teleporter() { }; this.disableMappings = function() { - print('jbp disable mappings internal') Controller.disableMapping(teleporter.telporterMappingInternalName); }; @@ -99,7 +93,6 @@ function Teleporter() { return; } - print('jbp hand on entering teleport mode: ' + hand); inTeleportMode = true; this.teleportHand = hand; this.initialize(); @@ -199,7 +192,6 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - print('jbp value on exit: ' + value); Script.update.disconnect(this.update); this.updateConnected = null; this.disableMappings(); @@ -363,8 +355,6 @@ function Teleporter() { }; this.leftLineOn = function(closePoint, farPoint, color) { - // draw a line - print('COLOR ON LINE : ' + JSON.stringify(color)) if (this.leftOverlayLine === null) { var lineProperties = { ignoreRayIntersection: true, // always ignore this @@ -437,7 +427,6 @@ function Teleporter() { this.teleport = function(value) { - print('TELEPORT CALLED'); if (_this.intersection !== null) { var offset = getAvatarFootOffset(); From 2db8160568cb01520cf1cf777cfbad0113de70d7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 6 Jul 2016 15:46:14 -0700 Subject: [PATCH 0931/1237] handControllerGrab improvements * Made handControllerGrab eslint clean * A model overlay is used instead of a sphere to draw equip hotspots. * The equip hotspot model will grow in size by 10% when hand is near enough to equip it. * The hand controller will now perform a haptic pulse when your hand is near enough to an equip hotspot. * Near triggers events will also perform a haptic pulse --- .../system/controllers/handControllerGrab.js | 315 ++++++++++-------- 1 file changed, 180 insertions(+), 135 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ecece8c4f7..869ea98b34 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -72,6 +72,9 @@ var LINE_ENTITY_DIMENSIONS = { var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray +var EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR = 0.75; +var EQUIP_RADIUS_EMBIGGEN_FACTOR = 1.1; + // // near grabbing // @@ -269,17 +272,17 @@ function restore2DMode() { function EntityPropertiesCache() { this.cache = {}; } -EntityPropertiesCache.prototype.clear = function() { +EntityPropertiesCache.prototype.clear = function () { this.cache = {}; }; -EntityPropertiesCache.prototype.findEntities = function(position, radius) { +EntityPropertiesCache.prototype.findEntities = function (position, radius) { var entities = Entities.findEntities(position, radius); var _this = this; - entities.forEach(function(x) { + entities.forEach(function (x) { _this.updateEntity(x); }); }; -EntityPropertiesCache.prototype.updateEntity = function(entityID) { +EntityPropertiesCache.prototype.updateEntity = function (entityID) { var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); // convert props.userData from a string to an object. @@ -295,14 +298,14 @@ EntityPropertiesCache.prototype.updateEntity = function(entityID) { this.cache[entityID] = props; }; -EntityPropertiesCache.prototype.getEntities = function() { +EntityPropertiesCache.prototype.getEntities = function () { return Object.keys(this.cache); }; -EntityPropertiesCache.prototype.getProps = function(entityID) { +EntityPropertiesCache.prototype.getProps = function (entityID) { var obj = this.cache[entityID]; return obj ? obj : undefined; }; -EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { +EntityPropertiesCache.prototype.getGrabbableProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA; @@ -310,7 +313,7 @@ EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { return undefined; } }; -EntityPropertiesCache.prototype.getGrabProps = function(entityID) { +EntityPropertiesCache.prototype.getGrabProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.grabKey ? props.userData.grabKey : {}; @@ -318,7 +321,7 @@ EntityPropertiesCache.prototype.getGrabProps = function(entityID) { return undefined; } }; -EntityPropertiesCache.prototype.getWearableProps = function(entityID) { +EntityPropertiesCache.prototype.getWearableProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.wearable ? props.userData.wearable : {}; @@ -326,7 +329,7 @@ EntityPropertiesCache.prototype.getWearableProps = function(entityID) { return undefined; } }; -EntityPropertiesCache.prototype.getEquipHotspotsProps = function(entityID) { +EntityPropertiesCache.prototype.getEquipHotspotsProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.equipHotspots ? props.userData.equipHotspots : {}; @@ -344,7 +347,7 @@ function MyController(hand) { this.getHandPosition = MyAvatar.getLeftPalmPosition; // this.getHandRotation = MyAvatar.getLeftPalmRotation; } - this.getHandRotation = function() { + this.getHandRotation = function () { var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; return Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).rotation); }; @@ -389,13 +392,13 @@ function MyController(hand) { var _this = this; var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; - this.ignoreInput = function() { + this.ignoreInput = function () { // We've made the decision to use 'this' for new code, even though it is fragile, // in order to keep/ the code uniform without making any no-op line changes. return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; - this.update = function(deltaTime) { + this.update = function (deltaTime) { this.updateSmoothedTrigger(); @@ -417,12 +420,12 @@ function MyController(hand) { } }; - this.callEntityMethodOnGrabbed = function(entityMethodName) { + this.callEntityMethodOnGrabbed = function (entityMethodName) { var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); }; - this.setState = function(newState, reason) { + this.setState = function (newState, reason) { if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); @@ -455,7 +458,7 @@ function MyController(hand) { } }; - this.debugLine = function(closePoint, farPoint, color) { + this.debugLine = function (closePoint, farPoint, color) { Entities.addEntity({ type: "Line", name: "Grab Debug Entity", @@ -475,7 +478,7 @@ function MyController(hand) { }); }; - this.lineOn = function(closePoint, farPoint, color) { + this.lineOn = function (closePoint, farPoint, color) { // draw a line if (this.pointer === null) { this.pointer = Entities.addEntity({ @@ -507,7 +510,7 @@ function MyController(hand) { }; var SEARCH_SPHERE_ALPHA = 0.5; - this.searchSphereOn = function(location, size, color) { + this.searchSphereOn = function (location, size, color) { if (this.searchSphere === null) { var sphereProperties = { position: location, @@ -530,7 +533,7 @@ function MyController(hand) { } }; - this.overlayLineOn = function(closePoint, farPoint, color) { + this.overlayLineOn = function (closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { lineWidth: 5, @@ -558,7 +561,7 @@ function MyController(hand) { } }; - this.searchIndicatorOn = function(distantPickRay) { + this.searchIndicatorOn = function (distantPickRay) { var handPosition = distantPickRay.origin; var SEARCH_SPHERE_SIZE = 0.011; var SEARCH_SPHERE_FOLLOW_RATE = 0.50; @@ -579,7 +582,7 @@ function MyController(hand) { } }; - this.handleDistantParticleBeam = function(handPosition, objectPosition, color) { + this.handleDistantParticleBeam = function (handPosition, objectPosition, color) { var handToObject = Vec3.subtract(objectPosition, handPosition); var finalRotationObject = Quat.rotationBetween(Vec3.multiply(-1, Vec3.UP), handToObject); @@ -595,7 +598,7 @@ function MyController(hand) { } }; - this.createParticleBeam = function(positionObject, orientationObject, color, speed, spread, lifespan) { + this.createParticleBeam = function (positionObject, orientationObject, color, speed, spread, lifespan) { var particleBeamPropertiesObject = { type: "ParticleEffect", @@ -649,7 +652,7 @@ function MyController(hand) { this.particleBeamObject = Entities.addEntity(particleBeamPropertiesObject); }; - this.updateParticleBeam = function(positionObject, orientationObject, color, speed, spread, lifespan) { + this.updateParticleBeam = function (positionObject, orientationObject, color, speed, spread, lifespan) { Entities.editEntity(this.particleBeamObject, { rotation: orientationObject, position: positionObject, @@ -661,7 +664,7 @@ function MyController(hand) { }); }; - this.evalLightWorldTransform = function(modelPos, modelRot) { + this.evalLightWorldTransform = function (modelPos, modelRot) { var MODEL_LIGHT_POSITION = { x: 0, @@ -681,7 +684,7 @@ function MyController(hand) { }; }; - this.handleSpotlight = function(parentID) { + this.handleSpotlight = function (parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -718,7 +721,7 @@ function MyController(hand) { } }; - this.handlePointLight = function(parentID) { + this.handlePointLight = function (parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -750,21 +753,21 @@ function MyController(hand) { } }; - this.lineOff = function() { + this.lineOff = function () { if (this.pointer !== null) { Entities.deleteEntity(this.pointer); } this.pointer = null; }; - this.overlayLineOff = function() { + this.overlayLineOff = function () { if (this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); } this.overlayLine = null; }; - this.searchSphereOff = function() { + this.searchSphereOff = function () { if (this.searchSphere !== null) { Overlays.deleteOverlay(this.searchSphere); this.searchSphere = null; @@ -773,14 +776,14 @@ function MyController(hand) { } }; - this.particleBeamOff = function() { + this.particleBeamOff = function () { if (this.particleBeamObject !== null) { Entities.deleteEntity(this.particleBeamObject); this.particleBeamObject = null; } }; - this.turnLightsOff = function() { + this.turnLightsOff = function () { if (this.spotlight !== null) { Entities.deleteEntity(this.spotlight); this.spotlight = null; @@ -792,7 +795,7 @@ function MyController(hand) { } }; - this.turnOffVisualizations = function() { + this.turnOffVisualizations = function () { if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); } @@ -809,63 +812,62 @@ function MyController(hand) { }; - this.triggerPress = function(value) { + this.triggerPress = function (value) { _this.rawTriggerValue = value; }; - this.secondaryPress = function(value) { + this.secondaryPress = function (value) { _this.rawSecondaryValue = value; }; - this.updateSmoothedTrigger = function() { + this.updateSmoothedTrigger = function () { var triggerValue = this.rawTriggerValue; // smooth out trigger value this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); }; - this.triggerSmoothedGrab = function() { + this.triggerSmoothedGrab = function () { return this.triggerValue > TRIGGER_GRAB_VALUE; }; - this.triggerSmoothedSqueezed = function() { + this.triggerSmoothedSqueezed = function () { return this.triggerValue > TRIGGER_ON_VALUE; }; - this.triggerSmoothedReleased = function() { + this.triggerSmoothedReleased = function () { return this.triggerValue < TRIGGER_OFF_VALUE; }; - this.secondarySqueezed = function() { + this.secondarySqueezed = function () { return _this.rawSecondaryValue > BUMPER_ON_VALUE; }; - this.secondaryReleased = function() { + this.secondaryReleased = function () { return _this.rawSecondaryValue < BUMPER_ON_VALUE; }; - // this.triggerOrsecondarySqueezed = function() { + // this.triggerOrsecondarySqueezed = function () { // return triggerSmoothedSqueezed() || secondarySqueezed(); // } - // this.triggerAndSecondaryReleased = function() { + // this.triggerAndSecondaryReleased = function () { // return triggerSmoothedReleased() && secondaryReleased(); // } - this.thumbPress = function(value) { + this.thumbPress = function (value) { _this.rawThumbValue = value; }; - this.thumbPressed = function() { + this.thumbPressed = function () { return _this.rawThumbValue > THUMB_ON_VALUE; }; - this.thumbReleased = function() { + this.thumbReleased = function () { return _this.rawThumbValue < THUMB_ON_VALUE; }; - this.off = function() { - + this.off = function () { if (this.triggerSmoothedReleased()) { this.waitForTriggerRelease = false; } @@ -875,11 +877,21 @@ function MyController(hand) { this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { this.setState(STATE_SEARCHING, "trigger squeeze detected"); + return; } } + + if (!this.waitForTriggerRelease) { + // update haptics when hand is near an equip hotspot + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(this.getHandPosition(), NEAR_GRAB_RADIUS); + var candidateEntities = this.entityPropertyCache.getEntities(); + var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); + this.updateEquipHaptics(potentialEquipHotspot); + } }; - this.createHotspots = function() { + this.createHotspots = function () { var _this = this; var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; @@ -890,9 +902,6 @@ function MyController(hand) { var HAND_GRAB_SPHERE_ALPHA = 0.3; var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; - var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; - var EQUIP_SPHERE_ALPHA = 0.3; - var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; var GRAB_BOX_ALPHA = 0.1; @@ -915,7 +924,9 @@ function MyController(hand) { this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, - type: "hand" + type: "hand", + localPosition: {x: 0, y: 0, z: 0}, + hotspot: {} }); // add larger blue sphere around the palm. @@ -933,7 +944,8 @@ function MyController(hand) { entityID: undefined, overlay: overlay, type: "hand", - localPosition: {x: 0, y: 0, z: 0} + localPosition: {x: 0, y: 0, z: 0}, + hotspot: {} }); } @@ -943,7 +955,7 @@ function MyController(hand) { if (DRAW_GRAB_BOXES) { // add blue box overlays for grabbable entities. - this.entityPropertyCache.getEntities().forEach(function(entityID) { + this.entityPropertyCache.getEntities().forEach(function (entityID) { var props = _this.entityPropertyCache.getProps(entityID); if (_this.entityIsGrabbable(entityID)) { var overlay = Overlays.addOverlay("cube", { @@ -961,50 +973,72 @@ function MyController(hand) { entityID: entityID, overlay: overlay, type: "near", - localPosition: {x: 0, y: 0, z: 0} + localPosition: {x: 0, y: 0, z: 0}, + hotspot: {} }); } }); } // add green spheres for each equippable hotspot. - flatten(this.entityPropertyCache.getEntities().map(function(entityID) { + flatten(this.entityPropertyCache.getEntities().map(function (entityID) { return _this.collectEquipHotspots(entityID); - })).filter(function(hotspot) { + })).filter(function (hotspot) { return _this.hotspotIsEquippable(hotspot); - }).forEach(function(hotspot) { - var overlay = Overlays.addOverlay("sphere", { + }).forEach(function (hotspot) { + var overlay = Overlays.addOverlay("model", { + url: "http://hifi-content.s3.amazonaws.com/alan/dev/equip-ico-2.fbx", position: hotspot.worldPosition, - size: hotspot.radius * 2, - color: EQUIP_SPHERE_COLOR, - alpha: EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false + rotation: {x: 0, y: 0, z: 0, w: 1}, + dimensions: (hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR, + ignoreRayIntersection: true }); _this.hotspotOverlays.push({ entityID: hotspot.entityID, overlay: overlay, type: "equip", - localPosition: hotspot.localPosition + localPosition: hotspot.localPosition, + hotspot: hotspot }); }); }; - this.updateHotspots = function() { + this.clearEquipHaptics = function () { + this.prevPotentialEquipHotspot = null; + }; + + this.updateEquipHaptics = function (potentialEquipHotspot) { + if (potentialEquipHotspot && !this.prevPotentialEquipHotspot || + !potentialEquipHotspot && this.prevPotentialEquipHotspot) { + Controller.triggerShortHapticPulse(1.0, this.hand); + } + this.prevPotentialEquipHotspot = potentialEquipHotspot; + }; + + this.updateHotspots = function (potentialEquipHotspot) { var _this = this; var props; - this.hotspotOverlays.forEach(function(overlayInfo) { + this.hotspotOverlays.forEach(function (overlayInfo) { if (overlayInfo.type === "hand") { Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); } else if (overlayInfo.type === "equip") { + + var radius = (overlayInfo.hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR; + + // embiggen the equipHotspot if it maches the potentialEquipHotspot + if (potentialEquipHotspot && overlayInfo.entityID == potentialEquipHotspot.entityID && + Vec3.equal(overlayInfo.localPosition, potentialEquipHotspot.localPosition)) { + radius = (overlayInfo.hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR * + EQUIP_RADIUS_EMBIGGEN_FACTOR; + } + _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); var entityXform = new Xform(props.rotation, props.position); Overlays.editOverlay(overlayInfo.overlay, { position: entityXform.xformPoint(overlayInfo.localPosition), - rotation: props.rotation + rotation: props.rotation, + dimensions: radius }); } else if (overlayInfo.type === "near") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); @@ -1014,18 +1048,18 @@ function MyController(hand) { }); }; - this.destroyHotspots = function() { - this.hotspotOverlays.forEach(function(overlayInfo) { + this.destroyHotspots = function () { + this.hotspotOverlays.forEach(function (overlayInfo) { Overlays.deleteOverlay(overlayInfo.overlay); }); this.hotspotOverlays = []; }; - this.searchEnter = function() { + this.searchEnter = function () { this.createHotspots(); }; - this.searchExit = function() { + this.searchExit = function () { this.destroyHotspots(); }; @@ -1033,7 +1067,7 @@ function MyController(hand) { // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND // @returns {object} returns object with two keys entityID and distance // - this.calcRayPickInfo = function(hand) { + this.calcRayPickInfo = function (hand) { var standardControllerValue = (hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var pose = Controller.getPoseValue(standardControllerValue); @@ -1091,7 +1125,7 @@ function MyController(hand) { } }; - this.entityWantsTrigger = function(entityID) { + this.entityWantsTrigger = function (entityID) { var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); return grabbableProps && grabbableProps.wantsTrigger; }; @@ -1105,7 +1139,7 @@ function MyController(hand) { // * radius {number} radius of equip hotspot // * joints {Object} keys are joint names values are arrays of two elements: // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. - this.collectEquipHotspots = function(entityID) { + this.collectEquipHotspots = function (entityID) { var result = []; var props = this.entityPropertyCache.getProps(entityID); var entityXform = new Xform(props.rotation, props.position); @@ -1139,7 +1173,7 @@ function MyController(hand) { return result; }; - this.hotspotIsEquippable = function(hotspot) { + this.hotspotIsEquippable = function (hotspot) { var props = this.entityPropertyCache.getProps(hotspot.entityID); var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1158,7 +1192,7 @@ function MyController(hand) { return true; }; - this.entityIsGrabbable = function(entityID) { + this.entityIsGrabbable = function (entityID) { var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); var grabProps = this.entityPropertyCache.getGrabProps(entityID); var props = this.entityPropertyCache.getProps(entityID); @@ -1210,7 +1244,7 @@ function MyController(hand) { return true; }; - this.entityIsDistanceGrabbable = function(entityID, handPosition) { + this.entityIsDistanceGrabbable = function (entityID, handPosition) { if (!this.entityIsGrabbable(entityID)) { return false; } @@ -1247,7 +1281,7 @@ function MyController(hand) { return true; }; - this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { + this.entityIsNearGrabbable = function (entityID, handPosition, maxDistance) { if (!this.entityIsGrabbable(entityID)) { return false; @@ -1268,12 +1302,31 @@ function MyController(hand) { return true; }; - this.search = function() { + this.chooseBestEquipHotspot = function (candidateEntities) { + var equippableHotspots = flatten(candidateEntities.map(function (entityID) { + return _this.collectEquipHotspots(entityID); + })).filter(function (hotspot) { + return (_this.hotspotIsEquippable(hotspot) && + Vec3.distance(hotspot.worldPosition, _this.getHandPosition()) < hotspot.radius); + }); + + if (equippableHotspots.length > 0) { + // sort by distance + equippableHotspots.sort(function (a, b) { + var aDistance = Vec3.distance(a.worldPosition, this.getHandPosition()); + var bDistance = Vec3.distance(b.worldPosition, this.getHandPosition()); + return aDistance - bDistance; + }); + return equippableHotspots[0]; + } else { + return null; + } + }; + + this.search = function () { var _this = this; var name; - this.updateHotspots(); - this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; @@ -1291,31 +1344,17 @@ function MyController(hand) { this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); var candidateEntities = this.entityPropertyCache.getEntities(); - var equippableHotspots = flatten(candidateEntities.map(function(entityID) { - return _this.collectEquipHotspots(entityID); - })).filter(function(hotspot) { - return _this.hotspotIsEquippable(hotspot) && Vec3.distance(hotspot.worldPosition, handPosition) < hotspot.radius; - }); - - var entity; - if (equippableHotspots.length > 0) { - // sort by distance - equippableHotspots.sort(function(a, b) { - var aDistance = Vec3.distance(a.worldPosition, handPosition); - var bDistance = Vec3.distance(b.worldPosition, handPosition); - return aDistance - bDistance; - }); + var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); + if (potentialEquipHotspot) { if (this.triggerSmoothedGrab()) { - this.grabbedHotspot = equippableHotspots[0]; - this.grabbedEntity = equippableHotspots[0].entityID; + this.grabbedHotspot = potentialEquipHotspot; + this.grabbedEntity = potentialEquipHotspot.entityID; this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(this.grabbedEntity).name + "'"); return; - } else { - // TODO: highlight the equippable object? } } - var grabbableEntities = candidateEntities.filter(function(entity) { + var grabbableEntities = candidateEntities.filter(function (entity) { return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); }); @@ -1330,9 +1369,10 @@ function MyController(hand) { this.intersectionDistance = 0; } + var entity; if (grabbableEntities.length > 0) { // sort by distance - grabbableEntities.sort(function(a, b) { + grabbableEntities.sort(function (a, b) { var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); return aDistance - bDistance; @@ -1345,11 +1385,10 @@ function MyController(hand) { this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); return; } else { - // TODO: highlight the near-triggerable object? + // potentialNearTriggerEntity = entity; } } else { if (this.triggerSmoothedGrab()) { - var props = this.entityPropertyCache.getProps(entity); var grabProps = this.entityPropertyCache.getGrabProps(entity); var refCount = grabProps.refCount ? grabProps.refCount : 0; @@ -1364,10 +1403,9 @@ function MyController(hand) { this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); return; } else { - // TODO: highlight the grabbable object? + // potentialNearGrabEntity = entity; } } - return; } if (rayPickInfo.entityID) { @@ -1379,7 +1417,7 @@ function MyController(hand) { this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); return; } else { - // TODO: highlight the far-triggerable object? + // potentialFarTriggerEntity = entity; } } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { if (this.triggerSmoothedGrab()) { @@ -1387,11 +1425,14 @@ function MyController(hand) { this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); return; } else { - // TODO: highlight the far-grabbable object? + // potentialFarGrabEntity = entity; } } } + this.updateEquipHaptics(potentialEquipHotspot); + this.updateHotspots(potentialEquipHotspot); + // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { this.lineOn(rayPickInfo.searchRay.origin, @@ -1403,7 +1444,7 @@ function MyController(hand) { Reticle.setVisible(false); }; - this.distanceGrabTimescale = function(mass, distance) { + this.distanceGrabTimescale = function (mass, distance) { var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / DISTANCE_HOLDING_UNITY_MASS * distance / DISTANCE_HOLDING_UNITY_DISTANCE; @@ -1413,11 +1454,13 @@ function MyController(hand) { return timeScale; }; - this.getMass = function(dimensions, density) { + this.getMass = function (dimensions, density) { return (dimensions.x * dimensions.y * dimensions.z) * density; }; - this.distanceHoldingEnter = function() { + this.distanceHoldingEnter = function () { + + this.clearEquipHaptics(); // controller pose is in avatar frame var avatarControllerPose = @@ -1476,7 +1519,7 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.distanceHolding = function() { + this.distanceHolding = function () { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); @@ -1608,7 +1651,7 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.setupHoldAction = function() { + this.setupHoldAction = function () { this.actionID = Entities.addAction("hold", this.grabbedEntity, { hand: this.hand === RIGHT_HAND ? "right" : "left", timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, @@ -1628,7 +1671,7 @@ function MyController(hand) { return true; }; - this.projectVectorAlongAxis = function(position, axisStart, axisEnd) { + this.projectVectorAlongAxis = function (position, axisStart, axisEnd) { var aPrime = Vec3.subtract(position, axisStart); var bPrime = Vec3.subtract(axisEnd, axisStart); var bPrimeMagnitude = Vec3.length(bPrime); @@ -1644,12 +1687,12 @@ function MyController(hand) { return projection; }; - this.dropGestureReset = function() { + this.dropGestureReset = function () { this.fastHandMoveDetected = false; this.fastHandMoveTimer = 0; }; - this.dropGestureProcess = function(deltaTime) { + this.dropGestureProcess = function (deltaTime) { var standardControllerValue = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var pose = Controller.getPoseValue(standardControllerValue); var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); @@ -1688,12 +1731,13 @@ function MyController(hand) { return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown; }; - this.nearGrabbingEnter = function() { + this.nearGrabbingEnter = function () { this.lineOff(); this.overlayLineOff(); this.dropGestureReset(); + this.clearEquipHaptics(); if (this.entityActivated) { var saveGrabbedID = this.grabbedEntity; @@ -1793,7 +1837,7 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.nearGrabbing = function(deltaTime) { + this.nearGrabbing = function (deltaTime) { var dropDetected = this.dropGestureProcess(deltaTime); @@ -1910,15 +1954,16 @@ function MyController(hand) { } }; - this.nearTriggerEnter = function() { + this.nearTriggerEnter = function () { + Controller.triggerShortHapticPulse(1.0, this.hand); this.callEntityMethodOnGrabbed("startNearTrigger"); }; - this.farTriggerEnter = function() { + this.farTriggerEnter = function () { this.callEntityMethodOnGrabbed("startFarTrigger"); }; - this.nearTrigger = function() { + this.nearTrigger = function () { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -1927,7 +1972,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueNearTrigger"); }; - this.farTrigger = function() { + this.farTrigger = function () { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -1960,11 +2005,11 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueFarTrigger"); }; - this.offEnter = function() { + this.offEnter = function () { this.release(); }; - this.release = function() { + this.release = function () { this.turnLightsOff(); this.turnOffVisualizations(); @@ -2002,14 +2047,14 @@ function MyController(hand) { } }; - this.cleanup = function() { + this.cleanup = function () { this.release(); Entities.deleteEntity(this.particleBeamObject); Entities.deleteEntity(this.spotLight); Entities.deleteEntity(this.pointLight); }; - this.heartBeat = function(entityID) { + this.heartBeat = function (entityID) { var now = Date.now(); if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) { var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); @@ -2019,7 +2064,7 @@ function MyController(hand) { } }; - this.resetAbandonedGrab = function(entityID) { + this.resetAbandonedGrab = function (entityID) { print("cleaning up abandoned grab on " + entityID); var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); data["refCount"] = 1; @@ -2027,7 +2072,7 @@ function MyController(hand) { this.deactivateEntity(entityID, false); }; - this.activateEntity = function(entityID, grabbedProperties, wasLoaded) { + this.activateEntity = function (entityID, grabbedProperties, wasLoaded) { if (this.entityActivated) { return; } @@ -2089,18 +2134,18 @@ function MyController(hand) { return data; }; - this.checkForStrayChildren = function() { + this.checkForStrayChildren = function () { // sometimes things can get parented to a hand and this script is unaware. Search for such entities and // unhook them. var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); - children.forEach(function(childID) { + children.forEach(function (childID) { print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); Entities.editEntity(childID, {parentID: NULL_UUID}); }); }; - this.deactivateEntity = function(entityID, noVelocity) { + this.deactivateEntity = function (entityID, noVelocity) { var deactiveProps; if (!this.entityActivated) { @@ -2185,7 +2230,7 @@ function MyController(hand) { setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; - this.getOtherHandController = function() { + this.getOtherHandController = function () { return (this.hand === RIGHT_HAND) ? leftController : rightController; }; } @@ -2227,7 +2272,7 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); -var handleHandMessages = function(channel, message, sender) { +var handleHandMessages = function (channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { From 3c4f9f8016327b8d7dc2f5dbfbdcd1fad138a3fc Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 6 Jul 2016 15:51:17 -0700 Subject: [PATCH 0932/1237] refert debug hack --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 827b1b895b..7eb7d87566 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -603,14 +603,6 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType type = getShapeType(); glm::vec3 dimensions = getDimensions(); - - // BEGIN temp HACK - int numSubMeshes = _model->getFBXGeometry().meshes.size(); - if (numSubMeshes > 1) { - qDebug() << "HACK entity name =" << getName() << " modelURL =" << getModelURL() << " pos =" << getPosition() << " numSubMeshes = " << numSubMeshes; - } - // END temp HACK - if (type == SHAPE_TYPE_COMPOUND) { updateModelBounds(); From 74ef82f800b3fe3b409e6f8c327f058c25c21312 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 6 Jul 2016 16:15:02 -0700 Subject: [PATCH 0933/1237] add 'dynamic' property as a load --- scripts/developer/tests/loadedMachine.js | 39 +++++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/scripts/developer/tests/loadedMachine.js b/scripts/developer/tests/loadedMachine.js index c222eb7379..375e3d8004 100644 --- a/scripts/developer/tests/loadedMachine.js +++ b/scripts/developer/tests/loadedMachine.js @@ -40,7 +40,9 @@ var numberLeftRunning = scriptData.length - otherScripts.length; print('initially running', otherScripts.length, 'scripts. Leaving', numberLeftRunning, 'and stopping', otherScripts); var typedEntities = {Light: [], ParticleEffect: []}; var interestingTypes = Object.keys(typedEntities); -var loads = ['ignore', 'baseline'].concat(otherScripts, interestingTypes); +var propertiedEntities = {dynamic: []}; +var interestingProperties = Object.keys(propertiedEntities); +var loads = ['ignore', 'baseline'].concat(otherScripts, interestingTypes, interestingProperties); var loadIndex = 0, nLoads = loads.length, load; var results = []; var initialLodIsAutomatic = LODManager.getAutomaticLODAdjust(); @@ -51,21 +53,42 @@ LODManager.setOctreeSizeScale(DEFAULT_LOD); // Fill the typedEnties with the entityIDs that are already visible. It would be nice if this were more efficient. var allEntities = Entities.findEntities(MyAvatar.position, SEARCH_RADIUS); -print('Searching', allEntities.length, 'entities for', interestingTypes); +print('Searching', allEntities.length, 'entities for', interestingTypes, 'and', interestingProperties); +var propertiesToGet = ['type', 'visible'].concat(interestingProperties); allEntities.forEach(function (entityID) { - var properties = Entities.getEntityProperties(entityID, ['type', 'visible']); - if (properties.visible && (interestingTypes.indexOf(properties.type) >= 0)) { - typedEntities[properties.type].push(entityID); + var properties = Entities.getEntityProperties(entityID, propertiesToGet); + if (properties.visible) { + if (interestingTypes.indexOf(properties.type) >= 0) { + typedEntities[properties.type].push(entityID); + } else { + interestingProperties.forEach(function (property) { + if (entityID && properties[property]) { + propertiedEntities[property].push(entityID); + entityID = false; // Put in only one bin + } + }); + } } }); +allEntities = undefined; // free them interestingTypes.forEach(function (type) { print('There are', typedEntities[type].length, type, 'entities.'); }); +interestingProperties.forEach(function (property) { + print('There are', propertiedEntities[property].length, property, 'entities.'); +}); function toggleVisibility(type, on) { typedEntities[type].forEach(function (entityID) { Entities.editEntity(entityID, {visible: on}); }); } +function toggleProperty(property, on) { + propertiedEntities[property].forEach(function (entityID) { + var properties = {}; + properties[property] = on; + Entities.editEntity(entityID, properties); + }); +} function restoreOneTest(load) { print('restore', load); switch (load) { @@ -76,6 +99,9 @@ function restoreOneTest(load) { case 'ParticleEffect': toggleVisibility(load, true); break; + case 'dynamic': + toggleProperty(load, 1); + break; default: Script.load(load); } @@ -90,6 +116,9 @@ function undoOneTest(load) { case 'ParticleEffect': toggleVisibility(load, false); break; + case 'dynamic': + toggleProperty(load, 0); + break; default: ScriptDiscoveryService.stopScript(load); } From 8f2bf2b4238570b691c4ae4fc3ea40b25230e924 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 6 Jul 2016 17:13:16 -0700 Subject: [PATCH 0934/1237] Integrating the scattering to the local lights --- libraries/model/src/model/Light.slh | 48 ++++--- .../render-utils/src/DeferredGlobalLight.slh | 2 +- .../render-utils/src/DeferredLighting.slh | 8 +- libraries/render-utils/src/LightPoint.slh | 113 +++++++++++++++++ libraries/render-utils/src/LightSpot.slh | 119 ++++++++++++++++++ libraries/render-utils/src/LightingModel.cpp | 10 +- libraries/render-utils/src/LightingModel.h | 9 ++ libraries/render-utils/src/LightingModel.slh | 112 +++++++++++++++++ libraries/render-utils/src/overlay3D.slf | 2 +- .../src/overlay3D_translucent.slf | 2 +- libraries/render-utils/src/point_light.slf | 63 +++++----- libraries/render-utils/src/spot_light.slf | 77 ++++++------ libraries/render/src/render/ShapePipeline.cpp | 4 + 13 files changed, 480 insertions(+), 89 deletions(-) create mode 100644 libraries/render-utils/src/LightPoint.slh create mode 100644 libraries/render-utils/src/LightSpot.slh diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 7cb745ff53..5e9d3d30c0 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -126,28 +126,48 @@ float getLightAmbientMapNumMips(Light l) { } -<@if GPU_FEATURE_PROFILE == GPU_CORE @> uniform lightBuffer { Light light; }; Light getLight() { return light; } -<@else@> -uniform vec4 lightBuffer[7]; -Light getLight() { - Light light; - light._position = lightBuffer[0]; - light._direction = lightBuffer[1]; - light._color = lightBuffer[2]; - light._attenuation = lightBuffer[3]; - light._spot = lightBuffer[4]; - light._shadow = lightBuffer[5]; - light._control = lightBuffer[6]; - return light; + + + +bool clipFragToLightVolumePoint(Light light, vec3 fragPos, out vec4 fragLightVecLen2) { + fragLightVecLen2.xyz = getLightPosition(light) - fragPos.xyz; + fragLightVecLen2.w = dot(fragLightVecLen2.xyz, fragLightVecLen2.xyz); + + // Kill if too far from the light center + if (fragLightVecLen2.w > getLightCutoffSquareRadius(light)) { + return false; + } + return true; +} + +bool clipFragToLightVolumeSpot(Light light, vec3 fragPos, out vec4 fragLightVecLen2, out vec4 fragLightDirLen, out float cosSpotAngle) { + fragLightVecLen2.xyz = getLightPosition(light) - fragPos.xyz; + fragLightVecLen2.w = dot(fragLightVecLen2.xyz, fragLightVecLen2.xyz); + + // Kill if too far from the light center + if (fragLightVecLen2.w > getLightCutoffSquareRadius(light)) { + return false; + } + + // Allright we re valid in the volume + fragLightDirLen.w = length(fragLightVecLen2.xyz); + fragLightDirLen.xyz = fragLightVecLen2.xyz / fragLightDirLen.w; + + // Kill if not in the spot light (ah ah !) + cosSpotAngle = max(-dot(fragLightDirLen.xyz, getLightDirection(light)), 0.0); + if (cosSpotAngle < getLightSpotAngleCos(light)) { + return false; + } + + return true; } -<@endif@> diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index d76c333206..2232638000 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -12,7 +12,7 @@ <@def DEFERRED_GLOBAL_LIGHT_SLH@> <@include model/Light.slh@> -<@include DeferredLighting.slh@> +!> <@include LightingModel.slh@> <$declareLightingModel()$> diff --git a/libraries/render-utils/src/DeferredLighting.slh b/libraries/render-utils/src/DeferredLighting.slh index 2e06deb0af..d656b3e054 100755 --- a/libraries/render-utils/src/DeferredLighting.slh +++ b/libraries/render-utils/src/DeferredLighting.slh @@ -8,6 +8,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html !> + <@def DEFERRED_LIGHTING_SLH@> @@ -25,14 +26,15 @@ float specularDistribution(float roughness, vec3 normal, vec3 halfDir) { float power = gloss2 / (3.14159 * denom * denom); return power; } - +*/ + // Frag Shading returns the diffuse amount as W and the specular rgb as xyz vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 fresnel, float roughness) { // Diffuse Lighting @@ -69,7 +71,6 @@ vec4 evalBlinnShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, vec3 <@endfunc@> - <$declareEvalPBRShading()$> // Return xyz the specular/reflection component and w the diffuse component @@ -78,3 +79,4 @@ vec4 evalFragShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float } <@endif@> +!> \ No newline at end of file diff --git a/libraries/render-utils/src/LightPoint.slh b/libraries/render-utils/src/LightPoint.slh new file mode 100644 index 0000000000..bf5c8aa307 --- /dev/null +++ b/libraries/render-utils/src/LightPoint.slh @@ -0,0 +1,113 @@ +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 7/5/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + + +<@func declareLightingPoint(supportScattering)@> + + +<@include DeferredLighting.slh@> + +void evalLightingPoint(out vec3 diffuse, out vec3 specular, Light light, + vec3 fragLightVec, vec3 fragEyeDir, vec3 normal, float roughness, + float metallic, vec3 fresnel, vec3 albedo, float shadow) { + + // Allright we re valid in the volume + float fragLightDistance = length(fragLightVec); + vec3 fragLightDir = fragLightVec / fragLightDistance; + + // Eval attenuation + float radialAttenuation = evalLightAttenuation(light, fragLightDistance); + + vec3 lightEnergy = radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); + + // Eval shading + vec4 shading = evalFragShading(normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); + + diffuse = albedo * shading.w * lightEnergy; + + specular = shading.rgb * lightEnergy; + + if (getLightShowContour(light) > 0.0) { + // Show edge + float edge = abs(2.0 * ((getLightRadius(light) - fragLightDistance) / (0.1)) - 1.0); + if (edge < 1) { + float edgeCoord = exp2(-8.0*edge*edge); + diffuse = vec3(edgeCoord * edgeCoord * getLightShowContour(light) * getLightColor(light)); + } + } +} + +<@if supportScattering@> + +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringBRDF()$> +<$declareSkinSpecularLighting()$> + +void evalLightingPointScattering(out vec3 diffuse, out vec3 specular, Light light, + vec3 fragLightVec, vec3 fragEyeDir, vec3 normal, float roughness, + float metallic, vec3 fresnel, vec3 albedo, float shadow, + float scattering, vec3 midNormal, vec3 lowNormal, float curvature) { + + // Allright we re valid in the volume + float fragLightDistance = length(fragLightVec); + vec3 fragLightDir = fragLightVec / fragLightDistance; + + // Eval attenuation + float radialAttenuation = evalLightAttenuation(light, fragLightDistance); + + vec3 lightEnergy = radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); + + // Eval shading + vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); + float scatteringLevel = getScatteringLevel(); + vec4 shading; + float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + { // Key Sun Lighting + // Diffuse Lighting + //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + + float specular = skinSpecular(normal, fragLightDir, fragEyeDir, roughness, 1.0); + + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + float power = specularDistribution(roughness, normal, halfDir); + //vec3 specular = power * fresnelColor * standardDiffuse; + + shading = vec4(vec3(specular), (1 - fresnelColor.x)); + } + + + if (scatteringLevel < 0.1) { + brdf = vec3(standardDiffuse); + } + brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); + + + diffuse = albedo * brdf.xyz * lightEnergy; + + specular = shading.rgb * lightEnergy; + + if (getLightShowContour(light) > 0.0) { + // Show edge + float edge = abs(2.0 * ((getLightRadius(light) - fragLightDistance) / (0.1)) - 1.0); + if (edge < 1) { + float edgeCoord = exp2(-8.0*edge*edge); + diffuse = vec3(edgeCoord * edgeCoord * getLightShowContour(light) * getLightColor(light)); + } + } +} + +<@endif@> + +<@endfunc@> + + diff --git a/libraries/render-utils/src/LightSpot.slh b/libraries/render-utils/src/LightSpot.slh new file mode 100644 index 0000000000..56c70d6c30 --- /dev/null +++ b/libraries/render-utils/src/LightSpot.slh @@ -0,0 +1,119 @@ +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 7/5/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + + +<@func declareLightingSpot(supportScattering)@> + + +<@include DeferredLighting.slh@> + +void evalLightingSpot(out vec3 diffuse, out vec3 specular, Light light, + vec4 fragLightDirLen, float cosSpotAngle, vec3 fragEyeDir, vec3 normal, float roughness, + float metallic, vec3 fresnel, vec3 albedo, float shadow) { + + // Allright we re valid in the volume + float fragLightDistance = fragLightDirLen.w; + vec3 fragLightDir = fragLightDirLen.xyz; + + // Eval attenuation + float radialAttenuation = evalLightAttenuation(light, fragLightDistance); + float angularAttenuation = evalLightSpotAttenuation(light, cosSpotAngle); + vec3 lightEnergy = angularAttenuation * radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); + + // Eval shading + vec4 shading = evalFragShading(normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); + + diffuse = albedo * shading.w * lightEnergy; + + specular = shading.rgb * lightEnergy; + + if (getLightShowContour(light) > 0.0) { + // Show edges + float edgeDistR = (getLightRadius(light) - fragLightDistance); + float edgeDistS = dot(fragLightDistance * vec2(cosSpotAngle, sqrt(1.0 - cosSpotAngle * cosSpotAngle)), -getLightSpotOutsideNormal2(light)); + float edgeDist = min(edgeDistR, edgeDistS); + float edge = abs(2.0 * (edgeDist / (0.1)) - 1.0); + if (edge < 1) { + float edgeCoord = exp2(-8.0*edge*edge); + diffuse = vec3(edgeCoord * edgeCoord * getLightColor(light)); + } + } +} + +<@if supportScattering@> + +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringBRDF()$> +<$declareSkinSpecularLighting()$> + +void evalLightingSpotScattering(out vec3 diffuse, out vec3 specular, Light light, + vec4 fragLightDirLen, float cosSpotAngle, vec3 fragEyeDir, vec3 normal, float roughness, + float metallic, vec3 fresnel, vec3 albedo, float shadow, + float scattering, vec3 midNormal, vec3 lowNormal, float curvature) { + + // Allright we re valid in the volume + float fragLightDistance = fragLightDirLen.w; + vec3 fragLightDir = fragLightDirLen.xyz; + + // Eval attenuation + float radialAttenuation = evalLightAttenuation(light, fragLightDistance); + float angularAttenuation = evalLightSpotAttenuation(light, cosSpotAngle); + vec3 lightEnergy = angularAttenuation * radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); + + // Eval shading + vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); + float scatteringLevel = getScatteringLevel(); + vec4 shading; + float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + { // Key Sun Lighting + // Diffuse Lighting + //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + + float specular = skinSpecular(normal, fragLightDir, fragEyeDir, roughness, 1.0); + + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + float power = specularDistribution(roughness, normal, halfDir); + //vec3 specular = power * fresnelColor * standardDiffuse; + + shading = vec4(vec3(specular), (1 - fresnelColor.x)); + } + + + if (scatteringLevel < 0.1) { + brdf = vec3(standardDiffuse); + } + brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); + + + diffuse = albedo * brdf.xyz * lightEnergy; + + specular = shading.rgb * lightEnergy; + + if (getLightShowContour(light) > 0.0) { + // Show edges + float edgeDistR = (getLightRadius(light) - fragLightDistance); + float edgeDistS = dot(fragLightDistance * vec2(cosSpotAngle, sqrt(1.0 - cosSpotAngle * cosSpotAngle)), -getLightSpotOutsideNormal2(light)); + float edgeDist = min(edgeDistR, edgeDistS); + float edge = abs(2.0 * (edgeDist / (0.1)) - 1.0); + if (edge < 1) { + float edgeCoord = exp2(-8.0*edge*edge); + diffuse = vec3(edgeCoord * edgeCoord * getLightColor(light)); + } + } +} + +<@endif@> + +<@endfunc@> + + diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index af546f54dd..9e66a9f673 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -108,7 +108,14 @@ void LightingModel::setSpotLight(bool enable) { bool LightingModel::isSpotLightEnabled() const { return (bool)_parametersBuffer.get().enableSpotLight; } - +void LightingModel::setShowLightContour(bool enable) { + if (enable != isShowLightContourEnabled()) { + _parametersBuffer.edit().showLightContour = (float)enable; + } +} +bool LightingModel::isShowLightContourEnabled() const { + return (bool)_parametersBuffer.get().showLightContour; +} MakeLightingModel::MakeLightingModel() { _lightingModel = std::make_shared(); @@ -126,6 +133,7 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setDirectionalLight(config.enableDirectionalLight); _lightingModel->setPointLight(config.enablePointLight); _lightingModel->setSpotLight(config.enableSpotLight); + _lightingModel->setShowLightContour(config.showLightContour); } void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 384110bc70..d8325925fe 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -52,6 +52,8 @@ public: void setSpotLight(bool enable); bool isSpotLightEnabled() const; + void setShowLightContour(bool enable); + bool isShowLightContourEnabled() const; UniformBufferView getParametersBuffer() const { return _parametersBuffer; } @@ -77,6 +79,9 @@ protected: float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; + float showLightContour{ 1.0f }; + glm::vec3 spares{ 0.0f }; + Parameters() {} }; UniformBufferView _parametersBuffer; @@ -104,6 +109,8 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty) + Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty) + public: MakeLightingModelConfig() : render::Job::Config() {} // Make Lighting Model is always on @@ -121,6 +128,8 @@ public: bool enablePointLight{ true }; bool enableSpotLight{ true }; + bool showLightContour{ true }; + signals: void dirty(); }; diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index cd25353f60..e0c7ce779f 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -17,6 +17,7 @@ struct LightingModel { vec4 _UnlitShadedEmissiveLightmap; vec4 _ScatteringDiffuseSpecular; vec4 _AmbientDirectionalPointSpot; + vec4 _ShowContour; }; uniform lightingModelBuffer { @@ -59,6 +60,117 @@ float isSpotEnabled() { return lightingModel._AmbientDirectionalPointSpot.w; } +float isShowContour() { + return lightingModel._ShowContour.x; +} + <@endfunc@> + + +<@func declareEvalPBRShading()@> + +vec3 fresnelSchlick(vec3 fresnelColor, vec3 lightDir, vec3 halfDir) { + return fresnelColor + (1.0 - fresnelColor) * pow(1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0), 5); +} + +float specularDistribution(float roughness, vec3 normal, vec3 halfDir) { + float ndoth = clamp(dot(halfDir, normal), 0.0, 1.0); + float gloss2 = pow(0.001 + roughness, 4); + float denom = (ndoth * ndoth*(gloss2 - 1) + 1); + float power = gloss2 / (3.14159 * denom * denom); + return power; +} + +// Frag Shading returns the diffuse amount as W and the specular rgb as xyz +vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 fresnel, float roughness) { + // Diffuse Lighting + float diffuse = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + float power = specularDistribution(roughness, fragNormal, halfDir); + vec3 specular = power * fresnelColor * diffuse; + + return vec4(specular, (1.0 - metallic) * diffuse * (1 - fresnelColor.x)); +} +<@endfunc@> + +<$declareEvalPBRShading()$> + +// Return xyz the specular/reflection component and w the diffuse component +vec4 evalFragShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 specular, float roughness) { + return evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, specular, roughness); +} + 0.0) { + vec3 h = L + V; + vec3 H = normalize(h); + float ndoth = dot(N, H); + float PH = fetchSpecularBeckmann(ndoth, roughness); + float F = fresnelReflectance(H, V, 0.028); + float frSpec = max(PH * F / dot(h, h), 0.0); + result = ndotl * intensity * frSpec; + } + + return result; +} + +// Eval shading +vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); +float scatteringLevel = getScatteringLevel(); +vec4 shading; +float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); +{ // Key Sun Lighting + // Diffuse Lighting + //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + + float specular = skinSpecular(normal, fragLightDir, fragEyeDir, roughness, 1.0); + + vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + float power = specularDistribution(roughness, normal, halfDir); + //vec3 specular = power * fresnelColor * standardDiffuse; + + shading = vec4(vec3(specular), (1 - fresnelColor.x)); +} + + +if (scatteringLevel < 0.1) { + brdf = vec3(standardDiffuse); +} +brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); + + +diffuse = albedo * brdf.xyz * lightEnergy; + +specular = shading.rgb * lightEnergy; + +!> + <@endif@> diff --git a/libraries/render-utils/src/overlay3D.slf b/libraries/render-utils/src/overlay3D.slf index 38f236e0b3..1c374b759b 100644 --- a/libraries/render-utils/src/overlay3D.slf +++ b/libraries/render-utils/src/overlay3D.slf @@ -11,7 +11,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include DeferredLighting.slh@> +<@include LightingModel.slh@> <@include model/Light.slh@> <@include gpu/Transform.slh@> diff --git a/libraries/render-utils/src/overlay3D_translucent.slf b/libraries/render-utils/src/overlay3D_translucent.slf index f8c18abf20..14c257c75e 100644 --- a/libraries/render-utils/src/overlay3D_translucent.slf +++ b/libraries/render-utils/src/overlay3D_translucent.slf @@ -12,7 +12,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include DeferredLighting.slh@> +<@include LightingModel.slh@> <@include model/Light.slh@> <@include gpu/Transform.slh@> diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index e9c6ee90a0..3012ad1ac2 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -5,7 +5,7 @@ // point_light.frag // fragment shader // -// Created by Andrzej Kapolka on 9/18/14. +// Created by Sam Gateau on 9/18/15. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -15,12 +15,18 @@ // Everything about deferred buffer <@include DeferredBufferRead.slh@> -//Everything about deferred lighting -<@include DeferredLighting.slh@> +<$declareDeferredCurvature()$> // Everything about light <@include model/Light.slh@> +<@include LightingModel.slh@> +<$declareLightingModel()$> + +<@include LightPoint.slh@> +<$declareLightingPoint(supportScattering)$> + + in vec4 _texCoord0; out vec4 _fragColor; @@ -36,8 +42,6 @@ void main(void) { discard; } - mat4 invViewMat = getViewInverse(); - // Kill if in front of the light volume float depth = frag.depthVal; if (depth < gl_FragCoord.z) { @@ -47,38 +51,41 @@ void main(void) { // Need the light now Light light = getLight(); - // Make the Light vector going from fragment to light center in world space + // Frag pos in world + mat4 invViewMat = getViewInverse(); vec4 fragPos = invViewMat * frag.position; - vec3 fragLightVec = getLightPosition(light) - fragPos.xyz; - // Kill if too far from the light center - if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) { + // Clip againgst the light volume and Make the Light vector going from fragment to light center in world space + vec4 fragLightVecLen2; + if (!clipFragToLightVolumePoint(light, fragPos.xyz, fragLightVecLen2)) { discard; } - // Allright we re valid in the volume - float fragLightDistance = length(fragLightVec); - vec3 fragLightDir = fragLightVec / fragLightDistance; - - // Eval shading - vec3 fragNormal = vec3(frag.normal); + // Frag to eye vec vec4 fragEyeVector = invViewMat * vec4(-frag.position.xyz, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); - // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance); - // Final Lighting color - vec3 fragColor = (shading.w * frag.diffuse + shading.xyz); - _fragColor = vec4(fragColor * radialAttenuation * getLightColor(light) * getLightIntensity(light) * frag.obscurance, 0.0); + vec3 diffuse; + vec3 specular; - if (getLightShowContour(light) > 0.0) { - // Show edge - float edge = abs(2.0 * ((getLightRadius(light) - fragLightDistance) / (0.1)) - 1.0); - if (edge < 1) { - float edgeCoord = exp2(-8.0*edge*edge); - _fragColor = vec4(edgeCoord * edgeCoord * getLightShowContour(light) * getLightColor(light), 0.0); - } + if ((isScatteringEnabled() > 0.0) && (frag.mode == FRAG_MODE_SCATTERING)) { + vec4 blurredCurvature = fetchCurvature(texCoord); + vec4 diffusedCurvature = fetchDiffusedCurvature(texCoord); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + float highCurvature = unpackCurvature(blurredCurvature.w); + float lowCurvature = unpackCurvature(diffusedCurvature.w); + evalLightingPointScattering(diffuse, specular, light, + fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, + frag.metallic, frag.specular, frag.diffuse, 1.0, + frag.scattering, midNormal, lowNormal, lowCurvature); + } else { + evalLightingPoint(diffuse, specular, light, + fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, + frag.metallic, frag.specular, frag.diffuse, 1.0); } + + _fragColor.rgb += diffuse * isDiffuseEnabled() * isPointEnabled(); + _fragColor.rgb += specular * isSpecularEnabled() * isPointEnabled(); } diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index bf046a613b..5654b86d68 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -5,7 +5,7 @@ // spot_light.frag // fragment shader // -// Created by Andrzej Kapolka on 9/18/14. +// Created by Sam Gateau on 9/18/15. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -15,12 +15,18 @@ // Everything about deferred buffer <@include DeferredBufferRead.slh@> -//Everything about deferred lighting -<@include DeferredLighting.slh@> +<$declareDeferredCurvature()$> // Everything about light <@include model/Light.slh@> +<@include LightingModel.slh@> +<$declareLightingModel()$> + +<@include LightSpot.slh@> +<$declareLightingSpot(supportScattering)$> + + in vec4 _texCoord0; out vec4 _fragColor; @@ -36,8 +42,6 @@ void main(void) { discard; } - mat4 invViewMat = getViewInverse(); - // Kill if in front of the light volume float depth = frag.depthVal; if (depth < gl_FragCoord.z) { @@ -47,50 +51,43 @@ void main(void) { // Need the light now Light light = getLight(); - // Make the Light vector going from fragment to light center in world space + // Frag pos in world + mat4 invViewMat = getViewInverse(); vec4 fragPos = invViewMat * frag.position; - vec3 fragLightVec = getLightPosition(light) - fragPos.xyz; - // Kill if too far from the light center - if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) { + // Clip againgst the light volume and Make the Light vector going from fragment to light center in world space + vec4 fragLightVecLen2; + vec4 fragLightDirLen; + float cosSpotAngle; + if (!clipFragToLightVolumeSpot(light, fragPos.xyz, fragLightVecLen2, fragLightDirLen, cosSpotAngle)) { discard; } - // Allright we re valid in the volume - float fragLightDistance = length(fragLightVec); - vec3 fragLightDir = fragLightVec / fragLightDistance; - - // Kill if not in the spot light (ah ah !) - vec3 lightSpotDir = getLightDirection(light); - float cosSpotAngle = max(-dot(fragLightDir, lightSpotDir), 0.0); - if (cosSpotAngle < getLightSpotAngleCos(light)) { - discard; - } - - // Eval shading - vec3 fragNormal = vec3(frag.normal); + // Frag to eye vec vec4 fragEyeVector = invViewMat * vec4(-frag.position.xyz, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - vec4 shading = evalFragShading(fragNormal, fragLightDir, fragEyeDir, frag.metallic, frag.specular, frag.roughness); - - // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance); - float angularAttenuation = evalLightSpotAttenuation(light, cosSpotAngle); - // Final Lighting color - vec3 fragColor = (shading.w * frag.diffuse + shading.xyz); - _fragColor = vec4(fragColor * angularAttenuation * radialAttenuation * getLightColor(light) * getLightIntensity(light) * frag.obscurance, 0.0); - if (getLightShowContour(light) > 0.0) { - // Show edges - float edgeDistR = (getLightRadius(light) - fragLightDistance); - float edgeDistS = dot(fragLightDistance * vec2(cosSpotAngle, sqrt(1.0 - cosSpotAngle * cosSpotAngle)), -getLightSpotOutsideNormal2(light)); - float edgeDist = min(edgeDistR, edgeDistS); - float edge = abs(2.0 * (edgeDist / (0.1)) - 1.0); - if (edge < 1) { - float edgeCoord = exp2(-8.0*edge*edge); - _fragColor = vec4(edgeCoord * edgeCoord * getLightColor(light), 0.0); - } + vec3 diffuse; + vec3 specular; + if ((isScatteringEnabled() > 0.0) && (frag.mode == FRAG_MODE_SCATTERING)) { + vec4 blurredCurvature = fetchCurvature(texCoord); + vec4 diffusedCurvature = fetchDiffusedCurvature(texCoord); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); + float highCurvature = unpackCurvature(blurredCurvature.w); + float lowCurvature = unpackCurvature(diffusedCurvature.w); + evalLightingSpotScattering(diffuse, specular, light, + fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, + frag.metallic, frag.specular, frag.diffuse, 1.0, + frag.scattering, midNormal, lowNormal, lowCurvature); + } else { + evalLightingSpot(diffuse, specular, light, + fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, + frag.metallic, frag.specular, frag.diffuse, 1.0); } + + _fragColor.rgb += diffuse * isDiffuseEnabled() * isSpotEnabled(); + _fragColor.rgb += specular * isSpecularEnabled() * isSpotEnabled(); } diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 5245992ec4..6c5128830e 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -69,6 +69,10 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p auto locations = std::make_shared(); locations->normalFittingMapUnit = program->getTextures().findLocation("normalFittingMap"); + if (program->getTextures().findLocation("normalFittingMap") > -1) { + locations->normalFittingMapUnit = program->getTextures().findLocation("normalFittingMap"); + + } locations->albedoTextureUnit = program->getTextures().findLocation("albedoMap"); locations->roughnessTextureUnit = program->getTextures().findLocation("roughnessMap"); locations->normalTextureUnit = program->getTextures().findLocation("normalMap"); From d05a27e0e6ad9fe36dee768b9f9ed5e31718812a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 6 Jul 2016 17:27:53 -0700 Subject: [PATCH 0935/1237] embed image locally --- interface/resources/images/preview.png | Bin 0 -> 112290 bytes .../display-plugins/hmd/HmdDisplayPlugin.cpp | 54 ++++++------------ .../display-plugins/hmd/HmdDisplayPlugin.h | 5 -- 3 files changed, 17 insertions(+), 42 deletions(-) create mode 100644 interface/resources/images/preview.png diff --git a/interface/resources/images/preview.png b/interface/resources/images/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..faebbfce8f89722c12643387a7bab30c7416ac05 GIT binary patch literal 112290 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�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 S6zzXCC?)tKSQ*sx)&Bt}KR+Y@ literal 0 HcmV?d00001 diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index f1f300c72b..c2497e5740 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -21,12 +21,11 @@ #include #include -#include -#include -#include #include #include +#include + #include "../Logging.h" #include "../CompositorHelper.h" @@ -65,33 +64,24 @@ bool HmdDisplayPlugin::internalActivate() { }); if (_previewTextureID == 0) { - const QUrl previewURL("https://hifi-content.s3.amazonaws.com/samuel/preview.png"); - QNetworkAccessManager& manager = NetworkAccessManager::getInstance(); - QNetworkRequest request(previewURL); - request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - auto rep = manager.get(request); - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + QImage previewTexture(PathUtils::resourcesPath() + "images/preview.png"); + if (!previewTexture.isNull()) { + glGenTextures(1, &_previewTextureID); + glBindTexture(GL_TEXTURE_2D, _previewTextureID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, previewTexture.width(), previewTexture.height(), 0, + GL_BGRA, GL_UNSIGNED_BYTE, previewTexture.mirrored(false, true).bits()); + using namespace oglplus; + Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); + Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); + glBindTexture(GL_TEXTURE_2D, 0); + _previewAspect = ((float)previewTexture.width())/((float)previewTexture.height()); + _firstPreview = true; + } } return Parent::internalActivate(); } -void HmdDisplayPlugin::downloadFinished() { - QNetworkReply* reply = static_cast(sender()); - - if (reply->error() != QNetworkReply::NetworkError::NoError) { - qDebug() << "HMDDisplayPlugin: error downloading preview image" << reply->errorString(); - return; - } - - _previewTexture.loadFromData(reply->readAll()); - - if (!_previewTexture.isNull()) { - _previewAspect = ((float)_previewTexture.width())/((float)_previewTexture.height()); - _firstPreview = true; - } -} - void HmdDisplayPlugin::internalDeactivate() { if (_previewTextureID != 0) { glDeleteTextures(1, &_previewTextureID); @@ -427,19 +417,8 @@ void HmdDisplayPlugin::internalPresent() { }); swapBuffers(); } else if (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio) { - if (_firstPreview) { - glGenTextures(1, &_previewTextureID); - glBindTexture(GL_TEXTURE_2D, _previewTextureID); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _previewTexture.width(), _previewTexture.height(), 0, - GL_BGRA, GL_UNSIGNED_BYTE, _previewTexture.mirrored(false, true).bits()); - using namespace oglplus; - Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); - Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); - glBindTexture(GL_TEXTURE_2D, 0); - _firstPreview = false; - } useProgram(_previewProgram); - glEnable (GL_BLEND); + glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -449,6 +428,7 @@ void HmdDisplayPlugin::internalPresent() { glBindTexture(GL_TEXTURE_2D, _previewTextureID); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); swapBuffers(); + _firstPreview = false; _prevWindowSize = windowSize; _prevDevicePixelRatio = devicePixelRatio; } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 61b352b17b..8e48690fd1 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -15,7 +15,6 @@ #include "../OpenGLDisplayPlugin.h" class HmdDisplayPlugin : public OpenGLDisplayPlugin { - Q_OBJECT using Parent = OpenGLDisplayPlugin; public: bool isHmd() const override final { return true; } @@ -87,9 +86,6 @@ protected: FrameInfo _currentPresentFrameInfo; FrameInfo _currentRenderFrameInfo; -public slots: - void downloadFinished(); - private: bool _enablePreview { false }; bool _monoPreview { true }; @@ -97,7 +93,6 @@ private: bool _firstPreview { true }; ProgramPtr _previewProgram; - QImage _previewTexture; float _previewAspect { 0 }; GLuint _previewTextureID { 0 }; glm::uvec2 _prevWindowSize { 0, 0 }; From 1728878982d165b2c232c73d7e3402e98e465fd5 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 17:52:18 -0700 Subject: [PATCH 0936/1237] working smooth arrival --- scripts/system/controllers/teleport.js | 77 +++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 2345ac7384..b2e462dd4a 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,6 +16,23 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; + +//slow +var SMOOTH_ARRIVAL_SPACING = 150; +var NUMBER_OF_STEPS = 2; + +//medium-slow +var SMOOTH_ARRIVAL_SPACING = 100; +var NUMBER_OF_STEPS = 4; + +//medium-fast +var SMOOTH_ARRIVAL_SPACING = 33; +var NUMBER_OF_STEPS = 6; + +//fast +var SMOOTH_ARRIVAL_SPACING = 10; +var NUMBER_OF_STEPS = 20; + var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -101,8 +118,8 @@ function Teleporter() { }; - this.findMidpoint = function(handPosition, intersection) { - var xy = Vec3.sum(handPosition, intersection.intersection); + this.findMidpoint = function(start, end) { + var xy = Vec3.sum(start, end); var midpoint = Vec3.multiply(0.5, xy); return midpoint }; @@ -196,7 +213,7 @@ function Teleporter() { this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); - this.deleteTargetOverlay(); + this.enableGrab(); Script.setTimeout(function() { inTeleportMode = false; @@ -396,7 +413,7 @@ function Teleporter() { }; this.updateTargetOverlay = function(intersection) { - this.intersection = intersection; + _this.intersection = intersection; var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP) var euler = Quat.safeEulerAngles(rotation) @@ -427,16 +444,60 @@ function Teleporter() { this.teleport = function(value) { - if (_this.intersection !== null) { + var offset = getAvatarFootOffset(); _this.intersection.intersection.y += offset; - MyAvatar.position = _this.intersection.intersection; + // MyAvatar.position = _this.intersection.intersection; + this.exitTeleportMode(); + this.smoothArrival(); + } - this.triggerHaptics(); - this.exitTeleportMode(); }; + + this.getArrivalPoints = function(startPoint, endPoint) { + var arrivalPoints = []; + + + var i; + var lastPoint; + + for (i = 0; i < NUMBER_OF_STEPS; i++) { + if (i === 0) { + lastPoint = startPoint; + } + var newPoint = _this.findMidpoint(lastPoint, endPoint); + lastPoint = newPoint; + arrivalPoints.push(newPoint); + } + + arrivalPoints.push(endPoint) + + return arrivalPoints + }; + + this.smoothArrival = function() { + + _this.arrivalPoints = _this.getArrivalPoints(MyAvatar.position, _this.intersection.intersection); + print('ARRIVAL POINTS: ' + JSON.stringify(_this.arrivalPoints)); + print('end point: ' + JSON.stringify(_this.intersection.intersection)) + _this.smoothArrivalInterval = Script.setInterval(function() { + print(_this.arrivalPoints.length+" arrival points remaining") + if (_this.arrivalPoints.length === 0) { + Script.clearInterval(_this.smoothArrivalInterval); + _this.triggerHaptics(); + _this.deleteTargetOverlay(); + return; + } + + var landingPoint = _this.arrivalPoints.shift(); + print('landing at: ' + JSON.stringify(landingPoint)) + MyAvatar.position =landingPoint; + + + }, SMOOTH_ARRIVAL_SPACING) + } } From 5220b52cd9de0e8ebcd957b7829c2cda7d5c64cb Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 4 Jul 2016 21:20:42 -0700 Subject: [PATCH 0937/1237] Don't trigger entity scripts without scripting interface --- .../src/EntityTreeRenderer.cpp | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 56f6438e70..1ec934be92 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -291,7 +291,9 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() { foreach(const EntityItemID& entityID, _currentEntitiesInside) { if (!entitiesContainingAvatar.contains(entityID)) { emit leaveEntity(entityID); - _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + } } } @@ -299,7 +301,9 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() { foreach(const EntityItemID& entityID, entitiesContainingAvatar) { if (!_currentEntitiesInside.contains(entityID)) { emit enterEntity(entityID); - _entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity"); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity"); + } } } _currentEntitiesInside = entitiesContainingAvatar; @@ -315,7 +319,9 @@ void EntityTreeRenderer::leaveAllEntities() { // for all of our previous containing entities, if they are no longer containing then send them a leave event foreach(const EntityItemID& entityID, _currentEntitiesInside) { emit leaveEntity(entityID); - _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + } } _currentEntitiesInside.clear(); forceRecheckEntities(); @@ -652,11 +658,15 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { } emit mousePressOnEntity(rayPickResult, event); - _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mousePressOnEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mousePressOnEntity", MouseEvent(*event)); + } _currentClickingOnEntityID = rayPickResult.entityID; emit clickDownOnEntity(_currentClickingOnEntityID, MouseEvent(*event)); - _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "clickDownOnEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "clickDownOnEntity", MouseEvent(*event)); + } } else { emit mousePressOffEntity(rayPickResult, event); } @@ -677,14 +687,18 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { if (rayPickResult.intersects) { //qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID; emit mouseReleaseOnEntity(rayPickResult, event); - _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseReleaseOnEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseReleaseOnEntity", MouseEvent(*event)); + } } // Even if we're no longer intersecting with an entity, if we started clicking on it, and now // we're releasing the button, then this is considered a clickOn event if (!_currentClickingOnEntityID.isInvalidID()) { emit clickReleaseOnEntity(_currentClickingOnEntityID, MouseEvent(*event)); - _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "clickReleaseOnEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "clickReleaseOnEntity", MouseEvent(*event)); + } } // makes it the unknown ID, we just released so we can't be clicking on anything @@ -707,8 +721,10 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::TryLock, precisionPicking); if (rayPickResult.intersects) { - _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveEvent", MouseEvent(*event)); - _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveOnEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveEvent", MouseEvent(*event)); + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveOnEntity", MouseEvent(*event)); + } // handle the hover logic... @@ -716,19 +732,25 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { // then we need to send the hover leave. if (!_currentHoverOverEntityID.isInvalidID() && rayPickResult.entityID != _currentHoverOverEntityID) { emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event)); - _entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event)); + } } // If the new hover entity does not match the previous hover entity then we are entering the new one // this is true if the _currentHoverOverEntityID is known or unknown if (rayPickResult.entityID != _currentHoverOverEntityID) { - _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverEnterEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverEnterEntity", MouseEvent(*event)); + } } // and finally, no matter what, if we're intersecting an entity then we're definitely hovering over it, and // we should send our hover over event emit hoverOverEntity(rayPickResult.entityID, MouseEvent(*event)); - _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverOverEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverOverEntity", MouseEvent(*event)); + } // remember what we're hovering over _currentHoverOverEntityID = rayPickResult.entityID; @@ -739,7 +761,9 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { // send the hover leave for our previous entity if (!_currentHoverOverEntityID.isInvalidID()) { emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event)); - _entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event)); + } _currentHoverOverEntityID = UNKNOWN_ENTITY_ID; // makes it the unknown ID } } @@ -748,14 +772,16 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { // not yet released the hold then this is still considered a holdingClickOnEntity event if (!_currentClickingOnEntityID.isInvalidID()) { emit holdingClickOnEntity(_currentClickingOnEntityID, MouseEvent(*event)); - _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "holdingClickOnEntity", MouseEvent(*event)); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "holdingClickOnEntity", MouseEvent(*event)); + } } _lastMouseEvent = MouseEvent(*event); _lastMouseEventValid = true; } void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { - if (_tree && !_shuttingDown) { + if (_tree && !_shuttingDown && _entitiesScriptEngine) { _entitiesScriptEngine->unloadEntityScript(entityID); } @@ -801,7 +827,7 @@ void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) { if (_tree && !_shuttingDown) { EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); - if (entity && entity->shouldPreloadScript()) { + if (entity && entity->shouldPreloadScript() && _entitiesScriptEngine) { QString scriptUrl = entity->getScript(); scriptUrl = ResourceManager::normalizeURL(scriptUrl); ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload); @@ -910,12 +936,16 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons // And now the entity scripts if (isCollisionOwner(myNodeID, entityTree, idA, collision)) { emit collisionWithEntity(idA, idB, collision); - _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); + } } if (isCollisionOwner(myNodeID, entityTree, idA, collision)) { emit collisionWithEntity(idB, idA, collision); - _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); + } } } @@ -941,7 +971,9 @@ void EntityTreeRenderer::updateZone(const EntityItemID& id) { if (zone && zone->contains(_lastAvatarPosition)) { _currentEntitiesInside << id; emit enterEntity(id); - _entitiesScriptEngine->callEntityScriptMethod(id, "enterEntity"); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(id, "enterEntity"); + } if (zone->getVisible()) { _bestZone = std::dynamic_pointer_cast(zone); } From 5626938fb40b97c59ff130d61a8a59bf9c5481d0 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 18:13:42 -0700 Subject: [PATCH 0938/1237] add instant mode --- scripts/system/controllers/teleport.js | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b2e462dd4a..c985259d21 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,22 +16,23 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; - +var NUMBER_OF_STEPS=0; +var SMOOTH_ARRIVAL_SPACING=0; //slow -var SMOOTH_ARRIVAL_SPACING = 150; -var NUMBER_OF_STEPS = 2; +// var SMOOTH_ARRIVAL_SPACING = 150; +// var NUMBER_OF_STEPS = 2; //medium-slow -var SMOOTH_ARRIVAL_SPACING = 100; -var NUMBER_OF_STEPS = 4; +// var SMOOTH_ARRIVAL_SPACING = 100; +// var NUMBER_OF_STEPS = 4; -//medium-fast -var SMOOTH_ARRIVAL_SPACING = 33; -var NUMBER_OF_STEPS = 6; +// //medium-fast +// var SMOOTH_ARRIVAL_SPACING = 33; +// var NUMBER_OF_STEPS = 6; -//fast -var SMOOTH_ARRIVAL_SPACING = 10; -var NUMBER_OF_STEPS = 20; +// //fast +// var SMOOTH_ARRIVAL_SPACING = 10; +// var NUMBER_OF_STEPS = 20; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -449,9 +450,9 @@ function Teleporter() { var offset = getAvatarFootOffset(); _this.intersection.intersection.y += offset; // MyAvatar.position = _this.intersection.intersection; - this.exitTeleportMode(); + this.exitTeleportMode(); this.smoothArrival(); - + } }; @@ -483,7 +484,7 @@ function Teleporter() { print('ARRIVAL POINTS: ' + JSON.stringify(_this.arrivalPoints)); print('end point: ' + JSON.stringify(_this.intersection.intersection)) _this.smoothArrivalInterval = Script.setInterval(function() { - print(_this.arrivalPoints.length+" arrival points remaining") + print(_this.arrivalPoints.length + " arrival points remaining") if (_this.arrivalPoints.length === 0) { Script.clearInterval(_this.smoothArrivalInterval); _this.triggerHaptics(); @@ -493,7 +494,7 @@ function Teleporter() { var landingPoint = _this.arrivalPoints.shift(); print('landing at: ' + JSON.stringify(landingPoint)) - MyAvatar.position =landingPoint; + MyAvatar.position = landingPoint; }, SMOOTH_ARRIVAL_SPACING) From 54e0c131a562474b38968041d380d4ee9b0a40c8 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 18:30:50 -0700 Subject: [PATCH 0939/1237] cleanup and protect against some interval naughtiness --- scripts/system/controllers/teleport.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index c985259d21..f52cd55a84 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,11 +16,11 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; -var NUMBER_OF_STEPS=0; -var SMOOTH_ARRIVAL_SPACING=0; -//slow -// var SMOOTH_ARRIVAL_SPACING = 150; -// var NUMBER_OF_STEPS = 2; +// var NUMBER_OF_STEPS = 0; +// var SMOOTH_ARRIVAL_SPACING = 0; +// slow +var SMOOTH_ARRIVAL_SPACING = 150; +var NUMBER_OF_STEPS = 2; //medium-slow // var SMOOTH_ARRIVAL_SPACING = 100; @@ -77,6 +77,7 @@ function Teleporter() { this.leftOverlayLine = null; this.targetOverlay = null; this.updateConnected = null; + this.smoothArrivalInterval=null; this.initialize = function() { this.createMappings(); @@ -111,6 +112,9 @@ function Teleporter() { return; } + if(this.smoothArrivalInterval!==null){ + Script.clearInterval(this.smoothArrivalInterval); + } inTeleportMode = true; this.teleportHand = hand; this.initialize(); @@ -487,14 +491,17 @@ function Teleporter() { print(_this.arrivalPoints.length + " arrival points remaining") if (_this.arrivalPoints.length === 0) { Script.clearInterval(_this.smoothArrivalInterval); - _this.triggerHaptics(); - _this.deleteTargetOverlay(); return; } var landingPoint = _this.arrivalPoints.shift(); print('landing at: ' + JSON.stringify(landingPoint)) MyAvatar.position = landingPoint; + if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { + print('clear target overlay') + _this.deleteTargetOverlay(); + _this.triggerHaptics(); + } }, SMOOTH_ARRIVAL_SPACING) From d2fedee6563ff9b27b23f844a5a030e130436d79 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 7 Jul 2016 10:22:52 -0700 Subject: [PATCH 0940/1237] Add missing loading images for Asset Browser --- interface/resources/images/Loading-Inner-H.png | Bin 0 -> 1551 bytes .../resources/images/Loading-Outer-Ring.png | Bin 0 -> 2638 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 interface/resources/images/Loading-Inner-H.png create mode 100644 interface/resources/images/Loading-Outer-Ring.png diff --git a/interface/resources/images/Loading-Inner-H.png b/interface/resources/images/Loading-Inner-H.png new file mode 100644 index 0000000000000000000000000000000000000000..cf09ea8317499d4d0e1bc9af5e162eab3daf0dd1 GIT binary patch literal 1551 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE{(JaZG%Q-e|yQz{EjrrIztFl%InM3hAM`dB6B=jtVb)aX^@7BGN-jeSKyVsdtB zi9%9pdS;%j()-=}l@u~lY?Z=IeGPmIoKrJ0J*tXQgRA^PlB=?lEmM^2?G$V(tSWK~ za#KqZ6)JLb@`|l0Y?Z*~TICg6frRyy6u?SKvTcwn`Gt*5rG&EE& zx70HGzDQNg6CTHe>1U13Be0{Av^NLFn^O93NU2K&qatrh_GgGWwj17%U-Hgm!TuqD& z4P8y09G#t=oL!xaEX@ra4V>IydR_99OLJ56N?>|ZGE=M^O)T9kT%3$tTulwJ>IEeT zP@q`3q!wkCrKY$Q<>xAZJ#LkW+bt%z-2%~@g2gRRf0*Lbt8b@ZqYsK+r09kT1-ZC^ znDE4Fr+`TQsd*{3N<~Wcc9V=QgfK8L)q1)(hE&{oGuziABv7Q`d8VF|uh1P;-KMO| z$6O_@D6y%Dy9u)XeHnDYr0DS~`7E)(?k1JBuG*~&1bD=g47kOegz}{7kOduA{QCouUma2hbG z9bf@-N~X!)u07$| zuh?~r;gq&TyrSJ(!#t76M?*BbEY z7Ub{NH|xmqpU_q_wfo%Xb9cW~+!eVxlZ(Ihq0L&)Gt6~WcKa_H&)emaan5+26=Q$m zr~Vt0RKhxEu>MuV?KF`ronY<2H#d&jX%$&-FOAaJ^`V z{L#?jm~m;B{04cq*sSiB_6`2s4ZAOJ=Rd6wk7-U-IpeTy+FfNnMpw6woEC4E8CU4N zX1@L+&$Hm3a^eE9yUN!h894JFDAm7fwcen`FFS=r!Sg;| zQFS^x;m0m9#>de<>$Sw3^V(m;Ji8vA9rKfC`@<@c|N0x^du}A1fJ8G=oG;jr!)3sb XyMAls+O}COpd#AS)z4*}Q$iB}Kk_vn literal 0 HcmV?d00001 diff --git a/interface/resources/images/Loading-Outer-Ring.png b/interface/resources/images/Loading-Outer-Ring.png new file mode 100644 index 0000000000000000000000000000000000000000..52613949e51e66c0a439dfb6e2efe4084e6d1abe GIT binary patch literal 2638 zcmaJ@doigsCd)Io`de>h2dG_<{z5jSqeVD7~YZz(( z0OotFVfX<6T$EQ`6#!7oi|hpe7)acLB>o6j62lU~fW|?#z>o)@wH5Y*S)AC&o3IlA zvzGG$f+RuSUSu}Hw_z!BY@~c4P#!0$RLEjSz!GQ+yp<=QU`K1~u@H|#!3NrUSNy$>akPk~(kd(hoASO#G*e|?fftaie&`VZy(4N5y6nXje|9tKp;SlBpZVLDqEtx6-2}n>~VM!j)1qu6UYQSnTUtJ9;`Fu zB;s(%ehl}owL}~)1sf)j2+25HOiYYTjI9kK+KMBPNF*Gdh$9lMMI5fRI94EGNv#E9 zvl#{kEM|*%LJ1EMKuSi|79>hS!D44D@P*%K1>&!5Vk3N}g5bfQGM24Fkkw9>> zr_)?r>8@^cBFT|J!_(boundG9#fJrw87${J*7k>3c7a@G;i*YjV z$6D||_rJzH6Y^uYE1%z%*BSc0{;+^U!HTd{=T}o0R{&Hm zc`#@J(yo^qL4rS-McR>B5N6jHar)$)ST!Oa+_Wz-sH3!^j9of2+!3e>-AGB zKHNI&_D7;z{|G6kKEzfaFvvLS%8%J|)a-R*C8wh!2K8mA>` zKU~u^u`9A;Vq(_S;+^QLo+W`lDe@c?0)=j zaM=m@Q)-;{1Q?42SF8;{?~{x0olTw()BemgeVZSy>#$DSXrXORd-y1R9$OQGsm~24 zo?O3TV7k)Ib@U$Ge65d7R!9`v6d{Vi#E#87Dzq;wa9L&K16(=+|6{eOur@wkZ|n&h zYi9ArBh}m2*Y^p2|Ktl?aS6ZuLwC-kww3zT2BY9{MLQ|f4BT}d1lt~c>`guP;6aSg zEKf(zh#l`$Mm^K<_q&~Knh8agGHp${eZx6o z92^9c(hCtbzc|Wdz0E3@cE2@QJUu%^1qzzhbVVR318%WPKI<=uraTEDw}On3U1 zoRFS*vu%hS$T5gupk%?u<_`BCvJWmys!9u2WIlWA3}{0OffsM+yu5EX#7(;6 z0lEcG#rnrKlufFD@z{*cmgsHNX6U1ayo3{e{RjwMwp-U0Uy@FHG-^xSq1^(tt^`5X z&U|nxs3Zcg&phjWBPBFF5d4<1dS|wI!oWDnVHZLA%j$yO6FQ)@_a54OgGlFf?vk5Q z%M-LL0IW8(FJ96dL_{lM9I($?LeBzF+AU^Vg)*yCEFx|7VVdHv;9S)US=sq&CDj@q z?#?IP)&?>Q2NO&Ko@;<^^STy{24ffY8nJser+~J5v4;V0Dj9!vW9|C-d8nND45m6b zH`or&v81eXeM9Fl88jox79~9SGJmqN0yn)Nog+u3FryV#j za7#-j$#^>#w1wMkf804B8n~e)+eQ>R=q# z8;@nol|IgOuf>^@GfGZdj!2?McGk(}(EG~eMSHziNMGz}pm_B8%n=U8&-Kb@cg=Zk zK5Hdu$SguC(@RAaBYtJmC`4E9y~v{Jgr)Ozz(`jp$&{N}Xt<^~K{E-LK{S5uzjp0w zvW?rdhN7yv;rN$&8tA@q+EX2Elh)YGJ8|H2gIUO}TP+`tWzYwEZo30hdShe%UeOFke{I`{N^h|2qJ-2UvXaPF)-blIbmzjmed610s_*i3hL)S-M_E_ig;H3pfP=Igc zq$2lPR_V({zT;7OO9R_7Gh0s$z;KyoX!T%B>iGrTIHx);FON|8Yu@y_Jz5mw6Q=D} zt=dJ~>4C=wFu38@Q}PEU-Bh(c{!y&f@arS=ca18`4h3?V`280JDS_PA!BfY2j~c8s zjpyyHc5LaWs#)Ri$MM@$H6C79#E}as>9!rn1OWAW^eoD+59a6Nm~m$3em=GSQ$oB` d(-0M<3fw&0pB%08ysP}-dAKnd=jow4{{g+kQvd(} literal 0 HcmV?d00001 From 38f0cd218bc15e2c1d1ba80dc0d47d317d2c3a68 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 7 Jul 2016 10:31:12 -0700 Subject: [PATCH 0941/1237] make client server plugins properly copy into plugins directory --- .../macros/SetupHifiClientServerPlugin.cmake | 54 +++++++++++++++++++ plugins/pcmCodec/CMakeLists.txt | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 cmake/macros/SetupHifiClientServerPlugin.cmake diff --git a/cmake/macros/SetupHifiClientServerPlugin.cmake b/cmake/macros/SetupHifiClientServerPlugin.cmake new file mode 100644 index 0000000000..5ace348e87 --- /dev/null +++ b/cmake/macros/SetupHifiClientServerPlugin.cmake @@ -0,0 +1,54 @@ +# +# Created by Brad Hefta-Gaub on 2016/07/07 +# Copyright 2016 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html +# +macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) + set(${TARGET_NAME}_SHARED 1) + setup_hifi_library(${ARGV}) + add_dependencies(interface ${TARGET_NAME}) + set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Plugins") + + if (APPLE) + set(PLUGIN_PATH "${INTERFACE_BUNDLE_NAME}.app/Contents/PlugIns") + else() + set(PLUGIN_PATH "plugins") + endif() + + if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_GENERATOR STREQUAL "Unix Makefiles") + set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${PLUGIN_PATH}/") + set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/${PLUGIN_PATH}/") + else() + set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/$/${PLUGIN_PATH}/") + set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/$/${PLUGIN_PATH}/") + endif() + + # create the destination for the client plugin binaries + add_custom_command( + TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory + ${CLIENT_PLUGIN_FULL_PATH} + ) + # copy the client plugin binaries + add_custom_command(TARGET ${DIR} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy + "$" + ${CLIENT_PLUGIN_FULL_PATH} + ) + + # create the destination for the server plugin binaries + add_custom_command( + TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory + ${SERVER_PLUGIN_FULL_PATH} + ) + # copy the server plugin binaries + add_custom_command(TARGET ${DIR} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy + "$" + ${SERVER_PLUGIN_FULL_PATH} + ) + +endmacro() diff --git a/plugins/pcmCodec/CMakeLists.txt b/plugins/pcmCodec/CMakeLists.txt index 162fe557cc..5dca1f0e14 100644 --- a/plugins/pcmCodec/CMakeLists.txt +++ b/plugins/pcmCodec/CMakeLists.txt @@ -7,5 +7,5 @@ # set(TARGET_NAME pcmCodec) -setup_hifi_plugin() +setup_hifi_client_server_plugin() link_hifi_libraries(shared plugins) From 09f2a1061352eb4633fc814e044e71b8024b68ee Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Jul 2016 17:02:28 -0700 Subject: [PATCH 0942/1237] Add rendering performance tool --- tests/render-perf/CMakeLists.txt | 13 + tests/render-perf/src/main.cpp | 650 +++++++++++++++++++++++++++++++ 2 files changed, 663 insertions(+) create mode 100644 tests/render-perf/CMakeLists.txt create mode 100644 tests/render-perf/src/main.cpp diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt new file mode 100644 index 0000000000..611cadd595 --- /dev/null +++ b/tests/render-perf/CMakeLists.txt @@ -0,0 +1,13 @@ + +set(TARGET_NAME render-perf-test) + +# This is not a testcase -- just set it up as a regular hifi project +setup_hifi_project(Quick Gui OpenGL) +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") + +# link in the shared libraries +link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) + +package_libraries_for_deployment() + +target_bullet() diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp new file mode 100644 index 0000000000..c953e51700 --- /dev/null +++ b/tests/render-perf/src/main.cpp @@ -0,0 +1,650 @@ +// +// Created by Bradley Austin Davis on 2016/07/01 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +static const QString LAST_SCENE_KEY = "lastSceneFile"; + +class ParentFinder : public SpatialParentFinder { +public: + EntityTreePointer _tree; + ParentFinder(EntityTreePointer tree) : _tree(tree) {} + + SpatiallyNestableWeakPointer find(QUuid parentID, bool& success, SpatialParentTree* entityTree = nullptr) const override { + SpatiallyNestableWeakPointer parent; + + if (parentID.isNull()) { + success = true; + return parent; + } + + // search entities + if (entityTree) { + parent = entityTree->findByID(parentID); + } else { + parent = _tree ? _tree->findEntityByEntityItemID(parentID) : nullptr; + } + + if (!parent.expired()) { + success = true; + return parent; + } + + success = false; + return parent; + } +}; + +class Camera { +protected: + float fov { 60.0f }; + float znear { 0.1f }, zfar { 512.0f }; + float aspect { 1.0f }; + + void updateViewMatrix() { + matrices.view = glm::inverse(glm::translate(glm::mat4(), position) * glm::mat4_cast(getOrientation())); + } + + glm::quat getOrientation() const { + return glm::angleAxis(yaw, Vectors::UP); + } +public: + float yaw { 0 }; + glm::vec3 position; + + float rotationSpeed { 1.0f }; + float movementSpeed { 1.0f }; + + struct Matrices { + glm::mat4 perspective; + glm::mat4 view; + } matrices; + enum Key { + RIGHT, + LEFT, + UP, + DOWN, + BACK, + FORWARD, + KEYS_SIZE, + INVALID = -1, + }; + + std::bitset keys; + + Camera() { + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + } + + bool moving() { + return keys.any(); + } + + void setFieldOfView(float fov) { + this->fov = fov; + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + } + + void setAspectRatio(const glm::vec2& size) { + setAspectRatio(size.x / size.y); + } + + void setAspectRatio(float aspect) { + this->aspect = aspect; + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + } + + void setPerspective(float fov, const glm::vec2& size, float znear = 0.1f, float zfar = 512.0f) { + setPerspective(fov, size.x / size.y, znear, zfar); + } + + void setPerspective(float fov, float aspect, float znear = 0.1f, float zfar = 512.0f) { + this->aspect = aspect; + this->fov = fov; + this->znear = znear; + this->zfar = zfar; + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + }; + + void rotate(const float delta) { + yaw += delta; + updateViewMatrix(); + } + + void setPosition(const glm::vec3& position) { + this->position = position; + updateViewMatrix(); + } + + // Translate in the Z axis of the camera + void dolly(float delta) { + auto direction = glm::vec3(0, 0, delta); + translate(direction); + } + + // Translate in the XY plane of the camera + void translate(const glm::vec2& delta) { + auto move = glm::vec3(delta.x, delta.y, 0); + translate(move); + } + + void translate(const glm::vec3& delta) { + position += getOrientation() * delta; + updateViewMatrix(); + } + + void update(float deltaTime) { + if (moving()) { + glm::vec3 camFront = getOrientation() * Vectors::FRONT; + glm::vec3 camRight = getOrientation() * Vectors::RIGHT; + glm::vec3 camUp = getOrientation() * Vectors::UP; + float moveSpeed = deltaTime * movementSpeed; + + if (keys[FORWARD]) { + position += camFront * moveSpeed; + } + if (keys[BACK]) { + position -= camFront * moveSpeed; + } + if (keys[LEFT]) { + position -= camRight * moveSpeed; + } + if (keys[RIGHT]) { + position += camRight * moveSpeed; + } + if (keys[UP]) { + position += camUp * moveSpeed; + } + if (keys[DOWN]) { + position -= camUp * moveSpeed; + } + updateViewMatrix(); + } + } +}; + +class QWindowCamera : public Camera { + Key forKey(int key) { + switch (key) { + case Qt::Key_W: return FORWARD; + case Qt::Key_S: return BACK; + case Qt::Key_A: return LEFT; + case Qt::Key_D: return RIGHT; + case Qt::Key_E: return UP; + case Qt::Key_C: return DOWN; + default: break; + } + return INVALID; + } + + vec2 _lastMouse; +public: + void onKeyPress(QKeyEvent* event) { + Key k = forKey(event->key()); + if (k == INVALID) { + return; + } + keys.set(k); + } + + void onKeyRelease(QKeyEvent* event) { + Key k = forKey(event->key()); + if (k == INVALID) { + return; + } + keys.reset(k); + } + + void onMouseMove(QMouseEvent* event) { + vec2 mouse = toGlm(event->localPos()); + vec2 delta = mouse - _lastMouse; + auto buttons = event->buttons(); + if (buttons & Qt::RightButton) { + dolly(delta.y * 0.01f); + } else if (buttons & Qt::LeftButton) { + rotate(delta.x * -0.01f); + } else if (buttons & Qt::MiddleButton) { + delta.y *= -1.0f; + translate(delta * -0.01f); + } + + _lastMouse = mouse; + } +}; + +// Create a simple OpenGL window that renders text in various ways +class QTestWindow : public QWindow, public AbstractViewStateInterface { + Q_OBJECT + +protected: + void copyCurrentViewFrustum(ViewFrustum& viewOut) const override { + viewOut = _viewFrustum; + } + + void copyShadowViewFrustum(ViewFrustum& viewOut) const override { + viewOut = _shadowViewFrustum; + } + + QThread* getMainThread() override { + return QThread::currentThread(); + } + + PickRay computePickRay(float x, float y) const override { + return PickRay(); + } + + glm::vec3 getAvatarPosition() const override { + return vec3(); + } + + void postLambdaEvent(std::function f) override {} + qreal getDevicePixelRatio() override { + return 1.0f; + } + + render::ScenePointer getMain3DScene() override { + return _main3DScene; + } + render::EnginePointer getRenderEngine() override { + return _renderEngine; + } + + void pushPostUpdateLambda(void* key, std::function func) override {} + +public: + //"/-17.2049,-8.08629,-19.4153/0,0.881994,0,-0.47126" + static void setup() { + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, 0); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + } + + QTestWindow() { + AbstractViewStateInterface::setInstance(this); + _octree = DependencyManager::set(false, this, nullptr); + _octree->init(); + DependencyManager::set(_octree->getTree()); + getEntities()->setViewFrustum(_viewFrustum); + auto nodeList = DependencyManager::get(); + NodePermissions permissions; + permissions.setAll(true); + nodeList->setPermissions(permissions); + + ResourceManager::init(); + setSurfaceType(QSurface::OpenGLSurface); + auto format = getDefaultOpenGLSurfaceFormat(); + format.setOption(QSurfaceFormat::DebugContext); + setFormat(format); + + _context.setFormat(format); + _context.create(); + + show(); + makeCurrent(); + glewInit(); + glGetError(); +#ifdef Q_OS_WIN + wglSwapIntervalEXT(0); +#endif + _camera.movementSpeed = 3.0f; + + setupDebugLogger(this); + qDebug() << (const char*)glGetString(GL_VERSION); + + // GPU library init + { + gpu::Context::init(); + _gpuContext = std::make_shared(); + } + + // Render engine library init + { + makeCurrent(); + DependencyManager::get()->init(); + _renderEngine->addJob("RenderShadowTask", _cullFunctor); + _renderEngine->addJob("RenderDeferredTask", _cullFunctor); + _renderEngine->load(); + _renderEngine->registerScene(_main3DScene); + } + + QVariant lastScene = _settings.value(LAST_SCENE_KEY); + if (lastScene.isValid()) { + auto result = QMessageBox::question(nullptr, "Question", "Load last scene " + lastScene.toString()); + if (result != QMessageBox::No) { + importScene(lastScene.toString()); + } + } + + resize(QSize(800, 600)); + _elapsed.start(); + + QTimer* timer = new QTimer(this); + timer->setInterval(0); + connect(timer, &QTimer::timeout, this, [this] { + draw(); + }); + timer->start(); + } + + virtual ~QTestWindow() { + ResourceManager::cleanup(); + } + +protected: + void keyPressEvent(QKeyEvent* event) override { + switch (event->key()) { + case Qt::Key_F1: + importScene(); + return; + + case Qt::Key_F2: + goTo(); + return; + + default: + break; + } + _camera.onKeyPress(event); + } + + void keyReleaseEvent(QKeyEvent* event) override { + _camera.onKeyRelease(event); + } + + void mouseMoveEvent(QMouseEvent* event) override { + _camera.onMouseMove(event); + } + + void resizeEvent(QResizeEvent* ev) override { + resizeWindow(ev->size()); + } + +private: + + static bool cull(const RenderArgs* renderArgs, const AABox& box) { + return true; + } + + void update() { + auto now = usecTimestampNow(); + static auto last = now; + + float delta = now - last; + // Update the camera + _camera.update(delta / USECS_PER_SECOND); + + + // load the view frustum + { + _viewFrustum.setProjection(_camera.matrices.perspective); + auto view = glm::inverse(_camera.matrices.view); + _viewFrustum.setPosition(glm::vec3(view[3])); + _viewFrustum.setOrientation(glm::quat_cast(view)); + } + last = now; + } + + void draw() { + if (!isVisible()) { + return; + } + update(); + + makeCurrent(); +#define RENDER_SCENE 1 + +#if RENDER_SCENE + RenderArgs renderArgs(_gpuContext, _octree.data(), DEFAULT_OCTREE_SIZE_SCALE, + 0, RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + + auto framebufferCache = DependencyManager::get(); + QSize windowSize = size(); + framebufferCache->setFrameBufferSize(windowSize); + // Viewport is assigned to the size of the framebuffer + renderArgs._viewport = ivec4(0, 0, windowSize.width(), windowSize.height()); + + renderArgs.setViewFrustum(_viewFrustum); + + // Final framebuffer that will be handled to the display-plugin + { + auto finalFramebuffer = framebufferCache->getFramebuffer(); + renderArgs._blitFramebuffer = finalFramebuffer; + } + + render(&renderArgs); + + { + gpu::gl::GLFramebuffer* framebuffer = gpu::Backend::getGPUObject(*renderArgs._blitFramebuffer); + auto fbo = framebuffer->_id; + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + const auto& vp = renderArgs._viewport; + glBlitFramebuffer(vp.x, vp.y, vp.z, vp.w, vp.x, vp.y, vp.z, vp.w, GL_COLOR_BUFFER_BIT, GL_NEAREST); + } +#else + glClearColor(0.0f, 0.5f, 0.8f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); +#endif + + _context.swapBuffers(this); + +#if RENDER_SCENE + framebufferCache->releaseFramebuffer(renderArgs._blitFramebuffer); + renderArgs._blitFramebuffer.reset(); + gpu::doInBatch(renderArgs._context, [&](gpu::Batch& batch) { + batch.resetStages(); + }); +#endif + fps.increment(); + static size_t _frameCount { 0 }; + ++_frameCount; + if (_elapsed.elapsed() >= 4000) { + qDebug() << "FPS " << fps.rate(); + _frameCount = 0; + _elapsed.restart(); + } + + if (0 == ++_frameCount % 100) { + } + } + + void render(RenderArgs* renderArgs) { + PROFILE_RANGE(__FUNCTION__); + PerformanceTimer perfTimer("draw"); + // The pending changes collecting the changes here + render::PendingChanges pendingChanges; + // Setup the current Zone Entity lighting + DependencyManager::get()->setGlobalLight(_sunSkyStage.getSunLight()); + { + PerformanceTimer perfTimer("SceneProcessPendingChanges"); + _main3DScene->enqueuePendingChanges(pendingChanges); + _main3DScene->processPendingChangesQueue(); + } + + // For now every frame pass the renderContext + { + PerformanceTimer perfTimer("EngineRun"); + _renderEngine->getRenderContext()->args = renderArgs; + // Before the deferred pass, let's try to use the render engine + _renderEngine->run(); + } + } + + void makeCurrent() { + _context.makeCurrent(this); + } + + void resizeWindow(const QSize& size) { + _size = size; + _camera.setAspectRatio((float)_size.width() / (float)_size.height()); + } + + void parsePath(const QString& viewpointString) { + static const QString FLOAT_REGEX_STRING = "([-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?)"; + static const QString SPACED_COMMA_REGEX_STRING = "\\s*,\\s*"; + static const QString POSITION_REGEX_STRING = QString("\\/") + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + FLOAT_REGEX_STRING + "\\s*(?:$|\\/)"; + static const QString QUAT_REGEX_STRING = QString("\\/") + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + + FLOAT_REGEX_STRING + "\\s*$"; + + static QRegExp orientationRegex(QUAT_REGEX_STRING); + static QRegExp positionRegex(POSITION_REGEX_STRING); + + if (positionRegex.indexIn(viewpointString) != -1) { + // we have at least a position, so emit our signal to say we need to change position + glm::vec3 newPosition(positionRegex.cap(1).toFloat(), + positionRegex.cap(2).toFloat(), + positionRegex.cap(3).toFloat()); + _camera.setPosition(newPosition); + + if (!glm::any(glm::isnan(newPosition))) { + // we may also have an orientation + if (viewpointString[positionRegex.matchedLength() - 1] == QChar('/') + && orientationRegex.indexIn(viewpointString, positionRegex.matchedLength() - 1) != -1) { + //glm::vec4 v = glm::vec4( + // orientationRegex.cap(1).toFloat(), + // orientationRegex.cap(2).toFloat(), + // orientationRegex.cap(3).toFloat(), + // orientationRegex.cap(4).toFloat()); + //if (!glm::any(glm::isnan(v))) { + // _camera.setRotation(glm::normalize(glm::quat(v.w, v.x, v.y, v.z))); + //} + } + } + } + } + + void importScene(const QString& fileName) { + _settings.setValue(LAST_SCENE_KEY, fileName); + _octree->clear(); + _octree->getTree()->readFromURL(fileName); + } + + void importScene() { + QString fileName = QFileDialog::getOpenFileName(nullptr, tr("Open File"), "/home", tr("Hifi Exports (*.json *.svo)")); + if (fileName.isNull()) { + return; + } + importScene(fileName); + } + + void goTo() { + QString destination = QInputDialog::getText(nullptr, tr("Go To Location"), "Enter path"); + if (destination.isNull()) { + return; + } + parsePath(destination); + } + +private: + render::CullFunctor _cullFunctor { cull }; + gpu::ContextPointer _gpuContext; // initialized during window creation + render::EnginePointer _renderEngine { new render::Engine() }; + render::ScenePointer _main3DScene { new render::Scene(glm::vec3(-0.5f * (float)TREE_SCALE), (float)TREE_SCALE) }; + QOpenGLContextWrapper _context; + QSize _size; + RateCounter<> fps; + QSettings _settings; + + QWindowCamera _camera; + ViewFrustum _viewFrustum; // current state of view frustum, perspective, orientation, etc. + ViewFrustum _shadowViewFrustum; // current state of view frustum, perspective, orientation, etc. + model::SunSkyStage _sunSkyStage; + model::LightPointer _globalLight { std::make_shared() }; + QElapsedTimer _elapsed; + QSharedPointer _octree; + QSharedPointer getEntities() { + return _octree; + } +}; + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + if (!message.isEmpty()) { +#ifdef Q_OS_WIN + OutputDebugStringA(message.toLocal8Bit().constData()); + OutputDebugStringA("\n"); +#endif + std::cout << message.toLocal8Bit().constData() << std::endl; + } +} + +const char * LOG_FILTER_RULES = R"V0G0N( +hifi.gpu=true +)V0G0N"; + +int main(int argc, char** argv) { + QApplication app(argc, argv); + QCoreApplication::setApplicationName("RenderPerf"); + QCoreApplication::setOrganizationName("High Fidelity"); + QCoreApplication::setOrganizationDomain("highfidelity.com"); + + qInstallMessageHandler(messageHandler); + QLoggingCategory::setFilterRules(LOG_FILTER_RULES); + QTestWindow::setup(); + QTestWindow window; + app.exec(); + return 0; +} + +#include "main.moc" From 5be238db08b7fa398db58472a5e8b443a1c338ff Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Jul 2016 20:29:35 -0700 Subject: [PATCH 0943/1237] Suppressing glew link warnings --- tests/render-perf/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 611cadd595..d4f90fdace 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -1,6 +1,10 @@ set(TARGET_NAME render-perf-test) +if (WIN32) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") +endif() + # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") @@ -10,4 +14,5 @@ link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking ne package_libraries_for_deployment() + target_bullet() From c7382ea886f0c0fcc8b6499a3bbef09be88290d2 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 7 Jul 2016 00:44:24 -0700 Subject: [PATCH 0944/1237] Optimize updateModelBounds --- .../src/RenderableModelEntityItem.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 43fea75eac..ef3c0e120b 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -342,16 +342,23 @@ void RenderableModelEntityItem::updateModelBounds() { if (!hasModel() || !_model) { return; } + if (!_dimensionsInitialized || !_model->isActive()) { + return; + } + + bool movingOrAnimating = isMovingRelativeToParent() || isAnimatingSomething(); glm::vec3 dimensions = getDimensions(); - if ((movingOrAnimating || + bool success; + auto transform = getTransform(success); + + if (movingOrAnimating || _needsInitialSimulation || _needsJointSimulation || - _model->getTranslation() != getPosition() || + _model->getTranslation() != transform.getTranslation() || _model->getScaleToFitDimensions() != dimensions || - _model->getRotation() != getRotation() || - _model->getRegistrationPoint() != getRegistrationPoint()) - && _model->isActive() && _dimensionsInitialized) { + _model->getRotation() != transform.getRotation() || + _model->getRegistrationPoint() != getRegistrationPoint()) { doInitialModelSimulation(); _needsJointSimulation = false; } From 721cd79b5759f3230601e50e603f42e6b4f21988 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Jul 2016 17:55:06 -0700 Subject: [PATCH 0945/1237] Aggressive batch pre-allocation --- libraries/gpu/src/gpu/Batch.cpp | 50 ++++++++++++++++------------- libraries/gpu/src/gpu/Batch.h | 56 ++++++++++++++------------------- libraries/gpu/src/gpu/Context.h | 4 +-- 3 files changed, 52 insertions(+), 58 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 6dc1d63ca8..8d16cd9262 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -30,24 +30,39 @@ ProfileRangeBatch::~ProfileRangeBatch() { using namespace gpu; -Batch::Batch(const CacheState& cacheState) : Batch() { - _commands.reserve(cacheState.commandsSize); - _commandOffsets.reserve(cacheState.offsetsSize); - _params.reserve(cacheState.paramsSize); - _data.reserve(cacheState.dataSize); -} +size_t Batch::_commandsMax { 128 }; +size_t Batch::_commandOffsetsMax { 128 }; +size_t Batch::_paramsMax { 128 }; +size_t Batch::_dataMax { 128 }; +size_t Batch::_objectsMax { 128 }; +size_t Batch::_drawCallInfosMax { 128 }; -Batch::CacheState Batch::getCacheState() { - return CacheState(_commands.size(), _commandOffsets.size(), _params.size(), _data.size(), - _buffers.size(), _textures.size(), _streamFormats.size(), _transforms.size(), _pipelines.size(), - _framebuffers.size(), _queries.size()); +Batch::Batch() { + _commands.reserve(_commandsMax); + _commandOffsets.reserve(_commandOffsetsMax); + _params.reserve(_paramsMax); + _data.reserve(_dataMax); + _objects.reserve(_objectsMax); + _drawCallInfos.reserve(_drawCallInfosMax); } Batch::~Batch() { - //qDebug() << "Batch::~Batch()... " << getCacheState(); + _commandsMax = std::max(_commands.size(), _commandsMax); + _commandOffsetsMax = std::max(_commandOffsets.size(), _commandOffsetsMax); + _paramsMax = std::max(_params.size(), _paramsMax); + _dataMax = std::max(_data.size(), _dataMax); + _objectsMax = std::max(_objects.size(), _objectsMax); + _drawCallInfosMax = std::max(_drawCallInfos.size(), _drawCallInfosMax); } void Batch::clear() { + _commandsMax = std::max(_commands.size(), _commandsMax); + _commandOffsetsMax = std::max(_commandOffsets.size(), _commandOffsetsMax); + _paramsMax = std::max(_params.size(), _paramsMax); + _dataMax = std::max(_data.size(), _dataMax); + _objectsMax = std::max(_objects.size(), _objectsMax); + _drawCallInfosMax = std::max(_drawCallInfos.size(), _drawCallInfosMax); + _commands.clear(); _commandOffsets.clear(); _params.clear(); @@ -58,6 +73,8 @@ void Batch::clear() { _transforms.clear(); _pipelines.clear(); _framebuffers.clear(); + _objects.clear(); + _drawCallInfos.clear(); } size_t Batch::cacheData(size_t size, const void* data) { @@ -458,17 +475,6 @@ void Batch::preExecute() { } } -QDebug& operator<<(QDebug& debug, const Batch::CacheState& cacheState) { - debug << "Batch::CacheState[ " - << "commandsSize:" << cacheState.commandsSize - << "offsetsSize:" << cacheState.offsetsSize - << "paramsSize:" << cacheState.paramsSize - << "dataSize:" << cacheState.dataSize - << "]"; - - return debug; -} - // Debugging void Batch::pushProfileRange(const char* name) { #if defined(NSIGHT_FOUND) diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 4e51038368..b56b5ee84b 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -87,6 +87,7 @@ public: using NamedBatchDataMap = std::map; DrawCallInfoBuffer _drawCallInfos; + static size_t _drawCallInfosMax; std::string _currentNamedCall; @@ -96,34 +97,7 @@ public: void captureDrawCallInfo(); void captureNamedDrawCallInfo(std::string name); - class CacheState { - public: - size_t commandsSize; - size_t offsetsSize; - size_t paramsSize; - size_t dataSize; - - size_t buffersSize; - size_t texturesSize; - size_t streamFormatsSize; - size_t transformsSize; - size_t pipelinesSize; - size_t framebuffersSize; - size_t queriesSize; - - CacheState() : commandsSize(0), offsetsSize(0), paramsSize(0), dataSize(0), buffersSize(0), texturesSize(0), - streamFormatsSize(0), transformsSize(0), pipelinesSize(0), framebuffersSize(0), queriesSize(0) { } - - CacheState(size_t commandsSize, size_t offsetsSize, size_t paramsSize, size_t dataSize, size_t buffersSize, - size_t texturesSize, size_t streamFormatsSize, size_t transformsSize, size_t pipelinesSize, - size_t framebuffersSize, size_t queriesSize) : - commandsSize(commandsSize), offsetsSize(offsetsSize), paramsSize(paramsSize), dataSize(dataSize), - buffersSize(buffersSize), texturesSize(texturesSize), streamFormatsSize(streamFormatsSize), - transformsSize(transformsSize), pipelinesSize(pipelinesSize), framebuffersSize(framebuffersSize), queriesSize(queriesSize) { } - }; - - Batch() {} - Batch(const CacheState& cacheState); + Batch(); explicit Batch(const Batch& batch); ~Batch(); @@ -131,9 +105,6 @@ public: void preExecute(); - CacheState getCacheState(); - - // Batches may need to override the context level stereo settings // if they're performing framebuffer copy operations, like the // deferred lighting resolution mechanism @@ -401,11 +372,21 @@ public: typedef T Data; Data _data; Cache(const Data& data) : _data(data) {} + static size_t _max; class Vector { public: std::vector< Cache > _items; + Vector() { + _items.reserve(_max); + } + + ~Vector() { + _max = std::max(_items.size(), _max); + } + + size_t size() const { return _items.size(); } size_t cache(const Data& data) { size_t offset = _items.size(); @@ -449,9 +430,16 @@ public: } Commands _commands; + static size_t _commandsMax; + CommandOffsets _commandOffsets; + static size_t _commandOffsetsMax; + Params _params; + static size_t _paramsMax; + Bytes _data; + static size_t _dataMax; // SSBO class... layout MUST match the layout in Transform.slh class TransformObject { @@ -464,6 +452,7 @@ public: bool _invalidModel { true }; Transform _currentModel; TransformObjects _objects; + static size_t _objectsMax; BufferCaches _buffers; TextureCaches _textures; @@ -491,6 +480,9 @@ protected: void captureDrawCallInfoImpl(); }; +template +size_t Batch::Cache::_max = 128; + } #if defined(NSIGHT_FOUND) @@ -512,6 +504,4 @@ private: #endif -QDebug& operator<<(QDebug& debug, const gpu::Batch::CacheState& cacheState); - #endif diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 652338f911..47233b2fe3 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -231,11 +231,9 @@ typedef std::shared_ptr ContextPointer; template void doInBatch(std::shared_ptr context, F f) { - static gpu::Batch::CacheState cacheState; - gpu::Batch batch(cacheState); + gpu::Batch batch; f(batch); context->render(batch); - cacheState = batch.getCacheState(); } }; From eff1c653884cd40911ba0957158209f837d331f4 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Jul 2016 18:06:51 -0700 Subject: [PATCH 0946/1237] Reduce the number of temporary objects in batch commands --- libraries/gpu/src/gpu/Batch.cpp | 236 ++++++++++++++++---------------- libraries/gpu/src/gpu/Batch.h | 2 +- 2 files changed, 119 insertions(+), 119 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 8d16cd9262..2c4c29cee9 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -26,7 +26,7 @@ ProfileRangeBatch::~ProfileRangeBatch() { } #endif -#define ADD_COMMAND(call) _commands.push_back(COMMAND_##call); _commandOffsets.push_back(_params.size()); +#define ADD_COMMAND(call) _commands.emplace_back(COMMAND_##call); _commandOffsets.emplace_back(_params.size()); using namespace gpu; @@ -89,9 +89,9 @@ size_t Batch::cacheData(size_t size, const void* data) { void Batch::draw(Primitive primitiveType, uint32 numVertices, uint32 startVertex) { ADD_COMMAND(draw); - _params.push_back(startVertex); - _params.push_back(numVertices); - _params.push_back(primitiveType); + _params.emplace_back(startVertex); + _params.emplace_back(numVertices); + _params.emplace_back(primitiveType); captureDrawCallInfo(); } @@ -99,9 +99,9 @@ void Batch::draw(Primitive primitiveType, uint32 numVertices, uint32 startVertex void Batch::drawIndexed(Primitive primitiveType, uint32 numIndices, uint32 startIndex) { ADD_COMMAND(drawIndexed); - _params.push_back(startIndex); - _params.push_back(numIndices); - _params.push_back(primitiveType); + _params.emplace_back(startIndex); + _params.emplace_back(numIndices); + _params.emplace_back(primitiveType); captureDrawCallInfo(); } @@ -109,11 +109,11 @@ void Batch::drawIndexed(Primitive primitiveType, uint32 numIndices, uint32 start void Batch::drawInstanced(uint32 numInstances, Primitive primitiveType, uint32 numVertices, uint32 startVertex, uint32 startInstance) { ADD_COMMAND(drawInstanced); - _params.push_back(startInstance); - _params.push_back(startVertex); - _params.push_back(numVertices); - _params.push_back(primitiveType); - _params.push_back(numInstances); + _params.emplace_back(startInstance); + _params.emplace_back(startVertex); + _params.emplace_back(numVertices); + _params.emplace_back(primitiveType); + _params.emplace_back(numInstances); captureDrawCallInfo(); } @@ -121,11 +121,11 @@ void Batch::drawInstanced(uint32 numInstances, Primitive primitiveType, uint32 n void Batch::drawIndexedInstanced(uint32 numInstances, Primitive primitiveType, uint32 numIndices, uint32 startIndex, uint32 startInstance) { ADD_COMMAND(drawIndexedInstanced); - _params.push_back(startInstance); - _params.push_back(startIndex); - _params.push_back(numIndices); - _params.push_back(primitiveType); - _params.push_back(numInstances); + _params.emplace_back(startInstance); + _params.emplace_back(startIndex); + _params.emplace_back(numIndices); + _params.emplace_back(primitiveType); + _params.emplace_back(numInstances); captureDrawCallInfo(); } @@ -133,16 +133,16 @@ void Batch::drawIndexedInstanced(uint32 numInstances, Primitive primitiveType, u void Batch::multiDrawIndirect(uint32 numCommands, Primitive primitiveType) { ADD_COMMAND(multiDrawIndirect); - _params.push_back(numCommands); - _params.push_back(primitiveType); + _params.emplace_back(numCommands); + _params.emplace_back(primitiveType); captureDrawCallInfo(); } void Batch::multiDrawIndexedIndirect(uint32 nbCommands, Primitive primitiveType) { ADD_COMMAND(multiDrawIndexedIndirect); - _params.push_back(nbCommands); - _params.push_back(primitiveType); + _params.emplace_back(nbCommands); + _params.emplace_back(primitiveType); captureDrawCallInfo(); } @@ -150,16 +150,16 @@ void Batch::multiDrawIndexedIndirect(uint32 nbCommands, Primitive primitiveType) void Batch::setInputFormat(const Stream::FormatPointer& format) { ADD_COMMAND(setInputFormat); - _params.push_back(_streamFormats.cache(format)); + _params.emplace_back(_streamFormats.cache(format)); } void Batch::setInputBuffer(Slot channel, const BufferPointer& buffer, Offset offset, Offset stride) { ADD_COMMAND(setInputBuffer); - _params.push_back(stride); - _params.push_back(offset); - _params.push_back(_buffers.cache(buffer)); - _params.push_back(channel); + _params.emplace_back(stride); + _params.emplace_back(offset); + _params.emplace_back(_buffers.cache(buffer)); + _params.emplace_back(channel); } void Batch::setInputBuffer(Slot channel, const BufferView& view) { @@ -180,9 +180,9 @@ void Batch::setInputStream(Slot startChannel, const BufferStream& stream) { void Batch::setIndexBuffer(Type type, const BufferPointer& buffer, Offset offset) { ADD_COMMAND(setIndexBuffer); - _params.push_back(offset); - _params.push_back(_buffers.cache(buffer)); - _params.push_back(type); + _params.emplace_back(offset); + _params.emplace_back(_buffers.cache(buffer)); + _params.emplace_back(type); } void Batch::setIndexBuffer(const BufferView& buffer) { @@ -192,9 +192,9 @@ void Batch::setIndexBuffer(const BufferView& buffer) { void Batch::setIndirectBuffer(const BufferPointer& buffer, Offset offset, Offset stride) { ADD_COMMAND(setIndirectBuffer); - _params.push_back(_buffers.cache(buffer)); - _params.push_back(offset); - _params.push_back(stride); + _params.emplace_back(_buffers.cache(buffer)); + _params.emplace_back(offset); + _params.emplace_back(stride); } @@ -208,56 +208,56 @@ void Batch::setModelTransform(const Transform& model) { void Batch::setViewTransform(const Transform& view) { ADD_COMMAND(setViewTransform); - _params.push_back(_transforms.cache(view)); + _params.emplace_back(_transforms.cache(view)); } void Batch::setProjectionTransform(const Mat4& proj) { ADD_COMMAND(setProjectionTransform); - _params.push_back(cacheData(sizeof(Mat4), &proj)); + _params.emplace_back(cacheData(sizeof(Mat4), &proj)); } void Batch::setViewportTransform(const Vec4i& viewport) { ADD_COMMAND(setViewportTransform); - _params.push_back(cacheData(sizeof(Vec4i), &viewport)); + _params.emplace_back(cacheData(sizeof(Vec4i), &viewport)); } void Batch::setDepthRangeTransform(float nearDepth, float farDepth) { ADD_COMMAND(setDepthRangeTransform); - _params.push_back(farDepth); - _params.push_back(nearDepth); + _params.emplace_back(farDepth); + _params.emplace_back(nearDepth); } void Batch::setPipeline(const PipelinePointer& pipeline) { ADD_COMMAND(setPipeline); - _params.push_back(_pipelines.cache(pipeline)); + _params.emplace_back(_pipelines.cache(pipeline)); } void Batch::setStateBlendFactor(const Vec4& factor) { ADD_COMMAND(setStateBlendFactor); - _params.push_back(factor.x); - _params.push_back(factor.y); - _params.push_back(factor.z); - _params.push_back(factor.w); + _params.emplace_back(factor.x); + _params.emplace_back(factor.y); + _params.emplace_back(factor.z); + _params.emplace_back(factor.w); } void Batch::setStateScissorRect(const Vec4i& rect) { ADD_COMMAND(setStateScissorRect); - _params.push_back(cacheData(sizeof(Vec4i), &rect)); + _params.emplace_back(cacheData(sizeof(Vec4i), &rect)); } void Batch::setUniformBuffer(uint32 slot, const BufferPointer& buffer, Offset offset, Offset size) { ADD_COMMAND(setUniformBuffer); - _params.push_back(size); - _params.push_back(offset); - _params.push_back(_buffers.cache(buffer)); - _params.push_back(slot); + _params.emplace_back(size); + _params.emplace_back(offset); + _params.emplace_back(_buffers.cache(buffer)); + _params.emplace_back(slot); } void Batch::setUniformBuffer(uint32 slot, const BufferView& view) { @@ -268,8 +268,8 @@ void Batch::setUniformBuffer(uint32 slot, const BufferView& view) { void Batch::setResourceTexture(uint32 slot, const TexturePointer& texture) { ADD_COMMAND(setResourceTexture); - _params.push_back(_textures.cache(texture)); - _params.push_back(slot); + _params.emplace_back(_textures.cache(texture)); + _params.emplace_back(slot); } void Batch::setResourceTexture(uint32 slot, const TextureView& view) { @@ -279,21 +279,21 @@ void Batch::setResourceTexture(uint32 slot, const TextureView& view) { void Batch::setFramebuffer(const FramebufferPointer& framebuffer) { ADD_COMMAND(setFramebuffer); - _params.push_back(_framebuffers.cache(framebuffer)); + _params.emplace_back(_framebuffers.cache(framebuffer)); } void Batch::clearFramebuffer(Framebuffer::Masks targets, const Vec4& color, float depth, int stencil, bool enableScissor) { ADD_COMMAND(clearFramebuffer); - _params.push_back(enableScissor); - _params.push_back(stencil); - _params.push_back(depth); - _params.push_back(color.w); - _params.push_back(color.z); - _params.push_back(color.y); - _params.push_back(color.x); - _params.push_back(targets); + _params.emplace_back(enableScissor); + _params.emplace_back(stencil); + _params.emplace_back(depth); + _params.emplace_back(color.w); + _params.emplace_back(color.z); + _params.emplace_back(color.y); + _params.emplace_back(color.x); + _params.emplace_back(targets); } void Batch::clearColorFramebuffer(Framebuffer::Masks targets, const Vec4& color, bool enableScissor) { @@ -316,40 +316,40 @@ void Batch::blit(const FramebufferPointer& src, const Vec4i& srcViewport, const FramebufferPointer& dst, const Vec4i& dstViewport) { ADD_COMMAND(blit); - _params.push_back(_framebuffers.cache(src)); - _params.push_back(srcViewport.x); - _params.push_back(srcViewport.y); - _params.push_back(srcViewport.z); - _params.push_back(srcViewport.w); - _params.push_back(_framebuffers.cache(dst)); - _params.push_back(dstViewport.x); - _params.push_back(dstViewport.y); - _params.push_back(dstViewport.z); - _params.push_back(dstViewport.w); + _params.emplace_back(_framebuffers.cache(src)); + _params.emplace_back(srcViewport.x); + _params.emplace_back(srcViewport.y); + _params.emplace_back(srcViewport.z); + _params.emplace_back(srcViewport.w); + _params.emplace_back(_framebuffers.cache(dst)); + _params.emplace_back(dstViewport.x); + _params.emplace_back(dstViewport.y); + _params.emplace_back(dstViewport.z); + _params.emplace_back(dstViewport.w); } void Batch::generateTextureMips(const TexturePointer& texture) { ADD_COMMAND(generateTextureMips); - _params.push_back(_textures.cache(texture)); + _params.emplace_back(_textures.cache(texture)); } void Batch::beginQuery(const QueryPointer& query) { ADD_COMMAND(beginQuery); - _params.push_back(_queries.cache(query)); + _params.emplace_back(_queries.cache(query)); } void Batch::endQuery(const QueryPointer& query) { ADD_COMMAND(endQuery); - _params.push_back(_queries.cache(query)); + _params.emplace_back(_queries.cache(query)); } void Batch::getQuery(const QueryPointer& query) { ADD_COMMAND(getQuery); - _params.push_back(_queries.cache(query)); + _params.emplace_back(_queries.cache(query)); } void Batch::resetStages() { @@ -358,12 +358,12 @@ void Batch::resetStages() { void Batch::runLambda(std::function f) { ADD_COMMAND(runLambda); - _params.push_back(_lambdas.cache(f)); + _params.emplace_back(_lambdas.cache(f)); } void Batch::startNamedCall(const std::string& name) { ADD_COMMAND(startNamedCall); - _params.push_back(_names.cache(name)); + _params.emplace_back(_names.cache(name)); _currentNamedCall = name; } @@ -439,14 +439,14 @@ void Batch::captureDrawCallInfoImpl() { //_model.getInverseMatrix(_object._modelInverse); object._modelInverse = glm::inverse(object._model); - _objects.push_back(object); + _objects.emplace_back(object); // Flag is clean _invalidModel = false; } auto& drawCallInfos = getDrawCallInfoBuffer(); - drawCallInfos.push_back((uint16)_objects.size() - 1); + drawCallInfos.emplace_back((uint16)_objects.size() - 1); } void Batch::captureDrawCallInfo() { @@ -479,7 +479,7 @@ void Batch::preExecute() { void Batch::pushProfileRange(const char* name) { #if defined(NSIGHT_FOUND) ADD_COMMAND(pushProfileRange); - _params.push_back(_profileRanges.cache(name)); + _params.emplace_back(_profileRanges.cache(name)); #endif } @@ -496,9 +496,9 @@ void Batch::_glActiveBindTexture(uint32 unit, uint32 target, uint32 texture) { setResourceTexture(unit - GL_TEXTURE0, nullptr); ADD_COMMAND(glActiveBindTexture); - _params.push_back(texture); - _params.push_back(target); - _params.push_back(unit); + _params.emplace_back(texture); + _params.emplace_back(target); + _params.emplace_back(unit); } void Batch::_glUniform1i(int32 location, int32 v0) { @@ -506,8 +506,8 @@ void Batch::_glUniform1i(int32 location, int32 v0) { return; } ADD_COMMAND(glUniform1i); - _params.push_back(v0); - _params.push_back(location); + _params.emplace_back(v0); + _params.emplace_back(location); } void Batch::_glUniform1f(int32 location, float v0) { @@ -515,89 +515,89 @@ void Batch::_glUniform1f(int32 location, float v0) { return; } ADD_COMMAND(glUniform1f); - _params.push_back(v0); - _params.push_back(location); + _params.emplace_back(v0); + _params.emplace_back(location); } void Batch::_glUniform2f(int32 location, float v0, float v1) { ADD_COMMAND(glUniform2f); - _params.push_back(v1); - _params.push_back(v0); - _params.push_back(location); + _params.emplace_back(v1); + _params.emplace_back(v0); + _params.emplace_back(location); } void Batch::_glUniform3f(int32 location, float v0, float v1, float v2) { ADD_COMMAND(glUniform3f); - _params.push_back(v2); - _params.push_back(v1); - _params.push_back(v0); - _params.push_back(location); + _params.emplace_back(v2); + _params.emplace_back(v1); + _params.emplace_back(v0); + _params.emplace_back(location); } void Batch::_glUniform4f(int32 location, float v0, float v1, float v2, float v3) { ADD_COMMAND(glUniform4f); - _params.push_back(v3); - _params.push_back(v2); - _params.push_back(v1); - _params.push_back(v0); - _params.push_back(location); + _params.emplace_back(v3); + _params.emplace_back(v2); + _params.emplace_back(v1); + _params.emplace_back(v0); + _params.emplace_back(location); } void Batch::_glUniform3fv(int32 location, int count, const float* value) { ADD_COMMAND(glUniform3fv); const int VEC3_SIZE = 3 * sizeof(float); - _params.push_back(cacheData(count * VEC3_SIZE, value)); - _params.push_back(count); - _params.push_back(location); + _params.emplace_back(cacheData(count * VEC3_SIZE, value)); + _params.emplace_back(count); + _params.emplace_back(location); } void Batch::_glUniform4fv(int32 location, int count, const float* value) { ADD_COMMAND(glUniform4fv); const int VEC4_SIZE = 4 * sizeof(float); - _params.push_back(cacheData(count * VEC4_SIZE, value)); - _params.push_back(count); - _params.push_back(location); + _params.emplace_back(cacheData(count * VEC4_SIZE, value)); + _params.emplace_back(count); + _params.emplace_back(location); } void Batch::_glUniform4iv(int32 location, int count, const int32* value) { ADD_COMMAND(glUniform4iv); const int VEC4_SIZE = 4 * sizeof(int); - _params.push_back(cacheData(count * VEC4_SIZE, value)); - _params.push_back(count); - _params.push_back(location); + _params.emplace_back(cacheData(count * VEC4_SIZE, value)); + _params.emplace_back(count); + _params.emplace_back(location); } void Batch::_glUniformMatrix3fv(int32 location, int count, uint8 transpose, const float* value) { ADD_COMMAND(glUniformMatrix3fv); const int MATRIX3_SIZE = 9 * sizeof(float); - _params.push_back(cacheData(count * MATRIX3_SIZE, value)); - _params.push_back(transpose); - _params.push_back(count); - _params.push_back(location); + _params.emplace_back(cacheData(count * MATRIX3_SIZE, value)); + _params.emplace_back(transpose); + _params.emplace_back(count); + _params.emplace_back(location); } void Batch::_glUniformMatrix4fv(int32 location, int count, uint8 transpose, const float* value) { ADD_COMMAND(glUniformMatrix4fv); const int MATRIX4_SIZE = 16 * sizeof(float); - _params.push_back(cacheData(count * MATRIX4_SIZE, value)); - _params.push_back(transpose); - _params.push_back(count); - _params.push_back(location); + _params.emplace_back(cacheData(count * MATRIX4_SIZE, value)); + _params.emplace_back(transpose); + _params.emplace_back(count); + _params.emplace_back(location); } void Batch::_glColor4f(float red, float green, float blue, float alpha) { ADD_COMMAND(glColor4f); - _params.push_back(alpha); - _params.push_back(blue); - _params.push_back(green); - _params.push_back(red); -} \ No newline at end of file + _params.emplace_back(alpha); + _params.emplace_back(blue); + _params.emplace_back(green); + _params.emplace_back(red); +} diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index b56b5ee84b..628e9e2d04 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -390,7 +390,7 @@ public: size_t size() const { return _items.size(); } size_t cache(const Data& data) { size_t offset = _items.size(); - _items.push_back(Cache(data)); + _items.emplace_back(data); return offset; } From b73fe2484813d0177dffd9970abbff0617e248bc Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 7 Jul 2016 09:56:26 -0700 Subject: [PATCH 0947/1237] PR feedback --- libraries/gpu/src/gpu/Batch.cpp | 12 ++++++------ libraries/gpu/src/gpu/Batch.h | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 2c4c29cee9..9161ee3642 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -30,12 +30,12 @@ ProfileRangeBatch::~ProfileRangeBatch() { using namespace gpu; -size_t Batch::_commandsMax { 128 }; -size_t Batch::_commandOffsetsMax { 128 }; -size_t Batch::_paramsMax { 128 }; -size_t Batch::_dataMax { 128 }; -size_t Batch::_objectsMax { 128 }; -size_t Batch::_drawCallInfosMax { 128 }; +size_t Batch::_commandsMax { BATCH_PREALLOCATE_MIN }; +size_t Batch::_commandOffsetsMax { BATCH_PREALLOCATE_MIN }; +size_t Batch::_paramsMax { BATCH_PREALLOCATE_MIN }; +size_t Batch::_dataMax { BATCH_PREALLOCATE_MIN }; +size_t Batch::_objectsMax { BATCH_PREALLOCATE_MIN }; +size_t Batch::_drawCallInfosMax { BATCH_PREALLOCATE_MIN }; Batch::Batch() { _commands.reserve(_commandsMax); diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 628e9e2d04..9cf1ca8269 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -26,7 +26,7 @@ #include "Transform.h" class QDebug; - +#define BATCH_PREALLOCATE_MIN 128 namespace gpu { enum ReservedSlot { @@ -481,7 +481,7 @@ protected: }; template -size_t Batch::Cache::_max = 128; +size_t Batch::Cache::_max = BATCH_PREALLOCATE_MIN; } From dfdc1ca1c6e2f3d93c7f827babb2040d882c0e71 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 7 Jul 2016 11:14:56 -0700 Subject: [PATCH 0948/1237] Ensuring render thread is highest priority --- tests/render-perf/src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index c953e51700..5367c65d94 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -319,6 +319,7 @@ public: } QTestWindow() { + QThread::currentThread()->setPriority(QThread::HighestPriority); AbstractViewStateInterface::setInstance(this); _octree = DependencyManager::set(false, this, nullptr); _octree->init(); From 8212ada4dec4930e7b270b8ed0a342cd0e43fc73 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 11:42:41 -0700 Subject: [PATCH 0949/1237] introduce delay --- scripts/system/controllers/teleport.js | 39 ++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index f52cd55a84..d8dd38c15a 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,11 +16,13 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; -// var NUMBER_OF_STEPS = 0; -// var SMOOTH_ARRIVAL_SPACING = 0; -// slow -var SMOOTH_ARRIVAL_SPACING = 150; -var NUMBER_OF_STEPS = 2; +// instant +var NUMBER_OF_STEPS = 0; +var SMOOTH_ARRIVAL_SPACING = 0; + +// // slow +// var SMOOTH_ARRIVAL_SPACING = 150; +// var NUMBER_OF_STEPS = 2; //medium-slow // var SMOOTH_ARRIVAL_SPACING = 100; @@ -77,7 +79,7 @@ function Teleporter() { this.leftOverlayLine = null; this.targetOverlay = null; this.updateConnected = null; - this.smoothArrivalInterval=null; + this.smoothArrivalInterval = null; this.initialize = function() { this.createMappings(); @@ -112,7 +114,7 @@ function Teleporter() { return; } - if(this.smoothArrivalInterval!==null){ + if (this.smoothArrivalInterval !== null) { Script.clearInterval(this.smoothArrivalInterval); } inTeleportMode = true; @@ -565,6 +567,8 @@ var rightTrigger = new Trigger('right'); var mappingName, teleportMapping; +var TELEPORT_DELAY = 100; + function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); @@ -572,11 +576,24 @@ function registerMappings() { teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - teleportMapping.from(leftPad.down).to(function() { - teleporter.enterTeleportMode('left') + teleportMapping.from(leftPad.down).to(function(value) { + print('left down' + value) + if (value === 1) { + + Script.setTimeout(function() { + teleporter.enterTeleportMode('left') + }, TELEPORT_DELAY) + } + }); - teleportMapping.from(rightPad.down).to(function() { - teleporter.enterTeleportMode('right') + teleportMapping.from(rightPad.down).to(function(value) { + print('right down' + value) + if (value === 1) { + + Script.setTimeout(function() { + teleporter.enterTeleportMode('right') + }, TELEPORT_DELAY) + } }); } From 93081e73d8b784276186927f6bee65db7eeaf45a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 12:12:22 -0700 Subject: [PATCH 0950/1237] thumb and trigger --- scripts/system/controllers/teleport.js | 58 +++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index d8dd38c15a..147a8634e2 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -121,7 +121,7 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - Script.update.connect(this.update); + Script.update.connect(this.updateForThumbAndTrigger); }; @@ -216,11 +216,10 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - Script.update.disconnect(this.update); + Script.update.disconnect(this.updateForThumbAndTrigger); this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); - this.enableGrab(); Script.setTimeout(function() { inTeleportMode = false; @@ -247,6 +246,35 @@ function Teleporter() { }; + this.updateForThumbAndTrigger = function() { + + if (teleporter.teleportHand === 'left') { + teleporter.leftRay(); + if (leftPad.buttonValue === 0) { + _this.exitTeleportMode(); + _this.deleteTargetOverlay(); + return; + } + if (leftTrigger.buttonValue === 0 && inTeleportMode === true) { + _this.teleport(); + return; + } + + } else { + teleporter.rightRay(); + if (rightPad.buttonValue === 0) { + _this.exitTeleportMode(); + _this.deleteTargetOverlay(); + return; + } + if (rightTrigger.buttonValue === 0 && inTeleportMode === true) { + _this.teleport(); + return; + } + } + + }; + this.rightRay = function() { @@ -595,9 +623,29 @@ function registerMappings() { }, TELEPORT_DELAY) } }); + } -registerMappings(); + +function registerMappingsWithThumbAndTrigger() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); + teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); + + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + + teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function(value) { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { + teleporter.enterTeleportMode('right') + }); +} + +//registerMappings(); +registerMappingsWithThumbAndTrigger() var teleporter = new Teleporter(); Controller.enableMapping(mappingName); @@ -610,6 +658,6 @@ function cleanup() { teleporter.deleteTargetOverlay(); teleporter.turnOffOverlayBeams(); if (teleporter.updateConnected !== null) { - Script.update.disconnect(teleporter.update); + Script.update.disconnect(teleporter.updateForThumbAndTrigger); } } \ No newline at end of file From be770c81777dea43f48f149ad2c9636bb8053f87 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 12:39:11 -0700 Subject: [PATCH 0951/1237] make thumb and trigger mode an option --- scripts/system/controllers/teleport.js | 33 ++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 147a8634e2..a59711b025 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -36,6 +36,8 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; +var USE_THUMB_AND_TRIGGER_MODE = true; + var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -121,8 +123,12 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - Script.update.connect(this.updateForThumbAndTrigger); + if (USE_THUMB_AND_TRIGGER_MODE === true) { + Script.update.connect(this.updateForThumbAndTrigger); + } else { + Script.update.connect(this.update); + } }; this.findMidpoint = function(start, end) { @@ -216,7 +222,14 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - Script.update.disconnect(this.updateForThumbAndTrigger); + if (USE_THUMB_AND_TRIGGER_MODE === true) { + Script.update.disconnect(this.updateForThumbAndTrigger); + + } else { + Script.update.disconnect(this.update); + + } + this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); @@ -644,8 +657,12 @@ function registerMappingsWithThumbAndTrigger() { }); } -//registerMappings(); -registerMappingsWithThumbAndTrigger() +if (USE_THUMB_AND_TRIGGER_MODE === true) { + registerMappingsWithThumbAndTrigger(); +} else { + registerMappings(); +} + var teleporter = new Teleporter(); Controller.enableMapping(mappingName); @@ -658,6 +675,12 @@ function cleanup() { teleporter.deleteTargetOverlay(); teleporter.turnOffOverlayBeams(); if (teleporter.updateConnected !== null) { - Script.update.disconnect(teleporter.updateForThumbAndTrigger); + + if (USE_THUMB_AND_TRIGGER_MODE === true) { + Script.update.disconnect(teleporter.updateForThumbAndTrigger); + + } else { + Script.update.disconnect(teleporter.update); + } } } \ No newline at end of file From 4a9f6e677c4187bb89c23b227409630ac203a987 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 12:52:24 -0700 Subject: [PATCH 0952/1237] add new target model --- scripts/system/controllers/teleport.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index a59711b025..577d6bf456 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -36,9 +36,9 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; -var USE_THUMB_AND_TRIGGER_MODE = true; +var USE_THUMB_AND_TRIGGER_MODE = false; -var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; +var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, From 23700dc90f3038de7397f9b43aa9cf59df1453b0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 7 Jul 2016 13:16:59 -0700 Subject: [PATCH 0953/1237] Ensure newly shown windows are popped to the top of the zorder --- interface/resources/qml/windows/Window.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index e3e70c1e74..82bcf011e9 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -209,6 +209,9 @@ Fadable { var targetVisibility = getTargetVisibility(); if (targetVisibility === visible) { + if (force) { + window.raise(); + } return; } From e4828b03b16a7952bb96e300a44cc509c3a43cb6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 7 Jul 2016 13:32:30 -0700 Subject: [PATCH 0954/1237] fix crash when building shape before model loaded --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7eb7d87566..933a6ca5c6 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -594,7 +594,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { // the model is still being downloaded. return false; - } else if (type == SHAPE_TYPE_STATIC_MESH) { + } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { return (_model && _model->isLoaded()); } return true; From aba6924dc7b9c844da3586776478763d62b43fc8 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 7 Jul 2016 13:33:36 -0700 Subject: [PATCH 0955/1237] Fix MyAvatar name and url accessors to JS. Q_INVOKABLE does not support returning references. MyAvatar.getFullAvatarURLFromPreferences() now returns a QUrl instead of a reference. MyAvatar.getFullAvatarModelName() now returns a QString instead of reference. This should fix the JS API for these methods, they will now receive a JS string instead of undefined. --- interface/src/avatar/MyAvatar.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 96fa999de5..a21922f1b1 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -212,8 +212,8 @@ public: virtual void clearJointsData() override; Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); - Q_INVOKABLE const QUrl& getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } - Q_INVOKABLE const QString& getFullAvatarModelName() const { return _fullAvatarModelName; } + Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } + Q_INVOKABLE QString getFullAvatarModelName() const { return _fullAvatarModelName; } void resetFullAvatarURL(); From 790f74da1dfc4d51e03c48f526e98de4a018e02d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Jul 2016 12:36:15 -0700 Subject: [PATCH 0956/1237] Support geometry shaders --- cmake/macros/AutoScribeShader.cmake | 4 +- libraries/gpu-gl/src/gpu/gl/GLShader.cpp | 55 +++++++++++++----------- libraries/gpu/src/gpu/Shader.cpp | 20 +++++++++ libraries/gpu/src/gpu/Shader.h | 3 ++ 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/cmake/macros/AutoScribeShader.cmake b/cmake/macros/AutoScribeShader.cmake index f51455e9d4..dfa59943d6 100755 --- a/cmake/macros/AutoScribeShader.cmake +++ b/cmake/macros/AutoScribeShader.cmake @@ -44,6 +44,8 @@ function(AUTOSCRIBE_SHADER SHADER_FILE) set(SHADER_TARGET ${SHADER_TARGET}_vert.h) elseif(${SHADER_EXT} STREQUAL .slf) set(SHADER_TARGET ${SHADER_TARGET}_frag.h) + elseif(${SHADER_EXT} STREQUAL .slg) + set(SHADER_TARGET ${SHADER_TARGET}_geom.h) endif() set(SHADER_TARGET "${SHADERS_DIR}/${SHADER_TARGET}") @@ -87,7 +89,7 @@ macro(AUTOSCRIBE_SHADER_LIB) #message(${HIFI_LIBRARIES_SHADER_INCLUDE_FILES}) file(GLOB_RECURSE SHADER_INCLUDE_FILES src/*.slh) - file(GLOB_RECURSE SHADER_SOURCE_FILES src/*.slv src/*.slf) + file(GLOB_RECURSE SHADER_SOURCE_FILES src/*.slv src/*.slf src/*.slg) #make the shader folder set(SHADERS_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders/${TARGET_NAME}") diff --git a/libraries/gpu-gl/src/gpu/gl/GLShader.cpp b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp index c89bc88899..8dc3854b41 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp @@ -25,40 +25,45 @@ GLShader::~GLShader() { } } +// GLSL version +static const std::string glslVersion { + "#version 410 core" +}; + +// Shader domain +static const size_t NUM_SHADER_DOMAINS = 3; + +// GL Shader type enums +// Must match the order of type specified in gpu::Shader::Type +static const std::array SHADER_DOMAINS { { + GL_VERTEX_SHADER, + GL_FRAGMENT_SHADER, + GL_GEOMETRY_SHADER, +} }; + +// Domain specific defines +// Must match the order of type specified in gpu::Shader::Type +static const std::array DOMAIN_DEFINES { { + "#define GPU_VERTEX_SHADER", + "#define GPU_PIXEL_SHADER", + "#define GPU_GEOMETRY_SHADER", +} }; + +// Versions specific of the shader +static const std::array VERSION_DEFINES { { + "" +} }; + GLShader* compileBackendShader(const Shader& shader) { // Any GLSLprogram ? normally yes... const std::string& shaderSource = shader.getSource().getCode(); - - // GLSL version - const std::string glslVersion = { - "#version 410 core" - }; - - // Shader domain - const int NUM_SHADER_DOMAINS = 2; - const GLenum SHADER_DOMAINS[NUM_SHADER_DOMAINS] = { - GL_VERTEX_SHADER, - GL_FRAGMENT_SHADER - }; GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; - - // Domain specific defines - const std::string domainDefines[NUM_SHADER_DOMAINS] = { - "#define GPU_VERTEX_SHADER", - "#define GPU_PIXEL_SHADER" - }; - - // Versions specific of the shader - const std::string versionDefines[GLShader::NumVersions] = { - "" - }; - GLShader::ShaderObjects shaderObjects; for (int version = 0; version < GLShader::NumVersions; version++) { auto& shaderObject = shaderObjects[version]; - std::string shaderDefines = glslVersion + "\n" + domainDefines[shader.getType()] + "\n" + versionDefines[version]; + std::string shaderDefines = glslVersion + "\n" + DOMAIN_DEFINES[shader.getType()] + "\n" + VERSION_DEFINES[version]; bool result = compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram); if (!result) { diff --git a/libraries/gpu/src/gpu/Shader.cpp b/libraries/gpu/src/gpu/Shader.cpp index 74b7734618..a044e4845e 100755 --- a/libraries/gpu/src/gpu/Shader.cpp +++ b/libraries/gpu/src/gpu/Shader.cpp @@ -31,6 +31,13 @@ Shader::Shader(Type type, const Pointer& vertex, const Pointer& pixel): _shaders[PIXEL] = pixel; } +Shader::Shader(Type type, const Pointer& vertex, const Pointer& geometry, const Pointer& pixel) : +_type(type) { + _shaders.resize(3); + _shaders[VERTEX] = vertex; + _shaders[GEOMETRY] = geometry; + _shaders[PIXEL] = pixel; +} Shader::~Shader() { @@ -44,6 +51,10 @@ Shader::Pointer Shader::createPixel(const Source& source) { return Pointer(new Shader(PIXEL, source)); } +Shader::Pointer Shader::createGeometry(const Source& source) { + return Pointer(new Shader(GEOMETRY, source)); +} + Shader::Pointer Shader::createProgram(const Pointer& vertexShader, const Pointer& pixelShader) { if (vertexShader && vertexShader->getType() == VERTEX && pixelShader && pixelShader->getType() == PIXEL) { @@ -52,6 +63,15 @@ Shader::Pointer Shader::createProgram(const Pointer& vertexShader, const Pointer return Pointer(); } +Shader::Pointer Shader::createProgram(const Pointer& vertexShader, const Pointer& geometryShader, const Pointer& pixelShader) { + if (vertexShader && vertexShader->getType() == VERTEX && + geometryShader && geometryShader->getType() == GEOMETRY && + pixelShader && pixelShader->getType() == PIXEL) { + return Pointer(new Shader(PROGRAM, vertexShader, geometryShader, pixelShader)); + } + return Pointer(); +} + void Shader::defineSlots(const SlotSet& uniforms, const SlotSet& buffers, const SlotSet& textures, const SlotSet& samplers, const SlotSet& inputs, const SlotSet& outputs) { _uniforms = uniforms; _buffers = buffers; diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index e4643f2b7c..9072bf23a9 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -110,8 +110,10 @@ public: static Pointer createVertex(const Source& source); static Pointer createPixel(const Source& source); + static Pointer createGeometry(const Source& source); static Pointer createProgram(const Pointer& vertexShader, const Pointer& pixelShader); + static Pointer createProgram(const Pointer& vertexShader, const Pointer& geometryShader, const Pointer& pixelShader); ~Shader(); @@ -163,6 +165,7 @@ public: protected: Shader(Type type, const Source& source); Shader(Type type, const Pointer& vertex, const Pointer& pixel); + Shader(Type type, const Pointer& vertex, const Pointer& geometry, const Pointer& pixel); Shader(const Shader& shader); // deep copy of the sysmem shader Shader& operator=(const Shader& shader); // deep copy of the sysmem texture From 8ca3630cfa37a8387fdad65365338675b64c0548 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Jul 2016 12:37:36 -0700 Subject: [PATCH 0957/1237] Support glow effect in 3D line overlays --- interface/src/ui/overlays/Line3DOverlay.cpp | 21 +- interface/src/ui/overlays/Line3DOverlay.h | 6 + libraries/render-utils/src/GeometryCache.cpp | 573 +++++++++++-------- libraries/render-utils/src/GeometryCache.h | 4 + libraries/render-utils/src/glowLine.slf | 32 ++ libraries/render-utils/src/glowLine.slg | 127 ++++ libraries/render-utils/src/glowLine.slv | 26 + 7 files changed, 545 insertions(+), 244 deletions(-) create mode 100644 libraries/render-utils/src/glowLine.slf create mode 100644 libraries/render-utils/src/glowLine.slg create mode 100644 libraries/render-utils/src/glowLine.slv diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index d53b287f76..c9a8b19f6a 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -55,12 +55,14 @@ void Line3DOverlay::render(RenderArgs* args) { batch->setModelTransform(_transform); auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(*batch, false, false, true, true); if (getIsDashedLine()) { // TODO: add support for color to renderDashedLine() + geometryCache->bindSimpleProgram(*batch, false, false, true, true); geometryCache->renderDashedLine(*batch, _start, _end, colorv4, _geometryCacheID); + } else if (_glow > 0.0f) { + geometryCache->renderGlowLine(*batch, _start, _end, colorv4, _glow, _glowWidth, _geometryCacheID); } else { - + geometryCache->bindSimpleProgram(*batch, false, false, true, true); geometryCache->renderLine(*batch, _start, _end, colorv4, _geometryCacheID); } } @@ -68,7 +70,7 @@ void Line3DOverlay::render(RenderArgs* args) { const render::ShapeKey Line3DOverlay::getShapeKey() { auto builder = render::ShapeKey::Builder().withOwnPipeline(); - if (getAlpha() != 1.0f) { + if (getAlpha() != 1.0f || _glow > 0.0f) { builder.withTranslucent(); } return builder.build(); @@ -94,6 +96,19 @@ void Line3DOverlay::setProperties(const QVariantMap& properties) { if (end.isValid()) { setEnd(vec3FromVariant(end)); } + + auto glow = properties["glow"]; + if (glow.isValid()) { + setGlow(glow.toFloat()); + if (_glow > 0.0f) { + _alpha = 0.5f; + } + } + + auto glowWidth = properties["glow"]; + if (glowWidth.isValid()) { + setGlow(glowWidth.toFloat()); + } } QVariant Line3DOverlay::getProperty(const QString& property) { diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index db50d11276..d066677c70 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -30,10 +30,14 @@ public: // getters const glm::vec3& getStart() const { return _start; } const glm::vec3& getEnd() const { return _end; } + const float& getGlow() const { return _glow; } + const float& getGlowWidth() const { return _glowWidth; } // setters void setStart(const glm::vec3& start) { _start = start; } void setEnd(const glm::vec3& end) { _end = end; } + void setGlow(const float& glow) { _glow = glow; } + void setGlowWidth(const float& glowWidth) { _glowWidth = glowWidth; } void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; @@ -43,6 +47,8 @@ public: protected: glm::vec3 _start; glm::vec3 _end; + float _glow { 0.0 }; + float _glowWidth { 0.0 }; int _geometryCacheID; }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 9ea4bd9905..e692a663c9 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -33,6 +34,9 @@ #include "simple_vert.h" #include "simple_textured_frag.h" #include "simple_textured_unlit_frag.h" +#include "glowLine_vert.h" +#include "glowLine_geom.h" +#include "glowLine_frag.h" #include "grid_frag.h" @@ -43,9 +47,9 @@ const int GeometryCache::UNKNOWN_ID = -1; static const int VERTICES_PER_TRIANGLE = 3; -static const gpu::Element POSITION_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; -static const gpu::Element NORMAL_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; -static const gpu::Element COLOR_ELEMENT{ gpu::VEC4, gpu::NUINT8, gpu::RGBA }; +static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA }; static gpu::Stream::FormatPointer SOLID_STREAM_FORMAT; static gpu::Stream::FormatPointer INSTANCED_SOLID_STREAM_FORMAT; @@ -190,7 +194,7 @@ void setupFlatShape(GeometryCache::ShapeData& shapeData, const geometry::Solid void setupSmoothShape(GeometryCache::ShapeData& shapeData, const geometry::Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { - using namespace geometry; + using namespace geometry; Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); VertexVector vertices; @@ -254,7 +258,7 @@ void GeometryCache::buildShapes() { setupFlatShape(_shapes[Octahedron], geometry::octahedron(), _shapeVertices, _shapeIndices); // Dodecahedron setupFlatShape(_shapes[Dodecahedron], geometry::dodecahedron(), _shapeVertices, _shapeIndices); - + // Sphere // FIXME this uses way more vertices than required. Should find a way to calculate the indices // using shared vertices for better vertex caching @@ -266,7 +270,7 @@ void GeometryCache::buildShapes() { { Index baseVertex = (Index)(_shapeVertices->getSize() / SHAPE_VERTEX_STRIDE); ShapeData& shapeData = _shapes[Line]; - shapeData.setupVertices(_shapeVertices, VertexVector{ + shapeData.setupVertices(_shapeVertices, VertexVector { vec3(-0.5, 0, 0), vec3(-0.5f, 0, 0), vec3(0.5f, 0, 0), vec3(0.5f, 0, 0) }); @@ -312,32 +316,31 @@ render::ShapePipelinePointer GeometryCache::_simplePipeline; render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; GeometryCache::GeometryCache() : - _nextID(0) -{ +_nextID(0) { buildShapes(); GeometryCache::_simplePipeline = std::make_shared(getSimplePipeline(), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) { - // Set the defaults needed for a simple program - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, - DependencyManager::get()->getWhiteTexture()); - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); - } - ); + [](const render::ShapePipeline&, gpu::Batch& batch) { + // Set the defaults needed for a simple program + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, + DependencyManager::get()->getWhiteTexture()); + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, + DependencyManager::get()->getNormalFittingTexture()); + } + ); GeometryCache::_simpleWirePipeline = std::make_shared(getSimplePipeline(false, false, true, true), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) { } - ); + [](const render::ShapePipeline&, gpu::Batch& batch) {} + ); } GeometryCache::~GeometryCache() { - #ifdef WANT_DEBUG - qCDebug(renderutils) << "GeometryCache::~GeometryCache()... "; - qCDebug(renderutils) << " _registeredLine3DVBOs.size():" << _registeredLine3DVBOs.size(); - qCDebug(renderutils) << " _line3DVBOs.size():" << _line3DVBOs.size(); - qCDebug(renderutils) << " BatchItemDetails... population:" << GeometryCache::BatchItemDetails::population; - #endif //def WANT_DEBUG +#ifdef WANT_DEBUG + qCDebug(renderutils) << "GeometryCache::~GeometryCache()... "; + qCDebug(renderutils) << " _registeredLine3DVBOs.size():" << _registeredLine3DVBOs.size(); + qCDebug(renderutils) << " _line3DVBOs.size():" << _line3DVBOs.size(); + qCDebug(renderutils) << " BatchItemDetails... population:" << GeometryCache::BatchItemDetails::population; +#endif //def WANT_DEBUG } void setupBatchInstance(gpu::Batch& batch, gpu::BufferPointer colorBuffer) { @@ -384,9 +387,9 @@ void GeometryCache::renderWireSphere(gpu::Batch& batch) { } void GeometryCache::renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, - int majorRows, int majorCols, float majorEdge, - int minorRows, int minorCols, float minorEdge, - const glm::vec4& color, bool isLayered, int id) { + int majorRows, int majorCols, float majorEdge, + int minorRows, int minorCols, float minorEdge, + const glm::vec4& color, bool isLayered, int id) { static const glm::vec2 MIN_TEX_COORD(0.0f, 0.0f); static const glm::vec2 MAX_TEX_COORD(1.0f, 1.0f); @@ -433,9 +436,9 @@ void GeometryCache::updateVertices(int id, const QVector& points, con if (details.isCreated) { details.clear(); - #ifdef WANT_DEBUG - qCDebug(renderutils) << "updateVertices()... RELEASING REGISTERED"; - #endif // def WANT_DEBUG +#ifdef WANT_DEBUG + qCDebug(renderutils) << "updateVertices()... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 2 + 3; // vertices + normals @@ -444,7 +447,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.isCreated = true; details.vertices = points.size(); details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -466,9 +469,9 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.vertexSize = FLOATS_PER_VERTEX; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX]; float* vertex = vertexData; @@ -477,13 +480,13 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorDataAt = colorData; const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - foreach (const glm::vec2& point, points) { + foreach(const glm::vec2& point, points) { *(vertex++) = point.x; *(vertex++) = point.y; *(vertex++) = NORMAL.x; *(vertex++) = NORMAL.y; *(vertex++) = NORMAL.z; - + *(colorDataAt++) = compactColor; } @@ -492,18 +495,18 @@ void GeometryCache::updateVertices(int id, const QVector& points, con delete[] vertexData; delete[] colorData; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size(); - #endif +#ifdef WANT_DEBUG + qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size(); +#endif } void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { BatchItemDetails& details = _registeredVertices[id]; if (details.isCreated) { details.clear(); - #ifdef WANT_DEBUG - qCDebug(renderutils) << "updateVertices()... RELEASING REGISTERED"; - #endif // def WANT_DEBUG +#ifdef WANT_DEBUG + qCDebug(renderutils) << "updateVertices()... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals @@ -512,7 +515,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.isCreated = true; details.vertices = points.size(); details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -534,9 +537,9 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.vertexSize = FLOATS_PER_VERTEX; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX]; float* vertex = vertexData; @@ -545,14 +548,14 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorDataAt = colorData; const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - foreach (const glm::vec3& point, points) { + foreach(const glm::vec3& point, points) { *(vertex++) = point.x; *(vertex++) = point.y; *(vertex++) = point.z; *(vertex++) = NORMAL.x; *(vertex++) = NORMAL.y; *(vertex++) = NORMAL.z; - + *(colorDataAt++) = compactColor; } @@ -561,9 +564,9 @@ void GeometryCache::updateVertices(int id, const QVector& points, con delete[] vertexData; delete[] colorData; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size(); - #endif +#ifdef WANT_DEBUG + qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size(); +#endif } void GeometryCache::updateVertices(int id, const QVector& points, const QVector& texCoords, const glm::vec4& color) { @@ -571,9 +574,9 @@ void GeometryCache::updateVertices(int id, const QVector& points, con if (details.isCreated) { details.clear(); - #ifdef WANT_DEBUG - qCDebug(renderutils) << "updateVertices()... RELEASING REGISTERED"; - #endif // def WANT_DEBUG +#ifdef WANT_DEBUG + qCDebug(renderutils) << "updateVertices()... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 3 + 3 + 2; // vertices + normals + tex coords @@ -584,7 +587,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.isCreated = true; details.vertices = points.size(); details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -609,9 +612,9 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.vertexSize = FLOATS_PER_VERTEX; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX]; float* vertex = vertexData; @@ -640,9 +643,9 @@ void GeometryCache::updateVertices(int id, const QVector& points, con delete[] vertexData; delete[] colorData; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size(); - #endif +#ifdef WANT_DEBUG + qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size(); +#endif } void GeometryCache::renderVertices(gpu::Batch& batch, gpu::Primitive primitiveType, int id) { @@ -664,27 +667,27 @@ void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int Vec3Pair& lastKey = _lastRegisteredBevelRects[id]; if (lastKey != key) { details.clear(); - _lastRegisteredBevelRects[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderBevelCornersRect()... RELEASING REGISTERED"; - #endif // def WANT_DEBUG + _lastRegisteredBevelRects[id] = key; +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderBevelCornersRect()... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG else { qCDebug(renderutils) << "renderBevelCornersRect()... REUSING PREVIOUSLY REGISTERED"; } - #endif // def WANT_DEBUG +#endif // def WANT_DEBUG } if (!details.isCreated) { static const int FLOATS_PER_VERTEX = 2; // vertices static const int NUM_VERTICES = 8; static const int NUM_FLOATS = NUM_VERTICES * FLOATS_PER_VERTEX; - + details.isCreated = true; details.vertices = NUM_VERTICES; details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -694,7 +697,7 @@ void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int details.colorBuffer = colorBuffer; details.streamFormat = streamFormat; details.stream = stream; - + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ)); details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); @@ -713,7 +716,7 @@ void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int // 2 8 // // \ / // // 4 ------ 6 // - + // 1 vertexBuffer[vertexPoint++] = x; vertexBuffer[vertexPoint++] = y + height - bevelDistance; @@ -738,13 +741,13 @@ void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int // 8 vertexBuffer[vertexPoint++] = x + width; vertexBuffer[vertexPoint++] = y + bevelDistance; - + int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_VERTICES] = { compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor }; + compactColor, compactColor, compactColor, compactColor }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); @@ -766,16 +769,16 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co Vec4Pair & lastKey = _lastRegisteredQuad2D[id]; if (lastKey != key) { details.clear(); - _lastRegisteredQuad2D[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderQuad() 2D ... RELEASING REGISTERED"; - #endif // def WANT_DEBUG + _lastRegisteredQuad2D[id] = key; +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderQuad() 2D ... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG else { qCDebug(renderutils) << "renderQuad() 2D ... REUSING PREVIOUSLY REGISTERED"; } - #endif // def WANT_DEBUG +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 2 + 3; // vertices + normals @@ -788,7 +791,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co details.isCreated = true; details.vertices = VERTICES; details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -798,7 +801,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co details.colorBuffer = colorBuffer; details.streamFormat = streamFormat; details.stream = stream; - + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0); details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); @@ -808,7 +811,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { + float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, maxCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, minCorner.x, maxCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, @@ -817,9 +820,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co const int NUM_COLOR_SCALARS_PER_QUAD = 4; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); @@ -841,13 +844,13 @@ void GeometryCache::renderUnitQuad(gpu::Batch& batch, const glm::vec4& color, in void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner, - const glm::vec2& texCoordMinCorner, const glm::vec2& texCoordMaxCorner, - const glm::vec4& color, int id) { + const glm::vec2& texCoordMinCorner, const glm::vec2& texCoordMaxCorner, + const glm::vec4& color, int id) { bool registered = (id != UNKNOWN_ID); Vec4PairVec4 key(Vec4Pair(glm::vec4(minCorner.x, minCorner.y, maxCorner.x, maxCorner.y), - glm::vec4(texCoordMinCorner.x, texCoordMinCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y)), - color); + glm::vec4(texCoordMinCorner.x, texCoordMinCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y)), + color); BatchItemDetails& details = registered ? _registeredQuad2DTextures[id] : _quad2DTextures[key]; // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed @@ -855,16 +858,16 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co Vec4PairVec4& lastKey = _lastRegisteredQuad2DTexture[id]; if (lastKey != key) { details.clear(); - _lastRegisteredQuad2DTexture[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderQuad() 2D+texture ... RELEASING REGISTERED"; - #endif // def WANT_DEBUG + _lastRegisteredQuad2DTexture[id] = key; +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderQuad() 2D+texture ... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG else { qCDebug(renderutils) << "renderQuad() 2D+texture ... REUSING PREVIOUSLY REGISTERED"; } - #endif // def WANT_DEBUG +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 2 + 3 + 2; // vertices + normals + tex coords @@ -879,7 +882,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co details.isCreated = true; details.vertices = VERTICES; details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); @@ -891,7 +894,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co details.streamFormat = streamFormat; details.stream = stream; - + // zzmp: fix the normal across all renderQuad details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0); details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); @@ -903,7 +906,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { + float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, texCoordMinCorner.x, texCoordMinCorner.y, maxCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, texCoordMaxCorner.x, texCoordMinCorner.y, minCorner.x, maxCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, texCoordMinCorner.x, texCoordMaxCorner.y, @@ -913,9 +916,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co const int NUM_COLOR_SCALARS_PER_QUAD = 4; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); @@ -937,16 +940,16 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co Vec3PairVec4& lastKey = _lastRegisteredQuad3D[id]; if (lastKey != key) { details.clear(); - _lastRegisteredQuad3D[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderQuad() 3D ... RELEASING REGISTERED"; - #endif // def WANT_DEBUG + _lastRegisteredQuad3D[id] = key; +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderQuad() 3D ... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG else { qCDebug(renderutils) << "renderQuad() 3D ... REUSING PREVIOUSLY REGISTERED"; } - #endif // def WANT_DEBUG +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals @@ -959,7 +962,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co details.isCreated = true; details.vertices = VERTICES; details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); @@ -971,7 +974,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co details.streamFormat = streamFormat; details.stream = stream; - + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); @@ -981,7 +984,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { + float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, minCorner.z, NORMAL.x, NORMAL.y, NORMAL.z, maxCorner.x, minCorner.y, minCorner.z, NORMAL.x, NORMAL.y, NORMAL.z, minCorner.x, maxCorner.y, maxCorner.z, NORMAL.x, NORMAL.y, NORMAL.z, @@ -990,9 +993,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co const int NUM_COLOR_SCALARS_PER_QUAD = 4; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); @@ -1004,28 +1007,28 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } -void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, const glm::vec3& bottomLeft, - const glm::vec3& bottomRight, const glm::vec3& topRight, - const glm::vec2& texCoordTopLeft, const glm::vec2& texCoordBottomLeft, - const glm::vec2& texCoordBottomRight, const glm::vec2& texCoordTopRight, - const glm::vec4& color, int id) { +void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, const glm::vec3& bottomLeft, + const glm::vec3& bottomRight, const glm::vec3& topRight, + const glm::vec2& texCoordTopLeft, const glm::vec2& texCoordBottomLeft, + const glm::vec2& texCoordBottomRight, const glm::vec2& texCoordTopRight, + const glm::vec4& color, int id) { + +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderQuad() vec3 + texture VBO..."; + qCDebug(renderutils) << " topLeft:" << topLeft; + qCDebug(renderutils) << " bottomLeft:" << bottomLeft; + qCDebug(renderutils) << " bottomRight:" << bottomRight; + qCDebug(renderutils) << " topRight:" << topRight; + qCDebug(renderutils) << " texCoordTopLeft:" << texCoordTopLeft; + qCDebug(renderutils) << " texCoordBottomRight:" << texCoordBottomRight; + qCDebug(renderutils) << " color:" << color; +#endif //def WANT_DEBUG - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderQuad() vec3 + texture VBO..."; - qCDebug(renderutils) << " topLeft:" << topLeft; - qCDebug(renderutils) << " bottomLeft:" << bottomLeft; - qCDebug(renderutils) << " bottomRight:" << bottomRight; - qCDebug(renderutils) << " topRight:" << topRight; - qCDebug(renderutils) << " texCoordTopLeft:" << texCoordTopLeft; - qCDebug(renderutils) << " texCoordBottomRight:" << texCoordBottomRight; - qCDebug(renderutils) << " color:" << color; - #endif //def WANT_DEBUG - bool registered = (id != UNKNOWN_ID); Vec3PairVec4Pair key(Vec3Pair(topLeft, bottomRight), - Vec4Pair(glm::vec4(texCoordTopLeft.x,texCoordTopLeft.y,texCoordBottomRight.x,texCoordBottomRight.y), - color)); - + Vec4Pair(glm::vec4(texCoordTopLeft.x, texCoordTopLeft.y, texCoordBottomRight.x, texCoordBottomRight.y), + color)); + BatchItemDetails& details = registered ? _registeredQuad3DTextures[id] : _quad3DTextures[key]; // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed @@ -1034,15 +1037,15 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons if (lastKey != key) { details.clear(); _lastRegisteredQuad3DTexture[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderQuad() 3D+texture ... RELEASING REGISTERED"; - #endif // def WANT_DEBUG +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderQuad() 3D+texture ... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG else { qCDebug(renderutils) << "renderQuad() 3D+texture ... REUSING PREVIOUSLY REGISTERED"; } - #endif // def WANT_DEBUG +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 3 + 3 + 2; // vertices + normals + tex coords @@ -1058,7 +1061,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons details.isCreated = true; details.vertices = VERTICES; details.vertexSize = FLOATS_PER_VERTEX; // NOTE: this isn't used for BatchItemDetails maybe we can get rid of it - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -1068,7 +1071,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons details.colorBuffer = colorBuffer; details.streamFormat = streamFormat; details.stream = stream; - + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); details.streamFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), VERTEX_TEXCOORD_OFFSET); @@ -1088,9 +1091,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons const int NUM_COLOR_SCALARS_PER_QUAD = 4; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); @@ -1103,7 +1106,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons } void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, - const float dash_length, const float gap_length, int id) { + const float dash_length, const float gap_length, int id) { bool registered = (id != UNKNOWN_ID); Vec3PairVec2Pair key(Vec3Pair(start, end), Vec2Pair(glm::vec2(color.x, color.y), glm::vec2(color.z, color.w))); @@ -1114,18 +1117,18 @@ void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, if (_lastRegisteredDashedLines[id] != key) { details.clear(); _lastRegisteredDashedLines[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderDashedLine()... RELEASING REGISTERED"; - #endif // def WANT_DEBUG +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderDashedLine()... RELEASING REGISTERED"; +#endif // def WANT_DEBUG } } if (!details.isCreated) { int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); // draw each line segment with appropriate gaps const float SEGMENT_LENGTH = dash_length + gap_length; @@ -1143,7 +1146,7 @@ void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, details.vertices = (segmentCountFloor + 1) * 2; details.vertexSize = FLOATS_PER_VERTEX; details.isCreated = true; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -1209,13 +1212,13 @@ void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, delete[] vertexData; delete[] colorData; - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG if (registered) { qCDebug(renderutils) << "new registered dashed line buffer made -- _registeredVertices:" << _registeredDashedLines.size(); } else { qCDebug(renderutils) << "new dashed lines buffer made -- _dashedLines:" << _dashedLines.size(); } - #endif +#endif } batch.setInputFormat(details.streamFormat); @@ -1227,41 +1230,39 @@ void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, int GeometryCache::BatchItemDetails::population = 0; GeometryCache::BatchItemDetails::BatchItemDetails() : - verticesBuffer(NULL), - colorBuffer(NULL), - streamFormat(NULL), - stream(NULL), - vertices(0), - vertexSize(0), - isCreated(false) -{ +verticesBuffer(NULL), +colorBuffer(NULL), +streamFormat(NULL), +stream(NULL), +vertices(0), +vertexSize(0), +isCreated(false) { population++; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "BatchItemDetails()... population:" << population << "**********************************"; - #endif +#ifdef WANT_DEBUG + qCDebug(renderutils) << "BatchItemDetails()... population:" << population << "**********************************"; +#endif } GeometryCache::BatchItemDetails::BatchItemDetails(const GeometryCache::BatchItemDetails& other) : - verticesBuffer(other.verticesBuffer), - colorBuffer(other.colorBuffer), - streamFormat(other.streamFormat), - stream(other.stream), - vertices(other.vertices), - vertexSize(other.vertexSize), - isCreated(other.isCreated) -{ +verticesBuffer(other.verticesBuffer), +colorBuffer(other.colorBuffer), +streamFormat(other.streamFormat), +stream(other.stream), +vertices(other.vertices), +vertexSize(other.vertexSize), +isCreated(other.isCreated) { population++; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "BatchItemDetails()... population:" << population << "**********************************"; - #endif +#ifdef WANT_DEBUG + qCDebug(renderutils) << "BatchItemDetails()... population:" << population << "**********************************"; +#endif } GeometryCache::BatchItemDetails::~BatchItemDetails() { population--; - clear(); - #ifdef WANT_DEBUG - qCDebug(renderutils) << "~BatchItemDetails()... population:" << population << "**********************************"; - #endif + clear(); +#ifdef WANT_DEBUG + qCDebug(renderutils) << "~BatchItemDetails()... population:" << population << "**********************************"; +#endif } void GeometryCache::BatchItemDetails::clear() { @@ -1272,23 +1273,23 @@ void GeometryCache::BatchItemDetails::clear() { stream.reset(); } -void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color1, const glm::vec4& color2, int id) { - +void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, + const glm::vec4& color1, const glm::vec4& color2, int id) { + bool registered = (id != UNKNOWN_ID); Vec3Pair key(p1, p2); BatchItemDetails& details = registered ? _registeredLine3DVBOs[id] : _line3DVBOs[key]; int compactColor1 = ((int(color1.x * 255.0f) & 0xFF)) | - ((int(color1.y * 255.0f) & 0xFF) << 8) | - ((int(color1.z * 255.0f) & 0xFF) << 16) | - ((int(color1.w * 255.0f) & 0xFF) << 24); + ((int(color1.y * 255.0f) & 0xFF) << 8) | + ((int(color1.z * 255.0f) & 0xFF) << 16) | + ((int(color1.w * 255.0f) & 0xFF) << 24); int compactColor2 = ((int(color2.x * 255.0f) & 0xFF)) | - ((int(color2.y * 255.0f) & 0xFF) << 8) | - ((int(color2.z * 255.0f) & 0xFF) << 16) | - ((int(color2.w * 255.0f) & 0xFF) << 24); + ((int(color2.y * 255.0f) & 0xFF) << 8) | + ((int(color2.z * 255.0f) & 0xFF) << 16) | + ((int(color2.w * 255.0f) & 0xFF) << 24); // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed @@ -1296,16 +1297,16 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm Vec3Pair& lastKey = _lastRegisteredLine3D[id]; if (lastKey != key) { details.clear(); - _lastRegisteredLine3D[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderLine() 3D ... RELEASING REGISTERED line"; - #endif // def WANT_DEBUG + _lastRegisteredLine3D[id] = key; +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderLine() 3D ... RELEASING REGISTERED line"; +#endif // def WANT_DEBUG } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG else { qCDebug(renderutils) << "renderLine() 3D ... REUSING PREVIOUSLY REGISTERED line"; } - #endif // def WANT_DEBUG +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals @@ -1317,7 +1318,7 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm details.isCreated = true; details.vertices = vertices; details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -1327,7 +1328,7 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm details.colorBuffer = colorBuffer; details.streamFormat = streamFormat; details.stream = stream; - + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); @@ -1338,7 +1339,7 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f); float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { p1.x, p1.y, p1.z, NORMAL.x, NORMAL.y, NORMAL.z, - p2.x, p2.y, p2.z, NORMAL.x, NORMAL.y, NORMAL.z}; + p2.x, p2.y, p2.z, NORMAL.x, NORMAL.y, NORMAL.z }; const int NUM_COLOR_SCALARS = 2; int colors[NUM_COLOR_SCALARS] = { compactColor1, compactColor2 }; @@ -1346,13 +1347,13 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - #ifdef WANT_DEBUG - if (id == UNKNOWN_ID) { - qCDebug(renderutils) << "new renderLine() 3D VBO made -- _line3DVBOs.size():" << _line3DVBOs.size(); - } else { - qCDebug(renderutils) << "new registered renderLine() 3D VBO made -- _registeredLine3DVBOs.size():" << _registeredLine3DVBOs.size(); - } - #endif +#ifdef WANT_DEBUG + if (id == UNKNOWN_ID) { + qCDebug(renderutils) << "new renderLine() 3D VBO made -- _line3DVBOs.size():" << _line3DVBOs.size(); + } else { + qCDebug(renderutils) << "new registered renderLine() 3D VBO made -- _registeredLine3DVBOs.size():" << _registeredLine3DVBOs.size(); + } +#endif } // this is what it takes to render a quad @@ -1361,23 +1362,23 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm batch.draw(gpu::LINES, 2, 0); } -void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, - const glm::vec4& color1, const glm::vec4& color2, int id) { - +void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, + const glm::vec4& color1, const glm::vec4& color2, int id) { + bool registered = (id != UNKNOWN_ID); Vec2Pair key(p1, p2); BatchItemDetails& details = registered ? _registeredLine2DVBOs[id] : _line2DVBOs[key]; int compactColor1 = ((int(color1.x * 255.0f) & 0xFF)) | - ((int(color1.y * 255.0f) & 0xFF) << 8) | - ((int(color1.z * 255.0f) & 0xFF) << 16) | - ((int(color1.w * 255.0f) & 0xFF) << 24); + ((int(color1.y * 255.0f) & 0xFF) << 8) | + ((int(color1.z * 255.0f) & 0xFF) << 16) | + ((int(color1.w * 255.0f) & 0xFF) << 24); int compactColor2 = ((int(color2.x * 255.0f) & 0xFF)) | - ((int(color2.y * 255.0f) & 0xFF) << 8) | - ((int(color2.z * 255.0f) & 0xFF) << 16) | - ((int(color2.w * 255.0f) & 0xFF) << 24); + ((int(color2.y * 255.0f) & 0xFF) << 8) | + ((int(color2.z * 255.0f) & 0xFF) << 16) | + ((int(color2.w * 255.0f) & 0xFF) << 24); // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed @@ -1386,15 +1387,15 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm if (lastKey != key) { details.clear(); _lastRegisteredLine2D[id] = key; - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderLine() 2D ... RELEASING REGISTERED line"; - #endif // def WANT_DEBUG +#ifdef WANT_DEBUG + qCDebug(renderutils) << "renderLine() 2D ... RELEASING REGISTERED line"; +#endif // def WANT_DEBUG } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG else { qCDebug(renderutils) << "renderLine() 2D ... REUSING PREVIOUSLY REGISTERED line"; } - #endif // def WANT_DEBUG +#endif // def WANT_DEBUG } const int FLOATS_PER_VERTEX = 2; @@ -1404,7 +1405,7 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm details.isCreated = true; details.vertices = vertices; details.vertexSize = FLOATS_PER_VERTEX; - + auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); auto streamFormat = std::make_shared(); @@ -1414,7 +1415,7 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm details.colorBuffer = colorBuffer; details.streamFormat = streamFormat; details.stream = stream; - + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); @@ -1430,13 +1431,103 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - #ifdef WANT_DEBUG - if (id == UNKNOWN_ID) { - qCDebug(renderutils) << "new renderLine() 2D VBO made -- _line3DVBOs.size():" << _line2DVBOs.size(); - } else { - qCDebug(renderutils) << "new registered renderLine() 2D VBO made -- _registeredLine2DVBOs.size():" << _registeredLine2DVBOs.size(); - } - #endif +#ifdef WANT_DEBUG + if (id == UNKNOWN_ID) { + qCDebug(renderutils) << "new renderLine() 2D VBO made -- _line3DVBOs.size():" << _line2DVBOs.size(); + } else { + qCDebug(renderutils) << "new registered renderLine() 2D VBO made -- _registeredLine2DVBOs.size():" << _registeredLine2DVBOs.size(); + } +#endif + } + + // this is what it takes to render a quad + batch.setInputFormat(details.streamFormat); + batch.setInputStream(0, *details.stream); + batch.draw(gpu::LINES, 2, 0); +} + + +void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, + const glm::vec4& color, float glowIntensity, float glowWidth, int id) { + if (glowIntensity <= 0) { + renderLine(batch, p1, p2, color, id); + return; + } + + // Compile the shaders + static std::once_flag once; + std::call_once(once, [&] { + auto state = std::make_shared(); + auto VS = gpu::Shader::createVertex(std::string(glowLine_vert)); + auto GS = gpu::Shader::createGeometry(std::string(glowLine_geom)); + auto PS = gpu::Shader::createPixel(std::string(glowLine_frag)); + auto program = gpu::Shader::createProgram(VS, GS, PS); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setDepthBias(1.0f); + state->setDepthBiasSlopeScale(1.0f); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); + gpu::Shader::makeProgram(*program, slotBindings); + _glowLinePipeline = gpu::Pipeline::create(program, state); + }); + + batch.setPipeline(_glowLinePipeline); + + Vec3Pair key(p1, p2); + bool registered = (id != UNKNOWN_ID); + BatchItemDetails& details = registered ? _registeredLine3DVBOs[id] : _line3DVBOs[key]; + + int compactColor = ((int(color.x * 255.0f) & 0xFF)) | + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); + + // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed + if (registered && details.isCreated) { + Vec3Pair& lastKey = _lastRegisteredLine3D[id]; + if (lastKey != key) { + details.clear(); + _lastRegisteredLine3D[id] = key; + } + } + + const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals + const int NUM_POS_COORDS = 3; + const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float); + const int vertices = 2; + if (!details.isCreated) { + details.isCreated = true; + details.vertices = vertices; + details.vertexSize = FLOATS_PER_VERTEX; + + auto verticesBuffer = std::make_shared(); + auto colorBuffer = std::make_shared(); + auto streamFormat = std::make_shared(); + auto stream = std::make_shared(); + + details.verticesBuffer = verticesBuffer; + details.colorBuffer = colorBuffer; + details.streamFormat = streamFormat; + details.stream = stream; + + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); + details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + + details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride); + details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); + + const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f); + float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { + p1.x, p1.y, p1.z, NORMAL.x, NORMAL.y, NORMAL.z, + p2.x, p2.y, p2.z, NORMAL.x, NORMAL.y, NORMAL.z }; + + const int NUM_COLOR_SCALARS = 2; + int colors[NUM_COLOR_SCALARS] = { compactColor, compactColor }; + details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); + details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); } // this is what it takes to render a quad @@ -1532,7 +1623,7 @@ public: SimpleProgramKey(bool textured = false, bool culled = true, - bool unlit = false, bool depthBias = false) { + bool unlit = false, bool depthBias = false) { _flags = (textured ? IS_TEXTURED : 0) | (culled ? IS_CULLED : 0) | (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0); } @@ -1562,7 +1653,7 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool cul } gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool culled, bool unlit, bool depthBiased) { - SimpleProgramKey config{ textured, culled, unlit, depthBiased }; + SimpleProgramKey config { textured, culled, unlit, depthBiased }; // Compile the shaders static std::once_flag once; @@ -1570,10 +1661,10 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool culled auto VS = gpu::Shader::createVertex(std::string(simple_vert)); auto PS = gpu::Shader::createPixel(std::string(simple_textured_frag)); auto PSUnlit = gpu::Shader::createPixel(std::string(simple_textured_unlit_frag)); - + _simpleShader = gpu::Shader::createProgram(VS, PS); _unlitShader = gpu::Shader::createProgram(VS, PSUnlit); - + gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); gpu::Shader::makeProgram(*_simpleShader, slotBindings); @@ -1610,16 +1701,16 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool culled uint32_t toCompactColor(const glm::vec4& color) { uint32_t compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); return compactColor; } static const size_t INSTANCE_COLOR_BUFFER = 0; void renderInstances(gpu::Batch& batch, const glm::vec4& color, bool isWire, - const render::ShapePipelinePointer& pipeline, GeometryCache::Shape shape) { + const render::ShapePipelinePointer& pipeline, GeometryCache::Shape shape) { // Add pipeline to name std::string instanceName = (isWire ? "wire_shapes_" : "solid_shapes_") + std::to_string(shape) + "_" + std::to_string(std::hash()(pipeline)); @@ -1663,7 +1754,7 @@ void GeometryCache::renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& #ifdef DEBUG_SHAPES static auto startTime = usecTimestampNow(); renderInstances(INSTANCE_NAME, batch, color, pipeline, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { - + auto usecs = usecTimestampNow(); usecs -= startTime; auto msecs = usecs / USECS_PER_MSEC; @@ -1671,7 +1762,7 @@ void GeometryCache::renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& seconds /= MSECS_PER_SECOND; float fractionalSeconds = seconds - floor(seconds); int shapeIndex = (int)seconds; - + // Every second we flip to the next shape. static const int SHAPE_COUNT = 5; GeometryCache::Shape shapes[SHAPE_COUNT] = { @@ -1681,10 +1772,10 @@ void GeometryCache::renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& GeometryCache::Icosahedron, GeometryCache::Line, }; - + shapeIndex %= SHAPE_COUNT; GeometryCache::Shape shape = shapes[shapeIndex]; - + // For the first half second for a given shape, show the wireframe, for the second half, show the solid. if (fractionalSeconds > 0.5f) { renderInstances(INSTANCE_NAME, batch, color, true, pipeline, shape); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 9f18f1644c..647fa9889c 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -259,6 +259,9 @@ public: void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color1, const glm::vec4& color2, int id = UNKNOWN_ID); + void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, + const glm::vec4& color, float glowIntensity = 1.0f, float glowWidth = 0.05f, int id = UNKNOWN_ID); + void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, int id = UNKNOWN_ID) { renderDashedLine(batch, start, end, color, 0.05f, 0.025f, id); } @@ -403,6 +406,7 @@ private: gpu::ShaderPointer _unlitShader; static render::ShapePipelinePointer _simplePipeline; static render::ShapePipelinePointer _simpleWirePipeline; + gpu::PipelinePointer _glowLinePipeline; QHash _simplePrograms; }; diff --git a/libraries/render-utils/src/glowLine.slf b/libraries/render-utils/src/glowLine.slf new file mode 100644 index 0000000000..93a12fe5ca --- /dev/null +++ b/libraries/render-utils/src/glowLine.slf @@ -0,0 +1,32 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Bradley Austin Davis on 2016/07/05 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +layout(location = 0) in vec4 inColor; +layout(location = 1) in vec3 inLineDistance; + +out vec4 _fragColor; + +void main(void) { + vec2 d = inLineDistance.xy; + d.y = abs(d.y); + d.x = abs(d.x); + if (d.x > 1.0) { + d.x = (d.x - 1.0) / 0.02; + } else { + d.x = 0.0; + } + float alpha = 1.0 - length(d); + if (alpha < 0.01) { + discard; + } + alpha = pow(alpha, 10.0); + _fragColor = vec4(inColor.rgb, alpha); +} diff --git a/libraries/render-utils/src/glowLine.slg b/libraries/render-utils/src/glowLine.slg new file mode 100644 index 0000000000..c577112abb --- /dev/null +++ b/libraries/render-utils/src/glowLine.slg @@ -0,0 +1,127 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Bradley Austin Davis on 2016/07/05 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#extension GL_EXT_geometry_shader4 : enable + +layout(location = 0) in vec4 inColor[]; + +layout(location = 0) out vec4 outColor; +layout(location = 1) out vec3 outLineDistance; + +layout(lines) in; +layout(triangle_strip, max_vertices = 24) out; + +struct TransformCamera { + mat4 _view; + mat4 _viewInverse; + mat4 _projectionViewUntranslated; + mat4 _projection; + mat4 _projectionInverse; + vec4 _viewport; +}; + +layout(std140) uniform transformCameraBuffer { + TransformCamera _camera; +}; + +TransformCamera getTransformCamera() { + return _camera; +} + +vec3 getEyeWorldPos() { + return _camera._viewInverse[3].xyz; +} + + + + +vec3 ndcToEyeSpace(in vec4 v) { + TransformCamera cam = getTransformCamera(); + vec4 u = cam._projectionInverse * v; + return u.xyz / u.w; +} + +vec2 toScreenSpace(in vec4 v) +{ + TransformCamera cam = getTransformCamera(); + vec4 u = cam._projection * cam._view * v; + return u.xy / u.w; +} + +vec3[2] getOrthogonals(in vec3 n, float scale) { + float yDot = abs(dot(n, vec3(0, 1, 0))); + + vec3 result[2]; + if (yDot < 0.9) { + result[0] = normalize(cross(n, vec3(0, 1, 0))); + } else { + result[0] = normalize(cross(n, vec3(1, 0, 0))); + } + // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other + result[1] = cross(result[0], n); + result[0] *= scale; + result[1] *= scale; + return result; +} + + +vec2 orthogonal(vec2 v) { + vec2 result = v.yx; + result.y *= -1.0; + return result; +} + +void main() { + vec2 endpoints[2]; + vec3 eyeSpace[2]; + TransformCamera cam = getTransformCamera(); + for (int i = 0; i < 2; ++i) { + eyeSpace[i] = ndcToEyeSpace(gl_PositionIn[i]); + endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; + } + vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); + vec2 lineOrthogonal = orthogonal(lineNormal); + lineNormal *= 0.02; + lineOrthogonal *= 0.02; + + gl_Position = gl_PositionIn[0]; + gl_Position.xy -= lineNormal; + gl_Position.xy -= lineOrthogonal; + outColor = inColor[0]; + outLineDistance = vec3(-1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[0]; + gl_Position.xy -= lineNormal; + gl_Position.xy += lineOrthogonal; + outColor = inColor[0]; + outLineDistance = vec3(-1.02, 1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy += lineNormal; + gl_Position.xy -= lineOrthogonal; + outColor = inColor[1]; + outLineDistance = vec3(1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy += lineNormal; + gl_Position.xy += lineOrthogonal; + outColor = inColor[1]; + outLineDistance = vec3(1.02, 1, gl_Position.z); + EmitVertex(); + + EndPrimitive(); +} + + + + diff --git a/libraries/render-utils/src/glowLine.slv b/libraries/render-utils/src/glowLine.slv new file mode 100644 index 0000000000..aa126fe31a --- /dev/null +++ b/libraries/render-utils/src/glowLine.slv @@ -0,0 +1,26 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Bradley Austin Davis on 2016/07/05 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Color.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +layout(location = 0) out vec4 _color; + +void main(void) { + _color = inColor; + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> +} \ No newline at end of file From 828be940895fe4d2857a5bd7c2129f40400b8b88 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 7 Jul 2016 14:04:59 -0700 Subject: [PATCH 0958/1237] Fix rendering of overlay spheres with alpha --- interface/src/ui/overlays/Sphere3DOverlay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 85530d1376..bbdd886d11 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -58,7 +58,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { } const render::ShapeKey Sphere3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withOwnPipeline(); + auto builder = render::ShapeKey::Builder(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } From 99fc472d772ab942f92249ff7d40eb13fd716374 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 7 Jul 2016 14:06:57 -0700 Subject: [PATCH 0959/1237] fixed errors in Browser.qml and UpdateDialog.qml --- interface/resources/qml/Browser.qml | 10 ++++--- interface/resources/qml/UpdateDialog.qml | 37 ++++++++---------------- interface/src/ui/UpdateDialog.cpp | 8 ----- interface/src/ui/UpdateDialog.h | 8 ++--- 4 files changed, 21 insertions(+), 42 deletions(-) diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 8c8cf05444..e9b99331c7 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -3,12 +3,14 @@ import QtQuick.Controls 1.2 import QtWebEngine 1.1 import "controls-uit" +import "styles" as HifiStyles import "styles-uit" import "windows" ScrollingWindow { id: root HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifistyles } title: "Browser" resizable: true destroyOnHidden: true @@ -46,7 +48,7 @@ ScrollingWindow { id: back; enabled: webview.canGoBack; text: hifi.glyphs.backward - color: enabled ? hifi.colors.text : hifi.colors.disabledText + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goBack() } } @@ -55,7 +57,7 @@ ScrollingWindow { id: forward; enabled: webview.canGoForward; text: hifi.glyphs.forward - color: enabled ? hifi.colors.text : hifi.colors.disabledText + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } @@ -64,7 +66,7 @@ ScrollingWindow { id: reload; enabled: webview.canGoForward; text: webview.loading ? hifi.glyphs.close : hifi.glyphs.reload - color: enabled ? hifi.colors.text : hifi.colors.disabledText + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } @@ -105,7 +107,7 @@ ScrollingWindow { focus: true colorScheme: hifi.colorSchemes.dark placeholderText: "Enter URL" - Component.onCompleted: scriptsModel.filterRegExp = new RegExp("^.*$", "i") + Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i") Keys.onPressed: { switch(event.key) { case Qt.Key_Enter: diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 91dc210eda..ca3a2da577 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -3,13 +3,16 @@ import QtQuick 2.3 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 import QtGraphicalEffects 1.0 + import "controls-uit" +import "styles" as HifiStyles import "styles-uit" import "windows" ScrollingWindow { id: root HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifistyles } objectName: "UpdateDialog" width: updateDialog.implicitWidth height: updateDialog.implicitHeight @@ -40,22 +43,6 @@ ScrollingWindow { width: updateDialog.contentWidth + updateDialog.borderWidth * 2 height: mainContent.height + updateDialog.borderWidth * 2 - updateDialog.closeMargin / 2 - - MouseArea { - width: parent.width - height: parent.height - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - drag { - target: root - minimumX: 0 - minimumY: 0 - maximumX: root.parent ? root.maximumX : 0 - maximumY: root.parent ? root.maximumY : 0 - } - } } Image { @@ -89,7 +76,7 @@ ScrollingWindow { text: "Update Available" font { family: updateDialog.fontFamily - pixelSize: hifi.fonts.pixelSize * 1.5 + pixelSize: hifistyles.fonts.pixelSize * 1.5 weight: Font.DemiBold } color: "#303030" @@ -100,10 +87,10 @@ ScrollingWindow { text: updateDialog.updateAvailableDetails font { family: updateDialog.fontFamily - pixelSize: hifi.fonts.pixelSize * 0.6 + pixelSize: hifistyles.fonts.pixelSize * 0.6 letterSpacing: -0.5 } - color: hifi.colors.text + color: hifistyles.colors.text anchors { top: updateAvailable.bottom } @@ -130,12 +117,12 @@ ScrollingWindow { Text { id: releaseNotes wrapMode: Text.Wrap - width: parent.width - updateDialog.closeMargin + width: parent.parent.width - updateDialog.closeMargin text: updateDialog.releaseNotes - color: hifi.colors.text + color: hifistyles.colors.text font { family: updateDialog.fontFamily - pixelSize: hifi.fonts.pixelSize * 0.65 + pixelSize: hifistyles.fonts.pixelSize * 0.65 } } } @@ -157,7 +144,7 @@ ScrollingWindow { color: "#0c9ab4" // Same as logo font { family: updateDialog.fontFamily - pixelSize: hifi.fonts.pixelSize * 1.2 + pixelSize: hifistyles.fonts.pixelSize * 1.2 weight: Font.DemiBold } anchors { @@ -169,7 +156,7 @@ ScrollingWindow { MouseArea { id: cancelButtonAction anchors.fill: parent - onClicked: updateDialog.closeDialog() + onClicked: root.shown = false cursorShape: "PointingHandCursor" } } @@ -185,7 +172,7 @@ ScrollingWindow { color: "#0c9ab4" // Same as logo font { family: updateDialog.fontFamily - pixelSize: hifi.fonts.pixelSize * 1.2 + pixelSize: hifistyles.fonts.pixelSize * 1.2 weight: Font.DemiBold } anchors { diff --git a/interface/src/ui/UpdateDialog.cpp b/interface/src/ui/UpdateDialog.cpp index 437e173807..60acd0895b 100644 --- a/interface/src/ui/UpdateDialog.cpp +++ b/interface/src/ui/UpdateDialog.cpp @@ -48,14 +48,6 @@ const QString& UpdateDialog::releaseNotes() const { return _releaseNotes; } -void UpdateDialog::closeDialog() { - hide(); -} - -void UpdateDialog::hide() { - ((QQuickItem*)parent())->setVisible(false); -} - void UpdateDialog::triggerUpgrade() { auto applicationUpdater = DependencyManager::get(); applicationUpdater.data()->performAutoUpdate(applicationUpdater.data()->getBuildData().lastKey()); diff --git a/interface/src/ui/UpdateDialog.h b/interface/src/ui/UpdateDialog.h index 84d390c942..4adff90283 100644 --- a/interface/src/ui/UpdateDialog.h +++ b/interface/src/ui/UpdateDialog.h @@ -21,22 +21,20 @@ class UpdateDialog : public OffscreenQmlDialog { Q_OBJECT HIFI_QML_DECL - Q_PROPERTY(QString updateAvailableDetails READ updateAvailableDetails) - Q_PROPERTY(QString releaseNotes READ releaseNotes) + Q_PROPERTY(QString updateAvailableDetails READ updateAvailableDetails CONSTANT) + Q_PROPERTY(QString releaseNotes READ releaseNotes CONSTANT) public: UpdateDialog(QQuickItem* parent = nullptr); const QString& updateAvailableDetails() const; const QString& releaseNotes() const; - + private: QString _updateAvailableDetails; QString _releaseNotes; protected: - void hide(); Q_INVOKABLE void triggerUpgrade(); - Q_INVOKABLE void closeDialog(); }; From 5c83db3b28302a817363877780dbde73dff2e574 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 14:37:51 -0700 Subject: [PATCH 0960/1237] take care of naughty intervals --- scripts/system/controllers/teleport.js | 73 ++++++++++++++++++-------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 577d6bf456..dfc471182d 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -12,9 +12,14 @@ var inTeleportMode = false; var currentFadeSphereOpacity = 1; var fadeSphereInterval = null; +var fadeSphereUpdateInterval = null; //milliseconds between fading one-tenth -- so this is a half second fade total -var FADE_IN_INTERVAL = 50; -var FADE_OUT_INTERVAL = 50; +var USE_FADE_MODE = true; +var USE_FADE_IN = false; +var USE_FADE_OUT = true; +var FADE_IN_INTERVAL = 25; +var FADE_OUT_INTERVAL = 25; + // instant var NUMBER_OF_STEPS = 0; @@ -24,7 +29,7 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 150; // var NUMBER_OF_STEPS = 2; -//medium-slow +// medium-slow // var SMOOTH_ARRIVAL_SPACING = 100; // var NUMBER_OF_STEPS = 4; @@ -32,10 +37,13 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 33; // var NUMBER_OF_STEPS = 6; -// //fast +//fast // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; + +// var NUMBER_OF_STEPS=9; +// var SMOOTH_ARRIVAL_SPACING=25; var USE_THUMB_AND_TRIGGER_MODE = false; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; @@ -82,6 +90,7 @@ function Teleporter() { this.targetOverlay = null; this.updateConnected = null; this.smoothArrivalInterval = null; + this.fadeSphere = null; this.initialize = function() { this.createMappings(); @@ -138,10 +147,11 @@ function Teleporter() { }; + this.createFadeSphere = function(avatarHead) { var sphereProps = { position: avatarHead, - size: 0.25, + size: -1, color: { red: 0, green: 0, @@ -151,30 +161,40 @@ function Teleporter() { solid: true, visible: true, ignoreRayIntersection: true, - drawInFront: true + drawInFront: false }; - currentFadeSphereOpacity = 1; - - _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); + currentFadeSphereOpacity = 10; + _this.fadeSphere = Overlays.addOverlay("cube", sphereProps); + Script.clearInterval(fadeSphereInterval) Script.update.connect(_this.updateFadeSphere); + if (USE_FADE_OUT === true) { + this.fadeSphereOut(); + } + if (USE_FADE_IN === true) { + this.fadeSphereIn(); + } + }; this.fadeSphereOut = function() { fadeSphereInterval = Script.setInterval(function() { - if (currentFadeSphereOpacity === 0) { + if (currentFadeSphereOpacity <= 0) { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); - fadeSphereInterval = null; + fadeSphereInterval = null; + print('sphere done fading out'); return; } if (currentFadeSphereOpacity > 0) { - currentFadeSphereOpacity -= 0.1; + currentFadeSphereOpacity = currentFadeSphereOpacity - 1; } + + print('setting sphere alpha to: ' + currentFadeSphereOpacity) Overlays.editOverlay(_this.fadeSphere, { - alpha: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity / 10 }) }, FADE_OUT_INTERVAL); @@ -182,17 +202,18 @@ function Teleporter() { this.fadeSphereIn = function() { fadeSphereInterval = Script.setInterval(function() { - if (currentFadeSphereOpacity === 1) { + if (currentFadeSphereOpacity >= 1) { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); fadeSphereInterval = null; + print('sphere done fading in') return; } if (currentFadeSphereOpacity < 1) { - currentFadeSphereOpacity += 0.1; + currentFadeSphereOpacity = currentFadeSphereOpacity - 1; } Overlays.editOverlay(_this.fadeSphere, { - alpha: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity / 10 }) }, FADE_IN_INTERVAL); @@ -206,8 +227,13 @@ function Teleporter() { }; this.deleteFadeSphere = function() { - Script.update.disconnect(_this.updateFadeSphere); - Overlays.deleteOverlay(_this.fadeSphere); + if (_this.fadeSphere !== null) { + print('deleting fade sphere'); + Script.update.disconnect(_this.updateFadeSphere); + Overlays.deleteOverlay(_this.fadeSphere); + _this.fadeSphere = null; + } + }; this.deleteTargetOverlay = function() { @@ -227,7 +253,6 @@ function Teleporter() { } else { Script.update.disconnect(this.update); - } this.updateConnected = null; @@ -492,10 +517,12 @@ function Teleporter() { this.teleport = function(value) { - if (_this.intersection !== null) { - + if (this.intersection !== null) { + if (USE_FADE_MODE === true) { + this.createFadeSphere(); + } var offset = getAvatarFootOffset(); - _this.intersection.intersection.y += offset; + this.intersection.intersection.y += offset; // MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); this.smoothArrival(); @@ -540,6 +567,7 @@ function Teleporter() { var landingPoint = _this.arrivalPoints.shift(); print('landing at: ' + JSON.stringify(landingPoint)) MyAvatar.position = landingPoint; + if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { print('clear target overlay') _this.deleteTargetOverlay(); @@ -674,6 +702,7 @@ function cleanup() { teleporter.disableMappings(); teleporter.deleteTargetOverlay(); teleporter.turnOffOverlayBeams(); + teleporter.deleteFadeSphere(); if (teleporter.updateConnected !== null) { if (USE_THUMB_AND_TRIGGER_MODE === true) { From 1eb4acf8159dec8346296d5415cd9fbee4a34b3c Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 7 Jul 2016 15:41:00 -0700 Subject: [PATCH 0961/1237] Unifying the lighting pass for scaterring and not --- .../render-utils/src/DeferredGlobalLight.slh | 13 +- .../render-utils/src/LightDirectional.slh | 37 +---- libraries/render-utils/src/LightPoint.slh | 38 +----- libraries/render-utils/src/LightSpot.slh | 41 +----- libraries/render-utils/src/LightingModel.slh | 127 +++++++++--------- .../render-utils/src/RenderDeferredTask.cpp | 3 +- .../src/directional_ambient_light.slf | 8 +- .../src/directional_skybox_light.slf | 7 +- libraries/render-utils/src/point_light.slf | 4 +- libraries/render-utils/src/spot_light.slf | 4 +- .../utilities/render/deferredLighting.qml | 33 ++++- 11 files changed, 137 insertions(+), 178 deletions(-) diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 2232638000..8247554920 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -34,16 +34,15 @@ vec3 color = vec3(0.0); <@if isScattering@> - vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin - float metallic = 0.0; <@else@> + color += emissive * isEmissiveEnabled(); +<@endif@> + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value if (metallic > 0.5) { fresnel = albedo; metallic = 1.0; } - color += emissive * isEmissiveEnabled(); -<@endif@> <@endfunc@> @@ -87,7 +86,7 @@ vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, floa <$declareDeferredCurvature()$> -vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { +vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { <$prepareGlobalLight(1)$> @@ -155,10 +154,10 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu <$declareDeferredCurvature()$> -vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { +vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { <$prepareGlobalLight(1)$> - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); + vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); float highCurvature = unpackCurvature(blurredCurvature.w); float lowCurvature = unpackCurvature(diffusedCurvature.w); diff --git a/libraries/render-utils/src/LightDirectional.slh b/libraries/render-utils/src/LightDirectional.slh index 63ee8b3eda..b7dd1f3c61 100644 --- a/libraries/render-utils/src/LightDirectional.slh +++ b/libraries/render-utils/src/LightDirectional.slh @@ -25,46 +25,23 @@ void evalLightingDirectional(out vec3 diffuse, out vec3 specular, Light light, <@if supportScattering@> -<@include SubsurfaceScattering.slh@> -<$declareSubsurfaceScatteringBRDF()$> -<$declareSkinSpecularLighting()$> - void evalLightingDirectionalScattering(out vec3 diffuse, out vec3 specular, Light light, vec3 eyeDir, vec3 normal, float roughness, float metallic, vec3 fresnel, vec3 albedo, float shadow, float scattering, vec3 midNormal, vec3 lowNormal, float curvature) { vec3 fragLightDir = -normalize(getLightDirection(light)); - vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); - float scatteringLevel = getScatteringLevel(); - vec4 shading; - float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - { // Key Sun Lighting - // Diffuse Lighting - //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - // Specular Lighting - vec3 halfDir = normalize(eyeDir + fragLightDir); + evalFragShading(diffuse, specular, + normal, fragLightDir, eyeDir, + metallic, fresnel, roughness, + scattering, vec4(midNormal, curvature), vec4(lowNormal, curvature)); - float specular = skinSpecular(normal, fragLightDir, eyeDir, roughness, 1.0); + vec3 lightEnergy = shadow * getLightColor(light) * getLightIntensity(light); - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); - float power = specularDistribution(roughness, normal, halfDir); - //vec3 specular = power * fresnelColor * standardDiffuse; + diffuse *= albedo * lightEnergy; - shading = vec4(vec3(specular), (1 - fresnelColor.x)); - } - - - if (scatteringLevel < 0.1) { - brdf = vec3(standardDiffuse); - } - brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); - - - diffuse = albedo * brdf.xyz * shadow * getLightColor(light) * getLightIntensity(light); - - specular = shading.rgb * shadow * getLightColor(light) * getLightIntensity(light); + specular *= lightEnergy; } <@endif@> diff --git a/libraries/render-utils/src/LightPoint.slh b/libraries/render-utils/src/LightPoint.slh index bf5c8aa307..f5d23bba1a 100644 --- a/libraries/render-utils/src/LightPoint.slh +++ b/libraries/render-utils/src/LightPoint.slh @@ -46,10 +46,6 @@ void evalLightingPoint(out vec3 diffuse, out vec3 specular, Light light, <@if supportScattering@> -<@include SubsurfaceScattering.slh@> -<$declareSubsurfaceScatteringBRDF()$> -<$declareSkinSpecularLighting()$> - void evalLightingPointScattering(out vec3 diffuse, out vec3 specular, Light light, vec3 fragLightVec, vec3 fragEyeDir, vec3 normal, float roughness, float metallic, vec3 fresnel, vec3 albedo, float shadow, @@ -65,36 +61,14 @@ void evalLightingPointScattering(out vec3 diffuse, out vec3 specular, Light ligh vec3 lightEnergy = radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); // Eval shading - vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); - float scatteringLevel = getScatteringLevel(); - vec4 shading; - float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - { // Key Sun Lighting - // Diffuse Lighting - //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + evalFragShading(diffuse, specular, + normal, fragLightDir, fragEyeDir, + metallic, fresnel, roughness, + scattering, vec4(midNormal, curvature), vec4(lowNormal, curvature)); - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); + diffuse *= albedo * lightEnergy; - float specular = skinSpecular(normal, fragLightDir, fragEyeDir, roughness, 1.0); - - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); - float power = specularDistribution(roughness, normal, halfDir); - //vec3 specular = power * fresnelColor * standardDiffuse; - - shading = vec4(vec3(specular), (1 - fresnelColor.x)); - } - - - if (scatteringLevel < 0.1) { - brdf = vec3(standardDiffuse); - } - brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); - - - diffuse = albedo * brdf.xyz * lightEnergy; - - specular = shading.rgb * lightEnergy; + specular *= lightEnergy; if (getLightShowContour(light) > 0.0) { // Show edge diff --git a/libraries/render-utils/src/LightSpot.slh b/libraries/render-utils/src/LightSpot.slh index 56c70d6c30..0a03789dce 100644 --- a/libraries/render-utils/src/LightSpot.slh +++ b/libraries/render-utils/src/LightSpot.slh @@ -11,9 +11,6 @@ <@func declareLightingSpot(supportScattering)@> - -<@include DeferredLighting.slh@> - void evalLightingSpot(out vec3 diffuse, out vec3 specular, Light light, vec4 fragLightDirLen, float cosSpotAngle, vec3 fragEyeDir, vec3 normal, float roughness, float metallic, vec3 fresnel, vec3 albedo, float shadow) { @@ -49,10 +46,6 @@ void evalLightingSpot(out vec3 diffuse, out vec3 specular, Light light, <@if supportScattering@> -<@include SubsurfaceScattering.slh@> -<$declareSubsurfaceScatteringBRDF()$> -<$declareSkinSpecularLighting()$> - void evalLightingSpotScattering(out vec3 diffuse, out vec3 specular, Light light, vec4 fragLightDirLen, float cosSpotAngle, vec3 fragEyeDir, vec3 normal, float roughness, float metallic, vec3 fresnel, vec3 albedo, float shadow, @@ -68,36 +61,14 @@ void evalLightingSpotScattering(out vec3 diffuse, out vec3 specular, Light light vec3 lightEnergy = angularAttenuation * radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); // Eval shading - vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); - float scatteringLevel = getScatteringLevel(); - vec4 shading; - float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - { // Key Sun Lighting - // Diffuse Lighting - //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); + evalFragShading(diffuse, specular, + normal, fragLightDir, fragEyeDir, + metallic, fresnel, roughness, + scattering, vec4(midNormal, curvature), vec4(lowNormal, curvature)); - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); + diffuse *= albedo * lightEnergy; - float specular = skinSpecular(normal, fragLightDir, fragEyeDir, roughness, 1.0); - - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); - float power = specularDistribution(roughness, normal, halfDir); - //vec3 specular = power * fresnelColor * standardDiffuse; - - shading = vec4(vec3(specular), (1 - fresnelColor.x)); - } - - - if (scatteringLevel < 0.1) { - brdf = vec3(standardDiffuse); - } - brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); - - - diffuse = albedo * brdf.xyz * lightEnergy; - - specular = shading.rgb * lightEnergy; + specular *= lightEnergy; if (getLightShowContour(light) > 0.0) { // Show edges diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index e0c7ce779f..787b583b98 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -66,14 +66,48 @@ float isShowContour() { <@endfunc@> +<@func declareBeckmannSpecular()@> +uniform sampler2D scatteringSpecularBeckmann; + +float fetchSpecularBeckmann(float ndoth, float roughness) { + return pow(2.0 * texture(scatteringSpecularBeckmann, vec2(ndoth, roughness)).r, 10.0); +} + +float fresnelSchlickScalar(float fresnelColor, vec3 lightDir, vec3 halfDir) { + float base = 1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0); + float exponential = pow(base, 5.0); + return (exponential)+fresnelColor * (1.0 - exponential); +} + +vec2 skinSpecular(vec3 N, vec3 L, vec3 V, float roughness, float intensity) { + vec2 result = vec2(0.0, 1.0); + float ndotl = dot(N, L); + if (ndotl > 0.0) { + vec3 h = L + V; + vec3 H = normalize(h); + float ndoth = dot(N, H); + float PH = fetchSpecularBeckmann(ndoth, roughness); + float F = fresnelSchlickScalar(0.028, H, V); + float frSpec = max(PH * F / dot(h, h), 0.0); + result.x = ndotl * intensity * frSpec; + result.y -= F; + } + + return result; +} +<@endfunc@> <@func declareEvalPBRShading()@> -vec3 fresnelSchlick(vec3 fresnelColor, vec3 lightDir, vec3 halfDir) { - return fresnelColor + (1.0 - fresnelColor) * pow(1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0), 5); +vec3 fresnelSchlickColor(vec3 fresnelColor, vec3 lightDir, vec3 halfDir) { + float base = 1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0); + float exponential = pow(base, 5.0); + return vec3(exponential) + fresnelColor * (1.0 - exponential); } + + float specularDistribution(float roughness, vec3 normal, vec3 halfDir) { float ndoth = clamp(dot(halfDir, normal), 0.0, 1.0); float gloss2 = pow(0.001 + roughness, 4); @@ -96,7 +130,7 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float m // Specular Lighting vec3 halfDir = normalize(fragEyeDir + fragLightDir); - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); + vec3 fresnelColor = fresnelSchlickColor(fresnel, fragLightDir, halfDir); float power = specularDistribution(roughness, fragNormal, halfDir); vec3 specular = power * fresnelColor * diffuse; @@ -104,73 +138,42 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float m } <@endfunc@> + + <$declareEvalPBRShading()$> // Return xyz the specular/reflection component and w the diffuse component -vec4 evalFragShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 specular, float roughness) { - return evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, specular, roughness); -} - 0.0) { - vec3 h = L + V; - vec3 H = normalize(h); - float ndoth = dot(N, H); - float PH = fetchSpecularBeckmann(ndoth, roughness); - float F = fresnelReflectance(H, V, 0.028); - float frSpec = max(PH * F / dot(h, h), 0.0); - result = ndotl * intensity * frSpec; +<$declareBeckmannSpecular()$> +<@include SubsurfaceScattering.slh@> +<$declareSubsurfaceScatteringBRDF()$> + +void evalFragShading(out vec3 diffuse, out vec3 specular, + vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, + float metallic, vec3 fresnel, float roughness, + float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature) { + if (scattering > 0.0) { + vec3 brdf = evalSkinBRDF(fragLightDir, fragNormal, midNormalCurvature.xyz, lowNormalCurvature.xyz, lowNormalCurvature.w); + float NdotL = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); + diffuse = mix(vec3(NdotL), brdf, scattering); + + // Specular Lighting + vec3 halfDir = normalize(fragEyeDir + fragLightDir); + vec2 specularBrdf = skinSpecular(fragNormal, fragLightDir, fragEyeDir, roughness, 1.0); + + diffuse *= specularBrdf.y; + specular = vec3(specularBrdf.x); + + } else { + vec4 shading = evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, specular, roughness); + diffuse = vec3(shading.w); + specular = shading.xyz; } - - return result; } -// Eval shading -vec3 brdf = evalSkinBRDF(fragLightDir, normal, midNormal, lowNormal, curvature); -float scatteringLevel = getScatteringLevel(); -vec4 shading; -float standardDiffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); -{ // Key Sun Lighting - // Diffuse Lighting - //float diffuse = clamp(dot(normal, fragLightDir), 0.0, 1.0); - - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); - - float specular = skinSpecular(normal, fragLightDir, fragEyeDir, roughness, 1.0); - - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir, halfDir); - float power = specularDistribution(roughness, normal, halfDir); - //vec3 specular = power * fresnelColor * standardDiffuse; - - shading = vec4(vec3(specular), (1 - fresnelColor.x)); -} - - -if (scatteringLevel < 0.1) { - brdf = vec3(standardDiffuse); -} -brdf = mix(vec3(standardDiffuse), brdf, scatteringLevel * scattering); - - -diffuse = albedo * brdf.xyz * lightEnergy; - -specular = shading.rgb * lightEnergy; - -!> <@endif@> diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 006279909e..13ea658d0c 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -140,6 +140,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Render transparent objects forward in LightingBuffer addJob("DrawTransparentDeferred", transparents, shapePlumber); + addJob("DebugScattering", deferredLightingInputs); + // Lighting Buffer ready for tone mapping addJob("ToneMapping"); @@ -151,7 +153,6 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Debugging stages { - addJob("DebugScattering", deferredLightingInputs); // Debugging Deferred buffer job const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(diffusedCurvatureFramebuffer, curvatureFramebuffer)); diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index 89663dbfa4..e8ccffa030 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -39,7 +39,7 @@ void main(void) { frag.diffuse, frag.specularVal.xyz); _fragColor = vec4(color, 1.0); - } else if (frag.mode == FRAG_MODE_SCATTERING) { + } else { //if (frag.mode == FRAG_MODE_SCATTERING) { vec4 blurredCurvature = fetchCurvature(_texCoord0); vec4 diffusedCurvature = fetchDiffusedCurvature(_texCoord0); @@ -51,12 +51,14 @@ void main(void) { frag.position.xyz, frag.normal, frag.diffuse, + frag.metallic, + frag.emissive, frag.roughness, frag.scattering, blurredCurvature, diffusedCurvature); _fragColor = vec4(color, 1.0); - } else { + /* } else { vec3 color = evalAmbientSphereGlobalColor( getViewInverse(), shadowAttenuation, @@ -67,6 +69,6 @@ void main(void) { frag.metallic, frag.emissive, frag.roughness); - _fragColor = vec4(color, frag.normalVal.a); + _fragColor = vec4(color, frag.normalVal.a);*/ } } diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index e2183887a5..851bc026e3 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -39,7 +39,7 @@ void main(void) { frag.diffuse, frag.specularVal.xyz); _fragColor = vec4(color, 1.0); - } else if (frag.mode == FRAG_MODE_SCATTERING) { + } else {// if (frag.mode == FRAG_MODE_SCATTERING) { vec4 blurredCurvature = fetchCurvature(_texCoord0); vec4 diffusedCurvature = fetchDiffusedCurvature(_texCoord0); @@ -51,12 +51,14 @@ void main(void) { frag.position.xyz, frag.normal, frag.diffuse, + frag.metallic, + frag.emissive, frag.roughness, frag.scattering, blurredCurvature, diffusedCurvature); _fragColor = vec4(color, 1.0); - } else { + /* } else { vec3 color = evalSkyboxGlobalColor( getViewInverse(), shadowAttenuation, @@ -69,5 +71,6 @@ void main(void) { frag.roughness); _fragColor = vec4(color, frag.normalVal.a); + */ } } diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 3012ad1ac2..ebf7726634 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -69,7 +69,7 @@ void main(void) { vec3 diffuse; vec3 specular; - if ((isScatteringEnabled() > 0.0) && (frag.mode == FRAG_MODE_SCATTERING)) { + if (frag.mode == FRAG_MODE_SCATTERING) { vec4 blurredCurvature = fetchCurvature(texCoord); vec4 diffusedCurvature = fetchDiffusedCurvature(texCoord); vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); @@ -79,7 +79,7 @@ void main(void) { evalLightingPointScattering(diffuse, specular, light, fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, frag.metallic, frag.specular, frag.diffuse, 1.0, - frag.scattering, midNormal, lowNormal, lowCurvature); + frag.scattering * isScatteringEnabled(), midNormal, lowNormal, lowCurvature); } else { evalLightingPoint(diffuse, specular, light, fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 5654b86d68..596666965b 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -70,7 +70,7 @@ void main(void) { vec3 diffuse; vec3 specular; - if ((isScatteringEnabled() > 0.0) && (frag.mode == FRAG_MODE_SCATTERING)) { + if (frag.mode == FRAG_MODE_SCATTERING) { vec4 blurredCurvature = fetchCurvature(texCoord); vec4 diffusedCurvature = fetchDiffusedCurvature(texCoord); vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); @@ -80,7 +80,7 @@ void main(void) { evalLightingSpotScattering(diffuse, specular, light, fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, frag.metallic, frag.specular, frag.diffuse, 1.0, - frag.scattering, midNormal, lowNormal, lowCurvature); + frag.scattering * isScatteringEnabled(), midNormal, lowNormal, lowCurvature); } else { evalLightingSpot(diffuse, specular, light, fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 8c3ff66c17..5a325ae7ce 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -11,10 +11,11 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import "configSlider" -Column { +Row { spacing: 8 + + Column { - id: deferredLighting spacing: 10 Repeater { model: [ @@ -22,9 +23,36 @@ Column { "Shaded:LightingModel:enableShaded", "Emissive:LightingModel:enableEmissive", "Lightmap:LightingModel:enableLightmap", + ] + CheckBox { + text: modelData.split(":")[0] + checked: Render.getConfig(modelData.split(":")[1]) + onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } + + + Column { + spacing: 10 + Repeater { + model: [ "Scattering:LightingModel:enableScattering", "Diffuse:LightingModel:enableDiffuse", "Specular:LightingModel:enableSpecular", + ] + CheckBox { + text: modelData.split(":")[0] + checked: Render.getConfig(modelData.split(":")[1]) + onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } + + Column { + spacing: 10 + Repeater { + model: [ "Ambient:LightingModel:enableAmbientLight", "Directional:LightingModel:enableDirectionalLight", "Point:LightingModel:enablePointLight", @@ -37,4 +65,5 @@ Column { } } } + } From 600348bf102fdef7bbb28e16bffd104b208d6838 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 1 Jul 2016 19:27:00 -0700 Subject: [PATCH 0962/1237] Initial cut of htrf for mono localOnly injectors Probably need to clean up a bit, but wanted to get this out there for comment on more general issues, etc... To test, I added a localOnly: true to the cow in the tutorial. --- libraries/audio-client/src/AudioClient.cpp | 263 +++++++++++++++++++-- libraries/audio-client/src/AudioClient.h | 11 +- libraries/audio/src/AudioInjector.h | 14 +- 3 files changed, 262 insertions(+), 26 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 7628c09748..01bd0d3c22 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include #ifdef __APPLE__ #include @@ -42,6 +44,8 @@ #include #include "AudioInjector.h" +#include "AudioLimiter.h" +#include "AudioHRTF.h" #include "AudioConstants.h" #include "PositionalAudioStream.h" #include "AudioClientLogging.h" @@ -852,33 +856,43 @@ void AudioClient::setIsStereoInput(bool isStereoInput) { bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { - if (injector->getLocalBuffer()) { - QAudioFormat localFormat = _desiredOutputFormat; - localFormat.setChannelCount(isStereo ? 2 : 1); + if (injector->getLocalBuffer() && _audioInput ) { + if(isStereo) { + QAudioFormat localFormat = _desiredOutputFormat; + localFormat.setChannelCount(isStereo ? 2 : 1); - QAudioOutput* localOutput = new QAudioOutput(getNamedAudioDeviceForMode(QAudio::AudioOutput, _outputAudioDeviceName), - localFormat, - injector->getLocalBuffer()); + QAudioOutput* localOutput = new QAudioOutput(getNamedAudioDeviceForMode(QAudio::AudioOutput, _outputAudioDeviceName), + localFormat, + injector->getLocalBuffer()); - // move the localOutput to the same thread as the local injector buffer - localOutput->moveToThread(injector->getLocalBuffer()->thread()); + // move the localOutput to the same thread as the local injector buffer + localOutput->moveToThread(injector->getLocalBuffer()->thread()); - // have it be stopped when that local buffer is about to close - // We don't want to stop this localOutput and injector whenever this AudioClient singleton goes idle, - // only when the localOutput does. But the connection is to localOutput, so that it happens on the right thread. - connect(localOutput, &QAudioOutput::stateChanged, localOutput, [=](QAudio::State state) { - if (state == QAudio::IdleState) { - localOutput->stop(); - injector->stop(); - } - }); + // have it be stopped when that local buffer is about to close + // We don't want to stop this localOutput and injector whenever this AudioClient singleton goes idle, + // only when the localOutput does. But the connection is to localOutput, so that it happens on the right thread. + connect(localOutput, &QAudioOutput::stateChanged, localOutput, [=](QAudio::State state) { + if (state == QAudio::IdleState) { + localOutput->stop(); + injector->stop(); + } + }); - connect(injector->getLocalBuffer(), &QIODevice::aboutToClose, localOutput, &QAudioOutput::stop); + connect(injector->getLocalBuffer(), &QIODevice::aboutToClose, localOutput, &QAudioOutput::stop); - qCDebug(audioclient) << "Starting QAudioOutput for local injector" << localOutput; + qCDebug(audioclient) << "Starting QAudioOutput for local injector" << localOutput; + + localOutput->start(injector->getLocalBuffer()); + return localOutput->state() == QAudio::ActiveState; + } else { + // just add it to the vector of active local injectors + // TODO: deal with concurrency perhaps? Maybe not + qDebug() << "adding new injector!!!!!!!"; + + _activeLocalAudioInjectors.append(injector); + return true; + } - localOutput->start(injector->getLocalBuffer()); - return localOutput->state() == QAudio::ActiveState; } return false; @@ -1134,20 +1148,225 @@ float AudioClient::getAudioOutputMsecsUnplayed() const { return msecsAudioOutputUnplayed; } +void AudioClient::AudioOutputIODevice::renderHRTF(AudioHRTF& hrtf, int16_t* data, float* hrtfBuffer, float azimuth, float gain, qint64 numSamples) { + qint64 numFrames = numSamples/AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + int16_t* dataPtr = data; + float* hrtfPtr = hrtfBuffer; + for(qint64 i=0; i < numFrames; i++) { + hrtf.render(dataPtr, hrtfPtr, 1, azimuth, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + //qDebug() << "processed frame " << i; + + dataPtr += AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + hrtfPtr += 2*AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + + } + assert(dataPtr - data <= numSamples); + assert(hrtfPtr - hrtfBuffer <= 2*numSamples); +} + +float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { + // copied from AudioMixer, more or less + glm::quat inverseOrientation = glm::inverse(_orientationGetter()); + + // compute sample delay for the 2 ears to create phase panning + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; + + // project the rotated source position vector onto x-y plane + rotatedSourcePosition.y = 0.0f; + + static const float SOURCE_DISTANCE_THRESHOLD = 1e-30f; + + if (glm::length2(rotatedSourcePosition) > SOURCE_DISTANCE_THRESHOLD) { + + // produce an oriented angle about the y-axis + return glm::orientedAngle(glm::vec3(0.0f, 0.0f, -1.0f), glm::normalize(rotatedSourcePosition), glm::vec3(0.0f, -1.0f, 0.0f)); + } else { + + // no azimuth if they are in same spot + return 0.0f; + } +} + +float AudioClient::gainForSource(const glm::vec3& relativePosition, float volume) { + // TODO: put these in a place where we can share with AudioMixer! + const float LOUDNESS_TO_DISTANCE_RATIO = 0.00001f; + const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.18f; + const float DEFAULT_NOISE_MUTING_THRESHOLD = 0.003f; + const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f; + + + //qDebug() << "initial gain is " << volume; + + // I'm assuming that the AudioMixer's getting of the stream's attenuation + // factor is basically same as getting volume + float gain = volume; + float distanceBetween = glm::length(relativePosition); + if (distanceBetween < EPSILON ) { + distanceBetween = EPSILON; + } + + // audio mixer has notion of zones. Unsure how to map that across here... + + // attenuate based on distance now + if (distanceBetween >= ATTENUATION_BEGINS_AT_DISTANCE) { + float distanceCoefficient = 1.0f - (logf(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE) / logf(2.0f) + * DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE); + if (distanceCoefficient < 0.0f) { + distanceCoefficient = 0.0f; + } + + gain *= distanceCoefficient; + } + + //qDebug() << "calculated gain as " << gain; + + return gain; +} + qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { auto samplesRequested = maxSize / sizeof(int16_t); int samplesPopped; int bytesWritten; + // limit the number of NETWORK_FRAME_SAMPLES_PER_CHANNEL to return regardless + // of what maxSize requests. + static const qint64 MAX_FRAMES = 256; + static float hrtfBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES]; + static int16_t scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES]; + + // injectors are 24Khz, use this to limit the injector audio only + static AudioLimiter audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + + // final output is 48Khz, so use this to limit network audio plus upsampled injectors + static AudioLimiter finalAudioLimiter(2*AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + + // Injectors are 24khz, but we are running at 48Khz here, so need an AudioSRC to upsample + static AudioSRC audioSRC(AudioConstants::SAMPLE_RATE, 2*AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + + // limit maxSize + if (maxSize > AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * MAX_FRAMES) { + maxSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * MAX_FRAMES; + } + + // initialize stuff + memset(hrtfBuffer, 0, sizeof(hrtfBuffer)); + QVector injectorsToRemove; + qint64 framesReadFromInjectors = 0; + + //qDebug() << "maxSize=" << maxSize << "data ptr: "<< (qint64)data; + + for (AudioInjector* injector : _audio->getActiveLocalAudioInjectors()) { + // pop off maxSize/4 (since it is mono, and 24Khz instead of 48Khz) + // bytes from the injector's localBuffer, using the data ptr as a temporary buffer + // note we are only hrtf-ing mono buffers, so we request half of what maxSize is, since it + // wants that many stereo bytes + if (injector->getLocalBuffer() ) { + + glm::vec3 relativePosition = injector->getPosition() - _audio->_positionGetter(); + + qint64 bytesReadFromInjector = injector->getLocalBuffer()->readData(data, maxSize/4); + qint64 framesReadFromInjector = bytesReadFromInjector/sizeof(int16_t); + if (framesReadFromInjector > framesReadFromInjectors) { + framesReadFromInjectors = framesReadFromInjector; + } + if (framesReadFromInjector > 0) { + + //qDebug() << "AudioClient::AudioOutputIODevice::readData found " << framesReadFromInjector << " frames"; + + // calculate these shortly + float gain = _audio->gainForSource(relativePosition, injector->getVolume()); + float azimuth = _audio->azimuthForSource(relativePosition); + + // now hrtf this guy, one NETWORK_FRAME_SAMPLES_PER_CHANNEL at a time + this->renderHRTF(injector->getLocalHRTF(), (int16_t*)data, hrtfBuffer, azimuth, gain, framesReadFromInjector); + + } else { + + // probably need to be more clever here than just getting rid of the injector? + //qDebug() << "AudioClient::AudioOutputIODevice::readData no more data, adding to list of injectors to remove"; + injectorsToRemove.append(injector); + injector->finish(); + } + } else { + + // probably need to be more clever here then just getting rid of injector? + //qDebug() << "AudioClient::AudioOutputIODevice::readData injector " << injector << " has no local buffer, adding to list of injectors to remove"; + injectorsToRemove.append(injector); + injector->finish(); + } + } + + if (framesReadFromInjectors > 0) { + // resample the 24Khz injector audio to 48Khz + // but first, hit with limiter :( + audioLimiter.render(hrtfBuffer, (int16_t*)data, framesReadFromInjectors); + audioSRC.render((int16_t*)data, scratchBuffer, framesReadFromInjectors); + + // now, lets move this back into the hrtfBuffer + for(int i=0; igetActiveLocalAudioInjectors().removeOne(injector); + + //qDebug() << "removed injector " << injector << " from active injector list!"; + } + + // now grab the stuff from the network, and mix it in... if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples((int16_t*)data, samplesPopped); + + //qDebug() << "AudioClient::AudioOutputIODevice::readData popped " << samplesPopped << "samples" << "to " << (qint64)data; + bytesWritten = samplesPopped * sizeof(int16_t); - } else { + if (framesReadFromInjectors > 0) { + + int16_t* dataPtr = (int16_t*)data; + // convert audio to floats for mixing with limiter... + //qDebug() << "AudioClient::AudioOUtputIODevice::readData converting to floats..."; + for (int i=0; i bytesWritten) { + bytesWritten = 8*framesReadFromInjectors; + } + + //qDebug() << "done, bytes written = " << bytesWritten; + } + } else if (framesReadFromInjectors == 0) { + // if nobody has data, 0 out buffer... memset(data, 0, maxSize); bytesWritten = maxSize; + } else { + // only sound from injectors, multiply by 2 as the htrf is stereo + finalAudioLimiter.render(hrtfBuffer, (int16_t*)data, framesReadFromInjectors*2); + bytesWritten = 8*framesReadFromInjectors; } + int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); if (!bytesAudioOutputUnplayed) { qCDebug(audioclient) << "empty audio buffer"; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index dc46db5657..91987178b0 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -37,6 +37,7 @@ #include #include #include +#include #include "AudioIOStats.h" #include "AudioNoiseGate.h" @@ -86,12 +87,12 @@ public: void stop() { close(); } qint64 readData(char * data, qint64 maxSize); qint64 writeData(const char * data, qint64 maxSize) { return 0; } - int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; + void renderHRTF(AudioHRTF& hrtf, int16_t* data, float* hrtfBuffer, float azimuth, float gain, qint64 numSamples); }; const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } @@ -124,6 +125,8 @@ public: void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; } void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; } + + QVector& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } static const float CALLBACK_ACCELERATOR_RATIO; @@ -291,6 +294,12 @@ private: void checkDevices(); bool _hasReceivedFirstPacket = false; + + QVector _activeLocalAudioInjectors; + + float azimuthForSource(const glm::vec3& relativePosition); + float gainForSource(const glm::vec3& relativePosition, float volume); + }; diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index c90256429d..a4cde77377 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -26,6 +26,7 @@ #include "AudioInjectorLocalBuffer.h" #include "AudioInjectorOptions.h" +#include "AudioHRTF.h" #include "Sound.h" class AbstractAudioInterface; @@ -54,8 +55,11 @@ public: void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } + AudioHRTF& getLocalHRTF() { return _localHRTF; } + bool isLocalOnly() const { return _options.localOnly; } - + float getVolume() const { return _options.volume; } + glm::vec3 getPosition() const { return _options.position; } void setLocalAudioInterface(AbstractAudioInterface* localAudioInterface) { _localAudioInterface = localAudioInterface; } static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface); @@ -74,14 +78,15 @@ public slots: float getLoudness() const { return _loudness; } bool isPlaying() const { return _state == State::NotFinished || _state == State::NotFinishedWithPendingDelete; } + void finish(); signals: void finished(); void restarting(); -private slots: +/*private slots: void finish(); - +*/ private: void setupInjection(); int64_t injectNextFrame(); @@ -103,6 +108,9 @@ private: std::unique_ptr _frameTimer { nullptr }; quint16 _outgoingSequenceNumber { 0 }; + // when the injector is local, we need this + AudioHRTF _localHRTF; + // for local injection, friend class AudioInjectorManager; }; From 5827993125670a20ea5bb4fbce8181f82f8b8de8 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 7 Jul 2016 16:30:10 -0700 Subject: [PATCH 0963/1237] Fixing warnings --- libraries/render-utils/src/DeferredLightingEffect.cpp | 1 - libraries/render-utils/src/SubsurfaceScattering.cpp | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index d7e9b311e3..4161688c7a 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -491,7 +491,6 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, auto eyePoint = viewFrustum.getPosition(); float nearRadius = glm::distance(eyePoint, viewFrustum.getNearTopLeft()); - float nearClip = 1.01f * viewFrustum.getNearClip(); auto deferredLightingEffect = DependencyManager::get(); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 8ec80b11c3..349e6cc0e2 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -137,9 +137,7 @@ void SubsurfaceScattering::configure(const Config& config) { void SubsurfaceScattering::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, Outputs& outputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); - - RenderArgs* args = renderContext->args; - + if (!_scatteringResource->getScatteringTable()) { _scatteringResource->generateScatteringTable(renderContext->args); } @@ -510,7 +508,6 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont auto& frameTransform = inputs.get0(); - auto& lightingModel = inputs.get1(); auto& curvatureFramebuffer = inputs.get2(); auto& diffusedFramebuffer = inputs.get3(); auto& scatteringResource = inputs.get4(); From 71392bc1b24eb632c0549c6d91369e35316c864d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 16:30:30 -0700 Subject: [PATCH 0964/1237] change to sphere now that sphere alpha works. --- scripts/system/controllers/teleport.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index dfc471182d..57806a9b36 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -6,8 +6,22 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +//FEATURES: +// ENTRY MODES +// Thumbpad only +// Thumpad + trigger +// JUMP MODES +// Instant +// Smoth arrival aka stepwise (number of steps, duration of step) + +// FADE MODE +// Cube-overlay (steps,duration) +// Model-overlay (steps, duration) +// Fade out / fade in + +// defaults with instant jump with fade. to try smooth arrival mode, change use fade to false and then switch the number of arrival steps and spacing var inTeleportMode = false; var currentFadeSphereOpacity = 1; @@ -20,7 +34,6 @@ var USE_FADE_OUT = true; var FADE_IN_INTERVAL = 25; var FADE_OUT_INTERVAL = 25; - // instant var NUMBER_OF_STEPS = 0; var SMOOTH_ARRIVAL_SPACING = 0; @@ -33,7 +46,7 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 100; // var NUMBER_OF_STEPS = 4; -// //medium-fast +//medium-fast // var SMOOTH_ARRIVAL_SPACING = 33; // var NUMBER_OF_STEPS = 6; @@ -42,8 +55,6 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var NUMBER_OF_STEPS = 20; -// var NUMBER_OF_STEPS=9; -// var SMOOTH_ARRIVAL_SPACING=25; var USE_THUMB_AND_TRIGGER_MODE = false; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; @@ -166,7 +177,7 @@ function Teleporter() { currentFadeSphereOpacity = 10; - _this.fadeSphere = Overlays.addOverlay("cube", sphereProps); + _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); Script.clearInterval(fadeSphereInterval) Script.update.connect(_this.updateFadeSphere); if (USE_FADE_OUT === true) { @@ -184,7 +195,7 @@ function Teleporter() { if (currentFadeSphereOpacity <= 0) { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); - fadeSphereInterval = null; + fadeSphereInterval = null; print('sphere done fading out'); return; } From 11542aeca2b9b01b6e2d6fb9ec58abd8c41d1dcb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 7 Jul 2016 16:36:25 -0700 Subject: [PATCH 0965/1237] pick against avatar's capsule and then against the T-pose mesh --- interface/src/avatar/Avatar.cpp | 9 ++ interface/src/avatar/Avatar.h | 1 + interface/src/avatar/AvatarManager.cpp | 32 ++++-- libraries/render-utils/src/Model.cpp | 137 ++++--------------------- libraries/render-utils/src/Model.h | 5 - 5 files changed, 52 insertions(+), 132 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 39bb7eac17..4d9481f002 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1085,6 +1085,15 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { shapeInfo.setOffset(uniformScale * _skeletonModel->getBoundingCapsuleOffset()); } +void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { + ShapeInfo shapeInfo; + computeShapeInfo(shapeInfo); + glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight + start = getPosition() - glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); + end = getPosition() + glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); + radius = halfExtents.x; +} + void Avatar::setMotionState(AvatarMotionState* motionState) { _motionState = motionState; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 064f0a9533..b9f44613c7 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -154,6 +154,7 @@ public: virtual void rebuildCollisionShape(); virtual void computeShapeInfo(ShapeInfo& shapeInfo); + void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); AvatarMotionState* getMotionState() { return _motionState; } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index a5a02bdaff..31a77df0cf 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -429,18 +429,34 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& glm::vec3 surfaceNormal; SkeletonModelPointer avatarModel = avatar->getSkeletonModel(); - AABox avatarBounds = avatarModel->getRenderableMeshBound(); - if (!avatarBounds.findRayIntersection(ray.origin, normDirection, distance, face, surfaceNormal)) { - // ray doesn't intersect avatar's bounding-box + + // It's better to intersect the ray against the avatar's actual mesh, but this is currently difficult to + // do, because the transformed mesh data only exists over in GPU-land. As a compromise, this code + // intersects against the avatars capsule and then against the (T-pose) mesh. The end effect is that picking + // against the avatar is sort-of right, but you likely wont be able to pick against the arms. + + // TODO -- find a way to extract transformed avatar mesh data from the rendering engine. + + // if we weren't picking against the capsule, we would want to pick against the avatarBounds... + // AABox avatarBounds = avatarModel->getRenderableMeshBound(); + // if (!avatarBounds.findRayIntersection(ray.origin, normDirection, distance, face, surfaceNormal)) { + // // ray doesn't intersect avatar's bounding-box + // continue; + // } + + glm::vec3 start; + glm::vec3 end; + float radius; + avatar->getCapsule(start, end, radius); + bool intersects = findRayCapsuleIntersection(ray.origin, normDirection, start, end, radius, distance); + if (!intersects) { + // ray doesn't intersect avatar's capsule continue; } - avatarModel->invalidCalculatedMeshBoxes(); - avatarModel->recalculateMeshBoxes(true); - QString extraInfo; - bool intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, - distance, face, surfaceNormal, extraInfo, true); + intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, + distance, face, surfaceNormal, extraInfo, true); if (intersects && (!result.intersects || distance < result.distance)) { result.intersects = true; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 4969585af4..43eced3107 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -442,31 +442,23 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { PROFILE_RANGE(__FUNCTION__); bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; - if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || - (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { + if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { const FBXGeometry& geometry = getFBXGeometry(); int numberOfMeshes = geometry.meshes.size(); _calculatedMeshBoxes.resize(numberOfMeshes); _calculatedMeshTriangles.clear(); _calculatedMeshTriangles.resize(numberOfMeshes); _calculatedMeshPartBoxes.clear(); - - - - int okCount = 0; - int notOkCount = 0; - - - for (int meshIndex = 0; meshIndex < numberOfMeshes; meshIndex++) { - const FBXMesh& mesh = geometry.meshes.at(meshIndex); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& mesh = geometry.meshes.at(i); Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents, _translation, _rotation); - _calculatedMeshBoxes[meshIndex] = AABox(scaledMeshExtents); + _calculatedMeshBoxes[i] = AABox(scaledMeshExtents); if (pickAgainstTriangles) { QVector thisMeshTriangles; - for (int partIndex = 0; partIndex < mesh.parts.size(); partIndex++) { - const FBXMeshPart& part = mesh.parts.at(partIndex); + for (int j = 0; j < mesh.parts.size(); j++) { + const FBXMeshPart& part = mesh.parts.at(j); bool atLeastOnePointInBounds = false; AABox thisPartBounds; @@ -483,63 +475,11 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; - glm::vec3 v[3]; - int ok = 0; - if (meshIndex < _meshStates.size()) { - int quadPointIndexes[ 4 ] = {i0, i1, i2, i3}; - for (int pointInQuadIndex = 0; pointInQuadIndex < 4; pointInQuadIndex++) { - int vertexIndex = quadPointIndexes[pointInQuadIndex]; - glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; - glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; - - bool vSet = false; - for (int ci = 0; ci < 4; ci++) { - int clusterIndex = (int) clusterIndices[ci]; - float clusterWeight = clusterWeights[ci]; - if (clusterIndex < 0 || - clusterIndex >= mesh.clusters.size() || - clusterWeight == 0.0f) { - continue; - } - const FBXCluster& cluster = mesh.clusters.at(clusterIndex); - auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; - glm::vec3 meshVertex = mesh.vertices[vertexIndex]; - glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); - glm::vec3 tpoint = clusterWeight * (_translation + fuh); - v[pointInQuadIndex] += tpoint; - vSet = true; - } - if (vSet) { - ok++; - } - } - } - - - glm::vec3 v0; - glm::vec3 v1; - glm::vec3 v2; - glm::vec3 v3; - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); - if (ok == 4) { - okCount++; - v0 = v[0]; - v1 = v[1]; - v2 = v[2]; - v3 = v[3]; - } else { - notOkCount++; - v0 = calculateScaledOffsetPoint(mv0); - v1 = calculateScaledOffsetPoint(mv1); - v2 = calculateScaledOffsetPoint(mv2); - v3 = calculateScaledOffsetPoint(mv3); - } - // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); @@ -551,6 +491,11 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv2; thisPartBounds += mv3; + glm::vec3 v0 = calculateScaledOffsetPoint(mv0); + glm::vec3 v1 = calculateScaledOffsetPoint(mv1); + glm::vec3 v2 = calculateScaledOffsetPoint(mv2); + glm::vec3 v3 = calculateScaledOffsetPoint(mv3); + // Sam's recommended triangle slices Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; @@ -561,6 +506,7 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisMeshTriangles.push_back(tri1); thisMeshTriangles.push_back(tri2); + } } @@ -572,58 +518,10 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; - glm::vec3 v[3]; - int ok = 0; - if (meshIndex < _meshStates.size()) { - int trianglePointIndexes[ 3 ] = {i0, i1, i2}; - for (int pointInTriIndex = 0; pointInTriIndex < 3; pointInTriIndex++) { - int vertexIndex = trianglePointIndexes[pointInTriIndex]; - glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; - glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; - - bool vSet = false; - for (int ci = 0; ci < 4; ci++) { - int clusterIndex = (int) clusterIndices[ci]; - float clusterWeight = clusterWeights[ci]; - if (clusterIndex < 0 || - clusterIndex >= mesh.clusters.size() || - clusterWeight == 0.0f) { - continue; - } - const FBXCluster& cluster = mesh.clusters.at(clusterIndex); - auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; - glm::vec3 meshVertex = mesh.vertices[vertexIndex]; - glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); - glm::vec3 tpoint = clusterWeight * (_translation + fuh); - v[pointInTriIndex] += tpoint; - vSet = true; - } - if (vSet) { - ok++; - } - } - } - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - glm::vec3 v0; - glm::vec3 v1; - glm::vec3 v2; - - if (ok == 3) { - okCount++; - v0 = v[0]; - v1 = v[1]; - v2 = v[2]; - } else { - notOkCount++; - v0 = calculateScaledOffsetPoint(mv0); - v1 = calculateScaledOffsetPoint(mv1); - v2 = calculateScaledOffsetPoint(mv2); - } - // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); @@ -634,20 +532,21 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv1; thisPartBounds += mv2; + glm::vec3 v0 = calculateScaledOffsetPoint(mv0); + glm::vec3 v1 = calculateScaledOffsetPoint(mv1); + glm::vec3 v2 = calculateScaledOffsetPoint(mv2); + Triangle tri = { v0, v1, v2 }; thisMeshTriangles.push_back(tri); } } - _calculatedMeshPartBoxes[QPair(meshIndex, partIndex)] = thisPartBounds; + _calculatedMeshPartBoxes[QPair(i, j)] = thisPartBounds; } - _calculatedMeshTriangles[meshIndex] = thisMeshTriangles; + _calculatedMeshTriangles[i] = thisMeshTriangles; _calculatedMeshPartBoxesValid = true; } } - - qDebug() << "ok = " << okCount << " not-ok =" << notOkCount; - _calculatedMeshBoxesValid = true; _calculatedMeshTrianglesValid = pickAgainstTriangles; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 66c5eb019e..6a7c9ec560 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -316,14 +316,11 @@ protected: float getLimbLength(int jointIndex) const; /// Allow sub classes to force invalidating the bboxes -public: void invalidCalculatedMeshBoxes() { _calculatedMeshBoxesValid = false; _calculatedMeshPartBoxesValid = false; _calculatedMeshTrianglesValid = false; } -protected: - // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; @@ -360,9 +357,7 @@ protected: bool _calculatedMeshTrianglesValid; QMutex _mutex; -public: void recalculateMeshBoxes(bool pickAgainstTriangles = false); -protected: void segregateMeshGroups(); // used to calculate our list of translucent vs opaque meshes static model::MaterialPointer _collisionHullMaterial; From 7fa1dc7053845bc8e445cfe053a7ed5d38efd6f0 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 16:36:40 -0700 Subject: [PATCH 0966/1237] make thumb and trigger work in any order and on either button released --- scripts/system/controllers/teleport.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 57806a9b36..66eb876e6c 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -55,7 +55,7 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var NUMBER_OF_STEPS = 20; -var USE_THUMB_AND_TRIGGER_MODE = false; +var USE_THUMB_AND_TRIGGER_MODE = true; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -299,24 +299,16 @@ function Teleporter() { if (teleporter.teleportHand === 'left') { teleporter.leftRay(); - if (leftPad.buttonValue === 0) { - _this.exitTeleportMode(); - _this.deleteTargetOverlay(); - return; - } - if (leftTrigger.buttonValue === 0 && inTeleportMode === true) { + + if ((leftPad.buttonValue === 0 || leftTrigger.buttonValue === 0) && inTeleportMode === true) { _this.teleport(); return; } } else { teleporter.rightRay(); - if (rightPad.buttonValue === 0) { - _this.exitTeleportMode(); - _this.deleteTargetOverlay(); - return; - } - if (rightTrigger.buttonValue === 0 && inTeleportMode === true) { + + if ((rightPad.buttonValue === 0 || rightTrigger.buttonValue === 0) && inTeleportMode === true) { _this.teleport(); return; } @@ -694,6 +686,12 @@ function registerMappingsWithThumbAndTrigger() { teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { teleporter.enterTeleportMode('right') }); + teleportMapping.from(leftTrigger.down).when(leftPad.down).to(function(value) { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightTrigger.down).when(rightPad.down).to(function(value) { + teleporter.enterTeleportMode('right') + }); } if (USE_THUMB_AND_TRIGGER_MODE === true) { From 0d454cd45e85d2d188e48d31d1d28f225c2c2e73 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 7 Jul 2016 17:28:23 -0700 Subject: [PATCH 0967/1237] Equip hotspots render when your hand is near them. --- .../system/controllers/handControllerGrab.js | 295 ++++++++---------- 1 file changed, 130 insertions(+), 165 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 869ea98b34..228c4fe483 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -28,7 +28,7 @@ var WANT_DEBUG_SEARCH_NAME = null; var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.75; // Squeezed far enough to complete distant grab +var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; @@ -80,6 +80,7 @@ var EQUIP_RADIUS_EMBIGGEN_FACTOR = 1.1; // var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. +var EQUIP_HOTSPOT_RENDER_RADIUS = 0.3; // radius used for palm vs equip-hotspot for rendering hot-spots var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position @@ -182,9 +183,7 @@ CONTROLLER_STATE_MACHINE[STATE_OFF] = { }; CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { name: "searching", - updateMethod: "search", - enterMethod: "searchEnter", - exitMethod: "searchExit" + updateMethod: "search" }; CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { name: "distance_holding", @@ -389,6 +388,8 @@ function MyController(hand) { this.entityPropertyCache = new EntityPropertiesCache(); + this.equipOverlayInfoSetMap = {}; + var _this = this; var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; @@ -881,126 +882,17 @@ function MyController(hand) { } } + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(this.getHandPosition(), EQUIP_HOTSPOT_RENDER_RADIUS); + var candidateEntities = this.entityPropertyCache.getEntities(); + + var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); if (!this.waitForTriggerRelease) { - // update haptics when hand is near an equip hotspot - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(this.getHandPosition(), NEAR_GRAB_RADIUS); - var candidateEntities = this.entityPropertyCache.getEntities(); - var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); this.updateEquipHaptics(potentialEquipHotspot); } - }; - this.createHotspots = function () { - var _this = this; - - var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; - var HAND_EQUIP_SPHERE_ALPHA = 0.7; - var HAND_EQUIP_SPHERE_RADIUS = 0.01; - - var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; - var HAND_GRAB_SPHERE_ALPHA = 0.3; - var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; - - var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; - var GRAB_BOX_ALPHA = 0.1; - - this.hotspotOverlays = []; - - var overlay; - if (DRAW_HAND_SPHERES) { - // add tiny green sphere around the palm. - var handPosition = this.getHandPosition(); - overlay = Overlays.addOverlay("sphere", { - position: handPosition, - size: HAND_EQUIP_SPHERE_RADIUS * 2, - color: HAND_EQUIP_SPHERE_COLOR, - alpha: HAND_EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - this.hotspotOverlays.push({ - entityID: undefined, - overlay: overlay, - type: "hand", - localPosition: {x: 0, y: 0, z: 0}, - hotspot: {} - }); - - // add larger blue sphere around the palm. - overlay = Overlays.addOverlay("sphere", { - position: handPosition, - size: HAND_GRAB_SPHERE_RADIUS * 2, - color: HAND_GRAB_SPHERE_COLOR, - alpha: HAND_GRAB_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - this.hotspotOverlays.push({ - entityID: undefined, - overlay: overlay, - type: "hand", - localPosition: {x: 0, y: 0, z: 0}, - hotspot: {} - }); - } - - // find entities near the avatar that might be equipable. - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); - - if (DRAW_GRAB_BOXES) { - // add blue box overlays for grabbable entities. - this.entityPropertyCache.getEntities().forEach(function (entityID) { - var props = _this.entityPropertyCache.getProps(entityID); - if (_this.entityIsGrabbable(entityID)) { - var overlay = Overlays.addOverlay("cube", { - rotation: props.rotation, - position: props.position, - size: props.dimensions, - color: GRAB_BOX_COLOR, - alpha: GRAB_BOX_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - _this.hotspotOverlays.push({ - entityID: entityID, - overlay: overlay, - type: "near", - localPosition: {x: 0, y: 0, z: 0}, - hotspot: {} - }); - } - }); - } - - // add green spheres for each equippable hotspot. - flatten(this.entityPropertyCache.getEntities().map(function (entityID) { - return _this.collectEquipHotspots(entityID); - })).filter(function (hotspot) { - return _this.hotspotIsEquippable(hotspot); - }).forEach(function (hotspot) { - var overlay = Overlays.addOverlay("model", { - url: "http://hifi-content.s3.amazonaws.com/alan/dev/equip-ico-2.fbx", - position: hotspot.worldPosition, - rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: (hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR, - ignoreRayIntersection: true - }); - _this.hotspotOverlays.push({ - entityID: hotspot.entityID, - overlay: overlay, - type: "equip", - localPosition: hotspot.localPosition, - hotspot: hotspot - }); - }); + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); + this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); }; this.clearEquipHaptics = function () { @@ -1010,57 +902,109 @@ function MyController(hand) { this.updateEquipHaptics = function (potentialEquipHotspot) { if (potentialEquipHotspot && !this.prevPotentialEquipHotspot || !potentialEquipHotspot && this.prevPotentialEquipHotspot) { - Controller.triggerShortHapticPulse(1.0, this.hand); + Controller.triggerShortHapticPulse(0.5, this.hand); } this.prevPotentialEquipHotspot = potentialEquipHotspot; }; - this.updateHotspots = function (potentialEquipHotspot) { + this.clearEquipHotspotRendering = function () { + var keys = Object.keys(this.equipOverlayInfoSetMap); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]]; + this.deleteOverlayInfoSet(overlayInfoSet); + } + this.equipOverlayInfoSetMap = {}; + }; + + this.createOverlayInfoSet = function (hotspot, timestamp) { + var overlayInfoSet = { + timestamp: timestamp, + entityID: hotspot.entityID, + localPosition: hotspot.localPosition, + hotspot: hotspot, + overlays: [] + }; + + overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + url: "http://hifi-content.s3.amazonaws.com/alan/dev/Equip-Spark.fbx", + position: hotspot.worldPosition, + rotation: {x: 0, y: 0, z: 0, w: 1}, + dimensions: (hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR, + ignoreRayIntersection: true + })); + + // TODO: use shader to achive fresnel effect. + overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + url: "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx", + position: hotspot.worldPosition, + rotation: {x: 0, y: 0, z: 0, w: 1}, + dimensions: (hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR * 0.5, + ignoreRayIntersection: true + })); + + // TODO: add light + + return overlayInfoSet; + }; + + this.updateOverlayInfoSet = function (overlayInfoSet, timestamp, potentialEquipHotspot) { + overlayInfoSet.timestamp = timestamp; + + var radius = (overlayInfoSet.hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR; + + // embiggen the overlays if it maches the potentialEquipHotspot + if (potentialEquipHotspot && overlayInfoSet.entityID == potentialEquipHotspot.entityID && + Vec3.equal(overlayInfoSet.localPosition, potentialEquipHotspot.localPosition)) { + radius = radius * EQUIP_RADIUS_EMBIGGEN_FACTOR; + } + + var props = _this.entityPropertyCache.getProps(overlayInfoSet.entityID); + var entityXform = new Xform(props.rotation, props.position); + var position = entityXform.xformPoint(overlayInfoSet.localPosition); + + // spark + Overlays.editOverlay(overlayInfoSet.overlays[0], { + position: position, + rotation: props.rotation, + dimensions: radius + }); + + // fresnel sphere + Overlays.editOverlay(overlayInfoSet.overlays[1], { + position: position, + rotation: props.rotation, + dimensions: radius * 0.5 // HACK to adjust sphere size... + }); + }; + + this.deleteOverlayInfoSet = function (overlayInfoSet) { + overlayInfoSet.overlays.forEach(function (overlay) { + Overlays.deleteOverlay(overlay); + }); + }; + + this.updateEquipHotspotRendering = function (hotspots, potentialEquipHotspot) { + var now = Date.now(); var _this = this; - var props; - this.hotspotOverlays.forEach(function (overlayInfo) { - if (overlayInfo.type === "hand") { - Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); - } else if (overlayInfo.type === "equip") { - var radius = (overlayInfo.hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR; - - // embiggen the equipHotspot if it maches the potentialEquipHotspot - if (potentialEquipHotspot && overlayInfo.entityID == potentialEquipHotspot.entityID && - Vec3.equal(overlayInfo.localPosition, potentialEquipHotspot.localPosition)) { - radius = (overlayInfo.hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR * - EQUIP_RADIUS_EMBIGGEN_FACTOR; - } - - _this.entityPropertyCache.updateEntity(overlayInfo.entityID); - props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - var entityXform = new Xform(props.rotation, props.position); - Overlays.editOverlay(overlayInfo.overlay, { - position: entityXform.xformPoint(overlayInfo.localPosition), - rotation: props.rotation, - dimensions: radius - }); - } else if (overlayInfo.type === "near") { - _this.entityPropertyCache.updateEntity(overlayInfo.entityID); - props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + hotspots.forEach(function (hotspot) { + var overlayInfoSet = _this.equipOverlayInfoSetMap[hotspot.key]; + if (overlayInfoSet) { + _this.updateOverlayInfoSet(overlayInfoSet, now, potentialEquipHotspot); + } else { + _this.equipOverlayInfoSetMap[hotspot.key] = _this.createOverlayInfoSet(hotspot, now); } }); - }; - this.destroyHotspots = function () { - this.hotspotOverlays.forEach(function (overlayInfo) { - Overlays.deleteOverlay(overlayInfo.overlay); - }); - this.hotspotOverlays = []; - }; - - this.searchEnter = function () { - this.createHotspots(); - }; - - this.searchExit = function () { - this.destroyHotspots(); + // delete sets with old timestamps. + var keys = Object.keys(this.equipOverlayInfoSetMap); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]]; + if (overlayInfoSet.timestamp !== now) { + this.deleteOverlayInfoSet(overlayInfoSet); + delete this.equipOverlayInfoSetMap[keys[i]]; + } + } }; // Performs ray pick test from the hand controller into the world @@ -1133,6 +1077,7 @@ function MyController(hand) { // returns a list of all equip-hotspots assosiated with this entity. // @param {UUID} entityID // @returns {Object[]} array of objects with the following fields. + // * key {string} a string that can be used to uniquely identify this hotspot // * entityID {UUID} // * localPosition {Vec3} position of the hotspot in object space. // * worldPosition {vec3} position of the hotspot in world space. @@ -1150,6 +1095,7 @@ function MyController(hand) { var hotspot = equipHotspotsProps[i]; if (hotspot.position && hotspot.radius && hotspot.joints) { result.push({ + key: entityID.toString() + i.toString(), entityID: entityID, localPosition: hotspot.position, worldPosition: entityXform.xformPoint(hotspot.position), @@ -1162,6 +1108,7 @@ function MyController(hand) { var wearableProps = this.entityPropertyCache.getWearableProps(entityID); if (wearableProps && wearableProps.joints) { result.push({ + key: entityID.toString() + "0", entityID: entityID, localPosition: {x: 0, y: 0, z: 0}, worldPosition: entityXform.pos, @@ -1302,14 +1249,19 @@ function MyController(hand) { return true; }; - this.chooseBestEquipHotspot = function (candidateEntities) { + this.chooseNearEquipHotspots = function (candidateEntities, distance) { var equippableHotspots = flatten(candidateEntities.map(function (entityID) { return _this.collectEquipHotspots(entityID); })).filter(function (hotspot) { return (_this.hotspotIsEquippable(hotspot) && - Vec3.distance(hotspot.worldPosition, _this.getHandPosition()) < hotspot.radius); + Vec3.distance(hotspot.worldPosition, _this.getHandPosition()) < hotspot.radius + distance); }); + return equippableHotspots; + }; + this.chooseBestEquipHotspot = function (candidateEntities) { + var DISTANCE = 0; + var equippableHotspots = this.chooseNearEquipHotspots(candidateEntities, DISTANCE); if (equippableHotspots.length > 0) { // sort by distance equippableHotspots.sort(function (a, b) { @@ -1431,7 +1383,9 @@ function MyController(hand) { } this.updateEquipHaptics(potentialEquipHotspot); - this.updateHotspots(potentialEquipHotspot); + + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); + this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { @@ -1460,6 +1414,7 @@ function MyController(hand) { this.distanceHoldingEnter = function () { + this.clearEquipHotspotRendering(); this.clearEquipHaptics(); // controller pose is in avatar frame @@ -1738,6 +1693,9 @@ function MyController(hand) { this.dropGestureReset(); this.clearEquipHaptics(); + this.clearEquipHotspotRendering(); + + Controller.triggerShortHapticPulse(1.0, this.hand); if (this.entityActivated) { var saveGrabbedID = this.grabbedEntity; @@ -1955,11 +1913,18 @@ function MyController(hand) { }; this.nearTriggerEnter = function () { + + this.clearEquipHotspotRendering(); + this.clearEquipHaptics(); + Controller.triggerShortHapticPulse(1.0, this.hand); this.callEntityMethodOnGrabbed("startNearTrigger"); }; this.farTriggerEnter = function () { + this.clearEquipHotspotRendering(); + this.clearEquipHaptics(); + this.callEntityMethodOnGrabbed("startFarTrigger"); }; From fa62a0a73c2784e8dc50cdef6062db13fa6abba2 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 7 Jul 2016 18:05:36 -0700 Subject: [PATCH 0968/1237] One less warning --- libraries/render/src/render/BlurTask.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index d0fd52d7d4..19c45e6663 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -44,7 +44,6 @@ void BlurParams::setWidthHeight(int width, int height, bool isStereo) { _parametersBuffer.edit().resolutionInfo = glm::vec4((float) width, (float) height, 1.0f / (float) width, 1.0f / (float) height); } - auto stereoInfo = _parametersBuffer.get().stereoInfo; if (isStereo || resChanged) { _parametersBuffer.edit().stereoInfo = glm::vec4((float)width, (float)height, 1.0f / (float)width, 1.0f / (float)height); } From c644c87e6945a4e13da0b8a189935e06fc4b11d3 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 7 Jul 2016 19:01:41 -0700 Subject: [PATCH 0969/1237] Fix glow line vanishing, improve transparency interaction --- libraries/render-utils/src/GeometryCache.cpp | 13 +++++----- libraries/render-utils/src/glowLine.slf | 5 +++- libraries/render-utils/src/glowLine.slg | 27 +++----------------- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index e692a663c9..9dc104405e 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -11,11 +11,12 @@ #include "GeometryCache.h" + #include -#include -#include +#include #include +#include #include #include @@ -1462,11 +1463,11 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const auto GS = gpu::Shader::createGeometry(std::string(glowLine_geom)); auto PS = gpu::Shader::createPixel(std::string(glowLine_frag)); auto program = gpu::Shader::createProgram(VS, GS, PS); - state->setCullMode(gpu::State::CULL_BACK); + state->setCullMode(gpu::State::CULL_NONE); state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setDepthBias(1.0f); - state->setDepthBiasSlopeScale(1.0f); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); gpu::Shader::makeProgram(*program, slotBindings); diff --git a/libraries/render-utils/src/glowLine.slf b/libraries/render-utils/src/glowLine.slf index 93a12fe5ca..edebc99c81 100644 --- a/libraries/render-utils/src/glowLine.slf +++ b/libraries/render-utils/src/glowLine.slf @@ -24,9 +24,12 @@ void main(void) { d.x = 0.0; } float alpha = 1.0 - length(d); - if (alpha < 0.01) { + if (alpha <= 0.0) { discard; } alpha = pow(alpha, 10.0); + if (alpha < 0.05) { + discard; + } _fragColor = vec4(inColor.rgb, alpha); } diff --git a/libraries/render-utils/src/glowLine.slg b/libraries/render-utils/src/glowLine.slg index c577112abb..429cb8ee37 100644 --- a/libraries/render-utils/src/glowLine.slg +++ b/libraries/render-utils/src/glowLine.slg @@ -10,6 +10,9 @@ // #extension GL_EXT_geometry_shader4 : enable +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + layout(location = 0) in vec4 inColor[]; layout(location = 0) out vec4 outColor; @@ -18,30 +21,6 @@ layout(location = 1) out vec3 outLineDistance; layout(lines) in; layout(triangle_strip, max_vertices = 24) out; -struct TransformCamera { - mat4 _view; - mat4 _viewInverse; - mat4 _projectionViewUntranslated; - mat4 _projection; - mat4 _projectionInverse; - vec4 _viewport; -}; - -layout(std140) uniform transformCameraBuffer { - TransformCamera _camera; -}; - -TransformCamera getTransformCamera() { - return _camera; -} - -vec3 getEyeWorldPos() { - return _camera._viewInverse[3].xyz; -} - - - - vec3 ndcToEyeSpace(in vec4 v) { TransformCamera cam = getTransformCamera(); vec4 u = cam._projectionInverse * v; From f8ff0da9013515e8a1623e3ae09f4fbdae407ca9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 19:22:42 -0700 Subject: [PATCH 0970/1237] trying to fix interaction with grab script... --- .../utilities/tools/overlayFinder.js | 17 ++++++++++ .../system/controllers/handControllerGrab.js | 32 ++++++++++++++++--- scripts/system/controllers/teleport.js | 14 +++++--- 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 scripts/developer/utilities/tools/overlayFinder.js diff --git a/scripts/developer/utilities/tools/overlayFinder.js b/scripts/developer/utilities/tools/overlayFinder.js new file mode 100644 index 0000000000..81ee687cbe --- /dev/null +++ b/scripts/developer/utilities/tools/overlayFinder.js @@ -0,0 +1,17 @@ +function mousePressEvent(event) { + var overlay = Overlays.getOverlayAtPoint({x:event.x,y:event.y}); + print('overlay is: ' + overlay) + // var pickRay = Camera.computePickRay(event.x, event.y); + + // var intersection = Overlays.findRayIntersection(pickRay); + // print('intersection is: ' + intersection) +}; + +Controller.mouseMoveEvent.connect(function(event){ + print('mouse press') + mousePressEvent(event) +}); + +Script.scriptEnding.connect(function(){ + Controller.mousePressEvent.disconnect(mousePressEvent); +}) \ No newline at end of file diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ecece8c4f7..2c73a7a109 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -367,7 +367,6 @@ function MyController(hand) { // for lights this.spotlight = null; this.pointlight = null; - this.overlayLine = null; this.searchSphere = null; this.waitForTriggerRelease = false; @@ -400,6 +399,7 @@ function MyController(hand) { this.updateSmoothedTrigger(); if (this.ignoreInput()) { + // print('in ignore input turn off') this.turnOffVisualizations(); return; } @@ -531,6 +531,7 @@ function MyController(hand) { }; this.overlayLineOn = function(closePoint, farPoint, color) { + if (this.overlayLine === null) { var lineProperties = { lineWidth: 5, @@ -543,6 +544,7 @@ function MyController(hand) { alpha: 1 }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); + print('CREATED OVERLAY IT IS ' + this.overlayLine ) } else { Overlays.editOverlay(this.overlayLine, { @@ -555,7 +557,9 @@ function MyController(hand) { drawInFront: true, // Even when burried inside of something, show it. alpha: 1 }); + print('edited overlay line ' + this.overlayLine ) } + }; this.searchIndicatorOn = function(distantPickRay) { @@ -758,14 +762,20 @@ function MyController(hand) { }; this.overlayLineOff = function() { + return; if (this.overlayLine !== null) { - Overlays.deleteOverlay(this.overlayLine); + Overlays.deleteOverlay(this.overlayLine); + print('REMOVING OVERLAY LINE' + this.overlayLine) + this.overlayLine = null; } - this.overlayLine = null; + + // print('overlay shoudl be null and is line is ' + this.overlayLine) + }; this.searchSphereOff = function() { if (this.searchSphere !== null) { + print('removing search sphere' + this.searchSphere) Overlays.deleteOverlay(this.searchSphere); this.searchSphere = null; this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE; @@ -792,21 +802,28 @@ function MyController(hand) { } }; - this.turnOffVisualizations = function() { + this.turnOffVisualizations = function(hand) { + // print('TURN OFF VISUALIZATIONS: ' + hand) if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); + // print('after line off') } if (USE_OVERLAY_LINES_FOR_SEARCHING === true || USE_OVERLAY_LINES_FOR_MOVING === true) { this.overlayLineOff(); + // print('after overlay line off') } if (USE_PARTICLE_BEAM_FOR_MOVING === true) { this.particleBeamOff(); + // print('after particle beam off') + } this.searchSphereOff(); restore2DMode(); + // print('after all turn off calls') + }; this.triggerPress = function(value) { @@ -2232,11 +2249,18 @@ var handleHandMessages = function(channel, message, sender) { if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { + leftController.turnOffVisualizations('left'); handToDisable = LEFT_HAND; } if (message === 'right') { + rightController.turnOffVisualizations('right'); handToDisable = RIGHT_HAND; } + if (message === "both") { + print('disable both') + leftController.turnOffVisualizations('left'); + rightController.turnOffVisualizations('right'); + } if (message === 'both' || message === 'none') { handToDisable = message; } diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 66eb876e6c..25721aaa55 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -102,6 +102,7 @@ function Teleporter() { this.updateConnected = null; this.smoothArrivalInterval = null; this.fadeSphere = null; + this.teleportHand = null; this.initialize = function() { this.createMappings(); @@ -265,7 +266,7 @@ function Teleporter() { } else { Script.update.disconnect(this.update); } - + this.teleportHand = null; this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); @@ -301,6 +302,7 @@ function Teleporter() { teleporter.leftRay(); if ((leftPad.buttonValue === 0 || leftTrigger.buttonValue === 0) && inTeleportMode === true) { + print('TELEPORTING LEFT') _this.teleport(); return; } @@ -309,6 +311,8 @@ function Teleporter() { teleporter.rightRay(); if ((rightPad.buttonValue === 0 || rightTrigger.buttonValue === 0) && inTeleportMode === true) { + + print('TELEPORTING RIGHT') _this.teleport(); return; } @@ -429,6 +433,7 @@ function Teleporter() { visible: true, alpha: 1, solid: true, + drawInFront: true, glow: 1.0 }; @@ -457,7 +462,8 @@ function Teleporter() { visible: true, alpha: 1, solid: true, - glow: 1.0 + glow: 1.0, + drawInFront: true }; this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); @@ -506,7 +512,7 @@ function Teleporter() { }; this.disableGrab = function() { - Messages.sendLocalMessage('Hifi-Hand-Disabler', 'both'); + Messages.sendLocalMessage('Hifi-Hand-Disabler', this.teleportHand); }; this.enableGrab = function() { @@ -514,7 +520,7 @@ function Teleporter() { }; this.triggerHaptics = function() { - var hand = this.hand === 'left' ? 0 : 1; + var hand = this.teleportHand === 'left' ? 0 : 1; var haptic = Controller.triggerShortHapticPulse(0.2, hand); }; From 1a463665579c5a438c15059e8a3586468050f3e8 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 19:23:04 -0700 Subject: [PATCH 0971/1237] uncomment --- scripts/system/controllers/handControllerGrab.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2c73a7a109..6e7f5c98fa 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -762,7 +762,6 @@ function MyController(hand) { }; this.overlayLineOff = function() { - return; if (this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); print('REMOVING OVERLAY LINE' + this.overlayLine) From fff2a9a5a242df3460a2fe7c177f7480476d7c0b Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 7 Jul 2016 21:16:28 -0700 Subject: [PATCH 0972/1237] REfactoring the shading functions interface to account for scattering --- .../render-utils/src/DeferredBufferRead.slh | 9 +++++- .../render-utils/src/DeferredGlobalLight.slh | 24 +++++---------- .../render-utils/src/LightDirectional.slh | 10 ++++--- libraries/render-utils/src/LightPoint.slh | 2 +- libraries/render-utils/src/LightSpot.slh | 2 +- .../src/directional_ambient_light.slf | 27 ++++++----------- .../src/directional_skybox_light.slf | 29 +++++-------------- 7 files changed, 41 insertions(+), 62 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index 7d6aead855..12bc00855c 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -135,7 +135,14 @@ vec4 fetchDiffusedCurvature(vec2 texcoord) { return texture(diffusedCurvatureMap, texcoord); } - +void unpackMidLowNormalCurvature(vec2 texcoord, out vec4 midNormalCurvature, out vec4 vec4 lowNormalCurvature) { + midNormalCurvature = fetchCurvature(_texCoord0); + lowNormalCurvature = fetchDiffusedCurvature(_texCoord0); + midNormalCurvature.xyz = normalize((midNormalCurvature.xyz - 0.5f) * 2.0f); + lowNormalCurvature.xyz = normalize((lowNormalCurvature.xyz - 0.5f) * 2.0f); + midNormalCurvature.w = (midNormalCurvature.w * 2 - 1); + lowNormalCurvature.w = (lowNormalCurvature.w * 2 - 1); +} <@endfunc@> <@endif@> diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 8247554920..93c0557577 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -60,7 +60,8 @@ vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obsc <$declareLightingAmbient(1, 0, 0, supportScattering)$> <$declareLightingDirectional(supportScattering)$> -vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness) { +vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, + vec3 albedo, float metallic, vec3 emissive, float roughness) { <$prepareGlobalLight()$> @@ -86,22 +87,18 @@ vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, floa <$declareDeferredCurvature()$> -vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { +vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, + vec3 albedo, float metallic, vec3 emissive, float roughness, float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature) { <$prepareGlobalLight(1)$> - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float highCurvature = unpackCurvature(blurredCurvature.w); - float lowCurvature = unpackCurvature(diffusedCurvature.w); - // Ambient vec3 ambientDiffuse; vec3 ambientSpecular; evalLightingAmbientScattering(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance, - isScatteringEnabled() * scattering, lowNormal, highCurvature, lowCurvature); + isScatteringEnabled() * scattering, midNormalCurvature, lowNormalCurvature); color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); @@ -112,7 +109,7 @@ vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenua evalLightingDirectionalScattering(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation, - isScatteringEnabled() * scattering, midNormal, lowNormal, lowCurvature); + isScatteringEnabled() * scattering, midNormalCurvature, lowNormalCurvature); color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); @@ -157,12 +154,7 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { <$prepareGlobalLight(1)$> - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float highCurvature = unpackCurvature(blurredCurvature.w); - float lowCurvature = unpackCurvature(diffusedCurvature.w); - - // Ambient + // Ambient vec3 ambientDiffuse; vec3 ambientSpecular; evalLightingAmbientScattering(ambientDiffuse, ambientSpecular, light, @@ -178,7 +170,7 @@ vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, f evalLightingDirectionalScattering(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation, - isScatteringEnabled() * scattering, midNormal, lowNormal, lowCurvature); + isScatteringEnabled() * scattering, , midNormalCurvature, lowNormalCurvature); color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); diff --git a/libraries/render-utils/src/LightDirectional.slh b/libraries/render-utils/src/LightDirectional.slh index b7dd1f3c61..13a97a115f 100644 --- a/libraries/render-utils/src/LightDirectional.slh +++ b/libraries/render-utils/src/LightDirectional.slh @@ -18,9 +18,11 @@ void evalLightingDirectional(out vec3 diffuse, out vec3 specular, Light light, vec4 shading = evalFragShading(normal, -getLightDirection(light), eyeDir, metallic, fresnel, roughness); - diffuse = albedo * shading.w * shadow * getLightColor(light) * getLightIntensity(light); + vec3 lightEnergy = shadow * getLightColor(light) * getLightIntensity(light); - specular = shading.rgb * shadow * getLightColor(light) * getLightIntensity(light); + diffuse = albedo * shading.w * lightEnergy; + + specular = shading.rgb * lightEnergy; } <@if supportScattering@> @@ -28,14 +30,14 @@ void evalLightingDirectional(out vec3 diffuse, out vec3 specular, Light light, void evalLightingDirectionalScattering(out vec3 diffuse, out vec3 specular, Light light, vec3 eyeDir, vec3 normal, float roughness, float metallic, vec3 fresnel, vec3 albedo, float shadow, - float scattering, vec3 midNormal, vec3 lowNormal, float curvature) { + float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature) { vec3 fragLightDir = -normalize(getLightDirection(light)); evalFragShading(diffuse, specular, normal, fragLightDir, eyeDir, metallic, fresnel, roughness, - scattering, vec4(midNormal, curvature), vec4(lowNormal, curvature)); + scattering, midNormalCurvature, lowNormalCurvature); vec3 lightEnergy = shadow * getLightColor(light) * getLightIntensity(light); diff --git a/libraries/render-utils/src/LightPoint.slh b/libraries/render-utils/src/LightPoint.slh index f5d23bba1a..dc32149dc9 100644 --- a/libraries/render-utils/src/LightPoint.slh +++ b/libraries/render-utils/src/LightPoint.slh @@ -64,7 +64,7 @@ void evalLightingPointScattering(out vec3 diffuse, out vec3 specular, Light ligh evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness, - scattering, vec4(midNormal, curvature), vec4(lowNormal, curvature)); + scattering, midNormalCurvature, lowNormalCurvature); diffuse *= albedo * lightEnergy; diff --git a/libraries/render-utils/src/LightSpot.slh b/libraries/render-utils/src/LightSpot.slh index 0a03789dce..ece9280267 100644 --- a/libraries/render-utils/src/LightSpot.slh +++ b/libraries/render-utils/src/LightSpot.slh @@ -64,7 +64,7 @@ void evalLightingSpotScattering(out vec3 diffuse, out vec3 specular, Light light evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness, - scattering, vec4(midNormal, curvature), vec4(lowNormal, curvature)); + scattering, midNormalCurvature, lowNormalCurvature); diffuse *= albedo * lightEnergy; diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index e8ccffa030..b9847149b9 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -39,10 +39,12 @@ void main(void) { frag.diffuse, frag.specularVal.xyz); _fragColor = vec4(color, 1.0); - } else { //if (frag.mode == FRAG_MODE_SCATTERING) { - - vec4 blurredCurvature = fetchCurvature(_texCoord0); - vec4 diffusedCurvature = fetchDiffusedCurvature(_texCoord0); + } else { + vec4 midNormalCurvature; + vec4 lowNormalCurvature; + if (frag.mode == FRAG_MODE_SCATTERING) { + unpackMidLowNormalCurvature(_texcoord0, midNormalCurvature, lowNormalCurvature); + } vec3 color = evalAmbientSphereGlobalColorScattering( getViewInverse(), @@ -55,20 +57,9 @@ void main(void) { frag.emissive, frag.roughness, frag.scattering, - blurredCurvature, - diffusedCurvature); + midNormalCurvature, + lowNormalCurvature); _fragColor = vec4(color, 1.0); - /* } else { - vec3 color = evalAmbientSphereGlobalColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.position.xyz, - frag.normal, - frag.diffuse, - frag.metallic, - frag.emissive, - frag.roughness); - _fragColor = vec4(color, frag.normalVal.a);*/ + } } diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index 851bc026e3..ffafc56eb2 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -39,11 +39,12 @@ void main(void) { frag.diffuse, frag.specularVal.xyz); _fragColor = vec4(color, 1.0); - } else {// if (frag.mode == FRAG_MODE_SCATTERING) { - - vec4 blurredCurvature = fetchCurvature(_texCoord0); - vec4 diffusedCurvature = fetchDiffusedCurvature(_texCoord0); - + } else { + vec4 midNormalCurvature; + vec4 lowNormalCurvature; + if (frag.mode == FRAG_MODE_SCATTERING) { + unpackMidLowNormalCurvature(_texcoord0, midNormalCurvature, lowNormalCurvature); + } vec3 color = evalSkyboxGlobalColorScattering( getViewInverse(), shadowAttenuation, @@ -55,22 +56,8 @@ void main(void) { frag.emissive, frag.roughness, frag.scattering, - blurredCurvature, - diffusedCurvature); + midNormalCurvature, + lowNormalCurvature); _fragColor = vec4(color, 1.0); - /* } else { - vec3 color = evalSkyboxGlobalColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.position.xyz, - frag.normal, - frag.diffuse, - frag.metallic, - frag.emissive, - frag.roughness); - - _fragColor = vec4(color, frag.normalVal.a); - */ } } From 3c6447326e1e8678d0a05dfc99cc9b858bab6c1a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 7 Jul 2016 21:24:24 -0700 Subject: [PATCH 0973/1237] more codec plumbing --- assignment-client/src/audio/AudioMixer.cpp | 36 ++++++++++++++++--- .../src/audio/AudioMixerClientData.cpp | 7 +++- .../src/audio/AudioMixerClientData.h | 7 ++++ libraries/audio-client/src/AudioClient.cpp | 2 +- libraries/audio/CMakeLists.txt | 2 +- libraries/audio/src/InboundAudioStream.cpp | 11 +++++- libraries/audio/src/InboundAudioStream.h | 6 ++++ plugins/pcmCodec/src/PCMCodecManager.cpp | 8 +++-- 8 files changed, 68 insertions(+), 11 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 27b0a092d0..9d9194aef6 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -477,21 +477,39 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess } qDebug() << "all requested codecs:" << codecList; + CodecPluginPointer selectedCoded; + QString selectedCodecName; auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); if (codecPlugins.size() > 0) { for (auto& plugin : codecPlugins) { qDebug() << "Codec available:" << plugin->getName(); + + // choose first codec + if (!selectedCoded) { + selectedCoded = plugin; + selectedCodecName = plugin->getName(); + } } } else { qDebug() << "No Codecs available..."; } + auto clientData = dynamic_cast(sendingNode->getLinkedData()); + + clientData->_codec = selectedCoded; + clientData->_selectedCodecName = selectedCodecName; + qDebug() << "selectedCodecName:" << selectedCodecName; + + auto avatarAudioStream = clientData->getAvatarAudioStream(); + if (avatarAudioStream) { + avatarAudioStream->_codec = selectedCoded; + avatarAudioStream->_selectedCodecName = selectedCodecName; + } + auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); // write them to our packet - QString selectedCodec = codecList.front(); - qDebug() << "selectedCodec:" << selectedCodec; - replyPacket->writeString(selectedCodec); + replyPacket->writeString(selectedCodecName); auto nodeList = DependencyManager::get(); nodeList->sendPacket(std::move(replyPacket), *sendingNode); @@ -720,9 +738,17 @@ void AudioMixer::broadcastMixes() { quint16 sequence = nodeData->getOutgoingSequenceNumber(); mixPacket->writePrimitive(sequence); + // TODO - codec encode goes here + QByteArray decocedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); + QByteArray encodedBuffer; + if (nodeData->_codec) { + nodeData->_codec->encode(decocedBuffer, encodedBuffer); + } else { + encodedBuffer = decocedBuffer; + } + // pack mixed audio samples - mixPacket->write(reinterpret_cast(_clampedSamples), - AudioConstants::NETWORK_FRAME_BYTES_STEREO); + mixPacket->write(encodedBuffer.constData(), encodedBuffer.size()); } else { int silentPacketBytes = sizeof(quint16) + sizeof(quint16); mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 20003ba10d..080a833c0e 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -101,9 +101,14 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { bool isStereo = channelFlag == 1; + auto avatarAudioStream = new AvatarAudioStream(isStereo, AudioMixer::getStreamSettings()); + avatarAudioStream->_codec = _codec; + avatarAudioStream->_selectedCodecName = _selectedCodecName; + qDebug() << "creating new AvatarAudioStream... codec:" << avatarAudioStream->_selectedCodecName; + auto emplaced = _audioStreams.emplace( QUuid(), - std::unique_ptr { new AvatarAudioStream(isStereo, AudioMixer::getStreamSettings()) } + std::unique_ptr { avatarAudioStream } ); micStreamIt = emplaced.first; diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 17274a1519..0b3b352e66 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -19,6 +19,8 @@ #include #include +#include + #include "PositionalAudioStream.h" #include "AvatarAudioStream.h" @@ -65,6 +67,11 @@ public: AudioLimiter audioLimiter; + // FIXME -- maybe make these private + CodecPluginPointer _codec; + QString _selectedCodecName; + + signals: void injectorStreamFinished(const QUuid& streamIdentifier); diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 147030c831..9dc096eefd 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -837,7 +837,7 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { audioTransform.setTranslation(_positionGetter()); audioTransform.setRotation(_orientationGetter()); - // TODO - codec decode goes here + // TODO - codec encode goes here QByteArray encodedBuffer; if (_codec) { _codec->encode(audio, encodedBuffer); diff --git a/libraries/audio/CMakeLists.txt b/libraries/audio/CMakeLists.txt index c49c9547a5..1e9360b9a2 100644 --- a/libraries/audio/CMakeLists.txt +++ b/libraries/audio/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME audio) setup_hifi_library(Network) -link_hifi_libraries(networking shared) +link_hifi_libraries(networking shared plugins) diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 8b32ada296..c19eb0c161 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -178,7 +178,16 @@ int InboundAudioStream::parseStreamProperties(PacketType type, const QByteArray& } int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) { - return _ringBuffer.writeData(packetAfterStreamProperties.data(), numAudioSamples * sizeof(int16_t)); + + // codec decode goes here + QByteArray decodedBuffer; + if (_codec) { + _codec->decode(packetAfterStreamProperties, decodedBuffer); + } else { + decodedBuffer = packetAfterStreamProperties; + } + + return _ringBuffer.writeData(decodedBuffer.data(), numAudioSamples * sizeof(int16_t)); // FIXME? } int InboundAudioStream::writeDroppableSilentSamples(int silentSamples) { diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index 3f641f1ba4..ddc0dc1dc3 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -18,6 +18,8 @@ #include #include +#include + #include "AudioRingBuffer.h" #include "MovingMinMaxAvg.h" #include "SequenceNumberStats.h" @@ -174,6 +176,10 @@ public: void setReverb(float reverbTime, float wetLevel); void clearReverb() { _hasReverb = false; } + // FIXME -- maybe make these private + CodecPluginPointer _codec; + QString _selectedCodecName; + public slots: /// This function should be called every second for all the stats to function properly. If dynamic jitter buffers /// is enabled, those stats are used to calculate _desiredJitterBufferFrames. diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index eb2f4761b4..d204fb1100 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -39,13 +39,17 @@ bool PCMCodecManager::isSupported() const { void PCMCodecManager::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { - qDebug() << __FUNCTION__ << "encodedBuffer:" << encodedBuffer.size(); // this codec doesn't actually do anything.... decodedBuffer = encodedBuffer; + + //decodedBuffer = qUncompress(encodedBuffer); + //qDebug() << __FUNCTION__ << "from:" << encodedBuffer.size() << " to:" << decodedBuffer.size(); } void PCMCodecManager::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { - qDebug() << __FUNCTION__ << "decodedBuffer:" << decodedBuffer.size(); // this codec doesn't actually do anything.... encodedBuffer = decodedBuffer; + + //encodedBuffer = qCompress(decodedBuffer); + //qDebug() << __FUNCTION__ << "from:" << decodedBuffer.size() << " to:" << encodedBuffer.size(); } From 37f75961833b3f99e63193e9b27e01e83f1c4970 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 8 Jul 2016 09:04:10 -0700 Subject: [PATCH 0974/1237] Trying to unify the shaders with/without scattering eve more but found a scribe bug. REnder emissive and lightmap during forward pass --- libraries/gpu-gl/src/gpu/gl/GLShared.cpp | 9 +- .../render-utils/src/DeferredBufferRead.slh | 40 +++--- .../render-utils/src/DeferredBufferWrite.slh | 11 +- .../render-utils/src/DeferredGlobalLight.slh | 121 +++++++----------- .../src/DeferredLightingEffect.cpp | 8 +- .../render-utils/src/FramebufferCache.cpp | 1 + libraries/render-utils/src/LightAmbient.slh | 56 ++++---- .../render-utils/src/LightDirectional.slh | 36 ++---- libraries/render-utils/src/LightPoint.slh | 51 ++------ libraries/render-utils/src/LightSpot.slh | 55 ++------ libraries/render-utils/src/LightingModel.slh | 13 +- .../src/directional_ambient_light.slf | 27 ++-- .../src/directional_ambient_light_shadow.slf | 37 ++++-- .../render-utils/src/directional_light.slf | 12 +- .../src/directional_light_shadow.slf | 24 ++-- .../src/directional_skybox_light.slf | 28 ++-- .../src/directional_skybox_light_shadow.slf | 37 ++++-- libraries/render-utils/src/overlay3D.slf | 19 ++- .../src/overlay3D_translucent.slf | 18 ++- libraries/render-utils/src/point_light.slf | 22 +--- libraries/render-utils/src/spot_light.slf | 21 +-- 21 files changed, 291 insertions(+), 355 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index d4ecfe0b55..db64fb08d0 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -11,6 +11,7 @@ #include #include +#include Q_LOGGING_CATEGORY(gpugllogging, "hifi.gpu.gl") @@ -671,14 +672,14 @@ bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const s // if compilation fails if (!compiled) { // save the source code to a temp file so we can debug easily - /* std::ofstream filestream; + std::ofstream filestream; filestream.open("debugshader.glsl"); if (filestream.is_open()) { - filestream << shaderSource->source; + filestream << srcstr[0]; + filestream << srcstr[1]; filestream.close(); } - */ - + GLint infoLength = 0; glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength); diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index 12bc00855c..6c020b515f 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -33,9 +33,6 @@ uniform sampler2D lightingMap; struct DeferredFragment { - vec4 normalVal; - vec4 diffuseVal; - vec4 specularVal; vec4 position; vec3 normal; float metallic; @@ -43,44 +40,47 @@ struct DeferredFragment { float obscurance; vec3 specular; float roughness; - vec3 emissive; +// vec3 emissive; int mode; float scattering; float depthVal; }; DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { - + vec4 normalVal; + vec4 diffuseVal; + vec4 specularVal; + DeferredFragment frag; frag.depthVal = -1; - frag.normalVal = texture(normalMap, texcoord); - frag.diffuseVal = texture(albedoMap, texcoord); - frag.specularVal = texture(specularMap, texcoord); + normalVal = texture(normalMap, texcoord); + diffuseVal = texture(albedoMap, texcoord); + specularVal = texture(specularMap, texcoord); frag.obscurance = texture(obscuranceMap, texcoord).x; // Unpack the normal from the map - frag.normal = unpackNormal(frag.normalVal.xyz); - frag.roughness = frag.normalVal.a; + frag.normal = unpackNormal(normalVal.xyz); + frag.roughness = normalVal.a; // Diffuse color and unpack the mode and the metallicness - frag.diffuse = frag.diffuseVal.xyz; + frag.diffuse = diffuseVal.xyz; frag.scattering = 0.0; - unpackModeMetallic(frag.diffuseVal.w, frag.mode, frag.metallic); + unpackModeMetallic(diffuseVal.w, frag.mode, frag.metallic); - frag.emissive = frag.specularVal.xyz; - frag.obscurance = min(frag.specularVal.w, frag.obscurance); + //frag.emissive = specularVal.xyz; + frag.obscurance = min(specularVal.w, frag.obscurance); if (frag.mode == FRAG_MODE_SCATTERING) { - frag.scattering = frag.emissive.x; - frag.emissive = vec3(0.0); + frag.scattering = specularVal.x; + //frag.emissive = vec3(0.0); } if (frag.metallic <= 0.5) { frag.metallic = 0.0; frag.specular = vec3(0.03); // Default Di-electric fresnel value } else { - frag.specular = vec3(frag.diffuseVal.xyz); + frag.specular = vec3(diffuseVal.xyz); frag.metallic = 1.0; } @@ -135,9 +135,9 @@ vec4 fetchDiffusedCurvature(vec2 texcoord) { return texture(diffusedCurvatureMap, texcoord); } -void unpackMidLowNormalCurvature(vec2 texcoord, out vec4 midNormalCurvature, out vec4 vec4 lowNormalCurvature) { - midNormalCurvature = fetchCurvature(_texCoord0); - lowNormalCurvature = fetchDiffusedCurvature(_texCoord0); +void unpackMidLowNormalCurvature(vec2 texcoord, out vec4 midNormalCurvature, out vec4 lowNormalCurvature) { + midNormalCurvature = fetchCurvature(texcoord); + lowNormalCurvature = fetchDiffusedCurvature(texcoord); midNormalCurvature.xyz = normalize((midNormalCurvature.xyz - 0.5f) * 2.0f); lowNormalCurvature.xyz = normalize((lowNormalCurvature.xyz - 0.5f) * 2.0f); midNormalCurvature.w = (midNormalCurvature.w * 2 - 1); diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index ee90f26346..f1f518eee6 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -17,6 +17,7 @@ layout(location = 0) out vec4 _fragColor0; layout(location = 1) out vec4 _fragColor1; layout(location = 2) out vec4 _fragColor2; +layout(location = 3) out vec4 _fragColor3; // the alpha threshold const float alphaThreshold = 0.5; @@ -40,16 +41,20 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness _fragColor0 = vec4(albedo, ((scattering > 0.0) ? packScatteringMetallic(metallic) : packShadedMetallic(metallic))); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(((scattering > 0.0) ? vec3(scattering) : emissive), occlusion); + + _fragColor3 = vec4(emissive, 1.0); } -void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 fresnel, vec3 emissive) { +void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 fresnel, vec3 lightmap) { if (alpha != 1.0) { discard; } _fragColor0 = vec4(albedo, packLightmappedMetallic(metallic)); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); - _fragColor2 = vec4(emissive, 1.0); + _fragColor2 = vec4(lightmap, 1.0); + + _fragColor3 = vec4(albedo * lightmap, 1.0); } void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { @@ -59,6 +64,8 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { _fragColor0 = vec4(color, packUnlit()); _fragColor1 = vec4(packNormal(normal), 1.0); //_fragColor2 = vec4(vec3(0.0), 1.0); // If unlit, do not worry about the emissive color target + + _fragColor3 = vec4(color, 1.0); } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, vec3 fresnel, float roughness) { diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 93c0557577..c8bdff0af6 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -12,7 +12,6 @@ <@def DEFERRED_GLOBAL_LIGHT_SLH@> <@include model/Light.slh@> -!> <@include LightingModel.slh@> <$declareLightingModel()$> @@ -35,9 +34,9 @@ <@if isScattering@> <@else@> - color += emissive * isEmissiveEnabled(); + // color += emissive * isEmissiveEnabled(); <@endif@> - +//color += emissive * isEmissiveEnabled(); vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value if (metallic > 0.5) { fresnel = albedo; @@ -48,7 +47,7 @@ <@func declareEvalAmbientGlobalColor()@> -vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness) { +vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, float roughness) { <$prepareGlobalLight()$> color += albedo * getLightColor(light) * obscurance * getLightAmbientIntensity(light); return color; @@ -60,45 +59,27 @@ vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obsc <$declareLightingAmbient(1, 0, 0, supportScattering)$> <$declareLightingDirectional(supportScattering)$> -vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, - vec3 albedo, float metallic, vec3 emissive, float roughness) { - - <$prepareGlobalLight()$> - - // Ambient - vec3 ambientDiffuse; - vec3 ambientSpecular; - evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); - color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); - color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); - - - // Directional - vec3 directionalDiffuse; - vec3 directionalSpecular; - evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); - color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); - color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); - - return color; -} - <@if supportScattering@> - <$declareDeferredCurvature()$> +<@endif@> -vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, - vec3 albedo, float metallic, vec3 emissive, float roughness, float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature) { +vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, + vec3 albedo, float metallic, float roughness +<@if supportScattering@> + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> + ) { - <$prepareGlobalLight(1)$> + <$prepareGlobalLight(supportScattering)$> // Ambient vec3 ambientDiffuse; vec3 ambientSpecular; - evalLightingAmbientScattering(ambientDiffuse, ambientSpecular, light, - fragEyeDir, fragNormal, roughness, - metallic, fresnel, albedo, obscurance, - isScatteringEnabled() * scattering, midNormalCurvature, lowNormalCurvature); + evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); @@ -106,18 +87,17 @@ vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenua // Directional vec3 directionalDiffuse; vec3 directionalSpecular; - evalLightingDirectionalScattering(directionalDiffuse, directionalSpecular, light, - fragEyeDir, fragNormal, roughness, - metallic, fresnel, albedo, shadowAttenuation, - isScatteringEnabled() * scattering, midNormalCurvature, lowNormalCurvature); + evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); return color; } -<@endif@> - <@endfunc@> @@ -126,13 +106,26 @@ vec3 evalAmbientSphereGlobalColorScattering(mat4 invViewMat, float shadowAttenua <$declareLightingAmbient(0, 1, 0, supportScattering)$> <$declareLightingDirectional(supportScattering)$> -vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness) { - <$prepareGlobalLight()$> +<@if supportScattering@> +<$declareDeferredCurvature()$> +<@endif@> + +vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, + vec3 albedo, float metallic, float roughness +<@if supportScattering@> + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> + ) { + <$prepareGlobalLight(supportScattering)$> // Ambient vec3 ambientDiffuse; vec3 ambientSpecular; - evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); + evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); @@ -140,45 +133,17 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu // Directional vec3 directionalDiffuse; vec3 directionalSpecular; - evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); return color; } -<@if supportScattering@> - -<$declareDeferredCurvature()$> - -vec3 evalSkyboxGlobalColorScattering(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float scattering, vec4 blurredCurvature, vec4 diffusedCurvature) { - <$prepareGlobalLight(1)$> - - // Ambient - vec3 ambientDiffuse; - vec3 ambientSpecular; - evalLightingAmbientScattering(ambientDiffuse, ambientSpecular, light, - fragEyeDir, fragNormal, roughness, - metallic, fresnel, albedo, obscurance, - isScatteringEnabled() * scattering, lowNormal, highCurvature, lowCurvature); - color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); - color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); - - // Directional - vec3 directionalDiffuse; - vec3 directionalSpecular; - evalLightingDirectionalScattering(directionalDiffuse, directionalSpecular, light, - fragEyeDir, fragNormal, roughness, - metallic, fresnel, albedo, shadowAttenuation, - isScatteringEnabled() * scattering, , midNormalCurvature, lowNormalCurvature); - color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); - color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); - - return vec3(color); -} - -<@endif@> - <@endfunc@> <@func declareEvalLightmappedColor()@> @@ -215,6 +180,8 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float opacity) { <$prepareGlobalLight()$> + + color += emissive * isEmissiveEnabled(); // Ambient vec3 ambientDiffuse; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 4161688c7a..59c5012147 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -209,11 +209,13 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo state->setDepthTest(true, false, gpu::LESS_EQUAL); // TODO: We should use DepthClamp and avoid changing geometry for inside /outside cases - // additive blending state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + } else { state->setCullMode(gpu::State::CULL_BACK); + // additive blending + state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); } pipeline = gpu::Pipeline::create(program, state); @@ -334,11 +336,11 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC batch.setStateScissorRect(args->_viewport); // Clear Lighting buffer - auto lightingFbo = DependencyManager::get()->getLightingFramebuffer(); + /* auto lightingFbo = DependencyManager::get()->getLightingFramebuffer(); batch.setFramebuffer(lightingFbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(vec3(0), 0), true); - +*/ // Clear deferred auto deferredFbo = DependencyManager::get()->getDeferredFramebuffer(); diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index fa95a66b0b..602e559114 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -104,6 +104,7 @@ void FramebufferCache::createPrimaryFramebuffer() { _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + _deferredFramebuffer->setRenderBuffer(3, _lightingTexture); // For AO: auto pointMipSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT); diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index e677eac6f8..e2aa1107df 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -57,44 +57,38 @@ vec3 evalAmbientSpecularIrradiance(Light light, vec3 fragEyeDir, vec3 fragNormal } <@endfunc@> -<@func declareLightingAmbient(supportAmbientSphere, supportAmbientMap, supportIfAmbientMapElseAmbientSphere, supportScattering)@> +<@func declareLightingAmbient(supportAmbientSphere1, supportAmbientMap1, supportIfAmbientMapElseAmbientSphere1, supportScattering)@> -<$declareEvalAmbientSpecularIrradiance(supportAmbientSphere, supportAmbientMap, supportIfAmbientMapElseAmbientSphere)$> - -void evalLightingAmbient(out vec3 diffuse, out vec3 specular, Light light, vec3 eyeDir, vec3 normal, float roughness, float metallic, vec3 fresnel, vec3 albedo, float obscurance) { - // Diffuse from ambient - diffuse = (1 - metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), normal).xyz * obscurance * getLightAmbientIntensity(light); - - // Specular highlight from ambient - specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); -} +<$declareEvalAmbientSpecularIrradiance(supportAmbientSphere1, supportAmbientMap1, supportIfAmbientMapElseAmbientSphere1)$> <@if supportScattering@> - -!> - float curvatureAO(in float k) { return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369; } +<@endif@> -void evalLightingAmbientScattering(out vec3 diffuse, out vec3 specular, Light light, - vec3 eyeDir, vec3 normal, float roughness, - float metallic, vec3 fresnel, vec3 albedo, float obscurance, - float scatttering, vec3 lowNormal, float highCurvature, float lowCurvature) { +void evalLightingAmbient(out vec3 diffuse, out vec3 specular, Light light, vec3 eyeDir, vec3 normal, + float roughness, float metallic, vec3 fresnel, vec3 albedo, float obscurance +<@if supportScattering@> + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> + ) { - float ambientOcclusion = curvatureAO(lowCurvature * 20.0f) * 0.5f; - float ambientOcclusionHF = curvatureAO(highCurvature * 8.0f) * 0.5f; +<@if supportScattering@> + + float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; + float ambientOcclusionHF = curvatureAO(midNormalCurvature.w * 8.0f) * 0.5f; ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF); - /* if (showCurvature()) { - diffuse = vec3(ambientOcclusion); - specular = vec3(0.0); - return; - }*/ + /* if (showCurvature()) { + diffuse = vec3(ambientOcclusion); + specular = vec3(0.0); + return; + }*/ // Diffuse from ambient - diffuse = ambientOcclusion * albedo * evalSphericalLight(getLightAmbientSphere(light), lowNormal).xyz *getLightAmbientIntensity(light); + diffuse = ambientOcclusion * albedo * evalSphericalLight(getLightAmbientSphere(light), lowNormalCurvature.xyz).xyz *getLightAmbientIntensity(light); // Specular highlight from ambient // vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel); @@ -102,10 +96,16 @@ void evalLightingAmbientScattering(out vec3 diffuse, out vec3 specular, Light li // Specular highlight from ambient -// specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); + // specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); specular = vec3(0.0); + +<@else@> + // Diffuse from ambient + diffuse = (1 - metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), normal).xyz * obscurance * getLightAmbientIntensity(light); + + // Specular highlight from ambient + specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); +<@endif@> } -<@endif@> - <@endfunc@> diff --git a/libraries/render-utils/src/LightDirectional.slh b/libraries/render-utils/src/LightDirectional.slh index 13a97a115f..6fc27f2250 100644 --- a/libraries/render-utils/src/LightDirectional.slh +++ b/libraries/render-utils/src/LightDirectional.slh @@ -10,34 +10,20 @@ <@func declareLightingDirectional(supportScattering)@> -<@include DeferredLighting.slh@> void evalLightingDirectional(out vec3 diffuse, out vec3 specular, Light light, vec3 eyeDir, vec3 normal, float roughness, - float metallic, vec3 fresnel, vec3 albedo, float shadow) { - - vec4 shading = evalFragShading(normal, -getLightDirection(light), eyeDir, metallic, fresnel, roughness); - - vec3 lightEnergy = shadow * getLightColor(light) * getLightIntensity(light); - - diffuse = albedo * shading.w * lightEnergy; - - specular = shading.rgb * lightEnergy; -} - + float metallic, vec3 fresnel, vec3 albedo, float shadow <@if supportScattering@> - -void evalLightingDirectionalScattering(out vec3 diffuse, out vec3 specular, Light light, - vec3 eyeDir, vec3 normal, float roughness, - float metallic, vec3 fresnel, vec3 albedo, float shadow, - float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature) { - - vec3 fragLightDir = -normalize(getLightDirection(light)); - - evalFragShading(diffuse, specular, - normal, fragLightDir, eyeDir, - metallic, fresnel, roughness, - scattering, midNormalCurvature, lowNormalCurvature); + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> + ) { + + evalFragShading(diffuse, specular, normal, -getLightDirection(light), eyeDir, metallic, fresnel, roughness +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); vec3 lightEnergy = shadow * getLightColor(light) * getLightIntensity(light); @@ -46,7 +32,5 @@ void evalLightingDirectionalScattering(out vec3 diffuse, out vec3 specular, Ligh specular *= lightEnergy; } -<@endif@> - <@endfunc@> diff --git a/libraries/render-utils/src/LightPoint.slh b/libraries/render-utils/src/LightPoint.slh index dc32149dc9..ab63276004 100644 --- a/libraries/render-utils/src/LightPoint.slh +++ b/libraries/render-utils/src/LightPoint.slh @@ -11,12 +11,13 @@ <@func declareLightingPoint(supportScattering)@> - -<@include DeferredLighting.slh@> - void evalLightingPoint(out vec3 diffuse, out vec3 specular, Light light, vec3 fragLightVec, vec3 fragEyeDir, vec3 normal, float roughness, - float metallic, vec3 fresnel, vec3 albedo, float shadow) { + float metallic, vec3 fresnel, vec3 albedo, float shadow +<@if supportScattering@> + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> + ) { // Allright we re valid in the volume float fragLightDistance = length(fragLightVec); @@ -28,43 +29,11 @@ void evalLightingPoint(out vec3 diffuse, out vec3 specular, Light light, vec3 lightEnergy = radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); // Eval shading - vec4 shading = evalFragShading(normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); - - diffuse = albedo * shading.w * lightEnergy; - - specular = shading.rgb * lightEnergy; - - if (getLightShowContour(light) > 0.0) { - // Show edge - float edge = abs(2.0 * ((getLightRadius(light) - fragLightDistance) / (0.1)) - 1.0); - if (edge < 1) { - float edgeCoord = exp2(-8.0*edge*edge); - diffuse = vec3(edgeCoord * edgeCoord * getLightShowContour(light) * getLightColor(light)); - } - } -} - + evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness <@if supportScattering@> - -void evalLightingPointScattering(out vec3 diffuse, out vec3 specular, Light light, - vec3 fragLightVec, vec3 fragEyeDir, vec3 normal, float roughness, - float metallic, vec3 fresnel, vec3 albedo, float shadow, - float scattering, vec3 midNormal, vec3 lowNormal, float curvature) { - - // Allright we re valid in the volume - float fragLightDistance = length(fragLightVec); - vec3 fragLightDir = fragLightVec / fragLightDistance; - - // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance); - - vec3 lightEnergy = radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); - - // Eval shading - evalFragShading(diffuse, specular, - normal, fragLightDir, fragEyeDir, - metallic, fresnel, roughness, - scattering, midNormalCurvature, lowNormalCurvature); + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); diffuse *= albedo * lightEnergy; @@ -80,8 +49,6 @@ void evalLightingPointScattering(out vec3 diffuse, out vec3 specular, Light ligh } } -<@endif@> - <@endfunc@> diff --git a/libraries/render-utils/src/LightSpot.slh b/libraries/render-utils/src/LightSpot.slh index ece9280267..03a7a6120e 100644 --- a/libraries/render-utils/src/LightSpot.slh +++ b/libraries/render-utils/src/LightSpot.slh @@ -13,7 +13,11 @@ void evalLightingSpot(out vec3 diffuse, out vec3 specular, Light light, vec4 fragLightDirLen, float cosSpotAngle, vec3 fragEyeDir, vec3 normal, float roughness, - float metallic, vec3 fresnel, vec3 albedo, float shadow) { + float metallic, vec3 fresnel, vec3 albedo, float shadow +<@if supportScattering@> + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> + ) { // Allright we re valid in the volume float fragLightDistance = fragLightDirLen.w; @@ -25,49 +29,14 @@ void evalLightingSpot(out vec3 diffuse, out vec3 specular, Light light, vec3 lightEnergy = angularAttenuation * radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); // Eval shading - vec4 shading = evalFragShading(normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); - - diffuse = albedo * shading.w * lightEnergy; - - specular = shading.rgb * lightEnergy; - - if (getLightShowContour(light) > 0.0) { - // Show edges - float edgeDistR = (getLightRadius(light) - fragLightDistance); - float edgeDistS = dot(fragLightDistance * vec2(cosSpotAngle, sqrt(1.0 - cosSpotAngle * cosSpotAngle)), -getLightSpotOutsideNormal2(light)); - float edgeDist = min(edgeDistR, edgeDistS); - float edge = abs(2.0 * (edgeDist / (0.1)) - 1.0); - if (edge < 1) { - float edgeCoord = exp2(-8.0*edge*edge); - diffuse = vec3(edgeCoord * edgeCoord * getLightColor(light)); - } - } -} - + evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness <@if supportScattering@> - -void evalLightingSpotScattering(out vec3 diffuse, out vec3 specular, Light light, - vec4 fragLightDirLen, float cosSpotAngle, vec3 fragEyeDir, vec3 normal, float roughness, - float metallic, vec3 fresnel, vec3 albedo, float shadow, - float scattering, vec3 midNormal, vec3 lowNormal, float curvature) { - - // Allright we re valid in the volume - float fragLightDistance = fragLightDirLen.w; - vec3 fragLightDir = fragLightDirLen.xyz; - - // Eval attenuation - float radialAttenuation = evalLightAttenuation(light, fragLightDistance); - float angularAttenuation = evalLightSpotAttenuation(light, cosSpotAngle); - vec3 lightEnergy = angularAttenuation * radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); - - // Eval shading - evalFragShading(diffuse, specular, - normal, fragLightDir, fragEyeDir, - metallic, fresnel, roughness, - scattering, midNormalCurvature, lowNormalCurvature); - + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); + diffuse *= albedo * lightEnergy; - + specular *= lightEnergy; if (getLightShowContour(light) > 0.0) { @@ -83,8 +52,6 @@ void evalLightingSpotScattering(out vec3 diffuse, out vec3 specular, Light light } } -<@endif@> - <@endfunc@> diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 787b583b98..811ceb2412 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -143,10 +143,17 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float m <$declareEvalPBRShading()$> // Return xyz the specular/reflection component and w the diffuse component -vec4 evalFragShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 fresnel, float roughness) { - return evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); -} +//vec4 evalFragShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 fresnel, float roughness) { +// return evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); +//} +void evalFragShading(out vec3 diffuse, out vec3 specular, + vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, + float metallic, vec3 fresnel, float roughness) { + vec4 shading = evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); + diffuse = vec3(shading.w); + specular = shading.xyz; +} <$declareBeckmannSpecular()$> <@include SubsurfaceScattering.slh@> diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index b9847149b9..471d12ad3a 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -16,6 +16,7 @@ <@include DeferredGlobalLight.slh@> <$declareEvalLightmappedColor()$> + <$declareEvalAmbientSphereGlobalColor(supportScattering)$> @@ -29,24 +30,26 @@ void main(void) { float shadowAttenuation = 1.0; if (frag.mode == FRAG_MODE_UNLIT) { - _fragColor = vec4(frag.diffuse, 1.0); + // _fragColor = vec4(frag.diffuse, 1.0); + discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0); + /* vec3 color = evalLightmappedColor( + getViewInverse(), + shadowAttenuation, + frag.obscurance, + frag.normal, + frag.diffuse, + frag.specularVal.xyz); + _fragColor = vec4(color, 1.0);*/ + discard; } else { vec4 midNormalCurvature; vec4 lowNormalCurvature; if (frag.mode == FRAG_MODE_SCATTERING) { - unpackMidLowNormalCurvature(_texcoord0, midNormalCurvature, lowNormalCurvature); + unpackMidLowNormalCurvature(_texCoord0, midNormalCurvature, lowNormalCurvature); } - vec3 color = evalAmbientSphereGlobalColorScattering( + vec3 color = evalAmbientSphereGlobalColor( getViewInverse(), shadowAttenuation, frag.obscurance, @@ -54,7 +57,7 @@ void main(void) { frag.normal, frag.diffuse, frag.metallic, - frag.emissive, + // frag.emissive, frag.roughness, frag.scattering, midNormalCurvature, diff --git a/libraries/render-utils/src/directional_ambient_light_shadow.slf b/libraries/render-utils/src/directional_ambient_light_shadow.slf index 1729350687..5f257dce5c 100644 --- a/libraries/render-utils/src/directional_ambient_light_shadow.slf +++ b/libraries/render-utils/src/directional_ambient_light_shadow.slf @@ -17,7 +17,7 @@ <@include DeferredGlobalLight.slh@> <$declareEvalLightmappedColor()$> -<$declareEvalAmbientSphereGlobalColor()$> +<$declareEvalAmbientSphereGlobalColor(isScattering)$> in vec2 _texCoord0; out vec4 _fragColor; @@ -30,17 +30,24 @@ void main(void) { float shadowAttenuation = evalShadowAttenuation(worldPos); if (frag.mode == FRAG_MODE_UNLIT) { - _fragColor = vec4(frag.diffuse, 1.0); + // _fragColor = vec4(frag.diffuse, 1.0); + discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0); + /* vec3 color = evalLightmappedColor( + getViewInverse(), + shadowAttenuation, + frag.obscurance, + frag.normal, + frag.diffuse, + frag.specularVal.xyz); + _fragColor = vec4(color, 1.0);*/ + discard; } else { + vec4 midNormalCurvature; + vec4 lowNormalCurvature; + if (frag.mode == FRAG_MODE_SCATTERING) { + unpackMidLowNormalCurvature(_texCoord0, midNormalCurvature, lowNormalCurvature); + } vec3 color = evalAmbientSphereGlobalColor( getViewInverse(), shadowAttenuation, @@ -49,8 +56,12 @@ void main(void) { frag.normal, frag.diffuse, frag.metallic, - frag.emissive, - frag.roughness); - _fragColor = vec4(color, frag.normalVal.a); + // frag.emissive, + frag.roughness, + frag.scattering, + midNormalCurvature, + lowNormalCurvature); + + _fragColor = vec4(color, 1.0); } } diff --git a/libraries/render-utils/src/directional_light.slf b/libraries/render-utils/src/directional_light.slf index 7e19007260..7193e34448 100644 --- a/libraries/render-utils/src/directional_light.slf +++ b/libraries/render-utils/src/directional_light.slf @@ -29,16 +29,18 @@ void main(void) { // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - _fragColor = vec4(frag.diffuse, 1.0); + // _fragColor = vec4(frag.diffuse, 1.0); + discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - vec3 color = evalLightmappedColor( + /* vec3 color = evalLightmappedColor( getViewInverse(), shadowAttenuation, frag.obscurance, frag.normal, frag.diffuse, frag.specularVal.xyz); - _fragColor = vec4(color, 1.0); + _fragColor = vec4(color, 1.0);*/ + discard; } else { vec3 color = evalAmbientGlobalColor( getViewInverse(), @@ -48,8 +50,8 @@ void main(void) { frag.normal, frag.diffuse, frag.metallic, - frag.emissive, + // frag.emissive, frag.roughness); - _fragColor = vec4(color, frag.normalVal.a); + _fragColor = vec4(color, 1.0); } } diff --git a/libraries/render-utils/src/directional_light_shadow.slf b/libraries/render-utils/src/directional_light_shadow.slf index 2698aec1ac..248f90b733 100644 --- a/libraries/render-utils/src/directional_light_shadow.slf +++ b/libraries/render-utils/src/directional_light_shadow.slf @@ -31,16 +31,18 @@ void main(void) { // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - _fragColor = vec4(frag.diffuse, 1.0); + // _fragColor = vec4(frag.diffuse, 1.0); + discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0); + /* vec3 color = evalLightmappedColor( + getViewInverse(), + shadowAttenuation, + frag.obscurance, + frag.normal, + frag.diffuse, + frag.specularVal.xyz); + _fragColor = vec4(color, 1.0);*/ + discard; } else { vec3 color = evalAmbientGlobalColor( getViewInverse(), @@ -50,8 +52,8 @@ void main(void) { frag.normal, frag.diffuse, frag.metallic, - frag.emissive, + // frag.emissive, frag.roughness); - _fragColor = vec4(color, frag.normalVal.a); + _fragColor = vec4(color, 1.0); } } diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index ffafc56eb2..01c85672c2 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -16,7 +16,7 @@ <@include DeferredGlobalLight.slh@> <$declareEvalLightmappedColor()$> -<$declareEvalSkyboxGlobalColor(supportScattering)$> +<$declareEvalSkyboxGlobalColor(isScattering)$> in vec2 _texCoord0; out vec4 _fragColor; @@ -29,23 +29,25 @@ void main(void) { // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - _fragColor = vec4(frag.diffuse, 1.0); + // _fragColor = vec4(frag.diffuse, 1.0); + discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0); + /* vec3 color = evalLightmappedColor( + getViewInverse(), + shadowAttenuation, + frag.obscurance, + frag.normal, + frag.diffuse, + frag.specularVal.xyz); + _fragColor = vec4(color, 1.0);*/ + discard; } else { vec4 midNormalCurvature; vec4 lowNormalCurvature; if (frag.mode == FRAG_MODE_SCATTERING) { - unpackMidLowNormalCurvature(_texcoord0, midNormalCurvature, lowNormalCurvature); + unpackMidLowNormalCurvature(_texCoord0, midNormalCurvature, lowNormalCurvature); } - vec3 color = evalSkyboxGlobalColorScattering( + vec3 color = evalSkyboxGlobalColor( getViewInverse(), shadowAttenuation, frag.obscurance, @@ -53,7 +55,7 @@ void main(void) { frag.normal, frag.diffuse, frag.metallic, - frag.emissive, + // frag.emissive, frag.roughness, frag.scattering, midNormalCurvature, diff --git a/libraries/render-utils/src/directional_skybox_light_shadow.slf b/libraries/render-utils/src/directional_skybox_light_shadow.slf index 98c6912d22..c87a2d15b2 100644 --- a/libraries/render-utils/src/directional_skybox_light_shadow.slf +++ b/libraries/render-utils/src/directional_skybox_light_shadow.slf @@ -17,7 +17,7 @@ <@include DeferredGlobalLight.slh@> <$declareEvalLightmappedColor()$> -<$declareEvalSkyboxGlobalColor()$> +<$declareEvalSkyboxGlobalColor(isScattering)$> in vec2 _texCoord0; out vec4 _fragColor; @@ -31,17 +31,24 @@ void main(void) { // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - _fragColor = vec4(frag.diffuse, 1.0); + // _fragColor = vec4(frag.diffuse, 1.0); + discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0); + /* vec3 color = evalLightmappedColor( + getViewInverse(), + shadowAttenuation, + frag.obscurance, + frag.normal, + frag.diffuse, + frag.specularVal.xyz); + _fragColor = vec4(color, 1.0);*/ + discard; } else { + vec4 midNormalCurvature; + vec4 lowNormalCurvature; + if (frag.mode == FRAG_MODE_SCATTERING) { + unpackMidLowNormalCurvature(_texCoord0, midNormalCurvature, lowNormalCurvature); + } vec3 color = evalSkyboxGlobalColor( getViewInverse(), shadowAttenuation, @@ -50,9 +57,13 @@ void main(void) { frag.normal, frag.diffuse, frag.metallic, - frag.emissive, - frag.roughness); + //frag.emissive, + frag.roughness, + frag.scattering, + midNormalCurvature, + lowNormalCurvature); - _fragColor = vec4(color, frag.normalVal.a); + + _fragColor = vec4(color, 1.0); } } diff --git a/libraries/render-utils/src/overlay3D.slf b/libraries/render-utils/src/overlay3D.slf index 1c374b759b..6ff882188c 100644 --- a/libraries/render-utils/src/overlay3D.slf +++ b/libraries/render-utils/src/overlay3D.slf @@ -11,13 +11,19 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include LightingModel.slh@> + <@include model/Light.slh@> +<@include LightingModel.slh@> +<$declareLightingModel()$> + +<@include LightDirectional.slh@> +<$declareLightingDirectional()$> + <@include gpu/Transform.slh@> <$declareStandardCameraTransform()$> -vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 specular, float roughness, float opacity) { +vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 fresnel, float roughness, float opacity) { // Need the light now Light light = getLight(); @@ -30,9 +36,12 @@ vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 a vec3 color = opacity * albedo * getLightColor(light) * getLightAmbientIntensity(light); - vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, metallic, specular, roughness); - - color += vec3(albedo * shading.w * opacity + shading.rgb) * shadowAttenuation * getLightColor(light) * getLightIntensity(light); + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); + color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); return vec4(color, opacity); } diff --git a/libraries/render-utils/src/overlay3D_translucent.slf b/libraries/render-utils/src/overlay3D_translucent.slf index 14c257c75e..d3ddc04543 100644 --- a/libraries/render-utils/src/overlay3D_translucent.slf +++ b/libraries/render-utils/src/overlay3D_translucent.slf @@ -12,13 +12,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include LightingModel.slh@> <@include model/Light.slh@> +<@include LightingModel.slh@> +<$declareLightingModel()$> + +<@include LightDirectional.slh@> +<$declareLightingDirectional()$> + <@include gpu/Transform.slh@> <$declareStandardCameraTransform()$> -vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 specular, float roughness, float opacity) { +vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 fresnel, float roughness, float opacity) { // Need the light now Light light = getLight(); @@ -31,9 +36,12 @@ vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 a vec3 color = opacity * albedo * getLightColor(light) * getLightAmbientIntensity(light); - vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, metallic, specular, roughness); - - color += vec3(albedo * shading.w * opacity + shading.rgb) * shadowAttenuation * getLightColor(light) * getLightIntensity(light); + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); + color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); return vec4(color, opacity); } diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index ebf7726634..5916c60f22 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -68,23 +68,15 @@ void main(void) { vec3 diffuse; vec3 specular; - + vec4 midNormalCurvature; + vec4 lowNormalCurvature; if (frag.mode == FRAG_MODE_SCATTERING) { - vec4 blurredCurvature = fetchCurvature(texCoord); - vec4 diffusedCurvature = fetchDiffusedCurvature(texCoord); - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float highCurvature = unpackCurvature(blurredCurvature.w); - float lowCurvature = unpackCurvature(diffusedCurvature.w); - evalLightingPointScattering(diffuse, specular, light, - fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.specular, frag.diffuse, 1.0, - frag.scattering * isScatteringEnabled(), midNormal, lowNormal, lowCurvature); - } else { - evalLightingPoint(diffuse, specular, light, - fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.specular, frag.diffuse, 1.0); + unpackMidLowNormalCurvature(texCoord, midNormalCurvature, lowNormalCurvature); } + evalLightingPoint(diffuse, specular, light, + fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, + frag.metallic, frag.specular, frag.diffuse, 1.0, + frag.scattering * isScatteringEnabled(), midNormalCurvature, lowNormalCurvature); _fragColor.rgb += diffuse * isDiffuseEnabled() * isPointEnabled(); _fragColor.rgb += specular * isSpecularEnabled() * isPointEnabled(); diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 596666965b..b9b2be3ebf 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -70,22 +70,15 @@ void main(void) { vec3 diffuse; vec3 specular; + vec4 midNormalCurvature; + vec4 lowNormalCurvature; if (frag.mode == FRAG_MODE_SCATTERING) { - vec4 blurredCurvature = fetchCurvature(texCoord); - vec4 diffusedCurvature = fetchDiffusedCurvature(texCoord); - vec3 midNormal = normalize((blurredCurvature.xyz - 0.5f) * 2.0f); - vec3 lowNormal = normalize((diffusedCurvature.xyz - 0.5f) * 2.0f); - float highCurvature = unpackCurvature(blurredCurvature.w); - float lowCurvature = unpackCurvature(diffusedCurvature.w); - evalLightingSpotScattering(diffuse, specular, light, - fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.specular, frag.diffuse, 1.0, - frag.scattering * isScatteringEnabled(), midNormal, lowNormal, lowCurvature); - } else { - evalLightingSpot(diffuse, specular, light, - fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.specular, frag.diffuse, 1.0); + unpackMidLowNormalCurvature(texCoord, midNormalCurvature, lowNormalCurvature); } + evalLightingSpot(diffuse, specular, light, + fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, + frag.metallic, frag.specular, frag.diffuse, 1.0, + frag.scattering * isScatteringEnabled(), midNormalCurvature, lowNormalCurvature); _fragColor.rgb += diffuse * isDiffuseEnabled() * isSpotEnabled(); _fragColor.rgb += specular * isSpecularEnabled() * isSpotEnabled(); From 216e47b29a79fdce98e6cab450c6281e05a7fd84 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 8 Jul 2016 09:43:11 -0700 Subject: [PATCH 0975/1237] hide the long URL prefix of scripts from marketplace --- interface/src/Application.cpp | 10 +++++++++- interface/src/Application.h | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 82cbaf93c2..93465c8228 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4804,9 +4804,17 @@ bool Application::askToSetAvatarUrl(const QString& url) { } -bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { +bool Application::askToLoadScript(QString scriptFilenameOrURL) { QMessageBox::StandardButton reply; + + static const QString MARKETPLACE_SCRIPT_URL_HOSTNAME_SUFFIX = "mpassets.highfidelity.com"; + + if (scriptFilenameOrURL.contains(MARKETPLACE_SCRIPT_URL_HOSTNAME_SUFFIX)) { + scriptFilenameOrURL = scriptFilenameOrURL.mid(scriptFilenameOrURL.lastIndexOf('/') + 1); + } + QString message = "Would you like to run this script:\n" + scriptFilenameOrURL; + reply = OffscreenUi::question(getWindow(), "Run Script", message, QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 6857ba2a3a..234d79dc0d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -329,7 +329,7 @@ private slots: bool acceptSnapshot(const QString& urlString); bool askToSetAvatarUrl(const QString& url); - bool askToLoadScript(const QString& scriptFilenameOrURL); + bool askToLoadScript(QString scriptFilenameOrURL); bool askToWearAvatarAttachmentUrl(const QString& url); void displayAvatarAttachmentWarning(const QString& message) const; From ae1dd29a7a012414742ed9142ae17715b3291810 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 8 Jul 2016 10:27:53 -0700 Subject: [PATCH 0976/1237] Fix depth writing for glow lines --- libraries/render-utils/src/GeometryCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 9dc104405e..fd06272fa9 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1464,7 +1464,7 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const auto PS = gpu::Shader::createPixel(std::string(glowLine_frag)); auto program = gpu::Shader::createProgram(VS, GS, PS); state->setCullMode(gpu::State::CULL_NONE); - state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setDepthTest(true, false, gpu::LESS_EQUAL); state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); From 1ec34722303ad8f91a24ad7850448241bf2cfa96 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 8 Jul 2016 11:36:43 -0700 Subject: [PATCH 0977/1237] Model overlay dimensions fixes At the moment model overlays will ALWAYS scale to fit their dimensions Update handControllerGrab to account for this behavior. --- interface/src/ui/overlays/ModelOverlay.cpp | 25 ++++++------------- .../system/controllers/handControllerGrab.js | 13 +++++----- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index b07e8bb48a..cc3f0dd8e5 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -43,12 +43,14 @@ ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : void ModelOverlay::update(float deltatime) { if (_updateModel) { _updateModel = false; - _model->setSnapModelToCenter(true); - _model->setScale(getDimensions()); _model->setRotation(getRotation()); _model->setTranslation(getPosition()); _model->setURL(_url); + + // dimensions are ALWAYS scale to fit. + _model->setScaleToFit(true, getDimensions()); + _model->simulate(deltatime, true); } else { _model->simulate(deltatime); @@ -87,26 +89,15 @@ void ModelOverlay::render(RenderArgs* args) { void ModelOverlay::setProperties(const QVariantMap& properties) { auto position = getPosition(); auto rotation = getRotation(); - auto scale = getDimensions(); - + Volume3DOverlay::setProperties(properties); - + if (position != getPosition() || rotation != getRotation()) { _updateModel = true; } - if (scale != getDimensions()) { - auto newScale = getDimensions(); - if (newScale.x <= 0 || newScale.y <= 0 || newScale.z <= 0) { - setDimensions(scale); - } else { - QMetaObject::invokeMethod(_model.get(), "setScaleToFit", Qt::AutoConnection, - Q_ARG(const bool&, true), - Q_ARG(const glm::vec3&, getDimensions())); - _updateModel = true; - } - } - + _updateModel = true; + auto urlValue = properties["url"]; if (urlValue.isValid() && urlValue.canConvert()) { _url = urlValue.toString(); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 228c4fe483..b98a528882 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -26,6 +26,8 @@ var WANT_DEBUG_SEARCH_NAME = null; // these tune time-averaging and "on" value for analog trigger // +var SPARK_MODEL_SCALE_FACTOR = 0.75; + var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab @@ -72,7 +74,6 @@ var LINE_ENTITY_DIMENSIONS = { var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray -var EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR = 0.75; var EQUIP_RADIUS_EMBIGGEN_FACTOR = 1.1; // @@ -929,7 +930,7 @@ function MyController(hand) { url: "http://hifi-content.s3.amazonaws.com/alan/dev/Equip-Spark.fbx", position: hotspot.worldPosition, rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: (hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR, + dimensions: hotspot.radius * 2 * SPARK_MODEL_SCALE_FACTOR, ignoreRayIntersection: true })); @@ -938,7 +939,7 @@ function MyController(hand) { url: "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx", position: hotspot.worldPosition, rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: (hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR * 0.5, + dimensions: hotspot.radius * 2, ignoreRayIntersection: true })); @@ -950,7 +951,7 @@ function MyController(hand) { this.updateOverlayInfoSet = function (overlayInfoSet, timestamp, potentialEquipHotspot) { overlayInfoSet.timestamp = timestamp; - var radius = (overlayInfoSet.hotspot.radius / EQUIP_RADIUS) * EQUIP_RADIUS_TO_MODEL_SCALE_FACTOR; + var radius = overlayInfoSet.hotspot.radius; // embiggen the overlays if it maches the potentialEquipHotspot if (potentialEquipHotspot && overlayInfoSet.entityID == potentialEquipHotspot.entityID && @@ -966,14 +967,14 @@ function MyController(hand) { Overlays.editOverlay(overlayInfoSet.overlays[0], { position: position, rotation: props.rotation, - dimensions: radius + dimensions: radius * 2 * SPARK_MODEL_SCALE_FACTOR }); // fresnel sphere Overlays.editOverlay(overlayInfoSet.overlays[1], { position: position, rotation: props.rotation, - dimensions: radius * 0.5 // HACK to adjust sphere size... + dimensions: radius * 2 }); }; From 7a9b3978e96908034caee4c92c73082cd38bfafa Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 8 Jul 2016 13:01:06 -0700 Subject: [PATCH 0978/1237] Better processing of audio A couple things Ken suggested. First off, there are now AudioSRC calls for floats, which simplfied stuff a bit. Then, I switched to grabbing network packets first, and only pulling at most that amount of audio from the local injectors. That improves things - the occasional artifacts (when the injectors got more data than the network for instance) are gone. Also, fixed build issue (unused variable warning) for mac and android. --- libraries/audio-client/src/AudioClient.cpp | 168 +++++++++------------ 1 file changed, 71 insertions(+), 97 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 01bd0d3c22..0943585ab7 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1190,9 +1190,7 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { float AudioClient::gainForSource(const glm::vec3& relativePosition, float volume) { // TODO: put these in a place where we can share with AudioMixer! - const float LOUDNESS_TO_DISTANCE_RATIO = 0.00001f; const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.18f; - const float DEFAULT_NOISE_MUTING_THRESHOLD = 0.003f; const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f; @@ -1233,15 +1231,14 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // of what maxSize requests. static const qint64 MAX_FRAMES = 256; static float hrtfBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES]; + static float mixed48Khz[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES*2]; static int16_t scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES]; - // injectors are 24Khz, use this to limit the injector audio only - static AudioLimiter audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); - // final output is 48Khz, so use this to limit network audio plus upsampled injectors static AudioLimiter finalAudioLimiter(2*AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); // Injectors are 24khz, but we are running at 48Khz here, so need an AudioSRC to upsample + // TODO: probably an existing src the AudioClient is appropriate, check! static AudioSRC audioSRC(AudioConstants::SAMPLE_RATE, 2*AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); // limit maxSize @@ -1251,121 +1248,98 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // initialize stuff memset(hrtfBuffer, 0, sizeof(hrtfBuffer)); + memset(mixed48Khz, 0, sizeof(mixed48Khz)); QVector injectorsToRemove; qint64 framesReadFromInjectors = 0; - //qDebug() << "maxSize=" << maxSize << "data ptr: "<< (qint64)data; + qDebug() << "maxSize=" << maxSize; - for (AudioInjector* injector : _audio->getActiveLocalAudioInjectors()) { - // pop off maxSize/4 (since it is mono, and 24Khz instead of 48Khz) - // bytes from the injector's localBuffer, using the data ptr as a temporary buffer - // note we are only hrtf-ing mono buffers, so we request half of what maxSize is, since it - // wants that many stereo bytes - if (injector->getLocalBuffer() ) { - - glm::vec3 relativePosition = injector->getPosition() - _audio->_positionGetter(); + // first, grab the stuff from the network. Read into a + if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { + AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); + lastPopOutput.readSamples((int16_t*)data, samplesPopped); - qint64 bytesReadFromInjector = injector->getLocalBuffer()->readData(data, maxSize/4); - qint64 framesReadFromInjector = bytesReadFromInjector/sizeof(int16_t); - if (framesReadFromInjector > framesReadFromInjectors) { - framesReadFromInjectors = framesReadFromInjector; - } - if (framesReadFromInjector > 0) { + qDebug() << "AudioClient::AudioOutputIODevice::readData popped " << samplesPopped << "samples"; + + // no matter what, this is how many bytes will be written + bytesWritten = samplesPopped * sizeof(int16_t); + + + // now, grab same number of frames (samplesPopped/2 since network is stereo) + // and divide by 2 again because these are 24Khz not 48Khz + // from each of the localInjectors, if any + for (AudioInjector* injector : _audio->getActiveLocalAudioInjectors()) { + if (injector->getLocalBuffer() ) { + + // we want to get the same number of frames that we got from + // the network. Since network is stereo, that is samplesPopped/2. + // Since each frame is 2 bytes, we read samplesPopped bytes + qint64 bytesReadFromInjector = injector->getLocalBuffer()->readData((char*)scratchBuffer, samplesPopped/2); + qint64 framesReadFromInjector = bytesReadFromInjector/sizeof(int16_t); - //qDebug() << "AudioClient::AudioOutputIODevice::readData found " << framesReadFromInjector << " frames"; + // keep track of max frames read for all injectors + if (framesReadFromInjector > framesReadFromInjectors) { + framesReadFromInjectors = framesReadFromInjector; + } - // calculate these shortly - float gain = _audio->gainForSource(relativePosition, injector->getVolume()); - float azimuth = _audio->azimuthForSource(relativePosition); + if (framesReadFromInjector > 0) { - // now hrtf this guy, one NETWORK_FRAME_SAMPLES_PER_CHANNEL at a time - this->renderHRTF(injector->getLocalHRTF(), (int16_t*)data, hrtfBuffer, azimuth, gain, framesReadFromInjector); + qDebug() << "AudioClient::AudioOutputIODevice::readData found " << framesReadFromInjector << " frames"; + // calculate gain and azimuth for hrtf + glm::vec3 relativePosition = injector->getPosition() - _audio->_positionGetter(); + float gain = _audio->gainForSource(relativePosition, injector->getVolume()); + float azimuth = _audio->azimuthForSource(relativePosition); + + // now hrtf this guy, one NETWORK_FRAME_SAMPLES_PER_CHANNEL at a time + this->renderHRTF(injector->getLocalHRTF(), scratchBuffer, hrtfBuffer, azimuth, gain, framesReadFromInjector); + + } else { + + qDebug() << "AudioClient::AudioOutputIODevice::readData no more data, adding to list of injectors to remove"; + injectorsToRemove.append(injector); + injector->finish(); + } } else { - // probably need to be more clever here than just getting rid of the injector? - //qDebug() << "AudioClient::AudioOutputIODevice::readData no more data, adding to list of injectors to remove"; + qDebug() << "AudioClient::AudioOutputIODevice::readData injector " << injector << " has no local buffer, adding to list of injectors to remove"; injectorsToRemove.append(injector); injector->finish(); } - } else { - - // probably need to be more clever here then just getting rid of injector? - //qDebug() << "AudioClient::AudioOutputIODevice::readData injector " << injector << " has no local buffer, adding to list of injectors to remove"; - injectorsToRemove.append(injector); - injector->finish(); } - } - - if (framesReadFromInjectors > 0) { - // resample the 24Khz injector audio to 48Khz - // but first, hit with limiter :( - audioLimiter.render(hrtfBuffer, (int16_t*)data, framesReadFromInjectors); - audioSRC.render((int16_t*)data, scratchBuffer, framesReadFromInjectors); - - // now, lets move this back into the hrtfBuffer - for(int i=0; i 0) { + // resample the 24Khz injector audio in hrtfBuffer to 48Khz + audioSRC.render(hrtfBuffer, mixed48Khz, framesReadFromInjectors); + + qDebug() << "upsampled to" << framesReadFromInjectors*2 << " frames, stereo"; + + int16_t* dataPtr = (int16_t*)data; + + // now mix in the network data + for (int i=0; igetActiveLocalAudioInjectors().removeOne(injector); - - //qDebug() << "removed injector " << injector << " from active injector list!"; - } - - // now grab the stuff from the network, and mix it in... - if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { - AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - - //qDebug() << "AudioClient::AudioOutputIODevice::readData popped " << samplesPopped << "samples" << "to " << (qint64)data; - - bytesWritten = samplesPopped * sizeof(int16_t); - if (framesReadFromInjectors > 0) { - - int16_t* dataPtr = (int16_t*)data; - // convert audio to floats for mixing with limiter... - //qDebug() << "AudioClient::AudioOUtputIODevice::readData converting to floats..."; - for (int i=0; i bytesWritten) { - bytesWritten = 8*framesReadFromInjectors; - } - - //qDebug() << "done, bytes written = " << bytesWritten; - } - } else if (framesReadFromInjectors == 0) { - // if nobody has data, 0 out buffer... - memset(data, 0, maxSize); - bytesWritten = maxSize; - } else { - // only sound from injectors, multiply by 2 as the htrf is stereo - finalAudioLimiter.render(hrtfBuffer, (int16_t*)data, framesReadFromInjectors*2); - bytesWritten = 8*framesReadFromInjectors; + qDebug() << "removed injector " << injector << " from active injector list!"; } + qDebug() << "bytesWritten=" << bytesWritten; int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); if (!bytesAudioOutputUnplayed) { From 24832df14a7d7ce2fc7c9324c5a2de4c51295194 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 13:13:32 -0700 Subject: [PATCH 0979/1237] changes --- .../system/controllers/handControllerGrab.js | 35 +++++++++++++------ scripts/system/controllers/teleport.js | 3 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 6e7f5c98fa..00e9b2c699 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -520,6 +520,8 @@ function MyController(hand) { visible: true }; this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); + print('CREATED SEARCH OVERLAY : '+ this.searchSphere) + } else { Overlays.editOverlay(this.searchSphere, { position: location, @@ -527,6 +529,7 @@ function MyController(hand) { color: color, visible: true }); + print('EDITED SEARCH OVERLAY : '+ this.searchSphere) } }; @@ -762,10 +765,10 @@ function MyController(hand) { }; this.overlayLineOff = function() { - if (this.overlayLine !== null) { + if (_this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); print('REMOVING OVERLAY LINE' + this.overlayLine) - this.overlayLine = null; + _this.overlayLine = null; } // print('overlay shoudl be null and is line is ' + this.overlayLine) @@ -933,6 +936,7 @@ function MyController(hand) { overlay: overlay, type: "hand" }); + print('ADDED HAND SPHERE OVERLAY : '+overlay) // add larger blue sphere around the palm. overlay = Overlays.addOverlay("sphere", { @@ -945,6 +949,8 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); + print('ADDED HAND SPHERE OVERLAY : '+overlay) + this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, @@ -973,6 +979,8 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); + print('ADDED GRAB BOX OVERLAY : '+ overlay) + _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, @@ -999,6 +1007,8 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); + print('ADDED SPHERE HOTSTPOT OVERLAY : '+ overlay) + _this.hotspotOverlays.push({ entityID: hotspot.entityID, overlay: overlay, @@ -1032,6 +1042,8 @@ function MyController(hand) { this.destroyHotspots = function() { this.hotspotOverlays.forEach(function(overlayInfo) { + print('deleting overlay hotspot ' + overlayInfo.overlay) + Overlays.deleteOverlay(overlayInfo.overlay); }); this.hotspotOverlays = []; @@ -2220,8 +2232,8 @@ mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.secondaryPress); -mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); -mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); +// mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); +// mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); Controller.enableMapping(MAPPING_NAME); @@ -2243,25 +2255,26 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); + var handleHandMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { - leftController.turnOffVisualizations('left'); - handToDisable = LEFT_HAND; + leftController.turnOffVisualizations('left'); + handToDisable = LEFT_HAND; } if (message === 'right') { - rightController.turnOffVisualizations('right'); - handToDisable = RIGHT_HAND; + rightController.turnOffVisualizations('right'); + handToDisable = RIGHT_HAND; } if (message === "both") { print('disable both') - leftController.turnOffVisualizations('left'); - rightController.turnOffVisualizations('right'); + leftController.turnOffVisualizations('left'); + rightController.turnOffVisualizations('right'); } if (message === 'both' || message === 'none') { - handToDisable = message; + // handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { try { diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 25721aaa55..ee11d0413e 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -447,7 +447,8 @@ function Teleporter() { color: color, visible: true, ignoreRayIntersection: true, // always ignore this - alpha: 1 + alpha: 1, + glow: 1.0 }); } }; From 1cda8afa403fdb1d3a922e2ba1f12c6b60e5b46a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 13:37:59 -0700 Subject: [PATCH 0980/1237] simplify code --- scripts/system/controllers/teleport.js | 92 +++++++------------------- 1 file changed, 23 insertions(+), 69 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index ee11d0413e..5f6c6078c1 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -133,7 +133,9 @@ function Teleporter() { }; this.enterTeleportMode = function(hand) { + print('entered teleport from ' + hand) if (inTeleportMode === true) { + print('already in teleport mode so dont enter again') return; } @@ -144,12 +146,9 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - if (USE_THUMB_AND_TRIGGER_MODE === true) { - Script.update.connect(this.updateForThumbAndTrigger); + Script.update.connect(this.update); + - } else { - Script.update.connect(this.update); - } }; this.findMidpoint = function(start, end) { @@ -197,14 +196,12 @@ function Teleporter() { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); fadeSphereInterval = null; - print('sphere done fading out'); return; } if (currentFadeSphereOpacity > 0) { currentFadeSphereOpacity = currentFadeSphereOpacity - 1; } - print('setting sphere alpha to: ' + currentFadeSphereOpacity) Overlays.editOverlay(_this.fadeSphere, { alpha: currentFadeSphereOpacity / 10 }) @@ -218,7 +215,6 @@ function Teleporter() { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); fadeSphereInterval = null; - print('sphere done fading in') return; } if (currentFadeSphereOpacity < 1) { @@ -240,7 +236,6 @@ function Teleporter() { this.deleteFadeSphere = function() { if (_this.fadeSphere !== null) { - print('deleting fade sphere'); Script.update.disconnect(_this.updateFadeSphere); Overlays.deleteOverlay(_this.fadeSphere); _this.fadeSphere = null; @@ -260,44 +255,24 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - if (USE_THUMB_AND_TRIGGER_MODE === true) { - Script.update.disconnect(this.updateForThumbAndTrigger); + print('exiting teleport mode') - } else { - Script.update.disconnect(this.update); - } + Script.update.disconnect(this.update); this.teleportHand = null; this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); this.enableGrab(); Script.setTimeout(function() { + print('fully exited teleport mode') inTeleportMode = false; }, 100); }; + this.update = function() { - if (teleporter.teleportHand === 'left') { - teleporter.leftRay(); - if (leftPad.buttonValue === 0) { - _this.teleport(); - return; - } - - } else { - teleporter.rightRay(); - if (rightPad.buttonValue === 0) { - _this.teleport(); - return; - } - } - - }; - - this.updateForThumbAndTrigger = function() { - if (teleporter.teleportHand === 'left') { teleporter.leftRay(); @@ -526,7 +501,7 @@ function Teleporter() { }; this.teleport = function(value) { - + print('teleporting : ' + value) if (this.intersection !== null) { if (USE_FADE_MODE === true) { this.createFadeSphere(); @@ -648,39 +623,10 @@ var mappingName, teleportMapping; var TELEPORT_DELAY = 100; + function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); - - teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); - teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - - teleportMapping.from(leftPad.down).to(function(value) { - print('left down' + value) - if (value === 1) { - - Script.setTimeout(function() { - teleporter.enterTeleportMode('left') - }, TELEPORT_DELAY) - } - - }); - teleportMapping.from(rightPad.down).to(function(value) { - print('right down' + value) - if (value === 1) { - - Script.setTimeout(function() { - teleporter.enterTeleportMode('right') - }, TELEPORT_DELAY) - } - }); - -} - - -function registerMappingsWithThumbAndTrigger() { - mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); - teleportMapping = Controller.newMapping(mappingName); teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); @@ -688,24 +634,32 @@ function registerMappingsWithThumbAndTrigger() { teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function(value) { + print('tel 1') teleporter.enterTeleportMode('left') + return; }); teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { + print('tel 2') + teleporter.enterTeleportMode('right') + return; }); teleportMapping.from(leftTrigger.down).when(leftPad.down).to(function(value) { + print('tel 3') + teleporter.enterTeleportMode('left') + return; }); teleportMapping.from(rightTrigger.down).when(rightPad.down).to(function(value) { + print('tel 4') + teleporter.enterTeleportMode('right') + return; }); } -if (USE_THUMB_AND_TRIGGER_MODE === true) { - registerMappingsWithThumbAndTrigger(); -} else { - registerMappings(); -} + +registerMappings(); var teleporter = new Teleporter(); From bc7123d701ba33135c3c0feb9e2e09482153c1fe Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 8 Jul 2016 13:59:49 -0700 Subject: [PATCH 0981/1237] comment out logging for now --- libraries/audio-client/src/AudioClient.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0943585ab7..67fb1ff561 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1252,14 +1252,14 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { QVector injectorsToRemove; qint64 framesReadFromInjectors = 0; - qDebug() << "maxSize=" << maxSize; + //qDebug() << "maxSize=" << maxSize; // first, grab the stuff from the network. Read into a if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples((int16_t*)data, samplesPopped); - qDebug() << "AudioClient::AudioOutputIODevice::readData popped " << samplesPopped << "samples"; + //qDebug() << "AudioClient::AudioOutputIODevice::readData popped " << samplesPopped << "samples"; // no matter what, this is how many bytes will be written bytesWritten = samplesPopped * sizeof(int16_t); @@ -1284,7 +1284,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { if (framesReadFromInjector > 0) { - qDebug() << "AudioClient::AudioOutputIODevice::readData found " << framesReadFromInjector << " frames"; + //qDebug() << "AudioClient::AudioOutputIODevice::readData found " << framesReadFromInjector << " frames"; // calculate gain and azimuth for hrtf glm::vec3 relativePosition = injector->getPosition() - _audio->_positionGetter(); @@ -1312,7 +1312,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // resample the 24Khz injector audio in hrtfBuffer to 48Khz audioSRC.render(hrtfBuffer, mixed48Khz, framesReadFromInjectors); - qDebug() << "upsampled to" << framesReadFromInjectors*2 << " frames, stereo"; + //qDebug() << "upsampled to" << framesReadFromInjectors*2 << " frames, stereo"; int16_t* dataPtr = (int16_t*)data; @@ -1321,7 +1321,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { mixed48Khz[i] += (float)(*dataPtr++) * 1/32676.0f; } - qDebug() << "mixed network data into upsampled hrtf'd mixed injector data" ; + //qDebug() << "mixed network data into upsampled hrtf'd mixed injector data" ; finalAudioLimiter.render(mixed48Khz, (int16_t*)data, samplesPopped/2); } @@ -1336,10 +1336,10 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // k get rid of finished injectors for (AudioInjector* injector : injectorsToRemove) { _audio->getActiveLocalAudioInjectors().removeOne(injector); - qDebug() << "removed injector " << injector << " from active injector list!"; + //qDebug() << "removed injector " << injector << " from active injector list!"; } - qDebug() << "bytesWritten=" << bytesWritten; + //qDebug() << "bytesWritten=" << bytesWritten; int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); if (!bytesAudioOutputUnplayed) { From 7d1f52da7040f55659b84ad4ea8b951806bea118 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 8 Jul 2016 14:33:22 -0700 Subject: [PATCH 0982/1237] Added glow effect to search beam --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index cff580a3ec..07bc4ab1b2 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -550,7 +550,7 @@ function MyController(hand) { this.overlayLineOn = function (closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { - lineWidth: 5, + glow: 1.0, start: closePoint, end: farPoint, color: color, From 5bce924eda4dcb16be76ddd89346cd1503c47474 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 8 Jul 2016 10:11:59 -0700 Subject: [PATCH 0983/1237] keep signature for askToLoadScript --- interface/src/Application.cpp | 9 +++++---- interface/src/Application.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 93465c8228..94dc26693f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4804,16 +4804,17 @@ bool Application::askToSetAvatarUrl(const QString& url) { } -bool Application::askToLoadScript(QString scriptFilenameOrURL) { +bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QMessageBox::StandardButton reply; static const QString MARKETPLACE_SCRIPT_URL_HOSTNAME_SUFFIX = "mpassets.highfidelity.com"; - if (scriptFilenameOrURL.contains(MARKETPLACE_SCRIPT_URL_HOSTNAME_SUFFIX)) { - scriptFilenameOrURL = scriptFilenameOrURL.mid(scriptFilenameOrURL.lastIndexOf('/') + 1); + QString shortName = scriptFilenameOrURL; + if (shortName.contains(MARKETPLACE_SCRIPT_URL_HOSTNAME_SUFFIX)) { + shortName = shortName.mid(shortName.lastIndexOf('/') + 1); } - QString message = "Would you like to run this script:\n" + scriptFilenameOrURL; + QString message = "Would you like to run this script:\n" + shortName; reply = OffscreenUi::question(getWindow(), "Run Script", message, QMessageBox::Yes | QMessageBox::No); diff --git a/interface/src/Application.h b/interface/src/Application.h index 234d79dc0d..6857ba2a3a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -329,7 +329,7 @@ private slots: bool acceptSnapshot(const QString& urlString); bool askToSetAvatarUrl(const QString& url); - bool askToLoadScript(QString scriptFilenameOrURL); + bool askToLoadScript(const QString& scriptFilenameOrURL); bool askToWearAvatarAttachmentUrl(const QString& url); void displayAvatarAttachmentWarning(const QString& message) const; From 126e5c29264566657ef727e6858a992b0ecec759 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 8 Jul 2016 14:34:44 -0700 Subject: [PATCH 0984/1237] codec pipeline working, zlib example --- assignment-client/src/audio/AudioMixer.cpp | 8 ++++++++ libraries/audio-client/src/AudioClient.cpp | 16 +++------------- libraries/audio/src/InboundAudioStream.cpp | 11 ++++++++++- .../audio/src/MixedProcessedAudioStream.cpp | 15 +++++++++++++-- plugins/pcmCodec/src/PCMCodecManager.cpp | 16 +++++----------- 5 files changed, 39 insertions(+), 27 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 9d9194aef6..b419939912 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -496,6 +496,14 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess auto clientData = dynamic_cast(sendingNode->getLinkedData()); + // FIXME - why would we not have client data at this point?? + if (!clientData) { + qDebug() << "UNEXPECTED -- didn't have node linked data in " << __FUNCTION__; + sendingNode->setLinkedData(std::unique_ptr { new AudioMixerClientData(sendingNode->getUUID()) }); + clientData = dynamic_cast(sendingNode->getLinkedData()); + connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); + } + clientData->_codec = selectedCoded; clientData->_selectedCodecName = selectedCodecName; qDebug() << "selectedCodecName:" << selectedCodecName; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 9dc096eefd..a076b1b290 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -534,14 +534,13 @@ void AudioClient::negotiateAudioFormat() { void AudioClient::handleSelectedAudioFormat(QSharedPointer message) { qDebug() << __FUNCTION__; - // write them to our packet - _selectedCodecName = message->readString(); + _receivedAudioStream._selectedCodecName = _selectedCodecName = message->readString(); qDebug() << "Selected Codec:" << _selectedCodecName; auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { - _codec = plugin; + _receivedAudioStream._codec = _codec = plugin; qDebug() << "Selected Codec Plugin:" << _codec.get(); break; } @@ -849,16 +848,7 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho); } -void AudioClient::processReceivedSamples(const QByteArray& networkBuffer, QByteArray& outputBuffer) { - - // TODO - codec decode goes here - QByteArray decodedBuffer; - if (_codec) { - _codec->decode(networkBuffer, decodedBuffer); - } else { - decodedBuffer = networkBuffer; - } - +void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { const int numDecodecSamples = decodedBuffer.size() / sizeof(int16_t); const int numDeviceOutputSamples = _outputFrameSize; diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index c19eb0c161..5d4dbd9b94 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -187,7 +187,16 @@ int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packet decodedBuffer = packetAfterStreamProperties; } - return _ringBuffer.writeData(decodedBuffer.data(), numAudioSamples * sizeof(int16_t)); // FIXME? + auto actualSize = decodedBuffer.size(); + + /* + auto expectedSize = numAudioSamples * sizeof(int16_t); + if (expectedSize != actualSize) { + qDebug() << "DECODED SIZE NOT EXPECTED!!!! ----- buffer size:" << actualSize << "expected:" << expectedSize; + } + */ + + return _ringBuffer.writeData(decodedBuffer.data(), actualSize); } int InboundAudioStream::writeDroppableSilentSamples(int silentSamples) { diff --git a/libraries/audio/src/MixedProcessedAudioStream.cpp b/libraries/audio/src/MixedProcessedAudioStream.cpp index d236ac7aad..220b2bd9ee 100644 --- a/libraries/audio/src/MixedProcessedAudioStream.cpp +++ b/libraries/audio/src/MixedProcessedAudioStream.cpp @@ -44,10 +44,21 @@ int MixedProcessedAudioStream::writeLastFrameRepeatedWithFade(int samples) { int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int networkSamples) { - emit addedStereoSamples(packetAfterStreamProperties); + // TODO - codec decode goes here + QByteArray decodedBuffer; + if (_codec) { + _codec->decode(packetAfterStreamProperties, decodedBuffer); + } else { + decodedBuffer = packetAfterStreamProperties; + } + + qDebug() << __FUNCTION__ << "packetAfterStreamProperties:" << packetAfterStreamProperties.size() << "networkSamples:" << networkSamples << "decodedBuffer:" << decodedBuffer.size(); + + + emit addedStereoSamples(decodedBuffer); QByteArray outputBuffer; - emit processSamples(packetAfterStreamProperties, outputBuffer); + emit processSamples(decodedBuffer, outputBuffer); _ringBuffer.writeData(outputBuffer.data(), outputBuffer.size()); diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index d204fb1100..9d55cafedf 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -15,7 +15,7 @@ #include "PCMCodecManager.h" -const QString PCMCodecManager::NAME = "PCM Codec"; +const QString PCMCodecManager::NAME = "zlib"; void PCMCodecManager::init() { } @@ -39,17 +39,11 @@ bool PCMCodecManager::isSupported() const { void PCMCodecManager::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { - // this codec doesn't actually do anything.... - decodedBuffer = encodedBuffer; - - //decodedBuffer = qUncompress(encodedBuffer); - //qDebug() << __FUNCTION__ << "from:" << encodedBuffer.size() << " to:" << decodedBuffer.size(); + //decodedBuffer = encodedBuffer; + decodedBuffer = qUncompress(encodedBuffer); } void PCMCodecManager::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { - // this codec doesn't actually do anything.... - encodedBuffer = decodedBuffer; - - //encodedBuffer = qCompress(decodedBuffer); - //qDebug() << __FUNCTION__ << "from:" << decodedBuffer.size() << " to:" << encodedBuffer.size(); + //encodedBuffer = decodedBuffer; + encodedBuffer = qCompress(decodedBuffer); } From c8d0decab7d0483d27b8eeda0357bc91c2d1e170 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 8 Jul 2016 14:47:13 -0700 Subject: [PATCH 0985/1237] Fix for far grab not working until after edit button is pressed --- scripts/system/controllers/handControllerGrab.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 07bc4ab1b2..a92c59cfed 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -264,7 +264,8 @@ function propsArePhysical(props) { var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; var EDIT_SETTING = "io.highfidelity.isEditting"; function isEditing() { - return EXTERNALLY_MANAGED_2D_MINOR_MODE && Settings.getValue(EDIT_SETTING); + var actualSettingValue = Settings.getValue(EDIT_SETTING) === "false" ? false : !!Settings.getValue(EDIT_SETTING); + return EXTERNALLY_MANAGED_2D_MINOR_MODE && actualSettingValue; } function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, From 38e580b211908698f155dec7ab62035fbf6d6904 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 14:50:21 -0700 Subject: [PATCH 0986/1237] fix some bugs and also change it to use arrival mode --- scripts/system/controllers/teleport.js | 138 +++++++++---------------- 1 file changed, 49 insertions(+), 89 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5f6c6078c1..b30fa96023 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -8,8 +8,7 @@ //FEATURES: -// ENTRY MODES -// Thumbpad only +// ENTRY MODE // Thumpad + trigger // JUMP MODES @@ -28,15 +27,13 @@ var currentFadeSphereOpacity = 1; var fadeSphereInterval = null; var fadeSphereUpdateInterval = null; //milliseconds between fading one-tenth -- so this is a half second fade total -var USE_FADE_MODE = true; -var USE_FADE_IN = false; +var USE_FADE_MODE = false; var USE_FADE_OUT = true; -var FADE_IN_INTERVAL = 25; var FADE_OUT_INTERVAL = 25; // instant -var NUMBER_OF_STEPS = 0; -var SMOOTH_ARRIVAL_SPACING = 0; +// var NUMBER_OF_STEPS = 0; +// var SMOOTH_ARRIVAL_SPACING = 0; // // slow // var SMOOTH_ARRIVAL_SPACING = 150; @@ -46,17 +43,15 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 100; // var NUMBER_OF_STEPS = 4; -//medium-fast -// var SMOOTH_ARRIVAL_SPACING = 33; -// var NUMBER_OF_STEPS = 6; +// medium-fast +var SMOOTH_ARRIVAL_SPACING = 33; +var NUMBER_OF_STEPS = 6; //fast // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; -var USE_THUMB_AND_TRIGGER_MODE = true; - var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -67,15 +62,13 @@ var TARGET_MODEL_DIMENSIONS = { function ThumbPad(hand) { this.hand = hand; - var _this = this; + var _thisPad = this; this.buttonPress = function(value) { - _this.buttonValue = value; + _thisPad.buttonValue = value; }; - this.down = function() { - return _this.buttonValue === 1 ? 1.0 : 0.0; - }; + } function Trigger(hand) { @@ -88,14 +81,14 @@ function Trigger(hand) { }; this.down = function() { - return _this.buttonValue === 1 ? 1.0 : 0.0; + var down = _this.buttonValue === 1 ? 1.0 : 0.0; + return down }; } function Teleporter() { var _this = this; this.intersection = null; - this.targetProps = null; this.rightOverlayLine = null; this.leftOverlayLine = null; this.targetOverlay = null; @@ -133,32 +126,22 @@ function Teleporter() { }; this.enterTeleportMode = function(hand) { - print('entered teleport from ' + hand) if (inTeleportMode === true) { - print('already in teleport mode so dont enter again') return; } - + inTeleportMode = true; if (this.smoothArrivalInterval !== null) { Script.clearInterval(this.smoothArrivalInterval); } - inTeleportMode = true; + if (fadeSphereInterval !== null) { + Script.clearInterval(fadeSphereInterval); + } this.teleportHand = hand; this.initialize(); - this.updateConnected = true; Script.update.connect(this.update); - - + this.updateConnected = true; }; - this.findMidpoint = function(start, end) { - var xy = Vec3.sum(start, end); - var midpoint = Vec3.multiply(0.5, xy); - return midpoint - }; - - - this.createFadeSphere = function(avatarHead) { var sphereProps = { position: avatarHead, @@ -183,9 +166,7 @@ function Teleporter() { if (USE_FADE_OUT === true) { this.fadeSphereOut(); } - if (USE_FADE_IN === true) { - this.fadeSphereIn(); - } + }; @@ -209,23 +190,6 @@ function Teleporter() { }, FADE_OUT_INTERVAL); }; - this.fadeSphereIn = function() { - fadeSphereInterval = Script.setInterval(function() { - if (currentFadeSphereOpacity >= 1) { - Script.clearInterval(fadeSphereInterval); - _this.deleteFadeSphere(); - fadeSphereInterval = null; - return; - } - if (currentFadeSphereOpacity < 1) { - currentFadeSphereOpacity = currentFadeSphereOpacity - 1; - } - Overlays.editOverlay(_this.fadeSphere, { - alpha: currentFadeSphereOpacity / 10 - }) - - }, FADE_IN_INTERVAL); - }; this.updateFadeSphere = function() { var headPosition = MyAvatar.getHeadPosition(); @@ -255,16 +219,16 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - print('exiting teleport mode') - - Script.update.disconnect(this.update); - this.teleportHand = null; - this.updateConnected = null; + if (this.updateConnected === true) { + Script.update.disconnect(this.update); + } this.disableMappings(); this.turnOffOverlayBeams(); this.enableGrab(); + + this.updateConnected = null; + Script.setTimeout(function() { - print('fully exited teleport mode') inTeleportMode = false; }, 100); }; @@ -277,7 +241,6 @@ function Teleporter() { teleporter.leftRay(); if ((leftPad.buttonValue === 0 || leftTrigger.buttonValue === 0) && inTeleportMode === true) { - print('TELEPORTING LEFT') _this.teleport(); return; } @@ -286,8 +249,6 @@ function Teleporter() { teleporter.rightRay(); if ((rightPad.buttonValue === 0 || rightTrigger.buttonValue === 0) && inTeleportMode === true) { - - print('TELEPORTING RIGHT') _this.teleport(); return; } @@ -337,12 +298,12 @@ function Teleporter() { } else { + this.deleteTargetOverlay(); this.rightLineOn(rightPickRay.origin, location, { red: 7, green: 36, blue: 44 }); - this.deleteTargetOverlay(); } } @@ -388,12 +349,13 @@ function Teleporter() { } else { + + this.deleteTargetOverlay(); this.leftLineOn(leftPickRay.origin, location, { red: 7, green: 36, blue: 44 }); - this.deleteTargetOverlay(); } }; @@ -501,7 +463,9 @@ function Teleporter() { }; this.teleport = function(value) { - print('teleporting : ' + value) + if (value === undefined) { + this.exitTeleportMode(); + } if (this.intersection !== null) { if (USE_FADE_MODE === true) { this.createFadeSphere(); @@ -516,6 +480,15 @@ function Teleporter() { }; + + this.findMidpoint = function(start, end) { + var xy = Vec3.sum(start, end); + var midpoint = Vec3.multiply(0.5, xy); + return midpoint + }; + + + this.getArrivalPoints = function(startPoint, endPoint) { var arrivalPoints = []; @@ -540,23 +513,17 @@ function Teleporter() { this.smoothArrival = function() { _this.arrivalPoints = _this.getArrivalPoints(MyAvatar.position, _this.intersection.intersection); - print('ARRIVAL POINTS: ' + JSON.stringify(_this.arrivalPoints)); - print('end point: ' + JSON.stringify(_this.intersection.intersection)) _this.smoothArrivalInterval = Script.setInterval(function() { - print(_this.arrivalPoints.length + " arrival points remaining") if (_this.arrivalPoints.length === 0) { Script.clearInterval(_this.smoothArrivalInterval); return; } var landingPoint = _this.arrivalPoints.shift(); - print('landing at: ' + JSON.stringify(landingPoint)) MyAvatar.position = landingPoint; if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { - print('clear target overlay') _this.deleteTargetOverlay(); - _this.triggerHaptics(); } @@ -633,29 +600,28 @@ function registerMappings() { teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function(value) { + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).when(leftTrigger.down).to(function(value) { print('tel 1') teleporter.enterTeleportMode('left') return; }); - teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { + teleportMapping.from(Controller.Standard.RightPrimaryThumb).when(rightTrigger.down).to(function(value) { print('tel 2') - teleporter.enterTeleportMode('right') return; }); - teleportMapping.from(leftTrigger.down).when(leftPad.down).to(function(value) { + teleportMapping.from(Controller.Standard.RT).when(Controller.Standard.RightPrimaryThumb).to(function(value) { print('tel 3') - + teleporter.enterTeleportMode('right') + return; + }); + teleportMapping.from(Controller.Standard.LT).when(Controller.Standard.LeftPrimaryThumb).to(function(value) { + print('tel 4') teleporter.enterTeleportMode('left') return; }); - teleportMapping.from(rightTrigger.down).when(rightPad.down).to(function(value) { - print('tel 4') - teleporter.enterTeleportMode('right') - return; - }); + } @@ -674,12 +640,6 @@ function cleanup() { teleporter.turnOffOverlayBeams(); teleporter.deleteFadeSphere(); if (teleporter.updateConnected !== null) { - - if (USE_THUMB_AND_TRIGGER_MODE === true) { - Script.update.disconnect(teleporter.updateForThumbAndTrigger); - - } else { - Script.update.disconnect(teleporter.update); - } + Script.update.disconnect(teleporter.update); } } \ No newline at end of file From 3492367c63e27c544e900b043b159399d3b9c02e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:03:59 -0700 Subject: [PATCH 0987/1237] cleanup comments etc --- scripts/system/controllers/teleport.js | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b30fa96023..8426780a7a 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,24 +1,11 @@ // Created by james b. pollack @imgntn on 7/2/2016 // Copyright 2016 High Fidelity, Inc. // -// Creates a beam and target and then teleports you there when you let go of the activation button. +// Creates a beam and target and then teleports you there when you let go of either activation button. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -//FEATURES: - -// ENTRY MODE -// Thumpad + trigger - -// JUMP MODES -// Instant -// Smoth arrival aka stepwise (number of steps, duration of step) - -// FADE MODE -// Cube-overlay (steps,duration) -// Model-overlay (steps, duration) -// Fade out / fade in // defaults with instant jump with fade. to try smooth arrival mode, change use fade to false and then switch the number of arrival steps and spacing var inTeleportMode = false; @@ -68,7 +55,6 @@ function ThumbPad(hand) { _thisPad.buttonValue = value; }; - } function Trigger(hand) { @@ -114,7 +100,6 @@ function Teleporter() { }; this.createMappings = function() { - // peek at the trigger and thumbs to store their values teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); @@ -360,7 +345,6 @@ function Teleporter() { }; this.rightLineOn = function(closePoint, farPoint, color) { - // draw a line if (this.rightOverlayLine === null) { var lineProperties = { start: closePoint, @@ -472,7 +456,6 @@ function Teleporter() { } var offset = getAvatarFootOffset(); this.intersection.intersection.y += offset; - // MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); this.smoothArrival(); @@ -621,10 +604,8 @@ function registerMappings() { return; }); - } - registerMappings(); var teleporter = new Teleporter(); From 5ca14b528bf7e5260e819517b0c24468e83d0da2 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:08:44 -0700 Subject: [PATCH 0988/1237] remove prints and format --- scripts/defaultScripts.js | 1 + .../{teleportDestination.fbx => teleport.fbx} | Bin 188700 -> 189244 bytes scripts/system/assets/models/teleportBeam.fbx | Bin 40172 -> 0 bytes .../system/controllers/handControllerGrab.js | 221 ++++++++++++------ scripts/system/controllers/teleport.js | 6 +- 5 files changed, 147 insertions(+), 81 deletions(-) rename scripts/system/assets/models/{teleportDestination.fbx => teleport.fbx} (94%) delete mode 100644 scripts/system/assets/models/teleportBeam.fbx diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2bf908ab57..711e64f938 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -23,5 +23,6 @@ Script.load("system/controllers/handControllerGrab.js"); Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); +Script.load("system/controllers/teleport.js"); Script.load("system/dialTone.js"); Script.load("system/firstPersonHMD.js"); diff --git a/scripts/system/assets/models/teleportDestination.fbx b/scripts/system/assets/models/teleport.fbx similarity index 94% rename from scripts/system/assets/models/teleportDestination.fbx rename to scripts/system/assets/models/teleport.fbx index c850982f563417e960d9079dbc0da29408ed01c9..831f152add043dcdb04c4a567037167ba434eb97 100644 GIT binary patch delta 4084 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 diff --git a/scripts/system/assets/models/teleportBeam.fbx b/scripts/system/assets/models/teleportBeam.fbx deleted file mode 100644 index 21b63d5843ac14a12ea7601d1bf8475c696cac93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40172 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 bNQV9Ysr(37w%}RY!{p|7QjO6owI%;QsNJ!J diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 00e9b2c699..a2738396b6 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -49,8 +49,8 @@ var DROP_WITHOUT_SHAKE = false; var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position -var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects var NO_INTERSECT_COLOR = { @@ -81,7 +81,7 @@ var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. -var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand +var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. @@ -251,13 +251,15 @@ function propsArePhysical(props) { // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; + function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, // because other scripts (such as handControllerPointer) might be using some other visualization // instead of setting Reticle.visible. return (EXTERNALLY_MANAGED_2D_MINOR_MODE && - (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); + (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); } + function restore2DMode() { if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { Reticle.setVisible(true); @@ -399,7 +401,6 @@ function MyController(hand) { this.updateSmoothedTrigger(); if (this.ignoreInput()) { - // print('in ignore input turn off') this.turnOffVisualizations(); return; } @@ -520,7 +521,7 @@ function MyController(hand) { visible: true }; this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); - print('CREATED SEARCH OVERLAY : '+ this.searchSphere) + } else { Overlays.editOverlay(this.searchSphere, { @@ -529,7 +530,7 @@ function MyController(hand) { color: color, visible: true }); - print('EDITED SEARCH OVERLAY : '+ this.searchSphere) + } }; @@ -547,7 +548,6 @@ function MyController(hand) { alpha: 1 }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); - print('CREATED OVERLAY IT IS ' + this.overlayLine ) } else { Overlays.editOverlay(this.overlayLine, { @@ -560,9 +560,8 @@ function MyController(hand) { drawInFront: true, // Even when burried inside of something, show it. alpha: 1 }); - print('edited overlay line ' + this.overlayLine ) } - + }; this.searchIndicatorOn = function(distantPickRay) { @@ -577,12 +576,12 @@ function MyController(hand) { } var searchSphereLocation = Vec3.sum(distantPickRay.origin, - Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); + Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); if ((USE_OVERLAY_LINES_FOR_SEARCHING === true) && PICK_WITH_HAND_RAY) { this.overlayLineOn(handPosition, searchSphereLocation, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } }; @@ -766,13 +765,9 @@ function MyController(hand) { this.overlayLineOff = function() { if (_this.overlayLine !== null) { - Overlays.deleteOverlay(this.overlayLine); - print('REMOVING OVERLAY LINE' + this.overlayLine) - _this.overlayLine = null; + Overlays.deleteOverlay(this.overlayLine); + _this.overlayLine = null; } - - // print('overlay shoudl be null and is line is ' + this.overlayLine) - }; this.searchSphereOff = function() { @@ -805,27 +800,21 @@ function MyController(hand) { }; this.turnOffVisualizations = function(hand) { - // print('TURN OFF VISUALIZATIONS: ' + hand) if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); - // print('after line off') } if (USE_OVERLAY_LINES_FOR_SEARCHING === true || USE_OVERLAY_LINES_FOR_MOVING === true) { this.overlayLineOff(); - // print('after overlay line off') } if (USE_PARTICLE_BEAM_FOR_MOVING === true) { this.particleBeamOff(); - // print('after particle beam off') } this.searchSphereOff(); restore2DMode(); - // print('after all turn off calls') - }; this.triggerPress = function(value) { @@ -901,18 +890,34 @@ function MyController(hand) { this.createHotspots = function() { var _this = this; - var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var HAND_EQUIP_SPHERE_COLOR = { + red: 90, + green: 255, + blue: 90 + }; var HAND_EQUIP_SPHERE_ALPHA = 0.7; var HAND_EQUIP_SPHERE_RADIUS = 0.01; - var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; + var HAND_GRAB_SPHERE_COLOR = { + red: 90, + green: 90, + blue: 255 + }; var HAND_GRAB_SPHERE_ALPHA = 0.3; var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; - var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var EQUIP_SPHERE_COLOR = { + red: 90, + green: 255, + blue: 90 + }; var EQUIP_SPHERE_ALPHA = 0.3; - var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; + var GRAB_BOX_COLOR = { + red: 90, + green: 90, + blue: 255 + }; var GRAB_BOX_ALPHA = 0.1; this.hotspotOverlays = []; @@ -936,7 +941,6 @@ function MyController(hand) { overlay: overlay, type: "hand" }); - print('ADDED HAND SPHERE OVERLAY : '+overlay) // add larger blue sphere around the palm. overlay = Overlays.addOverlay("sphere", { @@ -949,13 +953,16 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - print('ADDED HAND SPHERE OVERLAY : '+overlay) this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, type: "hand", - localPosition: {x: 0, y: 0, z: 0} + localPosition: { + x: 0, + y: 0, + z: 0 + } }); } @@ -979,13 +986,16 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - print('ADDED GRAB BOX OVERLAY : '+ overlay) _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, type: "near", - localPosition: {x: 0, y: 0, z: 0} + localPosition: { + x: 0, + y: 0, + z: 0 + } }); } }); @@ -1007,7 +1017,6 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - print('ADDED SPHERE HOTSTPOT OVERLAY : '+ overlay) _this.hotspotOverlays.push({ entityID: hotspot.entityID, @@ -1023,7 +1032,9 @@ function MyController(hand) { var props; this.hotspotOverlays.forEach(function(overlayInfo) { if (overlayInfo.type === "hand") { - Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); + Overlays.editOverlay(overlayInfo.overlay, { + position: _this.getHandPosition() + }); } else if (overlayInfo.type === "equip") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); @@ -1035,15 +1046,16 @@ function MyController(hand) { } else if (overlayInfo.type === "near") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + Overlays.editOverlay(overlayInfo.overlay, { + position: props.position, + rotation: props.rotation + }); } }); }; this.destroyHotspots = function() { this.hotspotOverlays.forEach(function(overlayInfo) { - print('deleting overlay hotspot ' + overlayInfo.overlay) - Overlays.deleteOverlay(overlayInfo.overlay); }); this.hotspotOverlays = []; @@ -1071,8 +1083,8 @@ function MyController(hand) { var pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), length: PICK_MAX_DISTANCE }; @@ -1157,7 +1169,11 @@ function MyController(hand) { if (wearableProps && wearableProps.joints) { result.push({ entityID: entityID, - localPosition: {x: 0, y: 0, z: 0}, + localPosition: { + x: 0, + y: 0, + z: 0 + }, worldPosition: entityXform.pos, radius: EQUIP_RADIUS, joints: wearableProps.joints @@ -1174,8 +1190,8 @@ function MyController(hand) { var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || - this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedEntity == hotspot.entityID); + this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && + this.getOtherHandController().grabbedEntity == hotspot.entityID); if (refCount > 0 && !okToEquipFromOtherHand) { if (debug) { print("equip is skipping '" + props.name + "': grabbed by someone else"); @@ -1423,8 +1439,8 @@ function MyController(hand) { // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { this.lineOn(rayPickInfo.searchRay.origin, - Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), - NO_INTERSECT_COLOR); + Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), + NO_INTERSECT_COLOR); } this.searchIndicatorOn(rayPickInfo.searchRay); @@ -1515,7 +1531,7 @@ function MyController(hand) { // controller pose is in avatar frame var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); + Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); @@ -1537,7 +1553,7 @@ function MyController(hand) { // scale delta controller hand movement by radius. var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), - radius); + radius); /// double delta controller rotation // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did @@ -1562,7 +1578,7 @@ function MyController(hand) { var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, - Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); + Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + @@ -1571,7 +1587,7 @@ function MyController(hand) { var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * - this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1689,16 +1705,28 @@ function MyController(hand) { if (this.fastHandMoveTimer < 0) { this.fastHandMoveDetected = false; } - var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_REST_TIME = 1; // sec var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { this.fastHandMoveDetected = true; this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; } - var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; + var localHandUpAxis = this.hand === RIGHT_HAND ? { + x: 1, + y: 0, + z: 0 + } : { + x: -1, + y: 0, + z: 0 + }; var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); - var DOWN = {x: 0, y: -1, z: 0}; + var DOWN = { + x: 0, + y: -1, + z: 0 + }; var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); var handIsUpsideDown = false; @@ -1800,8 +1828,16 @@ function MyController(hand) { } Entities.editEntity(this.grabbedEntity, { - velocity: {x: 0, y: 0, z: 0}, - angularVelocity: {x: 0, y: 0, z: 0}, + velocity: { + x: 0, + y: 0, + z: 0 + }, + angularVelocity: { + x: 0, + y: 0, + z: 0 + }, dynamic: false }); @@ -1868,7 +1904,7 @@ function MyController(hand) { if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + - props.parentID + " " + vec3toStr(props.position)); + props.parentID + " " + vec3toStr(props.position)); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); @@ -2110,7 +2146,9 @@ function MyController(hand) { // people are holding something and one of them will be able (if the other releases at the right time) to // bootstrap themselves with the held object. This happens because the meaning of "otherAvatar" in // the collision mask hinges on who the physics simulation owner is. - Entities.editEntity(entityID, {"collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED}); + Entities.editEntity(entityID, { + "collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED + }); } } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); @@ -2124,7 +2162,9 @@ function MyController(hand) { var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); children.forEach(function(childID) { print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); - Entities.editEntity(childID, {parentID: NULL_UUID}); + Entities.editEntity(childID, { + parentID: NULL_UUID + }); }); }; @@ -2170,12 +2210,24 @@ function MyController(hand) { data["dynamic"] && data["parentID"] == NULL_UUID && !data["collisionless"]) { - deactiveProps["velocity"] = {x: 0.0, y: 0.1, z: 0.0}; + deactiveProps["velocity"] = { + x: 0.0, + y: 0.1, + z: 0.0 + }; doSetVelocity = false; } if (noVelocity) { - deactiveProps["velocity"] = {x: 0.0, y: 0.0, z: 0.0}; - deactiveProps["angularVelocity"] = {x: 0.0, y: 0.0, z: 0.0}; + deactiveProps["velocity"] = { + x: 0.0, + y: 0.0, + z: 0.0 + }; + deactiveProps["angularVelocity"] = { + x: 0.0, + y: 0.0, + z: 0.0 + }; doSetVelocity = false; } @@ -2188,7 +2240,7 @@ function MyController(hand) { // be fixed. Entities.editEntity(entityID, { velocity: this.currentVelocity - // angularVelocity: this.currentAngularVelocity + // angularVelocity: this.currentAngularVelocity }); } @@ -2198,14 +2250,32 @@ function MyController(hand) { deactiveProps = { parentID: this.previousParentID, parentJointIndex: this.previousParentJointIndex, - velocity: {x: 0.0, y: 0.0, z: 0.0}, - angularVelocity: {x: 0.0, y: 0.0, z: 0.0} + velocity: { + x: 0.0, + y: 0.0, + z: 0.0 + }, + angularVelocity: { + x: 0.0, + y: 0.0, + z: 0.0 + } }; Entities.editEntity(entityID, deactiveProps); } else if (noVelocity) { - Entities.editEntity(entityID, {velocity: {x: 0.0, y: 0.0, z: 0.0}, - angularVelocity: {x: 0.0, y: 0.0, z: 0.0}, - dynamic: data["dynamic"]}); + Entities.editEntity(entityID, { + velocity: { + x: 0.0, + y: 0.0, + z: 0.0 + }, + angularVelocity: { + x: 0.0, + y: 0.0, + z: 0.0 + }, + dynamic: data["dynamic"] + }); } } else { data = null; @@ -2261,20 +2331,20 @@ var handleHandMessages = function(channel, message, sender) { if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { - leftController.turnOffVisualizations('left'); - handToDisable = LEFT_HAND; + leftController.turnOffVisualizations('left'); + handToDisable = LEFT_HAND; } if (message === 'right') { - rightController.turnOffVisualizations('right'); - handToDisable = RIGHT_HAND; + rightController.turnOffVisualizations('right'); + handToDisable = RIGHT_HAND; } if (message === "both") { print('disable both') - leftController.turnOffVisualizations('left'); - rightController.turnOffVisualizations('right'); + leftController.turnOffVisualizations('left'); + rightController.turnOffVisualizations('right'); } if (message === 'both' || message === 'none') { - // handToDisable = message; + // handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { try { @@ -2352,5 +2422,4 @@ function handleMenuItemEvent(menuItem) { } } -Menu.menuItemEvent.connect(handleMenuItemEvent); - +Menu.menuItemEvent.connect(handleMenuItemEvent); \ No newline at end of file diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 8426780a7a..b92a85aaf4 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -39,7 +39,7 @@ var NUMBER_OF_STEPS = 6; // var NUMBER_OF_STEPS = 20; -var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; +var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport.fbx"); var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, @@ -584,22 +584,18 @@ function registerMappings() { teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); teleportMapping.from(Controller.Standard.LeftPrimaryThumb).when(leftTrigger.down).to(function(value) { - print('tel 1') teleporter.enterTeleportMode('left') return; }); teleportMapping.from(Controller.Standard.RightPrimaryThumb).when(rightTrigger.down).to(function(value) { - print('tel 2') teleporter.enterTeleportMode('right') return; }); teleportMapping.from(Controller.Standard.RT).when(Controller.Standard.RightPrimaryThumb).to(function(value) { - print('tel 3') teleporter.enterTeleportMode('right') return; }); teleportMapping.from(Controller.Standard.LT).when(Controller.Standard.LeftPrimaryThumb).to(function(value) { - print('tel 4') teleporter.enterTeleportMode('left') return; }); From 4f899d875288e0b5e9b3dd8af4b4e14c8e242e3c Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:14:04 -0700 Subject: [PATCH 0989/1237] play nice with grab --- scripts/system/controllers/handControllerGrab.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index a2738396b6..a7a897aa7f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -772,7 +772,6 @@ function MyController(hand) { this.searchSphereOff = function() { if (this.searchSphere !== null) { - print('removing search sphere' + this.searchSphere) Overlays.deleteOverlay(this.searchSphere); this.searchSphere = null; this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE; @@ -2302,8 +2301,8 @@ mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.secondaryPress); -// mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); -// mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); +mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); +mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); Controller.enableMapping(MAPPING_NAME); @@ -2339,12 +2338,11 @@ var handleHandMessages = function(channel, message, sender) { handToDisable = RIGHT_HAND; } if (message === "both") { - print('disable both') leftController.turnOffVisualizations('left'); rightController.turnOffVisualizations('right'); } if (message === 'both' || message === 'none') { - // handToDisable = message; + handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { try { From 70cf25bd8bb42ff6c7cd82b1e4df00f6ef8c1862 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 8 Jul 2016 15:10:18 -0700 Subject: [PATCH 0990/1237] Add missing read locks to LimitedNodeList's hash --- libraries/networking/src/LimitedNodeList.cpp | 1 + libraries/networking/src/LimitedNodeList.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d7a2d47fab..a03fa43093 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -522,6 +522,7 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret) { + QReadLocker readLocker(&_nodeMutex); NodeHash::const_iterator it = _nodeHash.find(uuid); if (it != _nodeHash.end()) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 483aa0734c..c9054ac6d7 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -131,7 +131,7 @@ public: std::function linkedDataCreateCallback; - size_t size() const { return _nodeHash.size(); } + size_t size() const { QReadLocker readLock(&_nodeMutex); return _nodeHash.size(); } SharedNodePointer nodeWithUUID(const QUuid& nodeUUID); @@ -287,7 +287,7 @@ protected: QUuid _sessionUUID; NodeHash _nodeHash; - QReadWriteLock _nodeMutex; + mutable QReadWriteLock _nodeMutex; udt::Socket _nodeSocket; QUdpSocket* _dtlsSocket; HifiSockAddr _localSockAddr; From 97da515585d4283986beb34eb8caa4ce435c6479 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 8 Jul 2016 15:10:48 -0700 Subject: [PATCH 0991/1237] Assign random UUID to DS without one. --- domain-server/src/DomainServer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d75a2c3245..8b3f09d1f7 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -389,6 +389,8 @@ void DomainServer::setupNodeListAndAssignments() { const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant) { nodeList->setSessionUUID(idValueVariant->toString()); + } else { + nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID } connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); From ee970c5e07a6c5ace193951a6f6f69ceb52df53a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:16:26 -0700 Subject: [PATCH 0992/1237] cleanup --- scripts/system/controllers/teleport.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b92a85aaf4..5c881c5fcc 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -6,8 +6,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -// defaults with instant jump with fade. to try smooth arrival mode, change use fade to false and then switch the number of arrival steps and spacing var inTeleportMode = false; var currentFadeSphereOpacity = 1; @@ -567,8 +565,6 @@ var rightPad = new ThumbPad('right'); var leftTrigger = new Trigger('left'); var rightTrigger = new Trigger('right'); -//create a controller mapping and make sure to disable it when the script is stopped - var mappingName, teleportMapping; var TELEPORT_DELAY = 100; From b665f38c1f868e9eab9ca13d99b10ec2234b2f3a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 8 Jul 2016 15:15:25 -0700 Subject: [PATCH 0993/1237] Don't process DomainList packets from 2 DS case 1037 --- libraries/networking/src/NodeList.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index fd1442d639..9678a0e2c6 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -513,9 +513,16 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { if (_domainHandler.getSockAddr().isNull()) { + qWarning() << "IGNORING DomainList packet while not connect to a Domain Server"; // refuse to process this packet if we aren't currently connected to the DS return; } + auto domainUuid = _domainHandler.getUUID(); + auto messageSourceUuid = message->getSourceID(); + if (!domainUuid.isNull() && domainUuid != messageSourceUuid) { + qWarning() << "IGNORING DomainList packet from" << messageSourceUuid << "while connected to" << domainUuid; + return; + } // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; From 489d2e4ca50ecc3ecc3ece28304c352b4352a472 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 8 Jul 2016 15:33:11 -0700 Subject: [PATCH 0994/1237] Update max request limit and number of processing threads --- interface/src/Application.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 82cbaf93c2..5d5bf45dcf 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -168,6 +168,9 @@ static QTimer locationUpdateTimer; static QTimer identityPacketTimer; static QTimer pingTimer; +static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; +static const int PROCESSING_THREAD_POOL_SIZE = 6; + static const QString SNAPSHOT_EXTENSION = ".jpg"; static const QString SVO_EXTENSION = ".svo"; static const QString SVO_JSON_EXTENSION = ".svo.json"; @@ -517,7 +520,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // (main thread, present thread, random OS load) // More threads == faster concurrent loads, but also more concurrent // load on the GPU until we can serialize GPU transfers (off the main thread) - QThreadPool::globalInstance()->setMaxThreadCount(2); + QThreadPool::globalInstance()->setMaxThreadCount(PROCESSING_THREAD_POOL_SIZE); thread()->setPriority(QThread::HighPriority); thread()->setObjectName("Main Thread"); @@ -728,7 +731,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&identityPacketTimer, &QTimer::timeout, getMyAvatar(), &MyAvatar::sendIdentityPacket); identityPacketTimer.start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); - ResourceCache::setRequestLimit(3); + ResourceCache::setRequestLimit(MAX_CONCURRENT_RESOURCE_DOWNLOADS); _glWidget = new GLCanvas(); getApplicationCompositor().setRenderingWidget(_glWidget); From 43967dc9a629111221be0f899697d5d4474eb0eb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 8 Jul 2016 15:47:56 -0700 Subject: [PATCH 0995/1237] handle running of non .svo.json JSON files from mp CDN --- interface/src/Application.cpp | 29 +++++++++++++++++++++++------ interface/src/Application.h | 1 + 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 94dc26693f..94ede3ca83 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -170,7 +170,8 @@ static QTimer pingTimer; static const QString SNAPSHOT_EXTENSION = ".jpg"; static const QString SVO_EXTENSION = ".svo"; -static const QString SVO_JSON_EXTENSION = ".svo.json"; +static const QString SVO_JSON_EXTENSION = ".svo.json"; +static const QString JSON_EXTENSION = ".json"; static const QString JS_EXTENSION = ".js"; static const QString FST_EXTENSION = ".fst"; static const QString FBX_EXTENSION = ".fbx"; @@ -202,13 +203,16 @@ static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStanda Setting::Handle maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS); +static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; + const QHash Application::_acceptedExtensions { { SNAPSHOT_EXTENSION, &Application::acceptSnapshot }, { SVO_EXTENSION, &Application::importSVOFromURL }, { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, + { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, + { JSON_EXTENSION, &Application::importJSONFromURL }, { JS_EXTENSION, &Application::askToLoadScript }, - { FST_EXTENSION, &Application::askToSetAvatarUrl }, - { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl } + { FST_EXTENSION, &Application::askToSetAvatarUrl } }; class DeadlockWatchdogThread : public QThread { @@ -1969,7 +1973,22 @@ void Application::resizeGL() { } } +bool Application::importJSONFromURL(const QString& urlString) { + // we only load files that terminate in just .json (not .svo.json and not .ava.json) + // if they come from the High Fidelity Marketplace Assets CDN + + QUrl jsonURL { urlString }; + + if (jsonURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + emit svoImportRequested(urlString); + return true; + } else { + return false; + } +} + bool Application::importSVOFromURL(const QString& urlString) { + emit svoImportRequested(urlString); return true; } @@ -4807,10 +4826,8 @@ bool Application::askToSetAvatarUrl(const QString& url) { bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QMessageBox::StandardButton reply; - static const QString MARKETPLACE_SCRIPT_URL_HOSTNAME_SUFFIX = "mpassets.highfidelity.com"; - QString shortName = scriptFilenameOrURL; - if (shortName.contains(MARKETPLACE_SCRIPT_URL_HOSTNAME_SUFFIX)) { + if (shortName.contains(MARKETPLACE_CDN_HOSTNAME)) { shortName = shortName.mid(shortName.lastIndexOf('/') + 1); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 6857ba2a3a..1ccef79198 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -380,6 +380,7 @@ private: void displaySide(RenderArgs* renderArgs, Camera& whichCamera, bool selfAvatarOnly = false); + bool importJSONFromURL(const QString& urlString); bool importSVOFromURL(const QString& urlString); bool nearbyEntitiesAreReadyForPhysics(); From 11aafc0f48fc3c574c77f87864ae1d908e73d0cb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 8 Jul 2016 16:00:06 -0700 Subject: [PATCH 0996/1237] use endsWith for scriptURL check --- interface/src/Application.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 94ede3ca83..b58fa57d3f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4827,7 +4827,10 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QMessageBox::StandardButton reply; QString shortName = scriptFilenameOrURL; - if (shortName.contains(MARKETPLACE_CDN_HOSTNAME)) { + + QUrl scriptURL { scriptFilenameOrURL }; + + if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { shortName = shortName.mid(shortName.lastIndexOf('/') + 1); } From f0fcb94a6ae902a074379e31b7c9685b802488bc Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 8 Jul 2016 16:00:08 -0700 Subject: [PATCH 0997/1237] Typos --- libraries/networking/src/NodeList.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 9678a0e2c6..02350ac23c 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -513,14 +513,14 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { if (_domainHandler.getSockAddr().isNull()) { - qWarning() << "IGNORING DomainList packet while not connect to a Domain Server"; + qWarning() << "IGNORING DomainList packet while not connected to a Domain Server"; // refuse to process this packet if we aren't currently connected to the DS return; } - auto domainUuid = _domainHandler.getUUID(); - auto messageSourceUuid = message->getSourceID(); - if (!domainUuid.isNull() && domainUuid != messageSourceUuid) { - qWarning() << "IGNORING DomainList packet from" << messageSourceUuid << "while connected to" << domainUuid; + QUuid domainHandlerUUID = _domainHandler.getUUID(); + QUuid messageSourceUUID = message->getSourceID(); + if (!domainHandlerUUID.isNull() && domainHandlerUUID != messageSourceUUID) { + qWarning() << "IGNORING DomainList packet from" << messageSourceUUID << "while connected to" << domainHandlerUUID; return; } From 6249f3ed12ea4c67238ca53529bb0d480312773d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 16:01:17 -0700 Subject: [PATCH 0998/1237] less changes to hand grab --- .../system/controllers/handControllerGrab.js | 238 ++++++------------ scripts/system/controllers/teleport.js | 3 +- 2 files changed, 77 insertions(+), 164 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index a7a897aa7f..81bd9cba2e 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -49,8 +49,8 @@ var DROP_WITHOUT_SHAKE = false; var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position -var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects var NO_INTERSECT_COLOR = { @@ -81,7 +81,7 @@ var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. -var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand +var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. @@ -221,6 +221,14 @@ function entityHasActions(entityID) { return Entities.getActionIDs(entityID).length > 0; } +function findRayIntersection(pickRay, precise, include, exclude) { + var entities = Entities.findRayIntersection(pickRay, precise, include, exclude); + var overlays = Overlays.findRayIntersection(pickRay); + if (!overlays.intersects || (entities.intersects && (entities.distance <= overlays.distance))) { + return entities; + } + return overlays; +} function entityIsGrabbedByOther(entityID) { // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*. var actionIDs = Entities.getActionIDs(entityID); @@ -251,15 +259,17 @@ function propsArePhysical(props) { // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; - +var EDIT_SETTING = "io.highfidelity.isEditting"; +function isEditing() { + return EXTERNALLY_MANAGED_2D_MINOR_MODE && Settings.getValue(EDIT_SETTING); +} function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, // because other scripts (such as handControllerPointer) might be using some other visualization // instead of setting Reticle.visible. return (EXTERNALLY_MANAGED_2D_MINOR_MODE && - (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); + (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); } - function restore2DMode() { if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { Reticle.setVisible(true); @@ -369,6 +379,7 @@ function MyController(hand) { // for lights this.spotlight = null; this.pointlight = null; + this.overlayLine = null; this.searchSphere = null; this.waitForTriggerRelease = false; @@ -521,8 +532,6 @@ function MyController(hand) { visible: true }; this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); - - } else { Overlays.editOverlay(this.searchSphere, { position: location, @@ -530,12 +539,10 @@ function MyController(hand) { color: color, visible: true }); - } }; this.overlayLineOn = function(closePoint, farPoint, color) { - if (this.overlayLine === null) { var lineProperties = { lineWidth: 5, @@ -561,7 +568,6 @@ function MyController(hand) { alpha: 1 }); } - }; this.searchIndicatorOn = function(distantPickRay) { @@ -576,12 +582,12 @@ function MyController(hand) { } var searchSphereLocation = Vec3.sum(distantPickRay.origin, - Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); + Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); if ((USE_OVERLAY_LINES_FOR_SEARCHING === true) && PICK_WITH_HAND_RAY) { this.overlayLineOn(handPosition, searchSphereLocation, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } }; @@ -764,10 +770,10 @@ function MyController(hand) { }; this.overlayLineOff = function() { - if (_this.overlayLine !== null) { + if (this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); - _this.overlayLine = null; } + this.overlayLine = null; }; this.searchSphereOff = function() { @@ -798,7 +804,7 @@ function MyController(hand) { } }; - this.turnOffVisualizations = function(hand) { + this.turnOffVisualizations = function() { if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); } @@ -809,7 +815,6 @@ function MyController(hand) { if (USE_PARTICLE_BEAM_FOR_MOVING === true) { this.particleBeamOff(); - } this.searchSphereOff(); restore2DMode(); @@ -889,34 +894,18 @@ function MyController(hand) { this.createHotspots = function() { var _this = this; - var HAND_EQUIP_SPHERE_COLOR = { - red: 90, - green: 255, - blue: 90 - }; + var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; var HAND_EQUIP_SPHERE_ALPHA = 0.7; var HAND_EQUIP_SPHERE_RADIUS = 0.01; - var HAND_GRAB_SPHERE_COLOR = { - red: 90, - green: 90, - blue: 255 - }; + var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; var HAND_GRAB_SPHERE_ALPHA = 0.3; var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; - var EQUIP_SPHERE_COLOR = { - red: 90, - green: 255, - blue: 90 - }; + var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; var EQUIP_SPHERE_ALPHA = 0.3; - var GRAB_BOX_COLOR = { - red: 90, - green: 90, - blue: 255 - }; + var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; var GRAB_BOX_ALPHA = 0.1; this.hotspotOverlays = []; @@ -952,16 +941,11 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, type: "hand", - localPosition: { - x: 0, - y: 0, - z: 0 - } + localPosition: {x: 0, y: 0, z: 0} }); } @@ -985,16 +969,11 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, type: "near", - localPosition: { - x: 0, - y: 0, - z: 0 - } + localPosition: {x: 0, y: 0, z: 0} }); } }); @@ -1016,7 +995,6 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - _this.hotspotOverlays.push({ entityID: hotspot.entityID, overlay: overlay, @@ -1031,9 +1009,7 @@ function MyController(hand) { var props; this.hotspotOverlays.forEach(function(overlayInfo) { if (overlayInfo.type === "hand") { - Overlays.editOverlay(overlayInfo.overlay, { - position: _this.getHandPosition() - }); + Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); } else if (overlayInfo.type === "equip") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); @@ -1045,10 +1021,7 @@ function MyController(hand) { } else if (overlayInfo.type === "near") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - Overlays.editOverlay(overlayInfo.overlay, { - position: props.position, - rotation: props.rotation - }); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); } }); }; @@ -1082,8 +1055,8 @@ function MyController(hand) { var pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), length: PICK_MAX_DISTANCE }; @@ -1108,20 +1081,15 @@ function MyController(hand) { var intersection; if (USE_BLACKLIST === true && blacklist.length !== 0) { - intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); + intersection = findRayIntersection(pickRayBacked, true, [], blacklist); } else { - intersection = Entities.findRayIntersection(pickRayBacked, true); - } - - var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); - if (!intersection.intersects || - (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { - intersection = overlayIntersection; + intersection = findRayIntersection(pickRayBacked, true); } if (intersection.intersects) { return { entityID: intersection.entityID, + overlayID: intersection.overlayID, searchRay: pickRay, distance: Vec3.distance(pickRay.origin, intersection.intersection) }; @@ -1168,11 +1136,7 @@ function MyController(hand) { if (wearableProps && wearableProps.joints) { result.push({ entityID: entityID, - localPosition: { - x: 0, - y: 0, - z: 0 - }, + localPosition: {x: 0, y: 0, z: 0}, worldPosition: entityXform.pos, radius: EQUIP_RADIUS, joints: wearableProps.joints @@ -1189,8 +1153,8 @@ function MyController(hand) { var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || - this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedEntity == hotspot.entityID); + this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && + this.getOtherHandController().grabbedEntity == hotspot.entityID); if (refCount > 0 && !okToEquipFromOtherHand) { if (debug) { print("equip is skipping '" + props.name + "': grabbed by someone else"); @@ -1369,6 +1333,8 @@ function MyController(hand) { if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { grabbableEntities.push(rayPickInfo.entityID); } + } else if (rayPickInfo.overlayID) { + this.intersectionDistance = rayPickInfo.distance; } else { this.intersectionDistance = 0; } @@ -1425,7 +1391,7 @@ function MyController(hand) { // TODO: highlight the far-triggerable object? } } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { - if (this.triggerSmoothedGrab()) { + if (this.triggerSmoothedGrab() && !isEditing()) { this.grabbedEntity = entity; this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); return; @@ -1438,8 +1404,8 @@ function MyController(hand) { // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { this.lineOn(rayPickInfo.searchRay.origin, - Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), - NO_INTERSECT_COLOR); + Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), + NO_INTERSECT_COLOR); } this.searchIndicatorOn(rayPickInfo.searchRay); @@ -1530,7 +1496,7 @@ function MyController(hand) { // controller pose is in avatar frame var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); + Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); @@ -1552,7 +1518,7 @@ function MyController(hand) { // scale delta controller hand movement by radius. var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), - radius); + radius); /// double delta controller rotation // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did @@ -1577,7 +1543,7 @@ function MyController(hand) { var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, - Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); + Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + @@ -1586,7 +1552,7 @@ function MyController(hand) { var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * - this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1704,28 +1670,16 @@ function MyController(hand) { if (this.fastHandMoveTimer < 0) { this.fastHandMoveDetected = false; } - var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_REST_TIME = 1; // sec var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { this.fastHandMoveDetected = true; this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; } - var localHandUpAxis = this.hand === RIGHT_HAND ? { - x: 1, - y: 0, - z: 0 - } : { - x: -1, - y: 0, - z: 0 - }; + var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); - var DOWN = { - x: 0, - y: -1, - z: 0 - }; + var DOWN = {x: 0, y: -1, z: 0}; var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); var handIsUpsideDown = false; @@ -1827,16 +1781,8 @@ function MyController(hand) { } Entities.editEntity(this.grabbedEntity, { - velocity: { - x: 0, - y: 0, - z: 0 - }, - angularVelocity: { - x: 0, - y: 0, - z: 0 - }, + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0}, dynamic: false }); @@ -1903,7 +1849,7 @@ function MyController(hand) { if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + - props.parentID + " " + vec3toStr(props.position)); + props.parentID + " " + vec3toStr(props.position)); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); @@ -2005,8 +1951,8 @@ function MyController(hand) { var now = Date.now(); if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { - var intersection = Entities.findRayIntersection(pickRay, true); - if (intersection.accurate) { + var intersection = findRayIntersection(pickRay, true); + if (intersection.accurate || intersection.overlayID) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { this.callEntityMethodOnGrabbed("stopFarTrigger"); @@ -2145,9 +2091,7 @@ function MyController(hand) { // people are holding something and one of them will be able (if the other releases at the right time) to // bootstrap themselves with the held object. This happens because the meaning of "otherAvatar" in // the collision mask hinges on who the physics simulation owner is. - Entities.editEntity(entityID, { - "collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED - }); + Entities.editEntity(entityID, {"collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED}); } } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); @@ -2161,9 +2105,7 @@ function MyController(hand) { var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); children.forEach(function(childID) { print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); - Entities.editEntity(childID, { - parentID: NULL_UUID - }); + Entities.editEntity(childID, {parentID: NULL_UUID}); }); }; @@ -2209,24 +2151,12 @@ function MyController(hand) { data["dynamic"] && data["parentID"] == NULL_UUID && !data["collisionless"]) { - deactiveProps["velocity"] = { - x: 0.0, - y: 0.1, - z: 0.0 - }; + deactiveProps["velocity"] = {x: 0.0, y: 0.1, z: 0.0}; doSetVelocity = false; } if (noVelocity) { - deactiveProps["velocity"] = { - x: 0.0, - y: 0.0, - z: 0.0 - }; - deactiveProps["angularVelocity"] = { - x: 0.0, - y: 0.0, - z: 0.0 - }; + deactiveProps["velocity"] = {x: 0.0, y: 0.0, z: 0.0}; + deactiveProps["angularVelocity"] = {x: 0.0, y: 0.0, z: 0.0}; doSetVelocity = false; } @@ -2239,7 +2169,7 @@ function MyController(hand) { // be fixed. Entities.editEntity(entityID, { velocity: this.currentVelocity - // angularVelocity: this.currentAngularVelocity + // angularVelocity: this.currentAngularVelocity }); } @@ -2249,32 +2179,14 @@ function MyController(hand) { deactiveProps = { parentID: this.previousParentID, parentJointIndex: this.previousParentJointIndex, - velocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - angularVelocity: { - x: 0.0, - y: 0.0, - z: 0.0 - } + velocity: {x: 0.0, y: 0.0, z: 0.0}, + angularVelocity: {x: 0.0, y: 0.0, z: 0.0} }; Entities.editEntity(entityID, deactiveProps); } else if (noVelocity) { - Entities.editEntity(entityID, { - velocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - angularVelocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - dynamic: data["dynamic"] - }); + Entities.editEntity(entityID, {velocity: {x: 0.0, y: 0.0, z: 0.0}, + angularVelocity: {x: 0.0, y: 0.0, z: 0.0}, + dynamic: data["dynamic"]}); } } else { data = null; @@ -2324,24 +2236,24 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); - var handleHandMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { - leftController.turnOffVisualizations('left'); handToDisable = LEFT_HAND; + leftController.turnOffVisualizations(); } if (message === 'right') { - rightController.turnOffVisualizations('right'); handToDisable = RIGHT_HAND; - } - if (message === "both") { - leftController.turnOffVisualizations('left'); - rightController.turnOffVisualizations('right'); + rightController.turnOffVisualizations(); } if (message === 'both' || message === 'none') { + if (message === 'both') { + rightController.turnOffVisualizations(); + leftController.turnOffVisualizations(); + + } handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { @@ -2420,4 +2332,4 @@ function handleMenuItemEvent(menuItem) { } } -Menu.menuItemEvent.connect(handleMenuItemEvent); \ No newline at end of file +Menu.menuItemEvent.connect(handleMenuItemEvent); diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5c881c5fcc..da0b4cb576 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -207,12 +207,13 @@ function Teleporter() { } this.disableMappings(); this.turnOffOverlayBeams(); - this.enableGrab(); + this.updateConnected = null; Script.setTimeout(function() { inTeleportMode = false; + _this.enableGrab(); }, 100); }; From 051616d7c3045d14132ae142453bf3066efc3ad5 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 8 Jul 2016 16:16:06 -0700 Subject: [PATCH 0999/1237] experimenting --- interface/src/avatar/AvatarActionHold.cpp | 2 +- scripts/system/controllers/handControllerGrab.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index c84cfecb40..1babb478b8 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -168,7 +168,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm:: position = palmPosition + rotation * _relativePosition; // update linearVelocity based on offset via _relativePosition; - linearVelocity = linearVelocity + glm::cross(angularVelocity, position - palmPosition); + // linearVelocity = linearVelocity + glm::cross(angularVelocity, position - palmPosition); }); return true; diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 373f203e80..4e4269df28 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -13,7 +13,7 @@ /* global setEntityCustomData, getEntityCustomData, vec3toStr, flatten, Xform */ Script.include("/~/system/libraries/utils.js"); -Script.include("../libraries/Xform.js"); +Script.include("/~/system/libraries/Xform.js"); // // add lines where the hand ray picking is happening From bec71e4af46ad63b0324fbbdc175b887eec254a5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 8 Jul 2016 17:05:23 -0700 Subject: [PATCH 1000/1237] Went back to transparent spheres for equip points --- .../system/controllers/handControllerGrab.js | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index a92c59cfed..39afe122b7 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -939,25 +939,21 @@ function MyController(hand) { overlays: [] }; - overlayInfoSet.overlays.push(Overlays.addOverlay("model", { - url: "http://hifi-content.s3.amazonaws.com/alan/dev/Equip-Spark.fbx", + var EQUIP_SPHERE_COLOR = { red: 179, green: 120, blue: 211 }; + var EQUIP_SPHERE_ALPHA = 0.3; + + overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { position: hotspot.worldPosition, rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: hotspot.radius * 2 * SPARK_MODEL_SCALE_FACTOR, - ignoreRayIntersection: true + size: hotspot.radius * 2, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false })); - // TODO: use shader to achive fresnel effect. - overlayInfoSet.overlays.push(Overlays.addOverlay("model", { - url: "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx", - position: hotspot.worldPosition, - rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: hotspot.radius * 2, - ignoreRayIntersection: true - })); - - // TODO: add light - return overlayInfoSet; }; @@ -976,18 +972,12 @@ function MyController(hand) { var entityXform = new Xform(props.rotation, props.position); var position = entityXform.xformPoint(overlayInfoSet.localPosition); - // spark - Overlays.editOverlay(overlayInfoSet.overlays[0], { - position: position, - rotation: props.rotation, - dimensions: radius * 2 * SPARK_MODEL_SCALE_FACTOR - }); - - // fresnel sphere - Overlays.editOverlay(overlayInfoSet.overlays[1], { - position: position, - rotation: props.rotation, - dimensions: radius * 2 + overlayInfoSet.overlays.forEach(function (overlay) { + Overlays.editOverlay(overlay, { + position: position, + rotation: props.rotation, + dimensions: radius * 2 + }); }); }; From 2fbdb22493982760eccd6c08400f1ef29cd0b142 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 8 Jul 2016 18:14:05 -0700 Subject: [PATCH 1001/1237] Cleaning up the lighting with the simpler approach where emissive and lightmap is displayed on forward --- .../render-utils/src/DebugDeferredBuffer.cpp | 6 +- .../render-utils/src/DeferredBufferRead.slh | 12 +- .../render-utils/src/DeferredBufferWrite.slh | 4 +- .../render-utils/src/DeferredGlobalLight.slh | 66 ++++------ .../render-utils/src/DeferredLighting.slh | 72 ----------- libraries/render-utils/src/LightAmbient.slh | 50 ++++---- .../render-utils/src/LightDirectional.slh | 14 +-- libraries/render-utils/src/LightPoint.slh | 10 +- libraries/render-utils/src/LightSpot.slh | 7 +- libraries/render-utils/src/LightingModel.cpp | 9 ++ libraries/render-utils/src/LightingModel.h | 8 +- libraries/render-utils/src/LightingModel.slh | 31 +++-- .../render-utils/src/ToneMappingEffect.cpp | 21 ++-- .../render-utils/src/ToneMappingEffect.h | 2 +- .../src/directional_ambient_light.slf | 15 +-- .../src/directional_ambient_light_shadow.slf | 13 +- .../render-utils/src/directional_light.slf | 16 +-- .../src/directional_light_shadow.slf | 14 +-- .../src/directional_skybox_light.slf | 14 +-- .../src/directional_skybox_light_shadow.slf | 13 +- .../render-utils/src/model_translucent.slf | 9 +- libraries/render-utils/src/point_light.slf | 8 +- libraries/render-utils/src/spot_light.slf | 8 +- .../utilities/render/debugDeferredLighting.js | 6 +- .../utilities/render/debugFramebuffer.js | 2 +- .../utilities/render/deferredLighting.qml | 114 +++++++++++------- .../utilities/render/framebuffer.qml | 72 +++++------ tools/scribe/src/TextTemplate.cpp | 13 +- tools/scribe/src/main.cpp | 1 + 29 files changed, 272 insertions(+), 358 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index ba7997c60e..2fd42ccacd 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -59,7 +59,7 @@ enum Slot { static const std::string DEFAULT_ALBEDO_SHADER { "vec4 getFragmentColor() {" " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" - " return vec4(pow(frag.diffuse, vec3(1.0 / 2.2)), 1.0);" + " return vec4(pow(frag.albedo, vec3(1.0 / 2.2)), 1.0);" " }" }; @@ -93,7 +93,7 @@ static const std::string DEFAULT_OCCLUSION_SHADER{ static const std::string DEFAULT_EMISSIVE_SHADER{ "vec4 getFragmentColor() {" " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" - " return (frag.mode == FRAG_MODE_SHADED ? vec4(pow(frag.emissive, vec3(1.0 / 2.2)), 1.0) : vec4(vec3(0.0), 1.0));" + " return (frag.mode == FRAG_MODE_SHADED ? vec4(pow(texture(specularMap, uv).rgb, vec3(1.0 / 2.2)), 1.0) : vec4(vec3(0.0), 1.0));" " }" }; @@ -107,7 +107,7 @@ static const std::string DEFAULT_UNLIT_SHADER{ static const std::string DEFAULT_LIGHTMAP_SHADER{ "vec4 getFragmentColor() {" " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" - " return (frag.mode == FRAG_MODE_LIGHTMAPPED ? vec4(pow(frag.emissive, vec3(1.0 / 2.2)), 1.0) : vec4(vec3(0.0), 1.0));" + " return (frag.mode == FRAG_MODE_LIGHTMAPPED ? vec4(pow(texture(specularMap, uv).rgb, vec3(1.0 / 2.2)), 1.0) : vec4(vec3(0.0), 1.0));" " }" }; diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index 6c020b515f..6b5060cc5f 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -36,11 +36,10 @@ struct DeferredFragment { vec4 position; vec3 normal; float metallic; - vec3 diffuse; + vec3 albedo; float obscurance; - vec3 specular; + vec3 fresnel; float roughness; -// vec3 emissive; int mode; float scattering; float depthVal; @@ -63,7 +62,7 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { frag.roughness = normalVal.a; // Diffuse color and unpack the mode and the metallicness - frag.diffuse = diffuseVal.xyz; + frag.albedo = diffuseVal.xyz; frag.scattering = 0.0; unpackModeMetallic(diffuseVal.w, frag.mode, frag.metallic); @@ -73,14 +72,13 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { if (frag.mode == FRAG_MODE_SCATTERING) { frag.scattering = specularVal.x; - //frag.emissive = vec3(0.0); } if (frag.metallic <= 0.5) { frag.metallic = 0.0; - frag.specular = vec3(0.03); // Default Di-electric fresnel value + frag.fresnel = vec3(0.03); // Default Di-electric fresnel value } else { - frag.specular = vec3(diffuseVal.xyz); + frag.fresnel = vec3(diffuseVal.xyz); frag.metallic = 1.0; } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index f1f518eee6..cd62a30611 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -53,7 +53,6 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float r _fragColor0 = vec4(albedo, packLightmappedMetallic(metallic)); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(lightmap, 1.0); - _fragColor3 = vec4(albedo * lightmap, 1.0); } @@ -63,8 +62,7 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { } _fragColor0 = vec4(color, packUnlit()); _fragColor1 = vec4(packNormal(normal), 1.0); - //_fragColor2 = vec4(vec3(0.0), 1.0); // If unlit, do not worry about the emissive color target - + _fragColor2 = vec4(vec3(0.0), 1.0); _fragColor3 = vec4(color, 1.0); } diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index c8bdff0af6..95cfc1d151 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -31,23 +31,12 @@ Light light = getLight(); vec3 color = vec3(0.0); - -<@if isScattering@> -<@else@> - // color += emissive * isEmissiveEnabled(); -<@endif@> -//color += emissive * isEmissiveEnabled(); - vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value - if (metallic > 0.5) { - fresnel = albedo; - metallic = 1.0; - } <@endfunc@> <@func declareEvalAmbientGlobalColor()@> -vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, float roughness) { +vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec3 fresnel, float metallic, float roughness) { <$prepareGlobalLight()$> color += albedo * getLightColor(light) * obscurance * getLightAmbientIntensity(light); return color; @@ -56,21 +45,20 @@ vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obsc <@func declareEvalAmbientSphereGlobalColor(supportScattering)@> -<$declareLightingAmbient(1, 0, 0, supportScattering)$> -<$declareLightingDirectional(supportScattering)$> +<$declareLightingAmbient(1, _SCRIBE_NULL, _SCRIBE_NULL, $supportScattering$)$> +<$declareLightingDirectional($supportScattering$)$> <@if supportScattering@> <$declareDeferredCurvature()$> <@endif@> vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, - vec3 albedo, float metallic, float roughness +vec3 albedo, vec3 fresnel, float metallic, float roughness <@if supportScattering@> , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature -<@endif@> - ) { +<@endif@> ) { - <$prepareGlobalLight(supportScattering)$> + <$prepareGlobalLight($supportScattering$)$> // Ambient vec3 ambientDiffuse; @@ -78,10 +66,9 @@ vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, floa evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance <@if supportScattering@> ,scattering, midNormalCurvature, lowNormalCurvature -<@endif@> - ); - color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); - color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); +<@endif@> ); + color += ambientDiffuse; + color += ambientSpecular; // Directional @@ -90,10 +77,9 @@ vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, floa evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation <@if supportScattering@> ,scattering, midNormalCurvature, lowNormalCurvature -<@endif@> - ); - color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); - color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); +<@endif@> ); + color += directionalDiffuse; + color += directionalSpecular; return color; } @@ -103,31 +89,31 @@ vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, floa <@func declareEvalSkyboxGlobalColor(supportScattering)@> -<$declareLightingAmbient(0, 1, 0, supportScattering)$> -<$declareLightingDirectional(supportScattering)$> +<$declareLightingAmbient(_SCRIBE_NULL, 1, _SCRIBE_NULL, $supportScattering$)$> +<$declareLightingDirectional($supportScattering$)$> <@if supportScattering@> <$declareDeferredCurvature()$> <@endif@> vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, - vec3 albedo, float metallic, float roughness + vec3 albedo, vec3 fresnel, float metallic, float roughness <@if supportScattering@> , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature <@endif@> ) { - <$prepareGlobalLight(supportScattering)$> + <$prepareGlobalLight($supportScattering$)$> // Ambient vec3 ambientDiffuse; vec3 ambientSpecular; evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance <@if supportScattering@> - ,scattering, midNormalCurvature, lowNormalCurvature + ,scattering, midNormalCurvature, lowNormalCurvature <@endif@> ); - color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); - color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled(); + color += ambientDiffuse; + color += ambientSpecular; // Directional @@ -138,8 +124,8 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu ,scattering, midNormalCurvature, lowNormalCurvature <@endif@> ); - color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); - color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); + color += directionalDiffuse; + color += directionalSpecular; return color; } @@ -178,7 +164,7 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur <$declareLightingAmbient(1, 1, 1)$> <$declareLightingDirectional()$> -vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 emissive, float roughness, float opacity) { +vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec3 fresnel, float metallic, vec3 emissive, float roughness, float opacity) { <$prepareGlobalLight()$> color += emissive * isEmissiveEnabled(); @@ -187,16 +173,16 @@ vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, fl vec3 ambientDiffuse; vec3 ambientSpecular; evalLightingAmbient(ambientDiffuse, ambientSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); - color += ambientDiffuse * isDiffuseEnabled() * isAmbientEnabled(); - color += ambientSpecular * isSpecularEnabled() * isAmbientEnabled() / opacity; + color += ambientDiffuse; + color += ambientSpecular / opacity; // Directional vec3 directionalDiffuse; vec3 directionalSpecular; evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); - color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); - color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled() / opacity; + color += directionalDiffuse; + color += directionalSpecular / opacity; return color; } diff --git a/libraries/render-utils/src/DeferredLighting.slh b/libraries/render-utils/src/DeferredLighting.slh index d656b3e054..06d103080e 100755 --- a/libraries/render-utils/src/DeferredLighting.slh +++ b/libraries/render-utils/src/DeferredLighting.slh @@ -7,76 +7,4 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -!> - -<@def DEFERRED_LIGHTING_SLH@> - - -<@func declareEvalPBRShading()@> - -vec3 fresnelSchlick(vec3 fresnelColor, vec3 lightDir, vec3 halfDir) { - return fresnelColor + (1.0 - fresnelColor) * pow(1.0 - clamp(dot(lightDir, halfDir), 0.0, 1.0), 5); -} - -float specularDistribution(float roughness, vec3 normal, vec3 halfDir) { - float ndoth = clamp(dot(halfDir, normal), 0.0, 1.0); - float gloss2 = pow(0.001 + roughness, 4); - float denom = (ndoth * ndoth*(gloss2 - 1) + 1); - float power = gloss2 / (3.14159 * denom * denom); - return power; -} -/* //NOTE: ANother implementation for specularDistribution -float specularDistribution(float roughness, vec3 normal, vec3 halfDir) { - float gloss = exp2(10 * (1.0 - roughness) + 1); - float power = pow(clamp(dot(halfDir, normal), 0.0, 1.0), gloss); - power *= (gloss * 0.125 + 0.25); - return power; -} -*/ - -// Frag Shading returns the diffuse amount as W and the specular rgb as xyz -vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 fresnel, float roughness) { - // Diffuse Lighting - float diffuse = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); - - // Specular Lighting - vec3 halfDir = normalize(fragEyeDir + fragLightDir); - vec3 fresnelColor = fresnelSchlick(fresnel, fragLightDir,halfDir); - float power = specularDistribution(roughness, fragNormal, halfDir); - vec3 specular = power * fresnelColor * diffuse; - - return vec4(specular, (1.0 - metallic) * diffuse * (1 - fresnelColor.x)); -} -<@endfunc@> - -<@func declareEvalBlinnRShading()@> - -vec4 evalBlinnShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, vec3 specular, float roughness) { - // Diffuse Lighting - float diffuseDot = dot(fragNormal, fragLightDir); - float facingLight = step(0.0, diffuseDot); - float diffuse = diffuseDot * facingLight; - - // Specular Lighting depends on the half vector and the roughness - vec3 halfDir = normalize(fragEyeDir + fragLightDir); - - float gloss = (1.0 - roughness) * 128.0; - glos *= gloss; - float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss); - vec3 reflect = specularPower * specular * diffuse; - - return vec4(reflect, diffuse); -} - -<@endfunc@> - -<$declareEvalPBRShading()$> - -// Return xyz the specular/reflection component and w the diffuse component -vec4 evalFragShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float metallic, vec3 specular, float roughness) { - return evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, specular, roughness); -} - -<@endif@> !> \ No newline at end of file diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index e2aa1107df..d40bd921af 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -57,9 +57,9 @@ vec3 evalAmbientSpecularIrradiance(Light light, vec3 fragEyeDir, vec3 fragNormal } <@endfunc@> -<@func declareLightingAmbient(supportAmbientSphere1, supportAmbientMap1, supportIfAmbientMapElseAmbientSphere1, supportScattering)@> +<@func declareLightingAmbient(supportAmbientSphere, supportAmbientMap, supportIfAmbientMapElseAmbientSphere, supportScattering)@> -<$declareEvalAmbientSpecularIrradiance(supportAmbientSphere1, supportAmbientMap1, supportIfAmbientMapElseAmbientSphere1)$> +<$declareEvalAmbientSpecularIrradiance($supportAmbientSphere$, $supportAmbientMap$, $supportIfAmbientMapElseAmbientSphere$)$> <@if supportScattering@> float curvatureAO(in float k) { @@ -74,38 +74,38 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, Light light, vec3 <@endif@> ) { -<@if supportScattering@> + // Diffuse from ambient + diffuse = (1 - metallic) * evalSphericalLight(getLightAmbientSphere(light), normal).xyz; + + // Specular highlight from ambient + specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); + + +<@if supportScattering@> float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; float ambientOcclusionHF = curvatureAO(midNormalCurvature.w * 8.0f) * 0.5f; ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF); - /* if (showCurvature()) { - diffuse = vec3(ambientOcclusion); - specular = vec3(0.0); - return; - }*/ + obscurance = min(obscurance, ambientOcclusion); + if (scattering * isScatteringEnabled() > 0.0) { - // Diffuse from ambient - diffuse = ambientOcclusion * albedo * evalSphericalLight(getLightAmbientSphere(light), lowNormalCurvature.xyz).xyz *getLightAmbientIntensity(light); + // Diffuse from ambient + diffuse = evalSphericalLight(getLightAmbientSphere(light), lowNormalCurvature.xyz).xyz; - // Specular highlight from ambient - // vec3 specularLighting = evalGlobalSpecularIrradiance(light, fragEyeDir, fragNormal, roughness, fresnel); - // color += specularLighting; - - - // Specular highlight from ambient - // specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); - specular = vec3(0.0); - -<@else@> - // Diffuse from ambient - diffuse = (1 - metallic) * albedo * evalSphericalLight(getLightAmbientSphere(light), normal).xyz * obscurance * getLightAmbientIntensity(light); - - // Specular highlight from ambient - specular = evalAmbientSpecularIrradiance(light, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(light); + specular = vec3(0.0); + } <@endif@> + + float lightEnergy = obscurance * getLightAmbientIntensity(light); + + if (isAlbedoEnabled() > 0.0) { + diffuse *= albedo; + } + + diffuse *= lightEnergy * isDiffuseEnabled() * isAmbientEnabled(); + specular *= lightEnergy * isSpecularEnabled() * isAmbientEnabled(); } <@endfunc@> diff --git a/libraries/render-utils/src/LightDirectional.slh b/libraries/render-utils/src/LightDirectional.slh index 6fc27f2250..86eb130491 100644 --- a/libraries/render-utils/src/LightDirectional.slh +++ b/libraries/render-utils/src/LightDirectional.slh @@ -18,18 +18,18 @@ void evalLightingDirectional(out vec3 diffuse, out vec3 specular, Light light, , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature <@endif@> ) { - - evalFragShading(diffuse, specular, normal, -getLightDirection(light), eyeDir, metallic, fresnel, roughness + + // Attenuation + vec3 lightEnergy = shadow * getLightColor(light) * getLightIntensity(light); + + evalFragShading(diffuse, specular, normal, -getLightDirection(light), eyeDir, metallic, fresnel, roughness, albedo <@if supportScattering@> ,scattering, midNormalCurvature, lowNormalCurvature <@endif@> ); - vec3 lightEnergy = shadow * getLightColor(light) * getLightIntensity(light); - - diffuse *= albedo * lightEnergy; - - specular *= lightEnergy; + diffuse *= lightEnergy * isDiffuseEnabled() * isDirectionalEnabled(); + specular *= lightEnergy * isSpecularEnabled() * isDirectionalEnabled(); } <@endfunc@> diff --git a/libraries/render-utils/src/LightPoint.slh b/libraries/render-utils/src/LightPoint.slh index ab63276004..d13d52d360 100644 --- a/libraries/render-utils/src/LightPoint.slh +++ b/libraries/render-utils/src/LightPoint.slh @@ -23,21 +23,19 @@ void evalLightingPoint(out vec3 diffuse, out vec3 specular, Light light, float fragLightDistance = length(fragLightVec); vec3 fragLightDir = fragLightVec / fragLightDistance; - // Eval attenuation + // Eval attenuation float radialAttenuation = evalLightAttenuation(light, fragLightDistance); - vec3 lightEnergy = radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); // Eval shading - evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness + evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness, albedo <@if supportScattering@> ,scattering, midNormalCurvature, lowNormalCurvature <@endif@> ); - diffuse *= albedo * lightEnergy; - - specular *= lightEnergy; + diffuse *= lightEnergy * isDiffuseEnabled() * isPointEnabled(); + specular *= lightEnergy * isSpecularEnabled() * isPointEnabled(); if (getLightShowContour(light) > 0.0) { // Show edge diff --git a/libraries/render-utils/src/LightSpot.slh b/libraries/render-utils/src/LightSpot.slh index 03a7a6120e..0b66a3e4c1 100644 --- a/libraries/render-utils/src/LightSpot.slh +++ b/libraries/render-utils/src/LightSpot.slh @@ -29,15 +29,14 @@ void evalLightingSpot(out vec3 diffuse, out vec3 specular, Light light, vec3 lightEnergy = angularAttenuation * radialAttenuation * shadow * getLightColor(light) * getLightIntensity(light); // Eval shading - evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness + evalFragShading(diffuse, specular, normal, fragLightDir, fragEyeDir, metallic, fresnel, roughness, albedo <@if supportScattering@> ,scattering, midNormalCurvature, lowNormalCurvature <@endif@> ); - diffuse *= albedo * lightEnergy; - - specular *= lightEnergy; + diffuse *= lightEnergy * isDiffuseEnabled() * isSpotEnabled(); + specular *= lightEnergy * isSpecularEnabled() * isSpotEnabled(); if (getLightShowContour(light) > 0.0) { // Show edges diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 9e66a9f673..998659308d 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -75,6 +75,14 @@ void LightingModel::setSpecular(bool enable) { bool LightingModel::isSpecularEnabled() const { return (bool)_parametersBuffer.get().enableSpecular; } +void LightingModel::setAlbedo(bool enable) { + if (enable != isAlbedoEnabled()) { + _parametersBuffer.edit().enableAlbedo = (float)enable; + } +} +bool LightingModel::isAlbedoEnabled() const { + return (bool)_parametersBuffer.get().enableAlbedo; +} void LightingModel::setAmbientLight(bool enable) { if (enable != isAmbientLightEnabled()) { @@ -129,6 +137,7 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setScattering(config.enableScattering); _lightingModel->setDiffuse(config.enableDiffuse); _lightingModel->setSpecular(config.enableSpecular); + _lightingModel->setAlbedo(config.enableAlbedo); _lightingModel->setAmbientLight(config.enableAmbientLight); _lightingModel->setDirectionalLight(config.enableDirectionalLight); _lightingModel->setPointLight(config.enablePointLight); diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index d8325925fe..9bd982a080 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -37,12 +37,14 @@ public: void setScattering(bool enable); bool isScatteringEnabled() const; - void setDiffuse(bool enable); bool isDiffuseEnabled() const; void setSpecular(bool enable); bool isSpecularEnabled() const; + void setAlbedo(bool enable); + bool isAlbedoEnabled() const; + void setAmbientLight(bool enable); bool isAmbientLightEnabled() const; void setDirectionalLight(bool enable); @@ -72,7 +74,7 @@ protected: float enableScattering{ 1.0f }; float enableDiffuse{ 1.0f }; float enableSpecular{ 1.0f }; - float spare; + float enableAlbedo{ 1.0f }; float enableAmbientLight{ 1.0f }; float enableDirectionalLight{ 1.0f }; @@ -103,6 +105,7 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) Q_PROPERTY(bool enableDiffuse MEMBER enableDiffuse NOTIFY dirty) Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty) + Q_PROPERTY(bool enableAlbedo MEMBER enableAlbedo NOTIFY dirty) Q_PROPERTY(bool enableAmbientLight MEMBER enableAmbientLight NOTIFY dirty) Q_PROPERTY(bool enableDirectionalLight MEMBER enableDirectionalLight NOTIFY dirty) @@ -122,6 +125,7 @@ public: bool enableScattering{ true }; bool enableDiffuse{ true }; bool enableSpecular{ true }; + bool enableAlbedo{ true }; bool enableAmbientLight{ true }; bool enableDirectionalLight{ true }; diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 811ceb2412..b8c73f7dab 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -12,10 +12,13 @@ <@def LIGHTING_MODEL_SLH@> <@func declareLightingModel()@> +<@endfunc@> + +<@func declareLightingModelMaster()@> struct LightingModel { vec4 _UnlitShadedEmissiveLightmap; - vec4 _ScatteringDiffuseSpecular; + vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; vec4 _ShowContour; }; @@ -38,13 +41,16 @@ float isLightmapEnabled() { } float isScatteringEnabled() { - return lightingModel._ScatteringDiffuseSpecular.x; + return lightingModel._ScatteringDiffuseSpecularAlbedo.x; } float isDiffuseEnabled() { - return lightingModel._ScatteringDiffuseSpecular.y; + return lightingModel._ScatteringDiffuseSpecularAlbedo.y; } float isSpecularEnabled() { - return lightingModel._ScatteringDiffuseSpecular.z; + return lightingModel._ScatteringDiffuseSpecularAlbedo.z; +} +float isAlbedoEnabled() { + return lightingModel._ScatteringDiffuseSpecularAlbedo.w; } float isAmbientEnabled() { @@ -65,6 +71,7 @@ float isShowContour() { } <@endfunc@> +<$declareLightingModelMaster()$> <@func declareBeckmannSpecular()@> @@ -149,9 +156,12 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float m void evalFragShading(out vec3 diffuse, out vec3 specular, vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, - float metallic, vec3 fresnel, float roughness) { + float metallic, vec3 fresnel, float roughness, vec3 albedo) { vec4 shading = evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); diffuse = vec3(shading.w); + if (isAlbedoEnabled() > 0.0) { + diffuse *= albedo; + } specular = shading.xyz; } @@ -159,11 +169,12 @@ void evalFragShading(out vec3 diffuse, out vec3 specular, <@include SubsurfaceScattering.slh@> <$declareSubsurfaceScatteringBRDF()$> + void evalFragShading(out vec3 diffuse, out vec3 specular, vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, - float metallic, vec3 fresnel, float roughness, + float metallic, vec3 fresnel, float roughness, vec3 albedo, float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature) { - if (scattering > 0.0) { + if (scattering * isScatteringEnabled() > 0.0) { vec3 brdf = evalSkinBRDF(fragLightDir, fragNormal, midNormalCurvature.xyz, lowNormalCurvature.xyz, lowNormalCurvature.w); float NdotL = clamp(dot(fragNormal, fragLightDir), 0.0, 1.0); diffuse = mix(vec3(NdotL), brdf, scattering); @@ -174,12 +185,14 @@ void evalFragShading(out vec3 diffuse, out vec3 specular, diffuse *= specularBrdf.y; specular = vec3(specularBrdf.x); - } else { - vec4 shading = evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, specular, roughness); + vec4 shading = evalPBRShading(fragNormal, fragLightDir, fragEyeDir, metallic, fresnel, roughness); diffuse = vec3(shading.w); specular = shading.xyz; } + if (isAlbedoEnabled() > 0.0) { + diffuse *= albedo; + } } diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index b28f271f9d..6a0020edbf 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -102,12 +102,18 @@ void ToneMappingEffect::init() { } void ToneMappingEffect::setExposure(float exposure) { - _parametersBuffer.edit()._exposure = exposure; - _parametersBuffer.edit()._twoPowExposure = pow(2.0, exposure); + auto& params = _parametersBuffer.get(); + if (params._exposure != exposure) { + _parametersBuffer.edit()._exposure = exposure; + _parametersBuffer.edit()._twoPowExposure = pow(2.0, exposure); + } } void ToneMappingEffect::setToneCurve(ToneCurve curve) { - _parametersBuffer.edit()._toneCurve = curve; + auto& params = _parametersBuffer.get(); + if (params._toneCurve != curve) { + _parametersBuffer.edit()._toneCurve = curve; + } } void ToneMappingEffect::render(RenderArgs* args) { @@ -149,13 +155,8 @@ void ToneMappingEffect::render(RenderArgs* args) { void ToneMappingDeferred::configure(const Config& config) { - if (config.exposure >= 0.0f) { - _toneMappingEffect.setExposure(config.exposure); - } - - if (config.curve >= 0) { - _toneMappingEffect.setToneCurve((ToneMappingEffect::ToneCurve)config.curve); - } + _toneMappingEffect.setExposure(config.exposure); + _toneMappingEffect.setToneCurve((ToneMappingEffect::ToneCurve)config.curve); } void ToneMappingDeferred::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { diff --git a/libraries/render-utils/src/ToneMappingEffect.h b/libraries/render-utils/src/ToneMappingEffect.h index 3dd212c2dc..a6a08ab87b 100644 --- a/libraries/render-utils/src/ToneMappingEffect.h +++ b/libraries/render-utils/src/ToneMappingEffect.h @@ -71,7 +71,7 @@ class ToneMappingConfig : public render::Job::Config { public: ToneMappingConfig() : render::Job::Config(true) {} - void setExposure(float newExposure) { exposure = std::max(0.0f, newExposure); emit dirty(); } + void setExposure(float newExposure) { exposure = newExposure; emit dirty(); } void setCurve(int newCurve) { curve = std::max((int)ToneMappingEffect::None, std::min((int)ToneMappingEffect::Filmic, newCurve)); emit dirty(); } diff --git a/libraries/render-utils/src/directional_ambient_light.slf b/libraries/render-utils/src/directional_ambient_light.slf index 471d12ad3a..5b591cb01f 100755 --- a/libraries/render-utils/src/directional_ambient_light.slf +++ b/libraries/render-utils/src/directional_ambient_light.slf @@ -12,6 +12,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + + <@include DeferredBufferRead.slh@> <@include DeferredGlobalLight.slh@> @@ -30,17 +32,8 @@ void main(void) { float shadowAttenuation = 1.0; if (frag.mode == FRAG_MODE_UNLIT) { - // _fragColor = vec4(frag.diffuse, 1.0); discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - /* vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0);*/ discard; } else { vec4 midNormalCurvature; @@ -55,9 +48,9 @@ void main(void) { frag.obscurance, frag.position.xyz, frag.normal, - frag.diffuse, + frag.albedo, + frag.fresnel, frag.metallic, - // frag.emissive, frag.roughness, frag.scattering, midNormalCurvature, diff --git a/libraries/render-utils/src/directional_ambient_light_shadow.slf b/libraries/render-utils/src/directional_ambient_light_shadow.slf index 5f257dce5c..d3778c9228 100644 --- a/libraries/render-utils/src/directional_ambient_light_shadow.slf +++ b/libraries/render-utils/src/directional_ambient_light_shadow.slf @@ -30,17 +30,8 @@ void main(void) { float shadowAttenuation = evalShadowAttenuation(worldPos); if (frag.mode == FRAG_MODE_UNLIT) { - // _fragColor = vec4(frag.diffuse, 1.0); discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - /* vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0);*/ discard; } else { vec4 midNormalCurvature; @@ -54,9 +45,9 @@ void main(void) { frag.obscurance, frag.position.xyz, frag.normal, - frag.diffuse, + frag.albedo, + frag.fresnel, frag.metallic, - // frag.emissive, frag.roughness, frag.scattering, midNormalCurvature, diff --git a/libraries/render-utils/src/directional_light.slf b/libraries/render-utils/src/directional_light.slf index 7193e34448..cda03b0779 100644 --- a/libraries/render-utils/src/directional_light.slf +++ b/libraries/render-utils/src/directional_light.slf @@ -27,19 +27,9 @@ void main(void) { float shadowAttenuation = 1.0; - // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - // _fragColor = vec4(frag.diffuse, 1.0); - discard; + discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - /* vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0);*/ discard; } else { vec3 color = evalAmbientGlobalColor( @@ -48,9 +38,9 @@ void main(void) { frag.obscurance, frag.position.xyz, frag.normal, - frag.diffuse, + frag.albedo, + frag.fresnel, frag.metallic, - // frag.emissive, frag.roughness); _fragColor = vec4(color, 1.0); } diff --git a/libraries/render-utils/src/directional_light_shadow.slf b/libraries/render-utils/src/directional_light_shadow.slf index 248f90b733..7f98330f84 100644 --- a/libraries/render-utils/src/directional_light_shadow.slf +++ b/libraries/render-utils/src/directional_light_shadow.slf @@ -29,19 +29,9 @@ void main(void) { vec4 worldPos = getViewInverse() * vec4(frag.position.xyz, 1.0); float shadowAttenuation = evalShadowAttenuation(worldPos); - // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - // _fragColor = vec4(frag.diffuse, 1.0); discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - /* vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0);*/ discard; } else { vec3 color = evalAmbientGlobalColor( @@ -50,9 +40,9 @@ void main(void) { frag.obscurance, frag.position.xyz, frag.normal, - frag.diffuse, + frag.albedo, + frag.fresnel, frag.metallic, - // frag.emissive, frag.roughness); _fragColor = vec4(color, 1.0); } diff --git a/libraries/render-utils/src/directional_skybox_light.slf b/libraries/render-utils/src/directional_skybox_light.slf index 01c85672c2..6130c8ac8e 100755 --- a/libraries/render-utils/src/directional_skybox_light.slf +++ b/libraries/render-utils/src/directional_skybox_light.slf @@ -29,17 +29,8 @@ void main(void) { // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - // _fragColor = vec4(frag.diffuse, 1.0); discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - /* vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0);*/ discard; } else { vec4 midNormalCurvature; @@ -53,13 +44,14 @@ void main(void) { frag.obscurance, frag.position.xyz, frag.normal, - frag.diffuse, + frag.albedo, + frag.fresnel, frag.metallic, - // frag.emissive, frag.roughness, frag.scattering, midNormalCurvature, lowNormalCurvature); _fragColor = vec4(color, 1.0); + } } diff --git a/libraries/render-utils/src/directional_skybox_light_shadow.slf b/libraries/render-utils/src/directional_skybox_light_shadow.slf index c87a2d15b2..3ca0f71df5 100644 --- a/libraries/render-utils/src/directional_skybox_light_shadow.slf +++ b/libraries/render-utils/src/directional_skybox_light_shadow.slf @@ -31,17 +31,8 @@ void main(void) { // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { - // _fragColor = vec4(frag.diffuse, 1.0); discard; } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - /* vec3 color = evalLightmappedColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.normal, - frag.diffuse, - frag.specularVal.xyz); - _fragColor = vec4(color, 1.0);*/ discard; } else { vec4 midNormalCurvature; @@ -55,9 +46,9 @@ void main(void) { frag.obscurance, frag.position.xyz, frag.normal, - frag.diffuse, + frag.albedo, + frag.fresnel, frag.metallic, - //frag.emissive, frag.roughness, frag.scattering, midNormalCurvature, diff --git a/libraries/render-utils/src/model_translucent.slf b/libraries/render-utils/src/model_translucent.slf index 8f62a3a3e0..6cf99a68ef 100755 --- a/libraries/render-utils/src/model_translucent.slf +++ b/libraries/render-utils/src/model_translucent.slf @@ -50,11 +50,17 @@ void main(void) { <$evalMaterialRoughness(roughnessTex, roughness, matKey, roughness)$>; float metallic = getMaterialMetallic(mat); + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + if (metallic <= 0.5) { + metallic = 0.0; + } else { + fresnel = albedo; + metallic = 1.0; + } vec3 emissive = getMaterialEmissive(mat); <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; - vec3 fragPosition = _position.xyz; vec3 fragNormal = normalize(_normal); @@ -67,6 +73,7 @@ void main(void) { fragPosition, fragNormal, albedo, + fresnel, metallic, emissive, roughness, opacity), diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 5916c60f22..02a5d0d36c 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -75,9 +75,9 @@ void main(void) { } evalLightingPoint(diffuse, specular, light, fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.specular, frag.diffuse, 1.0, - frag.scattering * isScatteringEnabled(), midNormalCurvature, lowNormalCurvature); + frag.metallic, frag.fresnel, frag.albedo, 1.0, + frag.scattering, midNormalCurvature, lowNormalCurvature); - _fragColor.rgb += diffuse * isDiffuseEnabled() * isPointEnabled(); - _fragColor.rgb += specular * isSpecularEnabled() * isPointEnabled(); + _fragColor.rgb += diffuse; + _fragColor.rgb += specular; } diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index b9b2be3ebf..3b860e3fb3 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -77,10 +77,10 @@ void main(void) { } evalLightingSpot(diffuse, specular, light, fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.specular, frag.diffuse, 1.0, - frag.scattering * isScatteringEnabled(), midNormalCurvature, lowNormalCurvature); + frag.metallic, frag.fresnel, frag.albedo, 1.0, + frag.scattering, midNormalCurvature, lowNormalCurvature); - _fragColor.rgb += diffuse * isDiffuseEnabled() * isSpotEnabled(); - _fragColor.rgb += specular * isSpecularEnabled() * isSpotEnabled(); + _fragColor.rgb += diffuse; + _fragColor.rgb += specular; } diff --git a/scripts/developer/utilities/render/debugDeferredLighting.js b/scripts/developer/utilities/render/debugDeferredLighting.js index 710f08d401..b61ac77c19 100644 --- a/scripts/developer/utilities/render/debugDeferredLighting.js +++ b/scripts/developer/utilities/render/debugDeferredLighting.js @@ -11,10 +11,10 @@ // Set up the qml ui var qml = Script.resolvePath('deferredLighting.qml'); var window = new OverlayWindow({ - title: 'Deferred Lighting Pass', + title: 'Lighting', source: qml, - width: 400, height: 100, + width: 400, height: 150, }); -window.setPosition(250, 800); +window.setPosition(250, 800);a window.closed.connect(function() { Script.stop(); }); diff --git a/scripts/developer/utilities/render/debugFramebuffer.js b/scripts/developer/utilities/render/debugFramebuffer.js index e764cf52d8..12a19085c8 100644 --- a/scripts/developer/utilities/render/debugFramebuffer.js +++ b/scripts/developer/utilities/render/debugFramebuffer.js @@ -19,7 +19,7 @@ var qml = Script.resolvePath('framebuffer.qml'); var window = new OverlayWindow({ title: 'Framebuffer Debug', source: qml, - width: 400, height: 400, + width: 400, height: 50, }); window.setPosition(25, 50); window.closed.connect(function() { Script.stop(); }); diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 5a325ae7ce..372f97bf43 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -11,59 +11,91 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import "configSlider" -Row { +Column { spacing: 8 - Column { - spacing: 10 - Repeater { - model: [ - "Unlit:LightingModel:enableUnlit", - "Shaded:LightingModel:enableShaded", - "Emissive:LightingModel:enableEmissive", - "Lightmap:LightingModel:enableLightmap", - ] - CheckBox { - text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1]) - onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + Row { + spacing: 8 + Column { + spacing: 10 + Repeater { + model: [ + "Unlit:LightingModel:enableUnlit", + "Shaded:LightingModel:enableShaded", + "Emissive:LightingModel:enableEmissive", + "Lightmap:LightingModel:enableLightmap", + ] + CheckBox { + text: modelData.split(":")[0] + checked: Render.getConfig(modelData.split(":")[1]) + onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } + + + Column { + spacing: 10 + Repeater { + model: [ + "Scattering:LightingModel:enableScattering", + "Diffuse:LightingModel:enableDiffuse", + "Specular:LightingModel:enableSpecular", + "Albedo:LightingModel:enableAlbedo", + ] + CheckBox { + text: modelData.split(":")[0] + checked: Render.getConfig(modelData.split(":")[1]) + onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } + + Column { + spacing: 10 + Repeater { + model: [ + "Ambient:LightingModel:enableAmbientLight", + "Directional:LightingModel:enableDirectionalLight", + "Point:LightingModel:enablePointLight", + "Spot:LightingModel:enableSpotLight" + ] + CheckBox { + text: modelData.split(":")[0] + checked: Render.getConfig(modelData.split(":")[1]) + onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } } } } - - Column { - spacing: 10 + spacing: 10 Repeater { - model: [ - "Scattering:LightingModel:enableScattering", - "Diffuse:LightingModel:enableDiffuse", - "Specular:LightingModel:enableSpecular", - ] - CheckBox { - text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1]) - onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + model: [ "Tone Mapping exposure:ToneMapping:exposure:5.0:-5.0" + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: false + config: Render.getConfig(modelData.split(":")[1]) + property: modelData.split(":")[2] + max: modelData.split(":")[3] + min: modelData.split(":")[4] } } - } - Column { - spacing: 10 - Repeater { - model: [ - "Ambient:LightingModel:enableAmbientLight", - "Directional:LightingModel:enableDirectionalLight", - "Point:LightingModel:enablePointLight", - "Spot:LightingModel:enableSpotLight" - ] - CheckBox { - text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1]) - onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + ComboBox { + currentIndex: 1 + model: ListModel { + id: cbItems + ListElement { text: "RGB"; color: "Yellow" } + ListElement { text: "SRGB"; color: "Green" } + ListElement { text: "Reinhard"; color: "Yellow" } + ListElement { text: "Filmic"; color: "White" } } + width: 200 + onCurrentIndexChanged: { Render.getConfig("ToneMapping")["curve"] = currentIndex } } } - } + diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml index 48f1550409..65046106dc 100644 --- a/scripts/developer/utilities/render/framebuffer.qml +++ b/scripts/developer/utilities/render/framebuffer.qml @@ -23,51 +23,35 @@ Column { debug.config.mode = mode; } - function setX(x) { - print(x) - - debug.config.size = Vec4({ x: x, y: -1, z: 1, w: 1 }); - } - Slider { - minimumValue: -1.0 - value: debug.config.size.x - onValueChanged: { - debug.setX( value); - } - } - - ExclusiveGroup { id: bufferGroup } - Repeater { - model: [ - "Off", - "Depth", - "Albedo", - "Normal", - "Roughness", - "Metallic", - "Emissive", - "Unlit", - "Occlusion", - "Lightmap", - "Scattering", - "Lighting", - "Shadow", - "Pyramid Depth", - "Curvature", - "NormalCurvature", - "DiffusedCurvature", - "DiffusedNormalCurvature", - "Debug Scattering", - "Ambient Occlusion", - "Ambient Occlusion Blurred", - "Custom Shader" - ] - RadioButton { - text: qsTr(modelData) - exclusiveGroup: bufferGroup - checked: index == 0 - onCheckedChanged: if (checked) debug.setDebugMode(index - 1); + ComboBox { + currentIndex: 0 + model: ListModel { + id: cbItems + ListElement { text: "Off"; color: "Yellow" } + ListElement { text: "Depth"; color: "Green" } + ListElement { text: "Albedo"; color: "Yellow" } + ListElement { text: "Normal"; color: "White" } + ListElement { text: "Roughness"; color: "White" } + ListElement { text: "Metallic"; color: "White" } + ListElement { text: "Emissive"; color: "White" } + ListElement { text: "Unlit"; color: "White" } + ListElement { text: "Occlusion"; color: "White" } + ListElement { text: "Lightmap"; color: "White" } + ListElement { text: "Scattering"; color: "White" } + ListElement { text: "Lighting"; color: "White" } + ListElement { text: "Shadow"; color: "White" } + ListElement { text: "Linear Depth"; color: "White" } + ListElement { text: "Mid Curvature"; color: "White" } + ListElement { text: "Mid Normal"; color: "White" } + ListElement { text: "Low Curvature"; color: "White" } + ListElement { text: "Low Normal"; color: "White" } + ListElement { text: "Debug Scattering"; color: "White" } + ListElement { text: "Ambient Occlusion"; color: "White" } + ListElement { text: "Ambient Occlusion Blurred"; color: "White" } + ListElement { text: "Custom"; color: "White" } } + width: 200 + onCurrentIndexChanged: { debug.setDebugMode(currentIndex - 1) } } } } diff --git a/tools/scribe/src/TextTemplate.cpp b/tools/scribe/src/TextTemplate.cpp index 741ddb6846..1fad1eac02 100755 --- a/tools/scribe/src/TextTemplate.cpp +++ b/tools/scribe/src/TextTemplate.cpp @@ -741,6 +741,7 @@ int TextTemplate::evalBlockGeneration(std::ostream& dst, const BlockPointer& blo std::vector< String > paramCache; paramCache.push_back(""); String val; + bool valIsVar = false; for (int i = 1; i < nbParams; i++) { val = block->command.arguments[i]; if ((val[0] == Tag::VAR) && (val[val.length()-1] == Tag::VAR)) { @@ -748,7 +749,10 @@ int TextTemplate::evalBlockGeneration(std::ostream& dst, const BlockPointer& blo Vars::iterator it = vars.find(val); if (it != vars.end()) { val = (*it).second; + } else { + val = Tag::NULL_VAR; } + valIsVar = true; } Vars::iterator it = vars.find(funcBlock->command.arguments[i]); @@ -759,14 +763,19 @@ int TextTemplate::evalBlockGeneration(std::ostream& dst, const BlockPointer& blo if (val != Tag::NULL_VAR) { vars.insert(Vars::value_type(funcBlock->command.arguments[i], val)); } - paramCache.push_back(""); + + paramCache.push_back(Tag::NULL_VAR); } } generateTree(dst, funcBlock, vars); for (int i = 1; i < nbParams; i++) { - vars[ funcBlock->command.arguments[i] ] = paramCache[i]; + if (paramCache[i] == Tag::NULL_VAR) { + vars.erase(funcBlock->command.arguments[i]); + } else { + vars[funcBlock->command.arguments[i]] = paramCache[i]; + } } } } diff --git a/tools/scribe/src/main.cpp b/tools/scribe/src/main.cpp index b7038e392e..810f6c0f45 100755 --- a/tools/scribe/src/main.cpp +++ b/tools/scribe/src/main.cpp @@ -240,4 +240,5 @@ int main (int argc, char** argv) { } return 0; + } From d933238bda885202ce8aaf3a009039b970b791b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Brisset?= Date: Fri, 8 Jul 2016 18:15:14 -0700 Subject: [PATCH 1002/1237] Revert "Fix crash in packet list" --- domain-server/src/DomainServer.cpp | 2 -- libraries/networking/src/LimitedNodeList.cpp | 1 - libraries/networking/src/LimitedNodeList.h | 4 ++-- libraries/networking/src/NodeList.cpp | 7 ------- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8b3f09d1f7..d75a2c3245 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -389,8 +389,6 @@ void DomainServer::setupNodeListAndAssignments() { const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant) { nodeList->setSessionUUID(idValueVariant->toString()); - } else { - nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID } connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a03fa43093..d7a2d47fab 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -522,7 +522,6 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret) { - QReadLocker readLocker(&_nodeMutex); NodeHash::const_iterator it = _nodeHash.find(uuid); if (it != _nodeHash.end()) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index c9054ac6d7..483aa0734c 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -131,7 +131,7 @@ public: std::function linkedDataCreateCallback; - size_t size() const { QReadLocker readLock(&_nodeMutex); return _nodeHash.size(); } + size_t size() const { return _nodeHash.size(); } SharedNodePointer nodeWithUUID(const QUuid& nodeUUID); @@ -287,7 +287,7 @@ protected: QUuid _sessionUUID; NodeHash _nodeHash; - mutable QReadWriteLock _nodeMutex; + QReadWriteLock _nodeMutex; udt::Socket _nodeSocket; QUdpSocket* _dtlsSocket; HifiSockAddr _localSockAddr; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 02350ac23c..fd1442d639 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -513,16 +513,9 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { if (_domainHandler.getSockAddr().isNull()) { - qWarning() << "IGNORING DomainList packet while not connected to a Domain Server"; // refuse to process this packet if we aren't currently connected to the DS return; } - QUuid domainHandlerUUID = _domainHandler.getUUID(); - QUuid messageSourceUUID = message->getSourceID(); - if (!domainHandlerUUID.isNull() && domainHandlerUUID != messageSourceUUID) { - qWarning() << "IGNORING DomainList packet from" << messageSourceUUID << "while connected to" << domainHandlerUUID; - return; - } // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; From 6f6fe5f2443467cedd2a7df5a1a2e97af6aca088 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 8 Jul 2016 18:22:37 -0700 Subject: [PATCH 1003/1237] reduced radius of grab sphere --- scripts/system/controllers/handControllerGrab.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 39afe122b7..416e6b10d8 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -45,6 +45,10 @@ var DRAW_GRAB_BOXES = false; var DRAW_HAND_SPHERES = false; var DROP_WITHOUT_SHAKE = false; +var EQUIP_SPHERE_COLOR = { red: 179, green: 120, blue: 211 }; +var EQUIP_SPHERE_ALPHA = 0.15; +var EQUIP_SPHERE_SCALE_FACTOR = 0.65; + // // distant manipulation // @@ -939,13 +943,12 @@ function MyController(hand) { overlays: [] }; - var EQUIP_SPHERE_COLOR = { red: 179, green: 120, blue: 211 }; - var EQUIP_SPHERE_ALPHA = 0.3; + var diameter = hotspot.radius * 2; overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { position: hotspot.worldPosition, rotation: {x: 0, y: 0, z: 0, w: 1}, - size: hotspot.radius * 2, + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, color: EQUIP_SPHERE_COLOR, alpha: EQUIP_SPHERE_ALPHA, solid: true, @@ -960,12 +963,12 @@ function MyController(hand) { this.updateOverlayInfoSet = function (overlayInfoSet, timestamp, potentialEquipHotspot) { overlayInfoSet.timestamp = timestamp; - var radius = overlayInfoSet.hotspot.radius; + var diameter = overlayInfoSet.hotspot.radius * 2; // embiggen the overlays if it maches the potentialEquipHotspot if (potentialEquipHotspot && overlayInfoSet.entityID == potentialEquipHotspot.entityID && Vec3.equal(overlayInfoSet.localPosition, potentialEquipHotspot.localPosition)) { - radius = radius * EQUIP_RADIUS_EMBIGGEN_FACTOR; + diameter = diameter * EQUIP_RADIUS_EMBIGGEN_FACTOR; } var props = _this.entityPropertyCache.getProps(overlayInfoSet.entityID); @@ -976,7 +979,7 @@ function MyController(hand) { Overlays.editOverlay(overlay, { position: position, rotation: props.rotation, - dimensions: radius * 2 + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR }); }); }; From a71baf5601ea269804ac30ba92eb8a148f89833e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 8 Jul 2016 21:18:03 -0700 Subject: [PATCH 1004/1237] handle codec prioritization, handle multiple codec choices --- assignment-client/src/audio/AudioMixer.cpp | 73 ++++++++++++++----- assignment-client/src/audio/AudioMixer.h | 2 + .../resources/describe-settings.json | 8 ++ libraries/audio-client/src/AudioClient.cpp | 9 +-- libraries/audio/src/InboundAudioStream.cpp | 15 +--- libraries/audio/src/InboundAudioStream.h | 2 +- .../audio/src/MixedProcessedAudioStream.cpp | 7 +- .../audio/src/MixedProcessedAudioStream.h | 2 +- libraries/networking/src/udt/BasePacket.cpp | 7 +- plugins/pcmCodec/src/PCMCodecManager.cpp | 51 ++++++++++--- plugins/pcmCodec/src/PCMCodecManager.h | 25 ++++++- plugins/pcmCodec/src/PCMCodecProvider.cpp | 13 +++- 12 files changed, 149 insertions(+), 65 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index b419939912..f49b674dce 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -466,34 +466,61 @@ void saveInputPluginSettings(const InputPluginList& plugins) { void AudioMixer::handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode) { qDebug() << __FUNCTION__; - // read the codecs requested by the client - quint8 numberOfCodecs = 0; - message->readPrimitive(&numberOfCodecs); - QStringList codecList; - for (quint16 i = 0; i < numberOfCodecs; i++) { - QString requestedCodec = message->readString(); - qDebug() << "requestedCodec:" << requestedCodec; - codecList.append(requestedCodec); - } - qDebug() << "all requested codecs:" << codecList; - - CodecPluginPointer selectedCoded; - QString selectedCodecName; + QStringList availableCodecs; auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); if (codecPlugins.size() > 0) { for (auto& plugin : codecPlugins) { - qDebug() << "Codec available:" << plugin->getName(); - - // choose first codec - if (!selectedCoded) { - selectedCoded = plugin; - selectedCodecName = plugin->getName(); - } + auto codecName = plugin->getName(); + qDebug() << "Codec available:" << codecName; + availableCodecs.append(codecName); } } else { qDebug() << "No Codecs available..."; } + CodecPluginPointer selectedCoded; + QString selectedCodecName; + + QStringList codecPreferenceList = _codecPreferenceOrder.split(","); + + // read the codecs requested by the client + const int MAX_PREFERENCE = 99999; + int preferredCodecIndex = MAX_PREFERENCE; + QString preferredCodec; + quint8 numberOfCodecs = 0; + message->readPrimitive(&numberOfCodecs); + qDebug() << "numberOfCodecs:" << numberOfCodecs; + QStringList codecList; + for (quint16 i = 0; i < numberOfCodecs; i++) { + QString requestedCodec = message->readString(); + int preferenceOfThisCodec = codecPreferenceList.indexOf(requestedCodec); + bool codecAvailable = availableCodecs.contains(requestedCodec); + qDebug() << "requestedCodec:" << requestedCodec << "preference:" << preferenceOfThisCodec << "available:" << codecAvailable; + if (codecAvailable) { + codecList.append(requestedCodec); + if (preferenceOfThisCodec >= 0 && preferenceOfThisCodec < preferredCodecIndex) { + qDebug() << "This codec is preferred..."; + selectedCodecName = requestedCodec; + preferredCodecIndex = preferenceOfThisCodec; + } + } + } + qDebug() << "all requested and available codecs:" << codecList; + + // choose first codec + if (!selectedCodecName.isEmpty()) { + if (codecPlugins.size() > 0) { + for (auto& plugin : codecPlugins) { + if (selectedCodecName == plugin->getName()) { + qDebug() << "Selecting codec:" << selectedCodecName; + selectedCoded = plugin; + break; + } + } + } + } + + auto clientData = dynamic_cast(sendingNode->getLinkedData()); // FIXME - why would we not have client data at this point?? @@ -882,6 +909,12 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { if (settingsObject.contains(AUDIO_ENV_GROUP_KEY)) { QJsonObject audioEnvGroupObject = settingsObject[AUDIO_ENV_GROUP_KEY].toObject(); + const QString CODEC_PREFERENCE_ORDER = "codec_preference_order"; + if (audioEnvGroupObject[CODEC_PREFERENCE_ORDER].isString()) { + _codecPreferenceOrder = audioEnvGroupObject[CODEC_PREFERENCE_ORDER].toString(); + qDebug() << "Codec preference order changed to" << _codecPreferenceOrder; + } + const QString ATTENATION_PER_DOULING_IN_DISTANCE = "attenuation_per_doubling_in_distance"; if (audioEnvGroupObject[ATTENATION_PER_DOULING_IN_DISTANCE].isString()) { bool ok = false; diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index c90a918a5b..4b2a27120d 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -92,6 +92,8 @@ private: int _manualEchoMixes { 0 }; int _totalMixes { 0 }; + QString _codecPreferenceOrder; + float _mixedSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _clampedSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 7375a0f650..948c6ddc18 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -718,6 +718,14 @@ "placeholder": "(in percent)" } ] + }, + { + "name": "codec_preference_order", + "label": "Audio Codec Preference Order", + "help": "List of codec names in order of preferred usage", + "placeholder": "hifiAC, zlib, pcm", + "default": "hifiAC,zlib,pcm", + "advanced": true } ] }, diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index a076b1b290..97ef4b2981 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -507,19 +507,14 @@ void AudioClient::handleMuteEnvironmentPacket(QSharedPointer me } void AudioClient::negotiateAudioFormat() { - qDebug() << __FUNCTION__; - auto nodeList = DependencyManager::get(); - auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat); - auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); - quint8 numberOfCodecs = (quint8)codecPlugins.size(); negotiateFormatPacket->writePrimitive(numberOfCodecs); for (auto& plugin : codecPlugins) { - qDebug() << "Codec available:" << plugin->getName(); - negotiateFormatPacket->writeString(plugin->getName()); + auto codecName = plugin->getName(); + negotiateFormatPacket->writeString(codecName); } // grab our audio mixer from the NodeList, if it exists diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 5d4dbd9b94..7c6a34ae55 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -131,7 +131,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { if (message.getType() == PacketType::SilentAudioFrame) { writeDroppableSilentSamples(networkSamples); } else { - parseAudioData(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead()), networkSamples); + parseAudioData(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead())); } break; } @@ -177,25 +177,14 @@ int InboundAudioStream::parseStreamProperties(PacketType type, const QByteArray& } } -int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) { - - // codec decode goes here +int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties) { QByteArray decodedBuffer; if (_codec) { _codec->decode(packetAfterStreamProperties, decodedBuffer); } else { decodedBuffer = packetAfterStreamProperties; } - auto actualSize = decodedBuffer.size(); - - /* - auto expectedSize = numAudioSamples * sizeof(int16_t); - if (expectedSize != actualSize) { - qDebug() << "DECODED SIZE NOT EXPECTED!!!! ----- buffer size:" << actualSize << "expected:" << expectedSize; - } - */ - return _ringBuffer.writeData(decodedBuffer.data(), actualSize); } diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index ddc0dc1dc3..f9ca088fab 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -207,7 +207,7 @@ protected: /// parses the audio data in the network packet. /// default implementation assumes packet contains raw audio samples after stream properties - virtual int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int networkSamples); + virtual int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties); /// writes silent samples to the buffer that may be dropped to reduce latency caused by the buffer virtual int writeDroppableSilentSamples(int silentSamples); diff --git a/libraries/audio/src/MixedProcessedAudioStream.cpp b/libraries/audio/src/MixedProcessedAudioStream.cpp index 220b2bd9ee..6939ee540a 100644 --- a/libraries/audio/src/MixedProcessedAudioStream.cpp +++ b/libraries/audio/src/MixedProcessedAudioStream.cpp @@ -42,9 +42,7 @@ int MixedProcessedAudioStream::writeLastFrameRepeatedWithFade(int samples) { return deviceSamplesWritten; } -int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int networkSamples) { - - // TODO - codec decode goes here +int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties) { QByteArray decodedBuffer; if (_codec) { _codec->decode(packetAfterStreamProperties, decodedBuffer); @@ -52,9 +50,6 @@ int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray& decodedBuffer = packetAfterStreamProperties; } - qDebug() << __FUNCTION__ << "packetAfterStreamProperties:" << packetAfterStreamProperties.size() << "networkSamples:" << networkSamples << "decodedBuffer:" << decodedBuffer.size(); - - emit addedStereoSamples(decodedBuffer); QByteArray outputBuffer; diff --git a/libraries/audio/src/MixedProcessedAudioStream.h b/libraries/audio/src/MixedProcessedAudioStream.h index 5ea0157421..2f9a691278 100644 --- a/libraries/audio/src/MixedProcessedAudioStream.h +++ b/libraries/audio/src/MixedProcessedAudioStream.h @@ -35,7 +35,7 @@ public: protected: int writeDroppableSilentSamples(int silentSamples); int writeLastFrameRepeatedWithFade(int samples); - int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int networkSamples); + int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties); private: int networkToDeviceSamples(int networkSamples); diff --git a/libraries/networking/src/udt/BasePacket.cpp b/libraries/networking/src/udt/BasePacket.cpp index dba241f221..0456e095ae 100644 --- a/libraries/networking/src/udt/BasePacket.cpp +++ b/libraries/networking/src/udt/BasePacket.cpp @@ -152,8 +152,11 @@ QByteArray BasePacket::readWithoutCopy(qint64 maxSize) { qint64 BasePacket::writeString(const QString& string) { QByteArray data = string.toUtf8(); - writePrimitive(static_cast(data.length())); - return writeData(data.constData(), data.length()); + uint32_t length = data.length(); + writePrimitive(length); + auto result = writeData(data.constData(), data.length()); + seek(pos() + length); + return length + sizeof(uint32_t); } QString BasePacket::readString() { diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index 9d55cafedf..f787c6682d 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -1,5 +1,5 @@ // -// PCMCodecManager.cpp +// PCMCodec.cpp // plugins/pcmCodec/src // // Created by Brad Hefta-Gaub on 6/9/2016 @@ -15,35 +15,64 @@ #include "PCMCodecManager.h" -const QString PCMCodecManager::NAME = "zlib"; +const QString PCMCodec::NAME = "pcm"; -void PCMCodecManager::init() { +void PCMCodec::init() { } -void PCMCodecManager::deinit() { +void PCMCodec::deinit() { } -bool PCMCodecManager::activate() { +bool PCMCodec::activate() { CodecPlugin::activate(); return true; } -void PCMCodecManager::deactivate() { +void PCMCodec::deactivate() { CodecPlugin::deactivate(); } -bool PCMCodecManager::isSupported() const { +bool PCMCodec::isSupported() const { return true; } -void PCMCodecManager::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { - //decodedBuffer = encodedBuffer; +void PCMCodec::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { + decodedBuffer = encodedBuffer; +} + +void PCMCodec::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { + encodedBuffer = decodedBuffer; +} + + +const QString zLibCodec::NAME = "zlib"; + +void zLibCodec::init() { +} + +void zLibCodec::deinit() { +} + +bool zLibCodec::activate() { + CodecPlugin::activate(); + return true; +} + +void zLibCodec::deactivate() { + CodecPlugin::deactivate(); +} + + +bool zLibCodec::isSupported() const { + return true; +} + +void zLibCodec::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { decodedBuffer = qUncompress(encodedBuffer); } -void PCMCodecManager::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { - //encodedBuffer = decodedBuffer; +void zLibCodec::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { encodedBuffer = qCompress(decodedBuffer); } diff --git a/plugins/pcmCodec/src/PCMCodecManager.h b/plugins/pcmCodec/src/PCMCodecManager.h index 3b7ca36c02..7816660c5d 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.h +++ b/plugins/pcmCodec/src/PCMCodecManager.h @@ -15,7 +15,7 @@ #include -class PCMCodecManager : public CodecPlugin { +class PCMCodec : public CodecPlugin { Q_OBJECT public: @@ -38,4 +38,27 @@ private: static const QString NAME; }; +class zLibCodec : public CodecPlugin { + Q_OBJECT + +public: + // Plugin functions + bool isSupported() const override; + const QString& getName() const override { return NAME; } + + void init() override; + void deinit() override; + + /// Called when a plugin is being activated for use. May be called multiple times. + bool activate() override; + /// Called when a plugin is no longer being used. May be called multiple times. + void deactivate() override; + + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override; + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override; + +private: + static const QString NAME; +}; + #endif // hifi__PCMCodecManager_h diff --git a/plugins/pcmCodec/src/PCMCodecProvider.cpp b/plugins/pcmCodec/src/PCMCodecProvider.cpp index 732ed2d57d..351b1adf3f 100644 --- a/plugins/pcmCodec/src/PCMCodecProvider.cpp +++ b/plugins/pcmCodec/src/PCMCodecProvider.cpp @@ -29,10 +29,17 @@ public: virtual CodecPluginList getCodecPlugins() override { static std::once_flag once; std::call_once(once, [&] { - CodecPluginPointer plugin(new PCMCodecManager()); - if (plugin->isSupported()) { - _codecPlugins.push_back(plugin); + + CodecPluginPointer pcmCodec(new PCMCodec()); + if (pcmCodec->isSupported()) { + _codecPlugins.push_back(pcmCodec); } + + CodecPluginPointer zlibCodec(new zLibCodec()); + if (zlibCodec->isSupported()) { + _codecPlugins.push_back(zlibCodec); + } + }); return _codecPlugins; } From 31948bce2d4a2a871dce7480e92cfd16db7b1063 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 8 Jul 2016 16:35:03 -0700 Subject: [PATCH 1005/1237] Support alpha and color gradients in circle overlays --- interface/src/ui/overlays/Circle3DOverlay.cpp | 313 +++++++++--------- interface/src/ui/overlays/Circle3DOverlay.h | 48 +-- libraries/render-utils/src/GeometryCache.cpp | 52 ++- libraries/render-utils/src/GeometryCache.h | 4 +- 4 files changed, 219 insertions(+), 198 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 2896ce711e..22995cbc35 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -17,28 +17,7 @@ QString const Circle3DOverlay::TYPE = "circle3d"; -Circle3DOverlay::Circle3DOverlay() : - _startAt(0.0f), - _endAt(360.0f), - _outerRadius(1.0f), - _innerRadius(0.0f), - _hasTickMarks(false), - _majorTickMarksAngle(0.0f), - _minorTickMarksAngle(0.0f), - _majorTickMarksLength(0.0f), - _minorTickMarksLength(0.0f), - _quadVerticesID(GeometryCache::UNKNOWN_ID), - _lineVerticesID(GeometryCache::UNKNOWN_ID), - _majorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _minorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _lastStartAt(-1.0f), - _lastEndAt(-1.0f), - _lastOuterRadius(-1.0f), - _lastInnerRadius(-1.0f) -{ - _majorTickMarksColor.red = _majorTickMarksColor.green = _majorTickMarksColor.blue = (unsigned char)0; - _minorTickMarksColor.red = _minorTickMarksColor.green = _minorTickMarksColor.blue = (unsigned char)0; -} +Circle3DOverlay::Circle3DOverlay() { } Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : Planar3DOverlay(circle3DOverlay), @@ -56,11 +35,7 @@ Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : _quadVerticesID(GeometryCache::UNKNOWN_ID), _lineVerticesID(GeometryCache::UNKNOWN_ID), _majorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _minorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _lastStartAt(-1.0f), - _lastEndAt(-1.0f), - _lastOuterRadius(-1.0f), - _lastInnerRadius(-1.0f) + _minorTicksVerticesID(GeometryCache::UNKNOWN_ID) { } @@ -70,36 +45,25 @@ void Circle3DOverlay::render(RenderArgs* args) { } float alpha = getAlpha(); - if (alpha == 0.0f) { return; // do nothing if our alpha is 0, we're not visible } - // Create the circle in the coordinates origin - float outerRadius = getOuterRadius(); - float innerRadius = getInnerRadius(); // only used in solid case - float startAt = getStartAt(); - float endAt = getEndAt(); - - bool geometryChanged = (startAt != _lastStartAt || endAt != _lastEndAt || - innerRadius != _lastInnerRadius || outerRadius != _lastOuterRadius); - + bool geometryChanged = _dirty; + _dirty = false; const float FULL_CIRCLE = 360.0f; const float SLICES = 180.0f; // The amount of segment to create the circle const float SLICE_ANGLE = FULL_CIRCLE / SLICES; - - xColor colorX = getColor(); const float MAX_COLOR = 255.0f; - glm::vec4 color(colorX.red / MAX_COLOR, colorX.green / MAX_COLOR, colorX.blue / MAX_COLOR, alpha); - - bool colorChanged = colorX.red != _lastColor.red || colorX.green != _lastColor.green || colorX.blue != _lastColor.blue; - _lastColor = colorX; auto geometryCache = DependencyManager::get(); Q_ASSERT(args->_batch); auto& batch = *args->_batch; + if (args->_pipeline) { + batch.setPipeline(args->_pipeline->pipeline); + } // FIXME: THe line width of _lineWidth is not supported anymore, we ll need a workaround @@ -110,81 +74,89 @@ void Circle3DOverlay::render(RenderArgs* args) { // for our overlay, is solid means we draw a ring between the inner and outer radius of the circle, otherwise // we just draw a line... if (getIsSolid()) { - if (_quadVerticesID == GeometryCache::UNKNOWN_ID) { + if (!_quadVerticesID) { _quadVerticesID = geometryCache->allocateID(); } - if (geometryChanged || colorChanged) { - + if (geometryChanged) { QVector points; - - float angle = startAt; - float angleInRadians = glm::radians(angle); - glm::vec2 mostRecentInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); - glm::vec2 mostRecentOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); - - while (angle < endAt) { - angleInRadians = glm::radians(angle); - glm::vec2 thisInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); - glm::vec2 thisOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); - - points << mostRecentInnerPoint << mostRecentOuterPoint << thisOuterPoint; // first triangle - points << mostRecentInnerPoint << thisInnerPoint << thisOuterPoint; // second triangle - - angle += SLICE_ANGLE; + QVector colors; - mostRecentInnerPoint = thisInnerPoint; - mostRecentOuterPoint = thisOuterPoint; + float pulseLevel = updatePulse(); + vec4 pulseModifier = vec4(1); + if (_alphaPulse != 0.0) { + pulseModifier.a = (_alphaPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); } - - // get the last slice portion.... - angle = endAt; - angleInRadians = glm::radians(angle); - glm::vec2 lastInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); - glm::vec2 lastOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + if (_colorPulse != 0.0) { + float pulseValue = (_colorPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); + pulseModifier = vec4(vec3(pulseValue), pulseModifier.a); + } + vec4 innerStartColor = vec4(toGlm(_innerStartColor), _innerStartAlpha) * pulseModifier; + vec4 outerStartColor = vec4(toGlm(_outerStartColor), _outerStartAlpha) * pulseModifier; + vec4 innerEndColor = vec4(toGlm(_innerEndColor), _innerEndAlpha) * pulseModifier; + vec4 outerEndColor = vec4(toGlm(_outerEndColor), _outerEndAlpha) * pulseModifier; - points << mostRecentInnerPoint << mostRecentOuterPoint << lastOuterPoint; // first triangle - points << mostRecentInnerPoint << lastInnerPoint << lastOuterPoint; // second triangle - - geometryCache->updateVertices(_quadVerticesID, points, color); + if (_innerRadius <= 0) { + _solidPrimitive = gpu::TRIANGLE_FAN; + points << vec2(); + colors << innerStartColor; + for (float angle = _startAt; angle <= _endAt; angle += SLICE_ANGLE) { + float range = (angle - _startAt) / (_endAt - _startAt); + float angleRadians = glm::radians(angle); + points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + colors << glm::mix(outerStartColor, outerEndColor, range); + } + } else { + _solidPrimitive = gpu::TRIANGLE_STRIP; + for (float angle = _startAt; angle <= _endAt; angle += SLICE_ANGLE) { + float range = (angle - _startAt) / (_endAt - _startAt); + + float angleRadians = glm::radians(angle); + points << glm::vec2(cos(angleRadians) * _innerRadius, sin(angleRadians) * _innerRadius); + colors << glm::mix(innerStartColor, innerEndColor, range); + + points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + colors << glm::mix(outerStartColor, outerEndColor, range); + } + } + geometryCache->updateVertices(_quadVerticesID, points, colors); } - geometryCache->renderVertices(batch, gpu::TRIANGLES, _quadVerticesID); + geometryCache->renderVertices(batch, _solidPrimitive, _quadVerticesID); } else { - if (_lineVerticesID == GeometryCache::UNKNOWN_ID) { + if (!_lineVerticesID) { _lineVerticesID = geometryCache->allocateID(); } - if (geometryChanged || colorChanged) { + if (geometryChanged) { QVector points; - float angle = startAt; + float angle = _startAt; float angleInRadians = glm::radians(angle); - glm::vec2 firstPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 firstPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << firstPoint; - while (angle < endAt) { + while (angle < _endAt) { angle += SLICE_ANGLE; angleInRadians = glm::radians(angle); - glm::vec2 thisPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 thisPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << thisPoint; if (getIsDashedLine()) { angle += SLICE_ANGLE / 2.0f; // short gap angleInRadians = glm::radians(angle); - glm::vec2 dashStartPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 dashStartPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << dashStartPoint; } } // get the last slice portion.... - angle = endAt; + angle = _endAt; angleInRadians = glm::radians(angle); - glm::vec2 lastPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 lastPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << lastPoint; - - geometryCache->updateVertices(_lineVerticesID, points, color); + geometryCache->updateVertices(_lineVerticesID, points, vec4(toGlm(getColor()), getAlpha())); } if (getIsDashedLine()) { @@ -214,13 +186,13 @@ void Circle3DOverlay::render(RenderArgs* args) { if (getMajorTickMarksAngle() > 0.0f && getMajorTickMarksLength() != 0.0f) { float tickMarkAngle = getMajorTickMarksAngle(); - float angle = startAt - fmodf(startAt, tickMarkAngle) + tickMarkAngle; + float angle = _startAt - fmodf(_startAt, tickMarkAngle) + tickMarkAngle; float angleInRadians = glm::radians(angle); float tickMarkLength = getMajorTickMarksLength(); - float startRadius = (tickMarkLength > 0.0f) ? innerRadius : outerRadius; + float startRadius = (tickMarkLength > 0.0f) ? _innerRadius : _outerRadius; float endRadius = startRadius + tickMarkLength; - while (angle <= endAt) { + while (angle <= _endAt) { angleInRadians = glm::radians(angle); glm::vec2 thisPointA(cosf(angleInRadians) * startRadius, sinf(angleInRadians) * startRadius); @@ -236,13 +208,13 @@ void Circle3DOverlay::render(RenderArgs* args) { if (getMinorTickMarksAngle() > 0.0f && getMinorTickMarksLength() != 0.0f) { float tickMarkAngle = getMinorTickMarksAngle(); - float angle = startAt - fmodf(startAt, tickMarkAngle) + tickMarkAngle; + float angle = _startAt - fmodf(_startAt, tickMarkAngle) + tickMarkAngle; float angleInRadians = glm::radians(angle); float tickMarkLength = getMinorTickMarksLength(); - float startRadius = (tickMarkLength > 0.0f) ? innerRadius : outerRadius; + float startRadius = (tickMarkLength > 0.0f) ? _innerRadius : _outerRadius; float endRadius = startRadius + tickMarkLength; - while (angle <= endAt) { + while (angle <= _endAt) { angleInRadians = glm::radians(angle); glm::vec2 thisPointA(cosf(angleInRadians) * startRadius, sinf(angleInRadians) * startRadius); @@ -269,17 +241,10 @@ void Circle3DOverlay::render(RenderArgs* args) { geometryCache->renderVertices(batch, gpu::LINES, _minorTicksVerticesID); } - - if (geometryChanged) { - _lastStartAt = startAt; - _lastEndAt = endAt; - _lastInnerRadius = innerRadius; - _lastOuterRadius = outerRadius; - } } const render::ShapeKey Circle3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace(); + auto builder = render::ShapeKey::Builder().withoutCullFace().withUnlit(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } @@ -289,72 +254,102 @@ const render::ShapeKey Circle3DOverlay::getShapeKey() { return builder.build(); } +template T fromVariant(const QVariant& v, bool& valid) { + valid = v.isValid(); + return qvariant_cast(v); +} + +template<> xColor fromVariant(const QVariant& v, bool& valid) { + return xColorFromVariant(v, valid); +} + +template +bool updateIfValid(const QVariantMap& properties, const char* key, T& output) { + bool valid; + T result = fromVariant(properties[key], valid); + if (!valid) { + return false; + } + + // Don't signal updates if the value was already set + if (result == output) { + return false; + } + + output = result; + return true; +} + +// Multicast, many outputs +template +bool updateIfValid(const QVariantMap& properties, const char* key, std::initializer_list> outputs) { + bool valid; + T value = fromVariant(properties[key], valid); + if (!valid) { + return false; + } + bool updated = false; + for (T& output : outputs) { + if (output != value) { + output = value; + updated = true; + } + } + return updated; +} + +// Multicast, multiple possible inputs, in order of preference +template +bool updateIfValid(const QVariantMap& properties, const std::initializer_list keys, T& output) { + for (const char* key : keys) { + if (updateIfValid(properties, key, output)) { + return true; + } + } + return false; +} + + void Circle3DOverlay::setProperties(const QVariantMap& properties) { Planar3DOverlay::setProperties(properties); + _dirty |= updateIfValid(properties, "alpha", { _innerStartAlpha, _innerEndAlpha, _outerStartAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "Alpha", { _innerStartAlpha, _innerEndAlpha, _outerStartAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "startAlpha", { _innerStartAlpha, _outerStartAlpha }); + _dirty |= updateIfValid(properties, "endAlpha", { _innerEndAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "innerAlpha", { _innerStartAlpha, _innerEndAlpha }); + _dirty |= updateIfValid(properties, "outerAlpha", { _outerStartAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "innerStartAlpha", _innerStartAlpha); + _dirty |= updateIfValid(properties, "innerEndAlpha", _innerEndAlpha); + _dirty |= updateIfValid(properties, "outerStartAlpha", _outerStartAlpha); + _dirty |= updateIfValid(properties, "outerEndAlpha", _outerEndAlpha); - QVariant startAt = properties["startAt"]; - if (startAt.isValid()) { - setStartAt(startAt.toFloat()); - } + _dirty |= updateIfValid(properties, "color", { _innerStartColor, _innerEndColor, _outerStartColor, _outerEndColor }); + _dirty |= updateIfValid(properties, "startColor", { _innerStartColor, _outerStartColor } ); + _dirty |= updateIfValid(properties, "endColor", { _innerEndColor, _outerEndColor } ); + _dirty |= updateIfValid(properties, "innerColor", { _innerStartColor, _innerEndColor } ); + _dirty |= updateIfValid(properties, "outerColor", { _outerStartColor, _outerEndColor } ); + _dirty |= updateIfValid(properties, "innerStartColor", _innerStartColor); + _dirty |= updateIfValid(properties, "innerEndColor", _innerEndColor); + _dirty |= updateIfValid(properties, "outerStartColor", _outerStartColor); + _dirty |= updateIfValid(properties, "outerEndColor", _outerEndColor); - QVariant endAt = properties["endAt"]; - if (endAt.isValid()) { - setEndAt(endAt.toFloat()); - } + _dirty |= updateIfValid(properties, "startAt", _startAt); + _dirty |= updateIfValid(properties, "endAt", _endAt); - QVariant outerRadius = properties["radius"]; - if (!outerRadius.isValid()) { - outerRadius = properties["outerRadius"]; - } - if (outerRadius.isValid()) { - setOuterRadius(outerRadius.toFloat()); - } + _dirty |= updateIfValid(properties, { "radius", "outerRadius" }, _outerRadius); + _dirty |= updateIfValid(properties, "innerRadius", _innerRadius); + _dirty |= updateIfValid(properties, "hasTickMarks", _hasTickMarks); + _dirty |= updateIfValid(properties, "majorTickMarksAngle", _majorTickMarksAngle); + _dirty |= updateIfValid(properties, "minorTickMarksAngle", _minorTickMarksAngle); + _dirty |= updateIfValid(properties, "majorTickMarksLength", _majorTickMarksLength); + _dirty |= updateIfValid(properties, "minorTickMarksLength", _minorTickMarksLength); + _dirty |= updateIfValid(properties, "majorTickMarksColor", _majorTickMarksColor); + _dirty |= updateIfValid(properties, "minorTickMarksColor", _minorTickMarksColor); - QVariant innerRadius = properties["innerRadius"]; - if (innerRadius.isValid()) { - setInnerRadius(innerRadius.toFloat()); - } - - QVariant hasTickMarks = properties["hasTickMarks"]; - if (hasTickMarks.isValid()) { - setHasTickMarks(hasTickMarks.toBool()); - } - - QVariant majorTickMarksAngle = properties["majorTickMarksAngle"]; - if (majorTickMarksAngle.isValid()) { - setMajorTickMarksAngle(majorTickMarksAngle.toFloat()); - } - - QVariant minorTickMarksAngle = properties["minorTickMarksAngle"]; - if (minorTickMarksAngle.isValid()) { - setMinorTickMarksAngle(minorTickMarksAngle.toFloat()); - } - - QVariant majorTickMarksLength = properties["majorTickMarksLength"]; - if (majorTickMarksLength.isValid()) { - setMajorTickMarksLength(majorTickMarksLength.toFloat()); - } - - QVariant minorTickMarksLength = properties["minorTickMarksLength"]; - if (minorTickMarksLength.isValid()) { - setMinorTickMarksLength(minorTickMarksLength.toFloat()); - } - - bool valid; - auto majorTickMarksColor = properties["majorTickMarksColor"]; - if (majorTickMarksColor.isValid()) { - auto color = xColorFromVariant(majorTickMarksColor, valid); - if (valid) { - _majorTickMarksColor = color; - } - } - - auto minorTickMarksColor = properties["minorTickMarksColor"]; - if (minorTickMarksColor.isValid()) { - auto color = xColorFromVariant(majorTickMarksColor, valid); - if (valid) { - _minorTickMarksColor = color; - } + if (_innerStartAlpha < 1.0f || _innerEndAlpha < 1.0f || _outerStartAlpha < 1.0f || _outerEndAlpha < 1.0f) { + // Force the alpha to 0.5, since we'll ignore it in the presence of these other values, but we need + // it to be non-1 in order to get the right pipeline and non-0 in order to render at all. + _alpha = 0.5f; } } diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index c0e84ef1c6..39f1ade7e9 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -59,28 +59,34 @@ public: virtual Circle3DOverlay* createClone() const override; protected: - float _startAt; - float _endAt; - float _outerRadius; - float _innerRadius; - bool _hasTickMarks; - float _majorTickMarksAngle; - float _minorTickMarksAngle; - float _majorTickMarksLength; - float _minorTickMarksLength; - xColor _majorTickMarksColor; - xColor _minorTickMarksColor; - - int _quadVerticesID; - int _lineVerticesID; - int _majorTicksVerticesID; - int _minorTicksVerticesID; + float _startAt { 0 }; + float _endAt { 360 }; + float _outerRadius { 1 }; + float _innerRadius { 0 }; - xColor _lastColor; - float _lastStartAt; - float _lastEndAt; - float _lastOuterRadius; - float _lastInnerRadius; + xColor _innerStartColor; + xColor _innerEndColor; + xColor _outerStartColor; + xColor _outerEndColor; + float _innerStartAlpha; + float _innerEndAlpha; + float _outerStartAlpha; + float _outerEndAlpha; + + bool _hasTickMarks { false }; + float _majorTickMarksAngle { 0 }; + float _minorTickMarksAngle { 0 }; + float _majorTickMarksLength { 0 }; + float _minorTickMarksLength { 0 }; + xColor _majorTickMarksColor {}; + xColor _minorTickMarksColor {}; + gpu::Primitive _solidPrimitive { gpu::TRIANGLE_FAN }; + int _quadVerticesID { 0 }; + int _lineVerticesID { 0 }; + int _majorTicksVerticesID { 0 }; + int _minorTicksVerticesID { 0 }; + + bool _dirty { true }; }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index fd06272fa9..8be1c478be 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -432,7 +432,7 @@ void GeometryCache::renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, co renderQuad(batch, minCorner, maxCorner, MIN_TEX_COORD, MAX_TEX_COORD, color, id); } -void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { +void GeometryCache::updateVertices(int id, const QVector& points, const QVector& colors) { BatchItemDetails& details = _registeredVertices[id]; if (details.isCreated) { @@ -469,11 +469,6 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.vertices = points.size(); details.vertexSize = FLOATS_PER_VERTEX; - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX]; float* vertex = vertexData; @@ -481,16 +476,25 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorDataAt = colorData; const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - foreach(const glm::vec2& point, points) { + auto pointCount = points.size(); + auto colorCount = colors.size(); + int compactColor; + for (auto i = 0; i < pointCount; ++i) { + const auto& point = points[i]; *(vertex++) = point.x; *(vertex++) = point.y; *(vertex++) = NORMAL.x; *(vertex++) = NORMAL.y; *(vertex++) = NORMAL.z; - + if (i < colorCount) { + const auto& color = colors[i]; + compactColor = ((int(color.x * 255.0f) & 0xFF)) | + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); + } *(colorDataAt++) = compactColor; } - details.verticesBuffer->append(sizeof(float) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*) vertexData); details.colorBuffer->append(sizeof(int) * details.vertices, (gpu::Byte*) colorData); delete[] vertexData; @@ -501,7 +505,11 @@ void GeometryCache::updateVertices(int id, const QVector& points, con #endif } -void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { +void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { + updateVertices(id, points, QVector({ color })); +} + +void GeometryCache::updateVertices(int id, const QVector& points, const QVector& colors) { BatchItemDetails& details = _registeredVertices[id]; if (details.isCreated) { details.clear(); @@ -537,11 +545,8 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.vertices = points.size(); details.vertexSize = FLOATS_PER_VERTEX; - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - + // Default to white + int compactColor = 0xFFFFFFFF; float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX]; float* vertex = vertexData; @@ -549,14 +554,23 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorDataAt = colorData; const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - foreach(const glm::vec3& point, points) { + auto pointCount = points.size(); + auto colorCount = colors.size(); + for (auto i = 0; i < pointCount; ++i) { + const glm::vec3& point = points[i]; + if (i < colorCount) { + const glm::vec4& color = colors[i]; + compactColor = ((int(color.x * 255.0f) & 0xFF)) | + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); + } *(vertex++) = point.x; *(vertex++) = point.y; *(vertex++) = point.z; *(vertex++) = NORMAL.x; *(vertex++) = NORMAL.y; *(vertex++) = NORMAL.z; - *(colorDataAt++) = compactColor; } @@ -570,6 +584,10 @@ void GeometryCache::updateVertices(int id, const QVector& points, con #endif } +void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { + updateVertices(id, points, QVector({ color })); +} + void GeometryCache::updateVertices(int id, const QVector& points, const QVector& texCoords, const glm::vec4& color) { BatchItemDetails& details = _registeredVertices[id]; diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 647fa9889c..1bfdc1798e 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -283,7 +283,9 @@ public: const glm::vec4& color1, const glm::vec4& color2, int id = UNKNOWN_ID); void updateVertices(int id, const QVector& points, const glm::vec4& color); + void updateVertices(int id, const QVector& points, const QVector& colors); void updateVertices(int id, const QVector& points, const glm::vec4& color); + void updateVertices(int id, const QVector& points, const QVector& colors); void updateVertices(int id, const QVector& points, const QVector& texCoords, const glm::vec4& color); void renderVertices(gpu::Batch& batch, gpu::Primitive primitiveType, int id); @@ -360,7 +362,7 @@ private: QHash _coneVBOs; - int _nextID{ 0 }; + int _nextID{ 1 }; QHash _lastRegisteredQuad3DTexture; QHash _quad3DTextures; From 2c72037e81872d61e055218aadb5773b908d5f2b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 8 Jul 2016 18:35:50 -0700 Subject: [PATCH 1006/1237] Fixing warnings --- interface/src/ui/overlays/Circle3DOverlay.cpp | 16 +++++++++------- interface/src/ui/overlays/Circle3DOverlay.h | 4 ++-- libraries/render-utils/src/GeometryCache.cpp | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 22995cbc35..6ebfd5c71c 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -14,10 +14,12 @@ #include #include - QString const Circle3DOverlay::TYPE = "circle3d"; -Circle3DOverlay::Circle3DOverlay() { } +Circle3DOverlay::Circle3DOverlay() { + memset(&_minorTickMarksColor, 0, sizeof(_minorTickMarksColor)); + memset(&_majorTickMarksColor, 0, sizeof(_majorTickMarksColor)); +} Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : Planar3DOverlay(circle3DOverlay), @@ -84,10 +86,10 @@ void Circle3DOverlay::render(RenderArgs* args) { float pulseLevel = updatePulse(); vec4 pulseModifier = vec4(1); - if (_alphaPulse != 0.0) { + if (_alphaPulse != 0.0f) { pulseModifier.a = (_alphaPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); } - if (_colorPulse != 0.0) { + if (_colorPulse != 0.0f) { float pulseValue = (_colorPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); pulseModifier = vec4(vec3(pulseValue), pulseModifier.a); } @@ -103,7 +105,7 @@ void Circle3DOverlay::render(RenderArgs* args) { for (float angle = _startAt; angle <= _endAt; angle += SLICE_ANGLE) { float range = (angle - _startAt) / (_endAt - _startAt); float angleRadians = glm::radians(angle); - points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + points << glm::vec2(cosf(angleRadians) * _outerRadius, sinf(angleRadians) * _outerRadius); colors << glm::mix(outerStartColor, outerEndColor, range); } } else { @@ -112,10 +114,10 @@ void Circle3DOverlay::render(RenderArgs* args) { float range = (angle - _startAt) / (_endAt - _startAt); float angleRadians = glm::radians(angle); - points << glm::vec2(cos(angleRadians) * _innerRadius, sin(angleRadians) * _innerRadius); + points << glm::vec2(cosf(angleRadians) * _innerRadius, sinf(angleRadians) * _innerRadius); colors << glm::mix(innerStartColor, innerEndColor, range); - points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + points << glm::vec2(cosf(angleRadians) * _outerRadius, sinf(angleRadians) * _outerRadius); colors << glm::mix(outerStartColor, outerEndColor, range); } } diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index 39f1ade7e9..82c7c47dc7 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -78,8 +78,8 @@ protected: float _minorTickMarksAngle { 0 }; float _majorTickMarksLength { 0 }; float _minorTickMarksLength { 0 }; - xColor _majorTickMarksColor {}; - xColor _minorTickMarksColor {}; + xColor _majorTickMarksColor; + xColor _minorTickMarksColor; gpu::Primitive _solidPrimitive { gpu::TRIANGLE_FAN }; int _quadVerticesID { 0 }; int _lineVerticesID { 0 }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 8be1c478be..4558b68af9 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -478,7 +478,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); auto pointCount = points.size(); auto colorCount = colors.size(); - int compactColor; + int compactColor = 0; for (auto i = 0; i < pointCount; ++i) { const auto& point = points[i]; *(vertex++) = point.x; From 207ddcea3863805597f066c23efe32192fcfa02d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 9 Jul 2016 22:25:28 -0700 Subject: [PATCH 1007/1237] wrap hull about each mesh part --- .../src/RenderableModelEntityItem.cpp | 84 ++++++++++++++++--- libraries/physics/src/ShapeFactory.cpp | 58 ++++++++++++- libraries/shared/src/ShapeInfo.h | 6 +- 3 files changed, 133 insertions(+), 15 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 416a38d27c..9f4ff63548 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -10,6 +10,7 @@ // #include +#include #include #include @@ -690,8 +691,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 scaleToFit = dimensions / _model->getFBXGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. - for (int i = 0; i < pointCollection.size(); i++) { - for (int j = 0; j < pointCollection[i].size(); j++) { + for (int32_t i = 0; i < pointCollection.size(); i++) { + for (int32_t j = 0; j < pointCollection[i].size(); j++) { // compensate for registration pointCollection[i][j] += _model->getOffset(); // scale so the collision points match the model points @@ -708,9 +709,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // compute meshPart local transforms QVector localTransforms; const FBXGeometry& fbxGeometry = _model->getFBXGeometry(); - int numberOfMeshes = fbxGeometry.meshes.size(); - int totalNumVertices = 0; - for (int i = 0; i < numberOfMeshes; i++) { + int32_t numMeshes = (int32_t)fbxGeometry.meshes.size(); + int32_t totalNumVertices = 0; + for (int32_t i = 0; i < numMeshes; i++) { const FBXMesh& mesh = fbxGeometry.meshes.at(i); if (mesh.clusters.size() > 0) { const FBXCluster& cluster = mesh.clusters.at(0); @@ -722,7 +723,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } totalNumVertices += mesh.vertices.size(); } - const int MAX_VERTICES_PER_STATIC_MESH = 1e6; + const int32_t MAX_VERTICES_PER_STATIC_MESH = 1e6; if (totalNumVertices > MAX_VERTICES_PER_STATIC_MESH) { qWarning() << "model" << getModelURL() << "has too many vertices" << totalNumVertices << "and will collide as a box."; info.setParams(SHAPE_TYPE_BOX, 0.5f * dimensions); @@ -730,7 +731,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); - int32_t numMeshes = (int32_t)(meshes.size()); + + // the render geometry's mesh count should match that of the FBXGeometry + assert(numMeshes == (int32_t)(meshes.size())); ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); pointCollection.clear(); @@ -741,8 +744,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } Extents extents; - int meshCount = 0; - int pointListIndex = 0; + int32_t meshCount = 0; + int32_t pointListIndex = 0; for (auto& mesh : meshes) { const gpu::BufferView& vertices = mesh->getVertexBuffer(); const gpu::BufferView& indices = mesh->getIndexBuffer(); @@ -775,6 +778,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { if (type == SHAPE_TYPE_STATIC_MESH) { // copy into triangleIndices ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + triangleIndices.clear(); triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); gpu::BufferView::Iterator partItr = parts.cbegin(); while (partItr != parts.cend()) { @@ -823,6 +827,64 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } ++partItr; } + } else if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { + // for each mesh copy unique part indices, separated by special bogus (flag) index values + ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + triangleIndices.clear(); + gpu::BufferView::Iterator partItr = parts.cbegin(); + while (partItr != parts.cend()) { + // collect unique list of indices for this part + std::set uniqueIndices; + if (partItr->_topology == model::Mesh::TRIANGLES) { + assert(partItr->_numIndices % 3 == 0); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + partItr->_numIndices; + while (indexItr != indexEnd) { + uniqueIndices.insert(*indexItr); + ++indexItr; + } + } else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) { + assert(partItr->_numIndices > 2); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + (partItr->_numIndices - 2); + + // first triangle uses the first three indices + uniqueIndices.insert(*(indexItr++)); + uniqueIndices.insert(*(indexItr++)); + uniqueIndices.insert(*(indexItr++)); + + // the rest use previous and next index + uint32_t triangleCount = 1; + while (indexItr != indexEnd) { + if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) { + if (triangleCount % 2 == 0) { + // even triangles use first two indices in order + uniqueIndices.insert(*(indexItr - 2)); + uniqueIndices.insert(*(indexItr - 1)); + } else { + // odd triangles swap order of first two indices + uniqueIndices.insert(*(indexItr - 1)); + uniqueIndices.insert(*(indexItr - 2)); + } + uniqueIndices.insert(*indexItr); + ++triangleCount; + } + ++indexItr; + } + } + + // store uniqueIndices in triangleIndices + triangleIndices.reserve(triangleIndices.size() + uniqueIndices.size()); + for (auto index : uniqueIndices) { + triangleIndices.push_back(index); + } + // flag end of part + triangleIndices.push_back(END_OF_MESH_PART); + + ++partItr; + } + // flag end of mesh + triangleIndices.push_back(END_OF_MESH); } ++meshCount; } @@ -830,13 +892,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // scale and shift glm::vec3 extentsSize = extents.size(); glm::vec3 scaleToFit = dimensions / extentsSize; - for (int i = 0; i < 3; ++i) { + for (int32_t i = 0; i < 3; ++i) { if (extentsSize[i] < 1.0e-6f) { scaleToFit[i] = 1.0f; } } for (auto points : pointCollection) { - for (int i = 0; i < points.size(); ++i) { + for (int32_t i = 0; i < points.size(); ++i) { points[i] = (points[i] * scaleToFit); } } diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index f71711eccd..e47a0fe8a2 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -69,6 +69,7 @@ static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = { // util method btConvexHullShape* createConvexHull(const ShapeInfo::PointList& points) { + //std::cout << "adebug createConvexHull() points.size() = " << points.size() << std::endl; // adebug assert(points.size() > 0); btConvexHullShape* hull = new btConvexHullShape(); @@ -240,6 +241,7 @@ void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray) { btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { btCollisionShape* shape = NULL; int type = info.getType(); + //std::cout << "adebug createShapeFromInfo() type = " << type << std::endl; // adebug switch(type) { case SHAPE_TYPE_BOX: { shape = new btBoxShape(glmToBullet(info.getHalfExtents())); @@ -258,8 +260,7 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } break; case SHAPE_TYPE_COMPOUND: - case SHAPE_TYPE_SIMPLE_HULL: - case SHAPE_TYPE_SIMPLE_COMPOUND: { + case SHAPE_TYPE_SIMPLE_HULL: { const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); uint32_t numSubShapes = info.getNumSubShapes(); if (numSubShapes == 1) { @@ -270,12 +271,63 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { trans.setIdentity(); foreach (const ShapeInfo::PointList& hullPoints, pointCollection) { btConvexHullShape* hull = createConvexHull(hullPoints); - compound->addChildShape (trans, hull); + compound->addChildShape(trans, hull); } shape = compound; } } break; + case SHAPE_TYPE_SIMPLE_COMPOUND: { + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + const ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + uint32_t numIndices = triangleIndices.size(); + uint32_t numMeshes = info.getNumSubShapes(); + const uint32_t MIN_NUM_SIMPLE_COMPOUND_INDICES = 2; // END_OF_MESH_PART + END_OF_MESH + if (numMeshes > 0 && numIndices > MIN_NUM_SIMPLE_COMPOUND_INDICES) { + uint32_t i = 0; + std::vector hulls; + for (auto& points : pointCollection) { + // build a hull around each part + while (i < numIndices) { + ShapeInfo::PointList hullPoints; + hullPoints.reserve(points.size()); + while (i < numIndices) { + int32_t j = triangleIndices[i]; + ++i; + if (j == END_OF_MESH_PART) { + // end of part + break; + } + hullPoints.push_back(points[j]); + } + if (hullPoints.size() > 0) { + btConvexHullShape* hull = createConvexHull(hullPoints); + hulls.push_back(hull); + } + + assert(i < numIndices); + if (triangleIndices[i] == END_OF_MESH) { + // end of mesh + ++i; + break; + } + } + } + uint32_t numHulls = hulls.size(); + if (numHulls == 1) { + shape = hulls[0]; + } else { + auto compound = new btCompoundShape(); + btTransform trans; + trans.setIdentity(); + for (auto hull : hulls) { + compound->addChildShape(trans, hull); + } + shape = compound; + } + } + } + break; case SHAPE_TYPE_STATIC_MESH: { btTriangleIndexVertexArray* dataArray = createStaticMeshArray(info); shape = new StaticMeshShape(dataArray); diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 7bf145412a..a6ff8d6d4a 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -26,6 +26,10 @@ const float MIN_SHAPE_OFFSET = 0.001f; // offsets less than 1mm will be ignored // trim convex hulls with many points down to only 42 points. const int MAX_HULL_POINTS = 42; + +const int32_t END_OF_MESH_PART = -1; // bogus vertex index at end of mesh part +const int32_t END_OF_MESH = -2; // bogus vertex index at end of mesh + enum ShapeType { SHAPE_TYPE_NONE, SHAPE_TYPE_BOX, @@ -50,7 +54,7 @@ public: using PointList = QVector; using PointCollection = QVector; - using TriangleIndices = QVector; + using TriangleIndices = QVector; void clear(); From 72175ae09e1510de3adef4fcb2b90e515cc05a0a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 9 Jul 2016 22:55:55 -0700 Subject: [PATCH 1008/1237] fix warnings about implicit casts --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 2 +- libraries/physics/src/ShapeFactory.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 9f4ff63548..d63361538a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -874,7 +874,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } // store uniqueIndices in triangleIndices - triangleIndices.reserve(triangleIndices.size() + uniqueIndices.size()); + triangleIndices.reserve(triangleIndices.size() + (int32_t)uniqueIndices.size()); for (auto index : uniqueIndices) { triangleIndices.push_back(index); } diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index e47a0fe8a2..944e05e571 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -313,7 +313,7 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } } } - uint32_t numHulls = hulls.size(); + uint32_t numHulls = (uint32_t)hulls.size(); if (numHulls == 1) { shape = hulls[0]; } else { From ba6bb245956b4657b4d234afd342f40d50296f6c Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 10 Jul 2016 16:49:03 -0700 Subject: [PATCH 1009/1237] rework plugins to allow different decoder/encoder instances per streams --- assignment-client/src/audio/AudioMixer.cpp | 6 +- .../src/audio/AudioMixerClientData.cpp | 11 ++++ .../src/audio/AudioMixerClientData.h | 3 + libraries/audio-client/src/AudioClient.cpp | 10 +-- libraries/audio-client/src/AudioClient.h | 1 + libraries/audio/src/InboundAudioStream.cpp | 4 +- libraries/audio/src/InboundAudioStream.h | 1 + .../audio/src/MixedProcessedAudioStream.cpp | 4 +- libraries/plugins/src/plugins/CodecPlugin.h | 18 +++++- plugins/pcmCodec/src/PCMCodecManager.cpp | 64 ++++++++++++++++--- plugins/pcmCodec/src/PCMCodecManager.h | 37 +++++++++-- 11 files changed, 135 insertions(+), 24 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index f49b674dce..825f970f2e 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -533,12 +533,16 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess clientData->_codec = selectedCoded; clientData->_selectedCodecName = selectedCodecName; + clientData->_encoder = selectedCoded->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + clientData->_decoder = selectedCoded->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + qDebug() << "selectedCodecName:" << selectedCodecName; auto avatarAudioStream = clientData->getAvatarAudioStream(); if (avatarAudioStream) { avatarAudioStream->_codec = selectedCoded; avatarAudioStream->_selectedCodecName = selectedCodecName; + avatarAudioStream->_decoder = selectedCoded->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); } auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); @@ -777,7 +781,7 @@ void AudioMixer::broadcastMixes() { QByteArray decocedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); QByteArray encodedBuffer; if (nodeData->_codec) { - nodeData->_codec->encode(decocedBuffer, encodedBuffer); + nodeData->_encoder->encode(decocedBuffer, encodedBuffer); } else { encodedBuffer = decocedBuffer; } diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 080a833c0e..3e9b72d60e 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -39,6 +39,14 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : _frameToSendStats = distribution(numberGenerator); } +AudioMixerClientData::~AudioMixerClientData() { + if (_codec) { + _codec->releaseDecoder(_decoder); + _codec->releaseEncoder(_encoder); + } +} + + AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { QReadLocker readLocker { &_streamsLock }; @@ -104,6 +112,9 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { auto avatarAudioStream = new AvatarAudioStream(isStereo, AudioMixer::getStreamSettings()); avatarAudioStream->_codec = _codec; avatarAudioStream->_selectedCodecName = _selectedCodecName; + if (_codec) { + avatarAudioStream->_decoder = _codec->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + } qDebug() << "creating new AvatarAudioStream... codec:" << avatarAudioStream->_selectedCodecName; auto emplaced = _audioStreams.emplace( diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 0b3b352e66..ee6f22ff3b 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -29,6 +29,7 @@ class AudioMixerClientData : public NodeData { Q_OBJECT public: AudioMixerClientData(const QUuid& nodeID); + ~AudioMixerClientData(); using SharedStreamPointer = std::shared_ptr; using AudioStreamMap = std::unordered_map; @@ -70,6 +71,8 @@ public: // FIXME -- maybe make these private CodecPluginPointer _codec; QString _selectedCodecName; + Encoder* _encoder { nullptr }; // for outbound mixed stream + Decoder* _decoder { nullptr }; // for mic stream signals: diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 97ef4b2981..db5376a380 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -536,6 +536,8 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { _receivedAudioStream._codec = _codec = plugin; + _receivedAudioStream._decoder = plugin->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); qDebug() << "Selected Codec Plugin:" << _codec.get(); break; } @@ -815,8 +817,8 @@ void AudioClient::handleAudioInput() { // TODO - codec encode goes here QByteArray decocedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); QByteArray encodedBuffer; - if (_codec) { - _codec->encode(decocedBuffer, encodedBuffer); + if (_encoder) { + _encoder->encode(decocedBuffer, encodedBuffer); } else { encodedBuffer = decocedBuffer; } @@ -833,8 +835,8 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { // TODO - codec encode goes here QByteArray encodedBuffer; - if (_codec) { - _codec->encode(audio, encodedBuffer); + if (_encoder) { + _encoder->encode(audio, encodedBuffer); } else { encodedBuffer = audio; } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 66555d1e2d..bd0afe453d 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -299,6 +299,7 @@ private: CodecPluginPointer _codec; QString _selectedCodecName; + Encoder* _encoder { nullptr }; // for outbound mic stream }; diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 7c6a34ae55..f47c6cce46 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -179,8 +179,8 @@ int InboundAudioStream::parseStreamProperties(PacketType type, const QByteArray& int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties) { QByteArray decodedBuffer; - if (_codec) { - _codec->decode(packetAfterStreamProperties, decodedBuffer); + if (_decoder) { + _decoder->decode(packetAfterStreamProperties, decodedBuffer); } else { decodedBuffer = packetAfterStreamProperties; } diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index f9ca088fab..57bff80fff 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -179,6 +179,7 @@ public: // FIXME -- maybe make these private CodecPluginPointer _codec; QString _selectedCodecName; + Decoder* _decoder { nullptr }; public slots: /// This function should be called every second for all the stats to function properly. If dynamic jitter buffers diff --git a/libraries/audio/src/MixedProcessedAudioStream.cpp b/libraries/audio/src/MixedProcessedAudioStream.cpp index 6939ee540a..728deae0b1 100644 --- a/libraries/audio/src/MixedProcessedAudioStream.cpp +++ b/libraries/audio/src/MixedProcessedAudioStream.cpp @@ -44,8 +44,8 @@ int MixedProcessedAudioStream::writeLastFrameRepeatedWithFade(int samples) { int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties) { QByteArray decodedBuffer; - if (_codec) { - _codec->decode(packetAfterStreamProperties, decodedBuffer); + if (_decoder) { + _decoder->decode(packetAfterStreamProperties, decodedBuffer); } else { decodedBuffer = packetAfterStreamProperties; } diff --git a/libraries/plugins/src/plugins/CodecPlugin.h b/libraries/plugins/src/plugins/CodecPlugin.h index 6944d91bed..280853e37e 100644 --- a/libraries/plugins/src/plugins/CodecPlugin.h +++ b/libraries/plugins/src/plugins/CodecPlugin.h @@ -12,9 +12,23 @@ #include "Plugin.h" -class CodecPlugin : public Plugin { +class Encoder { public: - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) = 0; }; +class Decoder { +public: + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; + + // numFrames - number of samples (mono) or sample-pairs (stereo) + virtual void trackLostFrames(int numFrames) = 0; +}; + +class CodecPlugin : public Plugin { +public: + virtual Encoder* createEncoder(int sampleRate, int numChannels) = 0; + virtual Decoder* createDecoder(int sampleRate, int numChannels) = 0; + virtual void releaseEncoder(Encoder* encoder) = 0; + virtual void releaseDecoder(Decoder* decoder) = 0; +}; diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index f787c6682d..1cad1ea4e6 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -37,15 +37,54 @@ bool PCMCodec::isSupported() const { return true; } +class PCMEncoder : public Encoder { +public: + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer = decodedBuffer; + } +}; -void PCMCodec::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { - decodedBuffer = encodedBuffer; +class PCMDecoder : public Decoder { +public: + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer = encodedBuffer; + } + + virtual void trackLostFrames(int numFrames) override { } +}; + +Encoder* PCMCodec::createEncoder(int sampleRate, int numChannels) { + return new PCMEncoder(); } -void PCMCodec::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { - encodedBuffer = decodedBuffer; +Decoder* PCMCodec::createDecoder(int sampleRate, int numChannels) { + return new PCMDecoder(); } +void PCMCodec::releaseEncoder(Encoder* encoder) { + delete encoder; +} + +void PCMCodec::releaseDecoder(Decoder* decoder) { + delete decoder; +} + + +class zLibEncoder : public Encoder { +public: + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer = qCompress(decodedBuffer); + } +}; + +class zLibDecoder : public Decoder { +public: + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer = qUncompress(encodedBuffer); + } + + virtual void trackLostFrames(int numFrames) override { } +}; const QString zLibCodec::NAME = "zlib"; @@ -69,10 +108,19 @@ bool zLibCodec::isSupported() const { return true; } -void zLibCodec::decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) { - decodedBuffer = qUncompress(encodedBuffer); +Encoder* zLibCodec::createEncoder(int sampleRate, int numChannels) { + return new zLibEncoder(); } -void zLibCodec::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { - encodedBuffer = qCompress(decodedBuffer); +Decoder* zLibCodec::createDecoder(int sampleRate, int numChannels) { + return new zLibDecoder(); } + +void zLibCodec::releaseEncoder(Encoder* encoder) { + delete encoder; +} + +void zLibCodec::releaseDecoder(Decoder* decoder) { + delete decoder; +} + diff --git a/plugins/pcmCodec/src/PCMCodecManager.h b/plugins/pcmCodec/src/PCMCodecManager.h index 7816660c5d..6eb7f44c15 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.h +++ b/plugins/pcmCodec/src/PCMCodecManager.h @@ -12,9 +12,31 @@ #ifndef hifi__PCMCodecManager_h #define hifi__PCMCodecManager_h - #include +/* +class Encoder { +public: +virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) = 0; +}; + +class Decoder { +public: +virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; + +// numFrames - number of samples (mono) or sample-pairs (stereo) +virtual void trackLostFrames(int numFrames) = 0; +}; + +class CodecPlugin : public Plugin { +public: +virtual Encoder* createEncoder(int sampleRate, int numChannels) = 0; +virtual Decoder* createDecoder(int sampleRate, int numChannels) = 0; +virtual void releaseEncoder(Encoder* encoder) = 0; +virtual void releaseDecoder(Decoder* decoder) = 0; +}; +*/ + class PCMCodec : public CodecPlugin { Q_OBJECT @@ -31,8 +53,10 @@ public: /// Called when a plugin is no longer being used. May be called multiple times. void deactivate() override; - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override; - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override; + virtual Encoder* createEncoder(int sampleRate, int numChannels) override; + virtual Decoder* createDecoder(int sampleRate, int numChannels) override; + virtual void releaseEncoder(Encoder* encoder) override; + virtual void releaseDecoder(Decoder* decoder) override; private: static const QString NAME; @@ -54,8 +78,11 @@ public: /// Called when a plugin is no longer being used. May be called multiple times. void deactivate() override; - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override; - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override; + virtual Encoder* createEncoder(int sampleRate, int numChannels) override; + virtual Decoder* createDecoder(int sampleRate, int numChannels) override; + virtual void releaseEncoder(Encoder* encoder) override; + virtual void releaseDecoder(Decoder* decoder) override; + private: static const QString NAME; From ed9715ae5f25a828dca312740c9da33690653b96 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 10 Jul 2016 17:40:58 -0700 Subject: [PATCH 1010/1237] some cleanup, proper memory allocation/deallocation --- assignment-client/src/audio/AudioMixer.cpp | 22 ++++---------- .../src/audio/AudioMixerClientData.cpp | 30 +++++++++++++++---- .../src/audio/AudioMixerClientData.h | 20 +++++++++---- libraries/audio-client/src/AudioClient.cpp | 15 ++++++++-- libraries/audio/src/InboundAudioStream.cpp | 18 +++++++++++ libraries/audio/src/InboundAudioStream.h | 11 ++++--- 6 files changed, 81 insertions(+), 35 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 825f970f2e..2f1a5de309 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -478,7 +478,7 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess qDebug() << "No Codecs available..."; } - CodecPluginPointer selectedCoded; + CodecPluginPointer selectedCodec; QString selectedCodecName; QStringList codecPreferenceList = _codecPreferenceOrder.split(","); @@ -513,7 +513,7 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess for (auto& plugin : codecPlugins) { if (selectedCodecName == plugin->getName()) { qDebug() << "Selecting codec:" << selectedCodecName; - selectedCoded = plugin; + selectedCodec = plugin; break; } } @@ -531,18 +531,13 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); } - clientData->_codec = selectedCoded; - clientData->_selectedCodecName = selectedCodecName; - clientData->_encoder = selectedCoded->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); - clientData->_decoder = selectedCoded->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + clientData->setupCodec(selectedCodec, selectedCodecName); qDebug() << "selectedCodecName:" << selectedCodecName; auto avatarAudioStream = clientData->getAvatarAudioStream(); if (avatarAudioStream) { - avatarAudioStream->_codec = selectedCoded; - avatarAudioStream->_selectedCodecName = selectedCodecName; - avatarAudioStream->_decoder = selectedCoded->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + avatarAudioStream->setupCodec(selectedCodec, selectedCodecName, AudioConstants::MONO); } auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); @@ -777,14 +772,9 @@ void AudioMixer::broadcastMixes() { quint16 sequence = nodeData->getOutgoingSequenceNumber(); mixPacket->writePrimitive(sequence); - // TODO - codec encode goes here - QByteArray decocedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); + QByteArray decodedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); QByteArray encodedBuffer; - if (nodeData->_codec) { - nodeData->_encoder->encode(decocedBuffer, encodedBuffer); - } else { - encodedBuffer = decocedBuffer; - } + nodeData->encode(decodedBuffer, encodedBuffer); // pack mixed audio samples mixPacket->write(encodedBuffer.constData(), encodedBuffer.size()); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 3e9b72d60e..c733683e5a 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -110,12 +110,8 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { bool isStereo = channelFlag == 1; auto avatarAudioStream = new AvatarAudioStream(isStereo, AudioMixer::getStreamSettings()); - avatarAudioStream->_codec = _codec; - avatarAudioStream->_selectedCodecName = _selectedCodecName; - if (_codec) { - avatarAudioStream->_decoder = _codec->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); - } - qDebug() << "creating new AvatarAudioStream... codec:" << avatarAudioStream->_selectedCodecName; + avatarAudioStream->setupCodec(_codec, _selectedCodecName, AudioConstants::MONO); + qDebug() << "creating new AvatarAudioStream... codec:" << _selectedCodecName; auto emplaced = _audioStreams.emplace( QUuid(), @@ -340,3 +336,25 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { return result; } + +void AudioMixerClientData::setupCodec(CodecPluginPointer codec, const QString& codecName) { + cleanupCodec(); // cleanup any previously allocated coders first + _codec = codec; + _selectedCodecName = codecName; + _encoder = codec->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + _decoder = codec->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); +} + +void AudioMixerClientData::cleanupCodec() { + // release any old codec encoder/decoder first... + if (_codec) { + if (_decoder) { + _codec->releaseDecoder(_decoder); + _decoder = nullptr; + } + if (_encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + } + } +} diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index ee6f22ff3b..da2bf8997c 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -68,12 +68,15 @@ public: AudioLimiter audioLimiter; - // FIXME -- maybe make these private - CodecPluginPointer _codec; - QString _selectedCodecName; - Encoder* _encoder { nullptr }; // for outbound mixed stream - Decoder* _decoder { nullptr }; // for mic stream - + void setupCodec(CodecPluginPointer codec, const QString& codecName); + void cleanupCodec(); + void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { + if (_encoder) { + _encoder->encode(decodedBuffer, encodedBuffer); + } else { + encodedBuffer = decodedBuffer; + } + } signals: void injectorStreamFinished(const QUuid& streamIdentifier); @@ -91,6 +94,11 @@ private: AudioStreamStats _downstreamAudioStreamStats; int _frameToSendStats { 0 }; + + CodecPluginPointer _codec; + QString _selectedCodecName; + Encoder* _encoder{ nullptr }; // for outbound mixed stream + Decoder* _decoder{ nullptr }; // for mic stream }; #endif // hifi_AudioMixerClientData_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index db5376a380..28fd95f599 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -140,6 +140,10 @@ AudioClient::AudioClient() : AudioClient::~AudioClient() { stop(); + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + } } void AudioClient::reset() { @@ -529,14 +533,19 @@ void AudioClient::negotiateAudioFormat() { void AudioClient::handleSelectedAudioFormat(QSharedPointer message) { qDebug() << __FUNCTION__; - _receivedAudioStream._selectedCodecName = _selectedCodecName = message->readString(); + _selectedCodecName = message->readString(); qDebug() << "Selected Codec:" << _selectedCodecName; auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { - _receivedAudioStream._codec = _codec = plugin; - _receivedAudioStream._decoder = plugin->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + // release any old codec encoder/decoder first... + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + } + _codec = plugin; + _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); qDebug() << "Selected Codec Plugin:" << _codec.get(); break; diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index f47c6cce46..144005bc11 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -504,3 +504,21 @@ float calculateRepeatedFrameFadeFactor(int indexOfRepeat) { return 0.0f; } +void InboundAudioStream::setupCodec(CodecPluginPointer codec, const QString& codecName, int numChannels) { + cleanupCodec(); // cleanup any previously allocated coders first + _codec = codec; + _selectedCodecName = codecName; + if (_codec) { + _decoder = codec->createDecoder(AudioConstants::SAMPLE_RATE, numChannels); + } +} + +void InboundAudioStream::cleanupCodec() { + // release any old codec encoder/decoder first... + if (_codec) { + if (_decoder) { + _codec->releaseDecoder(_decoder); + _decoder = nullptr; + } + } +} diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index 57bff80fff..5da63f96c2 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -105,6 +105,7 @@ public: public: InboundAudioStream(int numFrameSamples, int numFramesCapacity, const Settings& settings); + ~InboundAudioStream() { cleanupCodec(); } void reset(); virtual void resetStats(); @@ -176,10 +177,8 @@ public: void setReverb(float reverbTime, float wetLevel); void clearReverb() { _hasReverb = false; } - // FIXME -- maybe make these private - CodecPluginPointer _codec; - QString _selectedCodecName; - Decoder* _decoder { nullptr }; + void setupCodec(CodecPluginPointer codec, const QString& codecName, int numChannels); + void cleanupCodec(); public slots: /// This function should be called every second for all the stats to function properly. If dynamic jitter buffers @@ -274,6 +273,10 @@ protected: bool _hasReverb; float _reverbTime; float _wetLevel; + + CodecPluginPointer _codec; + QString _selectedCodecName; + Decoder* _decoder{ nullptr }; }; float calculateRepeatedFrameFadeFactor(int indexOfRepeat); From 7d608ba5925001be0c2a16f9b27425a20527adbb Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 10 Jul 2016 21:01:20 -0700 Subject: [PATCH 1011/1237] groundwork for injector support --- assignment-client/src/audio/AudioMixer.cpp | 5 ---- .../src/audio/AudioMixerClientData.cpp | 26 ++++++++++++++++++- libraries/audio/src/AudioInjector.cpp | 9 ++++++- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 2f1a5de309..720f9d52de 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -535,11 +535,6 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess qDebug() << "selectedCodecName:" << selectedCodecName; - auto avatarAudioStream = clientData->getAvatarAudioStream(); - if (avatarAudioStream) { - avatarAudioStream->setupCodec(selectedCodec, selectedCodecName, AudioConstants::MONO); - } - auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); // write them to our packet diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index c733683e5a..182f443225 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -142,9 +142,16 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { if (streamIt == _audioStreams.end()) { // we don't have this injected stream yet, so add it + auto injectorStream = new InjectedAudioStream(streamIdentifier, isStereo, AudioMixer::getStreamSettings()); + +#if INJECTORS_SUPPORT_CODECS + injectorStream->setupCodec(_codec, _selectedCodecName, isStereo ? AudioConstants::STEREO : AudioConstants::MONO); + qDebug() << "creating new injectorStream... codec:" << _selectedCodecName; +#endif + auto emplaced = _audioStreams.emplace( streamIdentifier, - std::unique_ptr { new InjectedAudioStream(streamIdentifier, isStereo, AudioMixer::getStreamSettings()) } + std::unique_ptr { injectorStream } ); streamIt = emplaced.first; @@ -343,6 +350,23 @@ void AudioMixerClientData::setupCodec(CodecPluginPointer codec, const QString& c _selectedCodecName = codecName; _encoder = codec->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); _decoder = codec->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + + auto avatarAudioStream = getAvatarAudioStream(); + if (avatarAudioStream) { + avatarAudioStream->setupCodec(codec, codecName, AudioConstants::MONO); + } + +#if INJECTORS_SUPPORT_CODECS + // fixup codecs for any active injectors... + auto it = _audioStreams.begin(); + while (it != _audioStreams.end()) { + SharedStreamPointer stream = it->second; + if (stream->getType() == PositionalAudioStream::Injector) { + stream->setupCodec(codec, codecName, stream->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); + } + ++it; + } +#endif } void AudioMixerClientData::cleanupCodec() { diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 878a4c627c..fee33dcb92 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -289,16 +289,23 @@ int64_t AudioInjector::injectNextFrame() { _currentPacket->seek(audioDataOffset); + // This code is copying bytes from the _audioData directly into the packet, handling looping appropriately. + // Might be a reasonable place to do the encode step here. + QByteArray decodedAudio; while (totalBytesLeftToCopy > 0) { int bytesToCopy = std::min(totalBytesLeftToCopy, _audioData.size() - _currentSendOffset); - _currentPacket->write(_audioData.data() + _currentSendOffset, bytesToCopy); + decodedAudio.append(_audioData.data() + _currentSendOffset, bytesToCopy); _currentSendOffset += bytesToCopy; totalBytesLeftToCopy -= bytesToCopy; if (_options.loop && _currentSendOffset >= _audioData.size()) { _currentSendOffset = 0; } } + // FIXME -- good place to call codec encode here. We need to figure out how to tell the AudioInjector which + // codec to use... possible through AbstractAudioInterface. + QByteArray encodedAudio = decodedAudio; + _currentPacket->write(encodedAudio.data(), encodedAudio.size()); // set the correct size used for this packet _currentPacket->setPayloadSize(_currentPacket->pos()); From 80d33ee2516616beb3b08d9279e812686e37a866 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 8 Jul 2016 13:59:49 -0700 Subject: [PATCH 1012/1237] Working now as frame-at-a-time So localOnly sounds get HRTF'd, one network frame at a time. Less processing (upsampling, limiting, etc...) than doing this at the end of the pipeline (in the AudioOutputIODevice::readData call). --- libraries/audio-client/src/AudioClient.cpp | 199 ++++++++------------- libraries/audio-client/src/AudioClient.h | 20 ++- 2 files changed, 87 insertions(+), 132 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 67fb1ff561..190dd1583b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -43,10 +43,6 @@ #include #include -#include "AudioInjector.h" -#include "AudioLimiter.h" -#include "AudioHRTF.h" -#include "AudioConstants.h" #include "PositionalAudioStream.h" #include "AudioClientLogging.h" @@ -111,7 +107,8 @@ AudioClient::AudioClient() : _stats(&_receivedAudioStream), _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), - _orientationGetter(DEFAULT_ORIENTATION_GETTER) + _orientationGetter(DEFAULT_ORIENTATION_GETTER), + _audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO) { // clear the array of locally injected samples memset(_localProceduralSamples, 0, AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL); @@ -787,15 +784,84 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { emitAudioPacket(audio.data(), audio.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho); } +void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { + + memset(_hrtfBuffer, 0, sizeof(_hrtfBuffer)); + QVector injectorsToRemove; + + bool injectorsHaveData = false; + + for (AudioInjector* injector : getActiveLocalAudioInjectors()) { + if (injector->getLocalBuffer()) { + + // get one (mono) frame from the injector + if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*sizeof(int16_t)) ) { + + injectorsHaveData = true; + + // calculate gain and azimuth for hrtf + glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + float gain = gainForSource(relativePosition, injector->getVolume()); + float azimuth = azimuthForSource(relativePosition); + + + injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + } else { + + qDebug() << "injector has no more data, marking finished for removal"; + + injector->finish(); + injectorsToRemove.append(injector); + } + + } else { + + qDebug() << "injector has no local buffer, marking as finished for removal"; + + injector->finish(); + injectorsToRemove.append(injector); + } + } + + if(injectorsHaveData) { + + // mix network into the hrtfBuffer + for(int i=0; i(inputBuffer.data()); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); + const int16_t* receivedSamples; + char inputBufferCopy[AudioConstants::NETWORK_FRAME_BYTES_STEREO]; + assert(inputBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO); + + if(getActiveLocalAudioInjectors().size() > 0) { + // gotta copy the since it is const + for(int i = 0; i < sizeof(inputBufferCopy); i++) { + inputBufferCopy[i] = inputBuffer.data()[i]; + } + mixLocalAudioInjectors((int16_t*)inputBufferCopy); + receivedSamples = reinterpret_cast(inputBufferCopy); + } else { + receivedSamples = reinterpret_cast(inputBuffer.data()); + } // copy the packet from the RB to the output possibleResampling(_networkToOutputResampler, receivedSamples, outputSamples, @@ -807,6 +873,7 @@ void AudioClient::processReceivedSamples(const QByteArray& inputBuffer, QByteArr if (hasReverb) { assert(_outputFormat.channelCount() == 2); updateReverbOptions(); + qDebug() << "handling reverb"; _listenerReverb.render(outputSamples, outputSamples, numDeviceOutputSamples/2); } } @@ -1148,22 +1215,6 @@ float AudioClient::getAudioOutputMsecsUnplayed() const { return msecsAudioOutputUnplayed; } -void AudioClient::AudioOutputIODevice::renderHRTF(AudioHRTF& hrtf, int16_t* data, float* hrtfBuffer, float azimuth, float gain, qint64 numSamples) { - qint64 numFrames = numSamples/AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; - int16_t* dataPtr = data; - float* hrtfPtr = hrtfBuffer; - for(qint64 i=0; i < numFrames; i++) { - hrtf.render(dataPtr, hrtfPtr, 1, azimuth, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - //qDebug() << "processed frame " << i; - - dataPtr += AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; - hrtfPtr += 2*AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; - - } - assert(dataPtr - data <= numSamples); - assert(hrtfPtr - hrtfBuffer <= 2*numSamples); -} float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { // copied from AudioMixer, more or less @@ -1226,105 +1277,11 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { auto samplesRequested = maxSize / sizeof(int16_t); int samplesPopped; int bytesWritten; - - // limit the number of NETWORK_FRAME_SAMPLES_PER_CHANNEL to return regardless - // of what maxSize requests. - static const qint64 MAX_FRAMES = 256; - static float hrtfBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES]; - static float mixed48Khz[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES*2]; - static int16_t scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*MAX_FRAMES]; - // final output is 48Khz, so use this to limit network audio plus upsampled injectors - static AudioLimiter finalAudioLimiter(2*AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); - - // Injectors are 24khz, but we are running at 48Khz here, so need an AudioSRC to upsample - // TODO: probably an existing src the AudioClient is appropriate, check! - static AudioSRC audioSRC(AudioConstants::SAMPLE_RATE, 2*AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); - - // limit maxSize - if (maxSize > AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * MAX_FRAMES) { - maxSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * MAX_FRAMES; - } - - // initialize stuff - memset(hrtfBuffer, 0, sizeof(hrtfBuffer)); - memset(mixed48Khz, 0, sizeof(mixed48Khz)); - QVector injectorsToRemove; - qint64 framesReadFromInjectors = 0; - - //qDebug() << "maxSize=" << maxSize; - - // first, grab the stuff from the network. Read into a if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples((int16_t*)data, samplesPopped); - - //qDebug() << "AudioClient::AudioOutputIODevice::readData popped " << samplesPopped << "samples"; - - // no matter what, this is how many bytes will be written bytesWritten = samplesPopped * sizeof(int16_t); - - - // now, grab same number of frames (samplesPopped/2 since network is stereo) - // and divide by 2 again because these are 24Khz not 48Khz - // from each of the localInjectors, if any - for (AudioInjector* injector : _audio->getActiveLocalAudioInjectors()) { - if (injector->getLocalBuffer() ) { - - // we want to get the same number of frames that we got from - // the network. Since network is stereo, that is samplesPopped/2. - // Since each frame is 2 bytes, we read samplesPopped bytes - qint64 bytesReadFromInjector = injector->getLocalBuffer()->readData((char*)scratchBuffer, samplesPopped/2); - qint64 framesReadFromInjector = bytesReadFromInjector/sizeof(int16_t); - - // keep track of max frames read for all injectors - if (framesReadFromInjector > framesReadFromInjectors) { - framesReadFromInjectors = framesReadFromInjector; - } - - if (framesReadFromInjector > 0) { - - //qDebug() << "AudioClient::AudioOutputIODevice::readData found " << framesReadFromInjector << " frames"; - - // calculate gain and azimuth for hrtf - glm::vec3 relativePosition = injector->getPosition() - _audio->_positionGetter(); - float gain = _audio->gainForSource(relativePosition, injector->getVolume()); - float azimuth = _audio->azimuthForSource(relativePosition); - - // now hrtf this guy, one NETWORK_FRAME_SAMPLES_PER_CHANNEL at a time - this->renderHRTF(injector->getLocalHRTF(), scratchBuffer, hrtfBuffer, azimuth, gain, framesReadFromInjector); - - } else { - - qDebug() << "AudioClient::AudioOutputIODevice::readData no more data, adding to list of injectors to remove"; - injectorsToRemove.append(injector); - injector->finish(); - } - } else { - - qDebug() << "AudioClient::AudioOutputIODevice::readData injector " << injector << " has no local buffer, adding to list of injectors to remove"; - injectorsToRemove.append(injector); - injector->finish(); - } - } - - if (framesReadFromInjectors > 0) { - // resample the 24Khz injector audio in hrtfBuffer to 48Khz - audioSRC.render(hrtfBuffer, mixed48Khz, framesReadFromInjectors); - - //qDebug() << "upsampled to" << framesReadFromInjectors*2 << " frames, stereo"; - - int16_t* dataPtr = (int16_t*)data; - - // now mix in the network data - for (int i=0; igetActiveLocalAudioInjectors().removeOne(injector); - //qDebug() << "removed injector " << injector << " from active injector list!"; - } - - //qDebug() << "bytesWritten=" << bytesWritten; - int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); if (!bytesAudioOutputUnplayed) { qCDebug(audioclient) << "empty audio buffer"; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 91987178b0..3630358771 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -38,11 +38,14 @@ #include #include #include +#include +#include +#include +#include +#include #include "AudioIOStats.h" #include "AudioNoiseGate.h" -#include "AudioSRC.h" -#include "AudioReverb.h" #ifdef _WIN32 #pragma warning( push ) @@ -92,7 +95,6 @@ public: MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; - void renderHRTF(AudioHRTF& hrtf, int16_t* data, float* hrtfBuffer, float azimuth, float gain, qint64 numSamples); }; const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } @@ -208,6 +210,9 @@ protected: private: void outputFormatChanged(); + void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer); + float azimuthForSource(const glm::vec3& relativePosition); + float gainForSource(const glm::vec3& relativePosition, float volume); QByteArray firstInputFrame; QAudioInput* _audioInput; @@ -264,6 +269,11 @@ private: AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; + // for local hrtf-ing + float _hrtfBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + AudioLimiter _audioLimiter; + // Adds Reverb void configureReverb(); void updateReverbOptions(); @@ -296,10 +306,6 @@ private: bool _hasReceivedFirstPacket = false; QVector _activeLocalAudioInjectors; - - float azimuthForSource(const glm::vec3& relativePosition); - float gainForSource(const glm::vec3& relativePosition, float volume); - }; From 46dc5bea0404526d1e487c43d19ee7a325c8ea89 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 11 Jul 2016 09:31:05 -0700 Subject: [PATCH 1013/1237] cleanup from PR comments --- libraries/audio-client/src/AudioClient.cpp | 6 +++--- libraries/audio/src/AudioInjector.h | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 190dd1583b..b83165eac6 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -939,11 +939,11 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { // We don't want to stop this localOutput and injector whenever this AudioClient singleton goes idle, // only when the localOutput does. But the connection is to localOutput, so that it happens on the right thread. connect(localOutput, &QAudioOutput::stateChanged, localOutput, [=](QAudio::State state) { - if (state == QAudio::IdleState) { + if (state == QAudio::IdleState) { localOutput->stop(); injector->stop(); - } - }); + } + }); connect(injector->getLocalBuffer(), &QIODevice::aboutToClose, localOutput, &QAudioOutput::stop); diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index a4cde77377..7560863b03 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -84,9 +84,6 @@ signals: void finished(); void restarting(); -/*private slots: - void finish(); -*/ private: void setupInjection(); int64_t injectNextFrame(); @@ -110,7 +107,6 @@ private: // when the injector is local, we need this AudioHRTF _localHRTF; - // for local injection, friend class AudioInjectorManager; }; From eac144f35476762dfb6d99155320d22a00445b69 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 09:52:24 -0700 Subject: [PATCH 1014/1237] experimenting --- interface/src/avatar/AvatarActionHold.cpp | 9 ++++++++- interface/src/avatar/AvatarActionHold.h | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 1babb478b8..2baaae6ee4 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -204,8 +204,15 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { } withWriteLock([&]{ + if (_previousSet) { + _measuredLinearVelocity = (_positionalTarget - _previousPositionalTarget) / deltaTimeStep; + } else { + _measuredLinearVelocity = glm::vec3(); + } + if (_kinematicSetVelocity) { - rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); + // rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); + rigidBody->setLinearVelocity(glmToBullet(_measuredLinearVelocity)); rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget)); } diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index 609fd57ff3..e246ac5f36 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -64,6 +64,8 @@ private: glm::vec3 _palmOffsetFromRigidBody; // leaving this here for future refernece. // glm::quat _palmRotationFromRigidBody; + + glm::vec3 _measuredLinearVelocity; }; #endif // hifi_AvatarActionHold_h From 7295e5c101fe1c0fe06b605f8087d7da53738059 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 10:22:44 -0700 Subject: [PATCH 1015/1237] experimenting --- interface/src/avatar/AvatarActionHold.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 2baaae6ee4..d1750ddf53 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -206,6 +206,8 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { withWriteLock([&]{ if (_previousSet) { _measuredLinearVelocity = (_positionalTarget - _previousPositionalTarget) / deltaTimeStep; + qDebug() << _measuredLinearVelocity.x << _measuredLinearVelocity.y << _measuredLinearVelocity.z + << deltaTimeStep; } else { _measuredLinearVelocity = glm::vec3(); } From 4f80c77b77b6ae808e4b8a33eb6405a5092e6f19 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 11 Jul 2016 10:36:55 -0700 Subject: [PATCH 1016/1237] PR feedback simple enough - not sure why I had an issue with this in the first place. --- libraries/audio-client/src/AudioClient.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b83165eac6..fad34f01de 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -849,16 +849,12 @@ void AudioClient::processReceivedSamples(const QByteArray& inputBuffer, QByteArr outputBuffer.resize(numDeviceOutputSamples * sizeof(int16_t)); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); const int16_t* receivedSamples; - char inputBufferCopy[AudioConstants::NETWORK_FRAME_BYTES_STEREO]; + QByteArray inputBufferCopy = inputBuffer; assert(inputBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO); if(getActiveLocalAudioInjectors().size() > 0) { - // gotta copy the since it is const - for(int i = 0; i < sizeof(inputBufferCopy); i++) { - inputBufferCopy[i] = inputBuffer.data()[i]; - } - mixLocalAudioInjectors((int16_t*)inputBufferCopy); - receivedSamples = reinterpret_cast(inputBufferCopy); + mixLocalAudioInjectors((int16_t*)inputBufferCopy.data()); + receivedSamples = reinterpret_cast(inputBufferCopy.data()); } else { receivedSamples = reinterpret_cast(inputBuffer.data()); } From c84ef5f62688f181b6e99ee9ceb19d5205c003da Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 11 Jul 2016 11:31:47 -0700 Subject: [PATCH 1017/1237] Stereo injectors now handled same as mono Except of course no HRTF. --- libraries/audio-client/src/AudioClient.cpp | 65 ++++++++-------------- libraries/audio/src/AudioInjector.h | 3 +- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index fad34f01de..3178a53e94 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -788,24 +788,37 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { memset(_hrtfBuffer, 0, sizeof(_hrtfBuffer)); QVector injectorsToRemove; + static const float INT16_TO_FLOAT_SCALE_FACTOR = 1/32768.0f; bool injectorsHaveData = false; for (AudioInjector* injector : getActiveLocalAudioInjectors()) { if (injector->getLocalBuffer()) { - // get one (mono) frame from the injector - if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL*sizeof(int16_t)) ) { + qint64 samplesToRead = injector->isStereo() ? + AudioConstants::NETWORK_FRAME_BYTES_STEREO : + AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + + // get one frame from the injector (mono or stereo) + if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, samplesToRead)) { injectorsHaveData = true; - // calculate gain and azimuth for hrtf - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); - float gain = gainForSource(relativePosition, injector->getVolume()); - float azimuth = azimuthForSource(relativePosition); + if (injector->isStereo() ) { + for(int i=0; igetPosition() - _positionGetter(); + float gain = gainForSource(relativePosition, injector->getVolume()); + float azimuth = azimuthForSource(relativePosition); - injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } } else { @@ -828,7 +841,7 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { // mix network into the hrtfBuffer for(int i=0; igetLocalBuffer() && _audioInput ) { - if(isStereo) { - QAudioFormat localFormat = _desiredOutputFormat; - localFormat.setChannelCount(isStereo ? 2 : 1); - - QAudioOutput* localOutput = new QAudioOutput(getNamedAudioDeviceForMode(QAudio::AudioOutput, _outputAudioDeviceName), - localFormat, - injector->getLocalBuffer()); - - // move the localOutput to the same thread as the local injector buffer - localOutput->moveToThread(injector->getLocalBuffer()->thread()); - - // have it be stopped when that local buffer is about to close - // We don't want to stop this localOutput and injector whenever this AudioClient singleton goes idle, - // only when the localOutput does. But the connection is to localOutput, so that it happens on the right thread. - connect(localOutput, &QAudioOutput::stateChanged, localOutput, [=](QAudio::State state) { - if (state == QAudio::IdleState) { - localOutput->stop(); - injector->stop(); - } - }); - - connect(injector->getLocalBuffer(), &QIODevice::aboutToClose, localOutput, &QAudioOutput::stop); - - qCDebug(audioclient) << "Starting QAudioOutput for local injector" << localOutput; - - localOutput->start(injector->getLocalBuffer()); - return localOutput->state() == QAudio::ActiveState; - } else { // just add it to the vector of active local injectors // TODO: deal with concurrency perhaps? Maybe not - qDebug() << "adding new injector!!!!!!!"; - - _activeLocalAudioInjectors.append(injector); - return true; - } + qDebug() << "adding new injector!!!!!!!"; + _activeLocalAudioInjectors.append(injector); + return true; } return false; diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 7560863b03..c74497ce4d 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -59,7 +59,8 @@ public: bool isLocalOnly() const { return _options.localOnly; } float getVolume() const { return _options.volume; } - glm::vec3 getPosition() const { return _options.position; } + glm::vec3 getPosition() const { return _options.position; } + bool isStereo() const { return _options.stereo; } void setLocalAudioInterface(AbstractAudioInterface* localAudioInterface) { _localAudioInterface = localAudioInterface; } static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface); From f5b693cebb0574486a61a26157d5e77caf96da3a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 12:22:27 -0700 Subject: [PATCH 1018/1237] added support for external hificodec and hifi codec plugin --- cmake/externals/hifiAudioCodec/CMakeLists.txt | 50 +++++++++++++ plugins/hifiCodec/CMakeLists.txt | 21 ++++++ plugins/hifiCodec/src/HiFiCodec.cpp | 72 +++++++++++++++++++ plugins/hifiCodec/src/HiFiCodec.h | 42 +++++++++++ plugins/hifiCodec/src/HiFiCodecProvider.cpp | 46 ++++++++++++ plugins/hifiCodec/src/plugin.json | 1 + 6 files changed, 232 insertions(+) create mode 100644 cmake/externals/hifiAudioCodec/CMakeLists.txt create mode 100644 plugins/hifiCodec/CMakeLists.txt create mode 100644 plugins/hifiCodec/src/HiFiCodec.cpp create mode 100644 plugins/hifiCodec/src/HiFiCodec.h create mode 100644 plugins/hifiCodec/src/HiFiCodecProvider.cpp create mode 100644 plugins/hifiCodec/src/plugin.json diff --git a/cmake/externals/hifiAudioCodec/CMakeLists.txt b/cmake/externals/hifiAudioCodec/CMakeLists.txt new file mode 100644 index 0000000000..5c563774b7 --- /dev/null +++ b/cmake/externals/hifiAudioCodec/CMakeLists.txt @@ -0,0 +1,50 @@ +include(ExternalProject) +include(SelectLibraryConfigurations) + +set(EXTERNAL_NAME HiFiAudioCodec) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +ExternalProject_Add( + ${EXTERNAL_NAME} + URL https://s3.amazonaws.com/hifi-public/dependencies/codecSDK.zip + URL_MD5 4add25b7cc5dfdb3cbfc1156b95cfff7 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 +) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) + +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include CACHE TYPE INTERNAL) + +if (WIN32) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/audio.lib CACHE TYPE INTERNAL) + + # FIXME need to account for different architectures + #if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + # set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/win64/audio.lib CACHE TYPE INTERNAL) + # add_paths_to_fixup_libs(${SOURCE_DIR}/bin/win64) + #else() + # set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/win32/audio.lib CACHE TYPE INTERNAL) + # add_paths_to_fixup_libs(${SOURCE_DIR}/bin/win32) + #endif() + +elseif(APPLE) + + # FIXME need to account for different architectures + #set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/osx32/audio.dylib CACHE TYPE INTERNAL) + #add_paths_to_fixup_libs(${SOURCE_DIR}/bin/osx32) + +elseif(NOT ANDROID) + + # FIXME need to account for different architectures + #set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/linux64/audio.so CACHE TYPE INTERNAL) + #add_paths_to_fixup_libs(${SOURCE_DIR}/bin/linux64) + +endif() + diff --git a/plugins/hifiCodec/CMakeLists.txt b/plugins/hifiCodec/CMakeLists.txt new file mode 100644 index 0000000000..50ba6de54e --- /dev/null +++ b/plugins/hifiCodec/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# Created by Brad Hefta-Gaub on 7/10/2016 +# Copyright 2016 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html +# + + + +if (WIN32) + set(TARGET_NAME hifiCodec) + setup_hifi_client_server_plugin() + + link_hifi_libraries(shared plugins) + + add_dependency_external_projects(HiFiAudioCodec) + target_include_directories(${TARGET_NAME} PRIVATE ${HIFIAUDIOCODEC_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${HIFIAUDIOCODEC_LIBRARIES}) +endif() + diff --git a/plugins/hifiCodec/src/HiFiCodec.cpp b/plugins/hifiCodec/src/HiFiCodec.cpp new file mode 100644 index 0000000000..03a0b04bb8 --- /dev/null +++ b/plugins/hifiCodec/src/HiFiCodec.cpp @@ -0,0 +1,72 @@ +// +// HiFiCodec.cpp +// plugins/hifiCodec/src +// +// Created by Brad Hefta-Gaub on 7/10/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include // should be from the external... + +#include + +#include "HiFiCodec.h" + +const QString HiFiCodec::NAME = "hifiAC"; + +void HiFiCodec::init() { +} + +void HiFiCodec::deinit() { +} + +bool HiFiCodec::activate() { + CodecPlugin::activate(); + return true; +} + +void HiFiCodec::deactivate() { + CodecPlugin::deactivate(); +} + + +bool HiFiCodec::isSupported() const { + return true; +} + +class HiFiEncoder : public Encoder { +public: + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer = decodedBuffer; + } +}; + +class HiFiDecoder : public Decoder { +public: + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer = encodedBuffer; + } + + virtual void trackLostFrames(int numFrames) override { } +}; + +Encoder* HiFiCodec::createEncoder(int sampleRate, int numChannels) { + return new HiFiEncoder(); +} + +Decoder* HiFiCodec::createDecoder(int sampleRate, int numChannels) { + return new HiFiDecoder(); +} + +void HiFiCodec::releaseEncoder(Encoder* encoder) { + delete encoder; +} + +void HiFiCodec::releaseDecoder(Decoder* decoder) { + delete decoder; +} diff --git a/plugins/hifiCodec/src/HiFiCodec.h b/plugins/hifiCodec/src/HiFiCodec.h new file mode 100644 index 0000000000..eeba8d56d8 --- /dev/null +++ b/plugins/hifiCodec/src/HiFiCodec.h @@ -0,0 +1,42 @@ +// +// HiFiCodec.h +// plugins/hifiCodec/src +// +// Created by Brad Hefta-Gaub on 7/10/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_HiFiCodec_h +#define hifi_HiFiCodec_h + +#include + +class HiFiCodec : public CodecPlugin { + Q_OBJECT + +public: + // Plugin functions + bool isSupported() const override; + const QString& getName() const override { return NAME; } + + void init() override; + void deinit() override; + + /// Called when a plugin is being activated for use. May be called multiple times. + bool activate() override; + /// Called when a plugin is no longer being used. May be called multiple times. + void deactivate() override; + + virtual Encoder* createEncoder(int sampleRate, int numChannels) override; + virtual Decoder* createDecoder(int sampleRate, int numChannels) override; + virtual void releaseEncoder(Encoder* encoder) override; + virtual void releaseDecoder(Decoder* decoder) override; + +private: + static const QString NAME; +}; + +#endif // hifi_HiFiCodec_h diff --git a/plugins/hifiCodec/src/HiFiCodecProvider.cpp b/plugins/hifiCodec/src/HiFiCodecProvider.cpp new file mode 100644 index 0000000000..b9698cc821 --- /dev/null +++ b/plugins/hifiCodec/src/HiFiCodecProvider.cpp @@ -0,0 +1,46 @@ +// +// Created by Brad Hefta-Gaub on 7/10/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include +#include +#include + +#include +#include + +#include "HiFiCodec.h" + +class HiFiCodecProvider : public QObject, public CodecProvider { + Q_OBJECT + Q_PLUGIN_METADATA(IID CodecProvider_iid FILE "plugin.json") + Q_INTERFACES(CodecProvider) + +public: + HiFiCodecProvider(QObject* parent = nullptr) : QObject(parent) {} + virtual ~HiFiCodecProvider() {} + + virtual CodecPluginList getCodecPlugins() override { + static std::once_flag once; + std::call_once(once, [&] { + + CodecPluginPointer hiFiCodec(new HiFiCodec()); + if (hiFiCodec->isSupported()) { + _codecPlugins.push_back(hiFiCodec); + } + + }); + return _codecPlugins; + } + +private: + CodecPluginList _codecPlugins; +}; + +#include "HiFiCodecProvider.moc" diff --git a/plugins/hifiCodec/src/plugin.json b/plugins/hifiCodec/src/plugin.json new file mode 100644 index 0000000000..df26a67ea8 --- /dev/null +++ b/plugins/hifiCodec/src/plugin.json @@ -0,0 +1 @@ +{"name":"HiFi 4:1 Audio Codec"} From 540ab9f07ca60357f57cd97bfe8f6562c46775fc Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 13:00:58 -0700 Subject: [PATCH 1019/1237] wire up audio codec --- plugins/hifiCodec/CMakeLists.txt | 2 +- plugins/hifiCodec/src/HiFiCodec.cpp | 42 ++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/plugins/hifiCodec/CMakeLists.txt b/plugins/hifiCodec/CMakeLists.txt index 50ba6de54e..da964bfdfd 100644 --- a/plugins/hifiCodec/CMakeLists.txt +++ b/plugins/hifiCodec/CMakeLists.txt @@ -12,7 +12,7 @@ if (WIN32) set(TARGET_NAME hifiCodec) setup_hifi_client_server_plugin() - link_hifi_libraries(shared plugins) + link_hifi_libraries(audio shared plugins) add_dependency_external_projects(HiFiAudioCodec) target_include_directories(${TARGET_NAME} PRIVATE ${HIFIAUDIOCODEC_INCLUDE_DIRS}) diff --git a/plugins/hifiCodec/src/HiFiCodec.cpp b/plugins/hifiCodec/src/HiFiCodec.cpp index 03a0b04bb8..3bb17a0275 100644 --- a/plugins/hifiCodec/src/HiFiCodec.cpp +++ b/plugins/hifiCodec/src/HiFiCodec.cpp @@ -11,7 +11,8 @@ #include -#include // should be from the external... +#include +#include #include @@ -39,28 +40,49 @@ bool HiFiCodec::isSupported() const { return true; } -class HiFiEncoder : public Encoder { +class HiFiEncoder : public Encoder, public AudioEncoder { public: - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { - encodedBuffer = decodedBuffer; + HiFiEncoder(int sampleRate, int numChannels) : AudioEncoder(sampleRate, numChannels) { + qDebug() << __FUNCTION__ << "sampleRate:" << sampleRate << "numChannels:" << numChannels; + _encodedSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * sizeof(int16_t) * numChannels) / 4; // codec reduces by 1/4th } + + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer.resize(_encodedSize); + AudioEncoder::process((const int16_t*)decodedBuffer.constData(), (int16_t*)encodedBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } +private: + int _encodedSize; }; -class HiFiDecoder : public Decoder { +class HiFiDecoder : public Decoder, public AudioDecoder { public: - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { - decodedBuffer = encodedBuffer; + HiFiDecoder(int sampleRate, int numChannels) : AudioDecoder(sampleRate, numChannels) { + qDebug() << __FUNCTION__ << "sampleRate:" << sampleRate << "numChannels:" << numChannels; + _decodedSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * sizeof(int16_t) * numChannels; } - virtual void trackLostFrames(int numFrames) override { } + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer.resize(_decodedSize); + AudioDecoder::process((const int16_t*)encodedBuffer.constData(), (int16_t*)decodedBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL, true); + } + + virtual void trackLostFrames(int numFrames) override { + QByteArray encodedBuffer; + QByteArray decodedBuffer; + decodedBuffer.resize(_decodedSize); + AudioDecoder::process((const int16_t*)encodedBuffer.constData(), (int16_t*)decodedBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL, false); + } +private: + int _decodedSize; }; Encoder* HiFiCodec::createEncoder(int sampleRate, int numChannels) { - return new HiFiEncoder(); + return new HiFiEncoder(sampleRate, numChannels); } Decoder* HiFiCodec::createDecoder(int sampleRate, int numChannels) { - return new HiFiDecoder(); + return new HiFiDecoder(sampleRate, numChannels); } void HiFiCodec::releaseEncoder(Encoder* encoder) { From 53a366d4e882c8255aeabc45f8c327f3e9c5671d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 13:25:00 -0700 Subject: [PATCH 1020/1237] measure velocity over 6 frames --- interface/src/avatar/AvatarActionHold.cpp | 23 ++++++++++++++++------- interface/src/avatar/AvatarActionHold.h | 4 +++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index d1750ddf53..ce0cf886d5 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -17,11 +17,14 @@ #include "CharacterController.h" const uint16_t AvatarActionHold::holdVersion = 1; +const int AvatarActionHold::velocitySmoothFrames = 6; + AvatarActionHold::AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntity) : ObjectActionSpring(id, ownerEntity) { _type = ACTION_TYPE_HOLD; + _measuredLinearVelocities.resize(AvatarActionHold::velocitySmoothFrames); #if WANT_DEBUG qDebug() << "AvatarActionHold::AvatarActionHold"; #endif @@ -168,7 +171,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm:: position = palmPosition + rotation * _relativePosition; // update linearVelocity based on offset via _relativePosition; - // linearVelocity = linearVelocity + glm::cross(angularVelocity, position - palmPosition); + linearVelocity = linearVelocity + glm::cross(angularVelocity, position - palmPosition); }); return true; @@ -205,16 +208,22 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { withWriteLock([&]{ if (_previousSet) { - _measuredLinearVelocity = (_positionalTarget - _previousPositionalTarget) / deltaTimeStep; - qDebug() << _measuredLinearVelocity.x << _measuredLinearVelocity.y << _measuredLinearVelocity.z - << deltaTimeStep; - } else { - _measuredLinearVelocity = glm::vec3(); + glm::vec3 oneFrameVelocity = (_positionalTarget - _previousPositionalTarget) / deltaTimeStep; + _measuredLinearVelocities[_measuredLinearVelocitiesIndex++] = oneFrameVelocity; + if (_measuredLinearVelocitiesIndex >= AvatarActionHold::velocitySmoothFrames) { + _measuredLinearVelocitiesIndex = 0; + } } + glm::vec3 measuredLinearVelocity; + for (int i = 0; i < AvatarActionHold::velocitySmoothFrames; i++) { + measuredLinearVelocity += _measuredLinearVelocities[i]; + } + measuredLinearVelocity /= (float)AvatarActionHold::velocitySmoothFrames; + if (_kinematicSetVelocity) { // rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); - rigidBody->setLinearVelocity(glmToBullet(_measuredLinearVelocity)); + rigidBody->setLinearVelocity(glmToBullet(measuredLinearVelocity)); rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget)); } diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index e246ac5f36..bfa392172d 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -65,7 +65,9 @@ private: // leaving this here for future refernece. // glm::quat _palmRotationFromRigidBody; - glm::vec3 _measuredLinearVelocity; + static const int velocitySmoothFrames; + QVector _measuredLinearVelocities; + int _measuredLinearVelocitiesIndex { 0 }; }; #endif // hifi_AvatarActionHold_h From 743576134f687322fc44911ebe861d0a2c73eb6d Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 13:38:08 -0700 Subject: [PATCH 1021/1237] fix warnings --- libraries/networking/src/udt/BasePacket.cpp | 2 +- plugins/pcmCodec/src/PCMCodecManager.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/networking/src/udt/BasePacket.cpp b/libraries/networking/src/udt/BasePacket.cpp index 0456e095ae..18552ca966 100644 --- a/libraries/networking/src/udt/BasePacket.cpp +++ b/libraries/networking/src/udt/BasePacket.cpp @@ -154,7 +154,7 @@ qint64 BasePacket::writeString(const QString& string) { QByteArray data = string.toUtf8(); uint32_t length = data.length(); writePrimitive(length); - auto result = writeData(data.constData(), data.length()); + writeData(data.constData(), data.length()); seek(pos() + length); return length + sizeof(uint32_t); } diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index 1cad1ea4e6..4a2380717d 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -62,11 +62,11 @@ Decoder* PCMCodec::createDecoder(int sampleRate, int numChannels) { } void PCMCodec::releaseEncoder(Encoder* encoder) { - delete encoder; + delete static_cast(encoder); } void PCMCodec::releaseDecoder(Decoder* decoder) { - delete decoder; + delete static_cast(decoder); } @@ -117,10 +117,10 @@ Decoder* zLibCodec::createDecoder(int sampleRate, int numChannels) { } void zLibCodec::releaseEncoder(Encoder* encoder) { - delete encoder; + delete static_cast(encoder); } void zLibCodec::releaseDecoder(Decoder* decoder) { - delete decoder; + delete static_cast(decoder); } From 9c17890035422ecbc0bc0031ecc72f8d6e699c0f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 14:18:28 -0700 Subject: [PATCH 1022/1237] try not including the most recent sample in the smoothed velocity --- interface/src/avatar/AvatarActionHold.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index ce0cf886d5..4583813131 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -207,6 +207,15 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { } withWriteLock([&]{ + glm::vec3 measuredLinearVelocity; + // there is a bit of lag between when someone releases the trigger and the software reacts to + // the release. we calculate the velocity from previous frames but not this frame (by having + // this code before where _measuredLinearVelocities is set, below) in order to help mask this. + for (int i = 0; i < AvatarActionHold::velocitySmoothFrames; i++) { + measuredLinearVelocity += _measuredLinearVelocities[i]; + } + measuredLinearVelocity /= (float)AvatarActionHold::velocitySmoothFrames; + if (_previousSet) { glm::vec3 oneFrameVelocity = (_positionalTarget - _previousPositionalTarget) / deltaTimeStep; _measuredLinearVelocities[_measuredLinearVelocitiesIndex++] = oneFrameVelocity; @@ -215,12 +224,6 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { } } - glm::vec3 measuredLinearVelocity; - for (int i = 0; i < AvatarActionHold::velocitySmoothFrames; i++) { - measuredLinearVelocity += _measuredLinearVelocities[i]; - } - measuredLinearVelocity /= (float)AvatarActionHold::velocitySmoothFrames; - if (_kinematicSetVelocity) { // rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); rigidBody->setLinearVelocity(glmToBullet(measuredLinearVelocity)); From 125aa6b337c303a24f7183709e27cc0ea07d4272 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 14:21:32 -0700 Subject: [PATCH 1023/1237] make zlib and pcm codecs not actually allocate encoder/decoders --- libraries/plugins/src/plugins/CodecPlugin.h | 2 + plugins/pcmCodec/src/PCMCodecManager.cpp | 48 ++++----------------- plugins/pcmCodec/src/PCMCodecManager.h | 45 +++++++++---------- 3 files changed, 30 insertions(+), 65 deletions(-) diff --git a/libraries/plugins/src/plugins/CodecPlugin.h b/libraries/plugins/src/plugins/CodecPlugin.h index 280853e37e..404f05e860 100644 --- a/libraries/plugins/src/plugins/CodecPlugin.h +++ b/libraries/plugins/src/plugins/CodecPlugin.h @@ -14,11 +14,13 @@ class Encoder { public: + virtual ~Encoder() { } virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) = 0; }; class Decoder { public: + virtual ~Decoder() { } virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; // numFrames - number of samples (mono) or sample-pairs (stereo) diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index 4a2380717d..315d0622ab 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -37,55 +37,24 @@ bool PCMCodec::isSupported() const { return true; } -class PCMEncoder : public Encoder { -public: - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { - encodedBuffer = decodedBuffer; - } -}; -class PCMDecoder : public Decoder { -public: - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { - decodedBuffer = encodedBuffer; - } - - virtual void trackLostFrames(int numFrames) override { } -}; Encoder* PCMCodec::createEncoder(int sampleRate, int numChannels) { - return new PCMEncoder(); + return this; } Decoder* PCMCodec::createDecoder(int sampleRate, int numChannels) { - return new PCMDecoder(); + return this; } void PCMCodec::releaseEncoder(Encoder* encoder) { - delete static_cast(encoder); + // do nothing } void PCMCodec::releaseDecoder(Decoder* decoder) { - delete static_cast(decoder); + // do nothing } - -class zLibEncoder : public Encoder { -public: - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { - encodedBuffer = qCompress(decodedBuffer); - } -}; - -class zLibDecoder : public Decoder { -public: - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { - decodedBuffer = qUncompress(encodedBuffer); - } - - virtual void trackLostFrames(int numFrames) override { } -}; - const QString zLibCodec::NAME = "zlib"; void zLibCodec::init() { @@ -103,24 +72,23 @@ void zLibCodec::deactivate() { CodecPlugin::deactivate(); } - bool zLibCodec::isSupported() const { return true; } Encoder* zLibCodec::createEncoder(int sampleRate, int numChannels) { - return new zLibEncoder(); + return this; } Decoder* zLibCodec::createDecoder(int sampleRate, int numChannels) { - return new zLibDecoder(); + return this; } void zLibCodec::releaseEncoder(Encoder* encoder) { - delete static_cast(encoder); + // do nothing... it wasn't allocated } void zLibCodec::releaseDecoder(Decoder* decoder) { - delete static_cast(decoder); + // do nothing... it wasn't allocated } diff --git a/plugins/pcmCodec/src/PCMCodecManager.h b/plugins/pcmCodec/src/PCMCodecManager.h index 6eb7f44c15..55d7c866f1 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.h +++ b/plugins/pcmCodec/src/PCMCodecManager.h @@ -14,30 +14,7 @@ #include -/* -class Encoder { -public: -virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) = 0; -}; - -class Decoder { -public: -virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; - -// numFrames - number of samples (mono) or sample-pairs (stereo) -virtual void trackLostFrames(int numFrames) = 0; -}; - -class CodecPlugin : public Plugin { -public: -virtual Encoder* createEncoder(int sampleRate, int numChannels) = 0; -virtual Decoder* createDecoder(int sampleRate, int numChannels) = 0; -virtual void releaseEncoder(Encoder* encoder) = 0; -virtual void releaseDecoder(Decoder* decoder) = 0; -}; -*/ - -class PCMCodec : public CodecPlugin { +class PCMCodec : public CodecPlugin, public Encoder, public Decoder { Q_OBJECT public: @@ -58,11 +35,20 @@ public: virtual void releaseEncoder(Encoder* encoder) override; virtual void releaseDecoder(Decoder* decoder) override; + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer = decodedBuffer; + } + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer = encodedBuffer; + } + + virtual void trackLostFrames(int numFrames) override { } + private: static const QString NAME; }; -class zLibCodec : public CodecPlugin { +class zLibCodec : public CodecPlugin, public Encoder, public Decoder { Q_OBJECT public: @@ -83,6 +69,15 @@ public: virtual void releaseEncoder(Encoder* encoder) override; virtual void releaseDecoder(Decoder* decoder) override; + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer = qCompress(decodedBuffer); + } + + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer = qUncompress(encodedBuffer); + } + + virtual void trackLostFrames(int numFrames) override { } private: static const QString NAME; From b0f1492ac7b3844fd5440dc9542069bdfe687ea5 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 15:08:00 -0700 Subject: [PATCH 1024/1237] ignore 3 frames for current velocity rather than just 1 --- interface/src/avatar/AvatarActionHold.cpp | 34 +++++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 4583813131..3ab81c5462 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -207,15 +207,6 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { } withWriteLock([&]{ - glm::vec3 measuredLinearVelocity; - // there is a bit of lag between when someone releases the trigger and the software reacts to - // the release. we calculate the velocity from previous frames but not this frame (by having - // this code before where _measuredLinearVelocities is set, below) in order to help mask this. - for (int i = 0; i < AvatarActionHold::velocitySmoothFrames; i++) { - measuredLinearVelocity += _measuredLinearVelocities[i]; - } - measuredLinearVelocity /= (float)AvatarActionHold::velocitySmoothFrames; - if (_previousSet) { glm::vec3 oneFrameVelocity = (_positionalTarget - _previousPositionalTarget) / deltaTimeStep; _measuredLinearVelocities[_measuredLinearVelocitiesIndex++] = oneFrameVelocity; @@ -224,6 +215,31 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { } } + glm::vec3 measuredLinearVelocity; + for (int i = 0; i < AvatarActionHold::velocitySmoothFrames; i++) { + // there is a bit of lag between when someone releases the trigger and when the software reacts to + // the release. we calculate the velocity from previous frames but we don't include several + // of the most recent. + // + // if _measuredLinearVelocitiesIndex is + // 0 -- ignore i of 3 4 5 + // 1 -- ignore i of 4 5 0 + // 2 -- ignore i of 5 0 1 + // 3 -- ignore i of 0 1 2 + // 4 -- ignore i of 1 2 3 + // 5 -- ignore i of 2 3 4 + if ((i + 1) % 6 == _measuredLinearVelocitiesIndex || + (i + 2) % 6 == _measuredLinearVelocitiesIndex || + (i + 3) % 6 == _measuredLinearVelocitiesIndex) { + continue; + } + + measuredLinearVelocity += _measuredLinearVelocities[i]; + } + measuredLinearVelocity /= (float)AvatarActionHold::velocitySmoothFrames; + + + if (_kinematicSetVelocity) { // rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); rigidBody->setLinearVelocity(glmToBullet(measuredLinearVelocity)); From dc9c06a159810f0f3fdc039fd9896c1f172042ac Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 11 Jul 2016 15:14:19 -0700 Subject: [PATCH 1025/1237] clean util --- .../developer/utilities/tools/overlayFinder.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/developer/utilities/tools/overlayFinder.js b/scripts/developer/utilities/tools/overlayFinder.js index 81ee687cbe..75b8aeacfa 100644 --- a/scripts/developer/utilities/tools/overlayFinder.js +++ b/scripts/developer/utilities/tools/overlayFinder.js @@ -1,17 +1,17 @@ function mousePressEvent(event) { - var overlay = Overlays.getOverlayAtPoint({x:event.x,y:event.y}); - print('overlay is: ' + overlay) - // var pickRay = Camera.computePickRay(event.x, event.y); - - // var intersection = Overlays.findRayIntersection(pickRay); - // print('intersection is: ' + intersection) + var overlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + // var pickRay = Camera.computePickRay(event.x, event.y); + // var intersection = Overlays.findRayIntersection(pickRay); + // print('intersection is: ' + intersection) }; -Controller.mouseMoveEvent.connect(function(event){ - print('mouse press') +Controller.mousePressEvent.connect(function(event) { mousePressEvent(event) }); -Script.scriptEnding.connect(function(){ +Script.scriptEnding.connect(function() { Controller.mousePressEvent.disconnect(mousePressEvent); }) \ No newline at end of file From ceffa219b3bec454f175eed596aef5b897b5ada6 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 15:18:02 -0700 Subject: [PATCH 1026/1237] support for lost packets --- libraries/audio/src/InboundAudioStream.cpp | 6 +++++- plugins/hifiCodec/src/HiFiCodec.cpp | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 144005bc11..c9b9363b1b 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -172,7 +172,7 @@ int InboundAudioStream::parseStreamProperties(PacketType type, const QByteArray& return sizeof(quint16); } else { // mixed audio packets do not have any info between the seq num and the audio data. - numAudioSamples = packetAfterSeqNum.size() / sizeof(int16_t); + numAudioSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; return 0; } } @@ -189,6 +189,10 @@ int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packet } int InboundAudioStream::writeDroppableSilentSamples(int silentSamples) { + if (_decoder) { + _decoder->trackLostFrames(silentSamples); + } + // calculate how many silent frames we should drop. int samplesPerFrame = _ringBuffer.getNumFrameSamples(); int desiredJitterBufferFramesPlusPadding = _desiredJitterBufferFrames + DESIRED_JITTER_BUFFER_FRAMES_PADDING; diff --git a/plugins/hifiCodec/src/HiFiCodec.cpp b/plugins/hifiCodec/src/HiFiCodec.cpp index 3bb17a0275..4e9336ff90 100644 --- a/plugins/hifiCodec/src/HiFiCodec.cpp +++ b/plugins/hifiCodec/src/HiFiCodec.cpp @@ -43,7 +43,6 @@ bool HiFiCodec::isSupported() const { class HiFiEncoder : public Encoder, public AudioEncoder { public: HiFiEncoder(int sampleRate, int numChannels) : AudioEncoder(sampleRate, numChannels) { - qDebug() << __FUNCTION__ << "sampleRate:" << sampleRate << "numChannels:" << numChannels; _encodedSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * sizeof(int16_t) * numChannels) / 4; // codec reduces by 1/4th } @@ -58,7 +57,6 @@ private: class HiFiDecoder : public Decoder, public AudioDecoder { public: HiFiDecoder(int sampleRate, int numChannels) : AudioDecoder(sampleRate, numChannels) { - qDebug() << __FUNCTION__ << "sampleRate:" << sampleRate << "numChannels:" << numChannels; _decodedSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * sizeof(int16_t) * numChannels; } @@ -71,6 +69,7 @@ public: QByteArray encodedBuffer; QByteArray decodedBuffer; decodedBuffer.resize(_decodedSize); + // NOTE: we don't actually use the results of this decode, we just do it to keep the state of the codec clean AudioDecoder::process((const int16_t*)encodedBuffer.constData(), (int16_t*)decodedBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL, false); } private: From 1ef26d39eb43162f1fe5253841ab5ad56bf79a8c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 15:31:15 -0700 Subject: [PATCH 1027/1237] blah --- interface/src/avatar/AvatarActionHold.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 3ab81c5462..ce623cf604 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -236,7 +236,7 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { measuredLinearVelocity += _measuredLinearVelocities[i]; } - measuredLinearVelocity /= (float)AvatarActionHold::velocitySmoothFrames; + measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 3); From 0d0e0925e5b27f4944bc4cc074cf3e333da49386 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 16:00:55 -0700 Subject: [PATCH 1028/1237] try skipping 2 rather than 3 --- interface/src/avatar/AvatarActionHold.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index ce623cf604..3bbdf9f099 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -222,21 +222,20 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { // of the most recent. // // if _measuredLinearVelocitiesIndex is - // 0 -- ignore i of 3 4 5 - // 1 -- ignore i of 4 5 0 - // 2 -- ignore i of 5 0 1 - // 3 -- ignore i of 0 1 2 - // 4 -- ignore i of 1 2 3 - // 5 -- ignore i of 2 3 4 + // 0 -- ignore i of 4 5 + // 1 -- ignore i of 5 0 + // 2 -- ignore i of 0 1 + // 3 -- ignore i of 1 2 + // 4 -- ignore i of 2 3 + // 5 -- ignore i of 3 4 if ((i + 1) % 6 == _measuredLinearVelocitiesIndex || - (i + 2) % 6 == _measuredLinearVelocitiesIndex || - (i + 3) % 6 == _measuredLinearVelocitiesIndex) { + (i + 2) % 6 == _measuredLinearVelocitiesIndex) { continue; } measuredLinearVelocity += _measuredLinearVelocities[i]; } - measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 3); + measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 2); From 84c5bef487fe6d16e38c3aa4e8ffd9b6f5802f8a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 17:02:11 -0700 Subject: [PATCH 1029/1237] back to ignore more recent 3 frames. don't have released entities collide with myAvatar until 0.25 seconds after release --- interface/src/avatar/AvatarActionHold.cpp | 20 ++++++++--------- .../system/controllers/handControllerGrab.js | 22 ++++++++++++++++++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 3bbdf9f099..32905361dd 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -222,22 +222,20 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { // of the most recent. // // if _measuredLinearVelocitiesIndex is - // 0 -- ignore i of 4 5 - // 1 -- ignore i of 5 0 - // 2 -- ignore i of 0 1 - // 3 -- ignore i of 1 2 - // 4 -- ignore i of 2 3 - // 5 -- ignore i of 3 4 + // 0 -- ignore i of 3 4 5 + // 1 -- ignore i of 4 5 0 + // 2 -- ignore i of 5 0 1 + // 3 -- ignore i of 0 1 2 + // 4 -- ignore i of 1 2 3 + // 5 -- ignore i of 2 3 4 if ((i + 1) % 6 == _measuredLinearVelocitiesIndex || - (i + 2) % 6 == _measuredLinearVelocitiesIndex) { + (i + 2) % 6 == _measuredLinearVelocitiesIndex || + (i + 3) % 6 == _measuredLinearVelocitiesIndex) { continue; } - measuredLinearVelocity += _measuredLinearVelocities[i]; } - measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 2); - - + measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 3); if (_kinematicSetVelocity) { // rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2cef48290c..36cef8ec2d 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -33,6 +33,8 @@ var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; +var COLLIDE_WITH_AV_AFTER_RELEASE_DELAY = 0.25; // seconds + var BUMPER_ON_VALUE = 0.5; var THUMB_ON_VALUE = 0.5; @@ -263,6 +265,17 @@ function propsArePhysical(props) { return isPhysical; } +function removeMyAvatarFromCollidesWith(origCollidesWith) { + var collidesWithSplit = origCollidesWith.split(","); + // remove myAvatar from the array + for (var i = collidesWithSplit.length - 1; i >= 0; i--) { + if (collidesWithSplit[i] === "myAvatar") { + collidesWithSplit.splice(i, 1); + } + } + return collidesWithSplit.join(); +} + // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; @@ -2128,13 +2141,20 @@ function MyController(hand) { if (data["refCount"] < 1) { deactiveProps = { gravity: data["gravity"], - collidesWith: data["collidesWith"], + // don't set collidesWith back right away, because thrown things tend to bounce off the + // avatar's capsule. + collidesWith: removeMyAvatarFromCollidesWith(data["collidesWith"]), collisionless: data["collisionless"], dynamic: data["dynamic"], parentID: data["parentID"], parentJointIndex: data["parentJointIndex"] }; + Script.setTimeout(function () { + // set collidesWith back to original value a bit later than the rest + Entities.editEntity(entityID, { collidesWith: data["collidesWith"] }); + }, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY); + // things that are held by parenting and dropped with no velocity will end up as "static" in bullet. If // it looks like the dropped thing should fall, give it a little velocity. var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]); From e32929dce2742336b84310cf2f0a729e59307319 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 17:40:07 -0700 Subject: [PATCH 1030/1237] added support for material map in FST file --- libraries/fbx/src/FBXReader.cpp | 2 +- libraries/fbx/src/FBXReader.h | 2 +- libraries/fbx/src/FBXReader_Material.cpp | 27 ++++++++++++++++-------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 8c9cdb2110..6afaf933f2 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1366,7 +1366,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.meshExtents.reset(); // Create the Material Library - consolidateFBXMaterials(); + consolidateFBXMaterials(mapping); geometry.materials = _fbxMaterials; // see if any materials have texture children diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 606c6832d0..d2d433d53e 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -444,7 +444,7 @@ public: QHash _fbxMaterials; - void consolidateFBXMaterials(); + void consolidateFBXMaterials(const QVariantHash& mapping); bool _loadLightmaps = true; float _lightmapOffset = 0.0f; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index 0517dcd4ec..48b210b88e 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include "FBXReader.h" #include @@ -67,9 +69,13 @@ FBXTexture FBXReader::getTexture(const QString& textureID) { return texture; } -void FBXReader::consolidateFBXMaterials() { - - // foreach (const QString& materialID, materials) { +void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { + + QString materialMapString = mapping.value("materialMap").toString(); + QJsonDocument materialMapDocument = QJsonDocument::fromJson(materialMapString.toUtf8()); + QJsonObject materialMap = materialMapDocument.object(); + + // foreach (const QString& materialID, materials) { for (QHash::iterator it = _fbxMaterials.begin(); it != _fbxMaterials.end(); it++) { FBXMaterial& material = (*it); @@ -253,12 +259,15 @@ void FBXReader::consolidateFBXMaterials() { } } - if (material.name.contains("body_mat") || material.name.contains("skin")) { - material._material->setScattering(1.0); - if (!material.emissiveTexture.isNull()) { - material.scatteringTexture = material.emissiveTexture; - material.emissiveTexture = FBXTexture(); - } + if (materialMap.contains(material.name)) { + QJsonObject materialOptions = materialMap.value(material.name).toObject(); + float scattering = materialOptions.contains("scattering") ? materialOptions.value("scattering").toDouble() : 1.0f; + QByteArray scatteringMap = materialOptions.value("scatteringMap").toVariant().toByteArray(); + qDebug() << "Replacing material:" << material.name << "with skin scattering effect. scattering:" << scattering << "scatteringMap:" << scatteringMap; + material._material->setScattering(scattering); + material.scatteringTexture = FBXTexture(); + material.scatteringTexture.name = material.name + ".scatteringMap"; + material.scatteringTexture.filename = scatteringMap; } if (material.opacity <= 0.0f) { From 3ddac737182b2a54bbcadc2b3f85dee5a88d5a3f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 18:24:22 -0700 Subject: [PATCH 1031/1237] add mac lib to the external project --- cmake/externals/hifiAudioCodec/CMakeLists.txt | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/cmake/externals/hifiAudioCodec/CMakeLists.txt b/cmake/externals/hifiAudioCodec/CMakeLists.txt index 5c563774b7..3a8c714d79 100644 --- a/cmake/externals/hifiAudioCodec/CMakeLists.txt +++ b/cmake/externals/hifiAudioCodec/CMakeLists.txt @@ -7,8 +7,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://s3.amazonaws.com/hifi-public/dependencies/codecSDK.zip - URL_MD5 4add25b7cc5dfdb3cbfc1156b95cfff7 + URL https://s3.amazonaws.com/hifi-public/dependencies/codecSDK-1.zip + URL_MD5 23ec3fe51eaa155ea159a4971856fc13 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" @@ -24,27 +24,10 @@ set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include CACHE TYPE INTERNA if (WIN32) set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/audio.lib CACHE TYPE INTERNAL) - - # FIXME need to account for different architectures - #if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") - # set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/win64/audio.lib CACHE TYPE INTERNAL) - # add_paths_to_fixup_libs(${SOURCE_DIR}/bin/win64) - #else() - # set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/win32/audio.lib CACHE TYPE INTERNAL) - # add_paths_to_fixup_libs(${SOURCE_DIR}/bin/win32) - #endif() - elseif(APPLE) - - # FIXME need to account for different architectures - #set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/osx32/audio.dylib CACHE TYPE INTERNAL) - #add_paths_to_fixup_libs(${SOURCE_DIR}/bin/osx32) - + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/libaudio.a CACHE TYPE INTERNAL) elseif(NOT ANDROID) - # FIXME need to account for different architectures #set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/linux64/audio.so CACHE TYPE INTERNAL) - #add_paths_to_fixup_libs(${SOURCE_DIR}/bin/linux64) - endif() From 6f0967f3fc444ebbe1e7a6b013a0c9182f6209be Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 11 Jul 2016 18:28:04 -0700 Subject: [PATCH 1032/1237] add mac support to the codec plugin --- plugins/hifiCodec/CMakeLists.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/hifiCodec/CMakeLists.txt b/plugins/hifiCodec/CMakeLists.txt index da964bfdfd..3939529c3e 100644 --- a/plugins/hifiCodec/CMakeLists.txt +++ b/plugins/hifiCodec/CMakeLists.txt @@ -6,9 +6,7 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # - - -if (WIN32) +if (WIN32 OR APPLE) set(TARGET_NAME hifiCodec) setup_hifi_client_server_plugin() From ba7590712a928a0a5ecd7186db91fab800adef39 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 11 Jul 2016 18:33:57 -0700 Subject: [PATCH 1033/1237] BRinging the intermediate render textures in the pipeline as varyings and not throug the singletonn anymore --- interface/src/Application.cpp | 2 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 2 + .../src/AmbientOcclusionEffect.cpp | 2 + .../render-utils/src/AntialiasingEffect.cpp | 23 +++---- .../render-utils/src/AntialiasingEffect.h | 4 +- .../render-utils/src/DebugDeferredBuffer.cpp | 15 +++-- .../render-utils/src/DebugDeferredBuffer.h | 3 +- .../render-utils/src/DeferredFrameTransform.h | 2 +- .../src/DeferredLightingEffect.cpp | 67 +++++++++++++++---- .../render-utils/src/DeferredLightingEffect.h | 22 +++++- .../render-utils/src/FramebufferCache.cpp | 36 +++++----- libraries/render-utils/src/FramebufferCache.h | 11 ++- .../render-utils/src/RenderDeferredTask.cpp | 52 ++++++++------ .../render-utils/src/RenderDeferredTask.h | 9 +-- .../render-utils/src/SubsurfaceScattering.cpp | 12 ++-- .../render-utils/src/SubsurfaceScattering.h | 3 +- .../render-utils/src/SurfaceGeometryPass.cpp | 8 ++- .../render-utils/src/SurfaceGeometryPass.h | 6 +- .../render-utils/src/ToneMappingEffect.cpp | 28 ++++---- .../render-utils/src/ToneMappingEffect.h | 8 ++- libraries/render/src/render/Task.h | 37 ++++++++++ tests/gpu-test/src/TestWindow.cpp | 6 +- tests/gpu-test/src/TestWindow.h | 1 + 23 files changed, 242 insertions(+), 117 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b58fa57d3f..c8d5739a6c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1674,7 +1674,7 @@ void Application::paintGL() { auto inputs = AvatarInputs::getInstance(); if (inputs->mirrorVisible()) { PerformanceTimer perfTimer("Mirror"); - auto primaryFbo = DependencyManager::get()->getPrimaryFramebuffer(); + // auto primaryFbo = DependencyManager::get()->getPrimaryFramebuffer(); renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index e0c87fbbed..cd1c8eedd1 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -578,6 +578,8 @@ void OpenGLDisplayPlugin::present() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) + glEnable(GL_FRAMEBUFFER_SRGB); + updateTextures(); if (_currentSceneTexture) { // Write all layers to a local framebuffer diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 3b0e4c8d18..4b283731d2 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -283,6 +283,7 @@ void AmbientOcclusionEffect::updateGaussianDistribution() { } void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { +#ifdef FIX_THE_FRAMEBUFFER_CACHE assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -406,4 +407,5 @@ void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext // Update the timer std::static_pointer_cast(renderContext->jobConfig)->gpuTime = _gpuTimer.getAverage(); +#endif } diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index fc09f8431d..2f273f6202 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -32,6 +32,9 @@ Antialiasing::Antialiasing() { } const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { + int width = DependencyManager::get()->getFrameBufferSize().width(); + int height = DependencyManager::get()->getFrameBufferSize().height(); + if (!_antialiasingPipeline) { auto vs = gpu::Shader::createVertex(std::string(fxaa_vert)); auto ps = gpu::Shader::createPixel(std::string(fxaa_frag)); @@ -49,11 +52,8 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { state->setDepthTest(false, false, gpu::LESS_EQUAL); // Link the antialiasing FBO to texture - _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, - DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); - auto format = DependencyManager::get()->getLightingTexture()->getTexelFormat(); - auto width = _antialiasingBuffer->getWidth(); - auto height = _antialiasingBuffer->getHeight(); + _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); @@ -62,10 +62,8 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingPipeline = gpu::Pipeline::create(program, state); } - int w = DependencyManager::get()->getFrameBufferSize().width(); - int h = DependencyManager::get()->getFrameBufferSize().height(); - if (w != _antialiasingBuffer->getWidth() || h != _antialiasingBuffer->getHeight()) { - _antialiasingBuffer->resize(w, h); + if (width != _antialiasingBuffer->getWidth() || height != _antialiasingBuffer->getHeight()) { + _antialiasingBuffer->resize(width, height); } return _antialiasingPipeline; @@ -92,7 +90,7 @@ const gpu::PipelinePointer& Antialiasing::getBlendPipeline() { return _blendPipeline; } -void Antialiasing::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { +void Antialiasing::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceBuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -126,7 +124,7 @@ void Antialiasing::run(const render::SceneContextPointer& sceneContext, const re // FXAA step getAntialiasingPipeline(); - batch.setResourceTexture(0, framebufferCache->getLightingTexture()); + batch.setResourceTexture(0, sourceBuffer->getRenderBuffer(0)); batch.setFramebuffer(_antialiasingBuffer); batch.setPipeline(getAntialiasingPipeline()); @@ -152,10 +150,11 @@ void Antialiasing::run(const render::SceneContextPointer& sceneContext, const re glm::vec2 texCoordBottomRight(1.0f, 1.0f); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + // Blend step getBlendPipeline(); batch.setResourceTexture(0, _antialiasingTexture); - batch.setFramebuffer(framebufferCache->getLightingFramebuffer()); + batch.setFramebuffer(sourceBuffer); batch.setPipeline(getBlendPipeline()); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); diff --git a/libraries/render-utils/src/AntialiasingEffect.h b/libraries/render-utils/src/AntialiasingEffect.h index 6386622675..f0992ed843 100644 --- a/libraries/render-utils/src/AntialiasingEffect.h +++ b/libraries/render-utils/src/AntialiasingEffect.h @@ -26,11 +26,11 @@ public: class Antialiasing { public: using Config = AntiAliasingConfig; - using JobModel = render::Job::Model; + using JobModel = render::Job::ModelI; Antialiasing(); void configure(const Config& config) {} - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceBuffer); const gpu::PipelinePointer& getAntialiasingPipeline(); const gpu::PipelinePointer& getBlendPipeline(); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 2fd42ccacd..a1ad480170 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -351,8 +351,9 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; - auto& diffusedCurvatureFramebuffer = inputs.get0(); - auto& scatteringFramebuffer = inputs.get1(); + auto& deferredFramebuffer = inputs.get0(); + auto& diffusedCurvatureFramebuffer = inputs.get1(); + auto& scatteringFramebuffer = inputs.get2(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); @@ -376,11 +377,11 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setPipeline(getPipeline(_mode, first)); - batch.setResourceTexture(Albedo, framebufferCache->getDeferredColorTexture()); - batch.setResourceTexture(Normal, framebufferCache->getDeferredNormalTexture()); - batch.setResourceTexture(Specular, framebufferCache->getDeferredSpecularTexture()); - batch.setResourceTexture(Depth, framebufferCache->getPrimaryDepthTexture()); - batch.setResourceTexture(Lighting, framebufferCache->getLightingTexture()); + batch.setResourceTexture(Albedo, deferredFramebuffer->getDeferredColorTexture()); + batch.setResourceTexture(Normal, deferredFramebuffer->getDeferredNormalTexture()); + batch.setResourceTexture(Specular, deferredFramebuffer->getDeferredSpecularTexture()); + batch.setResourceTexture(Depth, deferredFramebuffer->getPrimaryDepthTexture()); + batch.setResourceTexture(Lighting, deferredFramebuffer->getLightingTexture()); batch.setResourceTexture(Shadow, lightStage.lights[0]->shadow.framebuffer->getDepthStencilBuffer()); batch.setResourceTexture(Pyramid, framebufferCache->getDepthPyramidTexture()); batch.setResourceTexture(Curvature, framebufferCache->getCurvatureTexture()); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 61ebec8954..09e0380e46 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -15,6 +15,7 @@ #include #include +#include "DeferredFramebuffer.h" class DebugDeferredBufferConfig : public render::Job::Config { Q_OBJECT @@ -34,7 +35,7 @@ signals: class DebugDeferredBuffer { public: - using Inputs = render::VaryingSet2; + using Inputs = render::VaryingSet3; using Config = DebugDeferredBufferConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/DeferredFrameTransform.h b/libraries/render-utils/src/DeferredFrameTransform.h index 70ac5fca92..1abd912f15 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.h +++ b/libraries/render-utils/src/DeferredFrameTransform.h @@ -75,4 +75,4 @@ public: private: }; -#endif // hifi_SurfaceGeometryPass_h +#endif // hifi_DeferredFrameTransform_h diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 59c5012147..12408f121f 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -328,8 +328,46 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { return _spotLightMesh; } -void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { +void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, gpu::FramebufferPointer& primaryFramebuffer) { auto args = renderContext->args; + + glm::ivec2 frameSize(args->_viewport.z, args->_viewport.w); + + if (!_primaryFramebuffer) { + _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + auto colorFormat = gpu::Element::COLOR_SRGBA_32; + + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + + + _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); + + + auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format + auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + + _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); + + } + _primaryFramebuffer->resize(frameSize.x, frameSize.y); + + primaryFramebuffer = _primaryFramebuffer; +} + +void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& primaryFramebuffer, Outputs& output) { + auto args = renderContext->args; + + if (!_deferredFramebuffer) { + _deferredFramebuffer = std::make_shared(); + } + _deferredFramebuffer->setPrimaryDepth(primaryFramebuffer->getDepthStencilBuffer()); + _deferredFramebuffer->setFrameSize(glm::ivec2(args->_viewport.z, args->_viewport.w)); + + output.edit0() = _deferredFramebuffer; + output.edit1() = _deferredFramebuffer->getLightingFramebuffer(); + + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); batch.setViewportTransform(args->_viewport); @@ -342,13 +380,13 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(vec3(0), 0), true); */ // Clear deferred - auto deferredFbo = DependencyManager::get()->getDeferredFramebuffer(); - + // auto deferredFbo = DependencyManager::get()->getDeferredFramebuffer(); + auto deferredFbo = _deferredFramebuffer->getDeferredFramebuffer(); batch.setFramebuffer(deferredFbo); // Clear Color, Depth and Stencil for deferred buffer batch.clearFramebuffer( - gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_COLOR1 | gpu::Framebuffer::BUFFER_COLOR2 | + gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_COLOR1 | gpu::Framebuffer::BUFFER_COLOR2 | gpu::Framebuffer::BUFFER_COLOR3 | gpu::Framebuffer::BUFFER_DEPTH | gpu::Framebuffer::BUFFER_STENCIL, vec4(vec3(0), 0), 1.0, 0.0, true); @@ -358,6 +396,7 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, + const DeferredFramebufferPointer& deferredFramebuffer, const LightingModelPointer& lightingModel, const gpu::TexturePointer& diffusedCurvature2, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource) { @@ -370,11 +409,12 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c // perform deferred lighting, rendering to free fbo auto framebufferCache = DependencyManager::get(); + auto textureCache = DependencyManager::get(); auto deferredLightingEffect = DependencyManager::get(); // binding the first framebuffer - auto lightingFBO = framebufferCache->getLightingFramebuffer(); + auto lightingFBO = deferredFramebuffer->getLightingFramebuffer(); batch.setFramebuffer(lightingFBO); batch.setViewportTransform(args->_viewport); @@ -382,10 +422,10 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c // Bind the G-Buffer surfaces - batch.setResourceTexture(DEFERRED_BUFFER_COLOR_UNIT, framebufferCache->getDeferredColorTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_NORMAL_UNIT, framebufferCache->getDeferredNormalTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_EMISSIVE_UNIT, framebufferCache->getDeferredSpecularTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_DEPTH_UNIT, framebufferCache->getPrimaryDepthTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_COLOR_UNIT, deferredFramebuffer->getDeferredColorTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_NORMAL_UNIT, deferredFramebuffer->getDeferredNormalTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_EMISSIVE_UNIT, deferredFramebuffer->getDeferredSpecularTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_DEPTH_UNIT, deferredFramebuffer->getPrimaryDepthTexture()); // FIXME: Different render modes should have different tasks if (args->_renderMode == RenderArgs::DEFAULT_RENDER_MODE && deferredLightingEffect->isAmbientOcclusionEnabled()) { @@ -631,11 +671,12 @@ void RenderDeferred::configure(const Config& config) { void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { auto deferredTransform = inputs.get0(); - auto lightingModel = inputs.get1(); - auto diffusedCurvature2 = inputs.get2()->getRenderBuffer(0); - auto subsurfaceScatteringResource = inputs.get4(); + auto deferredFramebuffer = inputs.get1(); + auto lightingModel = inputs.get2(); + auto diffusedCurvature2 = inputs.get3()->getRenderBuffer(0); + auto subsurfaceScatteringResource = inputs.get5(); - setupJob.run(sceneContext, renderContext, deferredTransform, lightingModel, diffusedCurvature2, subsurfaceScatteringResource); + setupJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel, diffusedCurvature2, subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform, lightingModel->isPointLightEnabled(), lightingModel->isSpotLightEnabled()); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 9fb28919d6..b850cce040 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -24,6 +24,7 @@ #include #include "DeferredFrameTransform.h" +#include "DeferredFramebuffer.h" #include "LightingModel.h" #include "LightStage.h" @@ -105,11 +106,25 @@ private: friend class RenderDeferredCleanup; }; +class PreparePrimaryFramebuffer { +public: + using JobModel = render::Job::ModelO; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::FramebufferPointer& primaryFramebuffer); + + gpu::FramebufferPointer _primaryFramebuffer; +}; + class PrepareDeferred { public: - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + // Output: DeferredFramebuffer, LightingFramebuffer + using Outputs = render::VaryingSet2; - using JobModel = render::Job::Model; + using JobModel = render::Job::ModelIO; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& primaryFramebuffer, Outputs& output); + + DeferredFramebufferPointer _deferredFramebuffer; }; class RenderDeferredSetup { @@ -118,6 +133,7 @@ public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, + const DeferredFramebufferPointer& deferredFramebuffer, const LightingModelPointer& lightingModel, const gpu::TexturePointer& diffusedCurvature2, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource); @@ -151,7 +167,7 @@ signals: class RenderDeferred { public: - using Inputs = render::VaryingSet5 < DeferredFrameTransformPointer, LightingModelPointer, gpu::FramebufferPointer, gpu::FramebufferPointer, SubsurfaceScatteringResourcePointer>; + using Inputs = render::VaryingSet6 < DeferredFrameTransformPointer, DeferredFramebufferPointer, LightingModelPointer, gpu::FramebufferPointer, gpu::FramebufferPointer, SubsurfaceScatteringResourcePointer>; using Config = RenderDeferredConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 602e559114..4c9d02ca41 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -33,7 +33,7 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; - _primaryFramebuffer.reset(); + /* _primaryFramebuffer.reset(); _primaryDepthTexture.reset(); _primaryColorTexture.reset(); _deferredFramebuffer.reset(); @@ -41,11 +41,11 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { _deferredColorTexture.reset(); _deferredNormalTexture.reset(); _deferredSpecularTexture.reset(); - _selfieFramebuffer.reset(); + */ _selfieFramebuffer.reset(); _cachedFramebuffers.clear(); - _lightingTexture.reset(); + /* _lightingTexture.reset(); _lightingFramebuffer.reset(); - _depthPyramidFramebuffer.reset(); + */ _depthPyramidFramebuffer.reset(); _depthPyramidTexture.reset(); _curvatureFramebuffer.reset(); _curvatureTexture.reset(); @@ -57,17 +57,17 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { - _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + /* _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create()); - + */ auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto linearFormat = gpu::Element::COLOR_RGBA_32; auto width = _frameBufferSize.width(); auto height = _frameBufferSize.height(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + /* _primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); _primaryFramebuffer->setRenderBuffer(0, _primaryColorTexture); @@ -81,10 +81,10 @@ void FramebufferCache::createPrimaryFramebuffer() { _deferredFramebuffer->setRenderBuffer(2, _deferredSpecularTexture); _deferredFramebufferDepthColor->setRenderBuffer(0, _deferredColorTexture); - + */ // auto depthFormat = gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); +/* _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); _primaryFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -92,31 +92,31 @@ void FramebufferCache::createPrimaryFramebuffer() { _deferredFramebufferDepthColor->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - +*/ _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); _selfieFramebuffer->setRenderBuffer(0, tex); auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + /* _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); _deferredFramebuffer->setRenderBuffer(3, _lightingTexture); - + */ // For AO: auto pointMipSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT); _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _depthPyramidFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _depthPyramidFramebuffer->setRenderBuffer(0, _depthPyramidTexture); - _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + // _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + // _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); resizeAmbientOcclusionBuffers(); } @@ -138,14 +138,14 @@ void FramebufferCache::resizeAmbientOcclusionBuffers() { _occlusionTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); - _occlusionFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + // _occlusionFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); - _occlusionBlurredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + // _occlusionBlurredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); } - +/* gpu::FramebufferPointer FramebufferCache::getPrimaryFramebuffer() { if (!_primaryFramebuffer) { createPrimaryFramebuffer(); @@ -214,7 +214,7 @@ gpu::TexturePointer FramebufferCache::getLightingTexture() { createPrimaryFramebuffer(); } return _lightingTexture; -} +}*/ gpu::FramebufferPointer FramebufferCache::getFramebuffer() { if (_cachedFramebuffers.isEmpty()) { diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index ab1d0a555b..9f3b8e504b 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -32,7 +32,7 @@ public: /// Returns a pointer to the primary framebuffer object. This render target includes a depth component, and is /// used for scene rendering. - gpu::FramebufferPointer getPrimaryFramebuffer(); +/* gpu::FramebufferPointer getPrimaryFramebuffer(); gpu::TexturePointer getPrimaryDepthTexture(); gpu::TexturePointer getPrimaryColorTexture(); @@ -43,26 +43,23 @@ public: gpu::TexturePointer getDeferredColorTexture(); gpu::TexturePointer getDeferredNormalTexture(); gpu::TexturePointer getDeferredSpecularTexture(); - + */ gpu::FramebufferPointer getDepthPyramidFramebuffer(); gpu::TexturePointer getDepthPyramidTexture(); gpu::FramebufferPointer getCurvatureFramebuffer(); gpu::TexturePointer getCurvatureTexture(); - gpu::FramebufferPointer getScatteringFramebuffer(); - gpu::TexturePointer getScatteringTexture(); - void setAmbientOcclusionResolutionLevel(int level); gpu::FramebufferPointer getOcclusionFramebuffer(); gpu::TexturePointer getOcclusionTexture(); gpu::FramebufferPointer getOcclusionBlurredFramebuffer(); gpu::TexturePointer getOcclusionBlurredTexture(); - + /** gpu::TexturePointer getLightingTexture(); gpu::FramebufferPointer getLightingFramebuffer(); - + */ /// Returns the framebuffer object used to render selfie maps; gpu::FramebufferPointer getSelfieFramebuffer(); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 13ea658d0c..5bf8a52426 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -27,6 +27,7 @@ #include "LightingModel.h" #include "DebugDeferredBuffer.h" +#include "DeferredFramebuffer.h" #include "DeferredLightingEffect.h" #include "SurfaceGeometryPass.h" #include "FramebufferCache.h" @@ -93,20 +94,23 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto lightingModel = addJob("LightingModel"); - // GPU jobs: Start preparing the deferred and lighting buffer - addJob("PrepareDeferred"); + // GPU jobs: Start preparing the primary, deferred and lighting buffer + const auto primaryFramebuffer = addJob("PreparePrimaryBuffer"); + + const auto deferredAndLightingFramebuffer = addJob("PrepareDeferred", primaryFramebuffer); + const auto deferredFramebuffer = deferredAndLightingFramebuffer.getN(0); + const auto lightingFramebuffer = deferredAndLightingFramebuffer.getN(1); // Render opaque objects in DeferredBuffer addJob("DrawOpaqueDeferred", opaques, shapePlumber); // Once opaque is all rendered create stencil background - addJob("DrawOpaqueStencil"); + addJob("DrawOpaqueStencil", deferredFramebuffer); - // Use Stencil and start drawing background in Lighting buffer - addJob("DrawBackgroundDeferred", background); // Opaque all rendered, generate surface geometry buffers - const auto curvatureFramebufferAndDepth = addJob("SurfaceGeometry", deferredFrameTransform); + const auto surfaceGeometryPassInputs = render::Varying(SurfaceGeometryPass::Inputs(deferredFrameTransform, deferredFramebuffer)); + const auto curvatureFramebufferAndDepth = addJob("SurfaceGeometry", surfaceGeometryPassInputs); const auto theCurvatureVarying = curvatureFramebufferAndDepth[0]; @@ -128,14 +132,16 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, lightingModel, curvatureFramebuffer, diffusedCurvatureFramebuffer, scatteringResource)); + const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, deferredFramebuffer, lightingModel, + curvatureFramebuffer, diffusedCurvatureFramebuffer, scatteringResource)); // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredLightingInputs); - // AA job to be revisited - addJob("Antialiasing"); + // Use Stencil and start drawing background in Lighting buffer + addJob("DrawBackgroundDeferred", background); + // Render transparent objects forward in LightingBuffer addJob("DrawTransparentDeferred", transparents, shapePlumber); @@ -144,7 +150,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Lighting Buffer ready for tone mapping - addJob("ToneMapping"); + const auto toneMappingInputs = render::Varying(ToneMappingDeferred::Inputs(lightingFramebuffer, primaryFramebuffer)); + addJob("ToneMapping", toneMappingInputs); // Overlays addJob("DrawOverlay3DOpaque", overlayOpaques, true); @@ -155,7 +162,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { { // Debugging Deferred buffer job - const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(diffusedCurvatureFramebuffer, curvatureFramebuffer)); + const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, diffusedCurvatureFramebuffer, curvatureFramebuffer)); addJob("DebugDeferredBuffer", debugFramebuffers); // Scene Octree Debuging job @@ -173,8 +180,12 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { } } + + // AA job to be revisited + addJob("Antialiasing", primaryFramebuffer); + // Blit! - addJob("Blit"); + addJob("Blit", primaryFramebuffer); } void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { @@ -311,7 +322,7 @@ const gpu::PipelinePointer& DrawStencilDeferred::getOpaquePipeline() { return _opaquePipeline; } -void DrawStencilDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { +void DrawStencilDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const DeferredFramebufferPointer& deferredFramebuffer) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -320,7 +331,9 @@ void DrawStencilDeferred::run(const SceneContextPointer& sceneContext, const Ren doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; - auto deferredFboColorDepthStencil = DependencyManager::get()->getDeferredFramebufferDepthColor(); + // auto deferredFboColorDepthStencil = DependencyManager::get()->getDeferredFramebufferDepthColor(); + auto deferredFboColorDepthStencil = deferredFramebuffer->getDeferredFramebufferDepthColor(); + batch.enableStereo(false); @@ -346,11 +359,11 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const args->_batch = &batch; _gpuTimer.begin(batch); - auto lightingFBO = DependencyManager::get()->getLightingFramebuffer(); + // auto lightingFBO = DependencyManager::get()->getLightingFramebuffer(); batch.enableSkybox(true); - batch.setFramebuffer(lightingFBO); + // batch.setFramebuffer(lightingFBO); batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); @@ -371,7 +384,7 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const std::static_pointer_cast(renderContext->jobConfig)->gpuTime = _gpuTimer.getAverage(); } -void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { +void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer) { assert(renderContext->args); assert(renderContext->args->_context); @@ -387,8 +400,9 @@ void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPoint int height = renderArgs->_viewport.w; // Blit primary to blit FBO - auto framebufferCache = DependencyManager::get(); - auto primaryFbo = framebufferCache->getPrimaryFramebuffer(); + // auto framebufferCache = DependencyManager::get(); + // auto primaryFbo = framebufferCache->getPrimaryFramebuffer(); + auto primaryFbo = srcFramebuffer; gpu::doInBatch(renderArgs->_context, [&](gpu::Batch& batch) { batch.setFramebuffer(blitFbo); diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 5a80d4bcea..612d1c5ea4 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -87,11 +87,12 @@ protected: bool _stateSort; }; +class DeferredFramebuffer; class DrawStencilDeferred { public: - using JobModel = render::Job::Model; + using JobModel = render::Job::ModelI>; - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const std::shared_ptr& deferredFramebuffer); static const gpu::PipelinePointer& getOpaquePipeline(); protected: @@ -157,9 +158,9 @@ protected: class Blit { public: - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + using JobModel = render::Job::ModelI; - using JobModel = render::Job::Model; + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer); }; class RenderDeferredTask : public render::Task { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 349e6cc0e2..2e8f81cfaf 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -508,9 +508,11 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont auto& frameTransform = inputs.get0(); - auto& curvatureFramebuffer = inputs.get2(); - auto& diffusedFramebuffer = inputs.get3(); - auto& scatteringResource = inputs.get4(); + auto& deferredFramebuffer = inputs.get1(); + + auto& curvatureFramebuffer = inputs.get3(); + auto& diffusedFramebuffer = inputs.get4(); + auto& scatteringResource = inputs.get5(); if (!scatteringResource) { return; @@ -563,8 +565,8 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont batch.setResourceTexture(ScatteringTask_ScatteringTableSlot, scatteringTable); batch.setResourceTexture(ScatteringTask_CurvatureMapSlot, curvatureFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); - batch.setResourceTexture(ScatteringTask_NormalMapSlot, framebufferCache->getDeferredNormalTexture()); - batch.setResourceTexture(ScatteringTask_AlbedoMapSlot, framebufferCache->getDeferredColorTexture()); + batch.setResourceTexture(ScatteringTask_NormalMapSlot, deferredFramebuffer->getDeferredNormalTexture()); + batch.setResourceTexture(ScatteringTask_AlbedoMapSlot, deferredFramebuffer->getDeferredColorTexture()); batch.setResourceTexture(ScatteringTask_LinearMapSlot, framebufferCache->getDepthPyramidTexture()); diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 905e30d74e..28e884b552 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -16,6 +16,7 @@ #include "render/DrawTask.h" #include "DeferredFrameTransform.h" +#include "DeferredFramebuffer.h" #include "LightingModel.h" class SubsurfaceScatteringResource { @@ -161,7 +162,7 @@ signals: class DebugSubsurfaceScattering { public: - using Inputs = render::VaryingSet5; + using Inputs = render::VaryingSet6; using Config = DebugSubsurfaceScatteringConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 00fce50bb4..b60ed21f7b 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -45,16 +45,18 @@ void SurfaceGeometryPass::configure(const Config& config) { } } -void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, Outputs& curvatureAndDepth) { +void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& curvatureAndDepth) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; + const auto frameTransform = inputs.get0(); + const auto deferredFramebuffer = inputs.get1(); auto framebufferCache = DependencyManager::get(); - auto depthBuffer = framebufferCache->getPrimaryDepthTexture(); - auto normalTexture = framebufferCache->getDeferredNormalTexture(); + auto depthBuffer = deferredFramebuffer->getPrimaryDepthTexture(); + auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); auto pyramidFBO = framebufferCache->getDepthPyramidFramebuffer(); auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 834b404429..2c1969a519 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -16,6 +16,7 @@ #include "render/DrawTask.h" #include "DeferredFrameTransform.h" +#include "DeferredFramebuffer.h" class SurfaceGeometryPassConfig : public render::Job::Config { Q_OBJECT @@ -40,14 +41,15 @@ signals: class SurfaceGeometryPass { public: + using Inputs = render::VaryingSet2; using Outputs = render::VaryingSet2; using Config = SurfaceGeometryPassConfig; - using JobModel = render::Job::ModelIO; + using JobModel = render::Job::ModelIO; SurfaceGeometryPass(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, Outputs& curvatureAndDepth); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& curvatureAndDepth); float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } float getCurvatureBasisScale() const { return _parametersBuffer.get().curvatureInfo.y; } diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index 6a0020edbf..1cd0e42ad1 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -116,18 +116,14 @@ void ToneMappingEffect::setToneCurve(ToneCurve curve) { } } -void ToneMappingEffect::render(RenderArgs* args) { +void ToneMappingEffect::render(RenderArgs* args, const gpu::TexturePointer& lightingBuffer, const gpu::FramebufferPointer& destinationFramebuffer) { if (!_blitLightBuffer) { init(); } - auto framebufferCache = DependencyManager::get(); + auto framebufferSize = glm::ivec2(lightingBuffer->getDimensions()); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); - QSize framebufferSize = framebufferCache->getFrameBufferSize(); - - auto lightingBuffer = framebufferCache->getLightingTexture(); - auto destFbo = framebufferCache->getPrimaryFramebuffer(); - batch.setFramebuffer(destFbo); + batch.setFramebuffer(destinationFramebuffer); // FIXME: Generate the Luminosity map //batch.generateTextureMips(lightingBuffer); @@ -136,10 +132,10 @@ void ToneMappingEffect::render(RenderArgs* args) { batch.setProjectionTransform(glm::mat4()); batch.setViewTransform(Transform()); { - float sMin = args->_viewport.x / (float)framebufferSize.width(); - float sWidth = args->_viewport.z / (float)framebufferSize.width(); - float tMin = args->_viewport.y / (float)framebufferSize.height(); - float tHeight = args->_viewport.w / (float)framebufferSize.height(); + float sMin = args->_viewport.x / (float)framebufferSize.x; + float sWidth = args->_viewport.z / (float)framebufferSize.x; + float tMin = args->_viewport.y / (float)framebufferSize.y; + float tHeight = args->_viewport.w / (float)framebufferSize.y; Transform model; batch.setPipeline(_blitLightBuffer); model.setTranslation(glm::vec3(sMin, tMin, 0.0)); @@ -159,6 +155,12 @@ void ToneMappingDeferred::configure(const Config& config) { _toneMappingEffect.setToneCurve((ToneMappingEffect::ToneCurve)config.curve); } -void ToneMappingDeferred::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { - _toneMappingEffect.render(renderContext->args); +void ToneMappingDeferred::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs) { + /* auto framebufferCache = DependencyManager::get(); + auto lightingBuffer = framebufferCache->getLightingTexture(); + auto destFbo = framebufferCache->getPrimaryFramebuffer(); + */ + auto lightingBuffer = inputs.get0()->getRenderBuffer(0); + auto destFbo = inputs.get1(); + _toneMappingEffect.render(renderContext->args, lightingBuffer, destFbo); } diff --git a/libraries/render-utils/src/ToneMappingEffect.h b/libraries/render-utils/src/ToneMappingEffect.h index a6a08ab87b..9e8b3f6aa4 100644 --- a/libraries/render-utils/src/ToneMappingEffect.h +++ b/libraries/render-utils/src/ToneMappingEffect.h @@ -27,7 +27,7 @@ public: ToneMappingEffect(); virtual ~ToneMappingEffect() {} - void render(RenderArgs* args); + void render(RenderArgs* args, const gpu::TexturePointer& lightingBuffer, const gpu::FramebufferPointer& destinationBuffer); void setExposure(float exposure); float getExposure() const { return _parametersBuffer.get()._exposure; } @@ -83,11 +83,13 @@ signals: class ToneMappingDeferred { public: + // Inputs: lightingFramebuffer, destinationFramebuffer + using Inputs = render::VaryingSet2; using Config = ToneMappingConfig; - using JobModel = render::Job::Model; + using JobModel = render::Job::ModelI; void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); ToneMappingEffect _toneMappingEffect; }; diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 409ff7b77f..db9573322f 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -55,6 +55,9 @@ public: // access potential sub varyings contained in this one. Varying operator[] (uint8_t index) const { return (*_concept)[index]; } uint8_t length() const { return (*_concept).length(); } + + template Varying getN (uint8_t index) const { return get()[index]; } + template Varying editN (uint8_t index) { return edit()[index]; } protected: class Concept { @@ -104,6 +107,12 @@ public: const T1& get1() const { return second.get(); } T1& edit1() { return second.edit(); } + + Varying operator[] (uint8_t index) const { + if (index == 1) { return second; } else { return first; } + } + + }; @@ -174,6 +183,34 @@ public: T4& edit4() { return std::get<4>((*this)).template edit(); } }; +template +class VaryingSet6 : public std::tuple{ +public: + using Parent = std::tuple; + + VaryingSet6() : Parent(Varying(T0()), Varying(T1()), Varying(T2()), Varying(T3()), Varying(T4()), Varying(T5())) {} + VaryingSet6(const VaryingSet6& src) : Parent(std::get<0>(src), std::get<1>(src), std::get<2>(src), std::get<3>(src), std::get<4>(src), std::get<5>(src)) {} + VaryingSet6(const Varying& first, const Varying& second, const Varying& third, const Varying& fourth, const Varying& fifth, const Varying& sixth) : Parent(first, second, third, fourth, fifth, sixth) {} + + const T0& get0() const { return std::get<0>((*this)).template get(); } + T0& edit0() { return std::get<0>((*this)).template edit(); } + + const T1& get1() const { return std::get<1>((*this)).template get(); } + T1& edit1() { return std::get<1>((*this)).template edit(); } + + const T2& get2() const { return std::get<2>((*this)).template get(); } + T2& edit2() { return std::get<2>((*this)).template edit(); } + + const T3& get3() const { return std::get<3>((*this)).template get(); } + T3& edit3() { return std::get<3>((*this)).template edit(); } + + const T4& get4() const { return std::get<4>((*this)).template get(); } + T4& edit4() { return std::get<4>((*this)).template edit(); } + + const T5& get5() const { return std::get<5>((*this)).template get(); } + T5& edit5() { return std::get<5>((*this)).template edit(); } +}; + template < class T, int NUM > class VaryingArray : public std::array { public: diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index 104755c1b6..2d08993436 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -99,7 +99,7 @@ void TestWindow::beginFrame() { #ifdef DEFERRED_LIGHTING auto deferredLightingEffect = DependencyManager::get(); - _prepareDeferred.run(_sceneContext, _renderContext); + _prepareDeferred.run(_sceneContext, _renderContext, _deferredFramebuffer); #else gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 0.0f, 0.1f, 0.2f, 1.0f }); @@ -134,13 +134,15 @@ void TestWindow::endFrame() { RenderDeferred::Inputs deferredInputs; deferredInputs.edit0() = frameTransform; + deferredInputs.edit1() = _deferredFramebuffer; _renderDeferred.run(_sceneContext, _renderContext, deferredInputs); gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { PROFILE_RANGE_BATCH(batch, "blit"); // Blit to screen auto framebufferCache = DependencyManager::get(); - auto framebuffer = framebufferCache->getLightingFramebuffer(); + // auto framebuffer = framebufferCache->getLightingFramebuffer(); + auto framebuffer = _deferredFramebuffer->getLightingFramebuffer(); batch.blit(framebuffer, _renderArgs->_viewport, nullptr, _renderArgs->_viewport); }); #endif diff --git a/tests/gpu-test/src/TestWindow.h b/tests/gpu-test/src/TestWindow.h index d6c11b7459..1260b519eb 100644 --- a/tests/gpu-test/src/TestWindow.h +++ b/tests/gpu-test/src/TestWindow.h @@ -37,6 +37,7 @@ protected: model::LightPointer _light { std::make_shared() }; GenerateDeferredFrameTransform _generateDeferredFrameTransform; + DeferredFramebufferPointer _deferredFramebuffer; PrepareDeferred _prepareDeferred; RenderDeferred _renderDeferred; #endif From e0993c593b2dadb3991d279ec8266b8895fce566 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 11 Jul 2016 18:34:18 -0700 Subject: [PATCH 1034/1237] BRinging the intermediate render textures in the pipeline as varyings and not throug the singletonn anymore --- .../render-utils/src/DeferredFramebuffer.cpp | 182 ++++++++++++++++++ .../render-utils/src/DeferredFramebuffer.h | 81 ++++++++ 2 files changed, 263 insertions(+) create mode 100644 libraries/render-utils/src/DeferredFramebuffer.cpp create mode 100644 libraries/render-utils/src/DeferredFramebuffer.h diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp new file mode 100644 index 0000000000..190bfbfab3 --- /dev/null +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -0,0 +1,182 @@ +// +// DeferredFramebuffer.h +// libraries/render-utils/src/ +// +// Created by Sam Gateau 7/11/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "DeferredFramebuffer.h" + + +DeferredFramebuffer::DeferredFramebuffer() { +} + + +void DeferredFramebuffer::setPrimaryDepth(const gpu::TexturePointer& depthBuffer) { + //If the size changed, we need to delete our FBOs + if (_primaryDepthTexture != depthBuffer) { + _primaryDepthTexture = depthBuffer; + + } +} + +void DeferredFramebuffer::setFrameSize(const glm::ivec2& frameBufferSize) { + //If the size changed, we need to delete our FBOs + if (_frameSize != frameBufferSize) { + _frameSize = frameBufferSize; + _deferredFramebuffer.reset(); + _deferredFramebufferDepthColor.reset(); + _deferredColorTexture.reset(); + _deferredNormalTexture.reset(); + _deferredSpecularTexture.reset(); + _lightingTexture.reset(); + _lightingFramebuffer.reset(); + + _occlusionFramebuffer.reset(); + _occlusionTexture.reset(); + _occlusionBlurredFramebuffer.reset(); + _occlusionBlurredTexture.reset(); + } +} + +void DeferredFramebuffer::allocate() { + _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create()); + + auto colorFormat = gpu::Element::COLOR_SRGBA_32; + auto linearFormat = gpu::Element::COLOR_RGBA_32; + auto width = _frameSize.x; + auto height = _frameSize.y; + + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + // _primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + + // _primaryFramebuffer->setRenderBuffer(0, _primaryColorTexture); + + _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); + _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + + _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); + _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); + _deferredFramebuffer->setRenderBuffer(2, _deferredSpecularTexture); + + _deferredFramebufferDepthColor->setRenderBuffer(0, _deferredColorTexture); + + // auto depthFormat = gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); + + auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format + if (!_primaryDepthTexture) { + _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); + } + + _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + _deferredFramebufferDepthColor->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + + auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); + + _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); + _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + _deferredFramebuffer->setRenderBuffer(3, _lightingTexture); + + // For AO: + // auto pointMipSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT); + // _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + // _depthPyramidFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + // _depthPyramidFramebuffer->setRenderBuffer(0, _depthPyramidTexture); + // _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + // resizeAmbientOcclusionBuffers(); +} + +/* +void DeferredFramebuffer::resizeAmbientOcclusionBuffers() { + _occlusionFramebuffer.reset(); + _occlusionTexture.reset(); + _occlusionBlurredFramebuffer.reset(); + _occlusionBlurredTexture.reset(); + + + auto width = _frameBufferSize.width() >> _AOResolutionLevel; + auto height = _frameBufferSize.height() >> _AOResolutionLevel; + auto colorFormat = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGB); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); + auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format + + _occlusionTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); + _occlusionFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); + _occlusionBlurredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); +} +*/ + + +gpu::TexturePointer DeferredFramebuffer::getPrimaryDepthTexture() { + if (!_primaryDepthTexture) { + allocate(); + } + return _primaryDepthTexture; +} + +gpu::FramebufferPointer DeferredFramebuffer::getDeferredFramebuffer() { + if (!_deferredFramebuffer) { + allocate(); + } + return _deferredFramebuffer; +} + +gpu::FramebufferPointer DeferredFramebuffer::getDeferredFramebufferDepthColor() { + if (!_deferredFramebufferDepthColor) { + allocate(); + } + return _deferredFramebufferDepthColor; +} + +gpu::TexturePointer DeferredFramebuffer::getDeferredColorTexture() { + if (!_deferredColorTexture) { + allocate(); + } + return _deferredColorTexture; +} + +gpu::TexturePointer DeferredFramebuffer::getDeferredNormalTexture() { + if (!_deferredNormalTexture) { + allocate(); + } + return _deferredNormalTexture; +} + +gpu::TexturePointer DeferredFramebuffer::getDeferredSpecularTexture() { + if (!_deferredSpecularTexture) { + allocate(); + } + return _deferredSpecularTexture; +} + +gpu::FramebufferPointer DeferredFramebuffer::getLightingFramebuffer() { + if (!_lightingFramebuffer) { + allocate(); + } + return _lightingFramebuffer; +} + +gpu::TexturePointer DeferredFramebuffer::getLightingTexture() { + if (!_lightingTexture) { + allocate(); + } + return _lightingTexture; +} diff --git a/libraries/render-utils/src/DeferredFramebuffer.h b/libraries/render-utils/src/DeferredFramebuffer.h new file mode 100644 index 0000000000..346ea3ee28 --- /dev/null +++ b/libraries/render-utils/src/DeferredFramebuffer.h @@ -0,0 +1,81 @@ +// +// DeferredFramebuffer.h +// libraries/render-utils/src/ +// +// Created by Sam Gateau 7/11/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DeferredFramebuffer_h +#define hifi_DeferredFramebuffer_h + +#include "gpu/Resource.h" +#include "gpu/Framebuffer.h" + +class RenderArgs; + +// DeferredFramebuffer is a helper class gathering in one place the GBuffer (Framebuffer) and lighting framebuffer +class DeferredFramebuffer { +public: + DeferredFramebuffer(); + + gpu::FramebufferPointer getDeferredFramebuffer(); + gpu::FramebufferPointer getDeferredFramebufferDepthColor(); + + gpu::TexturePointer getPrimaryDepthTexture(); + + gpu::TexturePointer getDeferredColorTexture(); + gpu::TexturePointer getDeferredNormalTexture(); + gpu::TexturePointer getDeferredSpecularTexture(); + + gpu::FramebufferPointer getDepthPyramidFramebuffer(); + gpu::TexturePointer getDepthPyramidTexture(); + + + void setAmbientOcclusionResolutionLevel(int level); + gpu::FramebufferPointer getOcclusionFramebuffer(); + gpu::TexturePointer getOcclusionTexture(); + gpu::FramebufferPointer getOcclusionBlurredFramebuffer(); + gpu::TexturePointer getOcclusionBlurredTexture(); + + + gpu::FramebufferPointer getLightingFramebuffer(); + gpu::TexturePointer getLightingTexture(); + + void setPrimaryDepth(const gpu::TexturePointer& depthBuffer); + + void setFrameSize(const glm::ivec2& size); + const glm::ivec2& getFrameSize() const { return _frameSize; } + + +protected: + void allocate(); + + gpu::TexturePointer _primaryDepthTexture; + + gpu::FramebufferPointer _deferredFramebuffer; + gpu::FramebufferPointer _deferredFramebufferDepthColor; + + gpu::TexturePointer _deferredColorTexture; + gpu::TexturePointer _deferredNormalTexture; + gpu::TexturePointer _deferredSpecularTexture; + + gpu::TexturePointer _lightingTexture; + gpu::FramebufferPointer _lightingFramebuffer; + + gpu::FramebufferPointer _occlusionFramebuffer; + gpu::TexturePointer _occlusionTexture; + + gpu::FramebufferPointer _occlusionBlurredFramebuffer; + gpu::TexturePointer _occlusionBlurredTexture; + + glm::ivec2 _frameSize; + +}; + +using DeferredFramebufferPointer = std::shared_ptr; + +#endif // hifi_DeferredFramebuffer_h \ No newline at end of file From 367c26a5eef1ba352ac5178d476b5b85cc5412bd Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Jul 2016 18:53:47 -0700 Subject: [PATCH 1035/1237] wait a bit before resetting collides-with-my-avatar --- .../system/controllers/handControllerGrab.js | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 36cef8ec2d..aaaa7c5962 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -181,6 +181,10 @@ var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic"; var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; +var avCollideLater; +var setCollidesLaterTimeout; +var collideLaterID; + var CONTROLLER_STATE_MACHINE = {}; CONTROLLER_STATE_MACHINE[STATE_OFF] = { @@ -2060,6 +2064,17 @@ function MyController(hand) { } this.entityActivated = true; + if (setCollidesLaterTimeout && collideLaterID == entityID) { + // we have a timeout waiting to set collisions with myAvatar back on (so that when something + // is thrown it doesn't collide with the avatar's capsule the moment it's released). We've + // regrabbed the entity before the timeout fired, so cancel the timeout, run the function now + // and adjust the grabbedProperties. This will make the saved set of properties (the ones that + // get re-instated after all the grabs have been released) be correct. + Script.clearTimeout(setCollidesLaterTimeout); + setCollidesLaterTimeout = null; + grabbedProperties["collidesWith"] = avCollideLater(); + } + var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); var now = Date.now(); @@ -2150,10 +2165,18 @@ function MyController(hand) { parentJointIndex: data["parentJointIndex"] }; - Script.setTimeout(function () { - // set collidesWith back to original value a bit later than the rest - Entities.editEntity(entityID, { collidesWith: data["collidesWith"] }); - }, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY); + if (data["collidesWith"].indexOf("myAvatar") >= 0) { + var javaScriptScopingIsBrokenData = data; + avCollideLater = function () { + // set collidesWith back to original value a bit later than the rest + _this.setCollidesLaterTimeout = null; + Entities.editEntity(entityID, { collidesWith: javaScriptScopingIsBrokenData["collidesWith"] }); + return javaScriptScopingIsBrokenData["collidesWith"]; + } + setCollidesLaterTimeout = + Script.setTimeout(avCollideLater, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY * MSECS_PER_SEC); + collideLaterID = entityID; + } // things that are held by parenting and dropped with no velocity will end up as "static" in bullet. If // it looks like the dropped thing should fall, give it a little velocity. From 504e9f154c87c1886551d0c869525a899b7bc366 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 12 Jul 2016 09:18:02 -0700 Subject: [PATCH 1036/1237] Less comments --- .../render-utils/src/RenderDeferredTask.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 5bf8a52426..77d23d10a1 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -113,16 +113,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto curvatureFramebufferAndDepth = addJob("SurfaceGeometry", surfaceGeometryPassInputs); - const auto theCurvatureVarying = curvatureFramebufferAndDepth[0]; - -//#define SIMPLE_BLUR 1 -#if SIMPLE_BLUR - const auto curvatureFramebuffer = addJob("DiffuseCurvature", curvatureFramebufferAndDepth.get().first); - const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth.get().first, true); -#else const auto curvatureFramebuffer = addJob("DiffuseCurvature", curvatureFramebufferAndDepth); const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth, true); -#endif const auto scatteringResource = addJob("Scattering"); @@ -138,17 +130,12 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredLightingInputs); - - // Use Stencil and start drawing background in Lighting buffer + // Use Stencil and draw background in Lighting buffer addJob("DrawBackgroundDeferred", background); - // Render transparent objects forward in LightingBuffer addJob("DrawTransparentDeferred", transparents, shapePlumber); - addJob("DebugScattering", deferredLightingInputs); - - // Lighting Buffer ready for tone mapping const auto toneMappingInputs = render::Varying(ToneMappingDeferred::Inputs(lightingFramebuffer, primaryFramebuffer)); addJob("ToneMapping", toneMappingInputs); @@ -160,6 +147,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Debugging stages { + addJob("DebugScattering", deferredLightingInputs); // Debugging Deferred buffer job const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, diffusedCurvatureFramebuffer, curvatureFramebuffer)); From 0efc684992f44fadd687de16a90984b5225f7671 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 11 Jul 2016 14:19:27 -0700 Subject: [PATCH 1037/1237] Add glow to the termination point of UI pointers --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 253 ++++++++++++++++-- .../display-plugins/hmd/HmdDisplayPlugin.h | 18 ++ 2 files changed, 244 insertions(+), 27 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c2497e5740..2dc7df341a 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include #include #include @@ -34,6 +36,9 @@ static const QString REPROJECTION = "Allow Reprojection"; static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; static const QString DEVELOPER_MENU_PATH = "Developer>" + DisplayPlugin::MENU_PATH(); static const bool DEFAULT_MONO_VIEW = true; +static const int NUMBER_OF_HANDS = 2; +static const glm::mat4 IDENTITY_MATRIX; + glm::uvec2 HmdDisplayPlugin::getRecommendedUiSize() const { return CompositorHelper::VIRTUAL_SCREEN_SIZE; @@ -241,6 +246,9 @@ void main() { )VS"; +static const char * LASER_GS = R"GS( +)GS"; + static const char * LASER_FS = R"FS(#version 410 core uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); @@ -254,6 +262,85 @@ void main() { )FS"; + +static const char * LASER_GLOW_VS = R"VS(#version 410 core + +uniform mat4 mvp = mat4(1); + +in vec3 Position; +in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} + +)VS"; + +static const char * LASER_GLOW_FS = R"FS(#version 410 core +#line 286 +uniform sampler2D sampler; +uniform float alpha = 1.0; +uniform vec4 glowPoints = vec4(-1); +uniform vec4 glowColor1 = vec4(0.01, 0.7, 0.9, 1.0); +uniform vec4 glowColor2 = vec4(0.9, 0.7, 0.01, 1.0); +uniform vec2 resolution = vec2(3960.0, 1188.0); +uniform float radius = 0.005; + +in vec3 vPosition; +in vec2 vTexCoord; +in vec4 vGlowPoints; + +out vec4 FragColor; + +float easeInOutCubic(float f) { + const float d = 1.0; + const float b = 0.0; + const float c = 1.0; + float t = f; + if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; + return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; +} + +void main() { + vec2 aspect = resolution; + aspect /= resolution.x; + FragColor = texture(sampler, vTexCoord); + + float glowIntensity = 0.0; + float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); + float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); + float dist = min(dist1, dist2); + vec3 glowColor = glowColor1.rgb; + if (dist2 < dist1) { + glowColor = glowColor2.rgb; + } + + if (dist <= radius) { + glowIntensity = 1.0 - (dist / radius); + glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); + glowIntensity = easeInOutCubic(glowIntensity); + glowIntensity = pow(glowIntensity, 0.5); + } + + if (alpha <= 0.0) { + if (glowIntensity <= 0.0) { + discard; + } + + FragColor = vec4(glowColor, glowIntensity); + return; + } + + FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); + FragColor.a *= alpha; +} +)FS"; + void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled @@ -263,7 +350,6 @@ void HmdDisplayPlugin::customizeContext() { #endif _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); - using namespace oglplus; if (!_enablePreview) { const std::string version("#version 410 core\n"); @@ -271,6 +357,7 @@ void HmdDisplayPlugin::customizeContext() { PREVIEW_TEXTURE_LOCATION = Uniform(*_previewProgram, "colorMap").Location(); } + updateGlowProgram(); compileProgram(_laserProgram, LASER_VS, LASER_FS); _laserGeometry = loadLaser(_laserProgram); @@ -280,7 +367,67 @@ void HmdDisplayPlugin::customizeContext() { PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "projections").Location(); } +#if 0 +static QString readFile(const char* filename) { + QFile file(filename); + file.open(QFile::Text | QFile::ReadOnly); + QString result; + result.append(QTextStream(&file).readAll()); + return result; +} +#endif + + +void HmdDisplayPlugin::updateGlowProgram() { +#if 0 + static qint64 vsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + static const char* vsFile = "H:/glow_vert.glsl"; + static const char* fsFile = "H:/glow_frag.glsl"; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + if (!_overlayProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + ProgramPtr newOverlayProgram; + compileProgram(newOverlayProgram, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (newOverlayProgram) { + using namespace oglplus; + _overlayProgram = newOverlayProgram; + auto uniforms = _overlayProgram->ActiveUniforms(); + _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); + _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); + _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); + _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); + _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); + useProgram(_overlayProgram); + Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); + } + vsBuiltAge = vsAge; + fsBuiltAge = fsAge; + + } +#else + if (!_overlayProgram) { + compileProgram(_overlayProgram, LASER_GLOW_VS, LASER_GLOW_FS); + using namespace oglplus; + auto uniforms = _overlayProgram->ActiveUniforms(); + _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); + _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); + _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); + _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); + _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); + useProgram(_overlayProgram); + Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); + } + +#endif +} + void HmdDisplayPlugin::uncustomizeContext() { + _overlayProgram.reset(); _sphereSection.reset(); _compositeFramebuffer.reset(); _previewProgram.reset(); @@ -327,19 +474,83 @@ void HmdDisplayPlugin::compositeOverlay() { auto compositorHelper = DependencyManager::get(); glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); - useProgram(_program); - // set the alpha - Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); + withPresentThreadLock([&] { + _presentHandLasers = _handLasers; + _presentHandPoses = _handPoses; + _presentUiModelTransform = _uiModelTransform; + }); + std::array handGlowPoints { { vec2(-1), vec2(-1) } }; + + // compute the glow point interesections + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { + if (_presentHandPoses[i] == IDENTITY_MATRIX) { + continue; + } + const auto& handLaser = _presentHandLasers[i]; + if (!handLaser.valid()) { + continue; + } + + const auto& laserDirection = handLaser.direction; + auto model = _presentHandPoses[i]; + auto castDirection = glm::quat_cast(model) * laserDirection; + if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { + castDirection = glm::normalize(castDirection); + castDirection = glm::inverse(_presentUiModelTransform.getRotation()) * castDirection; + } + + // FIXME fetch the actual UI radius from... somewhere? + float uiRadius = 1.0f; + + // Find the intersection of the laser with he UI and use it to scale the model matrix + float distance; + if (!glm::intersectRaySphere(vec3(_presentHandPoses[i][3]), castDirection, _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + continue; + } + + vec3 intersectionPosition = vec3(_presentHandPoses[i][3]) + (castDirection * distance) - _presentUiModelTransform.getTranslation(); + intersectionPosition = glm::inverse(_presentUiModelTransform.getRotation()) * intersectionPosition; + + // Take the interesection normal and convert it to a texture coordinate + vec2 yawPitch; + { + vec2 xdir = glm::normalize(vec2(intersectionPosition.x, -intersectionPosition.z)); + yawPitch.x = glm::atan(xdir.x, xdir.y); + yawPitch.y = (acosf(intersectionPosition.y) * -1.0f) + M_PI_2; + } + vec2 halfFov = CompositorHelper::VIRTUAL_UI_TARGET_FOV / 2.0f; + + // Are we out of range + if (glm::any(glm::greaterThan(glm::abs(yawPitch), halfFov))) { + continue; + } + + yawPitch /= CompositorHelper::VIRTUAL_UI_TARGET_FOV; + yawPitch += 0.5f; + handGlowPoints[i] = yawPitch; + } + + updateGlowProgram(); + useProgram(_overlayProgram); + // Setup the uniforms + { + if (_overlayUniforms.alpha >= 0) { + Uniform(*_overlayProgram, _overlayUniforms.alpha).Set(_compositeOverlayAlpha); + } + if (_overlayUniforms.glowPoints >= 0) { + vec4 glowPoints(handGlowPoints[0], handGlowPoints[1]); + Uniform(*_overlayProgram, _overlayUniforms.glowPoints).Set(glowPoints); + } + } + _sphereSection->Use(); for_each_eye([&](Eye eye) { eyeViewport(eye); auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; auto mvp = _eyeProjections[eye] * modelView; - Uniform(*_program, _mvpUniform).Set(mvp); + Uniform(*_overlayProgram, _overlayUniforms.mvp).Set(mvp); _sphereSection->Draw(); }); - // restore the alpha - Uniform(*_program, _alphaUniform).Set(1.0); } void HmdDisplayPlugin::compositePointer() { @@ -478,23 +689,12 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve } void HmdDisplayPlugin::compositeExtra() { - const int NUMBER_OF_HANDS = 2; - std::array handLasers; - std::array renderHandPoses; - Transform uiModelTransform; - withPresentThreadLock([&] { - handLasers = _handLasers; - renderHandPoses = _handPoses; - uiModelTransform = _uiModelTransform; - }); - // If neither hand laser is activated, exit - if (!handLasers[0].valid() && !handLasers[1].valid()) { + if (!_presentHandLasers[0].valid() && !_presentHandLasers[1].valid()) { return; } - static const glm::mat4 identity; - if (renderHandPoses[0] == identity && renderHandPoses[1] == identity) { + if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX) { return; } @@ -505,16 +705,16 @@ void HmdDisplayPlugin::compositeExtra() { std::array handLaserModelMatrices; for (int i = 0; i < NUMBER_OF_HANDS; ++i) { - if (renderHandPoses[i] == identity) { + if (_presentHandPoses[i] == IDENTITY_MATRIX) { continue; } - const auto& handLaser = handLasers[i]; + const auto& handLaser = _presentHandLasers[i]; if (!handLaser.valid()) { continue; } const auto& laserDirection = handLaser.direction; - auto model = renderHandPoses[i]; + auto model = _presentHandPoses[i]; auto castDirection = glm::quat_cast(model) * laserDirection; if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { castDirection = glm::normalize(castDirection); @@ -525,7 +725,7 @@ void HmdDisplayPlugin::compositeExtra() { // Find the intersection of the laser with he UI and use it to scale the model matrix float distance; - if (!glm::intersectRaySphere(vec3(renderHandPoses[i][3]), castDirection, uiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + if (!glm::intersectRaySphere(vec3(_presentHandPoses[i][3]), castDirection, _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { continue; } @@ -545,13 +745,12 @@ void HmdDisplayPlugin::compositeExtra() { auto view = glm::inverse(eyePose); const auto& projection = _eyeProjections[eye]; for (int i = 0; i < NUMBER_OF_HANDS; ++i) { - if (handLaserModelMatrices[i] == identity) { + if (handLaserModelMatrices[i] == IDENTITY_MATRIX) { continue; } Uniform(*_laserProgram, "mvp").Set(projection * view * handLaserModelMatrices[i]); - Uniform(*_laserProgram, "color").Set(handLasers[i].color); + Uniform(*_laserProgram, "color").Set(_presentHandLasers[i].color); _laserGeometry->Draw(); - // TODO render some kind of visual indicator at the intersection point with the UI. } }); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 8e48690fd1..be2811076d 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -61,13 +61,29 @@ protected: } }; + ProgramPtr _overlayProgram; + struct OverlayUniforms { + int32_t mvp { -1 }; + int32_t alpha { -1 }; + int32_t glowPoints { -1 }; + int32_t resolution { -1 }; + int32_t radius { -1 }; + } _overlayUniforms; + Transform _uiModelTransform; std::array _handLasers; std::array _handPoses; + + Transform _presentUiModelTransform; + std::array _presentHandLasers; + std::array _presentHandPoses; + std::array _eyeOffsets; std::array _eyeProjections; std::array _eyeInverseProjections; + + glm::mat4 _cullingProjection; glm::uvec2 _renderTargetSize; float _ipd { 0.064f }; @@ -87,6 +103,8 @@ protected: FrameInfo _currentRenderFrameInfo; private: + void updateGlowProgram(); + bool _enablePreview { false }; bool _monoPreview { true }; bool _enableReprojection { true }; From 6242816f68b47c74f20479bcc5fdabac5188f21f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 11 Jul 2016 18:59:22 -0700 Subject: [PATCH 1038/1237] Adding glow to hand lasers --- .../resources/shaders/hmd_hand_lasers.frag | 35 ++ .../resources/shaders/hmd_hand_lasers.geom | 70 +++ .../resources/shaders/hmd_hand_lasers.vert | 15 + .../resources/shaders/hmd_reproject.frag | 78 ++++ .../resources/shaders/hmd_reproject.vert | 20 + interface/resources/shaders/hmd_ui_glow.frag | 65 +++ interface/resources/shaders/hmd_ui_glow.vert | 23 + .../display-plugins/hmd/HmdDisplayPlugin.cpp | 425 ++++++------------ .../display-plugins/hmd/HmdDisplayPlugin.h | 39 +- libraries/gl/src/gl/OglplusHelpers.cpp | 30 ++ libraries/gl/src/gl/OglplusHelpers.h | 2 + 11 files changed, 493 insertions(+), 309 deletions(-) create mode 100644 interface/resources/shaders/hmd_hand_lasers.frag create mode 100644 interface/resources/shaders/hmd_hand_lasers.geom create mode 100644 interface/resources/shaders/hmd_hand_lasers.vert create mode 100644 interface/resources/shaders/hmd_reproject.frag create mode 100644 interface/resources/shaders/hmd_reproject.vert create mode 100644 interface/resources/shaders/hmd_ui_glow.frag create mode 100644 interface/resources/shaders/hmd_ui_glow.vert diff --git a/interface/resources/shaders/hmd_hand_lasers.frag b/interface/resources/shaders/hmd_hand_lasers.frag new file mode 100644 index 0000000000..6fffb1c521 --- /dev/null +++ b/interface/resources/shaders/hmd_hand_lasers.frag @@ -0,0 +1,35 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); + +layout(location = 0) in vec3 inLineDistance; + +out vec4 FragColor; + +void main() { + vec2 d = inLineDistance.xy; + d.y = abs(d.y); + d.x = abs(d.x); + if (d.x > 1.0) { + d.x = (d.x - 1.0) / 0.02; + } else { + d.x = 0.0; + } + float alpha = 1.0 - length(d); + if (alpha <= 0.0) { + discard; + } + alpha = pow(alpha, 10.0); + if (alpha < 0.05) { + discard; + } + FragColor = vec4(color.rgb, alpha); +} diff --git a/interface/resources/shaders/hmd_hand_lasers.geom b/interface/resources/shaders/hmd_hand_lasers.geom new file mode 100644 index 0000000000..16b5dafadd --- /dev/null +++ b/interface/resources/shaders/hmd_hand_lasers.geom @@ -0,0 +1,70 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#version 410 core +#extension GL_EXT_geometry_shader4 : enable + +layout(location = 0) out vec3 outLineDistance; + +layout(lines) in; +layout(triangle_strip, max_vertices = 24) out; + +vec3[2] getOrthogonals(in vec3 n, float scale) { + float yDot = abs(dot(n, vec3(0, 1, 0))); + + vec3 result[2]; + if (yDot < 0.9) { + result[0] = normalize(cross(n, vec3(0, 1, 0))); + } else { + result[0] = normalize(cross(n, vec3(1, 0, 0))); + } + // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other + result[1] = cross(result[0], n); + result[0] *= scale; + result[1] *= scale; + return result; +} + + +vec2 orthogonal(vec2 v) { + vec2 result = v.yx; + result.y *= -1.0; + return result; +} + +void main() { + vec2 endpoints[2]; + for (int i = 0; i < 2; ++i) { + endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; + } + vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); + vec2 lineOrthogonal = orthogonal(lineNormal); + lineNormal *= 0.02; + lineOrthogonal *= 0.02; + + gl_Position = gl_PositionIn[0]; + gl_Position.xy -= lineOrthogonal; + outLineDistance = vec3(-1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[0]; + gl_Position.xy += lineOrthogonal; + outLineDistance = vec3(-1.02, 1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy -= lineOrthogonal; + outLineDistance = vec3(1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy += lineOrthogonal; + outLineDistance = vec3(1.02, 1, gl_Position.z); + EmitVertex(); + + EndPrimitive(); +} diff --git a/interface/resources/shaders/hmd_hand_lasers.vert b/interface/resources/shaders/hmd_hand_lasers.vert new file mode 100644 index 0000000000..db5c7c1ecd --- /dev/null +++ b/interface/resources/shaders/hmd_hand_lasers.vert @@ -0,0 +1,15 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#version 410 core +uniform mat4 mvp = mat4(1); + +in vec3 Position; + +void main() { + gl_Position = mvp * vec4(Position, 1); +} diff --git a/interface/resources/shaders/hmd_reproject.frag b/interface/resources/shaders/hmd_reproject.frag new file mode 100644 index 0000000000..adda0315a3 --- /dev/null +++ b/interface/resources/shaders/hmd_reproject.frag @@ -0,0 +1,78 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform sampler2D sampler; +uniform mat3 reprojection = mat3(1); +uniform mat4 inverseProjections[2]; +uniform mat4 projections[2]; + +in vec2 vTexCoord; +in vec3 vPosition; + +out vec4 FragColor; + +void main() { + vec2 uv = vTexCoord; + + mat4 eyeInverseProjection; + mat4 eyeProjection; + + float xoffset = 1.0; + vec2 uvmin = vec2(0.0); + vec2 uvmax = vec2(1.0); + // determine the correct projection and inverse projection to use. + if (vTexCoord.x < 0.5) { + uvmax.x = 0.5; + eyeInverseProjection = inverseProjections[0]; + eyeProjection = projections[0]; + } else { + xoffset = -1.0; + uvmin.x = 0.5; + uvmax.x = 1.0; + eyeInverseProjection = inverseProjections[1]; + eyeProjection = projections[1]; + } + + // Account for stereo in calculating the per-eye NDC coordinates + vec4 ndcSpace = vec4(vPosition, 1.0); + ndcSpace.x *= 2.0; + ndcSpace.x += xoffset; + + // Convert from NDC to eyespace + vec4 eyeSpace = eyeInverseProjection * ndcSpace; + eyeSpace /= eyeSpace.w; + + // Convert to a noramlized ray + vec3 ray = eyeSpace.xyz; + ray = normalize(ray); + + // Adjust the ray by the rotation + ray = reprojection * ray; + + // Project back on to the texture plane + ray *= eyeSpace.z / ray.z; + + // Update the eyespace vector + eyeSpace.xyz = ray; + + // Reproject back into NDC + ndcSpace = eyeProjection * eyeSpace; + ndcSpace /= ndcSpace.w; + ndcSpace.x -= xoffset; + ndcSpace.x /= 2.0; + + // Calculate the new UV coordinates + uv = (ndcSpace.xy / 2.0) + 0.5; + if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + } else { + FragColor = texture(sampler, uv); + } +} \ No newline at end of file diff --git a/interface/resources/shaders/hmd_reproject.vert b/interface/resources/shaders/hmd_reproject.vert new file mode 100644 index 0000000000..923375613a --- /dev/null +++ b/interface/resources/shaders/hmd_reproject.vert @@ -0,0 +1,20 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core +in vec3 Position; +in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} diff --git a/interface/resources/shaders/hmd_ui_glow.frag b/interface/resources/shaders/hmd_ui_glow.frag new file mode 100644 index 0000000000..733f32d718 --- /dev/null +++ b/interface/resources/shaders/hmd_ui_glow.frag @@ -0,0 +1,65 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform sampler2D sampler; +uniform float alpha = 1.0; +uniform vec4 glowPoints = vec4(-1); +uniform vec4 glowColors[2]; +uniform vec2 resolution = vec2(3960.0, 1188.0); +uniform float radius = 0.005; + +in vec3 vPosition; +in vec2 vTexCoord; +in vec4 vGlowPoints; + +out vec4 FragColor; + +float easeInOutCubic(float f) { + const float d = 1.0; + const float b = 0.0; + const float c = 1.0; + float t = f; + if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; + return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; +} + +void main() { + vec2 aspect = resolution; + aspect /= resolution.x; + FragColor = texture(sampler, vTexCoord); + + float glowIntensity = 0.0; + float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); + float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); + float dist = min(dist1, dist2); + vec3 glowColor = glowColors[0].rgb; + if (dist2 < dist1) { + glowColor = glowColors[1].rgb; + } + + if (dist <= radius) { + glowIntensity = 1.0 - (dist / radius); + glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); + glowIntensity = easeInOutCubic(glowIntensity); + glowIntensity = pow(glowIntensity, 0.5); + } + + if (alpha <= 0.0) { + if (glowIntensity <= 0.0) { + discard; + } + + FragColor = vec4(glowColor, glowIntensity); + return; + } + + FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); + FragColor.a *= alpha; +} \ No newline at end of file diff --git a/interface/resources/shaders/hmd_ui_glow.vert b/interface/resources/shaders/hmd_ui_glow.vert new file mode 100644 index 0000000000..5defec085f --- /dev/null +++ b/interface/resources/shaders/hmd_ui_glow.vert @@ -0,0 +1,23 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform mat4 mvp = mat4(1); + +in vec3 Position; +in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 2dc7df341a..1bfa6c7921 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -95,252 +95,6 @@ void HmdDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); } - -static const char * REPROJECTION_VS = R"VS(#version 410 core -in vec3 Position; -in vec2 TexCoord; - -out vec3 vPosition; -out vec2 vTexCoord; - -void main() { - gl_Position = vec4(Position, 1); - vTexCoord = TexCoord; - vPosition = Position; -} - -)VS"; - -static GLint REPROJECTION_MATRIX_LOCATION = -1; -static GLint INVERSE_PROJECTION_MATRIX_LOCATION = -1; -static GLint PROJECTION_MATRIX_LOCATION = -1; -static const char * REPROJECTION_FS = R"FS(#version 410 core - -uniform sampler2D sampler; -uniform mat3 reprojection = mat3(1); -uniform mat4 inverseProjections[2]; -uniform mat4 projections[2]; - -in vec2 vTexCoord; -in vec3 vPosition; - -out vec4 FragColor; - -void main() { - vec2 uv = vTexCoord; - - mat4 eyeInverseProjection; - mat4 eyeProjection; - - float xoffset = 1.0; - vec2 uvmin = vec2(0.0); - vec2 uvmax = vec2(1.0); - // determine the correct projection and inverse projection to use. - if (vTexCoord.x < 0.5) { - uvmax.x = 0.5; - eyeInverseProjection = inverseProjections[0]; - eyeProjection = projections[0]; - } else { - xoffset = -1.0; - uvmin.x = 0.5; - uvmax.x = 1.0; - eyeInverseProjection = inverseProjections[1]; - eyeProjection = projections[1]; - } - - // Account for stereo in calculating the per-eye NDC coordinates - vec4 ndcSpace = vec4(vPosition, 1.0); - ndcSpace.x *= 2.0; - ndcSpace.x += xoffset; - - // Convert from NDC to eyespace - vec4 eyeSpace = eyeInverseProjection * ndcSpace; - eyeSpace /= eyeSpace.w; - - // Convert to a noramlized ray - vec3 ray = eyeSpace.xyz; - ray = normalize(ray); - - // Adjust the ray by the rotation - ray = reprojection * ray; - - // Project back on to the texture plane - ray *= eyeSpace.z / ray.z; - - // Update the eyespace vector - eyeSpace.xyz = ray; - - // Reproject back into NDC - ndcSpace = eyeProjection * eyeSpace; - ndcSpace /= ndcSpace.w; - ndcSpace.x -= xoffset; - ndcSpace.x /= 2.0; - - // Calculate the new UV coordinates - uv = (ndcSpace.xy / 2.0) + 0.5; - if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { - FragColor = vec4(0.0, 0.0, 0.0, 1.0); - } else { - FragColor = texture(sampler, uv); - } -} -)FS"; - -#ifdef DEBUG_REPROJECTION_SHADER -#include -#include -#include -#include - -static const QString REPROJECTION_FS_FILE = "c:/Users/bdavis/Git/hifi/interface/resources/shaders/reproject.frag"; - -static ProgramPtr getReprojectionProgram() { - static ProgramPtr _currentProgram; - uint64_t now = usecTimestampNow(); - static uint64_t _lastFileCheck = now; - - bool modified = false; - if ((now - _lastFileCheck) > USECS_PER_MSEC * 100) { - QFileInfo info(REPROJECTION_FS_FILE); - QDateTime lastModified = info.lastModified(); - static QDateTime _lastModified = lastModified; - qDebug() << lastModified.toTime_t(); - qDebug() << _lastModified.toTime_t(); - if (lastModified > _lastModified) { - _lastModified = lastModified; - modified = true; - } - } - - if (!_currentProgram || modified) { - _currentProgram.reset(); - try { - QFile shaderFile(REPROJECTION_FS_FILE); - shaderFile.open(QIODevice::ReadOnly); - QString fragment = shaderFile.readAll(); - compileProgram(_currentProgram, REPROJECTION_VS, fragment.toLocal8Bit().data()); - } catch (const std::runtime_error& error) { - qDebug() << "Failed to build: " << error.what(); - } - if (!_currentProgram) { - _currentProgram = loadDefaultShader(); - } - } - return _currentProgram; -} -#endif - -static GLint PREVIEW_TEXTURE_LOCATION = -1; - -static const char * LASER_VS = R"VS(#version 410 core -uniform mat4 mvp = mat4(1); - -in vec3 Position; - -out vec3 vPosition; - -void main() { - gl_Position = mvp * vec4(Position, 1); - vPosition = Position; -} - -)VS"; - -static const char * LASER_GS = R"GS( -)GS"; - -static const char * LASER_FS = R"FS(#version 410 core - -uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); -in vec3 vPosition; - -out vec4 FragColor; - -void main() { - FragColor = color; -} - -)FS"; - - -static const char * LASER_GLOW_VS = R"VS(#version 410 core - -uniform mat4 mvp = mat4(1); - -in vec3 Position; -in vec2 TexCoord; - -out vec3 vPosition; -out vec2 vTexCoord; - -void main() { - gl_Position = mvp * vec4(Position, 1); - vTexCoord = TexCoord; - vPosition = Position; -} - -)VS"; - -static const char * LASER_GLOW_FS = R"FS(#version 410 core -#line 286 -uniform sampler2D sampler; -uniform float alpha = 1.0; -uniform vec4 glowPoints = vec4(-1); -uniform vec4 glowColor1 = vec4(0.01, 0.7, 0.9, 1.0); -uniform vec4 glowColor2 = vec4(0.9, 0.7, 0.01, 1.0); -uniform vec2 resolution = vec2(3960.0, 1188.0); -uniform float radius = 0.005; - -in vec3 vPosition; -in vec2 vTexCoord; -in vec4 vGlowPoints; - -out vec4 FragColor; - -float easeInOutCubic(float f) { - const float d = 1.0; - const float b = 0.0; - const float c = 1.0; - float t = f; - if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; - return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; -} - -void main() { - vec2 aspect = resolution; - aspect /= resolution.x; - FragColor = texture(sampler, vTexCoord); - - float glowIntensity = 0.0; - float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); - float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); - float dist = min(dist1, dist2); - vec3 glowColor = glowColor1.rgb; - if (dist2 < dist1) { - glowColor = glowColor2.rgb; - } - - if (dist <= radius) { - glowIntensity = 1.0 - (dist / radius); - glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); - glowIntensity = easeInOutCubic(glowIntensity); - glowIntensity = pow(glowIntensity, 0.5); - } - - if (alpha <= 0.0) { - if (glowIntensity <= 0.0) { - discard; - } - - FragColor = vec4(glowColor, glowIntensity); - return; - } - - FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); - FragColor.a *= alpha; -} -)FS"; - void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled @@ -354,76 +108,139 @@ void HmdDisplayPlugin::customizeContext() { if (!_enablePreview) { const std::string version("#version 410 core\n"); compileProgram(_previewProgram, version + DrawUnitQuadTexcoord_vert, version + DrawTexture_frag); - PREVIEW_TEXTURE_LOCATION = Uniform(*_previewProgram, "colorMap").Location(); + _previewUniforms.previewTexture = Uniform(*_previewProgram, "colorMap").Location(); } - updateGlowProgram(); - compileProgram(_laserProgram, LASER_VS, LASER_FS); + updateReprojectionProgram(); + updateOverlayProgram(); + updateLaserProgram(); + _laserGeometry = loadLaser(_laserProgram); - - compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); - REPROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "reprojection").Location(); - INVERSE_PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "inverseProjections").Location(); - PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "projections").Location(); } +//#define LIVE_SHADER_RELOAD 1 -#if 0 -static QString readFile(const char* filename) { +static QString readFile(const QString& filename) { QFile file(filename); file.open(QFile::Text | QFile::ReadOnly); QString result; result.append(QTextStream(&file).readAll()); return result; } -#endif - -void HmdDisplayPlugin::updateGlowProgram() { -#if 0 +void HmdDisplayPlugin::updateReprojectionProgram() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.vert"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.frag"; +#if LIVE_SHADER_RELOAD + static qint64 vsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + if (!_reprojectionProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { + vsBuiltAge = vsAge; + fsBuiltAge = fsAge; +#else + if (!_reprojectionProgram) { +#endif + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + ProgramPtr program; + try { + compileProgram(program, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (program) { + using namespace oglplus; + _reprojectionUniforms.reprojectionMatrix = Uniform(*program, "reprojection").Location(); + _reprojectionUniforms.inverseProjectionMatrix = Uniform(*program, "inverseProjections").Location(); + _reprojectionUniforms.projectionMatrix = Uniform(*program, "projections").Location(); + _reprojectionProgram = program; + } + } catch (std::runtime_error& error) { + qWarning() << "Error building reprojection shader " << error.what(); + } + } + +} + +void HmdDisplayPlugin::updateLaserProgram() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.vert"; + static const QString gsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.geom"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.frag"; + +#if LIVE_SHADER_RELOAD + static qint64 vsBuiltAge = 0; + static qint64 gsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + QFileInfo gsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + auto gsAge = gsInfo.lastModified().toMSecsSinceEpoch(); + if (!_laserProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge || gsAge > gsBuiltAge) { + vsBuiltAge = vsAge; + gsBuiltAge = gsAge; + fsBuiltAge = fsAge; +#else + if (!_laserProgram) { +#endif + + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + QString gsSource = readFile(gsFile); + ProgramPtr program; + try { + compileProgram(program, vsSource.toLocal8Bit().toStdString(), gsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (program) { + using namespace oglplus; + _laserUniforms.color = Uniform(*program, "color").Location(); + _laserUniforms.mvp = Uniform(*program, "mvp").Location(); + _laserProgram = program; + } + } catch (std::runtime_error& error) { + qWarning() << "Error building hand laser composite shader " << error.what(); + } + } +} + +void HmdDisplayPlugin::updateOverlayProgram() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.vert"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.frag"; + +#if LIVE_SHADER_RELOAD static qint64 vsBuiltAge = 0; static qint64 fsBuiltAge = 0; - static const char* vsFile = "H:/glow_vert.glsl"; - static const char* fsFile = "H:/glow_frag.glsl"; QFileInfo vsInfo(vsFile); QFileInfo fsInfo(fsFile); auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); if (!_overlayProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { - QString vsSource = readFile(vsFile); - QString fsSource = readFile(fsFile); - ProgramPtr newOverlayProgram; - compileProgram(newOverlayProgram, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); - if (newOverlayProgram) { - using namespace oglplus; - _overlayProgram = newOverlayProgram; - auto uniforms = _overlayProgram->ActiveUniforms(); - _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); - _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); - _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); - _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); - _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); - useProgram(_overlayProgram); - Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); - } vsBuiltAge = vsAge; fsBuiltAge = fsAge; - - } #else if (!_overlayProgram) { - compileProgram(_overlayProgram, LASER_GLOW_VS, LASER_GLOW_FS); - using namespace oglplus; - auto uniforms = _overlayProgram->ActiveUniforms(); - _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); - _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); - _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); - _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); - _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); - useProgram(_overlayProgram); - Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); - } - #endif + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + ProgramPtr program; + try { + compileProgram(program, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (program) { + using namespace oglplus; + _overlayUniforms.mvp = Uniform(*program, "mvp").Location(); + _overlayUniforms.alpha = Uniform(*program, "alpha").Location(); + _overlayUniforms.glowColors = Uniform(*program, "glowColors").Location(); + _overlayUniforms.glowPoints = Uniform(*program, "glowPoints").Location(); + _overlayUniforms.resolution = Uniform(*program, "resolution").Location(); + _overlayUniforms.radius = Uniform(*program, "radius").Location(); + _overlayProgram = program; + useProgram(_overlayProgram); + Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); + } + } catch (std::runtime_error& error) { + qWarning() << "Error building overlay composite shader " << error.what(); + } + } } void HmdDisplayPlugin::uncustomizeContext() { @@ -459,12 +276,12 @@ void HmdDisplayPlugin::compositeScene() { using namespace oglplus; Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); - Uniform(*_reprojectionProgram, REPROJECTION_MATRIX_LOCATION).Set(_currentPresentFrameInfo.presentReprojection); + Uniform(*_reprojectionProgram, _reprojectionUniforms.reprojectionMatrix).Set(_currentPresentFrameInfo.presentReprojection); //Uniform(*_reprojectionProgram, PROJECTION_MATRIX_LOCATION).Set(_eyeProjections); //Uniform(*_reprojectionProgram, INVERSE_PROJECTION_MATRIX_LOCATION).Set(_eyeInverseProjections); // FIXME what's the right oglplus mechanism to do this? It's not that ^^^ ... better yet, switch to a uniform buffer - glUniformMatrix4fv(INVERSE_PROJECTION_MATRIX_LOCATION, 2, GL_FALSE, &(_eyeInverseProjections[0][0][0])); - glUniformMatrix4fv(PROJECTION_MATRIX_LOCATION, 2, GL_FALSE, &(_eyeProjections[0][0][0])); + glUniformMatrix4fv(_reprojectionUniforms.inverseProjectionMatrix, 2, GL_FALSE, &(_eyeInverseProjections[0][0][0])); + glUniformMatrix4fv(_reprojectionUniforms.projectionMatrix, 2, GL_FALSE, &(_eyeProjections[0][0][0])); _plane->UseInProgram(*_reprojectionProgram); _plane->Draw(); } @@ -530,7 +347,11 @@ void HmdDisplayPlugin::compositeOverlay() { handGlowPoints[i] = yawPitch; } - updateGlowProgram(); + updateOverlayProgram(); + if (!_overlayProgram) { + return; + } + useProgram(_overlayProgram); // Setup the uniforms { @@ -541,6 +362,12 @@ void HmdDisplayPlugin::compositeOverlay() { vec4 glowPoints(handGlowPoints[0], handGlowPoints[1]); Uniform(*_overlayProgram, _overlayUniforms.glowPoints).Set(glowPoints); } + if (_overlayUniforms.glowColors >= 0) { + std::array glowColors; + glowColors[0] = _presentHandLasers[0].color; + glowColors[1] = _presentHandLasers[1].color; + glProgramUniform4fv(GetName(*_overlayProgram), _overlayUniforms.glowColors, 2, &glowColors[0].r); + } } _sphereSection->Use(); @@ -634,7 +461,7 @@ void HmdDisplayPlugin::internalPresent() { glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); glViewport(targetViewportPosition.x, targetViewportPosition.y, targetViewportSize.x, targetViewportSize.y); - glUniform1i(PREVIEW_TEXTURE_LOCATION, 0); + glUniform1i(_previewUniforms.previewTexture, 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _previewTextureID); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); @@ -698,6 +525,8 @@ void HmdDisplayPlugin::compositeExtra() { return; } + updateLaserProgram(); + // Render hand lasers using namespace oglplus; useProgram(_laserProgram); @@ -739,6 +568,7 @@ void HmdDisplayPlugin::compositeExtra() { handLaserModelMatrices[i] = model; } + glEnable(GL_BLEND); for_each_eye([&](Eye eye) { eyeViewport(eye); auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); @@ -753,4 +583,5 @@ void HmdDisplayPlugin::compositeExtra() { _laserGeometry->Draw(); } }); + glDisable(GL_BLEND); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index be2811076d..f168ec9607 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -61,15 +61,6 @@ protected: } }; - ProgramPtr _overlayProgram; - struct OverlayUniforms { - int32_t mvp { -1 }; - int32_t alpha { -1 }; - int32_t glowPoints { -1 }; - int32_t resolution { -1 }; - int32_t radius { -1 }; - } _overlayUniforms; - Transform _uiModelTransform; std::array _handLasers; std::array _handPoses; @@ -82,8 +73,6 @@ protected: std::array _eyeProjections; std::array _eyeInverseProjections; - - glm::mat4 _cullingProjection; glm::uvec2 _renderTargetSize; float _ipd { 0.064f }; @@ -103,23 +92,49 @@ protected: FrameInfo _currentRenderFrameInfo; private: - void updateGlowProgram(); + void updateOverlayProgram(); + void updateLaserProgram(); + void updateReprojectionProgram(); bool _enablePreview { false }; bool _monoPreview { true }; bool _enableReprojection { true }; bool _firstPreview { true }; + ProgramPtr _overlayProgram; + struct OverlayUniforms { + int32_t mvp { -1 }; + int32_t alpha { -1 }; + int32_t glowColors { -1 }; + int32_t glowPoints { -1 }; + int32_t resolution { -1 }; + int32_t radius { -1 }; + } _overlayUniforms; + ProgramPtr _previewProgram; + struct PreviewUniforms { + int32_t previewTexture { -1 }; + } _previewUniforms; + float _previewAspect { 0 }; GLuint _previewTextureID { 0 }; glm::uvec2 _prevWindowSize { 0, 0 }; qreal _prevDevicePixelRatio { 0 }; ProgramPtr _reprojectionProgram; + struct ReprojectionUniforms { + int32_t reprojectionMatrix { -1 }; + int32_t inverseProjectionMatrix { -1 }; + int32_t projectionMatrix { -1 }; + } _reprojectionUniforms; + ShapeWrapperPtr _sphereSection; ProgramPtr _laserProgram; + struct LaserUniforms { + int32_t mvp { -1 }; + int32_t color { -1 }; + } _laserUniforms; ShapeWrapperPtr _laserGeometry; }; diff --git a/libraries/gl/src/gl/OglplusHelpers.cpp b/libraries/gl/src/gl/OglplusHelpers.cpp index 7a535a806d..1154042b4a 100644 --- a/libraries/gl/src/gl/OglplusHelpers.cpp +++ b/libraries/gl/src/gl/OglplusHelpers.cpp @@ -87,6 +87,36 @@ ProgramPtr loadCubemapShader() { return result; } +void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& gs, const std::string& fs) { + using namespace oglplus; + try { + result = std::make_shared(); + // attach the shaders to the program + result->AttachShader( + VertexShader() + .Source(GLSLSource(vs)) + .Compile() + ); + result->AttachShader( + GeometryShader() + .Source(GLSLSource(gs)) + .Compile() + ); + result->AttachShader( + FragmentShader() + .Source(GLSLSource(fs)) + .Compile() + ); + result->Link(); + } catch (ProgramBuildError& err) { + Q_UNUSED(err); + qWarning() << err.Log().c_str(); + Q_ASSERT_X(false, "compileProgram", "Failed to build shader program"); + qFatal("%s", (const char*)err.Message); + result.reset(); + } +} + void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs) { using namespace oglplus; try { diff --git a/libraries/gl/src/gl/OglplusHelpers.h b/libraries/gl/src/gl/OglplusHelpers.h index 8940205b21..c453fbad28 100644 --- a/libraries/gl/src/gl/OglplusHelpers.h +++ b/libraries/gl/src/gl/OglplusHelpers.h @@ -62,6 +62,8 @@ using Mat4Uniform = oglplus::Uniform; ProgramPtr loadDefaultShader(); ProgramPtr loadCubemapShader(); void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs); +void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& gs, const std::string& fs); + ShapeWrapperPtr loadSkybox(ProgramPtr program); ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect = 1.0f); ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 128, int stacks = 128); From 71b6210c4ea812b2cc0d802ba1096bbd417383a7 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 12 Jul 2016 10:18:37 -0700 Subject: [PATCH 1039/1237] removed some log spam --- assignment-client/src/audio/AudioMixer.cpp | 2 -- libraries/audio-client/src/AudioClient.cpp | 2 -- 2 files changed, 4 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 720f9d52de..51c4e25410 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -464,8 +464,6 @@ void saveInputPluginSettings(const InputPluginList& plugins) { void AudioMixer::handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode) { - qDebug() << __FUNCTION__; - QStringList availableCodecs; auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); if (codecPlugins.size() > 0) { diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 28fd95f599..0cd01bb579 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -531,8 +531,6 @@ void AudioClient::negotiateAudioFormat() { } void AudioClient::handleSelectedAudioFormat(QSharedPointer message) { - qDebug() << __FUNCTION__; - _selectedCodecName = message->readString(); qDebug() << "Selected Codec:" << _selectedCodecName; From 2b719e12f72cc8892778ff174babec5531634bd5 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 12 Jul 2016 10:54:45 -0700 Subject: [PATCH 1040/1237] attempt to make mac server plugins work --- cmake/macros/SetupHifiClientServerPlugin.cmake | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cmake/macros/SetupHifiClientServerPlugin.cmake b/cmake/macros/SetupHifiClientServerPlugin.cmake index 5ace348e87..dfe3113fbc 100644 --- a/cmake/macros/SetupHifiClientServerPlugin.cmake +++ b/cmake/macros/SetupHifiClientServerPlugin.cmake @@ -12,17 +12,22 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Plugins") if (APPLE) - set(PLUGIN_PATH "${INTERFACE_BUNDLE_NAME}.app/Contents/PlugIns") + set(CLIENT_PLUGIN_PATH "${INTERFACE_BUNDLE_NAME}.app/Contents/PlugIns") + set(SERVER_PLUGIN_PATH "Components.app/Contents/PlugIns") else() - set(PLUGIN_PATH "plugins") + set(CLIENT_PLUGIN_PATH "plugins") + set(SERVER_PLUGIN_PATH "plugins") endif() if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_GENERATOR STREQUAL "Unix Makefiles") - set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${PLUGIN_PATH}/") - set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/${PLUGIN_PATH}/") + set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${CLIENT_PLUGIN_PATH}/") + set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/${SERVER_PLUGIN_PATH}/") + elseif (APPLE) + set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/$/${CLIENT_PLUGIN_PATH}/") + set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/$/${SERVER_PLUGIN_PATH}/") else() - set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/$/${PLUGIN_PATH}/") - set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/$/${PLUGIN_PATH}/") + set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/$/${CLIENT_PLUGIN_PATH}/") + set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/$/${SERVER_PLUGIN_PATH}/") endif() # create the destination for the client plugin binaries From 2f07f0d24f53096c735e06109511e38cfa7afb91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Brisset?= Date: Fri, 8 Jul 2016 18:32:59 -0700 Subject: [PATCH 1041/1237] Revert "Revert "Fix crash in packet list"" --- domain-server/src/DomainServer.cpp | 2 ++ libraries/networking/src/LimitedNodeList.cpp | 1 + libraries/networking/src/LimitedNodeList.h | 4 ++-- libraries/networking/src/NodeList.cpp | 7 +++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d75a2c3245..8b3f09d1f7 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -389,6 +389,8 @@ void DomainServer::setupNodeListAndAssignments() { const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant) { nodeList->setSessionUUID(idValueVariant->toString()); + } else { + nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID } connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d7a2d47fab..a03fa43093 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -522,6 +522,7 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret) { + QReadLocker readLocker(&_nodeMutex); NodeHash::const_iterator it = _nodeHash.find(uuid); if (it != _nodeHash.end()) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 483aa0734c..c9054ac6d7 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -131,7 +131,7 @@ public: std::function linkedDataCreateCallback; - size_t size() const { return _nodeHash.size(); } + size_t size() const { QReadLocker readLock(&_nodeMutex); return _nodeHash.size(); } SharedNodePointer nodeWithUUID(const QUuid& nodeUUID); @@ -287,7 +287,7 @@ protected: QUuid _sessionUUID; NodeHash _nodeHash; - QReadWriteLock _nodeMutex; + mutable QReadWriteLock _nodeMutex; udt::Socket _nodeSocket; QUdpSocket* _dtlsSocket; HifiSockAddr _localSockAddr; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index fd1442d639..02350ac23c 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -513,9 +513,16 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { if (_domainHandler.getSockAddr().isNull()) { + qWarning() << "IGNORING DomainList packet while not connected to a Domain Server"; // refuse to process this packet if we aren't currently connected to the DS return; } + QUuid domainHandlerUUID = _domainHandler.getUUID(); + QUuid messageSourceUUID = message->getSourceID(); + if (!domainHandlerUUID.isNull() && domainHandlerUUID != messageSourceUUID) { + qWarning() << "IGNORING DomainList packet from" << messageSourceUUID << "while connected to" << domainHandlerUUID; + return; + } // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; From 1a91a8e20a0113eaa83b5541e7e190e5306cfa64 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 8 Jul 2016 18:38:25 -0700 Subject: [PATCH 1042/1237] Make DomainList a sourced packet --- libraries/networking/src/udt/PacketHeaders.cpp | 4 ++-- libraries/networking/src/udt/PacketHeaders.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6359ad0aff..bab33e1158 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -31,7 +31,7 @@ const QSet NON_VERIFIED_PACKETS = QSet() const QSet NON_SOURCED_PACKETS = QSet() << PacketType::StunResponse << PacketType::CreateAssignment << PacketType::RequestAssignment << PacketType::DomainServerRequireDTLS << PacketType::DomainConnectRequest - << PacketType::DomainList << PacketType::DomainConnectionDenied + << PacketType::DomainConnectionDenied << PacketType::DomainServerPathQuery << PacketType::DomainServerPathResponse << PacketType::DomainServerAddedNode << PacketType::DomainServerConnectionToken << PacketType::DomainSettingsRequest << PacketType::DomainSettings @@ -45,7 +45,7 @@ const QSet RELIABLE_PACKETS = QSet(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return static_cast(DomainListVersion::PermissionsGrid); + return static_cast(DomainListVersion::IsSourcedPacket); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 9140cf8738..67a6a4462d 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -208,7 +208,8 @@ enum class DomainServerAddedNodeVersion : PacketVersion { enum class DomainListVersion : PacketVersion { PrePermissionsGrid = 18, - PermissionsGrid + PermissionsGrid, + IsSourcedPacket }; #endif // hifi_PacketHeaders_h From 19991939073353002b098df01a6d193e67890832 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 12 Jul 2016 11:42:58 -0700 Subject: [PATCH 1043/1237] split the grab script's deactivation on an entity into two parts -- one that happens during release and a delayed part which finishes up. This makes collisions with a thrower's capsule less likely --- .../system/controllers/handControllerGrab.js | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index aaaa7c5962..ac903190c4 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -181,9 +181,9 @@ var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic"; var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; -var avCollideLater; -var setCollidesLaterTimeout; -var collideLaterID; +var delayedDeactivateFunc; +var delayedDeactivateTimeout; +var delayedDeactivateEntityID; var CONTROLLER_STATE_MACHINE = {}; @@ -2064,15 +2064,15 @@ function MyController(hand) { } this.entityActivated = true; - if (setCollidesLaterTimeout && collideLaterID == entityID) { + if (delayedDeactivateTimeout && delayedDeactivateEntityID == entityID) { // we have a timeout waiting to set collisions with myAvatar back on (so that when something // is thrown it doesn't collide with the avatar's capsule the moment it's released). We've // regrabbed the entity before the timeout fired, so cancel the timeout, run the function now // and adjust the grabbedProperties. This will make the saved set of properties (the ones that // get re-instated after all the grabs have been released) be correct. - Script.clearTimeout(setCollidesLaterTimeout); - setCollidesLaterTimeout = null; - grabbedProperties["collidesWith"] = avCollideLater(); + Script.clearTimeout(delayedDeactivateTimeout); + delayedDeactivateTimeout = null; + grabbedProperties["collidesWith"] = delayedDeactivateFunc(); } var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); @@ -2142,7 +2142,27 @@ function MyController(hand) { }); }; - this.deactivateEntity = function (entityID, noVelocity) { + this.delayedDeactivateEntity = function (entityID, collidesWith) { + // If, before the grab started, the held entity collided with myAvatar, we do the deactivation in + // two parts. Most of it is done in deactivateEntity(), but the final collidesWith and refcount + // are delayed a bit. This keeps thrown things from colliding with the avatar's capsule so often. + // The refcount is handled in this delayed fashion so things don't get confused if someone else + // grabs the entity before the timeout fires. + Entities.editEntity(entityID, { collidesWith: collidesWith }); + var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); + if (data && data["refCount"]) { + data["refCount"] = data["refCount"] - 1; + if (data["refCount"] < 1) { + data = null; + } + } else { + data = null; + } + + setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); + }; + + this.deactivateEntity = function (entityID, noVelocity, delayed) { var deactiveProps; if (!this.entityActivated) { @@ -2151,12 +2171,13 @@ function MyController(hand) { this.entityActivated = false; var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); + var doDelayedDeactivate = false; if (data && data["refCount"]) { data["refCount"] = data["refCount"] - 1; if (data["refCount"] < 1) { deactiveProps = { gravity: data["gravity"], - // don't set collidesWith back right away, because thrown things tend to bounce off the + // don't set collidesWith myAvatar back right away, because thrown things tend to bounce off the // avatar's capsule. collidesWith: removeMyAvatarFromCollidesWith(data["collidesWith"]), collisionless: data["collisionless"], @@ -2165,17 +2186,20 @@ function MyController(hand) { parentJointIndex: data["parentJointIndex"] }; - if (data["collidesWith"].indexOf("myAvatar") >= 0) { - var javaScriptScopingIsBrokenData = data; - avCollideLater = function () { + doDelayedDeactivate = (data["collidesWith"].indexOf("myAvatar") >= 0); + + if (doDelayedDeactivate) { + var delayedCollidesWith = data["collidesWith"]; + var delayedEntityID = entityID; + delayedDeactivateFunc = function () { // set collidesWith back to original value a bit later than the rest - _this.setCollidesLaterTimeout = null; - Entities.editEntity(entityID, { collidesWith: javaScriptScopingIsBrokenData["collidesWith"] }); - return javaScriptScopingIsBrokenData["collidesWith"]; + delayedDeactivateTimeout = null; + _this.delayedDeactivateEntity(delayedEntityID, delayedCollidesWith); + return delayedCollidesWith; } - setCollidesLaterTimeout = - Script.setTimeout(avCollideLater, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY * MSECS_PER_SEC); - collideLaterID = entityID; + delayedDeactivateTimeout = + Script.setTimeout(delayedDeactivateFunc, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY * MSECS_PER_SEC); + delayedDeactivateEntityID = entityID; } // things that are held by parenting and dropped with no velocity will end up as "static" in bullet. If @@ -2220,7 +2244,6 @@ function MyController(hand) { // angularVelocity: this.currentAngularVelocity }); } - data = null; } else if (this.shouldResetParentOnRelease) { // we parent-grabbed this from another parent grab. try to put it back where we found it. @@ -2239,7 +2262,9 @@ function MyController(hand) { } else { data = null; } - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); + if (!doDelayedDeactivate) { + setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); + } }; this.getOtherHandController = function () { From a7d631b7e1203c6f8754a7f216b36585dca80f00 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 12 Jul 2016 11:43:31 -0700 Subject: [PATCH 1044/1237] Revert packet version change --- libraries/networking/src/udt/PacketHeaders.cpp | 4 ++-- libraries/networking/src/udt/PacketHeaders.h | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index bab33e1158..6359ad0aff 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -31,7 +31,7 @@ const QSet NON_VERIFIED_PACKETS = QSet() const QSet NON_SOURCED_PACKETS = QSet() << PacketType::StunResponse << PacketType::CreateAssignment << PacketType::RequestAssignment << PacketType::DomainServerRequireDTLS << PacketType::DomainConnectRequest - << PacketType::DomainConnectionDenied + << PacketType::DomainList << PacketType::DomainConnectionDenied << PacketType::DomainServerPathQuery << PacketType::DomainServerPathResponse << PacketType::DomainServerAddedNode << PacketType::DomainServerConnectionToken << PacketType::DomainSettingsRequest << PacketType::DomainSettings @@ -45,7 +45,7 @@ const QSet RELIABLE_PACKETS = QSet(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return static_cast(DomainListVersion::IsSourcedPacket); + return static_cast(DomainListVersion::PermissionsGrid); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 67a6a4462d..9140cf8738 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -208,8 +208,7 @@ enum class DomainServerAddedNodeVersion : PacketVersion { enum class DomainListVersion : PacketVersion { PrePermissionsGrid = 18, - PermissionsGrid, - IsSourcedPacket + PermissionsGrid }; #endif // hifi_PacketHeaders_h From 7a7b64e87d58d2039323ea0e4fad387f71bfdc02 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 12 Jul 2016 11:43:46 -0700 Subject: [PATCH 1045/1237] Use domain ID in the stream --- libraries/networking/src/NodeList.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 02350ac23c..d3fc93b991 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -517,12 +517,6 @@ void NodeList::processDomainServerList(QSharedPointer message) // refuse to process this packet if we aren't currently connected to the DS return; } - QUuid domainHandlerUUID = _domainHandler.getUUID(); - QUuid messageSourceUUID = message->getSourceID(); - if (!domainHandlerUUID.isNull() && domainHandlerUUID != messageSourceUUID) { - qWarning() << "IGNORING DomainList packet from" << messageSourceUUID << "while connected to" << domainHandlerUUID; - return; - } // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; @@ -542,6 +536,10 @@ void NodeList::processDomainServerList(QSharedPointer message) if (!_domainHandler.isConnected()) { _domainHandler.setUUID(domainUUID); _domainHandler.setIsConnected(true); + } else if (_domainHandler.getUUID() != domainUUID) { + // Recieved packet from different domain. + qWarning() << "IGNORING DomainList packet from" << domainUUID << "while connected to" << _domainHandler.getUUID(); + return; } // pull our owner UUID from the packet, it's always the first thing From 029251f29335442c5d3847b4ad1474d9d43962be Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 10 Jul 2016 22:04:47 -0700 Subject: [PATCH 1046/1237] More render performace testing work --- tests/render-perf/src/Camera.hpp | 138 ++++ tests/render-perf/src/TextOverlay.hpp | 239 ++++++ tests/render-perf/src/main.cpp | 442 ++++++----- .../src/stb_font_consolas_24_latin1.inl | 734 ++++++++++++++++++ 4 files changed, 1356 insertions(+), 197 deletions(-) create mode 100644 tests/render-perf/src/Camera.hpp create mode 100644 tests/render-perf/src/TextOverlay.hpp create mode 100644 tests/render-perf/src/stb_font_consolas_24_latin1.inl diff --git a/tests/render-perf/src/Camera.hpp b/tests/render-perf/src/Camera.hpp new file mode 100644 index 0000000000..fbae04540b --- /dev/null +++ b/tests/render-perf/src/Camera.hpp @@ -0,0 +1,138 @@ +#pragma once + +#include + +class Camera { +protected: + float fov { 60.0f }; + float znear { DEFAULT_NEAR_CLIP }, zfar { DEFAULT_FAR_CLIP }; + float aspect { 1.0f }; + + void updateViewMatrix() { + matrices.view = glm::inverse(glm::translate(glm::mat4(), position) * glm::mat4_cast(getOrientation())); + } + +public: + glm::quat getOrientation() const { + return glm::angleAxis(yaw, Vectors::UP); + } + float yaw { 0 }; + glm::vec3 position; + float rotationSpeed { 1.0f }; + float movementSpeed { 1.0f }; + + struct Matrices { + glm::mat4 perspective; + glm::mat4 view; + } matrices; + enum Key { + RIGHT, + LEFT, + UP, + DOWN, + BACK, + FORWARD, + KEYS_SIZE, + INVALID = -1, + }; + + std::bitset keys; + + Camera() { + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + } + + bool moving() { + return keys.any(); + } + + void setFieldOfView(float fov) { + this->fov = fov; + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + } + + void setAspectRatio(const glm::vec2& size) { + setAspectRatio(size.x / size.y); + } + + void setAspectRatio(float aspect) { + this->aspect = aspect; + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + } + + void setPerspective(float fov, const glm::vec2& size, float znear = 0.1f, float zfar = 512.0f) { + setPerspective(fov, size.x / size.y, znear, zfar); + } + + void setPerspective(float fov, float aspect, float znear = 0.1f, float zfar = 512.0f) { + this->aspect = aspect; + this->fov = fov; + this->znear = znear; + this->zfar = zfar; + matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); + }; + + void rotate(const float delta) { + yaw += delta; + updateViewMatrix(); + } + + void setRotation(const glm::quat& rotation) { + glm::vec3 f = rotation * Vectors::UNIT_NEG_Z; + f.y = 0; + f = glm::normalize(f); + yaw = angleBetween(Vectors::UNIT_NEG_Z, f); + updateViewMatrix(); + } + + void setPosition(const glm::vec3& position) { + this->position = position; + updateViewMatrix(); + } + + // Translate in the Z axis of the camera + void dolly(float delta) { + auto direction = glm::vec3(0, 0, delta); + translate(direction); + } + + // Translate in the XY plane of the camera + void translate(const glm::vec2& delta) { + auto move = glm::vec3(delta.x, delta.y, 0); + translate(move); + } + + void translate(const glm::vec3& delta) { + position += getOrientation() * delta; + updateViewMatrix(); + } + + void update(float deltaTime) { + if (moving()) { + glm::vec3 camFront = getOrientation() * Vectors::FRONT; + glm::vec3 camRight = getOrientation() * Vectors::RIGHT; + glm::vec3 camUp = getOrientation() * Vectors::UP; + float moveSpeed = deltaTime * movementSpeed; + + if (keys[FORWARD]) { + position += camFront * moveSpeed; + } + if (keys[BACK]) { + position -= camFront * moveSpeed; + } + if (keys[LEFT]) { + position -= camRight * moveSpeed; + } + if (keys[RIGHT]) { + position += camRight * moveSpeed; + } + if (keys[UP]) { + position += camUp * moveSpeed; + } + if (keys[DOWN]) { + position -= camUp * moveSpeed; + } + updateViewMatrix(); + } + } +}; diff --git a/tests/render-perf/src/TextOverlay.hpp b/tests/render-perf/src/TextOverlay.hpp new file mode 100644 index 0000000000..d9a9f0a320 --- /dev/null +++ b/tests/render-perf/src/TextOverlay.hpp @@ -0,0 +1,239 @@ +/* +* Text overlay class for displaying debug information +* +* Copyright (C) 2016 by Sascha Willems - www.saschawillems.de +* +* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +#include "stb_font_consolas_24_latin1.inl" + +// Defines for the STB font used +// STB font files can be found at http://nothings.org/stb/font/ +#define STB_FONT_NAME stb_font_consolas_24_latin1 +#define STB_FONT_WIDTH STB_FONT_consolas_24_latin1_BITMAP_WIDTH +#define STB_FONT_HEIGHT STB_FONT_consolas_24_latin1_BITMAP_HEIGHT +#define STB_FIRST_CHAR STB_FONT_consolas_24_latin1_FIRST_CHAR +#define STB_NUM_CHARS STB_FONT_consolas_24_latin1_NUM_CHARS + +// Max. number of chars the text overlay buffer can hold +#define MAX_CHAR_COUNT 1024 + +// Mostly self-contained text overlay class +// todo : comment +class TextOverlay { +private: + FramebufferPtr _framebuffer; + TexturePtr _texture; + uvec2 _size; + BufferPtr _vertexBuffer; + ProgramPtr _program; + VertexArrayPtr _vertexArray; + + //vk::DescriptorPool descriptorPool; + //vk::DescriptorSetLayout descriptorSetLayout; + //vk::DescriptorSet descriptorSet; + //vk::PipelineLayout pipelineLayout; + //vk::Pipeline pipeline; + + // Pointer to mapped vertex buffer + glm::vec4* _mapped { nullptr }; + stb_fontchar stbFontData[STB_NUM_CHARS]; + uint32_t numLetters; + + const char* const VERTEX_SHADER = R"SHADER( +#version 450 core +layout (location = 0) in vec2 inPos; +layout (location = 1) in vec2 inUV; + +layout (location = 0) out vec2 outUV; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main(void) +{ + vec2 pos = inPos; + pos.y *= -1.0; + gl_Position = vec4(pos, 0.0, 1.0); + outUV = inUV; +} +)SHADER"; + + const char* const FRAGMENT_SHADER = R"SHADER( +#version 450 core + +layout (location = 0) in vec2 inUV; + +layout (binding = 0) uniform sampler2D samplerFont; + +layout (location = 0) out vec4 outFragColor; + +void main(void) +{ + float color = texture(samplerFont, inUV).r; + outFragColor = vec4(vec3(color), 1.0); +} +)SHADER"; + +public: + enum TextAlign { alignLeft, alignCenter, alignRight }; + + bool visible = true; + bool invalidated = false; + + + TextOverlay(const glm::uvec2& size) : _size(size) { + prepare(); + } + + ~TextOverlay() { + // Free up all Vulkan resources requested by the text overlay + _program.reset(); + _texture.reset(); + _vertexBuffer.reset(); + _vertexArray.reset(); + } + + void resize(const uvec2& size) { + _size = size; + } + + // Prepare all vulkan resources required to render the font + // The text overlay uses separate resources for descriptors (pool, sets, layouts), pipelines and command buffers + void prepare() { + static unsigned char font24pixels[STB_FONT_HEIGHT][STB_FONT_WIDTH]; + STB_FONT_NAME(stbFontData, font24pixels, STB_FONT_HEIGHT); + + // Vertex buffer + { + GLuint buffer; + GLuint bufferSize = MAX_CHAR_COUNT * sizeof(glm::vec4); + glCreateBuffers(1, &buffer); + glNamedBufferStorage(buffer, bufferSize, nullptr, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + using BufferName = oglplus::ObjectName; + BufferName name = BufferName(buffer); + _vertexBuffer = std::make_shared(oglplus::Buffer::FromRawName(name)); + _vertexArray = std::make_shared(); + _vertexArray->Bind(); + glBindBuffer(GL_ARRAY_BUFFER, buffer); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), 0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (void*)sizeof(glm::vec2)); + glBindVertexArray(0); + } + + // Font texture + { + GLuint texture; + glCreateTextures(GL_TEXTURE_2D, 1, &texture); + glTextureStorage2D(texture, 1, GL_R8, STB_FONT_WIDTH, STB_FONT_HEIGHT); + glTextureSubImage2D(texture, 0, 0, 0, STB_FONT_WIDTH, STB_FONT_HEIGHT, GL_RED, GL_UNSIGNED_BYTE, &font24pixels[0][0]); + glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + using TextureName = oglplus::ObjectName; + TextureName name = TextureName(texture); + _texture = std::make_shared(oglplus::Texture::FromRawName(name)); + } + + compileProgram(_program, VERTEX_SHADER, FRAGMENT_SHADER); + } + + + // Map buffer + void beginTextUpdate() { + using namespace oglplus; + _mapped = (glm::vec4*)glMapNamedBuffer(GetName(*_vertexBuffer), GL_WRITE_ONLY); + numLetters = 0; + } + + // Add text to the current buffer + // todo : drop shadow? color attribute? + void addText(std::string text, vec2 pos, TextAlign align) { + assert(_mapped != nullptr); + const vec2 fbSize = _size; + const vec2 charSize = vec2(1.5f) / fbSize; + pos = (pos / fbSize * 2.0f) - 1.0f; + + // Calculate text width + float textWidth = 0; + for (auto letter : text) { + stb_fontchar *charData = &stbFontData[(uint32_t)letter - STB_FIRST_CHAR]; + textWidth += charData->advance * charSize.x; + } + + switch (align) { + case alignRight: + pos.x -= textWidth; + break; + case alignCenter: + pos.x -= textWidth / 2.0f; + break; + case alignLeft: + break; + } + + // Generate a uv mapped quad per char in the new text + for (auto letter : text) { + stb_fontchar *charData = &stbFontData[(uint32_t)letter - STB_FIRST_CHAR]; + _mapped->x = pos.x + (float)charData->x0 * charSize.x; + _mapped->y = pos.y + (float)charData->y0 * charSize.y; + _mapped->z = charData->s0; + _mapped->w = charData->t0; + ++_mapped; + + _mapped->x = pos.x + (float)charData->x1 * charSize.x; + _mapped->y = pos.y + (float)charData->y0 * charSize.y; + _mapped->z = charData->s1; + _mapped->w = charData->t0; + ++_mapped; + + _mapped->x = pos.x + (float)charData->x0 * charSize.x; + _mapped->y = pos.y + (float)charData->y1 * charSize.y; + _mapped->z = charData->s0; + _mapped->w = charData->t1; + _mapped++; + + _mapped->x = pos.x + (float)charData->x1 * charSize.x; + _mapped->y = pos.y + (float)charData->y1 * charSize.y; + _mapped->z = charData->s1; + _mapped->w = charData->t1; + _mapped++; + pos.x += charData->advance * charSize.x; + numLetters++; + } + } + + // Unmap buffer and update command buffers + void endTextUpdate() { + glUnmapNamedBuffer(GetName(*_vertexBuffer)); + _mapped = nullptr; + } + + // Needs to be called by the application + void render() { + _texture->Bind(oglplus::TextureTarget::_2D); + _program->Use(); + _vertexArray->Bind(); + for (uint32_t j = 0; j < numLetters; j++) { + glDrawArrays(GL_TRIANGLE_STRIP, j * 4, 4); + } + } +}; diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 5367c65d94..f3e3271a5d 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -26,14 +27,19 @@ #include #include +#include +#include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -51,7 +57,11 @@ #include #include +#include "Camera.hpp" +#include "TextOverlay.hpp" + static const QString LAST_SCENE_KEY = "lastSceneFile"; +static const QString LAST_LOCATION_KEY = "lastLocation"; class ParentFinder : public SpatialParentFinder { public: @@ -83,133 +93,6 @@ public: } }; -class Camera { -protected: - float fov { 60.0f }; - float znear { 0.1f }, zfar { 512.0f }; - float aspect { 1.0f }; - - void updateViewMatrix() { - matrices.view = glm::inverse(glm::translate(glm::mat4(), position) * glm::mat4_cast(getOrientation())); - } - - glm::quat getOrientation() const { - return glm::angleAxis(yaw, Vectors::UP); - } -public: - float yaw { 0 }; - glm::vec3 position; - - float rotationSpeed { 1.0f }; - float movementSpeed { 1.0f }; - - struct Matrices { - glm::mat4 perspective; - glm::mat4 view; - } matrices; - enum Key { - RIGHT, - LEFT, - UP, - DOWN, - BACK, - FORWARD, - KEYS_SIZE, - INVALID = -1, - }; - - std::bitset keys; - - Camera() { - matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); - } - - bool moving() { - return keys.any(); - } - - void setFieldOfView(float fov) { - this->fov = fov; - matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); - } - - void setAspectRatio(const glm::vec2& size) { - setAspectRatio(size.x / size.y); - } - - void setAspectRatio(float aspect) { - this->aspect = aspect; - matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); - } - - void setPerspective(float fov, const glm::vec2& size, float znear = 0.1f, float zfar = 512.0f) { - setPerspective(fov, size.x / size.y, znear, zfar); - } - - void setPerspective(float fov, float aspect, float znear = 0.1f, float zfar = 512.0f) { - this->aspect = aspect; - this->fov = fov; - this->znear = znear; - this->zfar = zfar; - matrices.perspective = glm::perspective(glm::radians(fov), aspect, znear, zfar); - }; - - void rotate(const float delta) { - yaw += delta; - updateViewMatrix(); - } - - void setPosition(const glm::vec3& position) { - this->position = position; - updateViewMatrix(); - } - - // Translate in the Z axis of the camera - void dolly(float delta) { - auto direction = glm::vec3(0, 0, delta); - translate(direction); - } - - // Translate in the XY plane of the camera - void translate(const glm::vec2& delta) { - auto move = glm::vec3(delta.x, delta.y, 0); - translate(move); - } - - void translate(const glm::vec3& delta) { - position += getOrientation() * delta; - updateViewMatrix(); - } - - void update(float deltaTime) { - if (moving()) { - glm::vec3 camFront = getOrientation() * Vectors::FRONT; - glm::vec3 camRight = getOrientation() * Vectors::RIGHT; - glm::vec3 camUp = getOrientation() * Vectors::UP; - float moveSpeed = deltaTime * movementSpeed; - - if (keys[FORWARD]) { - position += camFront * moveSpeed; - } - if (keys[BACK]) { - position -= camFront * moveSpeed; - } - if (keys[LEFT]) { - position -= camRight * moveSpeed; - } - if (keys[RIGHT]) { - position += camRight * moveSpeed; - } - if (keys[UP]) { - position += camUp * moveSpeed; - } - if (keys[DOWN]) { - position -= camUp * moveSpeed; - } - updateViewMatrix(); - } - } -}; class QWindowCamera : public Camera { Key forKey(int key) { @@ -260,6 +143,8 @@ public: } }; + + // Create a simple OpenGL window that renders text in various ways class QTestWindow : public QWindow, public AbstractViewStateInterface { Q_OBJECT @@ -297,7 +182,10 @@ protected: return _renderEngine; } - void pushPostUpdateLambda(void* key, std::function func) override {} + std::map> _postUpdateLambdas; + void pushPostUpdateLambda(void* key, std::function func) override { + _postUpdateLambdas[key] = func; + } public: //"/-17.2049,-8.08629,-19.4153/0,0.881994,0,-0.47126" @@ -338,28 +226,39 @@ public: _context.setFormat(format); _context.create(); - + resize(QSize(800, 600)); show(); makeCurrent(); + glewExperimental = true; glewInit(); glGetError(); + setupDebugLogger(this); #ifdef Q_OS_WIN wglSwapIntervalEXT(0); #endif - _camera.movementSpeed = 3.0f; + { + makeCurrent(); + _quadProgram = loadDefaultShader(); + _plane = loadPlane(_quadProgram); + _textOverlay = new TextOverlay(glm::uvec2(800, 600)); + glViewport(0, 0, 800, 600); + } + + _camera.movementSpeed = 50.0f; - setupDebugLogger(this); - qDebug() << (const char*)glGetString(GL_VERSION); // GPU library init { + _offscreenContext = new OffscreenGLCanvas(); + _offscreenContext->create(_context.getContext()); + _offscreenContext->makeCurrent(); gpu::Context::init(); _gpuContext = std::make_shared(); } // Render engine library init { - makeCurrent(); + _offscreenContext->makeCurrent(); DependencyManager::get()->init(); _renderEngine->addJob("RenderShadowTask", _cullFunctor); _renderEngine->addJob("RenderDeferredTask", _cullFunctor); @@ -367,27 +266,23 @@ public: _renderEngine->registerScene(_main3DScene); } - QVariant lastScene = _settings.value(LAST_SCENE_KEY); - if (lastScene.isValid()) { - auto result = QMessageBox::question(nullptr, "Question", "Load last scene " + lastScene.toString()); - if (result != QMessageBox::No) { - importScene(lastScene.toString()); - } - } + reloadScene(); + restorePosition(); - resize(QSize(800, 600)); _elapsed.start(); - QTimer* timer = new QTimer(this); timer->setInterval(0); connect(timer, &QTimer::timeout, this, [this] { draw(); }); timer->start(); + _ready = true; } virtual ~QTestWindow() { ResourceManager::cleanup(); + try { _quadProgram.reset(); } catch (std::runtime_error&) {} + try { _plane.reset(); } catch (std::runtime_error&) {} } protected: @@ -398,9 +293,30 @@ protected: return; case Qt::Key_F2: + reloadScene(); + return; + + case Qt::Key_F5: goTo(); return; + case Qt::Key_F6: + savePosition(); + return; + + case Qt::Key_F7: + restorePosition(); + return; + + case Qt::Key_F8: + resetPosition(); + return; + + case Qt::Key_F9: + toggleCulling(); + return; + + default: break; } @@ -421,39 +337,22 @@ protected: private: - static bool cull(const RenderArgs* renderArgs, const AABox& box) { - return true; - } - - void update() { - auto now = usecTimestampNow(); - static auto last = now; - - float delta = now - last; - // Update the camera - _camera.update(delta / USECS_PER_SECOND); - - - // load the view frustum - { - _viewFrustum.setProjection(_camera.matrices.perspective); - auto view = glm::inverse(_camera.matrices.view); - _viewFrustum.setPosition(glm::vec3(view[3])); - _viewFrustum.setOrientation(glm::quat_cast(view)); - } - last = now; + static bool cull(const RenderArgs* args, const AABox& bounds) { + float renderAccuracy = calculateRenderAccuracy(args->getViewFrustum().getPosition(), bounds, args->_sizeScale, args->_boundaryLevelAdjust); + return (renderAccuracy > 0.0f); } void draw() { + if (!_ready) { + return; + } if (!isVisible()) { return; } update(); - makeCurrent(); -#define RENDER_SCENE 1 + _offscreenContext->makeCurrent(); -#if RENDER_SCENE RenderArgs renderArgs(_gpuContext, _octree.data(), DEFAULT_OCTREE_SIZE_SCALE, 0, RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); @@ -473,40 +372,114 @@ private: } render(&renderArgs); + GLuint glTex; + { + auto gpuTex = renderArgs._blitFramebuffer->getRenderBuffer(0); + glTex = gpu::Backend::getGPUObject(*gpuTex)->_id; + } + + makeCurrent(); + { + glBindTexture(GL_TEXTURE_2D, glTex); + _quadProgram->Use(); + _plane->Use(); + _plane->Draw(); + glBindVertexArray(0); + } { - gpu::gl::GLFramebuffer* framebuffer = gpu::Backend::getGPUObject(*renderArgs._blitFramebuffer); - auto fbo = framebuffer->_id; - glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - const auto& vp = renderArgs._viewport; - glBlitFramebuffer(vp.x, vp.y, vp.z, vp.w, vp.x, vp.y, vp.z, vp.w, GL_COLOR_BUFFER_BIT, GL_NEAREST); + _textOverlay->render(); } -#else - glClearColor(0.0f, 0.5f, 0.8f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); -#endif _context.swapBuffers(this); -#if RENDER_SCENE + _offscreenContext->makeCurrent(); framebufferCache->releaseFramebuffer(renderArgs._blitFramebuffer); renderArgs._blitFramebuffer.reset(); gpu::doInBatch(renderArgs._context, [&](gpu::Batch& batch) { batch.resetStages(); }); -#endif - fps.increment(); + _fpsCounter.increment(); static size_t _frameCount { 0 }; ++_frameCount; - if (_elapsed.elapsed() >= 4000) { - qDebug() << "FPS " << fps.rate(); + if (_elapsed.elapsed() >= 500) { + _fps = _fpsCounter.rate(); + updateText(); _frameCount = 0; _elapsed.restart(); } + } - if (0 == ++_frameCount % 100) { +private: + class EntityUpdateOperator : public RecurseOctreeOperator { + public: + EntityUpdateOperator(const qint64& now) : now(now) {} + bool preRecursion(OctreeElementPointer element) override { return true; } + bool postRecursion(OctreeElementPointer element) override { + EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); + entityTreeElement->forEachEntity([&](EntityItemPointer entityItem) { + if (!entityItem->isParentIDValid()) { + return; // we weren't able to resolve a parent from _parentID, so don't save this entity. + } + entityItem->update(now); + }); + return true; } + + const qint64& now; + }; + + void updateText() { + //qDebug() << "FPS " << fps.rate(); + { + _textBlocks.erase(TextBlock::Info); + auto& infoTextBlock = _textBlocks[TextBlock::Info]; + infoTextBlock.push_back({ vec2(98, 10), "FPS: ", TextOverlay::alignRight }); + infoTextBlock.push_back({ vec2(100, 10), std::to_string((uint32_t)_fps), TextOverlay::alignLeft }); + infoTextBlock.push_back({ vec2(98, 30), "Culling: ", TextOverlay::alignRight }); + infoTextBlock.push_back({ vec2(100, 30), _cullingEnabled ? "Enabled" : "Disabled", TextOverlay::alignLeft }); + } + + _textOverlay->beginTextUpdate(); + for (const auto& e : _textBlocks) { + for (const auto& b : e.second) { + _textOverlay->addText(b.text, b.position, b.alignment); + } + } + _textOverlay->endTextUpdate(); + } + + void update() { + auto now = usecTimestampNow(); + static auto last = now; + + float delta = now - last; + // Update the camera + _camera.update(delta / USECS_PER_SECOND); + { + _viewFrustum = ViewFrustum(); + _viewFrustum.setProjection(_camera.matrices.perspective); + auto view = glm::inverse(_camera.matrices.view); + _viewFrustum.setPosition(glm::vec3(view[3])); + _viewFrustum.setOrientation(glm::quat_cast(view)); + // Failing to do the calculation of the bound planes causes everything to be considered inside the frustum + if (_cullingEnabled) { + _viewFrustum.calculate(); + } + } + + getEntities()->setViewFrustum(_viewFrustum); + EntityUpdateOperator updateOperator(now); + //getEntities()->getTree()->recurseTreeWithOperator(&updateOperator); + { + PROFILE_RANGE_EX("PreRenderLambdas", 0xffff0000, (uint64_t)0); + for (auto& iter : _postUpdateLambdas) { + iter.second(); + } + _postUpdateLambdas.clear(); + } + + last = now; } void render(RenderArgs* renderArgs) { @@ -531,13 +504,21 @@ private: } } - void makeCurrent() { - _context.makeCurrent(this); + bool makeCurrent() { + bool currentResult = _context.makeCurrent(this); + Q_ASSERT(currentResult); + return currentResult; } void resizeWindow(const QSize& size) { _size = size; _camera.setAspectRatio((float)_size.width() / (float)_size.height()); + if (!_ready) { + return; + } + _textOverlay->resize(toGlm(_size)); + makeCurrent(); + glViewport(0, 0, size.width(), size.height()); } void parsePath(const QString& viewpointString) { @@ -563,20 +544,24 @@ private: // we may also have an orientation if (viewpointString[positionRegex.matchedLength() - 1] == QChar('/') && orientationRegex.indexIn(viewpointString, positionRegex.matchedLength() - 1) != -1) { - //glm::vec4 v = glm::vec4( - // orientationRegex.cap(1).toFloat(), - // orientationRegex.cap(2).toFloat(), - // orientationRegex.cap(3).toFloat(), - // orientationRegex.cap(4).toFloat()); - //if (!glm::any(glm::isnan(v))) { - // _camera.setRotation(glm::normalize(glm::quat(v.w, v.x, v.y, v.z))); - //} + + glm::vec4 v = glm::vec4( + orientationRegex.cap(1).toFloat(), + orientationRegex.cap(2).toFloat(), + orientationRegex.cap(3).toFloat(), + orientationRegex.cap(4).toFloat()); + if (!glm::any(glm::isnan(v))) { + _camera.setRotation(glm::normalize(glm::quat(v.w, v.x, v.y, v.z))); + } } } } } void importScene(const QString& fileName) { + auto assetClient = DependencyManager::get(); + QFileInfo fileInfo(fileName); + //assetClient->loadLocalMappings(fileInfo.absolutePath() + "/" + fileInfo.baseName() + ".atp"); _settings.setValue(LAST_SCENE_KEY, fileName); _octree->clear(); _octree->getTree()->readFromURL(fileName); @@ -598,26 +583,89 @@ private: parsePath(destination); } + void reloadScene() { + QVariant lastScene = _settings.value(LAST_SCENE_KEY); + if (lastScene.isValid()) { + importScene(lastScene.toString()); + } + } + + void savePosition() { + // /-17.2049,-8.08629,-19.4153/0,-0.48551,0,0.874231 + glm::quat q = _camera.getOrientation(); + glm::vec3 v = _camera.position; + QString viewpoint = QString("/%1,%2,%3/%4,%5,%6,%7"). + arg(v.x).arg(v.y).arg(v.z). + arg(q.x).arg(q.y).arg(q.z).arg(q.w); + _settings.setValue(LAST_LOCATION_KEY, viewpoint); + } + + void restorePosition() { + // /-17.2049,-8.08629,-19.4153/0,-0.48551,0,0.874231 + QVariant viewpoint = _settings.value(LAST_LOCATION_KEY); + if (viewpoint.isValid()) { + parsePath(viewpoint.toString()); + } + } + + void resetPosition() { + _camera.yaw = 0; + _camera.setPosition(vec3()); + } + + void toggleCulling() { + _cullingEnabled = !_cullingEnabled; + } + + QSharedPointer getEntities() { + return _octree; + } + private: - render::CullFunctor _cullFunctor { cull }; + render::CullFunctor _cullFunctor { [&](const RenderArgs* args, const AABox& bounds)->bool{ + if (_cullingEnabled) { + return cull(args, bounds); + } else { + return true; + } + } }; + + struct TextElement { + const glm::vec2 position; + const std::string text; + TextOverlay::TextAlign alignment; + }; + + enum TextBlock { + Help, + Info, + }; + + std::map> _textBlocks; + gpu::ContextPointer _gpuContext; // initialized during window creation render::EnginePointer _renderEngine { new render::Engine() }; render::ScenePointer _main3DScene { new render::Scene(glm::vec3(-0.5f * (float)TREE_SCALE), (float)TREE_SCALE) }; + OffscreenGLCanvas* _offscreenContext { nullptr }; QOpenGLContextWrapper _context; QSize _size; - RateCounter<> fps; + RateCounter<200> _fpsCounter; QSettings _settings; + ProgramPtr _quadProgram; + ShapeWrapperPtr _plane; + QWindowCamera _camera; ViewFrustum _viewFrustum; // current state of view frustum, perspective, orientation, etc. ViewFrustum _shadowViewFrustum; // current state of view frustum, perspective, orientation, etc. model::SunSkyStage _sunSkyStage; model::LightPointer _globalLight { std::make_shared() }; QElapsedTimer _elapsed; + bool _ready { false }; + float _fps { 0 }; + TextOverlay* _textOverlay; + bool _cullingEnabled { true }; QSharedPointer _octree; - QSharedPointer getEntities() { - return _octree; - } }; void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { diff --git a/tests/render-perf/src/stb_font_consolas_24_latin1.inl b/tests/render-perf/src/stb_font_consolas_24_latin1.inl new file mode 100644 index 0000000000..12eedd2f43 --- /dev/null +++ b/tests/render-perf/src/stb_font_consolas_24_latin1.inl @@ -0,0 +1,734 @@ +// Font generated by stb_font_inl_generator.c (4/1 bpp) +// +// Following instructions show how to use the only included font, whatever it is, in +// a generic way so you can replace it with any other font by changing the include. +// To use multiple fonts, replace STB_SOMEFONT_* below with STB_FONT_consolas_24_latin1_*, +// and separately install each font. Note that the CREATE function call has a +// totally different name; it's just 'stb_font_consolas_24_latin1'. +// +/* // Example usage: + +static stb_fontchar fontdata[STB_SOMEFONT_NUM_CHARS]; + +static void init(void) +{ + // optionally replace both STB_SOMEFONT_BITMAP_HEIGHT with STB_SOMEFONT_BITMAP_HEIGHT_POW2 + static unsigned char fontpixels[STB_SOMEFONT_BITMAP_HEIGHT][STB_SOMEFONT_BITMAP_WIDTH]; + STB_SOMEFONT_CREATE(fontdata, fontpixels, STB_SOMEFONT_BITMAP_HEIGHT); + ... create texture ... + // for best results rendering 1:1 pixels texels, use nearest-neighbor sampling + // if allowed to scale up, use bilerp +} + +// This function positions characters on integer coordinates, and assumes 1:1 texels to pixels +// Appropriate if nearest-neighbor sampling is used +static void draw_string_integer(int x, int y, char *str) // draw with top-left point x,y +{ + ... use texture ... + ... turn on alpha blending and gamma-correct alpha blending ... + glBegin(GL_QUADS); + while (*str) { + int char_codepoint = *str++; + stb_fontchar *cd = &fontdata[char_codepoint - STB_SOMEFONT_FIRST_CHAR]; + glTexCoord2f(cd->s0, cd->t0); glVertex2i(x + cd->x0, y + cd->y0); + glTexCoord2f(cd->s1, cd->t0); glVertex2i(x + cd->x1, y + cd->y0); + glTexCoord2f(cd->s1, cd->t1); glVertex2i(x + cd->x1, y + cd->y1); + glTexCoord2f(cd->s0, cd->t1); glVertex2i(x + cd->x0, y + cd->y1); + // if bilerping, in D3D9 you'll need a half-pixel offset here for 1:1 to behave correct + x += cd->advance_int; + } + glEnd(); +} + +// This function positions characters on float coordinates, and doesn't require 1:1 texels to pixels +// Appropriate if bilinear filtering is used +static void draw_string_float(float x, float y, char *str) // draw with top-left point x,y +{ + ... use texture ... + ... turn on alpha blending and gamma-correct alpha blending ... + glBegin(GL_QUADS); + while (*str) { + int char_codepoint = *str++; + stb_fontchar *cd = &fontdata[char_codepoint - STB_SOMEFONT_FIRST_CHAR]; + glTexCoord2f(cd->s0f, cd->t0f); glVertex2f(x + cd->x0f, y + cd->y0f); + glTexCoord2f(cd->s1f, cd->t0f); glVertex2f(x + cd->x1f, y + cd->y0f); + glTexCoord2f(cd->s1f, cd->t1f); glVertex2f(x + cd->x1f, y + cd->y1f); + glTexCoord2f(cd->s0f, cd->t1f); glVertex2f(x + cd->x0f, y + cd->y1f); + // if bilerping, in D3D9 you'll need a half-pixel offset here for 1:1 to behave correct + x += cd->advance; + } + glEnd(); +} +*/ + +#pragma once + +#ifndef STB_FONTCHAR__TYPEDEF +#define STB_FONTCHAR__TYPEDEF +typedef struct +{ + // coordinates if using integer positioning + float s0,t0,s1,t1; + signed short x0,y0,x1,y1; + int advance_int; + // coordinates if using floating positioning + float s0f,t0f,s1f,t1f; + float x0f,y0f,x1f,y1f; + float advance; +} stb_fontchar; +#endif + +#define STB_FONT_consolas_24_latin1_BITMAP_WIDTH 256 +#define STB_FONT_consolas_24_latin1_BITMAP_HEIGHT 170 +#define STB_FONT_consolas_24_latin1_BITMAP_HEIGHT_POW2 256 + +#define STB_FONT_consolas_24_latin1_FIRST_CHAR 32 +#define STB_FONT_consolas_24_latin1_NUM_CHARS 224 + +#define STB_FONT_consolas_24_latin1_LINE_SPACING 16 + +static unsigned int stb__consolas_24_latin1_pixels[]={ + 0x08262131,0xff904400,0x3ffe1fff,0x3b2206ff,0x2007913f,0x0000defa, + 0x64c00f32,0x0de5c402,0x00a614c0,0x4002ae62,0x98014c19,0x01aa881c, + 0x00d54400,0xb880154c,0x8330020b,0x1e980029,0xaa7d5dd4,0x2001d94f, + 0x3332a6e8,0x999ff0ff,0x37ffa207,0x2600df12,0x8000fffd,0x3fa005fd, + 0xfdff700f,0x8ffc409f,0x3ea02ff8,0x200dffff,0x0bfe0ff8,0x7d407ee0, + 0xfd10001f,0x9fff5007,0xcffff880,0x1ff104eb,0x320017fc,0x7d77e40f, + 0x17ee9f54,0xfd027ec0,0x7dc01fe1,0x0037c40d,0xb0017f22,0xffe8007f, + 0x7dc3fd80,0x741ff104,0x59ff701f,0x7401dfd7,0x2003f60e,0x3fa200fc, + 0x0ff44001,0x7dc6fdc0,0x32236604,0x3ba00eff,0x31003f60,0xdf90dd57, + 0xd93ea9f5,0x037e403f,0x803fc3fa,0x06f882fd,0x2006f980,0xa8800098, + 0xf903fb81,0x88040401,0x1ff441ff,0x0fb00000,0x00000000,0x00020000, + 0xffffa800,0xfadfb86f,0x7fc49f54,0x803ff301,0x200ff0fe,0x06f880fe, + 0x0000ff00,0x05f88000,0x40000fe6,0x0ff504fc,0x81540153,0x100affb9, + 0x22001573,0x31000ab9,0x26200157,0x731000ab,0xcffb8015,0x7dc4ffda, + 0x89f54fad,0x3fe80ff9,0x0ff0fe80,0x3e203fa0,0x807ddb36,0x01dd107f, + 0xdddb076c,0x1fb8bddd,0x1dd12fc0,0x3ff076c0,0x3f60ffc0,0xe98ff102, + 0xa84fffff,0x00dfffff,0x37ffffea,0x3fffea00,0x3fea00df,0x2a00dfff, + 0x80dfffff,0x3bb61ff8,0x3eb3ea3f,0x7f909f54,0xfd005fa8,0x7f401fe1, + 0xffaef880,0x1fe05ffe,0xdf504fd8,0xfffffff8,0x9db33746,0x09fb1b6b, + 0x80ff1bea,0x817ec2fd,0x33fe67f8,0x7dc3acfa,0x0efebacf,0xebacffb8, + 0xcffb80ef,0xb80efeba,0xefebacff,0xbacffb80,0x27e40efe,0x20df59f1, + 0x509f54fa,0x003fd8bf,0x401fe1fd,0x9fff107e,0x7407fe61,0x20ff500f, + 0x6f9803fd,0x777ccfe2,0x7fa8db6f,0x37cc7fb0,0x5fb0ff60,0x3fe9fe20, + 0x3ff105f3,0x7c43fe88,0x21ff441f,0x7f441ff8,0x220ffc43,0x0ffc43fe, + 0x3ff0ffa2,0x267f97d4,0x9f54fadf,0x1ff8ff10,0x1fe1fd00,0x7c417e20, + 0x704fb84f,0x217f405f,0xf9800ff8,0x47f47ea6,0xfd0f95f8,0x260ff885, + 0x21fe406f,0x2ff102fd,0x207ea6f9,0x8ff504fc,0x8ff504fc,0x8ff504fc, + 0x8ff504fc,0x8ff504fc,0x3fd3e47f,0x553eafea,0x261ff04f,0x1fd000ff, + 0xefcc81fe,0x260ff101,0x9bfb105f,0x7d45fb81,0x64df3005,0x9f34f98f, + 0x517ee1f2,0x407f98bf,0x817ec2fd,0x5cbf57f8,0x201ff80f,0x807fe1ff, + 0x807fe1ff,0x807fe1ff,0x807fe1ff,0xf8df31ff,0x47e4bf65,0x203514fa, + 0x00df52fd,0x107f87f4,0x7c403fff,0x440df306,0x7fc41ffe,0x4c00bf60, + 0x97ddf66f,0xf10db2fa,0x2217ec1f,0x20ff407f,0x2ff102fd,0x7c1f64fb, + 0xff17ec07,0x1fe2fd80,0x03fc5fb0,0x407f8bf6,0x4cdf32fd,0x9d4ff22f, + 0x9f7004fa,0xfe8013ee,0x6cc40ff0,0x20df103f,0x3bf506f9,0x7c4ff601, + 0xd9be6007,0x47f23f96,0x227fb06d,0x203ff07f,0x17ec0ff8,0x4df57f88, + 0x406f986e,0x80df33fd,0x80df33fd,0x80df33fd,0x80df33fd,0x64ff13fd, + 0x2a0bf60f,0x29f9004f,0x3fa005fa,0x1be00ff0,0x5fa837c4,0xfa801f90, + 0x4c009f76,0x87edba6f,0x2a09f0fd,0x3209f76f,0xb0bf704f,0x4dfe205f, + 0xf30bf0ff,0xf33fc80d,0xf33fc80d,0xf33fc80d,0xf33fc80d,0x3e3fc80d, + 0x03fd1ba7,0x3f6009f5,0x7400ff13,0x3a00ff0f,0x906f880f,0x401fa07f, + 0x001fe9ff,0xf96e9be6,0x017cdfe3,0x203fd3ff,0x7fd43ff9,0xf102fd81, + 0x27d7fecf,0xfb01fe63,0xfb01fe65,0xfb01fe65,0xfb01fe65,0xfb01fe65, + 0x1fc4ff45,0x9f501ff1,0xfe8bfe00,0x3e1fd001,0x101fd007,0x40df50df, + 0xfbf9007e,0x26f9800d,0xfdb9f56d,0x7e401fb7,0x7fdc06fd,0xb02ffede, + 0x89fe205f,0x4feefffc,0x1fe80ff1,0x1fe80ff1,0x1fe80ff1,0x1fe80ff1, + 0x1fe80ff1,0xb87ef7f2,0x2a9f505f,0x647f984f,0x21fd002f,0x01fd007f, + 0xfb99dff1,0x803f403f,0x4003fff8,0x6c5f26f9,0x01ffe8ef,0x401fffc4, + 0x01dfffda,0x2fd40ff2,0x0e77ff4c,0x3fe203ff,0x7c407fe0,0x4407fe0f, + 0x407fe0ff,0x07fe0ff8,0xff107fc4,0x807fd4fd,0x509f54fa,0x004fa8bf, + 0x401fe1fd,0xfff880fe,0x7400deff,0x01ff6007,0x0fb9be60,0x7ec00302, + 0x027dc007,0x4fc827dc,0x3f203f70,0xfc8bf704,0xfc8bf704,0xfc8bf704, + 0xfc8bf704,0xf30bf704,0x07ffd9df,0x84faa7d4,0x07fc41fe,0x07f87f40, + 0xdf101fd0,0x07f80022,0x2000ff60,0x00fe65fa,0x001fec00,0x98103fe6, + 0x0ffcc1ff,0xff100fc8,0x440ffa87,0x07fd43ff,0xff50ffe2,0x543ff881, + 0x1ffc40ff,0x7dc03fea,0x5401dfff,0xfc89f54f,0xd00df705,0x6c01fe1f, + 0x006f883f,0xf5006f98,0x9fb0001f,0xa80006e8,0xfd8000ff,0xfc86fcdf, + 0x04ffecdf,0xdff701f6,0x2e07ffd9,0x3ffeceff,0xfd9dff70,0x3bfee07f, + 0xf703ffec,0x07ffd9df,0x5000cfec,0xfa93ea9f,0x013f600f,0x401fe1fd, + 0x37c41efb,0x013f6600,0x33007fea,0x2e07fdc4,0xf500505f,0x7e40003f, + 0xfd703fff,0x6e805dff,0xdfffea80,0x7fff5401,0x7ff5401d,0x7f5401df, + 0x75401dff,0x7c01dfff,0x54fa8005,0x00bfe29f,0x775c3fd1,0xddff0ffe, + 0x7fff409d,0x2600df12,0xf300effe,0xf7007fff,0x207fffdf,0x7fdcdffb, + 0x07ffff30,0x80022000,0x0017e001,0x00060003,0x0018000c,0x07220030, + 0x7d53ea00,0x26007fb4,0x3bbbae6f,0x9ddddb0e,0xd12ecb80,0x0f3a600b, + 0x0019bd30,0x073bbb22,0x17bdb730,0x00337a60,0x00000000,0x00000000, + 0x00000000,0x00000000,0x4d3ea9f5,0x00154004,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x20000000,0x009f54fa, + 0x77777400,0xeeeeeeee,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x4c000000,0x0007d33e,0x77777740,0x0eeeeeee, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x54400000,0x2a20001a,0x0154c01a,0x54400b20, + 0xaaa98000,0x99953100,0x032e0059,0x10053066,0x22004177,0x04c801aa, + 0x01aa8800,0x500d5440,0x51017997,0x51000035,0x0bb88035,0x00d54402, + 0x0007fea0,0xf500ffa2,0x3e6009ff,0x3fef9803,0xfffff700,0xffffb89f, + 0xf8804fff,0x3e0ff886,0x3ffe202f,0x5404ebcf,0x0fec01ff,0x07fd1000, + 0x103fe880,0x05fffffd,0x80007fea,0x7c403fe8,0x04ebcfff,0x88003ff5, + 0x3a2001fe,0x46fdc01f,0x7dc404fb,0x6baec00b,0xabcffd80,0x3fff24ec, + 0x804fb9df,0x41dd03fb,0x236600fd,0x800effc8,0x3e601fe8,0x3fd10005, + 0x01fe8800,0x008833f6,0x20007fa2,0xd9801fe8,0x00effc88,0x00007fa2, + 0x00000000,0x9fffffd5,0x036cf640,0x98407bee,0xf54fffff,0x001fd009, + 0x00020000,0x0007f400,0x44000000,0x0000007f,0x00200000,0x40153000, + 0xa802a62a,0x2a802a62,0x33fb7ff2,0x3bfa204d,0x007fe201,0x53fffff2, + 0x27d404fa,0x40015540,0x553002aa,0x50555555,0x01aa807f,0x554c1544, + 0xf82aaaaa,0x2aa8002f,0x802aa800,0x50aa02a9,0x55555555,0xf102fd81, + 0xf8817ecf,0x7c40bf67,0x1f61ff37,0x5c02aa80,0xfff9005f,0x013ea9ff, + 0xff8803fb,0x3fe2002f,0xfffc802f,0xdf07ffff,0x6406fb80,0xffffc85f, + 0xffb87fff,0x3ffe2003,0x3ffe2002,0x41ffe402,0x7fffc6f8,0x6c0fffff, + 0x6cff102f,0x6cff102f,0x2eff102f,0x4401ba5f,0x3f202fff,0xffff7003, + 0x8813ea9f,0xfffa805f,0x3ffea004,0xaacfc804,0x5f902aaa,0xf103ff80, + 0x559f901f,0xff985555,0xfa802eff,0x3ea004ff,0x7fe404ff,0x5546f886, + 0x0aaadfda,0x7f8817ec,0x3fc40bf6,0x5fe205fb,0x4017e7fa,0x3604fffa, + 0xfff3002f,0x813ea9ff,0xafd802fc,0x2bf6007f,0x03fc807f,0x2a02fc40, + 0x04fd80ff,0x3fa007f9,0x404ffd89,0x2007fafd,0x6407fafd,0x37c42fef, + 0xfd809f70,0x7ecff102,0x7ecff102,0x3e2ff102,0xb004faef,0x7f40ff5f, + 0x3fff2002,0x7c09f54f,0xfd7f8807,0x3aff1003,0x01fe401f,0x3a00fec0, + 0x807fcc4f,0x5f9803fc,0xf8827fd4,0xf1003fd7,0xfc807faf,0x037c46fb, + 0x2fd809f7,0x17ecff10,0x0bf67f88,0xffd33fc4,0x7f88019f,0x0bfa03fd, + 0x29ffd500,0x07f504fa,0x13ee9f50,0x27dd3ea0,0x2000ff20,0x7fcc04fa, + 0xfc80ff60,0x886f9803,0x74fa81ff,0x29f5009f,0x2bf204fb,0x81be22fd, + 0x17ec04fb,0x0bf67f88,0x05fb3fc4,0x3fae1fe2,0x53ea03ff,0x07fb04fb, + 0x4fa84c00,0xfb001fd0,0x3601fe65,0xc80ff32f,0x1fd0003f,0xdf34fd80, + 0xf003fc80,0xb07f905f,0x201fe65f,0x40ff32fd,0x226faafc,0x013ee06f, + 0x9fe205fb,0x4ff102fd,0x0ff102fd,0x417ff7ee,0x20ff32fd,0x500006fb, + 0x00bf309f,0x01fe8ff1,0x03fd1fe2,0x777777e4,0x01fdc04e,0x0bf63fe2, + 0xdddddf90,0x43ffa89d,0x23fc43fb,0x1fe201fe,0x4bf203fd,0x40df11fe, + 0x17ec04fb,0x0bf67f88,0x05fb3fc4,0x9be41fe2,0x23fc43ff,0x2ff881fe, + 0x84fa8000,0x5fa801fc,0xbf5027e4,0x3f204fc8,0x06ffffff,0x7e4037c4, + 0xffc806fe,0xa86fffff,0x0ff89eff,0x13f22fd4,0x27e45fa8,0x2bf52fc8, + 0x13ee06f8,0x3e205fb0,0x7c40bf67,0x7c40bf67,0x2fdcdb07,0x09f917ea, + 0x0620bff2,0x3e227d40,0x985fb006,0x30bf607f,0x01fe40ff,0x8803f900, + 0xfc801fff,0x7fec4003,0x42fd82ff,0x0bf607f9,0x8bf20ff3,0x206f89ff, + 0x17ec04fb,0x0bf67f88,0x05fb3fc4,0xb2f41fe2,0x4c2fd89f,0x3fff607f, + 0x5004fedd,0x802fb89f,0x99999ff8,0x9ff881ff,0x01ff9999,0x4c0007f9, + 0x05fb805f,0x20007f90,0xff886fe9,0x1ff99999,0x9999ff88,0x2fc81ff9, + 0x81be33ee,0x1fe404fb,0x0ff25fa8,0x07f92fd4,0x9f04d7ea,0x7c43ff71, + 0xff99999f,0x3fffaa01,0x7f9002ce,0xff5007e8,0x9fffffff,0xffffff50, + 0x3f209fff,0x07f40003,0x64013ee0,0x3a20003f,0x7fffd42f,0xa84fffff, + 0xffffffff,0x222fc84f,0x2e06f9ff,0x827dc04f,0x413ee4fc,0x413ee4fc, + 0xfdffb4fc,0xfa87ffff,0xffffffff,0x0077c404,0x9f517f40,0x3337f600, + 0xd86fdccc,0xdcccccdf,0x007f906f,0x5c04fa80,0x07f9004f,0x6c2fe800, + 0xdcccccdf,0xccdfd86f,0xc86fdccc,0x37e7e42f,0xf9809f70,0x30ffcc1f, + 0x1ff983ff,0xff307fe6,0x3fffb6a3,0xcdfd80ce,0x06fdcccc,0x37601fd4, + 0x6c6fd989,0x0ff8800f,0xff887fe0,0x3207fe00,0x7f80003f,0xc8027dc0, + 0x4c15003f,0x3fe20ffb,0xf887fe00,0x907fe00f,0x6fff885f,0x32013ee0, + 0x4ffecdff,0xfecdffc8,0xcdffc84f,0x3f704ffe,0xf007fc40,0x00fe403f, + 0xdfffffb1,0xa802fcc3,0x527ec05f,0x84fd80bf,0xeeeeeefc,0x80bee006, + 0xdf9004fb,0xf8dddddd,0x41ffffff,0x27ec05fa,0x4fd80bf5,0x7fe417e4, + 0x7ff776c6,0xfd700eee,0x75c05dff,0x2e02efff,0x202efffe,0x17ea00fc, + 0x14c09fb0,0x82cdba80,0x7fb001ca,0x3f66fa80,0x6437d403,0x7fffffff, + 0xb80f2200,0xfff9004f,0xcb8fffff,0x7fb02cdd,0x3f66fa80,0x3237d403, + 0x46ff882f,0xffffffff,0x8001800f,0x90018001,0x403fd80b,0x000006fa, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x0d544000,0x2600aa60,0x54c014c1,0x14c19802,0x400154c0, + 0xcb9802c9,0x0332e02c,0x30a60f22,0x00332a05,0x4000b260,0x0cca83cc, + 0x01e64000,0x2e200b26,0x6644020b,0x00cca801,0xe8803ca8,0x7ffd403f, + 0xf83fe204,0x3ffea02f,0xf83fe204,0x3ffea02f,0x3f7ee004,0x3ffff604, + 0x7f7ec0ef,0x220fe40f,0x05ff11ff,0xa8003bee,0x6c003fff,0x03bee05f, + 0x202fec00,0x2203fffa,0x4eacffff,0x9d95ff70,0x9003bee0,0x1fe880bf, + 0x7dc6fdc0,0xfd83ba04,0xf71bf700,0xfb077409,0x2e37ee01,0x2a7d004f, + 0x261bf907,0x54fea4fd,0x2213ea3f,0x807fa0ff,0xfa800ef9,0xb003fc8d, + 0x1df3007f,0x403fd800,0x03fc8dfa,0xdffb31d3,0xfffdb881,0x3be603ef, + 0x0017f200,0x00000000,0x00000000,0x7b8dd800,0x6f981ff0,0x8a7c47ee, + 0x020200ee,0x22002620,0x4c400cc1,0x00262000,0x18800988,0x011000cc, + 0x0ffb7fe2,0xf9004c40,0x5555550b,0xaa981555,0x982aaaaa,0x2aaaaaaa, + 0xaaaaaaa8,0x555540aa,0x200aaaaa,0x7cc002aa,0x86f882ff,0x44bee5fa, + 0x0005f94f,0x00000000,0x00000000,0x00000000,0x001ff982,0xff0bf900, + 0x1fffffff,0xffffffc8,0xffffc87f,0xfff87fff,0x40ffffff,0xffffffff, + 0x5fff100f,0x26013000,0x30ffd45f,0xf33fb3bf,0x9dfb7009,0xcefdb801, + 0x677edc00,0x677edc00,0xdfdb7100,0x3b6e203b,0xb7101def,0x2203bdfd, + 0x01defedb,0x01bffb2a,0x7039dfb7,0xfb5550bf,0xfc81555b,0x82aaaaac, + 0xaaaaacfc,0xdfdaaa82,0x55540aaa,0x00aaadfd,0x1009fff5,0x83bdfdb7, + 0x07bee5f9,0x7f4fffee,0x76f7ec00,0xbdfb02ff,0x3f605ffd,0xb02ffede, + 0x05ffdbdf,0xfffddff7,0xfddff705,0xdff705ff,0xf705fffd,0x05fffddf, + 0x5ffddffb,0xffdffd30,0x404fb87f,0x1fe404fb,0x0007f900,0xfb8013ee, + 0xff5fb004,0xfddff700,0x8afcc5ff,0x426200ff,0x27e402fc,0x9f903fea, + 0x7e40ffa8,0x3207fd44,0x207fd44f,0x43fdc419,0x43fdc419,0x43fdc419, + 0x23fdc419,0x1bea0dfc,0x3ff513fa,0x3ee027dc,0x001fe404,0x2e0007f9, + 0x13ee004f,0x0ff5fe20,0xff710660,0x07f8afcc,0xf1017e60,0xf88ff20d, + 0x7c47f906,0x7c47f906,0x4007f906,0x3fe000ff,0x003fe000,0x0df30ff8, + 0x05fa87fa,0x027dcbf9,0x7f9013ee,0x001fe400,0x2e004fb8,0x74fa804f, + 0x7fc0009f,0x27fcbf30,0x5400fe80,0x549f504f,0x549f504f,0x549f504f, + 0x009f504f,0xfb000fec,0x00fec003,0x0fee3fb0,0x05f927e4,0x04fb9be2, + 0x3f2027dc,0x00ff2003,0x70027dc0,0x25fb009f,0x360007f9,0x7ccbf31f, + 0x4bee00df,0x77dc1dec,0x5feeeeee,0x3bbbbbee,0x3bee5fee,0x5feeeeee, + 0x3bbbbbee,0xec985fee,0x981ffffe,0x1ffffeec,0xfffeec98,0xfeec981f, + 0x05fb1fff,0x01fe97ea,0x403fa9fe,0x77e404fb,0xc84eeeee,0x4eeeeeef, + 0x70027dc0,0x47f8809f,0x764c01fe,0xf31ffffe,0x80efe98b,0xffbfd5f8, + 0x77777e43,0x3f24eeee,0xeeeeeeee,0x3bbbbf24,0x3f24eeee,0xeeeeeeee, + 0x6677fdc4,0x7fdc1ffc,0x41ffccce,0xfccceffb,0x677fdc1f,0x3fb1ffcc, + 0x1fe97ea0,0x03fa9fe0,0x3f2027dc,0x86ffffff,0xfffffffc,0x0027dc06, + 0x5fa809f7,0xff7027e4,0x23ff999d,0x4fe885f9,0x33f98fe8,0x002fdc9f, + 0x7dc00bf7,0x017ee005,0xfd81ff88,0x3607fe21,0x207fe21f,0x07fe21fd, + 0x817e47f6,0x40bf64fb,0x007266f8,0x3fc809f7,0x000ff200,0xf70027dc, + 0x4c2fd809,0x03ff107f,0x417e63fb,0xb9fdc6f9,0xffa97e1f,0x01ff5000, + 0x4003fea0,0xf5000ffa,0xfa87fa0b,0x7d43fd05,0x7d43fd05,0x3ee3fd05, + 0x7e45fb05,0x000bf704,0x3fc809f7,0x000ff200,0xf70027dc,0x4cffc409, + 0xa81ff999,0x263fd05f,0x44df305f,0x7c4bea6f,0x80067f44,0xfd000cfe, + 0x33fa0019,0x446fa800,0x1bea1ffd,0x7d43ffb1,0x50ffec46,0x1ffd88df, + 0x7fa85ff1,0x3e60ffc4,0x700faa1f,0x03fc809f,0x4000ff20,0x3ee004fb, + 0x3fffea04,0xa84fffff,0x8ffec46f,0xfb9935f9,0xf10fec5f,0xf983fb7d, + 0x0eccbdff,0x65efffcc,0x7ffcc0ec,0x4c0eccbd,0xeccbdfff,0x373bfe20, + 0x3e21feff,0xfeffdcef,0x373bfe21,0x3e21feff,0xfeffdcef,0x3737fee1, + 0xdffb82ff,0x7fc3ffdc,0x2027dc07,0x3f2003fc,0x09f70003,0xfb027dc0, + 0xfb99999b,0xb9dff10d,0x3e63fdff,0x45dfff35,0xfffa83fa,0xffffb102, + 0xffb101df,0xb101dfff,0x01dfffff,0xdfffffb1,0xdfffe981,0x7f4c1fc8, + 0x41fc8dff,0xc8dfffe9,0x7fff4c1f,0x7541fc8d,0x2a01efff,0xc81efffe, + 0x027dc05f,0xfc800ff2,0x09f70003,0xf8827dc0,0x207fe00f,0xc8dfffe9, + 0x0002201f,0x02620008,0x20009880,0x26200098,0x40008800,0x00440008, + 0x06000220,0x40600600,0xeeffeeed,0x7777e40e,0xefc86eee,0xd86eeeee, + 0xeeeffeee,0x7ff776c0,0x0bf50eee,0x02204fd8,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0xffffff80,0x7fe40fff,0xc87fffff, + 0x7fffffff,0xfffffff8,0x7fffc0ff,0xfb0fffff,0x006fa807,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x40f32000,0xca814c29,0x055cc00c,0x203cc800,0x98014c29,0x0bb8802c, + 0x40044002,0x298003c8,0x0310014c,0x8000b260,0x157300cb,0x76c17a20, + 0x00f32000,0x32a0aa62,0x027d400c,0xf882fec0,0x205ff11f,0x3ee00efb, + 0x36001eff,0x47fe205f,0x7d402ff8,0x3fe203ff,0x204eacff,0x203ffffa, + 0x7c4006f8,0x005ff11f,0x3fea01f6,0x3fb0003f,0x4fffff98,0x1fd06f88, + 0x8817f600,0x706ffffb,0x7ff401df,0x80ff6000,0x07fa0ff8,0x5100ef98, + 0xb005ffd7,0x0ff8807f,0xdfa807fa,0x1d303fc8,0x201dffb3,0x2ffdcffa, + 0x8800df10,0x007fa0ff,0x37ea02fc,0xd8003fc8,0x26ffea1f,0x37c44feb, + 0xfd800fe8,0x37ffe603,0x3be602ac,0x400bf700,0x10100098,0x20013100, + 0x26200ffb,0x00202000,0x20019831,0x717ec008,0x01be20bf,0xb7004040, + 0x18807fff,0x7ec000cc,0xff10bfa1,0xfe837c41,0x1004c400,0x310007ff, + 0x00000001,0x20000000,0x000004fd,0x00000000,0x6f983fe0,0x0000df10, + 0xfeffe980,0x000001ff,0x17ea3fb0,0x0df127dc,0x200003fa,0x000003fc, + 0x05e88026,0x82f443d9,0x417a21ec,0x17ee01ec,0x00e77edc,0x80e77edc, + 0x03d905e8,0x8073bf6e,0x413ee2fe,0x7ddb36f8,0x3bfb6e20,0xf13fe81d, + 0x6dc03ffd,0x65401cef,0xf91ffdee,0x44dfd305,0x701fd06f,0x7c0bdddd, + 0x7775c007,0x407f305e,0x45fb06f8,0x45fb06f8,0x05fb06f8,0x3fa61fdc, + 0x303fffef,0x7fffdffd,0x3f60df10,0x7f7ff4c2,0x2df903ff,0xdf100ffa, + 0x0bffdff5,0xfffddff7,0x5f52fdc5,0xffd309f7,0xb107fffd,0x3fffdfff, + 0xfff707f6,0xfe837c4f,0x7ffffc80,0x2aa2bf30,0xffff9009,0x8813ea0f, + 0x445fb06f,0x445fb06f,0x205fb06f,0x27f40ff9,0x3fa07fea,0x220ffd44, + 0xe85fb06f,0x40ffd44f,0x01efeff8,0x4c33ffe2,0x220cc1ff,0xd8bf27fb, + 0x9fd0bf17,0x7e41ffa8,0xfe8ff42d,0xff3dfb10,0x0fe837c4,0xfa83fc40, + 0x2fffffee,0xfa83fc40,0x6c1be204,0x6c1be22f,0x6c1be22f,0x3fffe62f, + 0xfc82fd44,0xfc82fd45,0xfd837c45,0x7e417ea2,0x00bffb05,0x9f709ff1, + 0xfe87fc00,0x547f93e0,0x44bf905f,0x3e3fb07f,0xf0dff98f,0x303fe21f, + 0x7f8803ff,0x537bff70,0x7c403ff9,0x4409f507,0x445fb06f,0x445fb06f, + 0x4c5fb06f,0x05f902ef,0x05f91be2,0x0df11be2,0x02fc8bf6,0xefe88df1, + 0x88ff11ff,0x00bf307f,0x50fe8fec,0x3f23fc5f,0x7dcdf102,0x3fa3fb04, + 0x13fc3ffc,0x7ff445ff,0xb83fc401,0x40bf904f,0x09f507f8,0x2fd837c4, + 0x17ec1be2,0x8bf60df1,0x07fa04f9,0x00ff47f8,0xb06f88ff,0xf00ff45f, + 0xdfb4fd8f,0x6f88df31,0xd930df30,0x323ffffd,0x37c4fb2f,0x27f807fa, + 0x23fb03fc,0x7c41effd,0x337ffe27,0x202efbef,0x09f707f8,0x7f881be6, + 0x7c40bf50,0x7c45fb06,0x7c45fb06,0x7cc5fb06,0xf807fa04,0xff00ff47, + 0x5fb06f88,0x4ff00ff4,0x56ff47f8,0x9837c45f,0x677fdc6f,0x9f51ffcc, + 0x745fa97e,0xfd9fe01f,0x3f23fb02,0x225fa80d,0xb0dffeef,0x83fc409f, + 0x0ff105fa,0x5fb83fc4,0x7ec1be20,0x7ec1be22,0x7ec1be22,0xfd80b222, + 0xfd8df102,0xf88df102,0x7ec5fb06,0x7ccdf102,0x17fffc46,0x2fd41be2, + 0x3fb03ff1,0x44bf3fe2,0x817ec1fe,0x413f26f8,0x20df31fe,0x45be22fd, + 0x07f88000,0x17ea0ff1,0xbf707f88,0x7c40ff80,0x2207fc2f,0x207fc2ff, + 0x64002ff8,0xc8bf704f,0xf0bf704f,0x22ff881f,0x4bf704fc,0xfffa87f9, + 0xfc837c40,0x7f417ea3,0x373ffea1,0x04fc84ff,0x42fdcbf7,0x0ffa1ffb, + 0x06f88ff5,0xb03fc400,0x02fe889f,0x2fdc1fe2,0xfe88bfe0,0xd117fc2f, + 0x22ff85ff,0x3a62ffe8,0x307fe204,0x1ff883ff,0x7fc0ffcc,0x88bffa22, + 0x0ffcc1ff,0xffd50ffe,0xfa86f883,0x36237d46,0x7ffd41ff,0x3ff102ef, + 0x3e21ff98,0x87ffea1f,0xffdcdff9,0x00037c41,0xff981fe2,0x404fecbd, + 0x0bf707f8,0xefcdffb8,0x6ffdc2fd,0x5c2fdefc,0xfdefcdff,0xb803ff62, + 0x3ffdcdff,0xfb9bff70,0x37fee07f,0x5c2fdefc,0x3ffdcdff,0xffceffa8, + 0x3e20efef,0x1ffdccef,0x7ee77fc4,0x5fd41fef,0x9bff7001,0xffb07ffb, + 0x83f99fdb,0x81dfffd8,0xcc8006f8,0x1cccffcc,0x177ffec4,0xcffcccc8, + 0x00df71cc,0xf71bffd5,0x1bffd505,0xffd505f7,0x7dc5f71b,0xfffea806, + 0x7ff5401e,0x7f5401ef,0xa82fb8df,0x201efffe,0xd1dfffea,0xfffdb8bf, + 0xffd300de,0xd83f91bf,0xffea8007,0x3ff201ef,0x0c03f93e,0x20017a20, + 0xffffffff,0x3e00603f,0xffffffff,0x4400bd73,0x00044000,0x00020022, + 0x000c0006,0x00c00044,0x01000040,0x3c800440,0x2000c000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x55530000,0x53035555,0x54c00557, + 0xba9800aa,0x2aaaa60a,0x2a600aaa,0x0032e009,0x32205950,0x020bb883, + 0x3c881654,0x6440736e,0x09999953,0x80357510,0x209aca98,0x20aa00a9, + 0x2e014c29,0x0164c03f,0x1dd127dc,0x64c076c0,0xfffffc82,0x7ffe44ff, + 0x3ee03fff,0x904fffff,0x2bffffff,0xfffffffc,0xfffffb81,0x000fe80d, + 0x320bffd3,0x7fffc41f,0x3fa64eac,0x6c3f905f,0x1fc80ffd,0x40fffff9, + 0xefffffc8,0xfffffc81,0x440bf65f,0x88ffc47f,0x1ffe02ff,0x203fffa8, + 0x3f62fff8,0x3a0df504,0x567e40ff,0x5dc1aaaa,0x81ffdb9a,0xecabcffd, + 0x5e7ff444,0x55535dca,0xfd83fd55,0x0efc989c,0xea8007f4,0x84fa85fa, + 0xeffd98e9,0x217ebaa0,0x83fb04fa,0x266624fa,0x2fbf607f,0x360ffda9, + 0x3baabcef,0x3fc40bf6,0x1fe83fe2,0x7d413f60,0x3e03fc8d,0x41fea1ff, + 0x1ffd03fd,0x40001fc8,0x0f7dc4fe,0x8077ec08,0xf70ff400,0xfd17ea07, + 0x45f88001,0x11000ee8,0x3a22fc40,0x22ffe40e,0xff100ee8,0x3e03fe20, + 0x003ff32f,0x1fe205fb,0x20000404,0x9300cc18,0xf885fd05,0x9035100f, + 0xfb80003f,0x4007fe25,0x20000ffa,0x4cdf11fe,0x743f91bb,0x2fc4000f, + 0x80000bf2,0x417e45f8,0x3f23fda9,0x441fe202,0x2e5fb06f,0x05fb005f, + 0x80001fe2,0x00000098,0x5fa8bf70,0x003f9000,0x3ee2fc80,0x00ff6005, + 0x3ee3fd00,0x22ffff52,0x3603fa4f,0x265f884e,0x2a9d104f,0xbf101cfd, + 0xc99827cc,0x8809f34f,0x10bfa07f,0x007fd4ff,0x7f8817ec,0x02f7775c, + 0xeeb82fb8,0x440005ee,0x70bf60ff,0xf90bdddd,0xf3000335,0x001fe41d, + 0xe80003fd,0xdf11f91f,0x3fa5f823,0x44077ec0,0x4403fa5f,0xffeefcdf, + 0x3fa5f884,0x21dfff00,0x3fc400fe,0xf519ff50,0x177f447f,0x7c40bf60, + 0x3ffffe47,0xfc82fb80,0x80007fff,0x90ff13fd,0xf90fffff,0x205bffff, + 0xd85feee8,0x83fe002f,0x403cccc9,0x3eafb1fe,0x07f4fb03,0x90980bfb, + 0x7ffc405f,0x2603fea3,0x4cc05f90,0x2200bf20,0x7ffcc07f,0xffe981ff, + 0x02fd80be,0x3fc40ff1,0x2017e440,0xa80007f8,0x8809f76f,0xdccca87f, + 0xff982fff,0x3fa0cfff,0xa8ff1002,0x7406ffff,0x0beadd1f,0x361fd3ec, + 0x17e6005f,0xff07ff10,0x002fcc03,0x7c4017e6,0x7ffec407,0x3ffae03f, + 0x8817ec3f,0x81fe207f,0x403fffd9,0x2da807f8,0x07fa7fe0,0x6400ff10, + 0x6fd9807f,0x7c400bf6,0x37d4cc47,0x8bec7fa0,0x7f4dd04f,0x74005fd8, + 0x83fc400f,0x03fa02fd,0x2001fd00,0x3fa607f8,0x405ffc8c,0x3f65ffda, + 0x440ff102,0x273fa07f,0x03fc4009,0xfc80fffc,0x7f8806fd,0x007fe200, + 0x03fc97f4,0x7cc03fe0,0xfc8ff406,0x7c737fd0,0x01bf7fa5,0x33265f70, + 0xfd837c42,0xd915f702,0x997dc05b,0x0ff102cc,0x3fe617fc,0xb2ffa802, + 0x81fe205f,0x0bf307f8,0xe807f880,0xfff103ff,0x00ff1007,0xf9002fe8, + 0xe801bee7,0x80df303f,0x267f51fe,0x47f37ffd,0x002ffafe,0x27ff4bf1, + 0x17ec1be2,0xecfaafc4,0x3a5f881f,0x0ff104ff,0x2fe417e6,0x7f94fc80, + 0xf8817ea0,0x8009f707,0xff9807f8,0x401ff603,0x3e2007f8,0x23fd000f, + 0xf5002ff8,0x40df301f,0x11fa0ff9,0x1fd07ec1,0xfd005ff3,0x113eff21, + 0x20bf60df,0x17dc20fe,0xfbfc87f4,0x2e0ff104,0x00bf704f,0x827dcff2, + 0x1fe204fc,0x220037dc,0x03ff007f,0xf8801fec,0x09fb1007,0xff91bee0, + 0xefe83105,0x20bb7cc0,0x77d46fc8,0x7f43fc80,0x5c03ff50,0x9f39f33f, + 0x5fb06f88,0xfe883fb8,0xcf99fdc0,0x0ff104f9,0x3fa03fea,0x8ffcc013, + 0x7fcc1ff9,0x441fe201,0x3e2003ff,0x206fb807,0xf1000ffa,0xfb999b0f, + 0xb999b0bf,0xfd881fff,0x44feddff,0xecdeffe8,0xffbdfd6f,0x59df703f, + 0x1fd09fd7,0x7c40ffdc,0x3bfbbf66,0x7ec1be23,0x3e61be22,0xfb37c41e, + 0xf107dfdd,0x667ff40f,0x3bf66ffc,0x44ffedcd,0xffecdffc,0x703fc404, + 0x88013bff,0x3bfb207f,0x003ff500,0x7ff43fc4,0xfff02dff,0xea807dff, + 0x702cefff,0x25bffffd,0x00dfffeb,0x0b7fff66,0x3ff207f4,0xdd90fec0, + 0x37c47dfd,0x07f62fd8,0x6c357ff5,0x3fbbb21f,0xff99993e,0x7dc43999, + 0x2e0bffff,0x2dffffff,0x5dfffd70,0x3ff33320,0x7fd41ccc,0x666644ff, + 0x3e1cccff,0xffff983d,0xfcccc803,0x1331cccf,0x00026600,0x00cc0004, + 0x00100011,0x1dfb01fd,0x4f980fea,0x2fd837c4,0xfff707f5,0x980fea9f, + 0x3ffffe4f,0x3103ffff,0x00133001,0xffff8018,0x503fffff,0xffff87b9, + 0x003fffff,0x20019bd3,0xffffffff,0x0000003f,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x02ae6200, + 0xdddfd910,0xdd9501dd,0x0f223b9b,0x00014400,0x22179995,0x07ddb34e, + 0x23deedb8,0x0d4c02a8,0x55440055,0x4c40009a,0x551001ab,0x2aa35555, + 0xaaaaaaaa,0x5555530a,0x5554c555,0xaaaa8009,0x57510009,0x00aaa001, + 0xff5013ee,0xf101bfff,0xddffd9bf,0xfeeffd81,0x00df11ff,0x22001fe0, + 0x12fffffe,0xffdff5bf,0xddffd30b,0x103fe8ff,0x01fe21ff,0xffffffa8, + 0x7ffd401d,0x7e401eff,0xdf4fffff,0xdddddddd,0x3ffff21f,0x3ff67fff, + 0xf00dffff,0x07ffffff,0x1fffffe4,0x80bffe20,0xf702fff8,0x1dfd759f, + 0x27e43fd8,0x3fa06fe4,0x2000df11,0xdfd8007f,0x3fe21312,0x83ff30cf, + 0x26140dfe,0x23fd80ff,0x3ea007f8,0x3ffecbae,0x9339ff30,0xefef805f, + 0x021f1aaa,0x559f90f8,0x67ec5555,0x82ffecba,0xffecabff,0xa9adfd83, + 0x3fea03ff,0x0fffc04f,0x3a20ffc4,0x7c43fc3f,0x6c07fc47,0x000df11f, + 0x3fe001fe,0x213fe201,0x017f24fb,0x37cc4fd8,0x3bffffe2,0xb85fa81d, + 0x83fd80ff,0x2fdfd400,0x85dfd0f8,0xb007f90f,0x81ff705f,0x547fc87f, + 0x206f986f,0x4c07fafd,0x209f902c,0x21fe27fa,0x837dc7f8,0x00df11fd, + 0x3bffbba6,0xf301eeee,0x307f880d,0x000ff4bf,0x17f43ff1,0x3b733fe2, + 0x82fd43ff,0x00fe85fc,0x05f9fe80,0xf27dc41f,0x3600ff21,0xf8bfb02f, + 0x320ff887,0x105fb03f,0x0007faff,0x3fe01ff8,0xbf70bfa1,0x3fb04fc8, + 0x67ed5be2,0x7ffffcc1,0x302fffff,0x06f880bf,0x007fcdf3,0x2fd57ee0, + 0x7fd43fc4,0x7cc17ea1,0x98007f87,0x1f05f8cf,0x643e1f50,0x02fd803f, + 0x887f8ff3,0xb817ec7f,0xf74fa83f,0x03fc0009,0xcffa8bf6,0x7ec0ffda, + 0x3e23fb02,0x4ffeefce,0xf3001fe0,0x306f880b,0x001fe2df,0x407f57f4, + 0x49f907f8,0x03fe05fa,0x3f9000ff,0x203e0bf1,0x3f21f1f9,0x202fd803, + 0x321fe0ff,0xb81fec5f,0xf32fd84f,0x1be6000f,0x6ff47fb0,0xfc80dfff, + 0x3e23fd03,0x03fea4ff,0x7ffc03fc,0x7fffffff,0x27d41be2,0xf50001fd, + 0x1fe207ff,0xffeeb7d4,0xa8ffc1ee,0x2aaaaffa,0xeff8b7c0,0x4cc1f1ee, + 0x3f21f0fd,0x24eeeeee,0x87fe02fd,0xefecbbff,0x64077d40,0x3a3fc45f, + 0x6f98001f,0x4f99fe40,0x709f7002,0x0ffe23ff,0x03fc07fe,0x67fee664, + 0x1be24ccc,0x07f71fe4,0x881bf600,0x3ebf707f,0x742fffff,0xffffff1f, + 0x3ee01fff,0x3dddff13,0x06f7c43e,0x3ffff21f,0x0bf66fff,0x3ffe1fe8, + 0xfd00bfff,0x9fffb99f,0x27e45fa8,0x0ff30150,0x3df52fd8,0xa83fe200, + 0x0ff11fff,0x03fc0bf6,0xf1017e60,0x4c6fb88d,0x74040bff,0xeeeffeee, + 0x7f41fe21,0xff817ea3,0x333ff331,0x887f4033,0x3e21f05f,0x07f90f80, + 0x7fc05fb0,0x3f267fe1,0x3ffb200e,0x7ec3fccf,0xfe83fcc2,0x203fc40f, + 0xfffd11fe,0xfb05bfff,0x3fb9fd9f,0x17ec1be2,0x7cc007f8,0x677fc405, + 0xfc81fffc,0xe87ecdff,0xeeeffeee,0xf931fe21,0x882fd41f,0x003fc0ff, + 0x82fc4bf3,0x43e03a0f,0x2fd803fc,0x3fc1ff10,0x320013f2,0x267fe22f, + 0x441ff999,0x1ff82fff,0x7e41ff10,0x4fffeeed,0xfb3effc8,0x7ec1be23, + 0x9800ff02,0x7ffc405f,0x2e00dfff,0x405ffffe,0x3fe204fb,0x41efffee, + 0x0df705fa,0x7fe400ff,0x1f05ffff,0x7f90f804,0x2e05fb00,0x3e23fc6f, + 0x07f4000f,0xfffffff5,0x1dfb09ff,0x3ee09f90,0x7dc17ee5,0x23fb0207, + 0x05fb06f8,0xf98007fa,0x08b7c405,0x803be200,0x99dfc999,0x77fffc41, + 0x10bf503d,0x00ff07ff,0x3bbbbfe2,0x3ea1f05f,0x07f90f82,0xff105fb0, + 0x4fc87f85,0xfb0ff200,0xfb99999b,0xff88060d,0xfb07fd43,0x003fe205, + 0x06f88fec,0x13f605fb,0x4405f980,0x7f50006f,0xffffff80,0x1fe21fff, + 0x7545fa80,0x200ff06f,0x82fc43fb,0x1f03d30f,0x3f600ff2,0x7c2ffd42, + 0x400ff987,0x7fc46fd9,0x0007fe00,0x3fb3bfee,0xc837ec3f,0x47f6005f, + 0x05fb06f8,0x3733ffe6,0x880bf301,0x3f90006f,0xdfdaaa80,0x1fe20aaa, + 0xfeeffa80,0x7f6c0dff,0x3eeeeeef,0x9ff103fa,0x2003e599,0xddddf90f, + 0x777ecddd,0xff05fffe,0xddb13f60,0xfa819fff,0x0027ec05,0x1dfffea8, + 0xeeefff98,0x36000eff,0x360df11f,0x3ffaa02f,0x0bf301ff,0x30006f88, + 0x04fb8005,0xfa801fe2,0xf01cefff,0xffffffff,0x2217e69f,0xff5fffff, + 0xffffffff,0x3ffff21f,0x3ff67fff,0x3e01ceef,0x741ff307,0xfb01cdef, + 0x006fa807,0xeb880180,0x8002ceee,0x20df11ec,0x013002fd,0x74405f98, + 0x00000005,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, + 0x00000000,0xaaaa8000,0xaaa98099,0x31aaaaaa,0x55555555,0x260aaa00, + 0x2055100a,0x2aa0d42a,0x0aaaaaaa,0x0aa60551,0x40335555,0x455302a9, + 0xaaaaaaa9,0x803551aa,0x02aa22a8,0x03530aa8,0x01551a88,0x0154c550, + 0x2aaaaa55,0x00aaaaaa,0x751002aa,0x80551015,0xfffffff8,0x3ffff20c, + 0xf95fffff,0x0fffffff,0xfb0fff70,0x7c1be205,0xfff0fe65,0x21ffffff, + 0x2ff886f8,0x3fffffe2,0x07fec0bf,0xfff737fc,0x2bffffff,0x2fe406fb, + 0x7fd413fa,0xf9807f50,0xfa809fb4,0x220fff26,0xffffff6f,0x501fffff, + 0x36203ffd,0x4c3fffff,0x57fc406f,0x2a5ffcba,0xdccccccc,0x555bf95f, + 0xff880555,0x102fd87f,0xfa93e0df,0x6fed5542,0x0df10aaa,0xaff887fd, + 0x6c5ffeba,0x3ffd43ff,0x99999993,0x81ffc9ff,0x7fcc0ff8,0xf517f441, + 0xf53f9809,0x323fc80f,0x56f886ff,0x55bfb555,0x7ff4c155,0x7bfd01ff, + 0x7cc3ff95,0x443fc406,0x3fd000ff,0x6c000ff2,0x2fd87fbf,0x9f10df10, + 0x9f700fdc,0x9fb1be20,0xff10ff10,0x3637f747,0xdf7007ee,0xfd80ffa8, + 0xfc8bf904,0x2a027cc5,0xf807fe3f,0x0bfbf21f,0x13ee0df1,0xff9dff98, + 0xbf905501,0x3e2037cc,0x3003fd07,0x001fe4df,0x43fc67d4,0x4df102fd, + 0xdaadfbaa,0x4fb81abf,0x2fdcdf10,0xbf907f88,0xf887e774,0xff8807dc, + 0x7cc4fe81,0x25ff100f,0x2fcc0ff9,0x4fd8bea0,0xbfc8df30,0xb837c46f, + 0xff0e404f,0x26f98003,0x3fc406f9,0x5fb007f8,0x22001fe4,0xd87f88ff, + 0x74df102f,0xffffffff,0x04fb85ff,0x03be6df1,0x6fa83fc4,0x7dcfe7ba, + 0x7ec00fd9,0x6c1ff304,0x47fd403f,0x45f883fe,0xfa8bee09,0xfc87f906, + 0x1be22fda,0x3e0027dc,0x37cc001f,0x7f880df3,0xf9804fc8,0x2001fe46, + 0xb0ff12fd,0x21be205f,0x701f61fb,0x23be209f,0x1fe201ff,0x3adf2fec, + 0x803f6dd6,0xa7ec06fa,0xdfb006f9,0xa9be20df,0xff07ee3f,0xfc81fd03, + 0x1be26faa,0x3e0027dc,0x2fdc001f,0xff880df3,0x002ffeee,0xcefc85fb, + 0xfa82cccc,0x3f61fe25,0xfeeeeeee,0x1ba0fc86,0x3e209f70,0x7c402fef, + 0x3e2ff887,0x367f5f95,0x03ff100f,0x2fd8ff88,0x03fff100,0x64df937c, + 0x2627ec1f,0xfd2fc86f,0x7dc1be23,0x00ffc004,0x7cc5fd10,0x7fffc406, + 0x4c02efff,0xffffc86f,0x0fe85fff,0x3ff61fe2,0x6fffffff,0x202fc7c8, + 0xfff104fb,0x9ff8805f,0x7c5ffdb9,0x321fff35,0x009fb01f,0x001bfbf2, + 0x37c01ffd,0x03f23fff,0x83fc8df5,0x22bf52fc,0x009f706f,0x36001ff8, + 0x2037cc5f,0x7fe447f8,0x320bf601,0x099999cf,0x1fe217e4,0x37c40bf6, + 0x7013e3ec,0x2bbe209f,0x3fe200ff,0x443fffff,0xfc97fa5f,0x2006fa81, + 0x2001fff8,0x3a05fffb,0x329f9f57,0x3a1ff80f,0x3e2fc80f,0xf706f89f, + 0x01ff8009,0xf981df90,0xc83fc406,0x20df305f,0xef9803fc,0x9ff99999, + 0x2205fb09,0xdffdd56f,0x20ddffdd,0x2df104fb,0xff100efb,0xf1013599, + 0x3f917dcb,0x4001ff88,0x3e6005fb,0xfd02ff9f,0x3edbe3f2,0x0df33fd8, + 0x8cfb8bf2,0x009f706f,0xfc801ff8,0x406f980e,0x0df507f8,0x1fe40ff6, + 0x7ffffdc0,0xb4ffffff,0x4dbe205f,0xfdccefcc,0x09f704cd,0x05fd9be2, + 0x3e600ff1,0x3617e405,0x4fb8004f,0x7dcffa00,0x5f8fd80f,0x2a0fb3f9, + 0x3207f76f,0x3e7fe22f,0x8009f706,0x77e401ff,0x880df300,0x309fb07f, + 0x01fe40ff,0x6666664c,0xfb2ccffc,0xf11be205,0x2e017d49,0x44df104f, + 0x07f883ff,0xfb809f30,0x0003bea2,0x7dc013ee,0x7e427f46,0xdd9f32fb, + 0x07f47fc0,0x67e42fc8,0x009f706f,0x3f201ff8,0x01be600e,0xffa88ff1, + 0x3203fd82,0x7c40003f,0xf102fd87,0x3ee4f88d,0x8827dc01,0x20ffcc6f, + 0x9f3007f8,0xff13fb80,0x13ee0003,0xf30bfe20,0x47efc83f,0x3f606eff, + 0x0bf206fc,0x2e0dfff1,0x0ffc004f,0x4c00efc8,0x77fc406f,0x983fffee, + 0x0ff200ff,0xb0ff1000,0x31be205f,0x6c07e47f,0xeeeffeee,0xff70df10, + 0xa803fc41,0xc9fdc04f,0xffffffff,0x013ee06f,0x37e417f6,0xff917fee, + 0x1fffd40b,0xff905f90,0xb013ee0d,0xdddffddd,0x3ffffe67,0xff36ffff, + 0x15dddddd,0x19dfffff,0xf900ff60,0x7f880007,0xdf102fd8,0x03f22fa8, + 0x3ffffffe,0x20df10ff,0x01fe26fd,0x3ee027d4,0xffffffb3,0x7dc0dfff, + 0x807fdc04,0x3fee4ff8,0x202ffcc2,0x3f200fff,0x706ff882,0xffff809f, + 0xf34fffff,0xffffffff,0x3ffffe6d,0x00003fff,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x80322000, + 0x8026200b,0x6c40001c,0xdb880000,0x2e01defe,0xe881cefd,0x6d43d905, + 0xdfd98cef,0x00054400,0x207bddb7,0x700cefdb,0xc83ddfdb,0x21bd505e, + 0xd52ed8bd,0xeeeed81b,0xaa986eee,0x2017540a,0x2e1d74e9,0x77441dfd, + 0x6c03b600,0x40df504f,0x7ff304fa,0x0ffe6000,0x7dc04fa8,0x42fffeef, + 0xfffeffe9,0xfd837c43,0x3bff7be2,0x7406fdef,0xffe9807f,0xefd87fee, + 0x7f42ffed,0x442feeef,0x17fc43ff,0xf3ffdb9f,0xfffe8bfd,0x7dc7ffff, + 0x3ea1ffff,0xfca7d404,0x1fffefc9,0x6fa81fec,0x32ebfe20,0x2a01ff9b, + 0x13fe604f,0x805ff700,0x20cc04fa,0x27f47fb8,0x6f887fea,0x36145fb0, + 0x405f90ff,0xdfe807fe,0x513f2140,0x417ee1ff,0x361ff980,0x5c77fc4f, + 0x200fd4ef,0xf70cc3fe,0x2e02fccb,0x45fdf93f,0x837d45fc,0x7fd403fd, + 0x403fffff,0x7f4404fa,0x1efc800d,0x0004fa80,0x82fd43fe,0x41be25fc, + 0x97ee02fd,0x072204fa,0xf100bf90,0x7e4ff20d,0xab7e4003,0xf52ff86f, + 0x32007ecf,0x37cc405f,0x9174cdf1,0x307ff23f,0x883ff0df,0x7fc400ff, + 0x402ffc9c,0xdfb004fa,0x03bf6201,0x00027d40,0x40bf23fb,0x41be26f8, + 0x8fea02fd,0xe80004f9,0x04fa801f,0x0bfee9f5,0x1ff9fd00,0xb27d4ff0, + 0x077d401f,0x37fff644,0x365fc9fe,0xd107f90f,0xfb89f90b,0x645fb804, + 0x7777645f,0x205eeeff,0x7f441ffb,0x5ccccc04,0x981999df,0x1ffffeec, + 0x13fc03fd,0x88bf60df,0xeeefeedb,0x266665fe,0x41999999,0xefb800ff, + 0x5feeeeee,0x0077fff6,0x7c0bffe2,0x0fd8fea7,0x3203ff30,0x746f99af, + 0x3f43ffa7,0xf88005f9,0x6c00ff47,0x363fcc2f,0xffffffff,0x8ffdc07f, + 0xff805ff8,0xffffffff,0x33bfee1f,0x3fd1ffcc,0x0df13fc0,0xcffe8bf6, + 0x2ccccefd,0x3ffffffe,0xff11ffff,0x3bbbf200,0x4c4eeeee,0x200efffd, + 0xff00ffe8,0x01fb1fd4,0x37c05fd1,0x98fd8df5,0x64df3fcf,0x2fd8002f, + 0x3f600df3,0x2a03fc42,0x3fe6004f,0x201ffd43,0xcefdcccc,0x3ff10ccc, + 0x0bf63fb0,0x0df137c4,0x52fd4bf6,0xcccc809f,0x0ccccccc,0x3ee003fe, + 0xfd510005,0x77f7ec0d,0x8fea7f80,0x04fd80fd,0x37ff6fec,0x3e3f27ee, + 0x017e4bf6,0x3fcafd40,0xf989fb00,0x8027d406,0xfd302ffb,0x027d4009, + 0x47fa0bf5,0x8bf704fc,0x97fc40ff,0x01bea2fc,0x01ff4000,0x00007fd4, + 0xdf701ff1,0xa9fe17f6,0xf703f63f,0x17b7100d,0x5ebfa877,0x7e49f5f9, + 0xd1ff0002,0x3bea001f,0xf500ff60,0x03df9009,0x800dfe88,0x1bea04fa, + 0x3e23ffb1,0x20ffcc1f,0x3ffa22ff,0x3fa27f72,0x0054001f,0x8102ffea, + 0x30000cfe,0x23ff30ff,0x53fc3ff8,0xf307ec7f,0x4c00001f,0xfbf32fdf, + 0x80017e47,0x2005fdfc,0xfffdfff8,0x1027d401,0x6c001dfb,0x27d400ef, + 0xfb9dff10,0x7fdc3fdf,0xb83ffdcd,0xfdefcdff,0x7dd9df12,0x403b99ff, + 0xffd806fd,0x7cc7ecdf,0x0eccbdff,0xffb999dd,0x4c3ff889,0xfa9fe1ff, + 0xfff83f63,0x32eeeeee,0x7ddddddd,0xff07ffc4,0x0017e45f,0x002fff98, + 0x37ffbbf6,0x8164c06f,0x70005fe8,0x27d403ff,0x37fffa60,0x7f541fc8, + 0x3aa01eff,0x22fb8dff,0xff90efeb,0x3ff403ff,0xffffea80,0xffffd885, + 0xffffb0ef,0x0bfb05df,0x53fc3ff2,0xff07ec7f,0x5fffffff,0x3bbbbba6, + 0x322ffc3e,0x00bf20ff,0x2006fe80,0x09fb06fb,0x001f4400,0x98807ea0, + 0x00011000,0x00088003,0x26002601,0x0088002c,0x4cc01310,0x00000009, + 0x00000000,0x00000000,0x4407ee00,0x3333264f,0x002ccccc,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x40000000, + 0x3fee0400,0x4fffffff,0x74400000,0x4039fb54,0x64400aa9,0xb9800001, + 0x000001bc,0x03372a00,0x4017bb93,0x16dc04c9,0x200e65c4,0x333100a8, + 0x81333333,0x4cc40bb9,0x09999999,0x26057700,0x257714c2,0x00000bb9, + 0x40000000,0xfeefcdf8,0x7fff444f,0x80be202f,0xc81d705c,0x40dfdcdf, + 0x3203645c,0xfd83320d,0x3f21ffff,0x6cc0ffff,0x3fe207ff,0x3fffea0f, + 0x41bf604f,0xfffffffa,0x0efb84ff,0x3ffffff2,0xfd802fff,0x11ff880d, + 0x54ffa3ff,0x000000ff,0x44000000,0x3fea3fff,0x3ee6ff60,0x8fc46a0f, + 0x717fa238,0x557641df,0x7ec5d88a,0x7d40ff23,0x1731be65,0x32049fb1, + 0x3f7fe23f,0x897ffc07,0x05fd10ff,0x5107fbf5,0x55555555,0x543be603, + 0xebbbbbbb,0x01fec02f,0xd1fe83fa,0x001fe67f,0x00000000,0x3e0ffe20, + 0xfc8bf31f,0x5cfd3fa3,0x57fa20ff,0x27e60efb,0x3f8afcfa,0x0fe83fa2, + 0x07fa0fe8,0x7ec0bf70,0x03fc45c2,0x1fd4bfea,0x75ba13ea,0x8800000f, + 0x05f90009,0x808004c4,0x3fccbf60,0x00000000,0x83fc4000,0xa87f52fd, + 0x77f7544f,0xefe880bf,0x6ab5c0ef,0xbf317299,0x8ff22fcc,0x7f4403fc, + 0x027ff542,0xff880ff1,0x4f987f70,0x44f98fdc,0x99999998,0x20000099, + 0x000002fc,0x37c4bf60,0x00000000,0x837c4000,0xb8bf32fd,0x1ffe883f, + 0x407ff440,0x21ddf54d,0x362fc86b,0x7ccdf12f,0x42ff4406,0x103ffdc9, + 0x25fc80ff,0x117e45f9,0x2a1fd8bf,0xffffffff,0x3200004f,0x0000002f, + 0x037c47f2,0x00000000,0xd837c400,0x223ff12f,0x37f220fe,0xf701dfdf, + 0x2ab90bff,0x88b90fae,0xd8df10ff,0x5407f62f,0x7f9804ff,0xd910ff10, + 0xbdfe81df,0x207e46fd,0x2eee66f8,0x02bbbbbb,0x00000000,0x00000000, + 0x00000000,0x17ec1be2,0x87ffdff7,0xfd33f2fe,0xfe8efb81,0xdb547d45, + 0x22fd89d4,0x137c42fd,0xbffb81df,0xff700999,0x3a61fe20,0xfffb102d, + 0xb827cc19,0x6d40003f,0x2ca8103d,0xffb80dcc,0x55534fff,0x00000555, + 0x00000000,0x7ec1be20,0x40e6e4c2,0x20c3f109,0xbfd10efb,0x99339dd8, + 0xf527dc1f,0xfb93ee0b,0x3ffffe25,0xffddb2ff,0xffffe85f,0x010001ff, + 0x00130062,0x2ffffdc0,0x7ffccbee,0x503fff11,0x3a799999,0x007fffff, + 0x00000000,0x0df10000,0x10000bf6,0x2077405f,0x3ba20fe8,0x741fda9b, + 0xfd007f46,0x999076c1,0x3ae39999,0xccb80bde,0x0000cccc,0x80000000, + 0x8dfd89fe,0xfff50fd8,0x00fffe65,0x333332e0,0x00000004,0x10000000, + 0x4cbf60df,0x3eeeeeee,0x04401510,0xdfb70088,0x00202019,0x00000101, + 0x00000000,0x7c400000,0x2ffffec5,0x1ffd1bfa,0x00000000,0x00000000, + 0x20df1000,0xdddd32fd,0x00007ddd,0x00000000,0x00000000,0x00000000, + 0x06a00000,0x80413bae,0x00000009,0x00000000,0x00000000,0x00000000, + 0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000, +}; + +static signed short stb__consolas_24_latin1_x[224]={ 0,5,3,0,1,0,0,5,3,3,1,0,2,3, +4,1,1,1,1,1,0,2,1,1,1,1,4,2,1,1,2,3,0,0,1,1,1,2,2,0,1,2,2,1, +2,0,1,0,1,0,1,1,1,1,0,0,0,0,1,4,1,3,1,0,0,1,1,1,1,1,0,1,1,2, +1,2,2,1,1,1,1,1,2,2,0,1,0,0,0,0,1,1,5,2,0,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,5,1,1,1,0, +5,1,0,0,2,1,1,3,1,0,2,1,2,3,0,1,1,4,5,2,2,1,0,0,0,2,0,0,0,0, +0,0,-1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0, +0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, + }; +static signed short stb__consolas_24_latin1_y[224]={ 17,0,0,1,-1,0,0,0,-1,-1,0,4,13,9, +13,0,1,1,1,1,1,1,1,1,1,1,5,5,4,7,4,0,0,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,20,0,5,0,5,0,5,0,5,0,0, +0,0,0,5,5,5,5,5,5,5,1,5,5,5,5,5,5,0,-3,0,8,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,17,5,-1,1,2,1, +-3,0,0,1,1,5,9,9,0,1,0,2,0,0,0,5,0,8,17,0,1,5,0,0,0,5,-3,-3,-3,-3, +-3,-4,1,1,-3,-3,-3,-3,-3,-3,-3,-3,1,-3,-3,-3,-3,-3,-3,5,-1,-3,-3,-3,-3,-3,1,0,0,0, +0,0,0,-1,5,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,2,0,0,0,0,0,0,0, + }; +static unsigned short stb__consolas_24_latin1_w[224]={ 0,4,8,13,11,13,14,3,8,7,11,13,7,8, +5,11,12,11,11,11,13,10,11,11,11,11,5,7,10,11,10,8,14,14,11,11,12,10,10,12,11,10,9,12, +10,13,11,13,11,14,12,11,12,11,14,13,13,14,11,6,11,7,11,14,8,11,11,11,11,11,13,12,11,10, +10,11,10,12,11,12,11,11,11,10,12,11,13,13,13,13,11,10,3,10,13,12,12,12,12,12,12,12,12,12, +12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,0,4,10,11,12,13, +3,11,11,14,9,11,11,8,11,10,9,11,9,8,12,12,11,5,3,9,9,11,13,13,13,8,14,14,14,14, +14,14,14,11,12,12,12,12,12,12,12,12,13,12,13,13,13,13,13,11,13,12,12,12,12,14,11,11,12,12, +12,12,12,12,13,11,12,12,12,12,12,12,12,12,11,12,13,13,13,13,13,13,12,12,12,12,12,13,11,13, + }; +static unsigned short stb__consolas_24_latin1_h[224]={ 0,18,6,16,21,18,18,6,23,23,11,13,9,3, +5,20,17,16,16,17,16,17,17,16,17,16,13,17,14,7,14,18,22,16,16,17,16,16,16,17,16,16,17,16, +16,16,16,17,16,21,16,17,16,17,16,16,16,16,16,22,20,22,9,2,6,13,18,13,18,13,17,17,17,17, +22,17,17,12,12,13,17,17,12,13,17,13,12,12,12,17,12,22,25,22,5,16,16,16,16,16,16,16,16,16, +16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,0,17,21,16,15,16, +25,20,6,17,12,11,6,3,11,5,9,15,10,10,6,17,20,5,4,10,12,11,17,17,17,17,20,20,20,20, +20,21,16,20,20,20,20,20,20,20,20,20,16,20,21,21,21,21,21,11,21,21,21,21,21,20,16,18,18,18, +18,18,18,19,13,16,18,18,18,18,17,17,17,17,18,17,18,18,18,18,18,13,18,18,18,18,18,22,22,22, + }; +static unsigned short stb__consolas_24_latin1_s[224]={ 252,250,247,62,40,106,104,252,17,9,70, +48,159,238,215,91,183,221,233,12,36,1,222,13,152,220,247,223,37,189,26, +40,100,232,1,24,194,183,25,36,50,76,49,87,245,112,196,1,100,129,207, +164,208,176,181,167,153,138,126,34,146,26,177,26,201,62,119,127,171,139,65, +15,40,245,89,74,141,176,48,74,79,28,225,151,52,87,237,211,162,231,189, +41,1,64,201,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170, +170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,252,247,157, +143,1,103,5,186,235,59,201,118,210,238,94,227,167,14,130,140,222,196,79, +221,252,149,60,106,86,113,127,201,198,213,66,118,103,52,155,67,133,173,14, +27,241,1,40,53,129,228,168,182,196,210,224,82,238,1,14,27,144,158,117, +94,172,185,198,211,131,81,99,91,133,159,146,120,234,209,210,188,224,100,236, +49,157,90,63,113,144,27,1,77,14,75,52,115, }; +static unsigned short stb__consolas_24_latin1_t[224]={ 13,49,156,125,27,49,70,1,1,1,156, +142,156,163,163,27,70,125,125,89,125,89,70,125,89,107,107,89,142,156,142, +70,1,107,125,89,107,107,125,89,125,125,89,125,125,125,125,107,125,1,107, +89,125,89,125,125,125,125,125,1,27,1,156,24,156,142,70,142,70,142,107, +107,107,89,1,89,89,142,156,142,107,107,142,142,107,142,142,142,142,89,142, +1,1,1,163,107,107,107,107,107,107,107,107,107,107,107,107,107,107,107,107, +107,107,107,107,107,107,107,107,107,107,107,107,107,107,107,107,107,13,70,1, +107,142,107,1,27,156,89,142,156,156,163,156,163,156,142,156,156,156,70,27, +163,8,156,156,156,89,89,89,89,27,27,49,27,27,27,107,27,27,27,49, +49,27,49,49,49,107,27,1,1,1,1,1,156,1,27,27,27,1,27,107, +49,49,49,49,49,70,49,142,107,49,49,49,49,70,70,89,89,49,89,49, +70,70,70,70,142,70,70,70,70,70,1,1,1, }; +static unsigned short stb__consolas_24_latin1_a[224]={ 211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211,211,211,211,211,211,211,211,211, +211,211,211,211,211,211,211,211, }; + +// Call this function with +// font: NULL or array length +// data: NULL or specified size +// height: STB_FONT_consolas_24_latin1_BITMAP_HEIGHT or STB_FONT_consolas_24_latin1_BITMAP_HEIGHT_POW2 +// return value: spacing between lines +static void stb_font_consolas_24_latin1(stb_fontchar font[STB_FONT_consolas_24_latin1_NUM_CHARS], + unsigned char data[STB_FONT_consolas_24_latin1_BITMAP_HEIGHT][STB_FONT_consolas_24_latin1_BITMAP_WIDTH], + int height) +{ + int i,j; + if (data != 0) { + unsigned int *bits = stb__consolas_24_latin1_pixels; + unsigned int bitpack = *bits++, numbits = 32; + for (i=0; i < STB_FONT_consolas_24_latin1_BITMAP_WIDTH*height; ++i) + data[0][i] = 0; // zero entire bitmap + for (j=1; j < STB_FONT_consolas_24_latin1_BITMAP_HEIGHT-1; ++j) { + for (i=1; i < STB_FONT_consolas_24_latin1_BITMAP_WIDTH-1; ++i) { + unsigned int value; + if (numbits==0) bitpack = *bits++, numbits=32; + value = bitpack & 1; + bitpack >>= 1, --numbits; + if (value) { + if (numbits < 3) bitpack = *bits++, numbits = 32; + data[j][i] = (bitpack & 7) * 0x20 + 0x1f; + bitpack >>= 3, numbits -= 3; + } else { + data[j][i] = 0; + } + } + } + } + + // build font description + if (font != 0) { + float recip_width = 1.0f / STB_FONT_consolas_24_latin1_BITMAP_WIDTH; + float recip_height = 1.0f / height; + for (i=0; i < STB_FONT_consolas_24_latin1_NUM_CHARS; ++i) { + // pad characters so they bilerp from empty space around each character + font[i].s0 = (stb__consolas_24_latin1_s[i]) * recip_width; + font[i].t0 = (stb__consolas_24_latin1_t[i]) * recip_height; + font[i].s1 = (stb__consolas_24_latin1_s[i] + stb__consolas_24_latin1_w[i]) * recip_width; + font[i].t1 = (stb__consolas_24_latin1_t[i] + stb__consolas_24_latin1_h[i]) * recip_height; + font[i].x0 = stb__consolas_24_latin1_x[i]; + font[i].y0 = stb__consolas_24_latin1_y[i]; + font[i].x1 = stb__consolas_24_latin1_x[i] + stb__consolas_24_latin1_w[i]; + font[i].y1 = stb__consolas_24_latin1_y[i] + stb__consolas_24_latin1_h[i]; + font[i].advance_int = (stb__consolas_24_latin1_a[i]+8)>>4; + font[i].s0f = (stb__consolas_24_latin1_s[i] - 0.5f) * recip_width; + font[i].t0f = (stb__consolas_24_latin1_t[i] - 0.5f) * recip_height; + font[i].s1f = (stb__consolas_24_latin1_s[i] + stb__consolas_24_latin1_w[i] + 0.5f) * recip_width; + font[i].t1f = (stb__consolas_24_latin1_t[i] + stb__consolas_24_latin1_h[i] + 0.5f) * recip_height; + font[i].x0f = stb__consolas_24_latin1_x[i] - 0.5f; + font[i].y0f = stb__consolas_24_latin1_y[i] - 0.5f; + font[i].x1f = stb__consolas_24_latin1_x[i] + stb__consolas_24_latin1_w[i] + 0.5f; + font[i].y1f = stb__consolas_24_latin1_y[i] + stb__consolas_24_latin1_h[i] + 0.5f; + font[i].advance = stb__consolas_24_latin1_a[i]/16.0f; + } + } +} + +#ifndef STB_SOMEFONT_CREATE +#define STB_SOMEFONT_CREATE stb_font_consolas_24_latin1 +#define STB_SOMEFONT_BITMAP_WIDTH STB_FONT_consolas_24_latin1_BITMAP_WIDTH +#define STB_SOMEFONT_BITMAP_HEIGHT STB_FONT_consolas_24_latin1_BITMAP_HEIGHT +#define STB_SOMEFONT_BITMAP_HEIGHT_POW2 STB_FONT_consolas_24_latin1_BITMAP_HEIGHT_POW2 +#define STB_SOMEFONT_FIRST_CHAR STB_FONT_consolas_24_latin1_FIRST_CHAR +#define STB_SOMEFONT_NUM_CHARS STB_FONT_consolas_24_latin1_NUM_CHARS +#define STB_SOMEFONT_LINE_SPACING STB_FONT_consolas_24_latin1_LINE_SPACING +#endif + From 6c14ef3392dbdbef2ffa577dabf567f22494d6c6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 12 Jul 2016 11:56:57 -0700 Subject: [PATCH 1047/1237] added a comment --- interface/src/avatar/AvatarActionHold.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 32905361dd..a2130240f5 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -235,7 +235,7 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { } measuredLinearVelocity += _measuredLinearVelocities[i]; } - measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 3); + measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 3); // 3 because of the 3 we skipped, above if (_kinematicSetVelocity) { // rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); From 0dd2171b759c1c135485501c32d1b80ada52354b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 12 Jul 2016 11:57:57 -0700 Subject: [PATCH 1048/1237] remove commented-out code --- interface/src/avatar/AvatarActionHold.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index a2130240f5..bac3b1e02f 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -238,7 +238,6 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { measuredLinearVelocity /= (float)(AvatarActionHold::velocitySmoothFrames - 3); // 3 because of the 3 we skipped, above if (_kinematicSetVelocity) { - // rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget)); rigidBody->setLinearVelocity(glmToBullet(measuredLinearVelocity)); rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget)); } From 3955e03aa2fa5a770e034c3729477222cc77b2e8 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 12 Jul 2016 11:38:30 -0700 Subject: [PATCH 1049/1237] Fix for build error doh! --- libraries/audio-client/src/AudioClient.cpp | 4 ++-- libraries/audio-client/src/AudioClient.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 3178a53e94..cb53e8f5de 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -102,13 +102,13 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), + _audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_receivedAudioStream, this), _stats(&_receivedAudioStream), _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), - _orientationGetter(DEFAULT_ORIENTATION_GETTER), - _audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO) + _orientationGetter(DEFAULT_ORIENTATION_GETTER) { // clear the array of locally injected samples memset(_localProceduralSamples, 0, AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3630358771..49c3a40627 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -210,7 +210,7 @@ protected: private: void outputFormatChanged(); - void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer); + void mixLocalAudioInjectors(int16_t* inputBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(const glm::vec3& relativePosition, float volume); From cde17ecaab7b3e886b5f1caa6b7b40b1b3fa95f0 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 12 Jul 2016 14:34:54 -0700 Subject: [PATCH 1050/1237] Clenaing the rendering and the framebuffer allocations --- libraries/gpu/src/gpu/Format.cpp | 1 + libraries/gpu/src/gpu/Format.h | 1 + .../render-utils/src/DebugDeferredBuffer.cpp | 11 +- .../render-utils/src/DebugDeferredBuffer.h | 3 +- .../render-utils/src/DeferredFramebuffer.cpp | 69 ++------ .../render-utils/src/DeferredFramebuffer.h | 27 +-- .../src/DeferredLightingEffect.cpp | 15 +- .../render-utils/src/DeferredLightingEffect.h | 5 +- .../render-utils/src/FramebufferCache.cpp | 160 +----------------- libraries/render-utils/src/FramebufferCache.h | 48 +----- .../render-utils/src/RenderDeferredTask.cpp | 27 ++- .../render-utils/src/SubsurfaceScattering.cpp | 16 +- .../render-utils/src/SubsurfaceScattering.h | 3 +- .../render-utils/src/SurfaceGeometryPass.cpp | 101 +++++++++-- .../render-utils/src/SurfaceGeometryPass.h | 39 ++++- libraries/render/src/render/Task.cpp | 14 -- libraries/render/src/render/Task.h | 45 +++-- .../utilities/render/surfaceGeometryPass.qml | 10 +- 18 files changed, 234 insertions(+), 361 deletions(-) diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index b7a8380f78..83b2a59a73 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -12,6 +12,7 @@ using namespace gpu; const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA }; const Element Element::COLOR_SRGBA_32{ VEC4, NUINT8, SRGBA }; +const Element Element::COLOR_R11G11B10{ SCALAR, FLOAT, R11G11B10 }; const Element Element::VEC4F_COLOR_RGBA{ VEC4, FLOAT, RGBA }; const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; const Element Element::VEC2F_XY{ VEC2, FLOAT, XY }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 6b2bc4b93e..bec218d1fd 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -246,6 +246,7 @@ public: static const Element COLOR_RGBA_32; static const Element COLOR_SRGBA_32; + static const Element COLOR_R11G11B10; static const Element VEC4F_COLOR_RGBA; static const Element VEC2F_UV; static const Element VEC2F_XY; diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index a1ad480170..8c12beeaad 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -100,7 +100,7 @@ static const std::string DEFAULT_EMISSIVE_SHADER{ static const std::string DEFAULT_UNLIT_SHADER{ "vec4 getFragmentColor() {" " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" - " return (frag.mode == FRAG_MODE_UNLIT ? vec4(pow(frag.diffuse, vec3(1.0 / 2.2)), 1.0) : vec4(vec3(0.0), 1.0));" + " return (frag.mode == FRAG_MODE_UNLIT ? vec4(pow(frag.albedo, vec3(1.0 / 2.2)), 1.0) : vec4(vec3(0.0), 1.0));" " }" }; @@ -352,8 +352,8 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren RenderArgs* args = renderContext->args; auto& deferredFramebuffer = inputs.get0(); - auto& diffusedCurvatureFramebuffer = inputs.get1(); - auto& scatteringFramebuffer = inputs.get2(); + auto& surfaceGeometryFramebuffer = inputs.get1(); + auto& diffusedCurvatureFramebuffer = inputs.get2(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); @@ -383,10 +383,9 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setResourceTexture(Depth, deferredFramebuffer->getPrimaryDepthTexture()); batch.setResourceTexture(Lighting, deferredFramebuffer->getLightingTexture()); batch.setResourceTexture(Shadow, lightStage.lights[0]->shadow.framebuffer->getDepthStencilBuffer()); - batch.setResourceTexture(Pyramid, framebufferCache->getDepthPyramidTexture()); - batch.setResourceTexture(Curvature, framebufferCache->getCurvatureTexture()); + batch.setResourceTexture(Pyramid, surfaceGeometryFramebuffer->getLinearDepthTexture()); + batch.setResourceTexture(Curvature, surfaceGeometryFramebuffer->getCurvatureTexture()); batch.setResourceTexture(DiffusedCurvature, diffusedCurvatureFramebuffer->getRenderBuffer(0)); - batch.setResourceTexture(Scattering, scatteringFramebuffer->getRenderBuffer(0)); if (DependencyManager::get()->isAmbientOcclusionEnabled()) { batch.setResourceTexture(AmbientOcclusion, framebufferCache->getOcclusionTexture()); } else { diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 09e0380e46..10f7dad330 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -16,6 +16,7 @@ #include #include "DeferredFramebuffer.h" +#include "SurfaceGeometryPass.h" class DebugDeferredBufferConfig : public render::Job::Config { Q_OBJECT @@ -35,7 +36,7 @@ signals: class DebugDeferredBuffer { public: - using Inputs = render::VaryingSet3; + using Inputs = render::VaryingSet3; using Config = DebugDeferredBufferConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index 190bfbfab3..32f91f83d8 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -15,34 +15,34 @@ DeferredFramebuffer::DeferredFramebuffer() { } -void DeferredFramebuffer::setPrimaryDepth(const gpu::TexturePointer& depthBuffer) { - //If the size changed, we need to delete our FBOs - if (_primaryDepthTexture != depthBuffer) { +void DeferredFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& depthBuffer) { + //If the depth buffer or size changed, we need to delete our FBOs + bool reset = false; + if ((_primaryDepthTexture != depthBuffer)) { _primaryDepthTexture = depthBuffer; - + reset = true; + } + if (_primaryDepthTexture) { + auto newFrameSize = glm::ivec2(_primaryDepthTexture->getDimensions()); + if (_frameSize != newFrameSize) { + _frameSize = newFrameSize; + reset = true; + } } -} -void DeferredFramebuffer::setFrameSize(const glm::ivec2& frameBufferSize) { - //If the size changed, we need to delete our FBOs - if (_frameSize != frameBufferSize) { - _frameSize = frameBufferSize; - _deferredFramebuffer.reset(); + if (reset) { + _deferredFramebuffer.reset(); _deferredFramebufferDepthColor.reset(); _deferredColorTexture.reset(); _deferredNormalTexture.reset(); _deferredSpecularTexture.reset(); _lightingTexture.reset(); _lightingFramebuffer.reset(); - - _occlusionFramebuffer.reset(); - _occlusionTexture.reset(); - _occlusionBlurredFramebuffer.reset(); - _occlusionBlurredTexture.reset(); } } void DeferredFramebuffer::allocate() { + _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create()); @@ -52,9 +52,6 @@ void DeferredFramebuffer::allocate() { auto height = _frameSize.y; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - // _primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - - // _primaryFramebuffer->setRenderBuffer(0, _primaryColorTexture); _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); @@ -67,8 +64,6 @@ void DeferredFramebuffer::allocate() { _deferredFramebufferDepthColor->setRenderBuffer(0, _deferredColorTexture); - // auto depthFormat = gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); - auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); @@ -88,42 +83,8 @@ void DeferredFramebuffer::allocate() { _deferredFramebuffer->setRenderBuffer(3, _lightingTexture); - // For AO: - // auto pointMipSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT); - // _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - // _depthPyramidFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - // _depthPyramidFramebuffer->setRenderBuffer(0, _depthPyramidTexture); - // _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - - // resizeAmbientOcclusionBuffers(); } -/* -void DeferredFramebuffer::resizeAmbientOcclusionBuffers() { - _occlusionFramebuffer.reset(); - _occlusionTexture.reset(); - _occlusionBlurredFramebuffer.reset(); - _occlusionBlurredTexture.reset(); - - - auto width = _frameBufferSize.width() >> _AOResolutionLevel; - auto height = _frameBufferSize.height() >> _AOResolutionLevel; - auto colorFormat = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGB); - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); - auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - - _occlusionTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); - _occlusionFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - - _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); - _occlusionBlurredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); -} -*/ - gpu::TexturePointer DeferredFramebuffer::getPrimaryDepthTexture() { if (!_primaryDepthTexture) { diff --git a/libraries/render-utils/src/DeferredFramebuffer.h b/libraries/render-utils/src/DeferredFramebuffer.h index 346ea3ee28..6c83fbb91f 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.h +++ b/libraries/render-utils/src/DeferredFramebuffer.h @@ -25,32 +25,18 @@ public: gpu::FramebufferPointer getDeferredFramebuffer(); gpu::FramebufferPointer getDeferredFramebufferDepthColor(); - gpu::TexturePointer getPrimaryDepthTexture(); - gpu::TexturePointer getDeferredColorTexture(); gpu::TexturePointer getDeferredNormalTexture(); gpu::TexturePointer getDeferredSpecularTexture(); - gpu::FramebufferPointer getDepthPyramidFramebuffer(); - gpu::TexturePointer getDepthPyramidTexture(); - - - void setAmbientOcclusionResolutionLevel(int level); - gpu::FramebufferPointer getOcclusionFramebuffer(); - gpu::TexturePointer getOcclusionTexture(); - gpu::FramebufferPointer getOcclusionBlurredFramebuffer(); - gpu::TexturePointer getOcclusionBlurredTexture(); - - gpu::FramebufferPointer getLightingFramebuffer(); gpu::TexturePointer getLightingTexture(); - void setPrimaryDepth(const gpu::TexturePointer& depthBuffer); - - void setFrameSize(const glm::ivec2& size); + // Update the depth buffer which will drive the allocation of all the other resources according to its size. + void updatePrimaryDepth(const gpu::TexturePointer& depthBuffer); + gpu::TexturePointer getPrimaryDepthTexture(); const glm::ivec2& getFrameSize() const { return _frameSize; } - protected: void allocate(); @@ -66,14 +52,7 @@ protected: gpu::TexturePointer _lightingTexture; gpu::FramebufferPointer _lightingFramebuffer; - gpu::FramebufferPointer _occlusionFramebuffer; - gpu::TexturePointer _occlusionTexture; - - gpu::FramebufferPointer _occlusionBlurredFramebuffer; - gpu::TexturePointer _occlusionBlurredTexture; - glm::ivec2 _frameSize; - }; using DeferredFramebufferPointer = std::shared_ptr; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 12408f121f..b15b980e51 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -361,8 +361,7 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC if (!_deferredFramebuffer) { _deferredFramebuffer = std::make_shared(); } - _deferredFramebuffer->setPrimaryDepth(primaryFramebuffer->getDepthStencilBuffer()); - _deferredFramebuffer->setFrameSize(glm::ivec2(args->_viewport.z, args->_viewport.w)); + _deferredFramebuffer->updatePrimaryDepth(primaryFramebuffer->getDepthStencilBuffer()); output.edit0() = _deferredFramebuffer; output.edit1() = _deferredFramebuffer->getLightingFramebuffer(); @@ -398,7 +397,8 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c const DeferredFrameTransformPointer& frameTransform, const DeferredFramebufferPointer& deferredFramebuffer, const LightingModelPointer& lightingModel, - const gpu::TexturePointer& diffusedCurvature2, + const SurfaceGeometryFramebufferPointer& surfaceGeometryFramebuffer, + const gpu::TexturePointer& lowCurvatureNormal, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource) { auto args = renderContext->args; @@ -442,8 +442,8 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, lightingModel->getParametersBuffer()); // Subsurface scattering specific - batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, framebufferCache->getCurvatureTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, diffusedCurvature2); + batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, surfaceGeometryFramebuffer->getCurvatureTexture()); + batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, lowCurvatureNormal); batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, subsurfaceScatteringResource->getParametersBuffer()); @@ -673,10 +673,11 @@ void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderCo auto deferredTransform = inputs.get0(); auto deferredFramebuffer = inputs.get1(); auto lightingModel = inputs.get2(); - auto diffusedCurvature2 = inputs.get3()->getRenderBuffer(0); + auto surfaceGeometryFramebuffer = inputs.get3(); + auto lowCurvatureNormal = inputs.get4()->getRenderBuffer(0); auto subsurfaceScatteringResource = inputs.get5(); - setupJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel, diffusedCurvature2, subsurfaceScatteringResource); + setupJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel, surfaceGeometryFramebuffer, lowCurvatureNormal, subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform, lightingModel->isPointLightEnabled(), lightingModel->isSpotLightEnabled()); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index b850cce040..d520ddc098 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -28,7 +28,7 @@ #include "LightingModel.h" #include "LightStage.h" - +#include "SurfaceGeometryPass.h" #include "SubsurfaceScattering.h" class RenderArgs; @@ -135,6 +135,7 @@ public: const DeferredFrameTransformPointer& frameTransform, const DeferredFramebufferPointer& deferredFramebuffer, const LightingModelPointer& lightingModel, + const SurfaceGeometryFramebufferPointer& surfaceGeometryFramebuffer, const gpu::TexturePointer& diffusedCurvature2, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource); }; @@ -167,7 +168,7 @@ signals: class RenderDeferred { public: - using Inputs = render::VaryingSet6 < DeferredFrameTransformPointer, DeferredFramebufferPointer, LightingModelPointer, gpu::FramebufferPointer, gpu::FramebufferPointer, SubsurfaceScatteringResourcePointer>; + using Inputs = render::VaryingSet6 < DeferredFrameTransformPointer, DeferredFramebufferPointer, LightingModelPointer, SurfaceGeometryFramebufferPointer, gpu::FramebufferPointer, SubsurfaceScatteringResourcePointer>; using Config = RenderDeferredConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 4c9d02ca41..c8308ce3f0 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -33,22 +33,8 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; - /* _primaryFramebuffer.reset(); - _primaryDepthTexture.reset(); - _primaryColorTexture.reset(); - _deferredFramebuffer.reset(); - _deferredFramebufferDepthColor.reset(); - _deferredColorTexture.reset(); - _deferredNormalTexture.reset(); - _deferredSpecularTexture.reset(); - */ _selfieFramebuffer.reset(); + _selfieFramebuffer.reset(); _cachedFramebuffers.clear(); - /* _lightingTexture.reset(); - _lightingFramebuffer.reset(); - */ _depthPyramidFramebuffer.reset(); - _depthPyramidTexture.reset(); - _curvatureFramebuffer.reset(); - _curvatureTexture.reset(); _occlusionFramebuffer.reset(); _occlusionTexture.reset(); _occlusionBlurredFramebuffer.reset(); @@ -57,66 +43,21 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { - /* _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _deferredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _deferredFramebufferDepthColor = gpu::FramebufferPointer(gpu::Framebuffer::create()); - */ - auto colorFormat = gpu::Element::COLOR_SRGBA_32; + auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto linearFormat = gpu::Element::COLOR_RGBA_32; auto width = _frameBufferSize.width(); auto height = _frameBufferSize.height(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - /* _primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - - _primaryFramebuffer->setRenderBuffer(0, _primaryColorTexture); - - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - - _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); - _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); - _deferredFramebuffer->setRenderBuffer(2, _deferredSpecularTexture); - - _deferredFramebufferDepthColor->setRenderBuffer(0, _deferredColorTexture); - */ - // auto depthFormat = gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format -/* _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); - _primaryFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - - _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - - _deferredFramebufferDepthColor->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - -*/ _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); _selfieFramebuffer->setRenderBuffer(0, tex); auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - /* _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); - _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); - _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - _deferredFramebuffer->setRenderBuffer(3, _lightingTexture); - */ - // For AO: - auto pointMipSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT); - _depthPyramidTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _depthPyramidFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _depthPyramidFramebuffer->setRenderBuffer(0, _depthPyramidTexture); - // _depthPyramidFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); - - _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - // _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); resizeAmbientOcclusionBuffers(); } @@ -145,76 +86,7 @@ void FramebufferCache::resizeAmbientOcclusionBuffers() { _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); // _occlusionBlurredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); } -/* -gpu::FramebufferPointer FramebufferCache::getPrimaryFramebuffer() { - if (!_primaryFramebuffer) { - createPrimaryFramebuffer(); - } - return _primaryFramebuffer; -} -gpu::TexturePointer FramebufferCache::getPrimaryDepthTexture() { - if (!_primaryDepthTexture) { - createPrimaryFramebuffer(); - } - return _primaryDepthTexture; -} - -gpu::TexturePointer FramebufferCache::getPrimaryColorTexture() { - if (!_primaryColorTexture) { - createPrimaryFramebuffer(); - } - return _primaryColorTexture; -} - -gpu::FramebufferPointer FramebufferCache::getDeferredFramebuffer() { - if (!_deferredFramebuffer) { - createPrimaryFramebuffer(); - } - return _deferredFramebuffer; -} - -gpu::FramebufferPointer FramebufferCache::getDeferredFramebufferDepthColor() { - if (!_deferredFramebufferDepthColor) { - createPrimaryFramebuffer(); - } - return _deferredFramebufferDepthColor; -} - -gpu::TexturePointer FramebufferCache::getDeferredColorTexture() { - if (!_deferredColorTexture) { - createPrimaryFramebuffer(); - } - return _deferredColorTexture; -} - -gpu::TexturePointer FramebufferCache::getDeferredNormalTexture() { - if (!_deferredNormalTexture) { - createPrimaryFramebuffer(); - } - return _deferredNormalTexture; -} - -gpu::TexturePointer FramebufferCache::getDeferredSpecularTexture() { - if (!_deferredSpecularTexture) { - createPrimaryFramebuffer(); - } - return _deferredSpecularTexture; -} - -gpu::FramebufferPointer FramebufferCache::getLightingFramebuffer() { - if (!_lightingFramebuffer) { - createPrimaryFramebuffer(); - } - return _lightingFramebuffer; -} - -gpu::TexturePointer FramebufferCache::getLightingTexture() { - if (!_lightingTexture) { - createPrimaryFramebuffer(); - } - return _lightingTexture; -}*/ gpu::FramebufferPointer FramebufferCache::getFramebuffer() { if (_cachedFramebuffers.isEmpty()) { @@ -238,34 +110,6 @@ gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() { return _selfieFramebuffer; } -gpu::FramebufferPointer FramebufferCache::getDepthPyramidFramebuffer() { - if (!_depthPyramidFramebuffer) { - createPrimaryFramebuffer(); - } - return _depthPyramidFramebuffer; -} - -gpu::TexturePointer FramebufferCache::getDepthPyramidTexture() { - if (!_depthPyramidTexture) { - createPrimaryFramebuffer(); - } - return _depthPyramidTexture; -} - -gpu::FramebufferPointer FramebufferCache::getCurvatureFramebuffer() { - if (!_curvatureFramebuffer) { - createPrimaryFramebuffer(); - } - return _curvatureFramebuffer; -} - -gpu::TexturePointer FramebufferCache::getCurvatureTexture() { - if (!_curvatureTexture) { - createPrimaryFramebuffer(); - } - return _curvatureTexture; -} - void FramebufferCache::setAmbientOcclusionResolutionLevel(int level) { const int MAX_AO_RESOLUTION_LEVEL = 4; level = std::max(0, std::min(level, MAX_AO_RESOLUTION_LEVEL)); diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index 9f3b8e504b..d3d26c35b0 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -30,36 +30,12 @@ public: void setFrameBufferSize(QSize frameBufferSize); const QSize& getFrameBufferSize() const { return _frameBufferSize; } - /// Returns a pointer to the primary framebuffer object. This render target includes a depth component, and is - /// used for scene rendering. -/* gpu::FramebufferPointer getPrimaryFramebuffer(); - - gpu::TexturePointer getPrimaryDepthTexture(); - gpu::TexturePointer getPrimaryColorTexture(); - - gpu::FramebufferPointer getDeferredFramebuffer(); - gpu::FramebufferPointer getDeferredFramebufferDepthColor(); - - gpu::TexturePointer getDeferredColorTexture(); - gpu::TexturePointer getDeferredNormalTexture(); - gpu::TexturePointer getDeferredSpecularTexture(); - */ - gpu::FramebufferPointer getDepthPyramidFramebuffer(); - gpu::TexturePointer getDepthPyramidTexture(); - - gpu::FramebufferPointer getCurvatureFramebuffer(); - gpu::TexturePointer getCurvatureTexture(); - void setAmbientOcclusionResolutionLevel(int level); gpu::FramebufferPointer getOcclusionFramebuffer(); gpu::TexturePointer getOcclusionTexture(); gpu::FramebufferPointer getOcclusionBlurredFramebuffer(); gpu::TexturePointer getOcclusionBlurredTexture(); - - /** - gpu::TexturePointer getLightingTexture(); - gpu::FramebufferPointer getLightingFramebuffer(); - */ + /// Returns the framebuffer object used to render selfie maps; gpu::FramebufferPointer getSelfieFramebuffer(); @@ -76,32 +52,10 @@ private: void createPrimaryFramebuffer(); - gpu::FramebufferPointer _primaryFramebuffer; - - gpu::TexturePointer _primaryDepthTexture; - gpu::TexturePointer _primaryColorTexture; - - gpu::FramebufferPointer _deferredFramebuffer; - gpu::FramebufferPointer _deferredFramebufferDepthColor; - - gpu::TexturePointer _deferredColorTexture; - gpu::TexturePointer _deferredNormalTexture; - gpu::TexturePointer _deferredSpecularTexture; - - gpu::TexturePointer _lightingTexture; - gpu::FramebufferPointer _lightingFramebuffer; - gpu::FramebufferPointer _shadowFramebuffer; gpu::FramebufferPointer _selfieFramebuffer; - gpu::FramebufferPointer _depthPyramidFramebuffer; - gpu::TexturePointer _depthPyramidTexture; - - - gpu::FramebufferPointer _curvatureFramebuffer; - gpu::TexturePointer _curvatureTexture; - gpu::FramebufferPointer _occlusionFramebuffer; gpu::TexturePointer _occlusionTexture; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 77d23d10a1..3fee16f2c3 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -110,11 +110,15 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Opaque all rendered, generate surface geometry buffers const auto surfaceGeometryPassInputs = render::Varying(SurfaceGeometryPass::Inputs(deferredFrameTransform, deferredFramebuffer)); - const auto curvatureFramebufferAndDepth = addJob("SurfaceGeometry", surfaceGeometryPassInputs); + const auto geometryFramebufferAndCurvatureFramebufferAndDepth = addJob("SurfaceGeometry", surfaceGeometryPassInputs); + const auto surfaceGeometryFramebuffer = geometryFramebufferAndCurvatureFramebufferAndDepth.getN(0); + const auto curvatureFramebuffer = geometryFramebufferAndCurvatureFramebufferAndDepth.getN(1); + const auto linearDepthTexture = geometryFramebufferAndCurvatureFramebufferAndDepth.getN(2); + const auto curvatureFramebufferAndDepth = render::Varying(BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture)); - const auto curvatureFramebuffer = addJob("DiffuseCurvature", curvatureFramebufferAndDepth); - const auto diffusedCurvatureFramebuffer = addJob("DiffuseCurvature2", curvatureFramebufferAndDepth, true); + const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", curvatureFramebufferAndDepth); + const auto lowCurvatureNormalFramebuffer = addJob("DiffuseCurvatureLow", curvatureFramebufferAndDepth, true); const auto scatteringResource = addJob("Scattering"); @@ -125,12 +129,12 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DrawLight", lights); const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, deferredFramebuffer, lightingModel, - curvatureFramebuffer, diffusedCurvatureFramebuffer, scatteringResource)); - + surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer, scatteringResource)); + // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredLightingInputs); - // Use Stencil and draw background in Lighting buffer + // Use Stencil and draw background in Lighting buffer to complete filling in the opaque addJob("DrawBackgroundDeferred", background); // Render transparent objects forward in LightingBuffer @@ -150,7 +154,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DebugScattering", deferredLightingInputs); // Debugging Deferred buffer job - const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, diffusedCurvatureFramebuffer, curvatureFramebuffer)); + const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer)); addJob("DebugDeferredBuffer", debugFramebuffers); // Scene Octree Debuging job @@ -319,7 +323,6 @@ void DrawStencilDeferred::run(const SceneContextPointer& sceneContext, const Ren doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; - // auto deferredFboColorDepthStencil = DependencyManager::get()->getDeferredFramebufferDepthColor(); auto deferredFboColorDepthStencil = deferredFramebuffer->getDeferredFramebufferDepthColor(); @@ -347,12 +350,8 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const args->_batch = &batch; _gpuTimer.begin(batch); - // auto lightingFBO = DependencyManager::get()->getLightingFramebuffer(); - batch.enableSkybox(true); - - // batch.setFramebuffer(lightingFBO); - + batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); @@ -388,8 +387,6 @@ void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPoint int height = renderArgs->_viewport.w; // Blit primary to blit FBO - // auto framebufferCache = DependencyManager::get(); - // auto primaryFbo = framebufferCache->getPrimaryFramebuffer(); auto primaryFbo = srcFramebuffer; gpu::doInBatch(renderArgs->_context, [&](gpu::Batch& batch) { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 2e8f81cfaf..5ed2b49dec 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -412,7 +412,9 @@ void computeSpecularBeckmannGPU(gpu::TexturePointer& beckmannMap, RenderArgs* ar gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(RenderArgs* args) { const int PROFILE_RESOLUTION = 512; - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; + const auto pixelFormat = gpu::Element::COLOR_R11G11B10; + auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); diffuseProfileGPU(profileMap, args); return profileMap; } @@ -420,7 +422,9 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScattering(const gpu::TexturePointer& profile, RenderArgs* args) { const int TABLE_RESOLUTION = 512; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_SRGBA_32, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; + const auto pixelFormat = gpu::Element::COLOR_R11G11B10; + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); diffuseScatterGPU(profile, scatteringLUT, args); return scatteringLUT; @@ -510,7 +514,10 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont auto& frameTransform = inputs.get0(); auto& deferredFramebuffer = inputs.get1(); - auto& curvatureFramebuffer = inputs.get3(); + auto& surfaceGeometryFramebuffer = inputs.get3(); + auto& curvatureFramebuffer = surfaceGeometryFramebuffer->getCurvatureFramebuffer(); + auto& linearDepthTexture = surfaceGeometryFramebuffer->getLinearDepthTexture(); + auto& diffusedFramebuffer = inputs.get4(); auto& scatteringResource = inputs.get5(); @@ -521,7 +528,6 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont auto scatteringTable = scatteringResource->getScatteringTable(); auto scatteringSpecular = scatteringResource->getScatteringSpecular(); - auto framebufferCache = DependencyManager::get(); @@ -567,7 +573,7 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont batch.setResourceTexture(ScatteringTask_DiffusedCurvatureMapSlot, diffusedFramebuffer->getRenderBuffer(0)); batch.setResourceTexture(ScatteringTask_NormalMapSlot, deferredFramebuffer->getDeferredNormalTexture()); batch.setResourceTexture(ScatteringTask_AlbedoMapSlot, deferredFramebuffer->getDeferredColorTexture()); - batch.setResourceTexture(ScatteringTask_LinearMapSlot, framebufferCache->getDepthPyramidTexture()); + batch.setResourceTexture(ScatteringTask_LinearMapSlot, linearDepthTexture); batch._glUniform2f(debugScatteringPipeline->getProgram()->getUniforms().findLocation("uniformCursorTexcoord"), _debugCursorTexcoord.x, _debugCursorTexcoord.y); diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 28e884b552..715d9bc77b 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -17,6 +17,7 @@ #include "render/DrawTask.h" #include "DeferredFrameTransform.h" #include "DeferredFramebuffer.h" +#include "SurfaceGeometryPass.h" #include "LightingModel.h" class SubsurfaceScatteringResource { @@ -162,7 +163,7 @@ signals: class DebugSubsurfaceScattering { public: - using Inputs = render::VaryingSet6; + using Inputs = render::VaryingSet6; using Config = DebugSubsurfaceScatteringConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index b60ed21f7b..dddc8a1c53 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -25,6 +25,82 @@ const int SurfaceGeometryPass_NormalMapSlot = 1; #include "surfaceGeometry_makeCurvature_frag.h" + +SurfaceGeometryFramebuffer::SurfaceGeometryFramebuffer() { +} + + +void SurfaceGeometryFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& depthBuffer) { + //If the depth buffer or size changed, we need to delete our FBOs + bool reset = false; + if ((_primaryDepthTexture != depthBuffer)) { + _primaryDepthTexture = depthBuffer; + reset = true; + } + if (_primaryDepthTexture) { + auto newFrameSize = glm::ivec2(_primaryDepthTexture->getDimensions()); + if (_frameSize != newFrameSize) { + _frameSize = newFrameSize; + reset = true; + } + } + + if (reset) { + _linearDepthFramebuffer.reset(); + _linearDepthTexture.reset(); + _curvatureFramebuffer.reset(); + _curvatureTexture.reset(); + + } +} + +void SurfaceGeometryFramebuffer::allocate() { + + auto width = _frameSize.x; + auto height = _frameSize.y; + + // For Linear Depth: + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); + // _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + + _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); + // _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); +} + +gpu::FramebufferPointer SurfaceGeometryFramebuffer::getLinearDepthFramebuffer() { + if (!_linearDepthFramebuffer) { + allocate(); + } + return _linearDepthFramebuffer; +} + +gpu::TexturePointer SurfaceGeometryFramebuffer::getLinearDepthTexture() { + if (!_linearDepthTexture) { + allocate(); + } + return _linearDepthTexture; +} + +gpu::FramebufferPointer SurfaceGeometryFramebuffer::getCurvatureFramebuffer() { + if (!_curvatureFramebuffer) { + allocate(); + } + return _curvatureFramebuffer; +} + +gpu::TexturePointer SurfaceGeometryFramebuffer::getCurvatureTexture() { + if (!_curvatureTexture) { + allocate(); + } + return _curvatureTexture; +} + + + SurfaceGeometryPass::SurfaceGeometryPass() { Parameters parameters; _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); @@ -54,18 +130,24 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c const auto frameTransform = inputs.get0(); const auto deferredFramebuffer = inputs.get1(); + if (!_surfaceGeometryFramebuffer) { + _surfaceGeometryFramebuffer = std::make_shared(); + } + _surfaceGeometryFramebuffer->updatePrimaryDepth(deferredFramebuffer->getPrimaryDepthTexture()); + auto framebufferCache = DependencyManager::get(); auto depthBuffer = deferredFramebuffer->getPrimaryDepthTexture(); auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); - auto pyramidFBO = framebufferCache->getDepthPyramidFramebuffer(); - - auto pyramidTexture = framebufferCache->getDepthPyramidTexture(); - auto curvatureFBO = framebufferCache->getCurvatureFramebuffer(); - curvatureAndDepth.edit0() = curvatureFBO; - curvatureAndDepth.edit1() = pyramidTexture; + auto linearDepthFBO = _surfaceGeometryFramebuffer->getLinearDepthFramebuffer(); + auto linearDepthTexture = _surfaceGeometryFramebuffer->getLinearDepthTexture(); + auto curvatureFBO = _surfaceGeometryFramebuffer->getCurvatureFramebuffer(); + auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); + + curvatureAndDepth.edit0() = _surfaceGeometryFramebuffer; + curvatureAndDepth.edit1() = curvatureFBO; + curvatureAndDepth.edit2() = linearDepthTexture; - auto curvatureTexture = framebufferCache->getCurvatureTexture(); QSize framebufferSize = framebufferCache->getFrameBufferSize(); float sMin = args->_viewport.x / (float)framebufferSize.width(); @@ -93,7 +175,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); // Pyramid pass - batch.setFramebuffer(pyramidFBO); + batch.setFramebuffer(linearDepthFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(args->getViewFrustum().getFarClip(), 0.0f, 0.0f, 0.0f)); batch.setPipeline(linearDepthPipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, depthBuffer); @@ -103,7 +185,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setFramebuffer(curvatureFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); batch.setPipeline(curvaturePipeline); - batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, pyramidTexture); + batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, linearDepthTexture); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, normalTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); @@ -163,4 +245,3 @@ const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { return _curvaturePipeline; } - diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 2c1969a519..30fb6a53ef 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -18,6 +18,40 @@ #include "DeferredFrameTransform.h" #include "DeferredFramebuffer.h" + +// SurfaceGeometryFramebuffer is a helper class gathering in one place theframebuffers and targets describing the surface geometry linear depth and curvature generated +// from a z buffer and a normal buffer +class SurfaceGeometryFramebuffer { +public: + SurfaceGeometryFramebuffer(); + + gpu::FramebufferPointer getLinearDepthFramebuffer(); + gpu::TexturePointer getLinearDepthTexture(); + + gpu::FramebufferPointer getCurvatureFramebuffer(); + gpu::TexturePointer getCurvatureTexture(); + + // Update the depth buffer which will drive the allocation of all the other resources according to its size. + void updatePrimaryDepth(const gpu::TexturePointer& depthBuffer); + gpu::TexturePointer getPrimaryDepthTexture(); + const glm::ivec2& getFrameSize() const { return _frameSize; } + +protected: + void allocate(); + + gpu::TexturePointer _primaryDepthTexture; + + gpu::FramebufferPointer _linearDepthFramebuffer; + gpu::TexturePointer _linearDepthTexture; + + gpu::FramebufferPointer _curvatureFramebuffer; + gpu::TexturePointer _curvatureTexture; + + glm::ivec2 _frameSize; +}; + +using SurfaceGeometryFramebufferPointer = std::shared_ptr; + class SurfaceGeometryPassConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) @@ -42,7 +76,7 @@ signals: class SurfaceGeometryPass { public: using Inputs = render::VaryingSet2; - using Outputs = render::VaryingSet2; + using Outputs = render::VaryingSet3; using Config = SurfaceGeometryPassConfig; using JobModel = render::Job::ModelIO; @@ -70,12 +104,15 @@ private: }; gpu::BufferView _parametersBuffer; + SurfaceGeometryFramebufferPointer _surfaceGeometryFramebuffer; + const gpu::PipelinePointer& getLinearDepthPipeline(); const gpu::PipelinePointer& getCurvaturePipeline(); gpu::PipelinePointer _linearDepthPipeline; gpu::PipelinePointer _curvaturePipeline; + gpu::RangeTimer _gpuTimer; }; diff --git a/libraries/render/src/render/Task.cpp b/libraries/render/src/render/Task.cpp index 8727923c70..c1a917d26a 100644 --- a/libraries/render/src/render/Task.cpp +++ b/libraries/render/src/render/Task.cpp @@ -24,17 +24,3 @@ void TaskConfig::refresh() { _task->configure(*this); } - -namespace render{ - - template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var) { - if (index == 0) { - var = data.first; - } else { - var = data.second; - } - } - - template <> uint8_t varyingLength(const VaryingPairBase& data) { return 2; } - -} \ No newline at end of file diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index db9573322f..967c75fc88 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -32,10 +32,6 @@ namespace render { class Varying; - -template < class T > void varyingGet(const T& data, uint8_t index, Varying& var) {} -template uint8_t varyingLength(const T& data) { return 0; } - // A varying piece of data, to be used as Job/Task I/O // TODO: Task IO class Varying { @@ -76,10 +72,9 @@ protected: virtual Varying operator[] (uint8_t index) const { Varying var; - varyingGet< T >(_data, index, var); return var; } - virtual uint8_t length() const { return varyingLength(_data); } + virtual uint8_t length() const { return 0; } Data _data; }; @@ -89,8 +84,6 @@ protected: using VaryingPairBase = std::pair; -template <> void varyingGet(const VaryingPairBase& data, uint8_t index, Varying& var); -template <> uint8_t varyingLength(const VaryingPairBase& data); template < typename T0, typename T1 > class VaryingSet2 : public VaryingPairBase { @@ -108,10 +101,14 @@ public: const T1& get1() const { return second.get(); } T1& edit1() { return second.edit(); } - Varying operator[] (uint8_t index) const { - if (index == 1) { return second; } else { return first; } + virtual Varying operator[] (uint8_t index) const { + if (index == 1) { + return std::get<1>((*this)); + } else { + return std::get<0>((*this)); + } } - + virtual uint8_t length() const { return 2; } }; @@ -133,6 +130,18 @@ public: const T2& get2() const { return std::get<2>((*this)).template get(); } T2& edit2() { return std::get<2>((*this)).template edit(); } + + virtual Varying operator[] (uint8_t index) const { + if (index == 2) { + return std::get<2>((*this)); + } else if (index == 1) { + return std::get<1>((*this)); + } else { + return std::get<0>((*this)); + } + } + virtual uint8_t length() const { return 3; } + }; template @@ -155,6 +164,20 @@ public: const T3& get3() const { return std::get<3>((*this)).template get(); } T3& edit3() { return std::get<3>((*this)).template edit(); } + + virtual Varying operator[] (uint8_t index) const { + if (index == 3) { + return std::get<3>((*this)); + } else if (index == 2) { + return std::get<2>((*this)); + } else if (index == 1) { + return std::get<1>((*this)); + } else { + return std::get<0>((*this)); + } + } + virtual uint8_t length() const { return 4; } + }; diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index dca84f6c1f..061402b80d 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -33,12 +33,12 @@ Column { Column{ CheckBox { - text: "Diffuse Curvature 1" + text: "Diffuse Curvature Mid" checked: true - onCheckedChanged: { Render.getConfig("DiffuseCurvature").enabled = checked } + onCheckedChanged: { Render.getConfig("DiffuseCurvatureMid").enabled = checked } } Repeater { - model: [ "Blur Scale:DiffuseCurvature:filterScale:2.0", "Blur Depth Threshold:DiffuseCurvature:depthThreshold:1.0", "Blur Scale2:DiffuseCurvature2:filterScale:2.0", "Blur Depth Threshold 2:DiffuseCurvature2:depthThreshold:1.0"] + model: [ "Blur Scale:DiffuseCurvatureMid:filterScale:2.0", "Blur Depth Threshold:DiffuseCurvatureMid:depthThreshold:1.0", "Blur Scale2:DiffuseCurvatureLow:filterScale:2.0", "Blur Depth Threshold 2:DiffuseCurvatureLow:depthThreshold:1.0"] ConfigSlider { label: qsTr(modelData.split(":")[0]) integral: false @@ -50,9 +50,9 @@ Column { } CheckBox { - text: "Diffuse Curvature 2" + text: "Diffuse Curvature Low" checked: true - onCheckedChanged: { Render.getConfig("DiffuseCurvature2").enabled = checked } + onCheckedChanged: { Render.getConfig("DiffuseCurvatureLow").enabled = checked } } } } From 6d7c8d5759562e74aa71200185df616ffe500e2c Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 12 Jul 2016 16:11:43 -0700 Subject: [PATCH 1051/1237] fix crash in audio-mixer if no plugins installed --- assignment-client/src/audio/AudioMixerClientData.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 182f443225..5c2ce8bf57 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -348,8 +348,10 @@ void AudioMixerClientData::setupCodec(CodecPluginPointer codec, const QString& c cleanupCodec(); // cleanup any previously allocated coders first _codec = codec; _selectedCodecName = codecName; - _encoder = codec->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); - _decoder = codec->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + if (codec) { + _encoder = codec->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO); + _decoder = codec->createDecoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + } auto avatarAudioStream = getAvatarAudioStream(); if (avatarAudioStream) { From 87ba1ffecabef45a7bd390ec1f4acc1ca5da996e Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Tue, 12 Jul 2016 16:05:46 -0700 Subject: [PATCH 1052/1237] Fix orientation being reset to 0,0,0,1 --- libraries/networking/src/AddressManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index df9b4094b0..7ed3888be0 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -564,10 +564,10 @@ bool AddressManager::handleViewpoint(const QString& viewpointString, bool should if (viewpointString[positionRegex.matchedLength() - 1] == QChar('/') && orientationRegex.indexIn(viewpointString, positionRegex.matchedLength() - 1) != -1) { - glm::quat newOrientation = glm::normalize(glm::quat(orientationRegex.cap(4).toFloat(), - orientationRegex.cap(1).toFloat(), - orientationRegex.cap(2).toFloat(), - orientationRegex.cap(3).toFloat())); + newOrientation = glm::normalize(glm::quat(orientationRegex.cap(4).toFloat(), + orientationRegex.cap(1).toFloat(), + orientationRegex.cap(2).toFloat(), + orientationRegex.cap(3).toFloat())); if (!isNaN(newOrientation.x) && !isNaN(newOrientation.y) && !isNaN(newOrientation.z) && !isNaN(newOrientation.w)) { From f2220288fb17e0fad0d582a290e1dc00975047c3 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Jul 2016 14:54:12 -0700 Subject: [PATCH 1053/1237] Expand frame decoration when mouse is over a window in HMD mode --- .../resources/qml/hifi/toolbars/Toolbar.qml | 14 ++- .../resources/qml/windows/Decoration.qml | 47 +++++++ .../resources/qml/windows/DefaultFrame.qml | 100 +-------------- .../qml/windows/DefaultFrameDecoration.qml | 115 ++++++++++++++++++ interface/resources/qml/windows/Frame.qml | 16 ++- interface/resources/qml/windows/ToolFrame.qml | 78 +----------- .../qml/windows/ToolFrameDecoration.qml | 98 +++++++++++++++ interface/resources/qml/windows/Window.qml | 8 ++ tests/ui/qml/main.qml | 33 ++++- 9 files changed, 325 insertions(+), 184 deletions(-) create mode 100644 interface/resources/qml/windows/Decoration.qml create mode 100644 interface/resources/qml/windows/DefaultFrameDecoration.qml create mode 100644 interface/resources/qml/windows/ToolFrameDecoration.qml diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 75c06e4199..c5c15a6406 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -21,7 +21,19 @@ Window { height: content.height visible: true // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' - activator: Item {} + activator: MouseArea { + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 + propagateComposedEvents: true + acceptedButtons: Qt.AllButtons + enabled: window.visible + hoverEnabled: true + onPressed: mouse.accepted = false; + onEntered: window.mouseEntered(); + onExited: window.mouseExited(); + } property bool horizontal: true property real buttonSize: 50; property var buttons: [] diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml new file mode 100644 index 0000000000..628a4d3370 --- /dev/null +++ b/interface/resources/qml/windows/Decoration.qml @@ -0,0 +1,47 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +Rectangle { + HifiConstants { id: hifi } + + property int frameMargin: 9 + property int frameMarginLeft: frameMargin + property int frameMarginRight: frameMargin + property int frameMarginTop: 2 * frameMargin + iconSize + property int frameMarginBottom: iconSize + 11 + + anchors { + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius + + // Enable dragging of the window, + // detect mouseover of the window (including decoration) + MouseArea { + anchors.fill: parent + drag.target: window + } +} + diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index 242209dbe0..33c2818849 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -16,104 +16,6 @@ import "../styles-uit" Frame { HifiConstants { id: hifi } - - Rectangle { - // Dialog frame - id: frameContent - - readonly property int iconSize: hifi.dimensions.frameIconSize - readonly property int frameMargin: 9 - readonly property int frameMarginLeft: frameMargin - readonly property int frameMarginRight: frameMargin - readonly property int frameMarginTop: 2 * frameMargin + iconSize - readonly property int frameMarginBottom: iconSize + 11 - - anchors { - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 - } - radius: hifi.dimensions.borderRadius - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - } - - Row { - id: controlsRow - anchors { - right: parent.right; - top: parent.top; - topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title - rightMargin: frameContent.frameMarginRight; - } - spacing: frameContent.iconSize / 4 - - HiFiGlyphs { - // "Pin" button - visible: window.pinnable - text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin - color: pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: pinClickArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - onClicked: window.pinned = !window.pinned; - } - } - - HiFiGlyphs { - // "Close" button - visible: window ? window.closable : false - text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close - color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: closeClickArea - anchors.fill: parent - hoverEnabled: true - onClicked: window.shown = false; - } - } - } - - RalewayRegular { - // Title - id: titleText - anchors { - left: parent.left - leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x - right: controlsRow.left - rightMargin: frameContent.iconSize - top: parent.top - topMargin: frameContent.frameMargin - } - text: window ? window.title : "" - color: hifi.colors.white - size: hifi.fontSizes.overlayTitle - } - - DropShadow { - source: titleText - anchors.fill: titleText - horizontalOffset: 2 - verticalOffset: 2 - samples: 2 - color: hifi.colors.baseGrayShadow60 - visible: (window && window.focus) - cached: true - } - } + DefaultFrameDecoration {} } diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml new file mode 100644 index 0000000000..ecb1760717 --- /dev/null +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -0,0 +1,115 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + + +Decoration { + HifiConstants { id: hifi } + + // Dialog frame + id: root + + property int iconSize: hifi.dimensions.frameIconSize + frameMargin: 9 + frameMarginLeft: frameMargin + frameMarginRight: frameMargin + frameMarginTop: 2 * frameMargin + iconSize + frameMarginBottom: iconSize + 11 + + Connections { + target: window + onMouseEntered: { + if (!HMD.active) { + return; + } + root.frameMargin = 18 + titleText.size = hifi.fontSizes.overlayTitle * 2 + root.iconSize = hifi.dimensions.frameIconSize * 2 + } + onMouseExited: { + root.frameMargin = 9 + titleText.size = hifi.fontSizes.overlayTitle + root.iconSize = hifi.dimensions.frameIconSize + } + } + + Row { + id: controlsRow + anchors { + right: parent.right; + top: parent.top; + topMargin: root.frameMargin + 1 // Move down a little to visually align with the title + rightMargin: root.frameMarginRight; + } + spacing: root.iconSize / 4 + + HiFiGlyphs { + // "Pin" button + visible: window.pinnable + text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin + color: pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white + size: root.iconSize + MouseArea { + id: pinClickArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + onClicked: window.pinned = !window.pinned; + } + } + + HiFiGlyphs { + // "Close" button + visible: window ? window.closable : false + text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close + color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white + size: root.iconSize + MouseArea { + id: closeClickArea + anchors.fill: parent + hoverEnabled: true + onClicked: window.shown = false; + } + } + } + + RalewayRegular { + // Title + id: titleText + anchors { + left: parent.left + leftMargin: root.frameMarginLeft + hifi.dimensions.contentMargin.x + right: controlsRow.left + rightMargin: root.iconSize + top: parent.top + topMargin: root.frameMargin + } + text: window ? window.title : "" + color: hifi.colors.white + size: hifi.fontSizes.overlayTitle + } + + DropShadow { + source: titleText + anchors.fill: titleText + horizontalOffset: 2 + verticalOffset: 2 + samples: 2 + color: hifi.colors.baseGrayShadow60 + visible: (window && window.focus) + cached: true + } +} + diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 88d8c3ad41..030af974f6 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -22,10 +22,10 @@ Item { property bool gradientsSupported: desktop.gradientsSupported - readonly property int frameMarginLeft: frameContent.frameMarginLeft - readonly property int frameMarginRight: frameContent.frameMarginRight - readonly property int frameMarginTop: frameContent.frameMarginTop - readonly property int frameMarginBottom: frameContent.frameMarginBottom + readonly property int frameMarginLeft: frame.decoration ? frame.decoration.frameMarginLeft : 0 + readonly property int frameMarginRight: frame.decoration ? frame.decoration.frameMarginRight : 0 + readonly property int frameMarginTop: frame.decoration ? frame.decoration.frameMarginTop : 0 + readonly property int frameMarginBottom: frame.decoration ? frame.decoration.frameMarginBottom : 0 // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes @@ -103,16 +103,14 @@ Item { } onReleased: { if (hid) { - pane.visible = true - frameContent.visible = true + window.content.visible = true hid = false; } } onPositionChanged: { if (pressed) { - if (pane.visible) { - pane.visible = false; - frameContent.visible = false + if (window.content.visible) { + window.content.visible = false; hid = true; } var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml index eff5fc0377..20c86afb5e 100644 --- a/interface/resources/qml/windows/ToolFrame.qml +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -16,81 +16,11 @@ import "../styles-uit" Frame { HifiConstants { id: hifi } - property bool horizontalSpacers: false - property bool verticalSpacers: false + property alias horizontalSpacers: decoration.horizontalSpacers + property alias verticalSpacers: decoration.verticalSpacers - Rectangle { - // Dialog frame - id: frameContent - readonly property int frameMargin: 6 - readonly property int frameMarginLeft: frameMargin + (horizontalSpacers ? 12 : 0) - readonly property int frameMarginRight: frameMargin + (horizontalSpacers ? 12 : 0) - readonly property int frameMarginTop: frameMargin + (verticalSpacers ? 12 : 0) - readonly property int frameMarginBottom: frameMargin + (verticalSpacers ? 12 : 0) - - Rectangle { - visible: horizontalSpacers - anchors.left: parent.left - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - width: 8 - height: window.height - color: "gray"; - radius: 4 - } - - Rectangle { - visible: horizontalSpacers - anchors.right: parent.right - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - width: 8 - height: window.height - color: "gray"; - radius: 4 - } - - Rectangle { - visible: verticalSpacers - anchors.top: parent.top - anchors.topMargin: 6 - anchors.horizontalCenter: parent.horizontalCenter - height: 8 - width: window.width - color: "gray"; - radius: 4 - } - - Rectangle { - visible: verticalSpacers - anchors.bottom: parent.bottom - anchors.bottomMargin: 6 - anchors.horizontalCenter: parent.horizontalCenter - height: 8 - width: window.width - color: "gray"; - radius: 4 - } - - anchors { - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - topMargin: -frameMarginTop - bottomMargin: -frameMarginBottom - } - anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 - } - radius: hifi.dimensions.borderRadius / 2 - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - } + ToolFrameDecoration { + id: decoration } } diff --git a/interface/resources/qml/windows/ToolFrameDecoration.qml b/interface/resources/qml/windows/ToolFrameDecoration.qml new file mode 100644 index 0000000000..a7068183c1 --- /dev/null +++ b/interface/resources/qml/windows/ToolFrameDecoration.qml @@ -0,0 +1,98 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +Decoration { + id: root + HifiConstants { id: hifi } + + property bool horizontalSpacers: false + property bool verticalSpacers: false + + // Dialog frame + property int spacerWidth: 8 + property int spacerRadius: 4 + property int spacerMargin: 12 + frameMargin: 6 + frameMarginLeft: frameMargin + (horizontalSpacers ? spacerMargin : 0) + frameMarginRight: frameMargin + (horizontalSpacers ? spacerMargin : 0) + frameMarginTop: frameMargin + (verticalSpacers ? spacerMargin : 0) + frameMarginBottom: frameMargin + (verticalSpacers ? spacerMargin : 0) + radius: hifi.dimensions.borderRadius / 2 + + Connections { + target: window + onMouseEntered: { + if (!HMD.active) { + return; + } + root.frameMargin = 18 + root.spacerWidth = 16 + root.spacerRadius = 8 + root.spacerMargin = 8 + } + onMouseExited: { + root.frameMargin = 6 + root.spacerWidth = 8 + root.spacerRadius = 4 + root.spacerMargin = 12 + } + } + + Rectangle { + visible: horizontalSpacers + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: root.spacerWidth + height: decoration.height - 12 + color: "gray"; + radius: root.spacerRadius + } + + Rectangle { + visible: horizontalSpacers + anchors.right: parent.right + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: root.spacerWidth + height: decoration.height - 12 + color: "gray"; + radius: root.spacerRadius + } + + Rectangle { + visible: verticalSpacers + anchors.top: parent.top + anchors.topMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: root.spacerWidth + width: decoration.width - 12 + color: "gray"; + radius: root.spacerRadius + } + + Rectangle { + visible: verticalSpacers + anchors.bottom: parent.bottom + anchors.bottomMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: root.spacerWidth + width: decoration.width - 12 + color: "gray"; + radius: root.spacerRadius + } +} + diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 82bcf011e9..ca37c55f4d 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -31,6 +31,8 @@ Fadable { // Signals // signal windowDestroyed(); + signal mouseEntered(); + signal mouseExited(); // // Native properties @@ -113,11 +115,14 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible + hoverEnabled: true onPressed: { //console.log("Pressed on activator area"); window.raise(); mouse.accepted = false; } + onEntered: window.mouseEntered(); + onExited: window.mouseExited(); } // This mouse area serves to swallow mouse events while the mouse is over the window @@ -287,4 +292,7 @@ Fadable { break; } } + + onMouseEntered: console.log("Mouse entered " + window) + onMouseExited: console.log("Mouse exited " + window) } diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 47d0f6d601..a23a1e3ade 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -28,6 +28,11 @@ ApplicationWindow { property var toolbar; property var lastButton; + Button { + text: HMD.active ? "Disable HMD" : "Enable HMD" + onClicked: HMD.active = !HMD.active + } + // Window visibility Button { text: "toggle desktop" @@ -340,13 +345,13 @@ ApplicationWindow { } */ - /* Window { id: blue closable: true visible: true resizable: true destroyOnHidden: false + title: "Blue" width: 100; height: 100 x: 1280 / 2; y: 720 / 2 @@ -366,7 +371,33 @@ ApplicationWindow { } } + Window { + id: green + closable: true + visible: true + resizable: true + title: "Green" + destroyOnHidden: false + width: 100; height: 100 + x: 1280 / 2; y: 720 / 2 + Settings { + category: "TestWindow.Green" + property alias x: green.x + property alias y: green.y + property alias width: green.width + property alias height: green.height + } + + Rectangle { + anchors.fill: parent + visible: true + color: "green" + Component.onDestruction: console.log("Green destroyed") + } + } + +/* Rectangle { width: 100; height: 100; x: 100; y: 100; color: "#00f" } Window { From 6336362d40fbd2e5e76c7d0000f83ee07da68934 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Jul 2016 17:23:13 -0700 Subject: [PATCH 1054/1237] Fixing decoration inflation --- interface/resources/qml/desktop/Desktop.qml | 1 + .../resources/qml/windows/Decoration.qml | 24 +++++++++++++++ .../qml/windows/DefaultFrameDecoration.qml | 28 ++++++++--------- .../qml/windows/ToolFrameDecoration.qml | 30 +++++++++---------- .../src/scripting/HMDScriptingInterface.cpp | 7 ++++- tests/ui/qml/main.qml | 5 ++++ 6 files changed, 63 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 59d5b435ba..e5ff849df8 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -25,6 +25,7 @@ FocusScope { property rect recommendedRect: Qt.rect(0,0,0,0); property var expectedChildren; property bool repositionLocked: true + property bool hmdHandMouseActive: false onRepositionLockedChanged: { if (!repositionLocked) { diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml index 628a4d3370..edfb369c0f 100644 --- a/interface/resources/qml/windows/Decoration.qml +++ b/interface/resources/qml/windows/Decoration.qml @@ -17,6 +17,9 @@ import "../styles-uit" Rectangle { HifiConstants { id: hifi } + signal inflateDecorations(); + signal deflateDecorations(); + property int frameMargin: 9 property int frameMarginLeft: frameMargin property int frameMarginRight: frameMargin @@ -43,5 +46,26 @@ Rectangle { anchors.fill: parent drag.target: window } + Connections { + target: window + onMouseEntered: { + if (desktop.hmdHandMouseActive) { + root.inflateDecorations() + } + } + onMouseExited: root.deflateDecorations(); + } + Connections { + target: desktop + onHmdHandMouseActiveChanged: { + if (desktop.hmdHandMouseActive) { + if (window.activator.containsMouse) { + root.inflateDecorations(); + } + } else { + root.deflateDecorations(); + } + } + } } diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml index ecb1760717..ce47b818b1 100644 --- a/interface/resources/qml/windows/DefaultFrameDecoration.qml +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -14,7 +14,6 @@ import QtGraphicalEffects 1.0 import "." import "../styles-uit" - Decoration { HifiConstants { id: hifi } @@ -28,23 +27,22 @@ Decoration { frameMarginTop: 2 * frameMargin + iconSize frameMarginBottom: iconSize + 11 - Connections { - target: window - onMouseEntered: { - if (!HMD.active) { - return; - } - root.frameMargin = 18 - titleText.size = hifi.fontSizes.overlayTitle * 2 - root.iconSize = hifi.dimensions.frameIconSize * 2 - } - onMouseExited: { - root.frameMargin = 9 - titleText.size = hifi.fontSizes.overlayTitle - root.iconSize = hifi.dimensions.frameIconSize + onInflateDecorations: { + if (!HMD.active) { + return; } + root.frameMargin = 18 + titleText.size = hifi.fontSizes.overlayTitle * 2 + root.iconSize = hifi.dimensions.frameIconSize * 2 } + onDeflateDecorations: { + root.frameMargin = 9 + titleText.size = hifi.fontSizes.overlayTitle + root.iconSize = hifi.dimensions.frameIconSize + } + + Row { id: controlsRow anchors { diff --git a/interface/resources/qml/windows/ToolFrameDecoration.qml b/interface/resources/qml/windows/ToolFrameDecoration.qml index a7068183c1..ba36a2a38c 100644 --- a/interface/resources/qml/windows/ToolFrameDecoration.qml +++ b/interface/resources/qml/windows/ToolFrameDecoration.qml @@ -32,23 +32,21 @@ Decoration { frameMarginBottom: frameMargin + (verticalSpacers ? spacerMargin : 0) radius: hifi.dimensions.borderRadius / 2 - Connections { - target: window - onMouseEntered: { - if (!HMD.active) { - return; - } - root.frameMargin = 18 - root.spacerWidth = 16 - root.spacerRadius = 8 - root.spacerMargin = 8 - } - onMouseExited: { - root.frameMargin = 6 - root.spacerWidth = 8 - root.spacerRadius = 4 - root.spacerMargin = 12 + onInflateDecorations: { + if (!HMD.active) { + return; } + root.frameMargin = 18 + root.spacerWidth = 16 + root.spacerRadius = 8 + root.spacerMargin = 8 + } + + onDeflateDecorations: { + root.frameMargin = 6 + root.spacerWidth = 8 + root.spacerRadius = 4 + root.spacerMargin = 12 } Rectangle { diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index e9677cc3c8..36cde378f8 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include "Application.h" @@ -110,13 +111,17 @@ QString HMDScriptingInterface::preferredAudioOutput() const { } bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([offscreenUi, enabled] { + offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", enabled); + }); return qApp->getActiveDisplayPlugin()->setHandLaser(hands, enabled ? DisplayPlugin::HandLaserMode::Overlay : DisplayPlugin::HandLaserMode::None, color, direction); } void HMDScriptingInterface::disableHandLasers(int hands) const { - qApp->getActiveDisplayPlugin()->setHandLaser(hands, DisplayPlugin::HandLaserMode::None); + setHandLasers(hands, false, vec4(0), vec3(0)); } bool HMDScriptingInterface::suppressKeyboard() { diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index a23a1e3ade..8ca9399b74 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -33,6 +33,11 @@ ApplicationWindow { onClicked: HMD.active = !HMD.active } + Button { + text: desktop.hmdHandMouseActive ? "Disable HMD HandMouse" : "Enable HMD HandMouse" + onClicked: desktop.hmdHandMouseActive = !desktop.hmdHandMouseActive + } + // Window visibility Button { text: "toggle desktop" From fcd59ea94f122c8644bf5a95cc7246cbf5ff382d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 12 Jul 2016 17:44:23 -0700 Subject: [PATCH 1055/1237] Update target number of processing threads to be based on system --- interface/src/Application.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5d5bf45dcf..d97dee6fd1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -169,7 +169,12 @@ static QTimer identityPacketTimer; static QTimer pingTimer; static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; -static const int PROCESSING_THREAD_POOL_SIZE = 6; + +// For processing on QThreadPool, target 2 less than the ideal number of threads, leaving +// 2 logical cores available for time sensitive tasks. +static const int MIN_PROCESSING_THREAD_POOL_SIZE = 2; +static const int PROCESSING_THREAD_POOL_SIZE = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, + QThread::idealThreadCount() - 2); static const QString SNAPSHOT_EXTENSION = ".jpg"; static const QString SVO_EXTENSION = ".svo"; @@ -514,12 +519,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care PluginManager::getInstance()->setContainer(pluginContainer); - // FIXME this may be excessively conservative. On the other hand - // maybe I'm used to having an 8-core machine - // Perhaps find the ideal thread count and subtract 2 or 3 - // (main thread, present thread, random OS load) - // More threads == faster concurrent loads, but also more concurrent - // load on the GPU until we can serialize GPU transfers (off the main thread) QThreadPool::globalInstance()->setMaxThreadCount(PROCESSING_THREAD_POOL_SIZE); thread()->setPriority(QThread::HighPriority); thread()->setObjectName("Main Thread"); From 51594fefa23a764b0adbc7a54abb50d2301cb248 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 12 Jul 2016 18:21:31 -0700 Subject: [PATCH 1056/1237] more clean up --- .../render-utils/src/DeferredBufferWrite.slh | 12 +++- .../render-utils/src/DeferredGlobalLight.slh | 1 - .../src/DeferredLightingEffect.cpp | 25 ++++---- .../render-utils/src/DeferredLightingEffect.h | 6 +- libraries/render-utils/src/LightPoint.slh | 2 +- libraries/render-utils/src/LightSpot.slh | 2 +- libraries/render-utils/src/LightingModel.cpp | 2 +- libraries/render-utils/src/LightingModel.h | 4 +- libraries/render-utils/src/LightingModel.slh | 7 +-- .../render-utils/src/RenderDeferredTask.cpp | 63 ++++++++++++------- .../render-utils/src/RenderDeferredTask.h | 25 +++++--- .../render-utils/src/ToneMappingEffect.cpp | 5 +- libraries/render-utils/src/overlay3D.slf | 1 - .../src/overlay3D_translucent.slf | 5 +- libraries/render-utils/src/point_light.slf | 1 - libraries/render-utils/src/spot_light.slf | 1 - libraries/render/src/render/ShapePipeline.cpp | 2 + libraries/render/src/render/ShapePipeline.h | 6 +- libraries/render/src/render/Task.h | 9 ++- .../utilities/render/deferredLighting.qml | 14 ++++- 20 files changed, 119 insertions(+), 74 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index cd62a30611..dece8e9ce3 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -12,6 +12,8 @@ <@def DEFERRED_BUFFER_WRITE_SLH@> <@include DeferredBuffer.slh@> +<@include LightingModel.slh@> + layout(location = 0) out vec4 _fragColor0; layout(location = 1) out vec4 _fragColor1; @@ -38,6 +40,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness if (alpha != 1.0) { discard; } + emissive *= isEmissiveEnabled(); _fragColor0 = vec4(albedo, ((scattering > 0.0) ? packScatteringMetallic(metallic) : packShadedMetallic(metallic))); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(((scattering > 0.0) ? vec3(scattering) : emissive), occlusion); @@ -50,10 +53,15 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float r if (alpha != 1.0) { discard; } + _fragColor0 = vec4(albedo, packLightmappedMetallic(metallic)); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(lightmap, 1.0); - _fragColor3 = vec4(albedo * lightmap, 1.0); + + _fragColor3 = vec4(lightmap * isLightmapEnabled(), 1.0); + if (isAlbedoEnabled() > 0.0) { + _fragColor3.rgb *= albedo; + } } void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { @@ -63,7 +71,7 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { _fragColor0 = vec4(color, packUnlit()); _fragColor1 = vec4(packNormal(normal), 1.0); _fragColor2 = vec4(vec3(0.0), 1.0); - _fragColor3 = vec4(color, 1.0); + _fragColor3 = vec4(color * isUnlitEnabled(), 1.0); } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, vec3 fresnel, float roughness) { diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 95cfc1d151..16cdab535c 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -14,7 +14,6 @@ <@include model/Light.slh@> <@include LightingModel.slh@> -<$declareLightingModel()$> <@include LightAmbient.slh@> <@include LightDirectional.slh@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index b15b980e51..57b079dab0 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -66,9 +66,9 @@ enum DeferredShader_MapSlot { }; enum DeferredShader_BufferSlot { DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 0, - LIGHTING_MODEL_BUFFER_SLOT, SCATTERING_PARAMETERS_BUFFER_SLOT, - LIGHT_GPU_SLOT, + LIGHTING_MODEL_BUFFER_SLOT = render::ShapePipeline::Slot::LIGHTING_MODEL, + LIGHT_GPU_SLOT = render::ShapePipeline::Slot::LIGHT, }; static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& program, LightLocationsPtr& locations); @@ -355,16 +355,19 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con primaryFramebuffer = _primaryFramebuffer; } -void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& primaryFramebuffer, Outputs& output) { +void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { auto args = renderContext->args; + auto primaryFramebuffer = inputs.get0(); + auto lightingModel = inputs.get1(); + if (!_deferredFramebuffer) { _deferredFramebuffer = std::make_shared(); } _deferredFramebuffer->updatePrimaryDepth(primaryFramebuffer->getDepthStencilBuffer()); - output.edit0() = _deferredFramebuffer; - output.edit1() = _deferredFramebuffer->getLightingFramebuffer(); + outputs.edit0() = _deferredFramebuffer; + outputs.edit1() = _deferredFramebuffer->getLightingFramebuffer(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { @@ -372,14 +375,7 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); - // Clear Lighting buffer - /* auto lightingFbo = DependencyManager::get()->getLightingFramebuffer(); - - batch.setFramebuffer(lightingFbo); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(vec3(0), 0), true); -*/ // Clear deferred - // auto deferredFbo = DependencyManager::get()->getDeferredFramebuffer(); auto deferredFbo = _deferredFramebuffer->getDeferredFramebuffer(); batch.setFramebuffer(deferredFbo); @@ -389,6 +385,9 @@ void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderC gpu::Framebuffer::BUFFER_DEPTH | gpu::Framebuffer::BUFFER_STENCIL, vec4(vec3(0), 0), 1.0, 0.0, true); + + // For the rest of the rendering, bind the lighting model + batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, lightingModel->getParametersBuffer()); }); } @@ -646,7 +645,7 @@ void RenderDeferredCleanup::run(const render::SceneContextPointer& sceneContext, batch.setResourceTexture(SCATTERING_SPECULAR_UNIT, nullptr); batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, nullptr); - batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, nullptr); + // batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, nullptr); batch.setUniformBuffer(DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT, nullptr); }); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index d520ddc098..af884c0102 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -117,12 +117,14 @@ public: class PrepareDeferred { public: + // Inputs: primaryFramebuffer and lightingModel + using Inputs = render::VaryingSet2 ; // Output: DeferredFramebuffer, LightingFramebuffer using Outputs = render::VaryingSet2; - using JobModel = render::Job::ModelIO; + using JobModel = render::Job::ModelIO; - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& primaryFramebuffer, Outputs& output); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs); DeferredFramebufferPointer _deferredFramebuffer; }; diff --git a/libraries/render-utils/src/LightPoint.slh b/libraries/render-utils/src/LightPoint.slh index d13d52d360..5c9e66dd24 100644 --- a/libraries/render-utils/src/LightPoint.slh +++ b/libraries/render-utils/src/LightPoint.slh @@ -37,7 +37,7 @@ void evalLightingPoint(out vec3 diffuse, out vec3 specular, Light light, diffuse *= lightEnergy * isDiffuseEnabled() * isPointEnabled(); specular *= lightEnergy * isSpecularEnabled() * isPointEnabled(); - if (getLightShowContour(light) > 0.0) { + if (isShowLightContour() > 0.0) { // Show edge float edge = abs(2.0 * ((getLightRadius(light) - fragLightDistance) / (0.1)) - 1.0); if (edge < 1) { diff --git a/libraries/render-utils/src/LightSpot.slh b/libraries/render-utils/src/LightSpot.slh index 0b66a3e4c1..8a17a5ef4d 100644 --- a/libraries/render-utils/src/LightSpot.slh +++ b/libraries/render-utils/src/LightSpot.slh @@ -38,7 +38,7 @@ void evalLightingSpot(out vec3 diffuse, out vec3 specular, Light light, diffuse *= lightEnergy * isDiffuseEnabled() * isSpotEnabled(); specular *= lightEnergy * isSpecularEnabled() * isSpotEnabled(); - if (getLightShowContour(light) > 0.0) { + if (isShowLightContour() > 0.0) { // Show edges float edgeDistR = (getLightRadius(light) - fragLightDistance); float edgeDistS = dot(fragLightDistance * vec2(cosSpotAngle, sqrt(1.0 - cosSpotAngle * cosSpotAngle)), -getLightSpotOutsideNormal2(light)); diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 998659308d..14aea67662 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -122,7 +122,7 @@ void LightingModel::setShowLightContour(bool enable) { } } bool LightingModel::isShowLightContourEnabled() const { - return (bool)_parametersBuffer.get().showLightContour; + return (bool)(_parametersBuffer.get().showLightContour > 0.0); } MakeLightingModel::MakeLightingModel() { diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 9bd982a080..d895be297c 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -81,7 +81,7 @@ protected: float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; - float showLightContour{ 1.0f }; + float showLightContour{ 0.0f }; // false by default glm::vec3 spares{ 0.0f }; Parameters() {} @@ -132,7 +132,7 @@ public: bool enablePointLight{ true }; bool enableSpotLight{ true }; - bool showLightContour{ true }; + bool showLightContour{ false }; // false by default signals: void dirty(); diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index b8c73f7dab..f34a770d72 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -12,9 +12,6 @@ <@def LIGHTING_MODEL_SLH@> <@func declareLightingModel()@> -<@endfunc@> - -<@func declareLightingModelMaster()@> struct LightingModel { vec4 _UnlitShadedEmissiveLightmap; @@ -66,12 +63,12 @@ float isSpotEnabled() { return lightingModel._AmbientDirectionalPointSpot.w; } -float isShowContour() { +float isShowLightContour() { return lightingModel._ShowContour.x; } <@endfunc@> -<$declareLightingModelMaster()$> +<$declareLightingModel()$> <@func declareBeckmannSpecular()@> diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 3fee16f2c3..2a0864c22b 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -97,28 +97,29 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // GPU jobs: Start preparing the primary, deferred and lighting buffer const auto primaryFramebuffer = addJob("PreparePrimaryBuffer"); - const auto deferredAndLightingFramebuffer = addJob("PrepareDeferred", primaryFramebuffer); - const auto deferredFramebuffer = deferredAndLightingFramebuffer.getN(0); - const auto lightingFramebuffer = deferredAndLightingFramebuffer.getN(1); + const auto prepareDeferredInputs = SurfaceGeometryPass::Inputs(primaryFramebuffer, lightingModel).hasVarying(); + const auto prepareDeferredOutputs = addJob("PrepareDeferred", prepareDeferredInputs); + const auto deferredFramebuffer = prepareDeferredOutputs.getN(0); + const auto lightingFramebuffer = prepareDeferredOutputs.getN(1); // Render opaque objects in DeferredBuffer - addJob("DrawOpaqueDeferred", opaques, shapePlumber); + const auto opaqueInputs = DrawStateSortDeferred::Inputs(opaques, lightingModel).hasVarying(); + addJob("DrawOpaqueDeferred", opaqueInputs, shapePlumber); // Once opaque is all rendered create stencil background addJob("DrawOpaqueStencil", deferredFramebuffer); // Opaque all rendered, generate surface geometry buffers - const auto surfaceGeometryPassInputs = render::Varying(SurfaceGeometryPass::Inputs(deferredFrameTransform, deferredFramebuffer)); - const auto geometryFramebufferAndCurvatureFramebufferAndDepth = addJob("SurfaceGeometry", surfaceGeometryPassInputs); - const auto surfaceGeometryFramebuffer = geometryFramebufferAndCurvatureFramebufferAndDepth.getN(0); - const auto curvatureFramebuffer = geometryFramebufferAndCurvatureFramebufferAndDepth.getN(1); - const auto linearDepthTexture = geometryFramebufferAndCurvatureFramebufferAndDepth.getN(2); + const auto surfaceGeometryPassInputs = SurfaceGeometryPass::Inputs(deferredFrameTransform, deferredFramebuffer).hasVarying(); + const auto surfaceGeometryPassOutputs = addJob("SurfaceGeometry", surfaceGeometryPassInputs); + const auto surfaceGeometryFramebuffer = surfaceGeometryPassOutputs.getN(0); + const auto curvatureFramebuffer = surfaceGeometryPassOutputs.getN(1); + const auto linearDepthTexture = surfaceGeometryPassOutputs.getN(2); - const auto curvatureFramebufferAndDepth = render::Varying(BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture)); - - const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", curvatureFramebufferAndDepth); - const auto lowCurvatureNormalFramebuffer = addJob("DiffuseCurvatureLow", curvatureFramebufferAndDepth, true); + const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture).hasVarying(); + const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", diffuseCurvaturePassInputs); + const auto lowCurvatureNormalFramebuffer = addJob("DiffuseCurvatureLow", diffuseCurvaturePassInputs, true); // THis blur pass generates it s render resource const auto scatteringResource = addJob("Scattering"); @@ -128,25 +129,29 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. addJob("DrawLight", lights); - const auto deferredLightingInputs = render::Varying(RenderDeferred::Inputs(deferredFrameTransform, deferredFramebuffer, lightingModel, - surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer, scatteringResource)); + const auto deferredLightingInputs = RenderDeferred::Inputs(deferredFrameTransform, deferredFramebuffer, lightingModel, + surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer, scatteringResource).hasVarying(); // DeferredBuffer is complete, now let's shade it into the LightingBuffer addJob("RenderDeferred", deferredLightingInputs); // Use Stencil and draw background in Lighting buffer to complete filling in the opaque - addJob("DrawBackgroundDeferred", background); + const auto backgroundInputs = DrawBackgroundDeferred::Inputs(background, lightingModel).hasVarying(); + addJob("DrawBackgroundDeferred", backgroundInputs); // Render transparent objects forward in LightingBuffer - addJob("DrawTransparentDeferred", transparents, shapePlumber); + const auto transparentsInputs = DrawDeferred::Inputs(transparents, lightingModel).hasVarying(); + addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); // Lighting Buffer ready for tone mapping const auto toneMappingInputs = render::Varying(ToneMappingDeferred::Inputs(lightingFramebuffer, primaryFramebuffer)); addJob("ToneMapping", toneMappingInputs); // Overlays - addJob("DrawOverlay3DOpaque", overlayOpaques, true); - addJob("DrawOverlay3DTransparent", overlayTransparents, false); + const auto overlayOpaquesInputs = DrawOverlay3D::Inputs(overlayOpaques, lightingModel).hasVarying(); + const auto overlayTransparentsInputs = DrawOverlay3D::Inputs(overlayTransparents, lightingModel).hasVarying(); + addJob("DrawOverlay3DOpaque", overlayOpaquesInputs, true); + addJob("DrawOverlay3DTransparent", overlayTransparentsInputs, false); // Debugging stages @@ -198,12 +203,15 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend } } -void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems) { +void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); auto config = std::static_pointer_cast(renderContext->jobConfig); + const auto& inItems = inputs.get0(); + const auto& lightingModel = inputs.get1(); + RenderArgs* args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { @@ -226,12 +234,15 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont config->setNumDrawn((int)inItems.size()); } -void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems) { +void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); auto config = std::static_pointer_cast(renderContext->jobConfig); + const auto& inItems = inputs.get0(); + const auto& lightingModel = inputs.get1(); + RenderArgs* args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { @@ -264,12 +275,15 @@ DrawOverlay3D::DrawOverlay3D(bool opaque) : initOverlay3DPipelines(*_shapePlumber); } -void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const render::ItemBounds& inItems) { +void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); auto config = std::static_pointer_cast(renderContext->jobConfig); + const auto& inItems = inputs.get0(); + const auto& lightingModel = inputs.get1(); + config->setNumDrawn((int)inItems.size()); emit config->numDrawnChanged(); @@ -341,10 +355,13 @@ void DrawStencilDeferred::run(const SceneContextPointer& sceneContext, const Ren args->_batch = nullptr; } -void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems) { +void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); + const auto& inItems = inputs.get0(); + const auto& lightingModel = inputs.get1(); + RenderArgs* args = renderContext->args; doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 612d1c5ea4..b6ad83985e 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -14,6 +14,8 @@ #include #include +#include "LightingModel.h" + class DrawConfig : public render::Job::Config { Q_OBJECT @@ -37,13 +39,14 @@ protected: class DrawDeferred { public: + using Inputs = render::VaryingSet2 ; using Config = DrawConfig; - using JobModel = render::Job::ModelI; + using JobModel = render::Job::ModelI; DrawDeferred(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} void configure(const Config& config) { _maxDrawn = config.maxDrawn; } - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const render::ItemBounds& inItems); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); protected: render::ShapePlumberPointer _shapePlumber; @@ -73,13 +76,15 @@ protected: class DrawStateSortDeferred { public: + using Inputs = render::VaryingSet2 ; + using Config = DrawStateSortConfig; - using JobModel = render::Job::ModelI; + using JobModel = render::Job::ModelI; DrawStateSortDeferred(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} void configure(const Config& config) { _maxDrawn = config.maxDrawn; _stateSort = config.stateSort; } - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const render::ItemBounds& inItems); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); protected: render::ShapePlumberPointer _shapePlumber; @@ -112,11 +117,13 @@ protected: class DrawBackgroundDeferred { public: + using Inputs = render::VaryingSet2 ; + using Config = DrawBackgroundDeferredConfig; - using JobModel = render::Job::ModelI; + using JobModel = render::Job::ModelI; void configure(const Config& config) {} - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const render::ItemBounds& inItems); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); protected: gpu::RangeTimer _gpuTimer; @@ -142,13 +149,15 @@ protected: class DrawOverlay3D { public: + using Inputs = render::VaryingSet2 ; + using Config = DrawOverlay3DConfig; - using JobModel = render::Job::ModelI; + using JobModel = render::Job::ModelI; DrawOverlay3D(bool opaque); void configure(const Config& config) { _maxDrawn = config.maxDrawn; } - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const render::ItemBounds& inItems); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs); protected: render::ShapePlumberPointer _shapePlumber; diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index 1cd0e42ad1..e844ee6eda 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -156,10 +156,7 @@ void ToneMappingDeferred::configure(const Config& config) { } void ToneMappingDeferred::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs) { - /* auto framebufferCache = DependencyManager::get(); - auto lightingBuffer = framebufferCache->getLightingTexture(); - auto destFbo = framebufferCache->getPrimaryFramebuffer(); - */ + auto lightingBuffer = inputs.get0()->getRenderBuffer(0); auto destFbo = inputs.get1(); _toneMappingEffect.render(renderContext->args, lightingBuffer, destFbo); diff --git a/libraries/render-utils/src/overlay3D.slf b/libraries/render-utils/src/overlay3D.slf index 6ff882188c..b29b9baf64 100644 --- a/libraries/render-utils/src/overlay3D.slf +++ b/libraries/render-utils/src/overlay3D.slf @@ -15,7 +15,6 @@ <@include model/Light.slh@> <@include LightingModel.slh@> -<$declareLightingModel()$> <@include LightDirectional.slh@> <$declareLightingDirectional()$> diff --git a/libraries/render-utils/src/overlay3D_translucent.slf b/libraries/render-utils/src/overlay3D_translucent.slf index d3ddc04543..ce5bbf3589 100644 --- a/libraries/render-utils/src/overlay3D_translucent.slf +++ b/libraries/render-utils/src/overlay3D_translucent.slf @@ -15,7 +15,6 @@ <@include model/Light.slh@> <@include LightingModel.slh@> -<$declareLightingModel()$> <@include LightDirectional.slh@> <$declareLightingDirectional()$> @@ -40,8 +39,8 @@ vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 a vec3 directionalDiffuse; vec3 directionalSpecular; evalLightingDirectional(directionalDiffuse, directionalSpecular, light, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); - color += directionalDiffuse * isDiffuseEnabled() * isDirectionalEnabled(); - color += directionalSpecular * isSpecularEnabled() * isDirectionalEnabled(); + color += directionalDiffuse; + color += directionalSpecular / opacity; return vec4(color, opacity); } diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 02a5d0d36c..84475be64e 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -21,7 +21,6 @@ <@include model/Light.slh@> <@include LightingModel.slh@> -<$declareLightingModel()$> <@include LightPoint.slh@> <$declareLightingPoint(supportScattering)$> diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 3b860e3fb3..cd379af51e 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -21,7 +21,6 @@ <@include model/Light.slh@> <@include LightingModel.slh@> -<$declareLightingModel()$> <@include LightSpot.slh@> <$declareLightingSpot(supportScattering)$> diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 6c5128830e..3b613e0f96 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -51,6 +51,7 @@ void ShapePlumber::addPipeline(const Key& key, const gpu::ShaderPointer& program void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, BatchSetter batchSetter) { gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), Slot::BUFFER::LIGHTING_MODEL)); slotBindings.insert(gpu::Shader::Binding(std::string("skinClusterBuffer"), Slot::BUFFER::SKINNING)); slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), Slot::BUFFER::MATERIAL)); slotBindings.insert(gpu::Shader::Binding(std::string("texMapArrayBuffer"), Slot::BUFFER::TEXMAPARRAY)); @@ -79,6 +80,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p locations->metallicTextureUnit = program->getTextures().findLocation("metallicMap"); locations->emissiveTextureUnit = program->getTextures().findLocation("emissiveMap"); locations->occlusionTextureUnit = program->getTextures().findLocation("occlusionMap"); + locations->lightingModelBufferUnit = program->getBuffers().findLocation("lightingModelBuffer"); locations->skinClusterBufferUnit = program->getBuffers().findLocation("skinClusterBuffer"); locations->materialBufferUnit = program->getBuffers().findLocation("materialBuffer"); locations->texMapArrayBufferUnit = program->getBuffers().findLocation("texMapArrayBuffer"); diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index e65281fd33..5b60521068 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -196,10 +196,11 @@ public: class Slot { public: enum BUFFER { - SKINNING = 2, + SKINNING = 0, MATERIAL, TEXMAPARRAY, - LIGHT + LIGHTING_MODEL, + LIGHT, }; enum MAP { @@ -225,6 +226,7 @@ public: int emissiveTextureUnit; int occlusionTextureUnit; int normalFittingMapUnit; + int lightingModelBufferUnit; int skinClusterBufferUnit; int materialBufferUnit; int texMapArrayBufferUnit; diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 967c75fc88..a90c7ea40e 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -83,8 +83,6 @@ protected: }; using VaryingPairBase = std::pair; - - template < typename T0, typename T1 > class VaryingSet2 : public VaryingPairBase { public: @@ -110,6 +108,7 @@ public: } virtual uint8_t length() const { return 2; } + Varying hasVarying() const { return Varying((*this)); } }; @@ -142,6 +141,7 @@ public: } virtual uint8_t length() const { return 3; } + Varying hasVarying() const { return Varying((*this)); } }; template @@ -178,6 +178,7 @@ public: } virtual uint8_t length() const { return 4; } + Varying hasVarying() const { return Varying((*this)); } }; @@ -204,6 +205,8 @@ public: const T4& get4() const { return std::get<4>((*this)).template get(); } T4& edit4() { return std::get<4>((*this)).template edit(); } + + Varying hasVarying() const { return Varying((*this)); } }; template @@ -232,6 +235,8 @@ public: const T5& get5() const { return std::get<5>((*this)).template get(); } T5& edit5() { return std::get<5>((*this)).template edit(); } + + Varying hasVarying() const { return Varying((*this)); } }; template < class T, int NUM > diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 372f97bf43..1425013640 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -22,7 +22,6 @@ Column { Repeater { model: [ "Unlit:LightingModel:enableUnlit", - "Shaded:LightingModel:enableShaded", "Emissive:LightingModel:enableEmissive", "Lightmap:LightingModel:enableLightmap", ] @@ -68,6 +67,19 @@ Column { } } } + Column { + spacing: 10 + Repeater { + model: [ + "Light Contour:LightingModel:showLightContour" + ] + CheckBox { + text: modelData.split(":")[0] + checked: Render.getConfig(modelData.split(":")[1]) + onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } } Column { spacing: 10 From 9f91dd5bfcfbe5474f7c5785b7889ecda0b482fc Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 13 Jul 2016 10:19:42 -0700 Subject: [PATCH 1057/1237] Cleaning the gpu-test app to work with the new deferred pipeline --- .../src/DeferredLightingEffect.cpp | 28 +++++++++------- .../render-utils/src/DeferredLightingEffect.h | 2 +- libraries/render-utils/src/LightingModel.cpp | 2 +- .../render-utils/src/RenderDeferredTask.cpp | 1 + tests/gpu-test/src/TestWindow.cpp | 33 ++++++++++++------- tests/gpu-test/src/TestWindow.h | 11 +++++-- 6 files changed, 50 insertions(+), 27 deletions(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 57b079dab0..a55a87a46e 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -397,7 +397,7 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c const DeferredFramebufferPointer& deferredFramebuffer, const LightingModelPointer& lightingModel, const SurfaceGeometryFramebufferPointer& surfaceGeometryFramebuffer, - const gpu::TexturePointer& lowCurvatureNormal, + const gpu::FramebufferPointer& lowCurvatureNormalFramebuffer, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource) { auto args = renderContext->args; @@ -441,15 +441,17 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, lightingModel->getParametersBuffer()); // Subsurface scattering specific - batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, surfaceGeometryFramebuffer->getCurvatureTexture()); - batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, lowCurvatureNormal); - - batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, subsurfaceScatteringResource->getParametersBuffer()); - - - batch.setResourceTexture(SCATTERING_LUT_UNIT, subsurfaceScatteringResource->getScatteringTable()); - batch.setResourceTexture(SCATTERING_SPECULAR_UNIT, subsurfaceScatteringResource->getScatteringSpecular()); - + if (surfaceGeometryFramebuffer) { + batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, surfaceGeometryFramebuffer->getCurvatureTexture()); + } + if (lowCurvatureNormalFramebuffer) { + batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, lowCurvatureNormalFramebuffer->getRenderBuffer(0)); + } + if (subsurfaceScatteringResource) { + batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, subsurfaceScatteringResource->getParametersBuffer()); + batch.setResourceTexture(SCATTERING_LUT_UNIT, subsurfaceScatteringResource->getScatteringTable()); + batch.setResourceTexture(SCATTERING_SPECULAR_UNIT, subsurfaceScatteringResource->getScatteringSpecular()); + } // Global directional light and ambient pass @@ -673,10 +675,12 @@ void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderCo auto deferredFramebuffer = inputs.get1(); auto lightingModel = inputs.get2(); auto surfaceGeometryFramebuffer = inputs.get3(); - auto lowCurvatureNormal = inputs.get4()->getRenderBuffer(0); + auto lowCurvatureNormalFramebuffer = inputs.get4(); auto subsurfaceScatteringResource = inputs.get5(); - setupJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel, surfaceGeometryFramebuffer, lowCurvatureNormal, subsurfaceScatteringResource); + + + setupJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel, surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer, subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform, lightingModel->isPointLightEnabled(), lightingModel->isSpotLightEnabled()); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index af884c0102..552bd1f006 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -138,7 +138,7 @@ public: const DeferredFramebufferPointer& deferredFramebuffer, const LightingModelPointer& lightingModel, const SurfaceGeometryFramebufferPointer& surfaceGeometryFramebuffer, - const gpu::TexturePointer& diffusedCurvature2, + const gpu::FramebufferPointer& lowCurvatureNormalFramebuffer, const SubsurfaceScatteringResourcePointer& subsurfaceScatteringResource); }; diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 14aea67662..891b59a726 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -147,5 +147,5 @@ void MakeLightingModel::configure(const Config& config) { void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { - lightingModel = _lightingModel; + lightingModel = _lightingModel; } \ No newline at end of file diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 2a0864c22b..d186b1ac1c 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -117,6 +117,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto curvatureFramebuffer = surfaceGeometryPassOutputs.getN(1); const auto linearDepthTexture = surfaceGeometryPassOutputs.getN(2); + // TODO: Push this 2 diffusion stages into surfaceGeometryPass as they are working together const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture).hasVarying(); const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", diffuseCurvaturePassInputs); const auto lowCurvatureNormalFramebuffer = addJob("DiffuseCurvatureLow", diffuseCurvaturePassInputs, true); // THis blur pass generates it s render resource diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index 2d08993436..f0cc3f9592 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -97,9 +97,26 @@ void TestWindow::beginFrame() { _renderArgs->_context->syncCache(); #ifdef DEFERRED_LIGHTING - auto deferredLightingEffect = DependencyManager::get(); - _prepareDeferred.run(_sceneContext, _renderContext, _deferredFramebuffer); + gpu::FramebufferPointer primaryFramebuffer; + _preparePrimaryFramebuffer.run(_sceneContext, _renderContext, primaryFramebuffer); + + DeferredFrameTransformPointer frameTransform; + _generateDeferredFrameTransform.run(_sceneContext, _renderContext, frameTransform); + + LightingModelPointer lightingModel; + _generateLightingModel.run(_sceneContext, _renderContext, lightingModel); + + _prepareDeferredInputs.edit0() = primaryFramebuffer; + _prepareDeferredInputs.edit1() = lightingModel; + _prepareDeferred.run(_sceneContext, _renderContext, _prepareDeferredInputs, _prepareDeferredOutputs); + + + _renderDeferredInputs.edit0() = frameTransform; // Pass the deferredFrameTransform + _renderDeferredInputs.edit1() = _prepareDeferredOutputs.get0(); // Pass the deferredFramebuffer + _renderDeferredInputs.edit2() = lightingModel; // Pass the lightingModel + // the rest of the renderDeferred inputs can be omitted + #else gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 0.0f, 0.1f, 0.2f, 1.0f }); @@ -120,7 +137,7 @@ void TestWindow::endFrame() { RenderArgs* args = _renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; - auto deferredFboColorDepthStencil = DependencyManager::get()->getDeferredFramebufferDepthColor(); + auto deferredFboColorDepthStencil = _prepareDeferredOutputs.get0()->getDeferredFramebufferDepthColor(); batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); batch.setFramebuffer(deferredFboColorDepthStencil); @@ -129,20 +146,14 @@ void TestWindow::endFrame() { batch.setResourceTexture(0, nullptr); }); - DeferredFrameTransformPointer frameTransform; - _generateDeferredFrameTransform.run(_sceneContext, _renderContext, frameTransform); - - RenderDeferred::Inputs deferredInputs; - deferredInputs.edit0() = frameTransform; - deferredInputs.edit1() = _deferredFramebuffer; - _renderDeferred.run(_sceneContext, _renderContext, deferredInputs); + _renderDeferred.run(_sceneContext, _renderContext, _renderDeferredInputs); gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { PROFILE_RANGE_BATCH(batch, "blit"); // Blit to screen auto framebufferCache = DependencyManager::get(); // auto framebuffer = framebufferCache->getLightingFramebuffer(); - auto framebuffer = _deferredFramebuffer->getLightingFramebuffer(); + auto framebuffer = _prepareDeferredOutputs.get0()->getLightingFramebuffer(); batch.blit(framebuffer, _renderArgs->_viewport, nullptr, _renderArgs->_viewport); }); #endif diff --git a/tests/gpu-test/src/TestWindow.h b/tests/gpu-test/src/TestWindow.h index 1260b519eb..c8d09825aa 100644 --- a/tests/gpu-test/src/TestWindow.h +++ b/tests/gpu-test/src/TestWindow.h @@ -35,11 +35,18 @@ protected: render::RenderContextPointer _renderContext{ std::make_shared() }; gpu::PipelinePointer _opaquePipeline; model::LightPointer _light { std::make_shared() }; - + GenerateDeferredFrameTransform _generateDeferredFrameTransform; - DeferredFramebufferPointer _deferredFramebuffer; + MakeLightingModel _generateLightingModel; + PreparePrimaryFramebuffer _preparePrimaryFramebuffer; + + PrepareDeferred::Inputs _prepareDeferredInputs; PrepareDeferred _prepareDeferred; + PrepareDeferred::Outputs _prepareDeferredOutputs; + + RenderDeferred::Inputs _renderDeferredInputs; RenderDeferred _renderDeferred; + #endif RenderArgs* _renderArgs { new RenderArgs() }; From 2c584f76908e4dd6ce36113ea1dab60edb4a4125 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 13 Jul 2016 10:34:43 -0700 Subject: [PATCH 1058/1237] Fixing a bad autoRef usage --- libraries/render-utils/src/SubsurfaceScattering.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 5ed2b49dec..e0d9234e49 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -515,8 +515,8 @@ void DebugSubsurfaceScattering::run(const render::SceneContextPointer& sceneCont auto& deferredFramebuffer = inputs.get1(); auto& surfaceGeometryFramebuffer = inputs.get3(); - auto& curvatureFramebuffer = surfaceGeometryFramebuffer->getCurvatureFramebuffer(); - auto& linearDepthTexture = surfaceGeometryFramebuffer->getLinearDepthTexture(); + auto curvatureFramebuffer = surfaceGeometryFramebuffer->getCurvatureFramebuffer(); + auto linearDepthTexture = surfaceGeometryFramebuffer->getLinearDepthTexture(); auto& diffusedFramebuffer = inputs.get4(); auto& scatteringResource = inputs.get5(); From 307243da8b83c1fd6e9ce3ff57d8a5ff24a92a2b Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 13 Jul 2016 11:04:53 -0700 Subject: [PATCH 1059/1237] cleaning compilation warnings and tuning the FBX material mapping pass --- libraries/fbx/src/FBXReader_Material.cpp | 20 ++++++++++++------- .../render-utils/src/FramebufferCache.cpp | 6 ++---- libraries/render-utils/src/LightingModel.cpp | 2 +- .../render-utils/src/RenderDeferredTask.cpp | 16 ++++++++++++++- tools/scribe/src/TextTemplate.cpp | 2 -- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index 48b210b88e..f68203a04b 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -261,13 +261,19 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { if (materialMap.contains(material.name)) { QJsonObject materialOptions = materialMap.value(material.name).toObject(); - float scattering = materialOptions.contains("scattering") ? materialOptions.value("scattering").toDouble() : 1.0f; - QByteArray scatteringMap = materialOptions.value("scatteringMap").toVariant().toByteArray(); - qDebug() << "Replacing material:" << material.name << "with skin scattering effect. scattering:" << scattering << "scatteringMap:" << scatteringMap; - material._material->setScattering(scattering); - material.scatteringTexture = FBXTexture(); - material.scatteringTexture.name = material.name + ".scatteringMap"; - material.scatteringTexture.filename = scatteringMap; + qDebug() << "Mapping fbx material:" << material.name << " with HifiMaterial: " << materialOptions; + + if (materialOptions.contains("scattering")) { + float scattering = (float) materialOptions.value("scattering").toDouble(); + material._material->setScattering(scattering); + } + + if (materialOptions.contains("scatteringMap")) { + QByteArray scatteringMap = materialOptions.value("scatteringMap").toVariant().toByteArray(); + material.scatteringTexture = FBXTexture(); + material.scatteringTexture.name = material.name + ".scatteringMap"; + material.scatteringTexture.filename = scatteringMap; + } } if (material.opacity <= 0.0f) { diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index c8308ce3f0..5375de273a 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -43,13 +43,11 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { - auto colorFormat = gpu::Element::COLOR_SRGBA_32; - auto linearFormat = gpu::Element::COLOR_RGBA_32; + auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto width = _frameBufferSize.width(); auto height = _frameBufferSize.height(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); @@ -74,7 +72,7 @@ void FramebufferCache::resizeAmbientOcclusionBuffers() { auto height = _frameBufferSize.height() >> _AOResolutionLevel; auto colorFormat = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGB); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); - auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format + // auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format _occlusionTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 891b59a726..a09c290b8d 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -122,7 +122,7 @@ void LightingModel::setShowLightContour(bool enable) { } } bool LightingModel::isShowLightContourEnabled() const { - return (bool)(_parametersBuffer.get().showLightContour > 0.0); + return (bool)(_parametersBuffer.get().showLightContour > 0.0f); } MakeLightingModel::MakeLightingModel() { diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d186b1ac1c..5299b395ca 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -217,6 +217,8 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; + + // Setup camera, projection and viewport for all items batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); @@ -228,6 +230,9 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat); + // Setup lighting model for all items; + batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); args->_batch = nullptr; }); @@ -248,6 +253,8 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; + + // Setup camera, projection and viewport for all items batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); @@ -259,6 +266,9 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat); + // Setup lighting model for all items; + batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); + if (_stateSort) { renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); } else { @@ -315,6 +325,9 @@ void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderCon batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat); + // Setup lighting model for all items; + batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); args->_batch = nullptr; }); @@ -361,7 +374,8 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const assert(renderContext->args->hasViewFrustum()); const auto& inItems = inputs.get0(); - const auto& lightingModel = inputs.get1(); + // Not used yet + // const auto& lightingModel = inputs.get1(); RenderArgs* args = renderContext->args; doInBatch(args->_context, [&](gpu::Batch& batch) { diff --git a/tools/scribe/src/TextTemplate.cpp b/tools/scribe/src/TextTemplate.cpp index 1fad1eac02..89937c4da6 100755 --- a/tools/scribe/src/TextTemplate.cpp +++ b/tools/scribe/src/TextTemplate.cpp @@ -741,7 +741,6 @@ int TextTemplate::evalBlockGeneration(std::ostream& dst, const BlockPointer& blo std::vector< String > paramCache; paramCache.push_back(""); String val; - bool valIsVar = false; for (int i = 1; i < nbParams; i++) { val = block->command.arguments[i]; if ((val[0] == Tag::VAR) && (val[val.length()-1] == Tag::VAR)) { @@ -752,7 +751,6 @@ int TextTemplate::evalBlockGeneration(std::ostream& dst, const BlockPointer& blo } else { val = Tag::NULL_VAR; } - valIsVar = true; } Vars::iterator it = vars.find(funcBlock->command.arguments[i]); From c951f507e3be2383a88b43c48e69201ce653ecdc Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 13 Jul 2016 11:28:36 -0700 Subject: [PATCH 1060/1237] Fixed issue with calling setOptions Since the stereo option is computed from the .wav file, if you call setOptions later (like the cow.js does), it resets stereo to false. So, I now just copy the stereo flag into the new options, since the sound file is the same. Also, calling AudioClient::outputLocalInjector on the AudioClient thread now to avoid potential concurrency issues accessing the vector of injectors. --- libraries/audio-client/src/AudioClient.cpp | 25 ++++++++++++++-------- libraries/audio/src/AudioInjector.cpp | 20 +++++++++++++---- libraries/audio/src/AudioInjector.h | 3 +-- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index cb53e8f5de..799c357944 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -823,7 +823,6 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { } else { qDebug() << "injector has no more data, marking finished for removal"; - injector->finish(); injectorsToRemove.append(injector); } @@ -831,7 +830,6 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { } else { qDebug() << "injector has no local buffer, marking as finished for removal"; - injector->finish(); injectorsToRemove.append(injector); } @@ -933,15 +931,24 @@ void AudioClient::setIsStereoInput(bool isStereoInput) { bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { if (injector->getLocalBuffer() && _audioInput ) { - // just add it to the vector of active local injectors - // TODO: deal with concurrency perhaps? Maybe not - qDebug() << "adding new injector!!!!!!!"; - - _activeLocalAudioInjectors.append(injector); + // just add it to the vector of active local injectors, if + // not already there. + // Since this is invoked with invokeMethod, there _should_ be + // no reason to lock access to the vector of injectors. + if (!_activeLocalAudioInjectors.contains(injector)) { + qDebug() << "adding new injector"; + + _activeLocalAudioInjectors.append(injector); + } else { + qDebug() << "injector exists in active list already"; + } + return true; - } - return false; + } else { + // no local buffer or audio + return false; + } } void AudioClient::outputFormatChanged() { diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 878a4c627c..d4382340bf 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -26,6 +26,8 @@ #include "AudioInjector.h" +int audioInjectorPtrMetaTypeId = qRegisterMetaType(); + AudioInjector::AudioInjector(QObject* parent) : QObject(parent) { @@ -41,11 +43,20 @@ AudioInjector::AudioInjector(const Sound& sound, const AudioInjectorOptions& inj AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions) : _audioData(audioData), - _options(injectorOptions) + _options(injectorOptions) { } +void AudioInjector::setOptions(const AudioInjectorOptions& options) { + // since options.stereo is computed from the audio stream, + // we need to copy it from existing options just in case. + bool currentlyStereo = _options.stereo; + _options = options; + _options.stereo = currentlyStereo; +} + + void AudioInjector::finish() { bool shouldDelete = (_state == State::NotFinishedWithPendingDelete); _state = State::Finished; @@ -115,11 +126,11 @@ void AudioInjector::restart() { _hasSetup = false; _shouldStop = false; _state = State::NotFinished; - + // call inject audio to start injection over again setupInjection(); - // if we're a local injector call inject locally to start injecting again + // if we're a local injector, just inject again if (_options.localOnly) { injectLocally(); } else { @@ -145,7 +156,8 @@ bool AudioInjector::injectLocally() { // give our current send position to the local buffer _localBuffer->setCurrentOffset(_currentSendOffset); - success = _localAudioInterface->outputLocalInjector(_options.stereo, this); + // call this function on the AudioClient's thread + success = QMetaObject::invokeMethod(_localAudioInterface, "outputLocalInjector", Q_ARG(bool, _options.stereo), Q_ARG(AudioInjector*, this)); if (!success) { qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index c74497ce4d..1af733ace6 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -75,7 +75,7 @@ public slots: void stopAndDeleteLater(); const AudioInjectorOptions& getOptions() const { return _options; } - void setOptions(const AudioInjectorOptions& options) { _options = options; } + void setOptions(const AudioInjectorOptions& options); float getLoudness() const { return _loudness; } bool isPlaying() const { return _state == State::NotFinished || _state == State::NotFinishedWithPendingDelete; } @@ -111,5 +111,4 @@ private: friend class AudioInjectorManager; }; - #endif // hifi_AudioInjector_h From b4b7b7ea34aeaf98a07bc8a98a0cab6f52484048 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 13 Jul 2016 13:41:45 -0700 Subject: [PATCH 1061/1237] Update all asset server responses to be reliable --- assignment-client/src/assets/AssetServer.cpp | 5 +++-- assignment-client/src/assets/UploadAssetTask.cpp | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 7f43b86328..905cc6fd30 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -298,7 +298,8 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh message->readPrimitive(&messageID); assetHash = message->readWithoutCopy(SHA256_HASH_LENGTH); - auto replyPacket = NLPacket::create(PacketType::AssetGetInfoReply); + auto size = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(AssetServerError) + sizeof(qint64)); + auto replyPacket = NLPacket::create(PacketType::AssetGetInfoReply, size, true); QByteArray hexHash = assetHash.toHex(); @@ -347,7 +348,7 @@ void AssetServer::handleAssetUpload(QSharedPointer message, Sha // for now this also means it isn't allowed to add assets // so return a packet with error that indicates that - auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetServerError)); + auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetServerError), true); MessageID messageID; message->readPrimitive(&messageID); diff --git a/assignment-client/src/assets/UploadAssetTask.cpp b/assignment-client/src/assets/UploadAssetTask.cpp index 5ca9b5bbf1..e09619a3cc 100644 --- a/assignment-client/src/assets/UploadAssetTask.cpp +++ b/assignment-client/src/assets/UploadAssetTask.cpp @@ -43,7 +43,7 @@ void UploadAssetTask::run() { qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID()); - auto replyPacket = NLPacket::create(PacketType::AssetUploadReply); + auto replyPacket = NLPacket::create(PacketType::AssetUploadReply, -1, true); replyPacket->writePrimitive(messageID); if (fileSize > MAX_UPLOAD_SIZE) { From b6ffabf5003228385b1543f13283aa44d5f7883a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 13 Jul 2016 09:08:57 -0700 Subject: [PATCH 1062/1237] attempt to get mac sandbox plugins to work --- cmake/macros/GenerateInstallers.cmake | 26 +++++++++++++++++++ .../macros/SetupHifiClientServerPlugin.cmake | 3 ++- server-console/CMakeLists.txt | 26 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index 0def701739..2d2ede69f0 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -78,6 +78,32 @@ macro(GENERATE_INSTALLERS) install(CODE "execute_process(COMMAND SetFile -a V \${CMAKE_INSTALL_PREFIX}/${ESCAPED_DMG_SUBFOLDER_NAME}/Icon\\r)") endif () + # this is a bit of a hack, but I couldn't find any other way to get plugins + # into the correct place under the console path. On windows this is handled + # exclusively in the plugins directories with the SetupHiFiClientServerPlugin + # macro + if (APPLE) + set(CONSOLE_PLUGINS_DIR "${CONSOLE_INSTALL_APP_PATH}/Contents/MacOS/Components.app/Contents/PlugIns") + set(SERVER_PLUGINS_DIR "${CMAKE_BINARY_DIR}/assignment-client/${CMAKE_BUILD_TYPE}/plugins") + + message("TARGET_NAME: ${TARGET_NAME}") + message("CONFIGURATION: ${CONFIGURATION}") + message(": $") + message("CONSOLE_PLUGINS_DIR: ${CONSOLE_PLUGINS_DIR}") + message("SERVER_PLUGINS_DIR: ${SERVER_PLUGINS_DIR}") + + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory + ${CONSOLE_PLUGINS_DIR} + ) + + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + ${SERVER_PLUGINS_DIR} + ${CONSOLE_PLUGINS_DIR} + ) + endif () + # configure a cpack properties file for custom variables in template set(CPACK_CONFIGURED_PROP_FILE "${CMAKE_CURRENT_BINARY_DIR}/CPackCustomProperties.cmake") configure_file("${HF_CMAKE_DIR}/templates/CPackProperties.cmake.in" ${CPACK_CONFIGURED_PROP_FILE}) diff --git a/cmake/macros/SetupHifiClientServerPlugin.cmake b/cmake/macros/SetupHifiClientServerPlugin.cmake index dfe3113fbc..a065ee99b6 100644 --- a/cmake/macros/SetupHifiClientServerPlugin.cmake +++ b/cmake/macros/SetupHifiClientServerPlugin.cmake @@ -13,7 +13,8 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) if (APPLE) set(CLIENT_PLUGIN_PATH "${INTERFACE_BUNDLE_NAME}.app/Contents/PlugIns") - set(SERVER_PLUGIN_PATH "Components.app/Contents/PlugIns") + #set(SERVER_PLUGIN_PATH "Components.app/Contents/PlugIns") + set(SERVER_PLUGIN_PATH "plugins") else() set(CLIENT_PLUGIN_PATH "plugins") set(SERVER_PLUGIN_PATH "plugins") diff --git a/server-console/CMakeLists.txt b/server-console/CMakeLists.txt index 1c6e40c582..7b9c17ace4 100644 --- a/server-console/CMakeLists.txt +++ b/server-console/CMakeLists.txt @@ -72,3 +72,29 @@ else () set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) set_target_properties(${TARGET_NAME}-npm-install PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) endif () + +# this is a bit of a hack, but I couldn't find any other way to get plugins +# into the correct place under the console path. On windows this is handled +# exclusively in the plugins directories with the SetupHiFiClientServerPlugin +# macro +if (APPLE) + set(CONSOLE_PLUGINS_DIR "${CONSOLE_INSTALL_APP_PATH}/Contents/MacOS/Components.app/Contents/PlugIns") + set(SERVER_PLUGINS_DIR "${CMAKE_BINARY_DIR}/assignment-client/${CMAKE_BUILD_TYPE}/plugins") + + message("TARGET_NAME: ${TARGET_NAME}") + message("CONFIGURATION: ${CONFIGURATION}") + message(": $") + message("CONSOLE_PLUGINS_DIR: ${CONSOLE_PLUGINS_DIR}") + message("SERVER_PLUGINS_DIR: ${SERVER_PLUGINS_DIR}") + + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory + ${CONSOLE_PLUGINS_DIR} + ) + + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + ${SERVER_PLUGINS_DIR} + ${CONSOLE_PLUGINS_DIR} + ) +endif () From f6681d4b3dd39424fa1478a07795dd4d1ac5038f Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 7 Jul 2016 13:07:43 -0700 Subject: [PATCH 1063/1237] remove unused cruft --- libraries/render-utils/src/GeometryCache.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 1bfdc1798e..385f2c6fa4 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -402,8 +402,6 @@ private: QHash _gridBuffers; QHash _registeredGridBuffers; - QHash > _networkGeometry; - gpu::ShaderPointer _simpleShader; gpu::ShaderPointer _unlitShader; static render::ShapePipelinePointer _simplePipeline; From 9f7d2cf2630832aa25f9af9a2e78539d9129c05f Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 7 Jul 2016 13:08:18 -0700 Subject: [PATCH 1064/1237] NetworkGeometry --> GeometryResourceWatcher Model class gets render and collision geometries with watchers also changed names for readability --- .../src/model-networking/ModelCache.cpp | 76 +++++++++------- .../src/model-networking/ModelCache.h | 87 +++++++++---------- .../render-utils/src/MeshPartPayload.cpp | 6 +- libraries/render-utils/src/Model.cpp | 77 ++++++++-------- libraries/render-utils/src/Model.h | 33 ++++--- 5 files changed, 144 insertions(+), 135 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 40388e6123..afe86e0d1e 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -70,7 +70,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { auto modelCache = DependencyManager::get(); GeometryExtra extra{ mapping, _textureBaseUrl }; - // Get the raw GeometryResource, not the wrapped NetworkGeometry + // Get the raw GeometryResource _geometryResource = modelCache->getResource(url, QUrl(), &extra).staticCast(); // Avoid caching nested resources - their references will be held by the parent _geometryResource->_isCacheable = false; @@ -90,8 +90,8 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { void GeometryMappingResource::onGeometryMappingLoaded(bool success) { if (success && _geometryResource) { - _geometry = _geometryResource->_geometry; - _shapes = _geometryResource->_shapes; + _fbxGeometry = _geometryResource->_fbxGeometry; + _meshParts = _geometryResource->_meshParts; _meshes = _geometryResource->_meshes; _materials = _geometryResource->_materials; @@ -200,31 +200,31 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxGeometry) { // Assume ownership of the geometry pointer - _geometry = fbxGeometry; + _fbxGeometry = fbxGeometry; // Copy materials QHash materialIDAtlas; - for (const FBXMaterial& material : _geometry->materials) { + for (const FBXMaterial& material : _fbxGeometry->materials) { materialIDAtlas[material.materialID] = _materials.size(); _materials.push_back(std::make_shared(material, _textureBaseUrl)); } - std::shared_ptr meshes = std::make_shared(); - std::shared_ptr shapes = std::make_shared(); + std::shared_ptr meshes = std::make_shared(); + std::shared_ptr shapes = std::make_shared(); int meshID = 0; - for (const FBXMesh& mesh : _geometry->meshes) { + for (const FBXMesh& mesh : _fbxGeometry->meshes) { // Copy mesh pointers meshes->emplace_back(mesh._mesh); int partID = 0; for (const FBXMeshPart& part : mesh.parts) { // Construct local shapes - shapes->push_back(std::make_shared(meshID, partID, (int)materialIDAtlas[part.materialID])); + shapes->push_back(std::make_shared(meshID, partID, (int)materialIDAtlas[part.materialID])); partID++; } meshID++; } _meshes = meshes; - _shapes = shapes; + _meshParts = shapes; finishedLoading(true); } @@ -250,17 +250,15 @@ QSharedPointer ModelCache::createResource(const QUrl& url, const QShar return QSharedPointer(resource, &Resource::deleter); } -std::shared_ptr ModelCache::getGeometry(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) { +GeometryResource::Pointer ModelCache::fetchResource(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) { GeometryExtra geometryExtra = { mapping, textureBaseUrl }; GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra).staticCast(); if (resource) { if (resource->isLoaded() && resource->shouldSetTextures()) { resource->setTextures(); } - return std::make_shared(resource); - } else { - return NetworkGeometry::Pointer(); } + return resource; } const QVariantMap Geometry::getTextures() const { @@ -278,9 +276,9 @@ const QVariantMap Geometry::getTextures() const { // FIXME: The materials should only be copied when modified, but the Model currently caches the original Geometry::Geometry(const Geometry& geometry) { - _geometry = geometry._geometry; + _fbxGeometry = geometry._fbxGeometry; _meshes = geometry._meshes; - _shapes = geometry._shapes; + _meshParts = geometry._meshParts; _materials.reserve(geometry._materials.size()); for (const auto& material : geometry._materials) { @@ -337,8 +335,8 @@ bool Geometry::areTexturesLoaded() const { } const std::shared_ptr Geometry::getShapeMaterial(int shapeID) const { - if ((shapeID >= 0) && (shapeID < (int)_shapes->size())) { - int materialID = _shapes->at(shapeID)->materialID; + if ((shapeID >= 0) && (shapeID < (int)_meshParts->size())) { + int materialID = _meshParts->at(shapeID)->materialID; if ((materialID >= 0) && (materialID < (int)_materials.size())) { return _materials[materialID]; } @@ -352,7 +350,7 @@ void GeometryResource::deleter() { } void GeometryResource::setTextures() { - for (const FBXMaterial& material : _geometry->materials) { + for (const FBXMaterial& material : _fbxGeometry->materials) { _materials.push_back(std::make_shared(material, _textureBaseUrl)); } } @@ -361,26 +359,40 @@ void GeometryResource::resetTextures() { _materials.clear(); } -NetworkGeometry::NetworkGeometry(const GeometryResource::Pointer& networkGeometry) : _resource(networkGeometry) { - connect(_resource.data(), &Resource::finished, this, &NetworkGeometry::resourceFinished); - connect(_resource.data(), &Resource::onRefresh, this, &NetworkGeometry::resourceRefreshed); +void GeometryResourceWatcher::startWatching() { + connect(_resource.data(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); + connect(_resource.data(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); if (_resource->isLoaded()) { resourceFinished(!_resource->getURL().isEmpty()); } } -void NetworkGeometry::resourceFinished(bool success) { - // FIXME: Model is not set up to handle a refresh - if (_instance) { - return; - } - if (success) { - _instance = std::make_shared(*_resource); - } - emit finished(success); +void GeometryResourceWatcher::stopWatching() { + disconnect(_resource.data(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); + disconnect(_resource.data(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); } -void NetworkGeometry::resourceRefreshed() { +void GeometryResourceWatcher::setResource(GeometryResource::Pointer resource) { + if (_resource) { + stopWatching(); + } + _resource = resource; + if (_resource) { + if (_resource->isLoaded()) { + _geometryRef = std::make_shared(*_resource); + } else { + startWatching(); + } + } +} + +void GeometryResourceWatcher::resourceFinished(bool success) { + if (success) { + _geometryRef = std::make_shared(*_resource); + } +} + +void GeometryResourceWatcher::resourceRefreshed() { // FIXME: Model is not set up to handle a refresh // _instance.reset(); } diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index f15e1106e2..c450f96846 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -22,52 +22,30 @@ #include "TextureCache.h" // Alias instead of derive to avoid copying -using NetworkMesh = model::Mesh; class NetworkTexture; class NetworkMaterial; -class NetworkShape; -class NetworkGeometry; +class MeshPart; class GeometryMappingResource; -/// Stores cached model geometries. -class ModelCache : public ResourceCache, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: - /// Loads a model geometry from the specified URL. - std::shared_ptr getGeometry(const QUrl& url, - const QVariantHash& mapping = QVariantHash(), const QUrl& textureBaseUrl = QUrl()); - -protected: - friend class GeometryMappingResource; - - virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, - const void* extra); - -private: - ModelCache(); - virtual ~ModelCache() = default; -}; - class Geometry { public: using Pointer = std::shared_ptr; + using WeakPointer = std::weak_ptr; Geometry() = default; Geometry(const Geometry& geometry); // Immutable over lifetime - using NetworkMeshes = std::vector>; - using NetworkShapes = std::vector>; + using GeometryMeshes = std::vector>; + using GeometryMeshParts = std::vector>; // Mutable, but must retain structure of vector using NetworkMaterials = std::vector>; - const FBXGeometry& getGeometry() const { return *_geometry; } - const NetworkMeshes& getMeshes() const { return *_meshes; } + const FBXGeometry& getFBXGeometry() const { return *_fbxGeometry; } + const GeometryMeshes& getMeshes() const { return *_meshes; } const std::shared_ptr getShapeMaterial(int shapeID) const; const QVariantMap getTextures() const; @@ -79,9 +57,9 @@ protected: friend class GeometryMappingResource; // Shared across all geometries, constant throughout lifetime - std::shared_ptr _geometry; - std::shared_ptr _meshes; - std::shared_ptr _shapes; + std::shared_ptr _fbxGeometry; + std::shared_ptr _meshes; + std::shared_ptr _meshParts; // Copied to each geometry, mutable throughout lifetime via setTextures NetworkMaterials _materials; @@ -108,7 +86,7 @@ protected: // Geometries may not hold onto textures while cached - that is for the texture cache // Instead, these methods clear and reset textures from the geometry when caching/loading - bool shouldSetTextures() const { return _geometry && _materials.empty(); } + bool shouldSetTextures() const { return _fbxGeometry && _materials.empty(); } void setTextures(); void resetTextures(); @@ -118,22 +96,21 @@ protected: bool _isCacheable { true }; }; -class NetworkGeometry : public QObject { +class GeometryResourceWatcher : public QObject { Q_OBJECT public: - using Pointer = std::shared_ptr; + using Pointer = std::shared_ptr; - NetworkGeometry() = delete; - NetworkGeometry(const GeometryResource::Pointer& networkGeometry); + GeometryResourceWatcher() = delete; + GeometryResourceWatcher(Geometry::Pointer& geometryPtr) : _geometryRef(geometryPtr) {} - const QUrl& getURL() { return _resource->getURL(); } + void setResource(GeometryResource::Pointer resource); - /// Returns the geometry, if it is loaded (must be checked!) - const Geometry::Pointer& getGeometry() { return _instance; } + const QUrl& getURL() const { return _resource->getURL(); } -signals: - /// Emitted when the NetworkGeometry loads (or fails to) - void finished(bool success); +private: + void startWatching(); + void stopWatching(); private slots: void resourceFinished(bool success); @@ -141,7 +118,27 @@ private slots: private: GeometryResource::Pointer _resource; - Geometry::Pointer _instance { nullptr }; + Geometry::Pointer& _geometryRef; +}; + +/// Stores cached model geometries. +class ModelCache : public ResourceCache, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + GeometryResource::Pointer fetchResource(const QUrl& url, + const QVariantHash& mapping = QVariantHash(), const QUrl& textureBaseUrl = QUrl()); + +protected: + friend class GeometryMappingResource; + + virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, + const void* extra); + +private: + ModelCache(); + virtual ~ModelCache() = default; }; class NetworkMaterial : public model::Material { @@ -185,9 +182,9 @@ private: bool _isOriginal { true }; }; -class NetworkShape { +class MeshPart { public: - NetworkShape(int mesh, int part, int material) : meshID { mesh }, partID { part }, materialID { material } {} + MeshPart(int mesh, int part, int material) : meshID { mesh }, partID { part }, materialID { material } {} int meshID { -1 }; int partID { -1 }; int materialID { -1 }; diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 08c8dc23b4..4746b5a0c5 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -310,7 +310,7 @@ ModelMeshPartPayload::ModelMeshPartPayload(Model* model, int _meshIndex, int par _shapeID(shapeIndex) { assert(_model && _model->isLoaded()); - auto& modelMesh = _model->getGeometry()->getGeometry()->getMeshes().at(_meshIndex); + auto& modelMesh = _model->getGeometry()->getMeshes().at(_meshIndex); updateMeshPart(modelMesh, partIndex); updateTransform(transform, offsetTransform); @@ -331,7 +331,7 @@ void ModelMeshPartPayload::initCache() { _isBlendShaped = !mesh.blendshapes.isEmpty(); } - auto networkMaterial = _model->getGeometry()->getGeometry()->getShapeMaterial(_shapeID); + auto networkMaterial = _model->getGeometry()->getShapeMaterial(_shapeID); if (networkMaterial) { _drawMaterial = networkMaterial; }; @@ -384,7 +384,7 @@ ItemKey ModelMeshPartPayload::getKey() const { ShapeKey ModelMeshPartPayload::getShapeKey() const { assert(_model->isLoaded()); const FBXGeometry& geometry = _model->getFBXGeometry(); - const auto& networkMeshes = _model->getGeometry()->getGeometry()->getMeshes(); + const auto& networkMeshes = _model->getGeometry()->getMeshes(); // guard against partially loaded meshes if (_meshIndex >= (int)networkMeshes.size() || _meshIndex >= (int)geometry.meshes.size() || _meshIndex >= (int)_model->_meshStates.size()) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 43eced3107..ab9e0e7403 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -31,7 +31,7 @@ using namespace std; int nakedModelPointerTypeId = qRegisterMetaType(); -int weakNetworkGeometryPointerTypeId = qRegisterMetaType >(); +int weakGeometryResourceBridgePointerTypeId = qRegisterMetaType(); int vec3VectorTypeId = qRegisterMetaType >(); float Model::FAKE_DIMENSION_PLACEHOLDER = -1.0f; #define HTTP_INVALID_COM "http://invalid.com" @@ -79,6 +79,10 @@ void initCollisionHullMaterials() { Model::Model(RigPointer rig, QObject* parent) : QObject(parent), + _renderGeometry(), + _collisionGeometry(), + _renderWatcher(_renderGeometry), + _collisionWatcher(_collisionGeometry), _translation(0.0f), _rotation(), _scale(1.0f, 1.0f, 1.0f), @@ -98,7 +102,6 @@ Model::Model(RigPointer rig, QObject* parent) : _calculatedMeshTrianglesValid(false), _meshGroupsKnown(false), _isWireframe(false), - _renderCollisionHull(false), _rig(rig) { // we may have been created in the network thread, but we live in the main thread if (_viewState) { @@ -116,7 +119,7 @@ AbstractViewStateInterface* Model::_viewState = NULL; bool Model::needsFixupInScene() const { if (readyToAddToScene()) { - if (_needsUpdateTextures && _geometry->getGeometry()->areTexturesLoaded()) { + if (_needsUpdateTextures && _renderGeometry->areTexturesLoaded()) { _needsUpdateTextures = false; return true; } @@ -793,13 +796,13 @@ int Model::getLastFreeJointIndex(int jointIndex) const { void Model::setTextures(const QVariantMap& textures) { if (isLoaded()) { _needsUpdateTextures = true; - _geometry->getGeometry()->setTextures(textures); + _renderGeometry->setTextures(textures); } } void Model::setURL(const QUrl& url) { // don't recreate the geometry if it's the same URL - if (_url == url && _geometry && _geometry->getURL() == url) { + if (_url == url && _renderWatcher.getURL() == url) { return; } @@ -818,7 +821,7 @@ void Model::setURL(const QUrl& url) { invalidCalculatedMeshBoxes(); deleteGeometry(); - _geometry = DependencyManager::get()->getGeometry(url); + _renderWatcher.setResource(DependencyManager::get()->fetchResource(url)); onInvalidate(); } @@ -827,7 +830,7 @@ void Model::setCollisionModelURL(const QUrl& url) { return; } _collisionUrl = url; - _collisionGeometry = DependencyManager::get()->getGeometry(url); + _collisionWatcher.setResource(DependencyManager::get()->fetchResource(url)); } bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { @@ -883,7 +886,7 @@ QStringList Model::getJointNames() const { class Blender : public QRunnable { public: - Blender(ModelPointer model, int blendNumber, const std::weak_ptr& geometry, + Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& meshes, const QVector& blendshapeCoefficients); virtual void run(); @@ -892,12 +895,12 @@ private: ModelPointer _model; int _blendNumber; - std::weak_ptr _geometry; + Geometry::WeakPointer _geometry; QVector _meshes; QVector _blendshapeCoefficients; }; -Blender::Blender(ModelPointer model, int blendNumber, const std::weak_ptr& geometry, +Blender::Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& meshes, const QVector& blendshapeCoefficients) : _model(model), _blendNumber(blendNumber), @@ -940,7 +943,7 @@ void Blender::run() { // post the result to the geometry cache, which will dispatch to the model if still alive QMetaObject::invokeMethod(DependencyManager::get().data(), "setBlendedVertices", Q_ARG(ModelPointer, _model), Q_ARG(int, _blendNumber), - Q_ARG(const std::weak_ptr&, _geometry), Q_ARG(const QVector&, vertices), + Q_ARG(const Geometry::WeakPointer&, _geometry), Q_ARG(const QVector&, vertices), Q_ARG(const QVector&, normals)); } @@ -1151,7 +1154,7 @@ bool Model::maybeStartBlender() { if (isLoaded()) { const FBXGeometry& fbxGeometry = getFBXGeometry(); if (fbxGeometry.hasBlendedMeshes()) { - QThreadPool::globalInstance()->start(new Blender(getThisPointer(), ++_blendNumber, _geometry, + QThreadPool::globalInstance()->start(new Blender(getThisPointer(), ++_blendNumber, _renderGeometry, fbxGeometry.meshes, _blendshapeCoefficients)); return true; } @@ -1159,10 +1162,10 @@ bool Model::maybeStartBlender() { return false; } -void Model::setBlendedVertices(int blendNumber, const std::weak_ptr& geometry, +void Model::setBlendedVertices(int blendNumber, const Geometry::WeakPointer& geometry, const QVector& vertices, const QVector& normals) { auto geometryRef = geometry.lock(); - if (!geometryRef || _geometry != geometryRef || _blendedVertexBuffers.empty() || blendNumber < _appliedBlendNumber) { + if (!geometryRef || _renderGeometry != geometryRef || _blendedVertexBuffers.empty() || blendNumber < _appliedBlendNumber) { return; } _appliedBlendNumber = blendNumber; @@ -1205,27 +1208,23 @@ AABox Model::getRenderableMeshBound() const { } void Model::segregateMeshGroups() { - NetworkGeometry::Pointer networkGeometry; + Geometry::Pointer geometry; bool showingCollisionHull = false; if (_showCollisionHull && _collisionGeometry) { if (isCollisionLoaded()) { - networkGeometry = _collisionGeometry; + geometry = _collisionGeometry; showingCollisionHull = true; } else { return; } } else { assert(isLoaded()); - networkGeometry = _geometry; + geometry = _renderGeometry; } - const FBXGeometry& geometry = networkGeometry->getGeometry()->getGeometry(); - const auto& networkMeshes = networkGeometry->getGeometry()->getMeshes(); + const auto& meshes = geometry->getMeshes(); // all of our mesh vectors must match in size - auto geoMeshesSize = geometry.meshes.size(); - if ((int)networkMeshes.size() != geoMeshesSize || - // geometry.meshes.size() != _meshStates.size()) { - geoMeshesSize > _meshStates.size()) { + if ((int)meshes.size() != _meshStates.size()) { qDebug() << "WARNING!!!! Mesh Sizes don't match! We will not segregate mesh groups yet."; return; } @@ -1249,23 +1248,25 @@ void Model::segregateMeshGroups() { // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; - for (int i = 0; i < (int)networkMeshes.size(); i++) { - const FBXMesh& mesh = geometry.meshes.at(i); - const auto& networkMesh = networkMeshes.at(i); + uint32_t numMeshes = (uint32_t)meshes.size(); + for (uint32_t i = 0; i < numMeshes; i++) { + const auto& mesh = meshes.at(i); + if (mesh) { - // Create the render payloads - int totalParts = mesh.parts.size(); - for (int partIndex = 0; partIndex < totalParts; partIndex++) { - if (showingCollisionHull) { - if (_collisionHullMaterials.empty()) { - initCollisionHullMaterials(); + // Create the render payloads + int numParts = (int)mesh->getNumParts(); + for (int partIndex = 0; partIndex < numParts; partIndex++) { + if (showingCollisionHull) { + if (_collisionHullMaterials.empty()) { + initCollisionHullMaterials(); + } + _collisionRenderItemsSet << std::make_shared(mesh, partIndex, _collisionHullMaterials[partIndex % NUM_COLLISION_HULL_COLORS], transform, offset); + } else { + _modelMeshRenderItemsSet << std::make_shared(this, i, partIndex, shapeID, transform, offset); } - _collisionRenderItemsSet << std::make_shared(networkMesh, partIndex, _collisionHullMaterials[partIndex % NUM_COLLISION_HULL_COLORS], transform, offset); - } else { - _modelMeshRenderItemsSet << std::make_shared(this, i, partIndex, shapeID, transform, offset); - } - shapeID++; + shapeID++; + } } } _meshGroupsKnown = true; @@ -1328,7 +1329,7 @@ void ModelBlender::noteRequiresBlend(ModelPointer model) { } void ModelBlender::setBlendedVertices(ModelPointer model, int blendNumber, - const std::weak_ptr& geometry, const QVector& vertices, const QVector& normals) { + const Geometry::WeakPointer& geometry, const QVector& vertices, const QVector& normals) { if (model) { model->setBlendedVertices(blendNumber, geometry, vertices, normals); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 6a7c9ec560..aa0c49f720 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -97,7 +97,7 @@ public: bool showCollisionHull = false); void removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges); void renderSetup(RenderArgs* args); - bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && getGeometry()->getGeometry()->getMeshes().empty()); } + bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && _renderGeometry->getMeshes().empty()); } bool isVisible() const { return _isVisible; } @@ -107,11 +107,11 @@ public: bool maybeStartBlender(); /// Sets blended vertices computed in a separate thread. - void setBlendedVertices(int blendNumber, const std::weak_ptr& geometry, + void setBlendedVertices(int blendNumber, const Geometry::WeakPointer& geometry, const QVector& vertices, const QVector& normals); - bool isLoaded() const { return _geometry && _geometry->getGeometry(); } - bool isCollisionLoaded() const { return _collisionGeometry && _collisionGeometry->getGeometry(); } + bool isLoaded() const { return (bool)_renderGeometry; } + bool isCollisionLoaded() const { return (bool)_collisionGeometry; } void setIsWireframe(bool isWireframe) { _isWireframe = isWireframe; } bool isWireframe() const { return _isWireframe; } @@ -128,18 +128,18 @@ public: virtual void updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrientation); /// Returns a reference to the shared geometry. - const NetworkGeometry::Pointer& getGeometry() const { return _geometry; } + const Geometry::Pointer& getGeometry() const { return _renderGeometry; } /// Returns a reference to the shared collision geometry. - const NetworkGeometry::Pointer& getCollisionGeometry() const { return _collisionGeometry; } + const Geometry::Pointer& getCollisionGeometry() const { return _collisionGeometry; } - const QVariantMap getTextures() const { assert(isLoaded()); return _geometry->getGeometry()->getTextures(); } + const QVariantMap getTextures() const { assert(isLoaded()); return _renderGeometry->getTextures(); } void setTextures(const QVariantMap& textures); /// Provided as a convenience, will crash if !isLoaded() // And so that getGeometry() isn't chained everywhere - const FBXGeometry& getFBXGeometry() const { assert(isLoaded()); return getGeometry()->getGeometry()->getGeometry(); } + const FBXGeometry& getFBXGeometry() const { assert(isLoaded()); return _renderGeometry->getFBXGeometry(); } /// Provided as a convenience, will crash if !isCollisionLoaded() - const FBXGeometry& getCollisionFBXGeometry() const { assert(isCollisionLoaded()); return getCollisionGeometry()->getGeometry()->getGeometry(); } + const FBXGeometry& getCollisionFBXGeometry() const { assert(isCollisionLoaded()); return _collisionGeometry->getFBXGeometry(); } // Set the model to use for collisions. // Should only be called from the model's rendering thread to avoid access violations of changed geometry. @@ -263,7 +263,11 @@ protected: /// \return true if joint exists bool getJointPosition(int jointIndex, glm::vec3& position) const; - NetworkGeometry::Pointer _geometry; + Geometry::Pointer _renderGeometry; // only ever set by its watcher + Geometry::Pointer _collisionGeometry; // only ever set by its watcher + + GeometryResourceWatcher _renderWatcher; + GeometryResourceWatcher _collisionWatcher; glm::vec3 _translation; glm::quat _rotation; @@ -330,8 +334,6 @@ protected: void deleteGeometry(); void initJointTransforms(); - NetworkGeometry::Pointer _collisionGeometry; - float _pupilDilation; QVector _blendshapeCoefficients; @@ -373,9 +375,6 @@ protected: static AbstractViewStateInterface* _viewState; - bool _renderCollisionHull; - - QSet> _collisionRenderItemsSet; QMap _collisionRenderItems; @@ -395,7 +394,7 @@ protected: }; Q_DECLARE_METATYPE(ModelPointer) -Q_DECLARE_METATYPE(std::weak_ptr) +Q_DECLARE_METATYPE(Geometry::WeakPointer) /// Handle management of pending models that need blending class ModelBlender : public QObject, public Dependency { @@ -408,7 +407,7 @@ public: void noteRequiresBlend(ModelPointer model); public slots: - void setBlendedVertices(ModelPointer model, int blendNumber, const std::weak_ptr& geometry, + void setBlendedVertices(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& vertices, const QVector& normals); private: From a134ac72de1c9f08b46c916f6d8825d56487e59a Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 12:08:26 -0700 Subject: [PATCH 1065/1237] add packet type and plumbing for node ignore --- assignment-client/src/audio/AudioMixer.cpp | 8 +++++++- assignment-client/src/audio/AudioMixer.h | 1 + assignment-client/src/avatars/AvatarMixer.cpp | 18 ++++++++++++------ assignment-client/src/avatars/AvatarMixer.h | 2 ++ libraries/networking/src/LimitedNodeList.cpp | 16 ++++++++++++---- libraries/networking/src/LimitedNodeList.h | 1 + libraries/networking/src/Node.cpp | 8 ++++++++ libraries/networking/src/Node.h | 8 ++++++++ libraries/networking/src/udt/PacketHeaders.h | 2 +- 9 files changed, 52 insertions(+), 12 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 51c4e25410..002f32a60b 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -93,6 +93,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : this, "handleNodeAudioPacket"); packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat"); + packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -324,7 +325,8 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) { // loop through all other nodes that have sufficient audio to mix DependencyManager::get()->eachNode([&](const SharedNodePointer& otherNode){ - if (otherNode->getLinkedData()) { + // make sure that we have audio data for this other node and that it isn't being ignored by our listening node + if (otherNode->getLinkedData() && !node->isIgnoringNodeWithID(otherNode->getUUID())) { AudioMixerClientData* otherNodeClientData = (AudioMixerClientData*) otherNode->getLinkedData(); // enumerate the ARBs attached to the otherNode and add all that should be added to mix @@ -554,6 +556,10 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { }); } +void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + sendingNode->handleNodeIgnoreRequest(packet); +} + void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { auto injectorClientData = qobject_cast(sender()); if (injectorClientData) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 4b2a27120d..9b26989847 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -47,6 +47,7 @@ private slots: void handleMuteEnvironmentPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); + void handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void removeHRTFsForFinishedInjector(const QUuid& streamID); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 46599396ca..7930ab843f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -45,6 +45,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::AvatarData, this, "handleAvatarDataPacket"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); + packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); @@ -227,14 +228,15 @@ void AvatarMixer::broadcastAvatarData() { // send back a packet with other active node data to this node nodeList->eachMatchingNode( [&](const SharedNodePointer& otherNode)->bool { - if (!otherNode->getLinkedData()) { + // make sure we have data for this avatar, that it isn't the same node, + // and isn't an avatar that the viewing node has ignored + if (!otherNode->getLinkedData() + || otherNode->getUUID() == node->getUUID() + || node->isIgnoringNodeWithID(otherNode->getUUID())) { return false; + } else { + return true; } - if (otherNode->getUUID() == node->getUUID()) { - return false; - } - - return true; }, [&](const SharedNodePointer& otherNode) { ++numOtherAvatars; @@ -431,6 +433,10 @@ void AvatarMixer::handleKillAvatarPacket(QSharedPointer message DependencyManager::get()->processKillNode(*message); } +void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { + senderNode->handleNodeIgnoreRequest(message); +} + void AvatarMixer::sendStatsPacket() { QJsonObject statsObject; statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames; diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index d1a9249c83..00cf457d40 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -37,9 +37,11 @@ private slots: void handleAvatarDataPacket(QSharedPointer message, SharedNodePointer senderNode); void handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode); void handleKillAvatarPacket(QSharedPointer message); + void handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); + private: void broadcastAvatarData(); void parseDomainServerSettings(const QJsonObject& domainSettings); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a03fa43093..2f085dc3cb 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -427,10 +427,7 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointer message, SharedNodePointer sendingNode) { QMutexLocker locker(&sendingNode->getMutex()); - NodeData* linkedData = sendingNode->getLinkedData(); - if (!linkedData && linkedDataCreateCallback) { - linkedDataCreateCallback(sendingNode.data()); - } + NodeData* linkedData = getOrCreateLinkedData(sendingNode); if (linkedData) { QMutexLocker linkedDataLocker(&linkedData->getMutex()); @@ -440,6 +437,17 @@ int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointergetMutex()); + + NodeData* linkedData = node->getLinkedData(); + if (!linkedData && linkedDataCreateCallback) { + linkedDataCreateCallback(node.data()); + } + + return node->getLinkedData(); +} + SharedNodePointer LimitedNodeList::nodeWithUUID(const QUuid& nodeUUID) { QReadLocker readLocker(&_nodeMutex); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index c9054ac6d7..03e82f053f 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -149,6 +149,7 @@ public: void processKillNode(ReceivedMessage& message); int updateNodeWithDataFromPacket(QSharedPointer packet, SharedNodePointer matchingNode); + NodeData* getOrCreateLinkedData(SharedNodePointer node); unsigned int broadcastToNodes(std::unique_ptr packet, const NodeSet& destinationNodeTypes); SharedNodePointer soloNodeOfType(NodeType_t nodeType); diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 7201b2fd9a..c95b9d5129 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -78,6 +78,14 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } +void Node::handleNodeIgnoreRequest(QSharedPointer packet) { + // parse out the UUID being ignored from the packet + QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + // add the session UUID to the set of ignored ones for this listening node + _ignoredNodeIDSet.insert(ignoredUUID); +} + QDataStream& operator<<(QDataStream& out, const Node& node) { out << node._type; out << node._uuid; diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index b277ac0083..daebc1fcfb 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -21,6 +21,10 @@ #include #include +#include + +#include + #include "HifiSockAddr.h" #include "NetworkPeer.h" #include "NodeData.h" @@ -66,6 +70,9 @@ public: bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; } bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; } + void handleNodeIgnoreRequest(QSharedPointer packet); + bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } + friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node); @@ -84,6 +91,7 @@ private: QMutex _mutex; MovingPercentile _clockSkewMovingPercentile; NodePermissions _permissions; + tbb::concurrent_unordered_set _ignoredNodeIDSet; }; Q_DECLARE_METATYPE(Node*) diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 296ebb8e68..ac40f8100b 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -61,7 +61,7 @@ public: AssignmentClientStatus, NoisyMute, AvatarIdentity, - TYPE_UNUSED_1, + NodeIgnoreRequest, DomainConnectRequest, DomainServerRequireDTLS, NodeJsonStats, From bb68e777e634aab8729b80b9cfb4061de20be6e8 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 13:49:31 -0700 Subject: [PATCH 1066/1237] add a scripting interface to ignore users --- interface/src/Application.cpp | 3 ++ libraries/networking/src/Node.cpp | 2 + .../networking/src/udt/PacketHeaders.cpp | 2 - libraries/networking/src/udt/PacketHeaders.h | 1 - .../src/UsersScriptingInterface.cpp | 39 +++++++++++++++++++ .../src/UsersScriptingInterface.h | 28 +++++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 libraries/script-engine/src/UsersScriptingInterface.cpp create mode 100644 libraries/script-engine/src/UsersScriptingInterface.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 48bbccc3b6..a4ae0af1c5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -98,6 +98,7 @@ #include #include #include +#include #include #include #include @@ -454,6 +455,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -4755,6 +4757,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); } bool Application::canAcceptURL(const QString& urlString) const { diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index c95b9d5129..27598d10ae 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -82,6 +82,8 @@ void Node::handleNodeIgnoreRequest(QSharedPointer packet) { // parse out the UUID being ignored from the packet QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + qDebug() << "Adding" << ignoredUUID << "to ignore set for" << _uuid; + // add the session UUID to the set of ignored ones for this listening node _ignoredNodeIDSet.insert(ignoredUUID); } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6359ad0aff..99e10caabd 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -40,8 +40,6 @@ const QSet NON_SOURCED_PACKETS = QSet() << PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode << PacketType::DomainServerRemovedNode; -const QSet RELIABLE_PACKETS = QSet(); - PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ac40f8100b..9581f3ca20 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -109,7 +109,6 @@ typedef char PacketVersion; extern const QSet NON_VERIFIED_PACKETS; extern const QSet NON_SOURCED_PACKETS; -extern const QSet RELIABLE_PACKETS; PacketVersion versionForPacketType(PacketType packetType); QByteArray protocolVersionsSignature(); /// returns a unqiue signature for all the current protocols diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp new file mode 100644 index 0000000000..3a05d81de1 --- /dev/null +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -0,0 +1,39 @@ +// +// UsersScriptingInterface.cpp +// libraries/script-engine/src +// +// Created by Stephen Birarda on 2016-07-11. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "UsersScriptingInterface.h" + +#include + +void UsersScriptingInterface::ignore(const QUuid& nodeID) { + // setup the ignore packet we send to all nodes (that currently handle it) + // to ignore the data (audio/avatar) for this user + + // enumerate the nodes to send a reliable ignore packet to each that can leverage it + auto nodeList = DependencyManager::get(); + + nodeList->eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { + if (node->getType() != NodeType::AudioMixer || node->getType() != NodeType::AvatarMixer) { + return false; + } else { + return true; + } + }, [&nodeID, &nodeList](const SharedNodePointer& destinationNode) { + // create a reliable NLPacket with space for the ignore UUID + auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); + + // write the node ID to the packet + ignorePacket->write(nodeID.toRfc4122()); + + // send off this ignore packet reliably to the matching node + nodeList->sendPacket(std::move(ignorePacket), *destinationNode); + }); +} diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h new file mode 100644 index 0000000000..0dc62c088c --- /dev/null +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -0,0 +1,28 @@ +// +// UsersScriptingInterface.h +// libraries/script-engine/src +// +// Created by Stephen Birarda on 2016-07-11. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#ifndef hifi_UsersScriptingInterface_h +#define hifi_UsersScriptingInterface_h + +#include + +class UsersScriptingInterface : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public slots: + void ignore(const QUuid& nodeID); +}; + + +#endif // hifi_UsersScriptingInterface_h From 441b6d2813fcbdb672ee9f652c96dc07d04ea619 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 14:05:48 -0700 Subject: [PATCH 1067/1237] fix recursive mutex lock, conditional, logging --- libraries/networking/src/LimitedNodeList.cpp | 1 - libraries/networking/src/Node.cpp | 3 ++- libraries/script-engine/src/UsersScriptingInterface.cpp | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 2f085dc3cb..e2d6b277a7 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -425,7 +425,6 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& } int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointer message, SharedNodePointer sendingNode) { - QMutexLocker locker(&sendingNode->getMutex()); NodeData* linkedData = getOrCreateLinkedData(sendingNode); diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 27598d10ae..4615c61506 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -82,7 +82,8 @@ void Node::handleNodeIgnoreRequest(QSharedPointer packet) { // parse out the UUID being ignored from the packet QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - qDebug() << "Adding" << ignoredUUID << "to ignore set for" << _uuid; + qDebug() << "Adding" << uuidStringWithoutCurlyBraces(ignoredUUID) << "to ignore set for" + << uuidStringWithoutCurlyBraces(_uuid); // add the session UUID to the set of ignored ones for this listening node _ignoredNodeIDSet.insert(ignoredUUID); diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 3a05d81de1..94c448ac86 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -21,10 +21,10 @@ void UsersScriptingInterface::ignore(const QUuid& nodeID) { auto nodeList = DependencyManager::get(); nodeList->eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { - if (node->getType() != NodeType::AudioMixer || node->getType() != NodeType::AvatarMixer) { - return false; - } else { + if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { return true; + } else { + return false; } }, [&nodeID, &nodeList](const SharedNodePointer& destinationNode) { // create a reliable NLPacket with space for the ignore UUID @@ -33,6 +33,8 @@ void UsersScriptingInterface::ignore(const QUuid& nodeID) { // write the node ID to the packet ignorePacket->write(nodeID.toRfc4122()); + qDebug() << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); + // send off this ignore packet reliably to the matching node nodeList->sendPacket(std::move(ignorePacket), *destinationNode); }); From 095bd7e6c8384a6b565535c9c25c2dde47ca49d9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 14:20:03 -0700 Subject: [PATCH 1068/1237] cleanup logging, move packet parsing to mixers --- assignment-client/src/audio/AudioMixer.cpp | 5 +++- assignment-client/src/avatars/AvatarMixer.cpp | 5 +++- libraries/networking/src/Node.cpp | 29 ++++++++++--------- libraries/networking/src/Node.h | 2 +- .../src/UsersScriptingInterface.cpp | 2 ++ .../src/UsersScriptingInterface.h | 2 ++ 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 002f32a60b..e9c4b9628a 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -557,7 +557,10 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { } void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { - sendingNode->handleNodeIgnoreRequest(packet); + // parse out the UUID being ignored from the packet + QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + sendingNode->addIgnoredNode(ignoredUUID); } void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 7930ab843f..7b2b98d3e2 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -434,7 +434,10 @@ void AvatarMixer::handleKillAvatarPacket(QSharedPointer message } void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { - senderNode->handleNodeIgnoreRequest(message); + // parse out the UUID being ignored from the packet + QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + senderNode->addIgnoredNode(ignoredUUID); } void AvatarMixer::sendStatsPacket() { diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 4615c61506..f7fa898fbe 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -12,15 +12,17 @@ #include #include -#include - -#include "Node.h" -#include "SharedUtil.h" -#include "NodePermissions.h" - #include #include +#include + +#include "NetworkLogging.h" +#include "NodePermissions.h" +#include "SharedUtil.h" + +#include "Node.h" + const QString UNKNOWN_NodeType_t_NAME = "Unknown"; int NodePtrMetaTypeId = qRegisterMetaType("Node*"); @@ -78,15 +80,16 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } -void Node::handleNodeIgnoreRequest(QSharedPointer packet) { - // parse out the UUID being ignored from the packet - QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - - qDebug() << "Adding" << uuidStringWithoutCurlyBraces(ignoredUUID) << "to ignore set for" +void Node::addIgnoredNode(const QUuid& otherNodeID) { + if (otherNodeID != _uuid) { + qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for" << uuidStringWithoutCurlyBraces(_uuid); - // add the session UUID to the set of ignored ones for this listening node - _ignoredNodeIDSet.insert(ignoredUUID); + // add the session UUID to the set of ignored ones for this listening node + _ignoredNodeIDSet.insert(otherNodeID); + } else { + qCWarning(networking) << "Node::addIgnoredNode called with ID of ignoring node - nodes cannot self-ignore."; + } } QDataStream& operator<<(QDataStream& out, const Node& node) { diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index daebc1fcfb..ae775e50b2 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -70,7 +70,7 @@ public: bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; } bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; } - void handleNodeIgnoreRequest(QSharedPointer packet); + void addIgnoredNode(const QUuid& otherNodeID); bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } friend QDataStream& operator<<(QDataStream& out, const Node& node); diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 94c448ac86..6f863268c6 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -38,4 +38,6 @@ void UsersScriptingInterface::ignore(const QUuid& nodeID) { // send off this ignore packet reliably to the matching node nodeList->sendPacket(std::move(ignorePacket), *destinationNode); }); + + emit ignoredNode(nodeID); } diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 0dc62c088c..a63d375f3f 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -22,6 +22,8 @@ class UsersScriptingInterface : public QObject, public Dependency { public slots: void ignore(const QUuid& nodeID); +signals: + void ignoredNode(const QUuid& nodeID); }; From d5af323057ef00ba229b542ea3afb7a4c2e910ac Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 14:23:22 -0700 Subject: [PATCH 1069/1237] don't self-ignore from UsersScriptingInterface --- .../src/UsersScriptingInterface.cpp | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 6f863268c6..fab6f453e5 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -20,24 +20,29 @@ void UsersScriptingInterface::ignore(const QUuid& nodeID) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it auto nodeList = DependencyManager::get(); - nodeList->eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { - if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { - return true; - } else { - return false; - } - }, [&nodeID, &nodeList](const SharedNodePointer& destinationNode) { - // create a reliable NLPacket with space for the ignore UUID - auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); + if (nodeList->getSessionUUID() != nodeID) { + nodeList->eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { + if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { + return true; + } else { + return false; + } + }, [&nodeID, &nodeList](const SharedNodePointer& destinationNode) { + // create a reliable NLPacket with space for the ignore UUID + auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); - // write the node ID to the packet - ignorePacket->write(nodeID.toRfc4122()); + // write the node ID to the packet + ignorePacket->write(nodeID.toRfc4122()); - qDebug() << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); + qDebug() << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); - // send off this ignore packet reliably to the matching node - nodeList->sendPacket(std::move(ignorePacket), *destinationNode); - }); - - emit ignoredNode(nodeID); + // send off this ignore packet reliably to the matching node + nodeList->sendPacket(std::move(ignorePacket), *destinationNode); + }); + + emit ignoredNode(nodeID); + } else { + qWarning() << "UsersScriptingInterface was asked to ignore a node ID which matches the current session ID, " + << "but self-ignore has no meaning."; + } } From 6b6513d5f9582469131c01f4c753d385e2f8f404 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 14:46:21 -0700 Subject: [PATCH 1070/1237] immediately fade out ignored avatars --- assignment-client/src/avatars/AvatarMixer.cpp | 2 +- interface/src/Application.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 9 ++++++++- interface/src/avatar/AvatarManager.h | 4 +++- libraries/networking/src/Node.cpp | 4 ++-- libraries/script-engine/src/UsersScriptingInterface.cpp | 5 ++--- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 7b2b98d3e2..64de015c9f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -435,7 +435,7 @@ void AvatarMixer::handleKillAvatarPacket(QSharedPointer message void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { // parse out the UUID being ignored from the packet - QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); senderNode->addIgnoredNode(ignoredUUID); } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a4ae0af1c5..0e0f56438e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -441,6 +441,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -455,7 +456,6 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 31a77df0cf..3f891ac207 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include "Application.h" @@ -73,6 +74,11 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); + + // when we hear that the user has ignored an avatar by session UUID + // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer + auto usersScriptingInterface = DependencyManager::get(); + connect(usersScriptingInterface.data(), &UsersScriptingInterface::ignoredNode, this, &AvatarManager::removeAvatar); } AvatarManager::~AvatarManager() { @@ -85,7 +91,8 @@ void AvatarManager::init() { _avatarHash.insert(MY_AVATAR_KEY, _myAvatar); } - connect(DependencyManager::get().data(), &SceneScriptingInterface::shouldRenderAvatarsChanged, this, &AvatarManager::updateAvatarRenderStatus, Qt::QueuedConnection); + connect(DependencyManager::get().data(), &SceneScriptingInterface::shouldRenderAvatarsChanged, + this, &AvatarManager::updateAvatarRenderStatus, Qt::QueuedConnection); render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index c49d566aa8..f09aa9791c 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -78,6 +78,9 @@ public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } void updateAvatarRenderStatus(bool shouldRenderAvatars); +private slots: + virtual void removeAvatar(const QUuid& sessionUUID) override; + private: explicit AvatarManager(QObject* parent = 0); explicit AvatarManager(const AvatarManager& other); @@ -88,7 +91,6 @@ private: virtual AvatarSharedPointer newSharedAvatar() override; virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) override; - virtual void removeAvatar(const QUuid& sessionUUID) override; virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) override; QVector _avatarFades; diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index f7fa898fbe..98249e3557 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -81,14 +81,14 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { } void Node::addIgnoredNode(const QUuid& otherNodeID) { - if (otherNodeID != _uuid) { + if (!otherNodeID.isNull() && otherNodeID != _uuid) { qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for" << uuidStringWithoutCurlyBraces(_uuid); // add the session UUID to the set of ignored ones for this listening node _ignoredNodeIDSet.insert(otherNodeID); } else { - qCWarning(networking) << "Node::addIgnoredNode called with ID of ignoring node - nodes cannot self-ignore."; + qCWarning(networking) << "Node::addIgnoredNode called with null ID or ID of ignoring node."; } } diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index fab6f453e5..c7d45503f0 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -20,7 +20,7 @@ void UsersScriptingInterface::ignore(const QUuid& nodeID) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it auto nodeList = DependencyManager::get(); - if (nodeList->getSessionUUID() != nodeID) { + if (!nodeID.isNull() && nodeList->getSessionUUID() != nodeID) { nodeList->eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { return true; @@ -42,7 +42,6 @@ void UsersScriptingInterface::ignore(const QUuid& nodeID) { emit ignoredNode(nodeID); } else { - qWarning() << "UsersScriptingInterface was asked to ignore a node ID which matches the current session ID, " - << "but self-ignore has no meaning."; + qWarning() << "UsersScriptingInterface::ignore called with an invalid ID or an ID which matches the current session ID."; } } From 609900f24634aec3e1fd7fe11c4e33890dff4dd5 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 15:12:14 -0700 Subject: [PATCH 1071/1237] move ignore set handling to NodeList --- interface/src/avatar/AvatarManager.cpp | 6 +-- libraries/avatars/src/AvatarHashMap.cpp | 4 +- libraries/avatars/src/AvatarHashMap.h | 4 +- libraries/networking/src/NodeList.cpp | 45 +++++++++++++++++++ libraries/networking/src/NodeList.h | 10 +++++ .../src/UsersScriptingInterface.cpp | 32 +------------ .../src/UsersScriptingInterface.h | 2 - 7 files changed, 66 insertions(+), 37 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 3f891ac207..bd76d2bd81 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -70,15 +70,15 @@ AvatarManager::AvatarManager(QObject* parent) : // register a meta type for the weak pointer we'll use for the owning avatar mixer for each avatar qRegisterMetaType >("NodeWeakPointer"); - auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + auto nodeList = DependencyManager::get(); + auto& packetReceiver = nodeList->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); // when we hear that the user has ignored an avatar by session UUID // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer - auto usersScriptingInterface = DependencyManager::get(); - connect(usersScriptingInterface.data(), &UsersScriptingInterface::ignoredNode, this, &AvatarManager::removeAvatar); + connect(nodeList.data(), &NodeList::ignoredNode, this, &AvatarManager::removeAvatar); } AvatarManager::~AvatarManager() { diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index d153cfd977..f1f4b4eae2 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -19,7 +19,9 @@ #include "AvatarHashMap.h" AvatarHashMap::AvatarHashMap() { - connect(DependencyManager::get().data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged); + auto nodeList = DependencyManager::get(); + + connect(nodeList.data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged); } QVector AvatarHashMap::getAvatarIdentifiers() { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 9d3ebb60f5..a59cc4fa96 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -19,12 +19,14 @@ #include #include +#include + #include #include #include #include "AvatarData.h" -#include + class AvatarHashMap : public QObject, public Dependency { Q_OBJECT diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index d3fc93b991..cde141fdad 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -215,6 +215,11 @@ void NodeList::reset() { _numNoReplyDomainCheckIns = 0; + // lock and clear our set of ignored IDs + _ignoredSetLock.lockForWrite(); + _ignoredNodeIDs.clear(); + _ignoredSetLock.unlock(); + // refresh the owner UUID to the NULL UUID setSessionUUID(QUuid()); @@ -692,3 +697,43 @@ void NodeList::sendKeepAlivePings() { sendPacket(constructPingPacket(), *node); }); } + +void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { + // enumerate the nodes to send a reliable ignore packet to each that can leverage it + + if (!nodeID.isNull() && _sessionUUID != nodeID) { + eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { + if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { + return true; + } else { + return false; + } + }, [&nodeID, this](const SharedNodePointer& destinationNode) { + // create a reliable NLPacket with space for the ignore UUID + auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); + + // write the node ID to the packet + ignorePacket->write(nodeID.toRfc4122()); + + qDebug() << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); + + // send off this ignore packet reliably to the matching node + sendPacket(std::move(ignorePacket), *destinationNode); + }); + + QReadLocker setLocker { &_ignoredSetLock }; + + // add this nodeID to our set of ignored IDs + _ignoredNodeIDs.insert(nodeID); + + emit ignoredNode(nodeID); + + } else { + qWarning() << "UsersScriptingInterface::ignore called with an invalid ID or an ID which matches the current session ID."; + } +} + +bool NodeList::isIgnoringNode(const QUuid& nodeID) const { + QReadLocker setLocker { &_ignoredSetLock }; + return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend(); +} diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 3fbc86c736..8d8f19ce8c 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -20,6 +20,8 @@ #include // not on windows, not needed for mac or windows #endif +#include + #include #include #include @@ -68,6 +70,9 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } + void ignoreNodeBySessionID(const QUuid& nodeID); + bool isIgnoringNode(const QUuid& nodeID) const; + public slots: void reset(); void sendDomainServerCheckIn(); @@ -92,6 +97,8 @@ public slots: signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); + void ignoredNode(const QUuid& nodeID); + private slots: void stopKeepalivePingTimer(); void sendPendingDSPathQuery(); @@ -129,6 +136,9 @@ private: bool _isShuttingDown { false }; QTimer _keepAlivePingTimer; + mutable QReadWriteLock _ignoredSetLock; + tbb::concurrent_unordered_set _ignoredNodeIDs; + #if (PR_BUILD || DEV_BUILD) bool _shouldSendNewerVersion { false }; #endif diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index c7d45503f0..ff7ccb0164 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -14,34 +14,6 @@ #include void UsersScriptingInterface::ignore(const QUuid& nodeID) { - // setup the ignore packet we send to all nodes (that currently handle it) - // to ignore the data (audio/avatar) for this user - - // enumerate the nodes to send a reliable ignore packet to each that can leverage it - auto nodeList = DependencyManager::get(); - - if (!nodeID.isNull() && nodeList->getSessionUUID() != nodeID) { - nodeList->eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { - if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { - return true; - } else { - return false; - } - }, [&nodeID, &nodeList](const SharedNodePointer& destinationNode) { - // create a reliable NLPacket with space for the ignore UUID - auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); - - // write the node ID to the packet - ignorePacket->write(nodeID.toRfc4122()); - - qDebug() << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); - - // send off this ignore packet reliably to the matching node - nodeList->sendPacket(std::move(ignorePacket), *destinationNode); - }); - - emit ignoredNode(nodeID); - } else { - qWarning() << "UsersScriptingInterface::ignore called with an invalid ID or an ID which matches the current session ID."; - } + // ask the NodeList to ignore this user (based on the session ID of their node) + DependencyManager::get()->ignoreNodeBySessionID(nodeID); } diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index a63d375f3f..0dc62c088c 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -22,8 +22,6 @@ class UsersScriptingInterface : public QObject, public Dependency { public slots: void ignore(const QUuid& nodeID); -signals: - void ignoredNode(const QUuid& nodeID); }; From 39c7805ca23b865b7fe510106694e6d63cae827f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Jul 2016 15:15:59 -0700 Subject: [PATCH 1072/1237] don't process packets for ignored avatars --- libraries/avatars/src/AvatarHashMap.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index f1f4b4eae2..48b701d142 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -107,7 +107,10 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer mess QByteArray byteArray = message->readWithoutCopy(message->getBytesLeftToRead()); - if (sessionUUID != _lastOwnerSessionUUID) { + // make sure this isn't our own avatar data or for a previously ignored node + auto nodeList = DependencyManager::get(); + + if (sessionUUID != _lastOwnerSessionUUID && !nodeList->isIgnoringNode(sessionUUID)) { auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); // have the matching (or new) avatar parse the data from the packet @@ -126,9 +129,13 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer AvatarData::Identity identity; AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity); - // mesh URL for a UUID, find avatar in our list - auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); - avatar->processAvatarIdentity(identity); + // make sure this isn't for an ignored avatar + auto nodeList = DependencyManager::get(); + if (!nodeList->isIgnoringNode(identity.uuid)) { + // mesh URL for a UUID, find avatar in our list + auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); + avatar->processAvatarIdentity(identity); + } } void AvatarHashMap::processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode) { From 345478eb36997a1e13c76b3a49676de42e74d284 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 13:36:03 -0700 Subject: [PATCH 1073/1237] add removeButton API to ToolbarScriptingInterface --- interface/src/scripting/ToolbarScriptingInterface.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp index 82332b3187..175a7fd539 100644 --- a/interface/src/scripting/ToolbarScriptingInterface.cpp +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -14,7 +14,7 @@ class QmlWrapper : public QObject { Q_OBJECT public: QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) - : QObject(parent), _qmlObject(qmlObject) { + : QObject(parent), _qmlObject(qmlObject) { } Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { @@ -91,6 +91,10 @@ public: return new ToolbarButtonProxy(rawButton, this); } + + Q_INVOKABLE void removeButton(const QVariant& name) { + QMetaObject::invokeMethod(_qmlObject, "removeButton", Qt::AutoConnection, Q_ARG(QVariant, name)); + } }; @@ -112,4 +116,4 @@ QObject* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { } -#include "ToolbarScriptingInterface.moc" \ No newline at end of file +#include "ToolbarScriptingInterface.moc" From 8724f0d0d9240b1e5c38fab6e7b838a913bacab1 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 13:40:29 -0700 Subject: [PATCH 1074/1237] add a stubbed version of the ignore script --- scripts/system/ignore.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 scripts/system/ignore.js diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js new file mode 100644 index 0000000000..eaeecb2e96 --- /dev/null +++ b/scripts/system/ignore.js @@ -0,0 +1,40 @@ +// +// ignore.js +// scripts/system/ +// +// Created by Stephen Birarda on 07/11/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// grab the toolbar +var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + +// setup the ignore button and add it to the toolbar +var button = toolbar.addButton({ + objectName: 'ignore', + imageURL: Script.resolvePath("assets/images/tools/mic.svg"), + visible: true, + buttonState: 1, + alpha: 0.9 +}); + +var isShowingOverlays = false; + +// handle clicks on the toolbar button +function buttonClicked(){ + if (isShowingOverlays) { + hideOverlays(); + } else { + showOverlays(); + } +} + +button.clicked.connect(buttonClicked); + +// remove the toolbar button when script is stopped +Script.scriptEnding.connect(function() { + toolbar.removeButton('ignore'); +}); From 3c330d0c48f6b9c5fa350c8ce6463cf9b0c0b814 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 13:59:31 -0700 Subject: [PATCH 1075/1237] show simple overlay for script testing --- scripts/system/ignore.js | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index eaeecb2e96..f8939d8e25 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -22,14 +22,56 @@ var button = toolbar.addButton({ }); var isShowingOverlays = false; +var currentOverlays = []; + +function hideOverlays() { + // enumerate the overlays and remove them + while (currentOverlays.length) { + // shift the element to remove it from the current overlays array when it is removed + Overlays.deleteOverlay(currentOverlays.shift()); + } +} + +var OVERLAY_SIZE = 0.25; + +function showOverlays() { + var identifiers = AvatarList.getAvatarIdentifiers(); + + for (i = 0; i < identifiers.length; ++i) { + // get the position for this avatar + var avatar = AvatarList.getAvatar(identifiers[i]); + var avatarPosition = avatar && avatar.position; + + if (!avatarPosition) { + // we don't have a valid position for this avatar, skip it + break; + } + + // add the overlay above this avatar + var newOverlay = Overlays.addOverlay("cube", { + position: avatarPosition, + size: 0.25, + color: { red: 0, green: 0, blue: 255}, + alpha: 1, + solid: true + }); + + // push this overlay to our array of overlays + currentOverlays.push(newOverlay); + } +} // handle clicks on the toolbar button function buttonClicked(){ if (isShowingOverlays) { hideOverlays(); + isShowingOverlays = false; } else { showOverlays(); + isShowingOverlays = true; } + + button.writeProperty('buttonState', isShowingOverlays ? 0 : 1); } button.clicked.connect(buttonClicked); @@ -37,4 +79,5 @@ button.clicked.connect(buttonClicked); // remove the toolbar button when script is stopped Script.scriptEnding.connect(function() { toolbar.removeButton('ignore'); + hideOverlays(); }); From 154834b0ab91897c141e210ff5cb5a0f884c4a1d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 14:35:46 -0700 Subject: [PATCH 1076/1237] hook up ignoring of user to overlay --- scripts/system/ignore.js | 52 +++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index f8939d8e25..05622237a9 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -22,13 +22,13 @@ var button = toolbar.addButton({ }); var isShowingOverlays = false; -var currentOverlays = []; +var ignoreOverlays = []; function hideOverlays() { // enumerate the overlays and remove them - while (currentOverlays.length) { + while (ignoreOverlays.length) { // shift the element to remove it from the current overlays array when it is removed - Overlays.deleteOverlay(currentOverlays.shift()); + Overlays.deleteOverlay(ignoreOverlays.shift()['overlayID']); } } @@ -39,7 +39,14 @@ function showOverlays() { for (i = 0; i < identifiers.length; ++i) { // get the position for this avatar - var avatar = AvatarList.getAvatar(identifiers[i]); + var identifier = identifiers[i]; + + if (identifier === null) { + // this is our avatar, skip it + break; + } + + var avatar = AvatarList.getAvatar(identifier); var avatarPosition = avatar && avatar.position; if (!avatarPosition) { @@ -57,7 +64,10 @@ function showOverlays() { }); // push this overlay to our array of overlays - currentOverlays.push(newOverlay); + ignoreOverlays.push({ + avatarID: identifiers[i], + overlayID: newOverlay + }); } } @@ -76,7 +86,37 @@ function buttonClicked(){ button.clicked.connect(buttonClicked); -// remove the toolbar button when script is stopped +Controller.mousePressEvent.connect(function(event){ + // handle click events so we can detect when our overlays are clicked + + if (!event.isLeftButton && !that.triggered) { + // if another mouse button than left is pressed ignore it + return false; + } + + // compute the pick ray from the event + var pickRay = Camera.computePickRay(event.x, event.y); + + // grab the clicked overlay for the given pick ray + var clickedOverlay = Overlays.findRayIntersection(pickRay); + + // see this is one of our ignore overlays + for (i = 0; i < ignoreOverlays.length; ++i) { + var ignoreOverlay = ignoreOverlays[i]['overlayID'] + if (clickedOverlay.overlayID == ignoreOverlay) { + // matched to an overlay, ask for the matching avatar to be ignored + Users.ignore(ignoreOverlays[i]['avatarID']); + + // remove the actual overlay + Overlays.deleteOverlay(ignoreOverlay); + + // remove the overlay from our internal array + ignoreOverlays.splice(i, 1); + } + } +}); + +// cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function() { toolbar.removeButton('ignore'); hideOverlays(); From 22ab3fc7b57ad568e53d1cca1d243413b4380ec1 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 14:36:54 -0700 Subject: [PATCH 1077/1237] cleanup a couple of comments --- scripts/system/ignore.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index 05622237a9..a1b2445d85 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -38,7 +38,6 @@ function showOverlays() { var identifiers = AvatarList.getAvatarIdentifiers(); for (i = 0; i < identifiers.length; ++i) { - // get the position for this avatar var identifier = identifiers[i]; if (identifier === null) { @@ -46,6 +45,7 @@ function showOverlays() { break; } + // get the position for this avatar var avatar = AvatarList.getAvatar(identifier); var avatarPosition = avatar && avatar.position; @@ -107,10 +107,10 @@ Controller.mousePressEvent.connect(function(event){ // matched to an overlay, ask for the matching avatar to be ignored Users.ignore(ignoreOverlays[i]['avatarID']); - // remove the actual overlay + // remove the actual overlay so it is no longer rendered Overlays.deleteOverlay(ignoreOverlay); - // remove the overlay from our internal array + // remove the overlay ID and avatar ID from our internal array ignoreOverlays.splice(i, 1); } } From 7bd8c45098d557b783601a4f5b5330a94f3c4cd0 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 14:59:42 -0700 Subject: [PATCH 1078/1237] add the correct ignore icon, cleanup data structure --- scripts/system/assets/images/tools/ignore.svg | 177 ++++++++++++++++++ scripts/system/ignore.js | 101 +++++----- 2 files changed, 234 insertions(+), 44 deletions(-) create mode 100644 scripts/system/assets/images/tools/ignore.svg diff --git a/scripts/system/assets/images/tools/ignore.svg b/scripts/system/assets/images/tools/ignore.svg new file mode 100644 index 0000000000..f315c5f249 --- /dev/null +++ b/scripts/system/assets/images/tools/ignore.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index a1b2445d85..d0206710ee 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -15,69 +15,77 @@ var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); // setup the ignore button and add it to the toolbar var button = toolbar.addButton({ objectName: 'ignore', - imageURL: Script.resolvePath("assets/images/tools/mic.svg"), + imageURL: Script.resolvePath("assets/images/tools/ignore.svg"), visible: true, buttonState: 1, alpha: 0.9 }); var isShowingOverlays = false; -var ignoreOverlays = []; +var ignoreOverlays = {}; -function hideOverlays() { +function removeOverlays() { // enumerate the overlays and remove them - while (ignoreOverlays.length) { - // shift the element to remove it from the current overlays array when it is removed - Overlays.deleteOverlay(ignoreOverlays.shift()['overlayID']); + var ignoreOverlayKeys = Object.keys(ignoreOverlays); + + for (i = 0; i < ignoreOverlayKeys.length; ++i) { + var avatarID = ignoreOverlayKeys[i]; + Overlays.deleteOverlay(ignoreOverlays[avatarID]); } + + ignoreOverlays = {}; } var OVERLAY_SIZE = 0.25; -function showOverlays() { - var identifiers = AvatarList.getAvatarIdentifiers(); +function updateOverlays() { + if (isShowingOverlays) { - for (i = 0; i < identifiers.length; ++i) { - var identifier = identifiers[i]; + var identifiers = AvatarList.getAvatarIdentifiers(); - if (identifier === null) { - // this is our avatar, skip it - break; + for (i = 0; i < identifiers.length; ++i) { + var avatarID = identifiers[i]; + + if (avatarID === null) { + // this is our avatar, skip it + continue; + } + + // get the position for this avatar + var avatar = AvatarList.getAvatar(avatarID); + var avatarPosition = avatar && avatar.position; + + if (!avatarPosition) { + // we don't have a valid position for this avatar, skip it + continue; + } + + if (avatarID in ignoreOverlays) { + + } else { + + // add the overlay above this avatar + var newOverlay = Overlays.addOverlay("cube", { + position: avatarPosition, + size: 0.25, + color: { red: 0, green: 0, blue: 255}, + alpha: 1, + solid: true + }); + + // push this overlay to our array of overlays + ignoreOverlays[avatarID] = newOverlay; + } } - - // get the position for this avatar - var avatar = AvatarList.getAvatar(identifier); - var avatarPosition = avatar && avatar.position; - - if (!avatarPosition) { - // we don't have a valid position for this avatar, skip it - break; - } - - // add the overlay above this avatar - var newOverlay = Overlays.addOverlay("cube", { - position: avatarPosition, - size: 0.25, - color: { red: 0, green: 0, blue: 255}, - alpha: 1, - solid: true - }); - - // push this overlay to our array of overlays - ignoreOverlays.push({ - avatarID: identifiers[i], - overlayID: newOverlay - }); } } // handle clicks on the toolbar button function buttonClicked(){ if (isShowingOverlays) { - hideOverlays(); + removeOverlays(); isShowingOverlays = false; } else { - showOverlays(); isShowingOverlays = true; } @@ -101,23 +109,28 @@ Controller.mousePressEvent.connect(function(event){ var clickedOverlay = Overlays.findRayIntersection(pickRay); // see this is one of our ignore overlays - for (i = 0; i < ignoreOverlays.length; ++i) { - var ignoreOverlay = ignoreOverlays[i]['overlayID'] + var ignoreOverlayKeys = Object.keys(ignoreOverlays) + for (i = 0; i < ignoreOverlayKeys.length; ++i) { + var avatarID = ignoreOverlayKeys[i]; + var ignoreOverlay = ignoreOverlays[avatarID]; + if (clickedOverlay.overlayID == ignoreOverlay) { // matched to an overlay, ask for the matching avatar to be ignored - Users.ignore(ignoreOverlays[i]['avatarID']); + Users.ignore(avatarID); // remove the actual overlay so it is no longer rendered Overlays.deleteOverlay(ignoreOverlay); // remove the overlay ID and avatar ID from our internal array - ignoreOverlays.splice(i, 1); + delete ignoreOverlays[avatarID]; } } }); +Script.update.connect(updateOverlays); + // cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function() { toolbar.removeButton('ignore'); - hideOverlays(); + removeOverlays(); }); From aeabae4faf76a9a8f55ff350834a20521be61b28 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 15:10:02 -0700 Subject: [PATCH 1079/1237] handle removal of an overlay for an avatar that is removed --- scripts/system/ignore.js | 43 ++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index d0206710ee..cff640248a 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -36,6 +36,20 @@ function removeOverlays() { ignoreOverlays = {}; } +// handle clicks on the toolbar button +function buttonClicked(){ + if (isShowingOverlays) { + removeOverlays(); + isShowingOverlays = false; + } else { + isShowingOverlays = true; + } + + button.writeProperty('buttonState', isShowingOverlays ? 0 : 1); +} + +button.clicked.connect(buttonClicked); + var OVERLAY_SIZE = 0.25; function updateOverlays() { @@ -80,19 +94,19 @@ function updateOverlays() { } } -// handle clicks on the toolbar button -function buttonClicked(){ +Script.update.connect(updateOverlays); + +AvatarList.avatarRemovedEvent.connect(function(avatarID){ if (isShowingOverlays) { - removeOverlays(); - isShowingOverlays = false; - } else { - isShowingOverlays = true; + // we are currently showing overlays and an avatar just went away + + // first remove the rendered overlay + Overlays.deleteOverlay(ignoreOverlays[avatarID]); + + // delete the saved ID of the overlay from our ignored overlays object + delete ignoreOverlays[avatarID]; } - - button.writeProperty('buttonState', isShowingOverlays ? 0 : 1); -} - -button.clicked.connect(buttonClicked); +}) Controller.mousePressEvent.connect(function(event){ // handle click events so we can detect when our overlays are clicked @@ -118,16 +132,11 @@ Controller.mousePressEvent.connect(function(event){ // matched to an overlay, ask for the matching avatar to be ignored Users.ignore(avatarID); - // remove the actual overlay so it is no longer rendered - Overlays.deleteOverlay(ignoreOverlay); - - // remove the overlay ID and avatar ID from our internal array - delete ignoreOverlays[avatarID]; + // cleanup of the overlay is handled by the connection to avatarRemovedEvent } } }); -Script.update.connect(updateOverlays); // cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function() { From 599d9c568aa027d0ad2e4cc29124dc17cc3a8992 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 15:43:57 -0700 Subject: [PATCH 1080/1237] remove unneeded check from copy-paste --- scripts/system/ignore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index cff640248a..da8c965540 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -111,7 +111,7 @@ AvatarList.avatarRemovedEvent.connect(function(avatarID){ Controller.mousePressEvent.connect(function(event){ // handle click events so we can detect when our overlays are clicked - if (!event.isLeftButton && !that.triggered) { + if (!event.isLeftButton) { // if another mouse button than left is pressed ignore it return false; } From 5fc2afe549dbac4095795292ac2af705b1a4b742 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Jul 2016 17:42:42 -0700 Subject: [PATCH 1081/1237] switch to new icon for ignore target --- .../system/assets/images/ignore-target-01.svg | 37 +++++++++++++++++++ scripts/system/ignore.js | 26 ++++++++----- 2 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 scripts/system/assets/images/ignore-target-01.svg diff --git a/scripts/system/assets/images/ignore-target-01.svg b/scripts/system/assets/images/ignore-target-01.svg new file mode 100644 index 0000000000..98cee89ca1 --- /dev/null +++ b/scripts/system/assets/images/ignore-target-01.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index da8c965540..ee6292909a 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -50,8 +50,6 @@ function buttonClicked(){ button.clicked.connect(buttonClicked); -var OVERLAY_SIZE = 0.25; - function updateOverlays() { if (isShowingOverlays) { @@ -74,17 +72,27 @@ function updateOverlays() { continue; } + // setup a position for the overlay that is just above this avatar's head + var overlayPosition = avatar.getJointPosition("Head"); + overlayPosition.y += 0.45; + if (avatarID in ignoreOverlays) { - + // keep the overlay above the current position of this avatar + Overlays.editOverlay(ignoreOverlays[avatarID], { + position: overlayPosition + }); } else { - // add the overlay above this avatar - var newOverlay = Overlays.addOverlay("cube", { - position: avatarPosition, - size: 0.25, - color: { red: 0, green: 0, blue: 255}, + var newOverlay = Overlays.addOverlay("image3d", { + url: Script.resolvePath("assets/images/ignore-target-01.svg"), + position: overlayPosition, + size: 0.4, + scale: 0.4, + color: { red: 255, green: 255, blue: 255}, alpha: 1, - solid: true + solid: true, + isFacingAvatar: true, + drawInFront: true }); // push this overlay to our array of overlays From 07c177748506e5daad500728f9f63d3d3856a8cb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 09:54:30 -0700 Subject: [PATCH 1082/1237] push the packet version for NodeIgnoreRequest --- libraries/networking/src/udt/PacketHeaders.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 99e10caabd..fca006ae87 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -60,6 +60,8 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AssetUpload: // Removal of extension from Asset requests return 18; + case PacketType::NodeIgnoreRequest: + return 18; // Introduction of node ignore request (which replaced an unused packet tpye) case PacketType::DomainConnectionDenied: return static_cast(DomainConnectionDeniedVersion::IncludesReasonCode); From 0918b55e7e679896fa60d177a2fb39d884f3e76c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 11:27:46 -0700 Subject: [PATCH 1083/1237] add ignore to defaultScripts --- scripts/defaultScripts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 711e64f938..6880de99b5 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -17,6 +17,7 @@ Script.load("system/goto.js"); Script.load("system/hmd.js"); Script.load("system/examples.js"); Script.load("system/edit.js"); +Script.load("system/ignore.js"); Script.load("system/selectAudioDevice.js"); Script.load("system/notifications.js"); Script.load("system/controllers/handControllerGrab.js"); From a6f39d5e6860a7621af918f193b1c980f4520442 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 11:42:28 -0700 Subject: [PATCH 1084/1237] add code from entitySelectionTool to handle hmd overlay pick --- scripts/system/ignore.js | 78 ++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index ee6292909a..3acbda4666 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -114,7 +114,24 @@ AvatarList.avatarRemovedEvent.connect(function(avatarID){ // delete the saved ID of the overlay from our ignored overlays object delete ignoreOverlays[avatarID]; } -}) +}); + +function handleSelectedOverlay(clickedOverlay) { + // see this is one of our ignore overlays + + var ignoreOverlayKeys = Object.keys(ignoreOverlays) + for (i = 0; i < ignoreOverlayKeys.length; ++i) { + var avatarID = ignoreOverlayKeys[i]; + var ignoreOverlay = ignoreOverlays[avatarID]; + + if (clickedOverlay.overlayID == ignoreOverlay) { + // matched to an overlay, ask for the matching avatar to be ignored + Users.ignore(avatarID); + + // cleanup of the overlay is handled by the connection to avatarRemovedEvent + } + } +} Controller.mousePressEvent.connect(function(event){ // handle click events so we can detect when our overlays are clicked @@ -129,25 +146,58 @@ Controller.mousePressEvent.connect(function(event){ // grab the clicked overlay for the given pick ray var clickedOverlay = Overlays.findRayIntersection(pickRay); - - // see this is one of our ignore overlays - var ignoreOverlayKeys = Object.keys(ignoreOverlays) - for (i = 0; i < ignoreOverlayKeys.length; ++i) { - var avatarID = ignoreOverlayKeys[i]; - var ignoreOverlay = ignoreOverlays[avatarID]; - - if (clickedOverlay.overlayID == ignoreOverlay) { - // matched to an overlay, ask for the matching avatar to be ignored - Users.ignore(avatarID); - - // cleanup of the overlay is handled by the connection to avatarRemovedEvent - } + if (clickedOverlay.intersects) { + handleSelectedOverlay(clickedOverlay); } }); +// We get mouseMoveEvents from the handControllers, via handControllerPointer. +// But we dont' get mousePressEvents. +var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); + +var TRIGGER_GRAB_VALUE = 0.85; // From handControllerGrab/Pointer.js. Should refactor. +var TRIGGER_ON_VALUE = 0.4; +var TRIGGER_OFF_VALUE = 0.15; +var triggered = false; +var activeHand = Controller.Standard.RightHand; + +function controllerComputePickRay() { + var controllerPose = Controller.getPoseValue(activeHand); + if (controllerPose.valid && triggered) { + var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), + MyAvatar.position); + // This gets point direction right, but if you want general quaternion it would be more complicated: + var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + return {origin: controllerPosition, direction: controllerDirection}; + } +} + +function makeTriggerHandler(hand) { + return function (value) { + if (!triggered && (value > TRIGGER_GRAB_VALUE)) { // should we smooth? + triggered = true; + if (activeHand !== hand) { + // No switching while the other is already triggered, so no need to release. + activeHand = (activeHand === Controller.Standard.RightHand) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + } + + var pickRay = controllerComputePickRay(); + if (pickRay) { + var overlayIntersection = Overlays.findRayIntersection(pickRay); + if (overlayIntersection.intersects) { + handleClickedOverlay(overlayIntersection); + } + } + } + }; +} + +triggerMapping.from(Controller.Standard.RT).peek().to(makeTriggerHandler(Controller.Standard.RightHand)); +triggerMapping.from(Controller.Standard.LT).peek().to(makeTriggerHandler(Controller.Standard.LeftHand)); // cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function() { toolbar.removeButton('ignore'); removeOverlays(); + triggerMapping.disable(); }); From b68958317a6ed83b27496f6ce04d648c4993cd54 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 13:40:22 -0700 Subject: [PATCH 1085/1237] re-send ignore requests when mixers are re-added --- assignment-client/src/audio/AudioMixer.cpp | 5 +--- assignment-client/src/avatars/AvatarMixer.cpp | 5 +--- libraries/networking/src/Node.cpp | 11 ++++++++ libraries/networking/src/Node.h | 1 + libraries/networking/src/NodeList.cpp | 26 +++++++++++++++++++ libraries/networking/src/NodeList.h | 2 ++ 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index e9c4b9628a..7ba9242306 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -557,10 +557,7 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { } void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { - // parse out the UUID being ignored from the packet - QUuid ignoredUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - - sendingNode->addIgnoredNode(ignoredUUID); + sendingNode->parseIgnoreRequestMessage(packet); } void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 64de015c9f..65989b389e 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -434,10 +434,7 @@ void AvatarMixer::handleKillAvatarPacket(QSharedPointer message } void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { - // parse out the UUID being ignored from the packet - QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - - senderNode->addIgnoredNode(ignoredUUID); + senderNode->parseIgnoreRequestMessage(message); } void AvatarMixer::sendStatsPacket() { diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 98249e3557..a7c1707648 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -80,6 +80,17 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } +void Node::parseIgnoreRequestMessage(QSharedPointer message) { + while (message->getBytesLeftToRead()) { + // parse out the UUID being ignored from the packet + QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + addIgnoredNode(ignoredUUID); + + message->seek(message->getPosition() + NUM_BYTES_RFC4122_UUID); + } +} + void Node::addIgnoredNode(const QUuid& otherNodeID) { if (!otherNodeID.isNull() && otherNodeID != _uuid) { qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for" diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index ae775e50b2..469a0c9755 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -70,6 +70,7 @@ public: bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; } bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; } + void parseIgnoreRequestMessage(QSharedPointer message); void addIgnoredNode(const QUuid& otherNodeID); bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index cde141fdad..985c51a4f1 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -93,6 +93,9 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); + + // anytime we get a new node we may need to re-send our set of ignored node IDs to it + connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::maybeSendIgnoreSetToNode); // setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect) _keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); @@ -737,3 +740,26 @@ bool NodeList::isIgnoringNode(const QUuid& nodeID) const { QReadLocker setLocker { &_ignoredSetLock }; return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend(); } + +void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { + if (newNode->getType() == NodeType::AudioMixer || newNode->getType() == NodeType::AvatarMixer) { + // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, + // so send that list along now (assuming it isn't empty) + + QReadLocker setLocker { &_ignoredSetLock }; + if (_ignoredNodeIDs.size() > 0) { + // setup a packet list so we can send the stream of ignore IDs + auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + + // enumerate the ignored IDs and write them to the packet list + auto it = _ignoredNodeIDs.cbegin(); + while (it != _ignoredNodeIDs.end()) { + ignorePacketList->write(it->toRfc4122()); + ++it; + } + + // send this NLPacketList to the new node + sendPacketList(std::move(ignorePacketList), *newNode); + } + } +} diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 8d8f19ce8c..ff994ce612 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -110,6 +110,8 @@ private slots: void pingPunchForDomainServer(); void sendKeepAlivePings(); + + void maybeSendIgnoreSetToNode(SharedNodePointer node); private: NodeList() : LimitedNodeList(0, 0) { assert(false); } // Not implemented, needed for DependencyManager templates compile From cc9b72daa4aefb8cc7eb691d02d30bc1e98f096d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 14:11:56 -0700 Subject: [PATCH 1086/1237] fix seeking in packet, use nodeActivated for ignore list send --- libraries/networking/src/Node.cpp | 4 +--- libraries/networking/src/NodeList.cpp | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index a7c1707648..406498b025 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -80,14 +80,12 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } -void Node::parseIgnoreRequestMessage(QSharedPointer message) { +void Node::parseIgnoreRequestMessage(QSharedPointer message) { while (message->getBytesLeftToRead()) { // parse out the UUID being ignored from the packet QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); addIgnoredNode(ignoredUUID); - - message->seek(message->getPosition() + NUM_BYTES_RFC4122_UUID); } } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 985c51a4f1..a73537aad0 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -95,7 +95,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); // anytime we get a new node we may need to re-send our set of ignored node IDs to it - connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::maybeSendIgnoreSetToNode); + connect(this, &LimitedNodeList::nodeActivated, this, &NodeList::maybeSendIgnoreSetToNode); // setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect) _keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); @@ -747,6 +747,7 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // so send that list along now (assuming it isn't empty) QReadLocker setLocker { &_ignoredSetLock }; + if (_ignoredNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); From 30f55418db2e116fe8f01428713534e664999d27 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 14:17:42 -0700 Subject: [PATCH 1087/1237] only prepare packet list packets if they need a message number --- libraries/networking/src/udt/PacketQueue.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index 4b8c0b187c..3684e5ba07 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -65,7 +65,9 @@ void PacketQueue::queuePacket(PacketPointer packet) { } void PacketQueue::queuePacketList(PacketListPointer packetList) { - packetList->preparePackets(getNextMessageNumber()); + if (packetList->isOrdered()) { + packetList->preparePackets(getNextMessageNumber()); + } LockGuard locker(_packetsLock); _channels.push_back(std::move(packetList->_packets)); From 25fdea6bac0693d4e74a2e69dcc70d2f8542e418 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 8 Jul 2016 23:02:05 -0700 Subject: [PATCH 1088/1237] fix for change of API after rebase --- .../src/RenderableModelEntityItem.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index d63361538a..bef790299c 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -709,9 +709,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // compute meshPart local transforms QVector localTransforms; const FBXGeometry& fbxGeometry = _model->getFBXGeometry(); - int32_t numMeshes = (int32_t)fbxGeometry.meshes.size(); - int32_t totalNumVertices = 0; - for (int32_t i = 0; i < numMeshes; i++) { + int numFbxMeshes = fbxGeometry.meshes.size(); + int totalNumVertices = 0; + for (int i = 0; i < numFbxMeshes; i++) { const FBXMesh& mesh = fbxGeometry.meshes.at(i); if (mesh.clusters.size() > 0) { const FBXCluster& cluster = mesh.clusters.at(0); @@ -730,10 +730,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { return; } - auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); - - // the render geometry's mesh count should match that of the FBXGeometry - assert(numMeshes == (int32_t)(meshes.size())); + auto& meshes = _model->getGeometry()->getMeshes(); + int32_t numMeshes = (int32_t)(meshes.size()); ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); pointCollection.clear(); From c50d41c532047a53445519c50efc996f4b8f9a80 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 8 Jul 2016 23:08:49 -0700 Subject: [PATCH 1089/1237] finish name changes as per PR feedback --- .../src/model-networking/ModelCache.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index afe86e0d1e..442bf008e7 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -210,21 +210,21 @@ void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxG } std::shared_ptr meshes = std::make_shared(); - std::shared_ptr shapes = std::make_shared(); + std::shared_ptr parts = std::make_shared(); int meshID = 0; for (const FBXMesh& mesh : _fbxGeometry->meshes) { // Copy mesh pointers meshes->emplace_back(mesh._mesh); int partID = 0; for (const FBXMeshPart& part : mesh.parts) { - // Construct local shapes - shapes->push_back(std::make_shared(meshID, partID, (int)materialIDAtlas[part.materialID])); + // Construct local parts + parts->push_back(std::make_shared(meshID, partID, (int)materialIDAtlas[part.materialID])); partID++; } meshID++; } _meshes = meshes; - _meshParts = shapes; + _meshParts = parts; finishedLoading(true); } @@ -334,9 +334,9 @@ bool Geometry::areTexturesLoaded() const { return true; } -const std::shared_ptr Geometry::getShapeMaterial(int shapeID) const { - if ((shapeID >= 0) && (shapeID < (int)_meshParts->size())) { - int materialID = _meshParts->at(shapeID)->materialID; +const std::shared_ptr Geometry::getShapeMaterial(int partID) const { + if ((partID >= 0) && (partID < (int)_meshParts->size())) { + int materialID = _meshParts->at(partID)->materialID; if ((materialID >= 0) && (materialID < (int)_materials.size())) { return _materials[materialID]; } From 4bebb682dc2a7db701c7b2fe6ff8d93688a5b39c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 11 Jul 2016 08:45:57 -0700 Subject: [PATCH 1090/1237] namechange: fetchResource --> getGeometryResource --- .../model-networking/src/model-networking/ModelCache.cpp | 2 +- libraries/model-networking/src/model-networking/ModelCache.h | 2 +- libraries/render-utils/src/Model.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 442bf008e7..30794cf312 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -250,7 +250,7 @@ QSharedPointer ModelCache::createResource(const QUrl& url, const QShar return QSharedPointer(resource, &Resource::deleter); } -GeometryResource::Pointer ModelCache::fetchResource(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) { +GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) { GeometryExtra geometryExtra = { mapping, textureBaseUrl }; GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra).staticCast(); if (resource) { diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index c450f96846..93eab6aa24 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -127,7 +127,7 @@ class ModelCache : public ResourceCache, public Dependency { SINGLETON_DEPENDENCY public: - GeometryResource::Pointer fetchResource(const QUrl& url, + GeometryResource::Pointer getGeometryResource(const QUrl& url, const QVariantHash& mapping = QVariantHash(), const QUrl& textureBaseUrl = QUrl()); protected: diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ab9e0e7403..485c578045 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -821,7 +821,7 @@ void Model::setURL(const QUrl& url) { invalidCalculatedMeshBoxes(); deleteGeometry(); - _renderWatcher.setResource(DependencyManager::get()->fetchResource(url)); + _renderWatcher.setResource(DependencyManager::get()->getGeometryResource(url)); onInvalidate(); } @@ -830,7 +830,7 @@ void Model::setCollisionModelURL(const QUrl& url) { return; } _collisionUrl = url; - _collisionWatcher.setResource(DependencyManager::get()->fetchResource(url)); + _collisionWatcher.setResource(DependencyManager::get()->getGeometryResource(url)); } bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { From 3e4385913927e69b1a9068139a8822d12c6c5917 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 11 Jul 2016 08:47:15 -0700 Subject: [PATCH 1091/1237] setCollisionModelURL() more symmetric with setURL() --- libraries/render-utils/src/Model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 485c578045..e2363d0cca 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -826,7 +826,7 @@ void Model::setURL(const QUrl& url) { } void Model::setCollisionModelURL(const QUrl& url) { - if (_collisionUrl == url) { + if (_collisionUrl == url && _collisionWatcher.getURL() == url) { return; } _collisionUrl = url; From c106f4c3a01578f24831a8dbb2b07cf402ee212f Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 12 Jul 2016 10:15:28 -0700 Subject: [PATCH 1092/1237] fix crash for null pointer --- libraries/model-networking/src/model-networking/ModelCache.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 93eab6aa24..f513a21fbc 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -106,7 +106,7 @@ public: void setResource(GeometryResource::Pointer resource); - const QUrl& getURL() const { return _resource->getURL(); } + QUrl GeometryResourceWatcher::getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); } private: void startWatching(); From cba49be9eaa64b28a0af35df60114058dbad115b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 12 Jul 2016 16:30:26 -0700 Subject: [PATCH 1093/1237] fix compile bug --- libraries/model-networking/src/model-networking/ModelCache.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index f513a21fbc..aa3ea78db3 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -106,7 +106,7 @@ public: void setResource(GeometryResource::Pointer resource); - QUrl GeometryResourceWatcher::getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); } + QUrl getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); } private: void startWatching(); From cd1c114807d4d81b0607822632fe8a5c2bd58f44 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 15:20:31 -0700 Subject: [PATCH 1094/1237] enable the trigger mapping so it's actually usable --- scripts/system/ignore.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index 3acbda4666..f99a45e684 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -191,10 +191,11 @@ function makeTriggerHandler(hand) { } }; } - triggerMapping.from(Controller.Standard.RT).peek().to(makeTriggerHandler(Controller.Standard.RightHand)); triggerMapping.from(Controller.Standard.LT).peek().to(makeTriggerHandler(Controller.Standard.LeftHand)); +triggerMapping.enable(); + // cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function() { toolbar.removeButton('ignore'); From 9deb9744c644e5b98107c68e495f99b42fe89c03 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 15:34:20 -0700 Subject: [PATCH 1095/1237] handle trigger off in controller mapping --- scripts/system/ignore.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index f99a45e684..46040ba2bc 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -188,6 +188,8 @@ function makeTriggerHandler(hand) { handleClickedOverlay(overlayIntersection); } } + } else if (triggered && (value < TRIGGER_OFF_VALUE)) { + triggered = false; } }; } From 61975fe33a583eef5861378d6fc3ed9cbae2eeef Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 15:47:52 -0700 Subject: [PATCH 1096/1237] use correct function for peeked overlay --- scripts/system/ignore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index 46040ba2bc..920c8fc1de 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -185,7 +185,7 @@ function makeTriggerHandler(hand) { if (pickRay) { var overlayIntersection = Overlays.findRayIntersection(pickRay); if (overlayIntersection.intersects) { - handleClickedOverlay(overlayIntersection); + handleSelectedOverlay(overlayIntersection); } } } else if (triggered && (value < TRIGGER_OFF_VALUE)) { From 1d77cec125876844b3c4afced504aa475db9a094 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Wed, 13 Jul 2016 07:32:05 -0700 Subject: [PATCH 1097/1237] Support for scale property on model overlays If "scale" property is present but "dimensions" are not, the model is NOT scale to fit. And the scale of the model's natural dimensions will be affected by the scale properties. If only the "dimensions" property is present, the model will "scale to fit" the dimensions. If both properties are present, the model still be "scale to fit" but the dimension will be scaled by the scale factor. For example: If a model is loaded that is 2cm tall, is loaded with no "dimensions" or "scale" properties. It will be displayed as 2cm tall. {"scale": 2} The above properties will result in a model that is 4cm tall. {"dimensions": 1} This will result in a model that is 1cm tall. {"scale": 2, "dimensions" 2} Will result in a model that is 2cm tall. --- interface/src/ui/overlays/Base3DOverlay.h | 16 +++--- interface/src/ui/overlays/ModelOverlay.cpp | 55 +++++++++++++------ interface/src/ui/overlays/ModelOverlay.h | 5 +- interface/src/ui/overlays/Volume3DOverlay.cpp | 6 +- interface/src/ui/overlays/Volume3DOverlay.h | 14 ++--- 5 files changed, 60 insertions(+), 36 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 41e7e517b7..e602dec48c 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -17,7 +17,7 @@ class Base3DOverlay : public Overlay { Q_OBJECT - + public: Base3DOverlay(); Base3DOverlay(const Base3DOverlay* base3DOverlay); @@ -27,10 +27,10 @@ public: const glm::vec3& getPosition() const { return _transform.getTranslation(); } const glm::quat& getRotation() const { return _transform.getRotation(); } const glm::vec3& getScale() const { return _transform.getScale(); } - + // TODO: consider implementing registration points in this class const glm::vec3& getCenter() const { return getPosition(); } - + float getLineWidth() const { return _lineWidth; } bool getIsSolid() const { return _isSolid; } bool getIsDashedLine() const { return _isDashedLine; } @@ -43,7 +43,7 @@ public: void setRotation(const glm::quat& value) { _transform.setRotation(value); } void setScale(float value) { _transform.setScale(value); } void setScale(const glm::vec3& value) { _transform.setScale(value); } - + void setLineWidth(float lineWidth) { _lineWidth = lineWidth; } void setIsSolid(bool isSolid) { _isSolid = isSolid; } void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; } @@ -55,22 +55,22 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal); - virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, + virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { return findRayIntersection(origin, direction, distance, face, surfaceNormal); } protected: Transform _transform; - + float _lineWidth; bool _isSolid; bool _isDashedLine; bool _ignoreRayIntersection; bool _drawInFront; }; - + #endif // hifi_Base3DOverlay_h diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index a857dc39e0..2897c07e64 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -19,8 +19,7 @@ QString const ModelOverlay::TYPE = "model"; ModelOverlay::ModelOverlay() : _model(std::make_shared(std::make_shared())), - _modelTextures(QVariantMap()), - _updateModel(false) + _modelTextures(QVariantMap()) { _model->init(); _isLoaded = false; @@ -44,7 +43,11 @@ void ModelOverlay::update(float deltatime) { if (_updateModel) { _updateModel = false; _model->setSnapModelToCenter(true); - _model->setScaleToFit(true, getDimensions()); + if (_scaleToFit) { + _model->setScaleToFit(true, getScale() * getDimensions()); + } else { + _model->setScale(getScale()); + } _model->setRotation(getRotation()); _model->setTranslation(getPosition()); _model->setURL(_url); @@ -84,16 +87,33 @@ void ModelOverlay::render(RenderArgs* args) { } void ModelOverlay::setProperties(const QVariantMap& properties) { - auto position = getPosition(); - auto rotation = getRotation(); + auto origPosition = getPosition(); + auto origRotation = getRotation(); + auto origDimensions = getDimensions(); + auto origScale = getScale(); - Volume3DOverlay::setProperties(properties); + Base3DOverlay::setProperties(properties); - if (position != getPosition() || rotation != getRotation()) { - _updateModel = true; + auto scale = properties["scale"]; + if (scale.isValid()) { + setScale(vec3FromVariant(scale)); } - _updateModel = true; + auto dimensions = properties["dimensions"]; + if (dimensions.isValid()) { + _scaleToFit = true; + setDimensions(vec3FromVariant(dimensions)); + } else if (scale.isValid()) { + // if "scale" property is set but "dimentions" is not. + // do NOT scale to fit. + if (scale.isValid()) { + _scaleToFit = false; + } + } + + if (origPosition != getPosition() || origRotation != getRotation() || origDimensions != getDimensions() || origScale != getScale()) { + _updateModel = true; + } auto urlValue = properties["url"]; if (urlValue.isValid() && urlValue.canConvert()) { @@ -101,15 +121,15 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { _updateModel = true; _isLoaded = false; } - + auto texturesValue = properties["textures"]; if (texturesValue.isValid() && texturesValue.canConvert(QVariant::Map)) { QVariantMap textureMap = texturesValue.toMap(); foreach(const QString& key, textureMap.keys()) { - + QUrl newTextureURL = textureMap[key].toUrl(); qDebug() << "Updating texture named" << key << "to texture at URL" << newTextureURL; - + QMetaObject::invokeMethod(_model.get(), "setTextureWithNameToURL", Qt::AutoConnection, Q_ARG(const QString&, key), Q_ARG(const QUrl&, newTextureURL)); @@ -123,8 +143,11 @@ QVariant ModelOverlay::getProperty(const QString& property) { if (property == "url") { return _url.toString(); } - if (property == "dimensions" || property == "scale" || property == "size") { - return vec3toVariant(_model->getScaleToFitDimensions()); + if (property == "dimensions" || property == "size") { + return vec3toVariant(getDimensions()); + } + if (property == "scale") { + return vec3toVariant(getScale()); } if (property == "textures") { if (_modelTextures.size() > 0) { @@ -143,14 +166,14 @@ QVariant ModelOverlay::getProperty(const QString& property) { bool ModelOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) { - + QString subMeshNameTemp; return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, subMeshNameTemp); } bool ModelOverlay::findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { - + return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, extraInfo); } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index dc4b4a853b..091cab44c9 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -43,9 +43,10 @@ private: ModelPointer _model; QVariantMap _modelTextures; - + QUrl _url; - bool _updateModel; + bool _updateModel = { false }; + bool _scaleToFit = { false }; }; #endif // hifi_ModelOverlay_h diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index c8078d35c6..563198c976 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -22,7 +22,7 @@ AABox Volume3DOverlay::getBounds() const { auto extents = Extents{_localBoundingBox}; extents.rotate(getRotation()); extents.shiftBy(getPosition()); - + return AABox(extents); } @@ -31,7 +31,7 @@ void Volume3DOverlay::setProperties(const QVariantMap& properties) { auto dimensions = properties["dimensions"]; - // if "dimensions" property was not there, check to see if they included aliases: scale + // if "dimensions" property was not there, check to see if they included aliases: scale, size if (!dimensions.isValid()) { dimensions = properties["scale"]; if (!dimensions.isValid()) { @@ -57,7 +57,7 @@ bool Volume3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve // extents is the entity relative, scaled, centered extents of the entity glm::mat4 worldToEntityMatrix; _transform.getInverseMatrix(worldToEntityMatrix); - + glm::vec3 overlayFrameOrigin = glm::vec3(worldToEntityMatrix * glm::vec4(origin, 1.0f)); glm::vec3 overlayFrameDirection = glm::vec3(worldToEntityMatrix * glm::vec4(direction, 0.0f)); diff --git a/interface/src/ui/overlays/Volume3DOverlay.h b/interface/src/ui/overlays/Volume3DOverlay.h index 4d087615d2..04b694b2f8 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.h +++ b/interface/src/ui/overlays/Volume3DOverlay.h @@ -15,13 +15,13 @@ class Volume3DOverlay : public Base3DOverlay { Q_OBJECT - + public: Volume3DOverlay() {} Volume3DOverlay(const Volume3DOverlay* volume3DOverlay); - + virtual AABox getBounds() const override; - + const glm::vec3& getDimensions() const { return _localBoundingBox.getDimensions(); } void setDimensions(float value) { _localBoundingBox.setBox(glm::vec3(-value / 2.0f), value); } void setDimensions(const glm::vec3& value) { _localBoundingBox.setBox(-value / 2.0f, value); } @@ -29,13 +29,13 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, glm::vec3& surfaceNormal) override; - + virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + BoxFace& face, glm::vec3& surfaceNormal) override; + protected: // Centered local bounding box AABox _localBoundingBox{ vec3(0.0f), 1.0f }; }; - + #endif // hifi_Volume3DOverlay_h From 946c7d46447fc1dfb82061de56495512289f9bec Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 16:21:12 -0700 Subject: [PATCH 1098/1237] only handle trigger events when overlays are shown --- scripts/system/ignore.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index 920c8fc1de..2216ecff7e 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -168,28 +168,30 @@ function controllerComputePickRay() { MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); - return {origin: controllerPosition, direction: controllerDirection}; + return { origin: controllerPosition, direction: controllerDirection }; } } function makeTriggerHandler(hand) { return function (value) { - if (!triggered && (value > TRIGGER_GRAB_VALUE)) { // should we smooth? - triggered = true; - if (activeHand !== hand) { - // No switching while the other is already triggered, so no need to release. - activeHand = (activeHand === Controller.Standard.RightHand) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; - } - - var pickRay = controllerComputePickRay(); - if (pickRay) { - var overlayIntersection = Overlays.findRayIntersection(pickRay); - if (overlayIntersection.intersects) { - handleSelectedOverlay(overlayIntersection); + if (isShowingOverlays) { + if (!triggered && (value > TRIGGER_GRAB_VALUE)) { // should we smooth? + triggered = true; + if (activeHand !== hand) { + // No switching while the other is already triggered, so no need to release. + activeHand = (activeHand === Controller.Standard.RightHand) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; } + + var pickRay = controllerComputePickRay(); + if (pickRay) { + var overlayIntersection = Overlays.findRayIntersection(pickRay); + if (overlayIntersection.intersects) { + handleSelectedOverlay(overlayIntersection); + } + } + } else if (triggered && (value < TRIGGER_OFF_VALUE)) { + triggered = false; } - } else if (triggered && (value < TRIGGER_OFF_VALUE)) { - triggered = false; } }; } From aa433e72332393c2bc44c17fe956073703714425 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 13 Jul 2016 16:23:40 -0700 Subject: [PATCH 1099/1237] handle click event on overlays only when in ignore mode --- scripts/system/ignore.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index 2216ecff7e..39405a2e57 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -134,20 +134,22 @@ function handleSelectedOverlay(clickedOverlay) { } Controller.mousePressEvent.connect(function(event){ - // handle click events so we can detect when our overlays are clicked + if (isShowingOverlays) { + // handle click events so we can detect when our overlays are clicked - if (!event.isLeftButton) { - // if another mouse button than left is pressed ignore it - return false; - } + if (!event.isLeftButton) { + // if another mouse button than left is pressed ignore it + return false; + } - // compute the pick ray from the event - var pickRay = Camera.computePickRay(event.x, event.y); + // compute the pick ray from the event + var pickRay = Camera.computePickRay(event.x, event.y); - // grab the clicked overlay for the given pick ray - var clickedOverlay = Overlays.findRayIntersection(pickRay); - if (clickedOverlay.intersects) { - handleSelectedOverlay(clickedOverlay); + // grab the clicked overlay for the given pick ray + var clickedOverlay = Overlays.findRayIntersection(pickRay); + if (clickedOverlay.intersects) { + handleSelectedOverlay(clickedOverlay); + } } }); From a267843e3ec55880130a820a48a1409847627667 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 13 Jul 2016 16:37:30 -0700 Subject: [PATCH 1100/1237] fix razor blades in your ears when switching between domains with no codecs installed --- libraries/audio-client/src/AudioClient.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0cd01bb579..78e71907f8 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -534,14 +534,18 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess _selectedCodecName = message->readString(); qDebug() << "Selected Codec:" << _selectedCodecName; + + // release any old codec encoder/decoder first... + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + _codec = nullptr; + } + _receivedAudioStream.cleanupCodec(); + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { - // release any old codec encoder/decoder first... - if (_codec && _encoder) { - _codec->releaseEncoder(_encoder); - _encoder = nullptr; - } _codec = plugin; _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); @@ -821,7 +825,6 @@ void AudioClient::handleAudioInput() { audioTransform.setRotation(_orientationGetter()); // FIXME find a way to properly handle both playback audio and user audio concurrently - // TODO - codec encode goes here QByteArray decocedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); QByteArray encodedBuffer; if (_encoder) { @@ -840,7 +843,6 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { audioTransform.setTranslation(_positionGetter()); audioTransform.setRotation(_orientationGetter()); - // TODO - codec encode goes here QByteArray encodedBuffer; if (_encoder) { _encoder->encode(audio, encodedBuffer); From 219b41e8133dbb97462f41628a2085365805d307 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 13 Jul 2016 17:37:35 -0700 Subject: [PATCH 1101/1237] Fix the mini mirror perf issue and improve the debugDeferredLighting script --- libraries/gpu/src/gpu/Framebuffer.cpp | 18 ++++- libraries/gpu/src/gpu/Framebuffer.h | 5 ++ .../src/DeferredLightingEffect.cpp | 6 +- libraries/render-utils/src/LightingModel.cpp | 26 ++++--- libraries/render-utils/src/LightingModel.h | 12 +-- libraries/render-utils/src/LightingModel.slh | 14 ++-- .../render-utils/src/RenderDeferredTask.cpp | 34 +++++--- .../render-utils/src/RenderDeferredTask.h | 5 +- .../render-utils/src/RenderPipelines.cpp | 16 ---- .../render-utils/src/SubsurfaceScattering.cpp | 1 - .../render-utils/src/SurfaceGeometryPass.cpp | 16 +--- .../render-utils/src/ToneMappingEffect.cpp | 77 ++----------------- .../render-utils/src/deferred_light_spot.slv | 1 + .../utilities/render/debugDeferredLighting.js | 29 ++++++- .../utilities/render/deferredLighting.qml | 68 ++++++++++++++-- tests/gpu-test/src/TestWindow.cpp | 1 - 16 files changed, 180 insertions(+), 149 deletions(-) diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index ac7b136550..fa6a32d9e4 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -13,6 +13,8 @@ #include #include +#include + using namespace gpu; Framebuffer::~Framebuffer() { @@ -290,4 +292,18 @@ Format Framebuffer::getDepthStencilBufferFormat() const { } else { return _depthStencilBuffer._element; } -} \ No newline at end of file +} + +Transform Framebuffer::evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec2& destRegionSize, const glm::ivec2& destRegionOffset) { + float sMin = destRegionOffset.x / (float)sourceSurface.x; + float sWidth = destRegionSize.x / (float)sourceSurface.x; + float tMin = destRegionOffset.y / (float)sourceSurface.y; + float tHeight = destRegionSize.y / (float)sourceSurface.y; + Transform model; + model.setTranslation(glm::vec3(sMin, tMin, 0.0)); + model.setScale(glm::vec3(sWidth, tHeight, 1.0)); + return model; +} +Transform Framebuffer::evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec4& destViewport) { + return evalSubregionTexcoordTransform(sourceSurface, glm::ivec2(destViewport.x, destViewport.y), glm::ivec2(destViewport.z, destViewport.w)); +} diff --git a/libraries/gpu/src/gpu/Framebuffer.h b/libraries/gpu/src/gpu/Framebuffer.h index 5d016645fe..46b10fa04d 100755 --- a/libraries/gpu/src/gpu/Framebuffer.h +++ b/libraries/gpu/src/gpu/Framebuffer.h @@ -14,6 +14,8 @@ #include "Texture.h" #include +class Transform; // Texcood transform util + namespace gpu { typedef Element Format; @@ -139,6 +141,9 @@ public: Stamp getDepthStamp() const { return _depthStamp; } const std::vector& getColorStamps() const { return _colorStamps; } + static Transform evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec2& destRegionSize, const glm::ivec2& destRegionOffset = glm::ivec2(0)); + static Transform evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec4& destViewport); + protected: SwapchainPointer _swapchain; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index a55a87a46e..0caee61f75 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -331,7 +331,11 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, gpu::FramebufferPointer& primaryFramebuffer) { auto args = renderContext->args; - glm::ivec2 frameSize(args->_viewport.z, args->_viewport.w); + auto framebufferCache = DependencyManager::get(); + auto framebufferSize = framebufferCache->getFrameBufferSize(); + + // glm::ivec2 frameSize(args->_viewport.z, args->_viewport.w); + glm::ivec2 frameSize(framebufferSize.width(), framebufferSize.height()); if (!_primaryFramebuffer) { _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index a09c290b8d..7f05daac51 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -24,15 +24,6 @@ bool LightingModel::isUnlitEnabled() const { return (bool)_parametersBuffer.get().enableUnlit; } -void LightingModel::setShaded(bool enable) { - if (enable != isShadedEnabled()) { - _parametersBuffer.edit().enableShaded = (float)enable; - } -} -bool LightingModel::isShadedEnabled() const { - return (bool)_parametersBuffer.get().enableShaded; -} - void LightingModel::setEmissive(bool enable) { if (enable != isEmissiveEnabled()) { _parametersBuffer.edit().enableEmissive = (float)enable; @@ -50,6 +41,16 @@ bool LightingModel::isLightmapEnabled() const { return (bool)_parametersBuffer.get().enableLightmap; } +void LightingModel::setBackground(bool enable) { + if (enable != isBackgroundEnabled()) { + _parametersBuffer.edit().enableBackground = (float)enable; + } +} +bool LightingModel::isBackgroundEnabled() const { + return (bool)_parametersBuffer.get().enableBackground; +} + + void LightingModel::setScattering(bool enable) { if (enable != isScatteringEnabled()) { _parametersBuffer.edit().enableScattering = (float)enable; @@ -122,7 +123,7 @@ void LightingModel::setShowLightContour(bool enable) { } } bool LightingModel::isShowLightContourEnabled() const { - return (bool)(_parametersBuffer.get().showLightContour > 0.0f); + return (bool)_parametersBuffer.get().showLightContour; } MakeLightingModel::MakeLightingModel() { @@ -131,17 +132,20 @@ MakeLightingModel::MakeLightingModel() { void MakeLightingModel::configure(const Config& config) { _lightingModel->setUnlit(config.enableUnlit); - _lightingModel->setShaded(config.enableShaded); _lightingModel->setEmissive(config.enableEmissive); _lightingModel->setLightmap(config.enableLightmap); + _lightingModel->setBackground(config.enableBackground); + _lightingModel->setScattering(config.enableScattering); _lightingModel->setDiffuse(config.enableDiffuse); _lightingModel->setSpecular(config.enableSpecular); _lightingModel->setAlbedo(config.enableAlbedo); + _lightingModel->setAmbientLight(config.enableAmbientLight); _lightingModel->setDirectionalLight(config.enableDirectionalLight); _lightingModel->setPointLight(config.enablePointLight); _lightingModel->setSpotLight(config.enableSpotLight); + _lightingModel->setShowLightContour(config.showLightContour); } diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index d895be297c..82e674db7b 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -27,14 +27,15 @@ public: void setUnlit(bool enable); bool isUnlitEnabled() const; - void setShaded(bool enable); - bool isShadedEnabled() const; void setEmissive(bool enable); bool isEmissiveEnabled() const; void setLightmap(bool enable); bool isLightmapEnabled() const; + void setBackground(bool enable); + bool isBackgroundEnabled() const; + void setScattering(bool enable); bool isScatteringEnabled() const; void setDiffuse(bool enable); @@ -67,9 +68,9 @@ protected: class Parameters { public: float enableUnlit{ 1.0f }; - float enableShaded{ 1.0f }; float enableEmissive{ 1.0f }; float enableLightmap{ 1.0f }; + float enableBackground{ 1.0f }; float enableScattering{ 1.0f }; float enableDiffuse{ 1.0f }; @@ -98,9 +99,9 @@ class MakeLightingModelConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(bool enableUnlit MEMBER enableUnlit NOTIFY dirty) - Q_PROPERTY(bool enableShaded MEMBER enableShaded NOTIFY dirty) Q_PROPERTY(bool enableEmissive MEMBER enableEmissive NOTIFY dirty) Q_PROPERTY(bool enableLightmap MEMBER enableLightmap NOTIFY dirty) + Q_PROPERTY(bool enableBackground MEMBER enableBackground NOTIFY dirty) Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) Q_PROPERTY(bool enableDiffuse MEMBER enableDiffuse NOTIFY dirty) @@ -118,9 +119,10 @@ public: MakeLightingModelConfig() : render::Job::Config() {} // Make Lighting Model is always on bool enableUnlit{ true }; - bool enableShaded{ true }; bool enableEmissive{ true }; bool enableLightmap{ true }; + bool enableBackground{ true }; + bool enableScattering{ true }; bool enableDiffuse{ true }; diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index f34a770d72..b246516df9 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -14,7 +14,7 @@ <@func declareLightingModel()@> struct LightingModel { - vec4 _UnlitShadedEmissiveLightmap; + vec4 _UnlitEmissiveLightmapBackground; vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; vec4 _ShowContour; @@ -25,16 +25,16 @@ uniform lightingModelBuffer { }; float isUnlitEnabled() { - return lightingModel._UnlitShadedEmissiveLightmap.x; -} -float isShadedEnabled() { - return lightingModel._UnlitShadedEmissiveLightmap.y; + return lightingModel._UnlitEmissiveLightmapBackground.x; } float isEmissiveEnabled() { - return lightingModel._UnlitShadedEmissiveLightmap.z; + return lightingModel._UnlitEmissiveLightmapBackground.y; } float isLightmapEnabled() { - return lightingModel._UnlitShadedEmissiveLightmap.w; + return lightingModel._UnlitEmissiveLightmapBackground.z; +} +float isBackgroundEnabled() { + return lightingModel._UnlitEmissiveLightmapBackground.w; } float isScatteringEnabled() { diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 5299b395ca..bc7af6e118 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -39,9 +39,12 @@ #include "ToneMappingEffect.h" #include "SubsurfaceScattering.h" -using namespace render; +#include -extern void initStencilPipeline(gpu::PipelinePointer& pipeline); +#include "drawOpaqueStencil_frag.h" + + +using namespace render; extern void initOverlay3DPipelines(render::ShapePlumber& plumber); extern void initDeferredPipelines(render::ShapePlumber& plumber); @@ -334,10 +337,21 @@ void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderCon } } -gpu::PipelinePointer DrawStencilDeferred::_opaquePipeline; -const gpu::PipelinePointer& DrawStencilDeferred::getOpaquePipeline() { + +gpu::PipelinePointer DrawStencilDeferred::getOpaquePipeline() { if (!_opaquePipeline) { - initStencilPipeline(_opaquePipeline); + const gpu::int8 STENCIL_OPAQUE = 1; + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(drawOpaqueStencil_frag)); + auto program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram((*program)); + + auto state = std::make_shared(); + state->setDepthTest(true, false, gpu::LESS_EQUAL); + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE)); + state->setColorWriteMask(0); + + _opaquePipeline = gpu::Pipeline::create(program, state); } return _opaquePipeline; } @@ -374,13 +388,15 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const assert(renderContext->args->hasViewFrustum()); const auto& inItems = inputs.get0(); - // Not used yet - // const auto& lightingModel = inputs.get1(); + const auto& lightingModel = inputs.get1(); + if (!lightingModel->isBackgroundEnabled()) { + return; + } RenderArgs* args = renderContext->args; doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; - _gpuTimer.begin(batch); + // _gpuTimer.begin(batch); batch.enableSkybox(true); @@ -396,7 +412,7 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const batch.setViewTransform(viewMat); renderItems(sceneContext, renderContext, inItems); - _gpuTimer.end(batch); + // _gpuTimer.end(batch); }); args->_batch = nullptr; diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index b6ad83985e..7ddbeecae4 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -98,10 +98,11 @@ public: using JobModel = render::Job::ModelI>; void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const std::shared_ptr& deferredFramebuffer); - static const gpu::PipelinePointer& getOpaquePipeline(); protected: - static gpu::PipelinePointer _opaquePipeline; //lazy evaluation hence mutable + gpu::PipelinePointer _opaquePipeline; + + gpu::PipelinePointer getOpaquePipeline(); }; class DrawBackgroundDeferredConfig : public render::Job::Config { diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 16681fd363..e50492fd64 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -45,25 +45,9 @@ #include "overlay3D_unlit_frag.h" #include "overlay3D_translucent_unlit_frag.h" -#include "drawOpaqueStencil_frag.h" using namespace render; -void initStencilPipeline(gpu::PipelinePointer& pipeline) { - const gpu::int8 STENCIL_OPAQUE = 1; - auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(drawOpaqueStencil_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - gpu::Shader::makeProgram((*program)); - - auto state = std::make_shared(); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE)); - state->setColorWriteMask(0); - - pipeline = gpu::Pipeline::create(program, state); -} - gpu::BufferView getDefaultMaterialBuffer() { model::Material::Schema schema; schema._albedo = vec3(1.0f); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index e0d9234e49..f1aec66433 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -454,7 +454,6 @@ void DebugSubsurfaceScattering::configure(const Config& config) { gpu::PipelinePointer DebugSubsurfaceScattering::getScatteringPipeline() { if (!_scatteringPipeline) { auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - // auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); auto ps = gpu::Shader::createPixel(std::string(subsurfaceScattering_drawScattering_frag)); gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index dddc8a1c53..c5d3f79bba 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -13,7 +13,6 @@ #include #include -#include "FramebufferCache.h" const int SurfaceGeometryPass_FrameTransformSlot = 0; const int SurfaceGeometryPass_ParamsSlot = 1; @@ -135,7 +134,6 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c } _surfaceGeometryFramebuffer->updatePrimaryDepth(deferredFramebuffer->getPrimaryDepthTexture()); - auto framebufferCache = DependencyManager::get(); auto depthBuffer = deferredFramebuffer->getPrimaryDepthTexture(); auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); @@ -148,14 +146,6 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c curvatureAndDepth.edit1() = curvatureFBO; curvatureAndDepth.edit2() = linearDepthTexture; - - QSize framebufferSize = framebufferCache->getFrameBufferSize(); - float sMin = args->_viewport.x / (float)framebufferSize.width(); - float sWidth = args->_viewport.z / (float)framebufferSize.width(); - float tMin = args->_viewport.y / (float)framebufferSize.height(); - float tHeight = args->_viewport.w / (float)framebufferSize.height(); - - auto linearDepthPipeline = getLinearDepthPipeline(); auto curvaturePipeline = getCurvaturePipeline(); @@ -165,11 +155,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); batch.setViewTransform(Transform()); - - Transform model; - model.setTranslation(glm::vec3(sMin, tMin, 0.0f)); - model.setScale(glm::vec3(sWidth, tHeight, 1.0f)); - batch.setModelTransform(model); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getFrameSize(), args->_viewport)); batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index e844ee6eda..b0ac40a839 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -18,6 +18,8 @@ #include "FramebufferCache.h" +#include "toneMapping_frag.h" + const int ToneMappingEffect_ParamsSlot = 0; const int ToneMappingEffect_LightingMapSlot = 0; @@ -27,67 +29,7 @@ ToneMappingEffect::ToneMappingEffect() { } void ToneMappingEffect::init() { - const char BlitTextureGamma_frag[] = R"SCRIBE( - // Generated on Sat Oct 24 09:34:37 2015 - // - // 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 - // - - struct ToneMappingParams { - vec4 _exp_2powExp_s0_s1; - ivec4 _toneCurve_s0_s1_s2; - }; - - const float INV_GAMMA_22 = 1.0 / 2.2; - const int ToneCurveNone = 0; - const int ToneCurveGamma22 = 1; - const int ToneCurveReinhard = 2; - const int ToneCurveFilmic = 3; - - uniform toneMappingParamsBuffer { - ToneMappingParams params; - }; - float getTwoPowExposure() { - return params._exp_2powExp_s0_s1.y; - } - int getToneCurve() { - return params._toneCurve_s0_s1_s2.x; - } - - uniform sampler2D colorMap; - - in vec2 varTexCoord0; - out vec4 outFragColor; - - void main(void) { - vec4 fragColorRaw = texture(colorMap, varTexCoord0); - vec3 fragColor = fragColorRaw.xyz; - - vec3 srcColor = fragColor * getTwoPowExposure(); - - int toneCurve = getToneCurve(); - vec3 tonedColor = srcColor; - if (toneCurve == ToneCurveFilmic) { - vec3 x = max(vec3(0.0), srcColor-0.004); - tonedColor = (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06); - } else if (toneCurve == ToneCurveReinhard) { - tonedColor = srcColor/(1.0 + srcColor); - tonedColor = pow(tonedColor, vec3(INV_GAMMA_22)); - } else if (toneCurve == ToneCurveGamma22) { - tonedColor = pow(srcColor, vec3(INV_GAMMA_22)); - } // else None toned = src - - outFragColor = vec4(tonedColor, 1.0); - } - - )SCRIBE"; - auto blitPS = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(BlitTextureGamma_frag))); + auto blitPS = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(toneMapping_frag))); auto blitVS = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); auto blitProgram = gpu::ShaderPointer(gpu::Shader::createProgram(blitVS, blitPS)); @@ -131,17 +73,8 @@ void ToneMappingEffect::render(RenderArgs* args, const gpu::TexturePointer& ligh batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); batch.setViewTransform(Transform()); - { - float sMin = args->_viewport.x / (float)framebufferSize.x; - float sWidth = args->_viewport.z / (float)framebufferSize.x; - float tMin = args->_viewport.y / (float)framebufferSize.y; - float tHeight = args->_viewport.w / (float)framebufferSize.y; - Transform model; - batch.setPipeline(_blitLightBuffer); - model.setTranslation(glm::vec3(sMin, tMin, 0.0)); - model.setScale(glm::vec3(sWidth, tHeight, 1.0)); - batch.setModelTransform(model); - } + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(framebufferSize, args->_viewport)); + batch.setPipeline(_blitLightBuffer); batch.setUniformBuffer(ToneMappingEffect_ParamsSlot, _parametersBuffer); batch.setResourceTexture(ToneMappingEffect_LightingMapSlot, lightingBuffer); diff --git a/libraries/render-utils/src/deferred_light_spot.slv b/libraries/render-utils/src/deferred_light_spot.slv index 3e11e2fc1e..a93e944771 100755 --- a/libraries/render-utils/src/deferred_light_spot.slv +++ b/libraries/render-utils/src/deferred_light_spot.slv @@ -19,6 +19,7 @@ <$declareStandardTransform()$> uniform vec4 coneParam; +uniform mat4 texcoordFrameTransform; out vec4 _texCoord0; diff --git a/scripts/developer/utilities/render/debugDeferredLighting.js b/scripts/developer/utilities/render/debugDeferredLighting.js index b61ac77c19..852e7e7f5c 100644 --- a/scripts/developer/utilities/render/debugDeferredLighting.js +++ b/scripts/developer/utilities/render/debugDeferredLighting.js @@ -13,8 +13,35 @@ var qml = Script.resolvePath('deferredLighting.qml'); var window = new OverlayWindow({ title: 'Lighting', source: qml, - width: 400, height: 150, + width: 400, height:250, }); window.setPosition(250, 800);a window.closed.connect(function() { Script.stop(); }); + +var DDB = Render.RenderDeferredTask.DebugDeferredBuffer; +DDB.enabled = true; + +// Debug buffer sizing +var resizing = false; +Controller.mousePressEvent.connect(function (e) { + if (shouldStartResizing(e.x)) { + resizing = true; + } +}); +Controller.mouseReleaseEvent.connect(function() { resizing = false; }); +Controller.mouseMoveEvent.connect(function (e) { resizing && setDebugBufferSize(e.x); }); +Script.scriptEnding.connect(function () { DDB.enabled = false; }); + +function shouldStartResizing(eventX) { + var x = Math.abs(eventX - Window.innerWidth * (1.0 + DDB.size.x) / 2.0); + var mode = DDB.mode; + return mode !== -1 && x < 20; +} + +function setDebugBufferSize(x) { + x = (2.0 * (x / Window.innerWidth) - 1.0); // scale + x = Math.min(Math.max(-1, x), 1); // clamp + DDB.size = { x: x, y: -1, z: 1, w: 1 }; +} + diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 1425013640..9d92b79fd6 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -24,6 +24,7 @@ Column { "Unlit:LightingModel:enableUnlit", "Emissive:LightingModel:enableEmissive", "Lightmap:LightingModel:enableLightmap", + "Background:LightingModel:enableBackground", ] CheckBox { text: modelData.split(":")[0] @@ -96,17 +97,70 @@ Column { } } + Row { + Label { + text: "Debug Framebuffer" + anchors.left: root.left + } + + ComboBox { + currentIndex: 1 + model: ListModel { + id: cbItems + ListElement { text: "RGB"; color: "Yellow" } + ListElement { text: "SRGB"; color: "Green" } + ListElement { text: "Reinhard"; color: "Yellow" } + ListElement { text: "Filmic"; color: "White" } + } + width: 200 + onCurrentIndexChanged: { Render.getConfig("ToneMapping")["curve"] = currentIndex } + } + } + } + Row { + id: framebuffer + + Label { + text: "Debug Framebuffer" + anchors.left: root.left + } + + property var config: Render.getConfig("DebugDeferredBuffer") + + function setDebugMode(mode) { + framebuffer.config.enabled = (mode != -1); + framebuffer.config.mode = mode; + } + ComboBox { - currentIndex: 1 + currentIndex: 0 model: ListModel { - id: cbItems - ListElement { text: "RGB"; color: "Yellow" } - ListElement { text: "SRGB"; color: "Green" } - ListElement { text: "Reinhard"; color: "Yellow" } - ListElement { text: "Filmic"; color: "White" } + id: cbItemsFramebuffer + ListElement { text: "Off"; color: "Yellow" } + ListElement { text: "Depth"; color: "Green" } + ListElement { text: "Albedo"; color: "Yellow" } + ListElement { text: "Normal"; color: "White" } + ListElement { text: "Roughness"; color: "White" } + ListElement { text: "Metallic"; color: "White" } + ListElement { text: "Emissive"; color: "White" } + ListElement { text: "Unlit"; color: "White" } + ListElement { text: "Occlusion"; color: "White" } + ListElement { text: "Lightmap"; color: "White" } + ListElement { text: "Scattering"; color: "White" } + ListElement { text: "Lighting"; color: "White" } + ListElement { text: "Shadow"; color: "White" } + ListElement { text: "Linear Depth"; color: "White" } + ListElement { text: "Mid Curvature"; color: "White" } + ListElement { text: "Mid Normal"; color: "White" } + ListElement { text: "Low Curvature"; color: "White" } + ListElement { text: "Low Normal"; color: "White" } + ListElement { text: "Debug Scattering"; color: "White" } + ListElement { text: "Ambient Occlusion"; color: "White" } + ListElement { text: "Ambient Occlusion Blurred"; color: "White" } + ListElement { text: "Custom"; color: "White" } } width: 200 - onCurrentIndexChanged: { Render.getConfig("ToneMapping")["curve"] = currentIndex } + onCurrentIndexChanged: { framebuffer.setDebugMode(currentIndex - 1) } } } } diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index f0cc3f9592..f31eb6f7bb 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -80,7 +80,6 @@ void TestWindow::initGl() { deferredLightingEffect->init(); deferredLightingEffect->setGlobalLight(_light); initDeferredPipelines(*_shapePlumber); - initStencilPipeline(_opaquePipeline); #endif } From 789297d8496cbb9228d12123ffc691f3ae70ab02 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 13 Jul 2016 17:39:39 -0700 Subject: [PATCH 1102/1237] Remove redundant isValid check. --- interface/src/ui/overlays/ModelOverlay.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 2897c07e64..9c203c0129 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -106,9 +106,7 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { } else if (scale.isValid()) { // if "scale" property is set but "dimentions" is not. // do NOT scale to fit. - if (scale.isValid()) { - _scaleToFit = false; - } + _scaleToFit = false; } if (origPosition != getPosition() || origRotation != getRotation() || origDimensions != getDimensions() || origScale != getScale()) { From f6671b34fa9b841dd7f87012df0c3f2d3f8f9662 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 13 Jul 2016 18:26:53 -0700 Subject: [PATCH 1103/1237] Fix the transformation problem for rendering the mini mirror (scattering is still screwed though) --- libraries/gpu-gl/src/gpu/gl/GLShared.cpp | 2 +- libraries/gpu/src/gpu/Framebuffer.cpp | 13 +++- libraries/gpu/src/gpu/Framebuffer.h | 3 + .../src/DeferredLightingEffect.cpp | 20 ++++++- .../render-utils/src/DeferredLightingEffect.h | 5 +- libraries/render-utils/src/deferred_light.slv | 5 ++ .../src/deferred_light_limited.slv | 1 + .../render-utils/src/deferred_light_spot.slv | 2 - libraries/render-utils/src/point_light.slf | 4 ++ libraries/render-utils/src/spot_light.slf | 4 ++ .../src/surfaceGeometry_makeCurvature.slf | 6 +- libraries/render-utils/src/toneMapping.slf | 59 +++++++++++++++++++ 12 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 libraries/render-utils/src/toneMapping.slf diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index db64fb08d0..db930cf696 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -701,7 +701,7 @@ bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const s } qCWarning(gpugllogging) << "GLShader::compileShader - errors:"; qCWarning(gpugllogging) << temp; - delete[] temp; + delete[] temp; glDeleteShader(glshader); return false; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index fa6a32d9e4..beb0334208 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -293,6 +293,17 @@ Format Framebuffer::getDepthStencilBufferFormat() const { return _depthStencilBuffer._element; } } +glm::vec4 Framebuffer::evalSubregionTexcoordTransformCoefficients(const glm::ivec2& sourceSurface, const glm::ivec2& destRegionSize, const glm::ivec2& destRegionOffset) { + float sMin = destRegionOffset.x / (float)sourceSurface.x; + float sWidth = destRegionSize.x / (float)sourceSurface.x; + float tMin = destRegionOffset.y / (float)sourceSurface.y; + float tHeight = destRegionSize.y / (float)sourceSurface.y; + return glm::vec4(sMin, tMin, sWidth, tHeight); +} + +glm::vec4 Framebuffer::evalSubregionTexcoordTransformCoefficients(const glm::ivec2& sourceSurface, const glm::ivec4& destViewport) { + return evalSubregionTexcoordTransformCoefficients(sourceSurface, glm::ivec2(destViewport.z, destViewport.w), glm::ivec2(destViewport.x, destViewport.y)); +} Transform Framebuffer::evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec2& destRegionSize, const glm::ivec2& destRegionOffset) { float sMin = destRegionOffset.x / (float)sourceSurface.x; @@ -305,5 +316,5 @@ Transform Framebuffer::evalSubregionTexcoordTransform(const glm::ivec2& sourceSu return model; } Transform Framebuffer::evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec4& destViewport) { - return evalSubregionTexcoordTransform(sourceSurface, glm::ivec2(destViewport.x, destViewport.y), glm::ivec2(destViewport.z, destViewport.w)); + return evalSubregionTexcoordTransform(sourceSurface, glm::ivec2(destViewport.z, destViewport.w), glm::ivec2(destViewport.x, destViewport.y)); } diff --git a/libraries/gpu/src/gpu/Framebuffer.h b/libraries/gpu/src/gpu/Framebuffer.h index 46b10fa04d..6fa6367c7d 100755 --- a/libraries/gpu/src/gpu/Framebuffer.h +++ b/libraries/gpu/src/gpu/Framebuffer.h @@ -141,6 +141,9 @@ public: Stamp getDepthStamp() const { return _depthStamp; } const std::vector& getColorStamps() const { return _colorStamps; } + static glm::vec4 evalSubregionTexcoordTransformCoefficients(const glm::ivec2& sourceSurface, const glm::ivec2& destRegionSize, const glm::ivec2& destRegionOffset = glm::ivec2(0)); + static glm::vec4 evalSubregionTexcoordTransformCoefficients(const glm::ivec2& sourceSurface, const glm::ivec4& destViewport); + static Transform evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec2& destRegionSize, const glm::ivec2& destRegionOffset = glm::ivec2(0)); static Transform evalSubregionTexcoordTransform(const glm::ivec2& sourceSurface, const glm::ivec4& destViewport); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 0caee61f75..665f1c14ce 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -44,6 +44,7 @@ struct LightLocations { int radius{ -1 }; int ambientSphere{ -1 }; int lightBufferUnit{ -1 }; + int texcoordFrameTransform{ -1 }; int sphereParam{ -1 }; int coneParam{ -1 }; int deferredFrameTransformBuffer{ -1 }; @@ -190,6 +191,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo locations->radius = program->getUniforms().findLocation("radius"); locations->ambientSphere = program->getUniforms().findLocation("ambientSphere.L00"); + locations->texcoordFrameTransform = program->getUniforms().findLocation("texcoordFrameTransform"); locations->sphereParam = program->getUniforms().findLocation("sphereParam"); locations->coneParam = program->getUniforms().findLocation("coneParam"); @@ -495,6 +497,10 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c batch.setPipeline(program); } + // Adjust the texcoordTransform in the case we are rendeirng a sub region(mini mirror) + auto textureFrameTransform = gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(deferredFramebuffer->getFrameSize(), args->_viewport); + batch._glUniform4fv(locations->texcoordFrameTransform, 1, reinterpret_cast< const float* >(&textureFrameTransform)); + { // Setup the global lighting deferredLightingEffect->setupKeyLightBatch(batch, locations->lightBufferUnit, SKYBOX_MAP_UNIT); } @@ -509,7 +515,14 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c } -void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, bool points, bool spots) { +void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, + const DeferredFrameTransformPointer& frameTransform, + const DeferredFramebufferPointer& deferredFramebuffer, + const LightingModelPointer& lightingModel) { + + bool points = lightingModel->isPointLightEnabled(); + bool spots = lightingModel->isSpotLightEnabled(); + if (!points && !spots) { return; } @@ -548,6 +561,7 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, // enlarge the scales slightly to account for tesselation const float SCALE_EXPANSION = 0.05f; + auto textureFrameTransform = gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(deferredFramebuffer->getFrameSize(), monoViewport); batch.setProjectionTransform(monoProjMat); batch.setViewTransform(monoViewTransform); @@ -556,6 +570,7 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, if (points && !deferredLightingEffect->_pointLights.empty()) { // POint light pipeline batch.setPipeline(deferredLightingEffect->_pointLight); + batch._glUniform4fv(deferredLightingEffect->_pointLightLocations->texcoordFrameTransform, 1, reinterpret_cast< const float* >(&textureFrameTransform)); for (auto lightID : deferredLightingEffect->_pointLights) { auto& light = deferredLightingEffect->_allocatedLights[lightID]; @@ -588,6 +603,7 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, if (spots && !deferredLightingEffect->_spotLights.empty()) { // Spot light pipeline batch.setPipeline(deferredLightingEffect->_spotLight); + batch._glUniform4fv(deferredLightingEffect->_spotLightLocations->texcoordFrameTransform, 1, reinterpret_cast< const float* >(&textureFrameTransform)); // Spot mesh auto mesh = deferredLightingEffect->getSpotLightMesh(); @@ -686,7 +702,7 @@ void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderCo setupJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel, surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer, subsurfaceScatteringResource); - lightsJob.run(sceneContext, renderContext, deferredTransform, lightingModel->isPointLightEnabled(), lightingModel->isSpotLightEnabled()); + lightsJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel); cleanupJob.run(sceneContext, renderContext); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 552bd1f006..ff372aa5b4 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -146,7 +146,10 @@ class RenderDeferredLocals { public: using JobModel = render::Job::ModelI; - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const DeferredFrameTransformPointer& frameTransform, bool points, bool spots); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, + const DeferredFrameTransformPointer& frameTransform, + const DeferredFramebufferPointer& deferredFramebuffer, + const LightingModelPointer& lightingModel); }; diff --git a/libraries/render-utils/src/deferred_light.slv b/libraries/render-utils/src/deferred_light.slv index fa2fcf8ef9..79e8130910 100644 --- a/libraries/render-utils/src/deferred_light.slv +++ b/libraries/render-utils/src/deferred_light.slv @@ -14,6 +14,8 @@ out vec2 _texCoord0; +uniform vec4 texcoordFrameTransform; + void main(void) { const float depth = 1.0; const vec4 UNIT_QUAD[4] = vec4[4]( @@ -26,5 +28,8 @@ void main(void) { _texCoord0 = (pos.xy + 1) * 0.5; + _texCoord0 += texcoordFrameTransform.xy; + _texCoord0 *= texcoordFrameTransform.zw; + gl_Position = pos; } diff --git a/libraries/render-utils/src/deferred_light_limited.slv b/libraries/render-utils/src/deferred_light_limited.slv index 164e2a27d5..81ec882bdf 100644 --- a/libraries/render-utils/src/deferred_light_limited.slv +++ b/libraries/render-utils/src/deferred_light_limited.slv @@ -48,6 +48,7 @@ void main(void) { vec4 pos = UNIT_QUAD[gl_VertexID]; _texCoord0 = vec4((pos.xy + 1) * 0.5, 0.0, 1.0); + if (cam_isStereo()) { _texCoord0.x = 0.5 * (_texCoord0.x + cam_getStereoSide()); } diff --git a/libraries/render-utils/src/deferred_light_spot.slv b/libraries/render-utils/src/deferred_light_spot.slv index a93e944771..6ae133b7a5 100755 --- a/libraries/render-utils/src/deferred_light_spot.slv +++ b/libraries/render-utils/src/deferred_light_spot.slv @@ -19,7 +19,6 @@ <$declareStandardTransform()$> uniform vec4 coneParam; -uniform mat4 texcoordFrameTransform; out vec4 _texCoord0; @@ -51,7 +50,6 @@ void main(void) { projected.x = 0.5 * (projected.x + cam_getStereoSide()); } _texCoord0 = vec4(projected.xy, 0.0, 1.0) * gl_Position.w; - } else { const float depth = -1.0; //Draw at near plane const vec4 UNIT_QUAD[4] = vec4[4]( diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 84475be64e..150460dc9f 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -26,6 +26,7 @@ <$declareLightingPoint(supportScattering)$> +uniform vec4 texcoordFrameTransform; in vec4 _texCoord0; out vec4 _fragColor; @@ -35,6 +36,9 @@ void main(void) { // Grab the fragment data from the uv vec2 texCoord = _texCoord0.st / _texCoord0.q; + texCoord += texcoordFrameTransform.xy; + texCoord *= texcoordFrameTransform.zw; + DeferredFragment frag = unpackDeferredFragment(deferredTransform, texCoord); if (frag.mode == FRAG_MODE_UNLIT) { diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index cd379af51e..0502fc2753 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -25,6 +25,7 @@ <@include LightSpot.slh@> <$declareLightingSpot(supportScattering)$> +uniform vec4 texcoordFrameTransform; in vec4 _texCoord0; out vec4 _fragColor; @@ -35,6 +36,9 @@ void main(void) { // Grab the fragment data from the uv vec2 texCoord = _texCoord0.st / _texCoord0.q; + texCoord += texcoordFrameTransform.xy; + texCoord *= texcoordFrameTransform.zw; + DeferredFragment frag = unpackDeferredFragment(deferredTransform, texCoord); if (frag.mode == FRAG_MODE_UNLIT) { diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index 6c19982c2c..c5cb12aeb8 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -92,6 +92,7 @@ float getEyeDepthDiff(vec2 texcoord, vec2 delta) { +in vec2 varTexCoord0; out vec4 outFragColor; void main(void) { @@ -101,7 +102,10 @@ void main(void) { ivec4 stereoSide; ivec2 framePixelPos = getPixelPosTexcoordPosAndSide(gl_FragCoord.xy, pixelPos, texcoordPos, stereoSide); vec2 stereoSideClip = vec2(stereoSide.x, (isStereo() ? 0.5 : 1.0)); - vec2 frameTexcoordPos = sideToFrameTexcoord(stereoSideClip, texcoordPos); + + // Texcoord to fetch in the deferred texture are the exact UVs comming from vertex shader + // sideToFrameTexcoord(stereoSideClip, texcoordPos); + vec2 frameTexcoordPos = varTexCoord0; // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); diff --git a/libraries/render-utils/src/toneMapping.slf b/libraries/render-utils/src/toneMapping.slf new file mode 100644 index 0000000000..d6504b5bff --- /dev/null +++ b/libraries/render-utils/src/toneMapping.slf @@ -0,0 +1,59 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on Sat Oct 24 09:34:37 2015 +// +// 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 +// + +struct ToneMappingParams { + vec4 _exp_2powExp_s0_s1; + ivec4 _toneCurve_s0_s1_s2; +}; + +const float INV_GAMMA_22 = 1.0 / 2.2; +const int ToneCurveNone = 0; +const int ToneCurveGamma22 = 1; +const int ToneCurveReinhard = 2; +const int ToneCurveFilmic = 3; + +uniform toneMappingParamsBuffer { + ToneMappingParams params; +}; +float getTwoPowExposure() { + return params._exp_2powExp_s0_s1.y; +} +int getToneCurve() { + return params._toneCurve_s0_s1_s2.x; +} + +uniform sampler2D colorMap; + +in vec2 varTexCoord0; +out vec4 outFragColor; + +void main(void) { + vec4 fragColorRaw = texture(colorMap, varTexCoord0); + vec3 fragColor = fragColorRaw.xyz; + + vec3 srcColor = fragColor * getTwoPowExposure(); + + int toneCurve = getToneCurve(); + vec3 tonedColor = srcColor; + if (toneCurve == ToneCurveFilmic) { + vec3 x = max(vec3(0.0), srcColor-0.004); + tonedColor = (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06); + } else if (toneCurve == ToneCurveReinhard) { + tonedColor = srcColor/(1.0 + srcColor); + tonedColor = pow(tonedColor, vec3(INV_GAMMA_22)); + } else if (toneCurve == ToneCurveGamma22) { + tonedColor = pow(srcColor, vec3(INV_GAMMA_22)); + } // else None toned = src + + outFragColor = vec4(tonedColor, 1.0); +} From b9a2b2778ee6d5dfc689b35e2f3b4f3507cc4a58 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 13 Jul 2016 18:53:37 -0700 Subject: [PATCH 1104/1237] Adding the support for the subregion in the blur passes to apply it correctly to mini mirror --- libraries/render-utils/src/deferred_light.slv | 2 +- libraries/render-utils/src/point_light.slf | 2 +- libraries/render-utils/src/spot_light.slf | 2 +- libraries/render/src/render/BlurTask.cpp | 11 +++++++++++ libraries/render/src/render/BlurTask.h | 5 +++++ libraries/render/src/render/BlurTask.slh | 9 +++++++-- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/libraries/render-utils/src/deferred_light.slv b/libraries/render-utils/src/deferred_light.slv index 79e8130910..27845bf6a2 100644 --- a/libraries/render-utils/src/deferred_light.slv +++ b/libraries/render-utils/src/deferred_light.slv @@ -28,8 +28,8 @@ void main(void) { _texCoord0 = (pos.xy + 1) * 0.5; - _texCoord0 += texcoordFrameTransform.xy; _texCoord0 *= texcoordFrameTransform.zw; + _texCoord0 += texcoordFrameTransform.xy; gl_Position = pos; } diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 150460dc9f..80b2e62b8a 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -36,8 +36,8 @@ void main(void) { // Grab the fragment data from the uv vec2 texCoord = _texCoord0.st / _texCoord0.q; - texCoord += texcoordFrameTransform.xy; texCoord *= texcoordFrameTransform.zw; + texCoord += texcoordFrameTransform.xy; DeferredFragment frag = unpackDeferredFragment(deferredTransform, texCoord); diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 0502fc2753..4d4ad71413 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -36,8 +36,8 @@ void main(void) { // Grab the fragment data from the uv vec2 texCoord = _texCoord0.st / _texCoord0.q; - texCoord += texcoordFrameTransform.xy; texCoord *= texcoordFrameTransform.zw; + texCoord += texcoordFrameTransform.xy; DeferredFragment frag = unpackDeferredFragment(deferredTransform, texCoord); diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 19c45e6663..041d69370a 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -49,6 +49,13 @@ void BlurParams::setWidthHeight(int width, int height, bool isStereo) { } } +void BlurParams::setTexcoordTransform(const glm::vec4 texcoordTransformViewport) { + auto texcoordTransform = _parametersBuffer.get().texcoordTransform; + if (texcoordTransformViewport != texcoordTransform) { + _parametersBuffer.edit().texcoordTransform = texcoordTransform; + } +} + void BlurParams::setFilterRadiusScale(float scale) { auto filterInfo = _parametersBuffer.get().filterInfo; if (scale != filterInfo.x) { @@ -211,6 +218,8 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont auto blurHPipeline = getBlurHPipeline(); _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); + glm::ivec2 textureSize(blurringResources.sourceTexture->getDimensions()); + _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, args->_viewport)); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); @@ -320,6 +329,8 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const auto blurHPipeline = getBlurHPipeline(); _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); + glm::ivec2 textureSize(blurringResources.sourceTexture->getDimensions()); + _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, args->_viewport)); _parameters->setDepthPerspective(args->getViewFrustum().getProjection()[1][1]); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 6a0cd55a95..88228d1d86 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -22,6 +22,8 @@ public: void setWidthHeight(int width, int height, bool isStereo); + void setTexcoordTransform(const glm::vec4 texcoordTransformViewport); + void setFilterRadiusScale(float scale); void setDepthPerspective(float oneOverTan2FOV); @@ -33,6 +35,9 @@ public: // Resolution info (width, height, inverse of width, inverse of height) glm::vec4 resolutionInfo{ 0.0f, 0.0f, 0.0f, 0.0f }; + // Viewport to Texcoord info, if the region of the blur (viewport) is smaller than the full frame + glm::vec4 texcoordTransform{ 0.0f, 0.0f, 1.0f, 1.0f }; + // Filter info (radius scale glm::vec4 filterInfo{ 1.0f, 0.0f, 0.0f, 0.0f }; diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index dd1a229599..c66eaff00b 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -23,6 +23,7 @@ const float gaussianDistributionOffset[NUM_TAPS] = float[]( struct BlurParameters { vec4 resolutionInfo; + vec4 texcoordTransform; vec4 filterInfo; vec4 depthInfo; vec4 stereoInfo; @@ -36,6 +37,10 @@ vec2 getViewportInvWidthHeight() { return parameters.resolutionInfo.zw; } +vec2 evalTexcoordTransformed(vec2 texcoord) { + return (texcoord * parameters.texcoordTransform.zw + parameters.texcoordTransform.xy); +} + float getFilterScale() { return parameters.filterInfo.x; } @@ -59,7 +64,7 @@ float getDepthPerspective() { uniform sampler2D sourceMap; vec4 pixelShaderGaussian(vec2 texcoord, vec2 direction, vec2 pixelStep) { - + texcoord = evalTexcoordTransformed(texcoord); vec4 sampleCenter = texture(sourceMap, texcoord); vec2 finalStep = getFilterScale() * direction * pixelStep; @@ -86,7 +91,7 @@ uniform sampler2D sourceMap; uniform sampler2D depthMap; vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep) { - + texcoord = evalTexcoordTransformed(texcoord); float sampleDepth = texture(depthMap, texcoord).x; vec4 sampleCenter = texture(sourceMap, texcoord); From 848d5a89753df1972ccdc730650d236cb94faa61 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 13 Jul 2016 13:57:43 -0700 Subject: [PATCH 1105/1237] Reduce calls to Entity.getEntityProperties(). By using a single EntityProprtiesCache instance instead of one per controller. --- .../system/controllers/handControllerGrab.js | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 94a10798fe..2f82dd9e17 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -286,20 +286,32 @@ function restore2DMode() { // EntityPropertiesCache is a helper class that contains a cache of entity properties. // the hope is to prevent excess calls to Entity.getEntityProperties() +// +// usage: +// call EntityPropertiesCache.addEntities with all the entities that you are interested in. +// This will fetch their properties. Then call EntityPropertiesCache.getProps to receive an object +// containing a cache of all the properties previously fetched. function EntityPropertiesCache() { this.cache = {}; } EntityPropertiesCache.prototype.clear = function () { this.cache = {}; }; -EntityPropertiesCache.prototype.findEntities = function (position, radius) { - var entities = Entities.findEntities(position, radius); +EntityPropertiesCache.prototype.addEntity = function (entityID) { + var cacheEntry = this.cache[entityID]; + if (cacheEntry && cacheEntry.refCount) { + cacheEntry.refCount += 1; + } else { + this._updateCacheEntry(entityID); + } +}; +EntityPropertiesCache.prototype.addEntities = function (entities) { var _this = this; - entities.forEach(function (x) { - _this.updateEntity(x); + entities.forEach(function (entityID) { + _this.addEntity(entityID); }); }; -EntityPropertiesCache.prototype.updateEntity = function (entityID) { +EntityPropertiesCache.prototype._updateCacheEntry = function (entityID) { var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); // convert props.userData from a string to an object. @@ -312,11 +324,21 @@ EntityPropertiesCache.prototype.updateEntity = function (entityID) { } } props.userData = userData; + props.refCount = 1; this.cache[entityID] = props; }; -EntityPropertiesCache.prototype.getEntities = function () { - return Object.keys(this.cache); +EntityPropertiesCache.prototype.update = function () { + // delete any cacheEntries with zero refCounts. + var entities = Object.keys(this.cache); + for (var i = 0; i < entities.length; i++) { + var props = this.cache[entities[i]]; + if (props.refCount === 0) { + delete this.cache[entities[i]]; + } else { + props.refCount = 0; + } + } }; EntityPropertiesCache.prototype.getProps = function (entityID) { var obj = this.cache[entityID]; @@ -355,6 +377,9 @@ EntityPropertiesCache.prototype.getEquipHotspotsProps = function (entityID) { } }; +// global cache +var entityPropertiesCache = new EntityPropertiesCache(); + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -404,8 +429,6 @@ function MyController(hand) { this.lastPickTime = 0; this.lastUnequipCheckTime = 0; - this.entityPropertyCache = new EntityPropertiesCache(); - this.equipOverlayInfoSetMap = {}; var _this = this; @@ -900,10 +923,8 @@ function MyController(hand) { } } - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(this.getHandPosition(), EQUIP_HOTSPOT_RENDER_RADIUS); - var candidateEntities = this.entityPropertyCache.getEntities(); - + var candidateEntities = Entities.findEntities(this.getHandPosition(), EQUIP_HOTSPOT_RENDER_RADIUS); + entityPropertiesCache.addEntities(candidateEntities); var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); if (!this.waitForTriggerRelease) { this.updateEquipHaptics(potentialEquipHotspot); @@ -971,7 +992,7 @@ function MyController(hand) { diameter = diameter * EQUIP_RADIUS_EMBIGGEN_FACTOR; } - var props = _this.entityPropertyCache.getProps(overlayInfoSet.entityID); + var props = entityPropertiesCache.getProps(overlayInfoSet.entityID); var entityXform = new Xform(props.rotation, props.position); var position = entityXform.xformPoint(overlayInfoSet.localPosition); @@ -1072,7 +1093,7 @@ function MyController(hand) { }; this.entityWantsTrigger = function (entityID) { - var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); return grabbableProps && grabbableProps.wantsTrigger; }; @@ -1088,9 +1109,9 @@ function MyController(hand) { // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. this.collectEquipHotspots = function (entityID) { var result = []; - var props = this.entityPropertyCache.getProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var entityXform = new Xform(props.rotation, props.position); - var equipHotspotsProps = this.entityPropertyCache.getEquipHotspotsProps(entityID); + var equipHotspotsProps = entityPropertiesCache.getEquipHotspotsProps(entityID); if (equipHotspotsProps && equipHotspotsProps.length > 0) { var i, length = equipHotspotsProps.length; for (i = 0; i < length; i++) { @@ -1107,7 +1128,7 @@ function MyController(hand) { } } } else { - var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + var wearableProps = entityPropertiesCache.getWearableProps(entityID); if (wearableProps && wearableProps.joints) { result.push({ key: entityID.toString() + "0", @@ -1123,8 +1144,8 @@ function MyController(hand) { }; this.hotspotIsEquippable = function (hotspot) { - var props = this.entityPropertyCache.getProps(hotspot.entityID); - var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); + var props = entityPropertiesCache.getProps(hotspot.entityID); + var grabProps = entityPropertiesCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; @@ -1142,9 +1163,9 @@ function MyController(hand) { }; this.entityIsGrabbable = function (entityID) { - var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); - var grabProps = this.entityPropertyCache.getGrabProps(entityID); - var props = this.entityPropertyCache.getProps(entityID); + var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); + var grabProps = entityPropertiesCache.getGrabProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var physical = propsArePhysical(props); var grabbable = false; var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1198,7 +1219,7 @@ function MyController(hand) { return false; } - var props = this.entityPropertyCache.getProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var distance = Vec3.distance(props.position, handPosition); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1236,7 +1257,7 @@ function MyController(hand) { return false; } - var props = this.entityPropertyCache.getProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var distance = Vec3.distance(props.position, handPosition); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1294,16 +1315,15 @@ function MyController(hand) { var handPosition = this.getHandPosition(); - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); - var candidateEntities = this.entityPropertyCache.getEntities(); + var candidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); + entityPropertiesCache.addEntities(candidateEntities); var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); if (potentialEquipHotspot) { if (this.triggerSmoothedGrab()) { this.grabbedHotspot = potentialEquipHotspot; this.grabbedEntity = potentialEquipHotspot.entityID; - this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(this.grabbedEntity).name + "'"); + this.setState(STATE_HOLD, "eqipping '" + entityPropertiesCache.getProps(this.grabbedEntity).name + "'"); return; } } @@ -1315,7 +1335,7 @@ function MyController(hand) { var rayPickInfo = this.calcRayPickInfo(this.hand); if (rayPickInfo.entityID) { this.intersectionDistance = rayPickInfo.distance; - this.entityPropertyCache.updateEntity(rayPickInfo.entityID); + entityPropertiesCache.addEntity(rayPickInfo.entityID); if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { grabbableEntities.push(rayPickInfo.entityID); } @@ -1329,12 +1349,12 @@ function MyController(hand) { if (grabbableEntities.length > 0) { // sort by distance grabbableEntities.sort(function (a, b) { - var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); - var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + var aDistance = Vec3.distance(entityPropertiesCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(entityPropertiesCache.getProps(b).position, handPosition); return aDistance - bDistance; }); entity = grabbableEntities[0]; - name = this.entityPropertyCache.getProps(entity).name; + name = entityPropertiesCache.getProps(entity).name; this.grabbedEntity = entity; if (this.entityWantsTrigger(entity)) { if (this.triggerSmoothedGrab()) { @@ -1345,8 +1365,8 @@ function MyController(hand) { } } else { if (this.triggerSmoothedGrab()) { - var props = this.entityPropertyCache.getProps(entity); - var grabProps = this.entityPropertyCache.getGrabProps(entity); + var props = entityPropertiesCache.getProps(entity); + var grabProps = entityPropertiesCache.getGrabProps(entity); var refCount = grabProps.refCount ? grabProps.refCount : 0; if (refCount >= 1) { // if another person is holding the object, remember to restore the @@ -1366,7 +1386,7 @@ function MyController(hand) { if (rayPickInfo.entityID) { entity = rayPickInfo.entityID; - name = this.entityPropertyCache.getProps(entity).name; + name = entityPropertiesCache.getProps(entity).name; if (this.entityWantsTrigger(entity)) { if (this.triggerSmoothedGrab()) { this.grabbedEntity = entity; @@ -1989,7 +2009,7 @@ function MyController(hand) { // and rotation of the held thing to help content creators set the userData. var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); if (grabData.refCount > 1) { - grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); if (grabbedProperties && grabbedProperties.localPosition && grabbedProperties.localRotation) { print((this.hand === RIGHT_HAND ? '"LeftHand"' : '"RightHand"') + ":" + '[{"x":' + grabbedProperties.localPosition.x + ', "y":' + grabbedProperties.localPosition.y + @@ -2248,6 +2268,7 @@ function update(deltaTime) { if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { rightController.update(deltaTime); } + entityPropertiesCache.update(); } Messages.subscribe('Hifi-Hand-Disabler'); From 0ce970408c7059a93880ae58a9a4fe0000672c45 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 13 Jul 2016 14:44:15 -0700 Subject: [PATCH 1106/1237] Fix for double rendering of equip hotspots --- .../system/controllers/handControllerGrab.js | 222 +++++++++--------- 1 file changed, 117 insertions(+), 105 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2f82dd9e17..47fb689c33 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -380,6 +380,106 @@ EntityPropertiesCache.prototype.getEquipHotspotsProps = function (entityID) { // global cache var entityPropertiesCache = new EntityPropertiesCache(); +// Each overlayInfoSet describes a single equip hotspot. +// It is an object with the following keys: +// timestamp - last time this object was updated, used to delete stale hotspot overlays. +// entityID - entity assosicated with this hotspot +// localPosition - position relative to the entity +// hotspot - hotspot object +// overlays - array of overlay objects created by Overlay.addOverlay() +function EquipHotspotBuddy() { + // holds map from {string} hotspot.key to {object} overlayInfoSet. + this.map = {}; + + // array of all hotspots that are highlighed. + this.highlightedHotspots = []; +} +EquipHotspotBuddy.prototype.clear = function () { + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + this.deleteOverlayInfoSet(overlayInfoSet); + } + this.map = {}; + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.highlightHotspot = function (hotspot) { + this.highlightedHotspots.push(hotspot); +}; +EquipHotspotBuddy.prototype.updateHotspot = function (hotspot, timestamp) { + var overlayInfoSet = this.map[hotspot.key]; + if (!overlayInfoSet) { + // create a new overlayInfoSet + overlayInfoSet = { + timestamp: timestamp, + entityID: hotspot.entityID, + localPosition: hotspot.localPosition, + hotspot: hotspot, + overlays: [] + }; + + var diameter = hotspot.radius * 2; + + // TODO: check for custom hotspot model urls. + + // default sphere + overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { + position: hotspot.worldPosition, + rotation: {x: 0, y: 0, z: 0, w: 1}, + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + })); + + this.map[hotspot.key] = overlayInfoSet; + } else { + overlayInfoSet.timestamp = timestamp; + } +}; +EquipHotspotBuddy.prototype.updateHotspots = function (hotspots, timestamp) { + var _this = this; + hotspots.forEach(function (hotspot) { + _this.updateHotspot(hotspot, timestamp); + }); + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.update = function (deltaTime, timestamp) { + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + + // TODO: figure out how to do animation.... fade in, fade out, highlight, un-highlight + + if (overlayInfoSet.timestamp != timestamp) { + // this is an old stale overlay, delete it! + overlayInfoSet.overlays.forEach(function (overlay) { + Overlays.deleteOverlay(overlay); + }); + delete this.map[keys[i]]; + } else { + // update overlay position, rotation to follow the object it's attached to. + + var props = entityPropertiesCache.getProps(overlayInfoSet.entityID); + var entityXform = new Xform(props.rotation, props.position); + var position = entityXform.xformPoint(overlayInfoSet.localPosition); + + overlayInfoSet.overlays.forEach(function (overlay) { + Overlays.editOverlay(overlay, { + position: position, + rotation: props.rotation + }); + }); + } + } +}; + +// global EquipHotspotBuddy instance +var equipHotspotBuddy = new EquipHotspotBuddy(); + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -440,7 +540,7 @@ function MyController(hand) { return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; - this.update = function (deltaTime) { + this.update = function (deltaTime, timestamp) { this.updateSmoothedTrigger(); @@ -453,7 +553,7 @@ function MyController(hand) { var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; var updateMethod = this[updateMethodName]; if (updateMethod) { - updateMethod.call(this, deltaTime); + updateMethod.call(this, deltaTime, timestamp); } else { print("WARNING: could not find updateMethod for state " + stateToName(this.state)); } @@ -909,7 +1009,7 @@ function MyController(hand) { return _this.rawThumbValue < THUMB_ON_VALUE; }; - this.off = function () { + this.off = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.waitForTriggerRelease = false; } @@ -931,7 +1031,8 @@ function MyController(hand) { } var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); - this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); }; this.clearEquipHaptics = function () { @@ -946,95 +1047,6 @@ function MyController(hand) { this.prevPotentialEquipHotspot = potentialEquipHotspot; }; - this.clearEquipHotspotRendering = function () { - var keys = Object.keys(this.equipOverlayInfoSetMap); - for (var i = 0; i < keys.length; i++) { - var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]]; - this.deleteOverlayInfoSet(overlayInfoSet); - } - this.equipOverlayInfoSetMap = {}; - }; - - this.createOverlayInfoSet = function (hotspot, timestamp) { - var overlayInfoSet = { - timestamp: timestamp, - entityID: hotspot.entityID, - localPosition: hotspot.localPosition, - hotspot: hotspot, - overlays: [] - }; - - var diameter = hotspot.radius * 2; - - overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { - position: hotspot.worldPosition, - rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, - color: EQUIP_SPHERE_COLOR, - alpha: EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - })); - - return overlayInfoSet; - }; - - this.updateOverlayInfoSet = function (overlayInfoSet, timestamp, potentialEquipHotspot) { - overlayInfoSet.timestamp = timestamp; - - var diameter = overlayInfoSet.hotspot.radius * 2; - - // embiggen the overlays if it maches the potentialEquipHotspot - if (potentialEquipHotspot && overlayInfoSet.entityID == potentialEquipHotspot.entityID && - Vec3.equal(overlayInfoSet.localPosition, potentialEquipHotspot.localPosition)) { - diameter = diameter * EQUIP_RADIUS_EMBIGGEN_FACTOR; - } - - var props = entityPropertiesCache.getProps(overlayInfoSet.entityID); - var entityXform = new Xform(props.rotation, props.position); - var position = entityXform.xformPoint(overlayInfoSet.localPosition); - - overlayInfoSet.overlays.forEach(function (overlay) { - Overlays.editOverlay(overlay, { - position: position, - rotation: props.rotation, - dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR - }); - }); - }; - - this.deleteOverlayInfoSet = function (overlayInfoSet) { - overlayInfoSet.overlays.forEach(function (overlay) { - Overlays.deleteOverlay(overlay); - }); - }; - - this.updateEquipHotspotRendering = function (hotspots, potentialEquipHotspot) { - var now = Date.now(); - var _this = this; - - hotspots.forEach(function (hotspot) { - var overlayInfoSet = _this.equipOverlayInfoSetMap[hotspot.key]; - if (overlayInfoSet) { - _this.updateOverlayInfoSet(overlayInfoSet, now, potentialEquipHotspot); - } else { - _this.equipOverlayInfoSetMap[hotspot.key] = _this.createOverlayInfoSet(hotspot, now); - } - }); - - // delete sets with old timestamps. - var keys = Object.keys(this.equipOverlayInfoSetMap); - for (var i = 0; i < keys.length; i++) { - var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]]; - if (overlayInfoSet.timestamp !== now) { - this.deleteOverlayInfoSet(overlayInfoSet); - delete this.equipOverlayInfoSetMap[keys[i]]; - } - } - }; - // Performs ray pick test from the hand controller into the world // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND // @returns {object} returns object with two keys entityID and distance @@ -1298,7 +1310,7 @@ function MyController(hand) { } }; - this.search = function () { + this.search = function (deltaTime, timestamp) { var _this = this; var name; @@ -1409,7 +1421,8 @@ function MyController(hand) { this.updateEquipHaptics(potentialEquipHotspot); var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); - this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { @@ -1438,7 +1451,6 @@ function MyController(hand) { this.distanceHoldingEnter = function () { - this.clearEquipHotspotRendering(); this.clearEquipHaptics(); // controller pose is in avatar frame @@ -1498,7 +1510,7 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.distanceHolding = function () { + this.distanceHolding = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); @@ -1717,7 +1729,6 @@ function MyController(hand) { this.dropGestureReset(); this.clearEquipHaptics(); - this.clearEquipHotspotRendering(); Controller.triggerShortHapticPulse(1.0, this.hand); @@ -1819,7 +1830,7 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.nearGrabbing = function (deltaTime) { + this.nearGrabbing = function (deltaTime, timestamp) { var dropDetected = this.dropGestureProcess(deltaTime); @@ -1938,7 +1949,6 @@ function MyController(hand) { this.nearTriggerEnter = function () { - this.clearEquipHotspotRendering(); this.clearEquipHaptics(); Controller.triggerShortHapticPulse(1.0, this.hand); @@ -1946,13 +1956,12 @@ function MyController(hand) { }; this.farTriggerEnter = function () { - this.clearEquipHotspotRendering(); this.clearEquipHaptics(); this.callEntityMethodOnGrabbed("startFarTrigger"); }; - this.nearTrigger = function () { + this.nearTrigger = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -1961,7 +1970,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueNearTrigger"); }; - this.farTrigger = function () { + this.farTrigger = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -2262,12 +2271,15 @@ Controller.enableMapping(MAPPING_NAME); var handToDisable = 'none'; function update(deltaTime) { + var timestamp = Date.now(); + if (handToDisable !== LEFT_HAND && handToDisable !== 'both') { - leftController.update(deltaTime); + leftController.update(deltaTime, timestamp); } if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { - rightController.update(deltaTime); + rightController.update(deltaTime, timestamp); } + equipHotspotBuddy.update(deltaTime, timestamp); entityPropertiesCache.update(); } From f3ef2801b4d5275dd27ff3070120a91e8781585d Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 13 Jul 2016 15:07:09 -0700 Subject: [PATCH 1107/1237] Support for model overrides for equip hotspots rendering Bug fix for modelScale on hotspots --- .../system/controllers/handControllerGrab.js | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 47fb689c33..01a4e2588a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -420,20 +420,30 @@ EquipHotspotBuddy.prototype.updateHotspot = function (hotspot, timestamp) { var diameter = hotspot.radius * 2; - // TODO: check for custom hotspot model urls. - - // default sphere - overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { - position: hotspot.worldPosition, - rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, - color: EQUIP_SPHERE_COLOR, - alpha: EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - })); + if (hotspot.modelURL) { + // override default sphere with a user specified model + overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + url: hotspot.modelURL, + position: hotspot.worldPosition, + rotation: {x: 0, y: 0, z: 0, w: 1}, + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, + scale: hotspot.modelScale, + ignoreRayIntersection: true + })); + } else { + // default sphere overlay + overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { + position: hotspot.worldPosition, + rotation: {x: 0, y: 0, z: 0, w: 1}, + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + })); + } this.map[hotspot.key] = overlayInfoSet; } else { @@ -1119,6 +1129,8 @@ function MyController(hand) { // * radius {number} radius of equip hotspot // * joints {Object} keys are joint names values are arrays of two elements: // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. + // * modelURL {string} url for model to use instead of default sphere. + // * modelScale {Vec3} scale factor for model this.collectEquipHotspots = function (entityID) { var result = []; var props = entityPropertiesCache.getProps(entityID); @@ -1135,7 +1147,9 @@ function MyController(hand) { localPosition: hotspot.position, worldPosition: entityXform.xformPoint(hotspot.position), radius: hotspot.radius, - joints: hotspot.joints + joints: hotspot.joints, + modelURL: hotspot.modelURL, + modelScale: hotspot.modelScale }); } } @@ -1148,7 +1162,9 @@ function MyController(hand) { localPosition: {x: 0, y: 0, z: 0}, worldPosition: entityXform.pos, radius: EQUIP_RADIUS, - joints: wearableProps.joints + joints: wearableProps.joints, + modelURL: null, + modelScale: null }); } } From 66830a05387ad12b32c9687461e6f626e93509c2 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 13 Jul 2016 19:44:51 -0700 Subject: [PATCH 1108/1237] First pass at equip sphere animation --- .../system/controllers/handControllerGrab.js | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 01a4e2588a..e446162278 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -78,8 +78,6 @@ var LINE_ENTITY_DIMENSIONS = { var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray -var EQUIP_RADIUS_EMBIGGEN_FACTOR = 1.1; - // // near grabbing // @@ -404,7 +402,7 @@ EquipHotspotBuddy.prototype.clear = function () { this.highlightedHotspots = []; }; EquipHotspotBuddy.prototype.highlightHotspot = function (hotspot) { - this.highlightedHotspots.push(hotspot); + this.highlightedHotspots.push(hotspot.key); }; EquipHotspotBuddy.prototype.updateHotspot = function (hotspot, timestamp) { var overlayInfoSet = this.map[hotspot.key]; @@ -415,6 +413,8 @@ EquipHotspotBuddy.prototype.updateHotspot = function (hotspot, timestamp) { entityID: hotspot.entityID, localPosition: hotspot.localPosition, hotspot: hotspot, + currentSize: 0, + targetSize: 1, overlays: [] }; @@ -458,14 +458,38 @@ EquipHotspotBuddy.prototype.updateHotspots = function (hotspots, timestamp) { this.highlightedHotspots = []; }; EquipHotspotBuddy.prototype.update = function (deltaTime, timestamp) { + + var HIGHLIGHT_SIZE = 1.1; + var NORMAL_SIZE = 1.0; + var keys = Object.keys(this.map); for (var i = 0; i < keys.length; i++) { var overlayInfoSet = this.map[keys[i]]; - // TODO: figure out how to do animation.... fade in, fade out, highlight, un-highlight + // this overlayInfo is highlighted. + if (this.highlightedHotspots.indexOf(keys[i]) != -1) { + overlayInfoSet.targetSize = HIGHLIGHT_SIZE; + } else { + overlayInfoSet.targetSize = NORMAL_SIZE; + } + // start to fade out this hotspot. if (overlayInfoSet.timestamp != timestamp) { - // this is an old stale overlay, delete it! + // because this item timestamp has expired, it might not be in the cache anymore.... + entityPropertiesCache.addEntity(overlayInfoSet.entityID); + overlayInfoSet.targetSize = 0; + } + + // animate the size. + var SIZE_TIMESCALE = 0.1; + var tau = deltaTime / SIZE_TIMESCALE; + if (tau > 1.0) { + tau = 1.0; + } + overlayInfoSet.currentSize += (overlayInfoSet.targetSize - overlayInfoSet.currentSize) * tau; + + if (overlayInfoSet.timestamp != timestamp && overlayInfoSet.currentSize <= 0.05) { + // this is an old overlay, that has finished fading out, delete it! overlayInfoSet.overlays.forEach(function (overlay) { Overlays.deleteOverlay(overlay); }); @@ -476,11 +500,13 @@ EquipHotspotBuddy.prototype.update = function (deltaTime, timestamp) { var props = entityPropertiesCache.getProps(overlayInfoSet.entityID); var entityXform = new Xform(props.rotation, props.position); var position = entityXform.xformPoint(overlayInfoSet.localPosition); + var alpha = overlayInfoSet.currentSize / HIGHLIGHT_SIZE; // AJT: TEMPORARY overlayInfoSet.overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, { position: position, - rotation: props.rotation + rotation: props.rotation, + alpha: alpha // AJT: TEMPORARY }); }); } @@ -1042,7 +1068,9 @@ function MyController(hand) { var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); - equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } }; this.clearEquipHaptics = function () { @@ -1438,7 +1466,9 @@ function MyController(hand) { var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); - equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { From c2fd055f0f12992cdd5d64cddf97a610d21badcb Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 13 Jul 2016 17:23:00 -0700 Subject: [PATCH 1109/1237] more hacking --- cmake/macros/GenerateInstallers.cmake | 26 ------------------- cmake/macros/InstallBesideConsole.cmake | 1 + cmake/macros/SetPackagingParameters.cmake | 2 ++ .../macros/SetupHifiClientServerPlugin.cmake | 1 - plugins/hifiCodec/CMakeLists.txt | 1 + plugins/pcmCodec/CMakeLists.txt | 2 ++ server-console/CMakeLists.txt | 26 ------------------- 7 files changed, 6 insertions(+), 53 deletions(-) diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index 2d2ede69f0..0def701739 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -78,32 +78,6 @@ macro(GENERATE_INSTALLERS) install(CODE "execute_process(COMMAND SetFile -a V \${CMAKE_INSTALL_PREFIX}/${ESCAPED_DMG_SUBFOLDER_NAME}/Icon\\r)") endif () - # this is a bit of a hack, but I couldn't find any other way to get plugins - # into the correct place under the console path. On windows this is handled - # exclusively in the plugins directories with the SetupHiFiClientServerPlugin - # macro - if (APPLE) - set(CONSOLE_PLUGINS_DIR "${CONSOLE_INSTALL_APP_PATH}/Contents/MacOS/Components.app/Contents/PlugIns") - set(SERVER_PLUGINS_DIR "${CMAKE_BINARY_DIR}/assignment-client/${CMAKE_BUILD_TYPE}/plugins") - - message("TARGET_NAME: ${TARGET_NAME}") - message("CONFIGURATION: ${CONFIGURATION}") - message(": $") - message("CONSOLE_PLUGINS_DIR: ${CONSOLE_PLUGINS_DIR}") - message("SERVER_PLUGINS_DIR: ${SERVER_PLUGINS_DIR}") - - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E make_directory - ${CONSOLE_PLUGINS_DIR} - ) - - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory - ${SERVER_PLUGINS_DIR} - ${CONSOLE_PLUGINS_DIR} - ) - endif () - # configure a cpack properties file for custom variables in template set(CPACK_CONFIGURED_PROP_FILE "${CMAKE_CURRENT_BINARY_DIR}/CPackCustomProperties.cmake") configure_file("${HF_CMAKE_DIR}/templates/CPackProperties.cmake.in" ${CPACK_CONFIGURED_PROP_FILE}) diff --git a/cmake/macros/InstallBesideConsole.cmake b/cmake/macros/InstallBesideConsole.cmake index 318a7a3ffe..0eb6025f38 100644 --- a/cmake/macros/InstallBesideConsole.cmake +++ b/cmake/macros/InstallBesideConsole.cmake @@ -16,6 +16,7 @@ macro(install_beside_console) install( TARGETS ${TARGET_NAME} RUNTIME DESTINATION ${COMPONENT_INSTALL_DIR} + LIBRARY DESTINATION ${CONSOLE_PLUGIN_INSTALL_DIR} COMPONENT ${SERVER_COMPONENT} ) else () diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index d8532aa081..76f6812921 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -69,6 +69,8 @@ macro(SET_PACKAGING_PARAMETERS) set(CONSOLE_APP_CONTENTS "${CONSOLE_INSTALL_APP_PATH}/Contents") set(COMPONENT_APP_PATH "${CONSOLE_APP_CONTENTS}/MacOS/Components.app") set(COMPONENT_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/MacOS") + set(CONSOLE_PLUGIN_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/PlugIns") + set(INTERFACE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app") set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns") diff --git a/cmake/macros/SetupHifiClientServerPlugin.cmake b/cmake/macros/SetupHifiClientServerPlugin.cmake index a065ee99b6..8ba38a09c3 100644 --- a/cmake/macros/SetupHifiClientServerPlugin.cmake +++ b/cmake/macros/SetupHifiClientServerPlugin.cmake @@ -13,7 +13,6 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) if (APPLE) set(CLIENT_PLUGIN_PATH "${INTERFACE_BUNDLE_NAME}.app/Contents/PlugIns") - #set(SERVER_PLUGIN_PATH "Components.app/Contents/PlugIns") set(SERVER_PLUGIN_PATH "plugins") else() set(CLIENT_PLUGIN_PATH "plugins") diff --git a/plugins/hifiCodec/CMakeLists.txt b/plugins/hifiCodec/CMakeLists.txt index 3939529c3e..0af7e42ea1 100644 --- a/plugins/hifiCodec/CMakeLists.txt +++ b/plugins/hifiCodec/CMakeLists.txt @@ -15,5 +15,6 @@ if (WIN32 OR APPLE) add_dependency_external_projects(HiFiAudioCodec) target_include_directories(${TARGET_NAME} PRIVATE ${HIFIAUDIOCODEC_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${HIFIAUDIOCODEC_LIBRARIES}) + install_beside_console() endif() diff --git a/plugins/pcmCodec/CMakeLists.txt b/plugins/pcmCodec/CMakeLists.txt index 5dca1f0e14..900a642a88 100644 --- a/plugins/pcmCodec/CMakeLists.txt +++ b/plugins/pcmCodec/CMakeLists.txt @@ -9,3 +9,5 @@ set(TARGET_NAME pcmCodec) setup_hifi_client_server_plugin() link_hifi_libraries(shared plugins) +install_beside_console() + diff --git a/server-console/CMakeLists.txt b/server-console/CMakeLists.txt index 7b9c17ace4..1c6e40c582 100644 --- a/server-console/CMakeLists.txt +++ b/server-console/CMakeLists.txt @@ -72,29 +72,3 @@ else () set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) set_target_properties(${TARGET_NAME}-npm-install PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) endif () - -# this is a bit of a hack, but I couldn't find any other way to get plugins -# into the correct place under the console path. On windows this is handled -# exclusively in the plugins directories with the SetupHiFiClientServerPlugin -# macro -if (APPLE) - set(CONSOLE_PLUGINS_DIR "${CONSOLE_INSTALL_APP_PATH}/Contents/MacOS/Components.app/Contents/PlugIns") - set(SERVER_PLUGINS_DIR "${CMAKE_BINARY_DIR}/assignment-client/${CMAKE_BUILD_TYPE}/plugins") - - message("TARGET_NAME: ${TARGET_NAME}") - message("CONFIGURATION: ${CONFIGURATION}") - message(": $") - message("CONSOLE_PLUGINS_DIR: ${CONSOLE_PLUGINS_DIR}") - message("SERVER_PLUGINS_DIR: ${SERVER_PLUGINS_DIR}") - - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E make_directory - ${CONSOLE_PLUGINS_DIR} - ) - - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory - ${SERVER_PLUGINS_DIR} - ${CONSOLE_PLUGINS_DIR} - ) -endif () From 23ea85541cbcae3f94b2b12e85c7806d7a7879ab Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Jul 2016 10:24:08 -0700 Subject: [PATCH 1110/1237] Animate equip hotspots scale instead of alpha --- scripts/system/controllers/handControllerGrab.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index e446162278..c3177c5338 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -385,6 +385,9 @@ var entityPropertiesCache = new EntityPropertiesCache(); // localPosition - position relative to the entity // hotspot - hotspot object // overlays - array of overlay objects created by Overlay.addOverlay() +// currentSize - current animated scale value +// targetSize - the target of our scale animations +// type - "sphere" or "model". function EquipHotspotBuddy() { // holds map from {string} hotspot.key to {object} overlayInfoSet. this.map = {}; @@ -430,6 +433,7 @@ EquipHotspotBuddy.prototype.updateHotspot = function (hotspot, timestamp) { scale: hotspot.modelScale, ignoreRayIntersection: true })); + overlayInfoSet.type = "model"; } else { // default sphere overlay overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { @@ -443,6 +447,7 @@ EquipHotspotBuddy.prototype.updateHotspot = function (hotspot, timestamp) { ignoreRayIntersection: true, drawInFront: false })); + overlayInfoSet.type = "sphere"; } this.map[hotspot.key] = overlayInfoSet; @@ -500,13 +505,19 @@ EquipHotspotBuddy.prototype.update = function (deltaTime, timestamp) { var props = entityPropertiesCache.getProps(overlayInfoSet.entityID); var entityXform = new Xform(props.rotation, props.position); var position = entityXform.xformPoint(overlayInfoSet.localPosition); - var alpha = overlayInfoSet.currentSize / HIGHLIGHT_SIZE; // AJT: TEMPORARY + + var dimensions; + if (overlayInfoSet.type == "sphere") { + dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR; + } else { + dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize; + } overlayInfoSet.overlays.forEach(function (overlay) { Overlays.editOverlay(overlay, { position: position, rotation: props.rotation, - alpha: alpha // AJT: TEMPORARY + dimensions: dimensions }); }); } From 091e798267096a612289d8b10f9e5dd5b214ff9a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 13 Jul 2016 16:06:37 -0700 Subject: [PATCH 1111/1237] Make hand pointer lasers 'click' on fully pressing and clicking the vive controller --- interface/resources/controllers/vive.json | 4 + .../src/controllers/StandardController.cpp | 2 + .../src/controllers/StandardControls.h | 2 + .../controllers/impl/RouteBuilderProxy.cpp | 15 +++- .../oculus/src/OculusControllerManager.cpp | 8 ++ plugins/openvr/src/ViveControllerManager.cpp | 18 +++- .../system/controllers/handControllerGrab.js | 15 +++- .../controllers/handControllerPointer.js | 18 ++-- scripts/system/libraries/Trigger.js | 87 +++++++++++++++++++ 9 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 scripts/system/libraries/Trigger.js diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 6be672900a..79114b8141 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -9,6 +9,8 @@ { "type": "deadZone", "min": 0.05 } ] }, + { "from": "Vive.LTClick", "to": "Standard.LTClick" }, + { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, @@ -21,6 +23,8 @@ { "type": "deadZone", "min": 0.05 } ] }, + { "from": "Vive.RTClick", "to": "Standard.RTClick" }, + { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index a9d317d407..02ae5706b7 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -65,6 +65,8 @@ Input::NamedVector StandardController::getAvailableInputs() const { // Triggers makePair(LT, "LT"), makePair(RT, "RT"), + makePair(LT_CLICK, "LTClick"), + makePair(RT_CLICK, "RTClick"), // Finger abstractions makePair(LEFT_PRIMARY_THUMB, "LeftPrimaryThumb"), diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index 501f97f04b..c21d8a2f6e 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -46,6 +46,7 @@ namespace controller { LS_CENTER, LS_X, LS_Y, + LT_CLICK, RIGHT_PRIMARY_THUMB, RIGHT_SECONDARY_THUMB, @@ -56,6 +57,7 @@ namespace controller { RS_CENTER, RS_X, RS_Y, + RT_CLICK, LEFT_PRIMARY_INDEX, LEFT_SECONDARY_INDEX, diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp index 5ae52893e0..7dedfda3cb 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp @@ -26,6 +26,7 @@ #include "filters/InvertFilter.h" #include "filters/PulseFilter.h" #include "filters/ScaleFilter.h" +#include "conditionals/AndConditional.h" using namespace controller; @@ -58,12 +59,22 @@ QObject* RouteBuilderProxy::peek(bool enable) { } QObject* RouteBuilderProxy::when(const QScriptValue& expression) { - _route->conditional = _parent.conditionalFor(expression); + auto newConditional = _parent.conditionalFor(expression); + if (_route->conditional) { + _route->conditional = ConditionalPointer(new AndConditional(_route->conditional, newConditional)); + } else { + _route->conditional = newConditional; + } return this; } QObject* RouteBuilderProxy::whenQml(const QJSValue& expression) { - _route->conditional = _parent.conditionalFor(expression); + auto newConditional = _parent.conditionalFor(expression); + if (_route->conditional) { + _route->conditional = ConditionalPointer(new AndConditional(_route->conditional, newConditional)); + } else { + _route->conditional = newConditional; + } return this; } diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index b3b1b20b2b..225ccf42b2 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -199,11 +199,19 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control _axisStateMap[LX] = inputState.Thumbstick[ovrHand_Left].x; _axisStateMap[LY] = inputState.Thumbstick[ovrHand_Left].y; _axisStateMap[LT] = inputState.IndexTrigger[ovrHand_Left]; + // FIXME add hysteresis? Move to JSON config? + if (inputState.IndexTrigger[ovrHand_Left] > 0.9) { + _buttonPressedMap.insert(LT_CLICK); + } _axisStateMap[LEFT_GRIP] = inputState.HandTrigger[ovrHand_Left]; _axisStateMap[RX] = inputState.Thumbstick[ovrHand_Right].x; _axisStateMap[RY] = inputState.Thumbstick[ovrHand_Right].y; _axisStateMap[RT] = inputState.IndexTrigger[ovrHand_Right]; + // FIXME add hysteresis? Move to JSON config? + if (inputState.IndexTrigger[ovrHand_Right] > 0.9) { + _buttonPressedMap.insert(RT_CLICK); + } _axisStateMap[RIGHT_GRIP] = inputState.HandTrigger[ovrHand_Right]; // Buttons diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 4a515978c3..4fccc624be 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -11,6 +11,9 @@ #include "ViveControllerManager.h" +#include +#include + #include #include #include @@ -22,6 +25,7 @@ #include #include + #include #include @@ -284,7 +288,9 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u vr::VRControllerState_t controllerState = vr::VRControllerState_t(); if (_system->GetControllerState(deviceIndex, &controllerState)) { - + //std::stringstream stream; + //stream << std::hex << controllerState.ulButtonPressed << " " << std::hex << controllerState.ulButtonTouched; + //qDebug() << deviceIndex << " " << stream.str().c_str() << controllerState.rAxis[1].x; // process each button for (uint32_t i = 0; i < vr::k_EButton_Max; ++i) { auto mask = vr::ButtonMaskFromId((vr::EVRButtonId)i); @@ -342,6 +348,11 @@ void ViveControllerManager::InputDevice::handleAxisEvent(float deltaTime, uint32 _axisStateMap[isLeftHand ? LY : RY] = stick.y; } else if (axis == vr::k_EButton_SteamVR_Trigger) { _axisStateMap[isLeftHand ? LT : RT] = x; + // The click feeling on the Vive controller trigger represents a value of *precisely* 1.0, + // so we can expose that as an additional button + if (x >= 1.0f) { + _buttonPressedMap.insert(isLeftHand ? LT_CLICK : RT_CLICK); + } } } @@ -463,10 +474,15 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(RS_X, "RSX"), makePair(RS_Y, "RSY"), + // triggers makePair(LT, "LT"), makePair(RT, "RT"), + // Trigger clicks + makePair(LT_CLICK, "LTClick"), + makePair(RT_CLICK, "RTClick"), + // low profile side grip button. makePair(LEFT_GRIP, "LeftGrip"), makePair(RIGHT_GRIP, "RightGrip"), diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 94a10798fe..0138e1bc3b 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -29,9 +29,8 @@ var WANT_DEBUG_SEARCH_NAME = null; var SPARK_MODEL_SCALE_FACTOR = 0.75; var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing -var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab -var TRIGGER_OFF_VALUE = 0.15; +var TRIGGER_OFF_VALUE = 0.1; +var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab var BUMPER_ON_VALUE = 0.5; @@ -376,6 +375,7 @@ function MyController(hand) { this.entityActivated = false; this.triggerValue = 0; // rolling average of trigger value + this.triggerClicked = false; this.rawTriggerValue = 0; this.rawSecondaryValue = 0; this.rawThumbValue = 0; @@ -835,6 +835,10 @@ function MyController(hand) { _this.rawTriggerValue = value; }; + this.triggerClick = function (value) { + _this.triggerClicked = value; + }; + this.secondaryPress = function (value) { _this.rawSecondaryValue = value; }; @@ -847,7 +851,7 @@ function MyController(hand) { }; this.triggerSmoothedGrab = function () { - return this.triggerValue > TRIGGER_GRAB_VALUE; + return this.triggerClicked; }; this.triggerSmoothedSqueezed = function () { @@ -2225,7 +2229,10 @@ var MAPPING_NAME = "com.highfidelity.handControllerGrab"; var mapping = Controller.newMapping(MAPPING_NAME); mapping.from([Controller.Standard.RT]).peek().to(rightController.triggerPress); +mapping.from([Controller.Standard.RTClick]).peek().to(rightController.triggerClick); + mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress); +mapping.from([Controller.Standard.LTClick]).peek().to(leftController.triggerClick); mapping.from([Controller.Standard.RB]).peek().to(rightController.secondaryPress); mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index cb35414cf8..0b41098eff 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -49,11 +49,15 @@ function Trigger(label) { var that = this; that.label = label; that.TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing - that.TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab - that.TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab - that.TRIGGER_OFF_VALUE = 0.15; + that.TRIGGER_OFF_VALUE = 0.10; + that.TRIGGER_ON_VALUE = that.TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab that.rawTriggerValue = 0; that.triggerValue = 0; // rolling average of trigger value + that.triggerClicked = false; + that.triggerClick = function (value) { + print("Trigger clicked is now " + value); + that.triggerClicked = value; + }; that.triggerPress = function (value) { that.rawTriggerValue = value; }; @@ -64,8 +68,8 @@ function Trigger(label) { (triggerValue * (1.0 - that.TRIGGER_SMOOTH_RATIO)); }; // Current smoothed state, without hysteresis. Answering booleans. - that.triggerSmoothedGrab = function () { - return that.triggerValue > that.TRIGGER_GRAB_VALUE; + that.triggerSmoothedClick = function () { + return that.triggerClicked; }; that.triggerSmoothedSqueezed = function () { return that.triggerValue > that.TRIGGER_ON_VALUE; @@ -81,7 +85,7 @@ function Trigger(label) { that.updateSmoothedTrigger(); // The first two are independent of previous state: - if (that.triggerSmoothedGrab()) { + if (that.triggerSmoothedClick()) { state = 'full'; } else if (that.triggerSmoothedReleased()) { state = null; @@ -365,6 +369,8 @@ Script.scriptEnding.connect(clickMapping.disable); // Gather the trigger data for smoothing. clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); +clickMapping.from(Controller.Standard.RTClick).peek().to(rightTrigger.triggerClick); +clickMapping.from(Controller.Standard.LTClick).peek().to(leftTrigger.triggerClick); // Full smoothed trigger is a click. function isPointingAtOverlayStartedNonFullTrigger(trigger) { // true if isPointingAtOverlay AND we were NOT full triggered when we became so. diff --git a/scripts/system/libraries/Trigger.js b/scripts/system/libraries/Trigger.js new file mode 100644 index 0000000000..ffde021f5d --- /dev/null +++ b/scripts/system/libraries/Trigger.js @@ -0,0 +1,87 @@ +"use strict"; + +/*jslint vars: true, plusplus: true*/ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print*/ + +Trigger = function(properties) { + properties = properties || {}; + var that = this; + that.label = properties.label || Math.random(); + that.SMOOTH_RATIO = properties.smooth || 0.1; // Time averaging of trigger - 0.0 disables smoothing + that.DEADZONE = properties.deadzone || 0.10; // Once pressed, a trigger must fall below the deadzone to be considered un-pressed once pressed. + that.HYSTERESIS = properties.hystersis || 0.05; // If not pressed, a trigger must go above DEADZONE + HYSTERSIS to be considered pressed + + that.value = 0; + that.pressed = false; + that.clicked = false; + + // Handlers + that.onPress = properties.onPress || function(){ + print("Pressed trigger " + that.label) + }; + that.onRelease = properties.onRelease || function(){ + print("Released trigger " + that.label) + }; + that.onClick = properties.onClick || function(){ + print("Clicked trigger " + that.label) + }; + that.onUnclick = properties.onUnclick || function(){ + print("Unclicked trigger " + that.label) + }; + + // Getters + that.isPressed = function() { + return that.pressed; + } + + that.isClicked = function() { + return that.clicked; + } + + that.getValue = function() { + return that.value; + } + + + // Private values + var controller = properties.controller || Controller.Standard.LT; + var controllerClick = properties.controllerClick || Controller.Standard.LTClick; + that.mapping = Controller.newMapping('com.highfidelity.controller.trigger.' + controller + '-' + controllerClick + '.' + that.label + Math.random()); + Script.scriptEnding.connect(that.mapping.disable); + + // Setup mapping, + that.mapping.from(controller).peek().to(function(value) { + that.value = (that.value * that.SMOOTH_RATIO) + + (value * (1.0 - that.SMOOTH_RATIO)); + + var oldPressed = that.pressed; + if (!that.pressed && that.value >= (that.DEADZONE + that.HYSTERESIS)) { + that.pressed = true; + that.onPress(); + } + + if (that.pressed && that.value < that.HYSTERESIS) { + that.pressed = false; + that.onRelease(); + } + }); + + that.mapping.from(controllerClick).peek().to(function(value){ + if (!that.clicked && value > 0.0) { + that.clicked = true; + that.onClick(); + } + if (that.clicked && value == 0.0) { + that.clicked = false; + that.onUnclick(); + } + }); + + that.enable = function() { + that.mapping.enable(); + } + + that.disable = function() { + that.mapping.disable(); + } +} From 656da16c4d9deebf723db547c9aa649f9137100b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 13 Jul 2016 18:03:22 -0700 Subject: [PATCH 1112/1237] Remove highlight animation duration on menus --- interface/resources/qml/menus/VrMenuView.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/menus/VrMenuView.qml b/interface/resources/qml/menus/VrMenuView.qml index d8cc0e6667..5db13fc160 100644 --- a/interface/resources/qml/menus/VrMenuView.qml +++ b/interface/resources/qml/menus/VrMenuView.qml @@ -45,6 +45,7 @@ FocusScope { onVisibleChanged: recalcSize(); onCountChanged: recalcSize(); focus: true + highlightMoveDuration: 0 highlight: Rectangle { anchors { From 51cce939b30e09f041028d5e5c5dcf3cfc4605af Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Jul 2016 10:32:02 -0700 Subject: [PATCH 1113/1237] removed shake to drop --- .../system/controllers/handControllerGrab.js | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index c3177c5338..3b3015563a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -41,10 +41,6 @@ var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only var PICK_WITH_HAND_RAY = true; -var DRAW_GRAB_BOXES = false; -var DRAW_HAND_SPHERES = false; -var DROP_WITHOUT_SHAKE = false; - var EQUIP_SPHERE_COLOR = { red: 179, green: 120, blue: 211 }; var EQUIP_SPHERE_ALPHA = 0.15; var EQUIP_SPHERE_SCALE_FACTOR = 0.65; @@ -1743,22 +1739,8 @@ function MyController(hand) { this.dropGestureProcess = function (deltaTime) { var standardControllerValue = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var pose = Controller.getPoseValue(standardControllerValue); - var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); - if (this.fastHandMoveDetected) { - this.fastHandMoveTimer -= deltaTime; - } - if (this.fastHandMoveTimer < 0) { - this.fastHandMoveDetected = false; - } - var FAST_HAND_SPEED_REST_TIME = 1; // sec - var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec - if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { - this.fastHandMoveDetected = true; - this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; - } - var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); var DOWN = {x: 0, y: -1, z: 0}; @@ -1776,7 +1758,7 @@ function MyController(hand) { print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); } - return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown; + return handIsUpsideDown; }; this.nearGrabbingEnter = function () { @@ -2407,38 +2389,7 @@ function cleanup() { leftController.cleanup(); Controller.disableMapping(MAPPING_NAME); Reticle.setVisible(true); - Menu.removeMenuItem("Developer > Hands", "Drop Without Shake"); } Script.scriptEnding.connect(cleanup); Script.update.connect(update); - -if (!Menu.menuExists("Developer > Grab Script")) { - Menu.addMenu("Developer > Grab Script"); -} - -Menu.addMenuItem({ - menuName: "Developer > Grab Script", - menuItemName: "Drop Without Shake", - isCheckable: true, - isChecked: DROP_WITHOUT_SHAKE -}); - -Menu.addMenuItem({ - menuName: "Developer > Grab Script", - menuItemName: "Draw Grab Boxes", - isCheckable: true, - isChecked: DRAW_GRAB_BOXES -}); - -function handleMenuItemEvent(menuItem) { - if (menuItem === "Drop Without Shake") { - DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); - } - if (menuItem === "Draw Grab Boxes") { - DRAW_GRAB_BOXES = Menu.isOptionChecked("Draw Grab Boxes"); - DRAW_HAND_SPHERES = DRAW_GRAB_BOXES; - } -} - -Menu.menuItemEvent.connect(handleMenuItemEvent); From d7399f5781b30db31796f1d9e475b60df2fec03f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 14 Jul 2016 10:34:42 -0700 Subject: [PATCH 1114/1237] Improved HRTF. Adds a distance filter to model the frequency-dependent attenuation of sound propagation in open air. Optimized using SIMD and computing all biquads in parallel. Performance impact is almost zero. Filter updates are continuously interpolated and clean to -90dB. Not enabled yet (distance hardcoded to 0.0f) --- assignment-client/src/audio/AudioMixer.cpp | 8 +- libraries/audio-client/src/AudioClient.cpp | 2 +- libraries/audio/src/AudioHRTF.cpp | 353 +++++++++++++++++---- libraries/audio/src/AudioHRTF.h | 18 +- 4 files changed, 308 insertions(+), 73 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 7ba9242306..24893ad1b6 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -240,7 +240,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& // this is not done for stereo streams since they do not go through the HRTF static int16_t silentMonoBlock[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL] = {}; - hrtf.renderSilent(silentMonoBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, gain, + hrtf.renderSilent(silentMonoBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++_hrtfSilentRenders;; @@ -287,7 +287,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& // silent frame from source // we still need to call renderSilent via the HRTF for mono source - hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, gain, + hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++_hrtfSilentRenders; @@ -300,7 +300,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& // the mixer is struggling so we're going to drop off some streams // we call renderSilent via the HRTF with the actual frame data and a gain of 0.0 - hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, + hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, 0.0f, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++_hrtfStruggleRenders; @@ -311,7 +311,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& ++_hrtfRenders; // mono stream, call the HRTF with our block and calculated azimuth and gain - hrtf.render(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, gain, + hrtf.render(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index dd72125d93..bc29ba5c96 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -888,7 +888,7 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { float azimuth = azimuthForSource(relativePosition); - injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, 0.0f, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } } else { diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index e7cf4436c6..3a193d786a 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -162,40 +162,68 @@ static void interleave_4x4(float* src0, float* src1, float* src2, float* src3, f } } -// 4 channels (interleaved) -static void biquad_4x4(float* src, float* dst, float coef[5][4], float state[2][4], int numFrames) { +// process 2 cascaded biquads on 4 channels (interleaved) +// biquads computed in parallel, by adding one sample of delay +static void biquad2_4x4(float* src, float* dst, float coef[5][8], float state[3][8], int numFrames) { // enable flush-to-zero mode to prevent denormals unsigned int ftz = _MM_GET_FLUSH_ZERO_MODE(); _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); - __m128 w1 = _mm_loadu_ps(state[0]); - __m128 w2 = _mm_loadu_ps(state[1]); + // restore state + __m128 y00 = _mm_loadu_ps(&state[0][0]); + __m128 w10 = _mm_loadu_ps(&state[1][0]); + __m128 w20 = _mm_loadu_ps(&state[2][0]); - __m128 b0 = _mm_loadu_ps(coef[0]); - __m128 b1 = _mm_loadu_ps(coef[1]); - __m128 b2 = _mm_loadu_ps(coef[2]); - __m128 a1 = _mm_loadu_ps(coef[3]); - __m128 a2 = _mm_loadu_ps(coef[4]); + __m128 y01; + __m128 w11 = _mm_loadu_ps(&state[1][4]); + __m128 w21 = _mm_loadu_ps(&state[2][4]); + + // first biquad coefs + __m128 b00 = _mm_loadu_ps(&coef[0][0]); + __m128 b10 = _mm_loadu_ps(&coef[1][0]); + __m128 b20 = _mm_loadu_ps(&coef[2][0]); + __m128 a10 = _mm_loadu_ps(&coef[3][0]); + __m128 a20 = _mm_loadu_ps(&coef[4][0]); + + // second biquad coefs + __m128 b01 = _mm_loadu_ps(&coef[0][4]); + __m128 b11 = _mm_loadu_ps(&coef[1][4]); + __m128 b21 = _mm_loadu_ps(&coef[2][4]); + __m128 a11 = _mm_loadu_ps(&coef[3][4]); + __m128 a21 = _mm_loadu_ps(&coef[4][4]); for (int i = 0; i < numFrames; i++) { + __m128 x00 = _mm_loadu_ps(&src[4*i]); + __m128 x01 = y00; // first biquad output + // transposed Direct Form II - __m128 x0 = _mm_loadu_ps(&src[4*i]); - __m128 y0; + y00 = _mm_add_ps(w10, _mm_mul_ps(x00, b00)); + y01 = _mm_add_ps(w11, _mm_mul_ps(x01, b01)); - y0 = _mm_add_ps(w1, _mm_mul_ps(x0, b0)); - w1 = _mm_add_ps(w2, _mm_mul_ps(x0, b1)); - w2 = _mm_mul_ps(x0, b2); - w1 = _mm_sub_ps(w1, _mm_mul_ps(y0, a1)); - w2 = _mm_sub_ps(w2, _mm_mul_ps(y0, a2)); + w10 = _mm_add_ps(w20, _mm_mul_ps(x00, b10)); + w11 = _mm_add_ps(w21, _mm_mul_ps(x01, b11)); - _mm_storeu_ps(&dst[4*i], y0); + w20 = _mm_mul_ps(x00, b20); + w21 = _mm_mul_ps(x01, b21); + + w10 = _mm_sub_ps(w10, _mm_mul_ps(y00, a10)); + w11 = _mm_sub_ps(w11, _mm_mul_ps(y01, a11)); + + w20 = _mm_sub_ps(w20, _mm_mul_ps(y00, a20)); + w21 = _mm_sub_ps(w21, _mm_mul_ps(y01, a21)); + + _mm_storeu_ps(&dst[4*i], y01); // second biquad output } // save state - _mm_storeu_ps(state[0], w1); - _mm_storeu_ps(state[1], w2); + _mm_storeu_ps(&state[0][0], y00); + _mm_storeu_ps(&state[1][0], w10); + _mm_storeu_ps(&state[2][0], w20); + + _mm_storeu_ps(&state[1][4], w11); + _mm_storeu_ps(&state[2][4], w21); _MM_SET_FLUSH_ZERO_MODE(ftz); } @@ -345,56 +373,105 @@ static void interleave_4x4(float* src0, float* src1, float* src2, float* src3, f } } -// 4 channels (interleaved) -static void biquad_4x4(float* src, float* dst, float coef[5][4], float state[2][4], int numFrames) { +// process 2 cascaded biquads on 4 channels (interleaved) +// biquads are computed in parallel, by adding one sample of delay +static void biquad2_4x4(float* src, float* dst, float coef[5][8], float state[3][8], int numFrames) { - // channel 0 - float w10 = state[0][0]; - float w20 = state[1][0]; + // restore state + float y00 = state[0][0]; + float w10 = state[1][0]; + float w20 = state[2][0]; + float y01 = state[0][1]; + float w11 = state[1][1]; + float w21 = state[2][1]; + + float y02 = state[0][2]; + float w12 = state[1][2]; + float w22 = state[2][2]; + + float y03 = state[0][3]; + float w13 = state[1][3]; + float w23 = state[2][3]; + + float y04; + float w14 = state[1][4]; + float w24 = state[2][4]; + + float y05; + float w15 = state[1][5]; + float w25 = state[2][5]; + + float y06; + float w16 = state[1][6]; + float w26 = state[2][6]; + + float y07; + float w17 = state[1][7]; + float w27 = state[2][7]; + + // first biquad coefs float b00 = coef[0][0]; float b10 = coef[1][0]; float b20 = coef[2][0]; float a10 = coef[3][0]; float a20 = coef[4][0]; - // channel 1 - float w11 = state[0][1]; - float w21 = state[1][1]; - float b01 = coef[0][1]; float b11 = coef[1][1]; float b21 = coef[2][1]; float a11 = coef[3][1]; float a21 = coef[4][1]; - // channel 2 - float w12 = state[0][2]; - float w22 = state[1][2]; - float b02 = coef[0][2]; float b12 = coef[1][2]; float b22 = coef[2][2]; float a12 = coef[3][2]; float a22 = coef[4][2]; - // channel 3 - float w13 = state[0][3]; - float w23 = state[1][3]; - float b03 = coef[0][3]; float b13 = coef[1][3]; float b23 = coef[2][3]; float a13 = coef[3][3]; float a23 = coef[4][3]; + // second biquad coefs + float b04 = coef[0][4]; + float b14 = coef[1][4]; + float b24 = coef[2][4]; + float a14 = coef[3][4]; + float a24 = coef[4][4]; + + float b05 = coef[0][5]; + float b15 = coef[1][5]; + float b25 = coef[2][5]; + float a15 = coef[3][5]; + float a25 = coef[4][5]; + + float b06 = coef[0][6]; + float b16 = coef[1][6]; + float b26 = coef[2][6]; + float a16 = coef[3][6]; + float a26 = coef[4][6]; + + float b07 = coef[0][7]; + float b17 = coef[1][7]; + float b27 = coef[2][7]; + float a17 = coef[3][7]; + float a27 = coef[4][7]; + for (int i = 0; i < numFrames; i++) { + // first biquad input float x00 = src[4*i+0] + 1.0e-20f; // prevent denormals float x01 = src[4*i+1] + 1.0e-20f; float x02 = src[4*i+2] + 1.0e-20f; float x03 = src[4*i+3] + 1.0e-20f; - float y00, y01, y02, y03; + // second biquad input is previous output + float x04 = y00; + float x05 = y01; + float x06 = y02; + float x07 = y03; // transposed Direct Form II y00 = b00 * x00 + w10; @@ -413,24 +490,57 @@ static void biquad_4x4(float* src, float* dst, float coef[5][4], float state[2][ w13 = b13 * x03 - a13 * y03 + w23; w23 = b23 * x03 - a23 * y03; - dst[4*i+0] = y00; - dst[4*i+1] = y01; - dst[4*i+2] = y02; - dst[4*i+3] = y03; + // transposed Direct Form II + y04 = b04 * x04 + w14; + w14 = b14 * x04 - a14 * y04 + w24; + w24 = b24 * x04 - a24 * y04; + + y05 = b05 * x05 + w15; + w15 = b15 * x05 - a15 * y05 + w25; + w25 = b25 * x05 - a25 * y05; + + y06 = b06 * x06 + w16; + w16 = b16 * x06 - a16 * y06 + w26; + w26 = b26 * x06 - a26 * y06; + + y07 = b07 * x07 + w17; + w17 = b17 * x07 - a17 * y07 + w27; + w27 = b27 * x07 - a27 * y07; + + dst[4*i+0] = y04; // second biquad output + dst[4*i+1] = y05; + dst[4*i+2] = y06; + dst[4*i+3] = y07; } // save state - state[0][0] = w10; - state[1][0] = w20; + state[0][0] = y00; + state[1][0] = w10; + state[2][0] = w20; - state[0][1] = w11; - state[1][1] = w21; + state[0][1] = y01; + state[1][1] = w11; + state[2][1] = w21; - state[0][2] = w12; - state[1][2] = w22; + state[0][2] = y02; + state[1][2] = w12; + state[2][2] = w22; - state[0][3] = w13; - state[1][3] = w23; + state[0][3] = y03; + state[1][3] = w13; + state[2][3] = w23; + + state[1][4] = w14; + state[2][4] = w24; + + state[1][5] = w15; + state[2][5] = w25; + + state[1][6] = w16; + state[2][6] = w26; + + state[1][7] = w17; + state[2][7] = w27; } // crossfade 4 inputs into 2 outputs with accumulation (interleaved) @@ -468,9 +578,85 @@ static void ThiranBiquad(float f, float& b0, float& b1, float& b2, float& a1, fl b2 = 1.0f; } -// compute new filters for a given azimuth and gain -static void setAzimuthAndGain(float firCoef[4][HRTF_TAPS], float bqCoef[5][4], int delay[4], - int index, float azimuth, float gain, int channel) { +// returns the gain of analog (s-plane) lowpass evaluated at w +static double analogFilter(double w0, double w) { + double w0sq, wsq; + double num, den; + + w0sq = w0 * w0; + wsq = w * w; + + num = w0sq * w0sq; + den = wsq * wsq + w0sq * w0sq; + + return sqrt(num / den); +} + +// design a lowpass biquad using analog matching +static void LowpassBiquad(double coef[5], double w0) { + double G1; + double wpi, wn, wd; + double wna, wda; + double gn, gd, gnsq, gdsq; + double num, den; + double Wnsq, Wdsq, B, A; + double b0, b1, b2, a0, a1, a2; + double temp, scale; + const double PI = 3.14159265358979323846; + + // compute the Nyquist gain + wpi = w0 + 2.8 * (1.0 - w0/PI); // minimax-like error + wpi = (wpi > PI) ? PI : wpi; + G1 = analogFilter(w0, wpi); + + // approximate wn and wd + wd = 0.5 * w0; + wn = wd * sqrt(1.0/G1); // down G1 at pi, instead of zeros + + Wnsq = wn * wn; + Wdsq = wd * wd; + + // analog freqs of wn and wd + wna = 2.0 * atan(wn); + wda = 2.0 * atan(wd); + + // normalized analog gains at wna and wda + temp = 1.0 / G1; + gn = temp * analogFilter(w0, wna); + gd = temp * analogFilter(w0, wda); + gnsq = gn * gn; + gdsq = gd * gd; + + // compute B, matching gains at wn and wd + temp = 1.0 / (wn * wd); + den = fabs(gnsq - gdsq); + num = gnsq * (Wnsq - Wdsq) * (Wnsq - Wdsq) * (Wnsq + gdsq * Wdsq); + B = temp * sqrt(num / den); + + // compute A, matching gains at wn and wd + num = (Wnsq - Wdsq) * (Wnsq - Wdsq) * (Wnsq + gnsq * Wdsq); + A = temp * sqrt(num / den); + + // design digital filter via bilinear transform + b0 = G1 * (1.0 + B + Wnsq); + b1 = G1 * 2.0 * (Wnsq - 1.0); + b2 = G1 * (1.0 - B + Wnsq); + a0 = 1.0 + A + Wdsq; + a1 = 2.0 * (Wdsq - 1.0); + a2 = 1.0 - A + Wdsq; + + // normalize + scale = 1.0 / a0; + coef[0] = b0 * scale; + coef[1] = b1 * scale; + coef[2] = b2 * scale; + coef[3] = a1 * scale; + coef[4] = a2 * scale; +} + +// compute new filters for a given azimuth, distance and gain +static void setFilters(float firCoef[4][HRTF_TAPS], float bqCoef[5][8], int delay[4], + int index, float azimuth, float distance, float gain, int channel) { // convert from radians to table units azimuth *= HRTF_AZIMUTHS / TWOPI; @@ -551,9 +737,43 @@ static void setAzimuthAndGain(float firCoef[4][HRTF_TAPS], float bqCoef[5][4], i bqCoef[4][channel+1] = a2; delay[channel+1] = itdi; } + + // + // Model the frequency-dependent attenuation of sound propogation in air. + // Fit using linear regression to a log-log model of lowpass cutoff frequency vs distance, + // loosely based on data from Handbook of Acoustics. Only the onset of significant + // attenuation is modelled, not the filter slope. + // + // 1m -> -3dB @ 55kHz + // 10m -> -3dB @ 12kHz + // 100m -> -3dB @ 2.5kHz + // 1km -> -3dB @ 0.6kHz + // 10km -> -3dB @ 0.1kHz + // + distance = (distance < 1.0f) ? 1.0f : distance; + double freq = exp2(-0.666 * log2(distance) + 15.75); + double coef[5]; + LowpassBiquad(coef, TWOPI * freq / 24000); + + // TESTING: compute attn at w=pi + //double num = coef[0] - coef[1] + coef[2]; + //double den = 1.0 - coef[3] + coef[4]; + //double mag = 10 * log10((num * num) / (den * den)); + + bqCoef[0][channel+4] = (float)coef[0]; + bqCoef[1][channel+4] = (float)coef[1]; + bqCoef[2][channel+4] = (float)coef[2]; + bqCoef[3][channel+4] = (float)coef[3]; + bqCoef[4][channel+4] = (float)coef[4]; + + bqCoef[0][channel+5] = (float)coef[0]; + bqCoef[1][channel+5] = (float)coef[1]; + bqCoef[2][channel+5] = (float)coef[2]; + bqCoef[3][channel+5] = (float)coef[3]; + bqCoef[4][channel+5] = (float)coef[4]; } -void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, float gain, int numFrames) { +void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames) { assert(index >= 0); assert(index < HRTF_TABLES); @@ -562,18 +782,19 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, float in[HRTF_TAPS + HRTF_BLOCK]; // mono float firCoef[4][HRTF_TAPS]; // 4-channel float firBuffer[4][HRTF_DELAY + HRTF_BLOCK]; // 4-channel - float bqCoef[5][4]; // 4-channel (interleaved) + float bqCoef[5][8]; // 4-channel (interleaved) float bqBuffer[4 * HRTF_BLOCK]; // 4-channel (interleaved) int delay[4]; // 4-channel (interleaved) // to avoid polluting the cache, old filters are recomputed instead of stored - setAzimuthAndGain(firCoef, bqCoef, delay, index, _azimuthState, _gainState, L0); + setFilters(firCoef, bqCoef, delay, index, _azimuthState, _distanceState, _gainState, L0); // compute new filters - setAzimuthAndGain(firCoef, bqCoef, delay, index, azimuth, gain, L1); + setFilters(firCoef, bqCoef, delay, index, azimuth, distance, gain, L1); // new parameters become old _azimuthState = azimuth; + _distanceState = distance; _gainState = gain; // convert mono input to float @@ -611,14 +832,25 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, &firBuffer[R1][HRTF_DELAY] - delay[R1], bqBuffer, HRTF_BLOCK); - // process old/new fractional delay - biquad_4x4(bqBuffer, bqBuffer, bqCoef, _bqState, HRTF_BLOCK); + // process old/new biquads + biquad2_4x4(bqBuffer, bqBuffer, bqCoef, _bqState, HRTF_BLOCK); // new state becomes old _bqState[0][L0] = _bqState[0][L1]; _bqState[1][L0] = _bqState[1][L1]; + _bqState[2][L0] = _bqState[2][L1]; + _bqState[0][R0] = _bqState[0][R1]; _bqState[1][R0] = _bqState[1][R1]; + _bqState[2][R0] = _bqState[2][R1]; + + _bqState[0][L2] = _bqState[0][L3]; + _bqState[1][L2] = _bqState[1][L3]; + _bqState[2][L2] = _bqState[2][L3]; + + _bqState[0][R2] = _bqState[0][R3]; + _bqState[1][R2] = _bqState[1][R3]; + _bqState[2][R2] = _bqState[2][R3]; // crossfade old/new output and accumulate crossfade_4x2(bqBuffer, output, crossfadeTable, HRTF_BLOCK); @@ -626,15 +858,16 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, _silentState = false; } -void AudioHRTF::renderSilent(int16_t* input, float* output, int index, float azimuth, float gain, int numFrames) { +void AudioHRTF::renderSilent(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames) { // process the first silent block, to flush internal state if (!_silentState) { - render(input, output, index, azimuth, gain, numFrames); + render(input, output, index, azimuth, distance, gain, numFrames); } // new parameters become old _azimuthState = azimuth; + _distanceState = distance; _gainState = gain; _silentState = true; diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index d3b6237d0c..111409f80c 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -33,15 +33,16 @@ public: // output: interleaved stereo mix buffer (accumulates into existing output) // index: HRTF subject index // azimuth: clockwise panning angle in radians + // distance: source distance in meters // gain: gain factor for distance attenuation // numFrames: must be HRTF_BLOCK in this version // - void render(int16_t* input, float* output, int index, float azimuth, float gain, int numFrames); + void render(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames); // // Fast path when input is known to be silent // - void renderSilent(int16_t* input, float* output, int index, float azimuth, float gain, int numFrames); + void renderSilent(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames); private: AudioHRTF(const AudioHRTF&) = delete; @@ -49,10 +50,10 @@ private: // SIMD channel assignmentS enum Channel { - L0, - R0, - L1, - R1 + L0, R0, + L1, R1, + L2, R2, + L3, R3 }; // For best cache utilization when processing thousands of instances, only @@ -64,11 +65,12 @@ private: // integer delay history float _delayState[4][HRTF_DELAY] = {}; - // fractional delay history - float _bqState[2][4] = {}; + // biquad history + float _bqState[3][8] = {}; // parameter history float _azimuthState = 0.0f; + float _distanceState = 0.0f; float _gainState = 0.0f; bool _silentState = false; From a42f9a5d0e978e8aa580ae554436157b47384c09 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 14 Jul 2016 10:53:13 -0700 Subject: [PATCH 1115/1237] FIxing the mini mirror perf issue and the gamma correction --- .../display-plugins/OpenGLDisplayPlugin.cpp | 2 -- .../stereo/StereoDisplayPlugin.cpp | 2 +- .../render-utils/src/RenderDeferredTask.cpp | 14 +++++++++++++- .../render-utils/src/RenderDeferredTask.h | 18 +++++++++++++++++- libraries/render/src/render/Task.h | 2 +- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index cd1c8eedd1..e0c87fbbed 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -578,8 +578,6 @@ void OpenGLDisplayPlugin::present() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) - glEnable(GL_FRAMEBUFFER_SRGB); - updateTextures(); if (_currentSceneTexture) { // Write all layers to a local framebuffer diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index 0fa3816a8b..cfdfb1fc21 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -75,7 +75,7 @@ bool StereoDisplayPlugin::internalActivate() { _container->removeMenu(FRAMERATE); _screen = qApp->primaryScreen(); - // _container->setFullscreen(_screen); + _container->setFullscreen(_screen); return Parent::internalActivate(); } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index bc7af6e118..7959c371f9 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -201,10 +201,22 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend if (!(renderContext->args && renderContext->args->hasViewFrustum())) { return; } + RenderArgs* args = renderContext->args; + auto config = std::static_pointer_cast(renderContext->jobConfig); + + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + _gpuTimer.begin(batch); + }); for (auto job : _jobs) { job.run(sceneContext, renderContext); } + + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + _gpuTimer.end(batch); + }); + + config->gpuTime = _gpuTimer.getAverage(); } void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { @@ -416,7 +428,7 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const }); args->_batch = nullptr; - std::static_pointer_cast(renderContext->jobConfig)->gpuTime = _gpuTimer.getAverage(); + // std::static_pointer_cast(renderContext->jobConfig)->gpuTime = _gpuTimer.getAverage(); } void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer) { diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 7ddbeecae4..22a460cd88 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -173,13 +173,29 @@ public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer); }; +class RenderDeferredTaskConfig : public render::Task::Config { + Q_OBJECT + Q_PROPERTY(double gpuTime READ getGpuTime) +public: + double getGpuTime() { return gpuTime; } + +protected: + friend class RenderDeferredTask; + double gpuTime; +}; + class RenderDeferredTask : public render::Task { public: + using Config = RenderDeferredTaskConfig; RenderDeferredTask(render::CullFunctor cullFunctor); + void configure(const Config& config) {} void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - using JobModel = Model; + using JobModel = Model; + +protected: + gpu::RangeTimer _gpuTimer; }; #endif // hifi_RenderDeferredTask_h diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index a90c7ea40e..3621fc5e0a 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -521,7 +521,7 @@ public: template Model(const Varying& input, A&&... args) : - Concept(nullptr), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { + Concept(std::make_shared()), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { _config = _data._config; std::static_pointer_cast(_config)->init(&_data); applyConfiguration(); From e62d34136d24ea7c4102fdd75f8a6d04a6c00b0c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Jul 2016 10:53:47 -0700 Subject: [PATCH 1116/1237] Added haptics and hysteresis to drop gesture --- .../system/controllers/handControllerGrab.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 3b3015563a..0ba0b1be7a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1732,8 +1732,7 @@ function MyController(hand) { }; this.dropGestureReset = function () { - this.fastHandMoveDetected = false; - this.fastHandMoveTimer = 0; + this.prevHandIsUpsideDown = false; }; this.dropGestureProcess = function (deltaTime) { @@ -1744,18 +1743,21 @@ function MyController(hand) { var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); var DOWN = {x: 0, y: -1, z: 0}; - var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); + + var DROP_ANGLE = Math.PI / 7; + var HYSTERESIS_FACTOR = 1.1; + var ROTATION_ENTER_THRESHOLD = Math.cos(DROP_ANGLE); + var ROTATION_EXIT_THRESHOLD = Math.cos(DROP_ANGLE * HYSTERESIS_FACTOR); + var rotationThreshold = this.prevHandIsUpsideDown ? ROTATION_EXIT_THRESHOLD : ROTATION_ENTER_THRESHOLD; var handIsUpsideDown = false; - if (Vec3.dot(worldHandUpAxis, DOWN) > ROTATION_THRESHOLD) { + if (Vec3.dot(worldHandUpAxis, DOWN) > rotationThreshold) { handIsUpsideDown = true; } - var WANT_DEBUG = false; - if (WANT_DEBUG) { - print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z); - print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD); - print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); + if (handIsUpsideDown != this.prevHandIsUpsideDown) { + this.prevHandIsUpsideDown = handIsUpsideDown; + Controller.triggerShortHapticPulse(0.5, this.hand); } return handIsUpsideDown; From c7c02d7a59b70c2f85f183a0fff535426d2fc4e4 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 14 Jul 2016 11:24:48 -0700 Subject: [PATCH 1117/1237] Fix compiler warning --- libraries/audio/src/AudioHRTF.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index 3a193d786a..658af01408 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -753,7 +753,7 @@ static void setFilters(float firCoef[4][HRTF_TAPS], float bqCoef[5][8], int dela distance = (distance < 1.0f) ? 1.0f : distance; double freq = exp2(-0.666 * log2(distance) + 15.75); double coef[5]; - LowpassBiquad(coef, TWOPI * freq / 24000); + LowpassBiquad(coef, (double)TWOPI * freq / 24000); // TESTING: compute attn at w=pi //double num = coef[0] - coef[1] + coef[2]; From 7a4bdc1779affed20fc0111689211bb2d5b23062 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 14 Jul 2016 11:30:55 -0700 Subject: [PATCH 1118/1237] enable in Interface and AudioMixer, by passing distance between source and listener --- assignment-client/src/audio/AudioMixer.cpp | 14 +++++++++----- libraries/audio-client/src/AudioClient.cpp | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 24893ad1b6..08e6e029a0 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -193,8 +193,12 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& // check if this is a server echo of a source back to itself bool isEcho = (&streamToAdd == &listeningNodeStream); - // figure out the gain for this source at the listener glm::vec3 relativePosition = streamToAdd.getPosition() - listeningNodeStream.getPosition(); + + // figure out the distance between source and listener + float distance = glm::max(glm::length(relativePosition), EPSILON); + + // figure out the gain for this source at the listener float gain = gainForSource(streamToAdd, listeningNodeStream, relativePosition, isEcho); // figure out the azimuth to this source at the listener @@ -240,7 +244,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& // this is not done for stereo streams since they do not go through the HRTF static int16_t silentMonoBlock[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL] = {}; - hrtf.renderSilent(silentMonoBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, gain, + hrtf.renderSilent(silentMonoBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++_hrtfSilentRenders;; @@ -287,7 +291,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& // silent frame from source // we still need to call renderSilent via the HRTF for mono source - hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, gain, + hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++_hrtfSilentRenders; @@ -300,7 +304,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& // the mixer is struggling so we're going to drop off some streams // we call renderSilent via the HRTF with the actual frame data and a gain of 0.0 - hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, 0.0f, + hrtf.renderSilent(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, distance, 0.0f, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++_hrtfStruggleRenders; @@ -311,7 +315,7 @@ void AudioMixer::addStreamToMixForListeningNodeWithStream(AudioMixerClientData& ++_hrtfRenders; // mono stream, call the HRTF with our block and calculated azimuth and gain - hrtf.render(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, 0.0f, gain, + hrtf.render(streamBlock, _mixedSamples, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index bc29ba5c96..50039ba668 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -882,13 +882,13 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { } else { - // calculate gain and azimuth for hrtf + // calculate distance, gain and azimuth for hrtf glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + float distance = glm::max(glm::length(relativePosition), EPSILON); float gain = gainForSource(relativePosition, injector->getVolume()); - float azimuth = azimuthForSource(relativePosition); + float azimuth = azimuthForSource(relativePosition); - - injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, 0.0f, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } } else { From be0446f9a90f51a9e0272c8e7a4a43de314232bf Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Thu, 14 Jul 2016 11:36:48 -0700 Subject: [PATCH 1119/1237] Delete old logs if the sum size of those logs exceeds reasonable size. --- interface/src/FileLogger.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/interface/src/FileLogger.cpp b/interface/src/FileLogger.cpp index 50f7d15d6b..9df7967baf 100644 --- a/interface/src/FileLogger.cpp +++ b/interface/src/FileLogger.cpp @@ -24,9 +24,14 @@ static const QString FILENAME_FORMAT = "hifi-log_%1_%2.txt"; static const QString DATETIME_FORMAT = "yyyy-MM-dd_hh.mm.ss"; static const QString LOGS_DIRECTORY = "Logs"; +static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; +static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; +static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. static const qint64 MAX_LOG_SIZE = 512 * 1024; +// Max log files found in the log directory is 100. +static const qint64 MAX_LOG_DIR_SIZE = 512 * 1024 * 100; // Max log age is 1 hour static const uint64_t MAX_LOG_AGE_USECS = USECS_PER_SECOND * 3600; @@ -71,6 +76,23 @@ void FilePersistThread::rollFileIfNecessary(QFile& file, bool notifyListenersIfR _lastRollTime = now; } + QStringList nameFilters; + nameFilters << FILENAME_WILDCARD; + + QDir logQDir(FileUtils::standardPath(LOGS_DIRECTORY)); + logQDir.setNameFilters(nameFilters); + logQDir.setSorting(QDir::Time); + QFileInfoList filesInDir = logQDir.entryInfoList(); + qint64 totalSizeOfDir = 0; + foreach (QFileInfo dirItm, filesInDir){ + if (totalSizeOfDir < MAX_LOG_DIR_SIZE){ + totalSizeOfDir += dirItm.size(); + } + else { + QFile file(dirItm.filePath()); + file.remove(); + } + } } } From fa55fc84f5942c4e6b4dcac523c7f28d816ad22e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 14 Jul 2016 12:04:05 -0700 Subject: [PATCH 1120/1237] Optimized compute of distance filters using log-quantized lookup tables. Magnitude error < 0.25dB for entire parameter space. --- libraries/audio/src/AudioHRTF.cpp | 253 +++++++++++++++++++----------- 1 file changed, 159 insertions(+), 94 deletions(-) diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index 658af01408..378e2154c1 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -16,6 +16,13 @@ #include "AudioHRTF.h" #include "AudioHRTFData.h" +#ifndef MAX +#define MAX(a,b) (((a) > (b)) ? (a) : (b)) +#endif +#ifndef MIN +#define MIN(a,b) (((a) < (b)) ? (a) : (b)) +#endif + // // Equal-gain crossfade // @@ -58,6 +65,103 @@ static const float crossfadeTable[HRTF_BLOCK] = { 0.0024846123f, 0.0019026510f, 0.0013981014f, 0.0009710421f, 0.0006215394f, 0.0003496476f, 0.0001554090f, 0.0000388538f, }; +// +// Model the frequency-dependent attenuation of sound propogation in air. +// +// Fit using linear regression to a log-log model of lowpass cutoff frequency vs distance, +// loosely based on data from Handbook of Acoustics. Only the onset of significant +// attenuation is modelled, not the filter slope. +// +// 1m -> -3dB @ 55kHz +// 10m -> -3dB @ 12kHz +// 100m -> -3dB @ 2.5kHz +// 1km -> -3dB @ 0.6kHz +// 10km -> -3dB @ 0.1kHz +// +static const int NLOWPASS = 64; +static const float lowpassTable[NLOWPASS][5] = { // { b0, b1, b2, a1, a2 } + // distance = 1 + { 0.999772371f, 1.399489756f, 0.454495527f, 1.399458985f, 0.454298669f }, + { 0.999631480f, 1.357609808f, 0.425210203f, 1.357549905f, 0.424901586f }, + { 0.999405154f, 1.311503050f, 0.394349994f, 1.311386830f, 0.393871368f }, + { 0.999042876f, 1.260674595f, 0.361869089f, 1.260450057f, 0.361136504f }, + // distance = 2 + { 0.998465222f, 1.204646525f, 0.327757118f, 1.204214978f, 0.326653886f }, + { 0.997548106f, 1.143019308f, 0.292064663f, 1.142195387f, 0.290436690f }, + { 0.996099269f, 1.075569152f, 0.254941286f, 1.074009405f, 0.252600301f }, + { 0.993824292f, 1.002389610f, 0.216688640f, 0.999469185f, 0.213433357f }, + // distance = 4 + { 0.990280170f, 0.924075266f, 0.177827150f, 0.918684864f, 0.173497723f }, + { 0.984818279f, 0.841917936f, 0.139164195f, 0.832151968f, 0.133748443f }, + { 0.976528670f, 0.758036513f, 0.101832398f, 0.740761682f, 0.095635899f }, + { 0.964216485f, 0.675305244f, 0.067243474f, 0.645654855f, 0.061110348f }, + // distance = 8 + { 0.946463038f, 0.596943020f, 0.036899688f, 0.547879974f, 0.032425772f }, + { 0.921823868f, 0.525770189f, 0.012060451f, 0.447952111f, 0.011702396f }, + { 0.890470015f, 0.463334299f, -0.001227816f, 0.347276405f, 0.005300092f }, + { 0.851335343f, 0.407521164f, -0.009353968f, 0.241900234f, 0.007602305f }, + // distance = 16 + { 0.804237360f, 0.358139558f, -0.014293332f, 0.130934213f, 0.017149373f }, + { 0.750073259f, 0.314581568f, -0.016625381f, 0.014505388f, 0.033524057f }, + { 0.690412072f, 0.275936128f, -0.017054561f, -0.106682490f, 0.055976129f }, + { 0.627245545f, 0.241342015f, -0.016246850f, -0.231302564f, 0.083643275f }, + // distance = 32 + { 0.562700627f, 0.210158533f, -0.014740899f, -0.357562697f, 0.115680957f }, + { 0.498787849f, 0.181982455f, -0.012925406f, -0.483461730f, 0.151306628f }, + { 0.437224055f, 0.156585449f, -0.011055180f, -0.607042210f, 0.189796534f }, + { 0.379336998f, 0.133834032f, -0.009281617f, -0.726580065f, 0.230469477f }, + // distance = 64 + { 0.326040627f, 0.113624970f, -0.007683443f, -0.840693542f, 0.272675696f }, + { 0.277861727f, 0.095845793f, -0.006291936f, -0.948380091f, 0.315795676f }, + { 0.234997480f, 0.080357656f, -0.005109519f, -1.049001190f, 0.359246807f }, + { 0.197386484f, 0.066993521f, -0.004122547f, -1.142236313f, 0.402493771f }, + // distance = 128 + { 0.164780457f, 0.055564709f, -0.003309645f, -1.228023442f, 0.445058962f }, + { 0.136808677f, 0.045870650f, -0.002646850f, -1.306498037f, 0.486530514f }, + { 0.113031290f, 0.037708627f, -0.002110591f, -1.377937457f, 0.526566783f }, + { 0.092980475f, 0.030881892f, -0.001679255f, -1.442713983f, 0.564897095f }, + // distance = 256 + { 0.076190239f, 0.025205585f, -0.001333863f, -1.501257246f, 0.601319206f }, + { 0.062216509f, 0.020510496f, -0.001058229f, -1.554025452f, 0.635694228f }, + { 0.050649464f, 0.016644994f, -0.000838826f, -1.601484205f, 0.667939837f }, + { 0.041120009f, 0.013475547f, -0.000664513f, -1.644091518f, 0.698022561f }, + // distance = 512 + { 0.033302044f, 0.010886252f, -0.000526217f, -1.682287704f, 0.725949783f }, + { 0.026911868f, 0.008777712f, -0.000416605f, -1.716488979f, 0.751761953f }, + { 0.021705773f, 0.007065551f, -0.000329788f, -1.747083800f, 0.775525335f }, + { 0.017476603f, 0.005678758f, -0.000261057f, -1.774431204f, 0.797325509f }, + // distance = 1024 + { 0.014049828f, 0.004558012f, -0.000206658f, -1.798860530f, 0.817261711f }, + { 0.011279504f, 0.003654067f, -0.000163610f, -1.820672082f, 0.835442043f }, + { 0.009044384f, 0.002926264f, -0.000129544f, -1.840138412f, 0.851979516f }, + { 0.007244289f, 0.002341194f, -0.000102586f, -1.857505967f, 0.866988864f }, + // distance = 2048 + { 0.005796846f, 0.001871515f, -0.000081250f, -1.872996926f, 0.880584038f }, + { 0.004634607f, 0.001494933f, -0.000064362f, -1.886811124f, 0.892876302f }, + { 0.003702543f, 0.001193324f, -0.000050993f, -1.899127955f, 0.903972829f }, + { 0.002955900f, 0.000951996f, -0.000040407f, -1.910108223f, 0.913975712f }, + // distance = 4096 + { 0.002358382f, 0.000759068f, -0.000032024f, -1.919895894f, 0.922981321f }, + { 0.001880626f, 0.000604950f, -0.000025383f, -1.928619738f, 0.931079931f }, + { 0.001498926f, 0.000481920f, -0.000020123f, -1.936394836f, 0.938355560f }, + { 0.001194182f, 0.000383767f, -0.000015954f, -1.943323983f, 0.944885977f }, + // distance = 8192 + { 0.000951028f, 0.000305502f, -0.000012651f, -1.949498943f, 0.950742822f }, + { 0.000757125f, 0.000243126f, -0.000010033f, -1.955001608f, 0.955991826f }, + { 0.000602572f, 0.000193434f, -0.000007957f, -1.959905036f, 0.960693085f }, + { 0.000479438f, 0.000153861f, -0.000006312f, -1.964274383f, 0.964901371f }, + // distance = 16384 + { 0.000381374f, 0.000122359f, -0.000005007f, -1.968167752f, 0.968666478f }, + { 0.000303302f, 0.000097288f, -0.000003972f, -1.971636944f, 0.972033562f }, + { 0.000241166f, 0.000077342f, -0.000003151f, -1.974728138f, 0.975043493f }, + { 0.000191726f, 0.000061475f, -0.000002500f, -1.977482493f, 0.977733194f }, + // distance = 32768 + { 0.000152399f, 0.000048857f, -0.000001984f, -1.979936697f, 0.980135969f }, + { 0.000121122f, 0.000038825f, -0.000001574f, -1.982123446f, 0.982281818f }, + { 0.000096252f, 0.000030849f, -0.000001249f, -1.984071877f, 0.984197728f }, + { 0.000076480f, 0.000024509f, -0.000000991f, -1.985807957f, 0.985907955f }, +}; + static const float TWOPI = 6.283185307f; // @@ -578,80 +682,58 @@ static void ThiranBiquad(float f, float& b0, float& b1, float& b2, float& a1, fl b2 = 1.0f; } -// returns the gain of analog (s-plane) lowpass evaluated at w -static double analogFilter(double w0, double w) { - double w0sq, wsq; - double num, den; +// split x into exponent and fraction (0.0f to 1.0f) +static void splitf(float x, int& expn, float& frac) { - w0sq = w0 * w0; - wsq = w * w; + union { float f; int i; } mant, bits = { x }; + const int IEEE754_MANT_BITS = 23; + const int IEEE754_EXPN_BIAS = 127; - num = w0sq * w0sq; - den = wsq * wsq + w0sq * w0sq; - - return sqrt(num / den); + mant.i = bits.i & ((1 << IEEE754_MANT_BITS) - 1); + mant.i |= (IEEE754_EXPN_BIAS << IEEE754_MANT_BITS); + + frac = mant.f - 1.0f; + expn = (bits.i >> IEEE754_MANT_BITS) - IEEE754_EXPN_BIAS; } -// design a lowpass biquad using analog matching -static void LowpassBiquad(double coef[5], double w0) { - double G1; - double wpi, wn, wd; - double wna, wda; - double gn, gd, gnsq, gdsq; - double num, den; - double Wnsq, Wdsq, B, A; - double b0, b1, b2, a0, a1, a2; - double temp, scale; - const double PI = 3.14159265358979323846; +static void distanceBiquad(float distance, float& b0, float& b1, float& b2, float& a1, float& a2) { - // compute the Nyquist gain - wpi = w0 + 2.8 * (1.0 - w0/PI); // minimax-like error - wpi = (wpi > PI) ? PI : wpi; - G1 = analogFilter(w0, wpi); + // + // Computed from a lookup table quantized to distance = 2^(N/4) + // and reconstructed by piecewise linear interpolation. + // Approximation error < 0.25dB + // - // approximate wn and wd - wd = 0.5 * w0; - wn = wd * sqrt(1.0/G1); // down G1 at pi, instead of zeros + float x = distance; + x = MIN(MAX(x, 1.0f), 1<<30); + x *= x; + x *= x; // x = distance^4 - Wnsq = wn * wn; - Wdsq = wd * wd; + // split x into e and frac, such that x = 2^(e+0) + frac * (2^(e+1) - 2^(e+0)) + int e; + float frac; + splitf(x, e, frac); - // analog freqs of wn and wd - wna = 2.0 * atan(wn); - wda = 2.0 * atan(wd); + // clamp to table limits + if (e < 0) { + e = 0; + frac = 0.0f; + } + if (e > NLOWPASS-2) { + e = NLOWPASS-2; + frac = 1.0f; + } + assert(frac >= 0.0f); + assert(frac <= 1.0f); + assert(e+0 >= 0); + assert(e+1 < NLOWPASS); - // normalized analog gains at wna and wda - temp = 1.0 / G1; - gn = temp * analogFilter(w0, wna); - gd = temp * analogFilter(w0, wda); - gnsq = gn * gn; - gdsq = gd * gd; - - // compute B, matching gains at wn and wd - temp = 1.0 / (wn * wd); - den = fabs(gnsq - gdsq); - num = gnsq * (Wnsq - Wdsq) * (Wnsq - Wdsq) * (Wnsq + gdsq * Wdsq); - B = temp * sqrt(num / den); - - // compute A, matching gains at wn and wd - num = (Wnsq - Wdsq) * (Wnsq - Wdsq) * (Wnsq + gnsq * Wdsq); - A = temp * sqrt(num / den); - - // design digital filter via bilinear transform - b0 = G1 * (1.0 + B + Wnsq); - b1 = G1 * 2.0 * (Wnsq - 1.0); - b2 = G1 * (1.0 - B + Wnsq); - a0 = 1.0 + A + Wdsq; - a1 = 2.0 * (Wdsq - 1.0); - a2 = 1.0 - A + Wdsq; - - // normalize - scale = 1.0 / a0; - coef[0] = b0 * scale; - coef[1] = b1 * scale; - coef[2] = b2 * scale; - coef[3] = a1 * scale; - coef[4] = a2 * scale; + // piecewise linear interpolation + b0 = lowpassTable[e+0][0] + frac * (lowpassTable[e+1][0] - lowpassTable[e+0][0]); + b1 = lowpassTable[e+0][1] + frac * (lowpassTable[e+1][1] - lowpassTable[e+0][1]); + b2 = lowpassTable[e+0][2] + frac * (lowpassTable[e+1][2] - lowpassTable[e+0][2]); + a1 = lowpassTable[e+0][3] + frac * (lowpassTable[e+1][3] - lowpassTable[e+0][3]); + a2 = lowpassTable[e+0][4] + frac * (lowpassTable[e+1][4] - lowpassTable[e+0][4]); } // compute new filters for a given azimuth, distance and gain @@ -739,38 +821,21 @@ static void setFilters(float firCoef[4][HRTF_TAPS], float bqCoef[5][8], int dela } // - // Model the frequency-dependent attenuation of sound propogation in air. - // Fit using linear regression to a log-log model of lowpass cutoff frequency vs distance, - // loosely based on data from Handbook of Acoustics. Only the onset of significant - // attenuation is modelled, not the filter slope. + // Second biquad implements the distance filter. // - // 1m -> -3dB @ 55kHz - // 10m -> -3dB @ 12kHz - // 100m -> -3dB @ 2.5kHz - // 1km -> -3dB @ 0.6kHz - // 10km -> -3dB @ 0.1kHz - // - distance = (distance < 1.0f) ? 1.0f : distance; - double freq = exp2(-0.666 * log2(distance) + 15.75); - double coef[5]; - LowpassBiquad(coef, (double)TWOPI * freq / 24000); + distanceBiquad(distance, b0, b1, b2, a1, a2); - // TESTING: compute attn at w=pi - //double num = coef[0] - coef[1] + coef[2]; - //double den = 1.0 - coef[3] + coef[4]; - //double mag = 10 * log10((num * num) / (den * den)); + bqCoef[0][channel+4] = b0; + bqCoef[1][channel+4] = b1; + bqCoef[2][channel+4] = b2; + bqCoef[3][channel+4] = a1; + bqCoef[4][channel+4] = a2; - bqCoef[0][channel+4] = (float)coef[0]; - bqCoef[1][channel+4] = (float)coef[1]; - bqCoef[2][channel+4] = (float)coef[2]; - bqCoef[3][channel+4] = (float)coef[3]; - bqCoef[4][channel+4] = (float)coef[4]; - - bqCoef[0][channel+5] = (float)coef[0]; - bqCoef[1][channel+5] = (float)coef[1]; - bqCoef[2][channel+5] = (float)coef[2]; - bqCoef[3][channel+5] = (float)coef[3]; - bqCoef[4][channel+5] = (float)coef[4]; + bqCoef[0][channel+5] = b0; + bqCoef[1][channel+5] = b1; + bqCoef[2][channel+5] = b2; + bqCoef[3][channel+5] = a1; + bqCoef[4][channel+5] = a2; } void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames) { From 3ab56062d4e7b993bbefadd201e3683d6e4b9ff1 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 14 Jul 2016 12:06:02 -0700 Subject: [PATCH 1121/1237] Address some warnings and syntax issues --- interface/src/Application.cpp | 1 - libraries/fbx/src/FBXReader_Material.cpp | 1 + libraries/gpu-gl/src/gpu/gl/GLShared.cpp | 7 +++++-- libraries/render-utils/src/DeferredLightingEffect.cpp | 5 +---- libraries/render/src/render/Task.h | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9af09dc21d..a14c9e0814 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1680,7 +1680,6 @@ void Application::paintGL() { auto inputs = AvatarInputs::getInstance(); if (inputs->mirrorVisible()) { PerformanceTimer perfTimer("Mirror"); - // auto primaryFbo = DependencyManager::get()->getPrimaryFramebuffer(); renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index f68203a04b..eb25f1d8a2 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -258,6 +258,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { } } } + qDebug() << " fbx material Name:" << material.name; if (materialMap.contains(material.name)) { QJsonObject materialOptions = materialMap.value(material.name).toObject(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index db930cf696..8f234ca6b4 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -671,15 +671,18 @@ bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const s // if compilation fails if (!compiled) { + // save the source code to a temp file so we can debug easily - std::ofstream filestream; + /* + std::ofstream filestream; filestream.open("debugshader.glsl"); if (filestream.is_open()) { filestream << srcstr[0]; filestream << srcstr[1]; filestream.close(); } - + */ + GLint infoLength = 0; glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 665f1c14ce..1299cb428f 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -331,13 +331,10 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { } void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, gpu::FramebufferPointer& primaryFramebuffer) { - auto args = renderContext->args; auto framebufferCache = DependencyManager::get(); auto framebufferSize = framebufferCache->getFrameBufferSize(); - - // glm::ivec2 frameSize(args->_viewport.z, args->_viewport.w); - glm::ivec2 frameSize(framebufferSize.width(), framebufferSize.height()); + glm::ivec2 frameSize(framebufferSize.width(), framebufferSize.height()); if (!_primaryFramebuffer) { _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 3621fc5e0a..a42fb4f0ac 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -521,7 +521,7 @@ public: template Model(const Varying& input, A&&... args) : - Concept(std::make_shared()), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { + Concept(std::make_shared()), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { _config = _data._config; std::static_pointer_cast(_config)->init(&_data); applyConfiguration(); From eb5107d9a510dcf6da5d117198bb13869cb6c267 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Jul 2016 12:07:07 -0700 Subject: [PATCH 1122/1237] highlight the grabbed hotspot when the drop gesture is detected --- scripts/system/controllers/handControllerGrab.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 0ba0b1be7a..c78cf4dbbb 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1882,6 +1882,14 @@ function MyController(hand) { } if (this.state == STATE_HOLD) { + + // highlight the grabbed hotspot when the dropGesture is detected. + if (dropDetected) { + entityPropertiesCache.addEntity(this.grabbedHotspot.entityID); + equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp); + equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); + } + if (dropDetected && this.triggerSmoothedGrab()) { this.callEntityMethodOnGrabbed("releaseEquip"); this.setState(STATE_OFF, "drop gesture detected"); From cdffb453c62fbc4e709f4c98af9ec9a9313a62ea Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 12:07:38 -0700 Subject: [PATCH 1123/1237] Trigger 'nav select' on the new trigger click signal, not any trigger value --- interface/resources/controllers/standard_navigation.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/interface/resources/controllers/standard_navigation.json b/interface/resources/controllers/standard_navigation.json index c9dc90cbe6..81096a7230 100644 --- a/interface/resources/controllers/standard_navigation.json +++ b/interface/resources/controllers/standard_navigation.json @@ -10,14 +10,7 @@ { "from": "Standard.RB", "to": "Actions.UiNavGroup" }, { "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" }, { "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" }, - { - "from": [ "Standard.RT", "Standard.LT" ], - "to": "Actions.UiNavSelect", - "filters": [ - { "type": "deadZone", "min": 0.5 }, - "constrainToInteger" - ] - }, + { "from": [ "Standard.RTClick", "Standard.LTClick" ], "to": "Actions.UiNavSelect" }, { "from": "Standard.LX", "to": "Actions.UiNavLateral", "filters": [ From 882139ebcff481bb4edf1e21f72590a218ca521d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 12:10:46 -0700 Subject: [PATCH 1124/1237] Allow scripts to override the navigation focus state set by QML dialogs --- interface/src/Application.cpp | 1 + libraries/ui/src/OffscreenUi.cpp | 19 ++++++++++++++++++- libraries/ui/src/OffscreenUi.h | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0e0f56438e..fb48472b14 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4705,6 +4705,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri qScriptRegisterMetaType(scriptEngine, RayToOverlayIntersectionResultToScriptValue, RayToOverlayIntersectionResultFromScriptValue); + scriptEngine->registerGlobalObject("OffscreenFlags", DependencyManager::get()->getFlags()); scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 1a7d4b2328..28aba4a365 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -29,6 +29,9 @@ class OffscreenFlags : public QObject { Q_OBJECT Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged) + // Allow scripts that are doing their own navigation support to disable navigation focus (i.e. handControllerPointer.js) + Q_PROPERTY(bool navigationFocusDisabled READ isNavigationFocusDisabled WRITE setNavigationFocusDisabled NOTIFY navigationFocusDisabledChanged) + public: OffscreenFlags(QObject* parent = nullptr) : QObject(parent) {} @@ -40,11 +43,21 @@ public: } } + bool isNavigationFocusDisabled() const { return _navigationFocusDisabled; } + void setNavigationFocusDisabled(bool disabled) { + if (_navigationFocusDisabled != disabled) { + _navigationFocusDisabled = disabled; + emit navigationFocusDisabledChanged(); + } + } + signals: void navigationFocusedChanged(); + void navigationFocusDisabledChanged(); private: bool _navigationFocused { false }; + bool _navigationFocusDisabled{ false }; }; QString fixupHifiUrl(const QString& urlString) { @@ -103,6 +116,10 @@ bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { OffscreenUi::OffscreenUi() { } +QObject* OffscreenUi::getFlags() { + return offscreenFlags; +} + void OffscreenUi::create(QOpenGLContext* context) { OffscreenQmlSurface::create(context); auto rootContext = getRootContext(); @@ -392,7 +409,7 @@ QVariant OffscreenUi::waitForInputDialogResult(QQuickItem* inputDialog) { } bool OffscreenUi::navigationFocused() { - return offscreenFlags->isNavigationFocused(); + return !offscreenFlags->isNavigationFocusDisabled() && offscreenFlags->isNavigationFocused(); } void OffscreenUi::setNavigationFocused(bool focused) { diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index e1d552c978..2bd00bf612 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -55,7 +55,7 @@ public: bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); - + QObject* getFlags(); QQuickItem* getDesktop(); QQuickItem* getToolWindow(); From 8685b236101b20a44403710364ee7de10b992f06 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 12:11:12 -0700 Subject: [PATCH 1125/1237] When using the hand controller as a mouse, disable the navigation focus state in QML --- scripts/system/controllers/handControllerPointer.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 0b41098eff..9ff82b3767 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -54,18 +54,14 @@ function Trigger(label) { that.rawTriggerValue = 0; that.triggerValue = 0; // rolling average of trigger value that.triggerClicked = false; - that.triggerClick = function (value) { - print("Trigger clicked is now " + value); - that.triggerClicked = value; - }; - that.triggerPress = function (value) { - that.rawTriggerValue = value; - }; + that.triggerClick = function (value) { that.triggerClicked = value; }; + that.triggerPress = function (value) { that.rawTriggerValue = value; }; that.updateSmoothedTrigger = function () { // e.g., call once/update for effect var triggerValue = that.rawTriggerValue; // smooth out trigger value that.triggerValue = (that.triggerValue * that.TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - that.TRIGGER_SMOOTH_RATIO)); + OffscreenFlags.navigationFocusDisabled = that.triggerValue != 0.0; }; // Current smoothed state, without hysteresis. Answering booleans. that.triggerSmoothedClick = function () { @@ -499,7 +495,10 @@ function checkSettings() { updateRecommendedArea(); } checkSettings(); + var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); + OffscreenFlags.navigationFocusDisabled = false; }); + From 8d5b60a266482f740251def186a6c996946fef5c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 12:30:17 -0700 Subject: [PATCH 1126/1237] Simulate LTClick/RTClick for Oculus Touch and Hydra controllers --- interface/resources/controllers/hydra.json | 8 ++++++++ interface/resources/controllers/oculus_touch.json | 8 ++++++++ plugins/oculus/src/OculusControllerManager.cpp | 8 -------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/interface/resources/controllers/hydra.json b/interface/resources/controllers/hydra.json index 066676140c..716d283a86 100644 --- a/interface/resources/controllers/hydra.json +++ b/interface/resources/controllers/hydra.json @@ -3,9 +3,17 @@ "channels": [ { "from": "Hydra.LY", "filters": "invert", "to": "Standard.LY" }, { "from": "Hydra.LX", "to": "Standard.LX" }, + { "from": "Hydra.LT", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "Hydra.LT", "to": "Standard.LT" }, { "from": "Hydra.RY", "filters": "invert", "to": "Standard.RY" }, { "from": "Hydra.RX", "to": "Standard.RX" }, + { "from": "Hydra.RT", "to": "Standard.RTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "Hydra.RT", "to": "Standard.RT" }, { "from": "Hydra.LB", "to": "Standard.LB" }, diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index 3def439221..001d5b8716 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -8,6 +8,10 @@ { "from": "OculusTouch.LY", "filters": "invert", "to": "Standard.LY" }, { "from": "OculusTouch.LX", "to": "Standard.LX" }, + { "from": "OculusTouch.LT", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "OculusTouch.LT", "to": "Standard.LT" }, { "from": "OculusTouch.LS", "to": "Standard.LS" }, { "from": "OculusTouch.LeftGrip", "to": "Standard.LeftGrip" }, @@ -15,6 +19,10 @@ { "from": "OculusTouch.RY", "filters": "invert", "to": "Standard.RY" }, { "from": "OculusTouch.RX", "to": "Standard.RX" }, + { "from": "OculusTouch.RT", "to": "Standard.RTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "OculusTouch.RT", "to": "Standard.RT" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, { "from": "OculusTouch.RightGrip", "to": "Standard.RightGrip" }, diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 225ccf42b2..b3b1b20b2b 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -199,19 +199,11 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control _axisStateMap[LX] = inputState.Thumbstick[ovrHand_Left].x; _axisStateMap[LY] = inputState.Thumbstick[ovrHand_Left].y; _axisStateMap[LT] = inputState.IndexTrigger[ovrHand_Left]; - // FIXME add hysteresis? Move to JSON config? - if (inputState.IndexTrigger[ovrHand_Left] > 0.9) { - _buttonPressedMap.insert(LT_CLICK); - } _axisStateMap[LEFT_GRIP] = inputState.HandTrigger[ovrHand_Left]; _axisStateMap[RX] = inputState.Thumbstick[ovrHand_Right].x; _axisStateMap[RY] = inputState.Thumbstick[ovrHand_Right].y; _axisStateMap[RT] = inputState.IndexTrigger[ovrHand_Right]; - // FIXME add hysteresis? Move to JSON config? - if (inputState.IndexTrigger[ovrHand_Right] > 0.9) { - _buttonPressedMap.insert(RT_CLICK); - } _axisStateMap[RIGHT_GRIP] = inputState.HandTrigger[ovrHand_Right]; // Buttons From e4dabc6be4f883ed3e3386720c0fcb5f564f1812 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 14 Jul 2016 12:53:50 -0700 Subject: [PATCH 1127/1237] fix task config initialization --- .../render-utils/src/RenderShadowTask.cpp | 3 +- libraries/render/src/render/Task.h | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index 5680660122..2e3901a769 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -85,8 +85,7 @@ void RenderShadowMap::run(const render::SceneContextPointer& sceneContext, const }); } -// The shadow task *must* use this base ctor to initialize with its own Config, see Task.h -RenderShadowTask::RenderShadowTask(CullFunctor cullFunctor) : Task(std::make_shared()) { +RenderShadowTask::RenderShadowTask(CullFunctor cullFunctor) { cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; }; // Prepare the ShapePipeline diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index a42fb4f0ac..506de4aa6d 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -368,8 +368,6 @@ public: TaskConfig() = default ; TaskConfig(bool enabled) : JobConfig(enabled) {} - void init(Task* task) { _task = task; } - // getter for qml integration, prefer the templated getter Q_INVOKABLE QObject* getConfig(const QString& name) { return QObject::findChild(name); } // getter for cpp (strictly typed), prefer this getter @@ -382,6 +380,7 @@ public slots: void refresh(); private: + friend class Task; Task* _task; }; @@ -521,9 +520,10 @@ public: template Model(const Varying& input, A&&... args) : - Concept(std::make_shared()), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { - _config = _data._config; - std::static_pointer_cast(_config)->init(&_data); + Concept(nullptr), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { + // Recreate the Config to use the templated type + _data.createConfiguration(); + _config = _data.getConfiguration(); applyConfiguration(); } @@ -545,23 +545,19 @@ public: using Jobs = std::vector; - // A task must use its Config for construction - Task() : _config{ std::make_shared() } {} - template Task(std::shared_ptr config) : _config{ config } {} - // Create a new job in the container's queue; returns the job's output template const Varying addJob(std::string name, const Varying& input, A&&... args) { _jobs.emplace_back(name, std::make_shared(input, std::forward(args)...)); QConfigPointer config = _jobs.back().getConfiguration(); - config->setParent(_config.get()); + config->setParent(getConfiguration().get()); config->setObjectName(name.c_str()); // Connect loaded->refresh - QObject::connect(config.get(), SIGNAL(loaded()), _config.get(), SLOT(refresh())); + QObject::connect(config.get(), SIGNAL(loaded()), getConfiguration().get(), SLOT(refresh())); static const char* DIRTY_SIGNAL = "dirty()"; if (config->metaObject()->indexOfSignal(DIRTY_SIGNAL) != -1) { // Connect dirty->refresh if defined - QObject::connect(config.get(), SIGNAL(dirty()), _config.get(), SLOT(refresh())); + QObject::connect(config.get(), SIGNAL(dirty()), getConfiguration().get(), SLOT(refresh())); } return _jobs.back().getOutput(); @@ -571,11 +567,24 @@ public: return addJob(name, input, std::forward(args)...); } + template void createConfiguration() { + auto config = std::make_shared(); + if (_config) { + // Transfer children to the new configuration + auto children = _config->children(); + for (auto& child : children) { + child->setParent(config.get()); + } + } + _config = config; + std::static_pointer_cast(_config)->_task = this; + } + std::shared_ptr getConfiguration() { - auto config = std::static_pointer_cast(_config); - // If we are here, we were not made by a Model, so we must initialize our own config - config->init(this); - return config; + if (!_config) { + createConfiguration(); + } + return std::static_pointer_cast(_config); } void configure(const QObject& configuration) { From e71a2097a5c786520d15b614f70296c9c9b6d2dd Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 14 Jul 2016 13:19:49 -0700 Subject: [PATCH 1128/1237] update doc for task template --- libraries/render/src/render/Task.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 506de4aa6d..6a7b04198f 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -497,8 +497,6 @@ public: // A task is a specialized job to run a collection of other jobs // It is defined with JobModel = Task::Model -// -// A task with a custom config *must* use the templated constructor class Task { public: using Config = TaskConfig; From 9641ae8cd816eb57f28366f4b66c3416df049a58 Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Thu, 14 Jul 2016 13:22:43 -0700 Subject: [PATCH 1129/1237] Coding standard fun! --- interface/src/FileLogger.cpp | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/interface/src/FileLogger.cpp b/interface/src/FileLogger.cpp index 9df7967baf..628aa30a3d 100644 --- a/interface/src/FileLogger.cpp +++ b/interface/src/FileLogger.cpp @@ -62,20 +62,20 @@ FilePersistThread::FilePersistThread(const FileLogger& logger) : _logger(logger) } void FilePersistThread::rollFileIfNecessary(QFile& file, bool notifyListenersIfRolled) { - uint64_t now = usecTimestampNow(); - if ((file.size() > MAX_LOG_SIZE) || (now - _lastRollTime) > MAX_LOG_AGE_USECS) { - QString newFileName = getLogRollerFilename(); - if (file.copy(newFileName)) { - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.close(); - qDebug() << "Rolled log file:" << newFileName; + uint64_t now = usecTimestampNow(); + if ((file.size() > MAX_LOG_SIZE) || (now - _lastRollTime) > MAX_LOG_AGE_USECS) { + QString newFileName = getLogRollerFilename(); + if (file.copy(newFileName)) { + file.open(QIODevice::WriteOnly | QIODevice::Truncate); + file.close(); + qDebug() << "Rolled log file:" << newFileName; - if (notifyListenersIfRolled) { - emit rollingLogFile(newFileName); - } + if (notifyListenersIfRolled) { + emit rollingLogFile(newFileName); + } - _lastRollTime = now; - } + _lastRollTime = now; + } QStringList nameFilters; nameFilters << FILENAME_WILDCARD; @@ -84,18 +84,16 @@ void FilePersistThread::rollFileIfNecessary(QFile& file, bool notifyListenersIfR logQDir.setSorting(QDir::Time); QFileInfoList filesInDir = logQDir.entryInfoList(); qint64 totalSizeOfDir = 0; - foreach (QFileInfo dirItm, filesInDir){ + foreach(QFileInfo dirItm, filesInDir){ if (totalSizeOfDir < MAX_LOG_DIR_SIZE){ totalSizeOfDir += dirItm.size(); - } - else { + } else { QFile file(dirItm.filePath()); file.remove(); } } - } + } } - bool FilePersistThread::processQueueItems(const Queue& messages) { QFile file(_logger._fileName); rollFileIfNecessary(file); From 44500889f8ff0336ad9911ed2f3015f134ea38d2 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 13:34:56 -0700 Subject: [PATCH 1130/1237] Remove 'updating reverb' logspam --- libraries/audio-client/src/AudioClient.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index dd72125d93..bec30edb4e 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -954,7 +954,6 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA if (hasReverb) { assert(_outputFormat.channelCount() == 2); updateReverbOptions(); - qDebug() << "handling reverb"; _listenerReverb.render(outputSamples, outputSamples, numDeviceOutputSamples/2); } } From 4fb9d8801c1dc2d95ac05415685505ebd9ddf556 Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Thu, 14 Jul 2016 13:55:26 -0700 Subject: [PATCH 1131/1237] More coding standard fun! --- interface/src/FileLogger.cpp | 56 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/interface/src/FileLogger.cpp b/interface/src/FileLogger.cpp index 628aa30a3d..48bd881bfd 100644 --- a/interface/src/FileLogger.cpp +++ b/interface/src/FileLogger.cpp @@ -62,37 +62,37 @@ FilePersistThread::FilePersistThread(const FileLogger& logger) : _logger(logger) } void FilePersistThread::rollFileIfNecessary(QFile& file, bool notifyListenersIfRolled) { - uint64_t now = usecTimestampNow(); - if ((file.size() > MAX_LOG_SIZE) || (now - _lastRollTime) > MAX_LOG_AGE_USECS) { - QString newFileName = getLogRollerFilename(); - if (file.copy(newFileName)) { - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.close(); - qDebug() << "Rolled log file:" << newFileName; + uint64_t now = usecTimestampNow(); + if ((file.size() > MAX_LOG_SIZE) || (now - _lastRollTime) > MAX_LOG_AGE_USECS) { + QString newFileName = getLogRollerFilename(); + if (file.copy(newFileName)) { + file.open(QIODevice::WriteOnly | QIODevice::Truncate); + file.close(); + qDebug() << "Rolled log file:" << newFileName; - if (notifyListenersIfRolled) { - emit rollingLogFile(newFileName); - } + if (notifyListenersIfRolled) { + emit rollingLogFile(newFileName); + } - _lastRollTime = now; - } - QStringList nameFilters; - nameFilters << FILENAME_WILDCARD; + _lastRollTime = now; + } + QStringList nameFilters; + nameFilters << FILENAME_WILDCARD; - QDir logQDir(FileUtils::standardPath(LOGS_DIRECTORY)); - logQDir.setNameFilters(nameFilters); - logQDir.setSorting(QDir::Time); - QFileInfoList filesInDir = logQDir.entryInfoList(); - qint64 totalSizeOfDir = 0; - foreach(QFileInfo dirItm, filesInDir){ - if (totalSizeOfDir < MAX_LOG_DIR_SIZE){ - totalSizeOfDir += dirItm.size(); - } else { - QFile file(dirItm.filePath()); - file.remove(); - } - } - } + QDir logQDir(FileUtils::standardPath(LOGS_DIRECTORY)); + logQDir.setNameFilters(nameFilters); + logQDir.setSorting(QDir::Time); + QFileInfoList filesInDir = logQDir.entryInfoList(); + qint64 totalSizeOfDir = 0; + foreach(QFileInfo dirItm, filesInDir){ + if (totalSizeOfDir < MAX_LOG_DIR_SIZE){ + totalSizeOfDir += dirItm.size(); + } else { + QFile file(dirItm.filePath()); + file.remove(); + } + } + } } bool FilePersistThread::processQueueItems(const Queue& messages) { QFile file(_logger._fileName); From 3132cca482cb13c6f7d56b83fb88723779367e1b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 14 Jul 2016 14:49:03 -0700 Subject: [PATCH 1132/1237] prototype --- interface/resources/qml/AddressBarDialog.qml | 162 +++++++++++++++++- .../qml/controls-uit/HorizontalSpacer.qml | 19 ++ interface/resources/qml/hifi/Card.qml | 64 +++++++ 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 interface/resources/qml/controls-uit/HorizontalSpacer.qml create mode 100644 interface/resources/qml/hifi/Card.qml diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index dc060b70e1..8ac199b326 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -11,8 +11,13 @@ import Hifi 1.0 import QtQuick 2.4 import "controls" +import "controls-uit" as HifiControls import "styles" import "windows" +import "hifi" + +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 Window { id: root @@ -43,12 +48,52 @@ Window { // Explicitly center in order to avoid warnings at shutdown anchors.centerIn = parent; } - + + function goCard(card) { + addressLine.text = card.userStory.name; + toggleOrGo(); + } + AddressBarDialog { id: addressBarDialog implicitWidth: backgroundImage.width implicitHeight: backgroundImage.height + Column { + width: backgroundImage.width; + anchors { + left: backgroundImage.left; + leftMargin: backgroundImage.height + 2 * hifi.layout.spacing + bottom: backgroundImage.top; + bottomMargin: 2 * hifi.layout.spacing; + } + Text { + text: "Suggestions" + } + Row { + Card { + id: s0; + width: 200; + height: 200; + goFunction: goCard + } + HifiControls.HorizontalSpacer { } + Card { + id: s1; + width: 200; + height: 200; + goFunction: goCard + } + HifiControls.HorizontalSpacer { } + Card { + id: s2; + width: 200; + height: 200; + goFunction: goCard + } + } + } + Image { id: backgroundImage source: "../images/address-bar.svg" @@ -135,9 +180,124 @@ Window { } } + function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. + // TODO: make available to other .qml. + var request = new XMLHttpRequest(); + // QT bug: apparently doesn't handle onload. Workaround using readyState. + request.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (request.readyState >= READY_STATE_DONE) { + var error = (request.status !== HTTP_OK) && request.status.toString() + ':' + request.statusText, + response = !error && request.responseText, + contentType = !error && request.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + cb(error, response); + } + }; + request.open("GET", url, true); + request.send(); + } + // call iterator(element, icb) once for each element of array, and then cb(error) when icb(error) has been called by each iterator. + // short-circuits if error. Note that iterator MUST be an asynchronous function. (Use setTimeout if necessary.) + function asyncEach(array, iterator, cb) { + var count = array.length; + function icb(error) { + if (!--count || error) { + count = -1; // don't cb multiple times (e.g., if error) + cb(error); + } + } + if (!count) { + return cb(); + } + array.forEach(function (element) { + iterator(element, icb); + }); + } + + function addPictureToDomain(domainInfo, cb) { // asynchronously add thumbnail and lobby to domainInfo, if available, and cb(error) + asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(function (x) { return x; }), function (name, icb) { + var url = "https://metaverse.highfidelity.com/api/v1/places/" + name; + getRequest(url, function (error, json) { + var previews = !error && json.data.place.previews; + if (previews) { + if (!domainInfo.thumbnail) { // just grab tghe first one + domainInfo.thumbnail = previews.thumbnail; + } + if (!domainInfo.lobby) { + domainInfo.lobby = previews.lobby; + } + } + icb(error); + }); + }, cb); + } + + function getDomains(options, cb) { // cb(error, arrayOfData) + if (!options.page) { + options.page = 1; + } + // FIXME: really want places I'm allowed in, not just open ones + var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&active&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; + getRequest(url, function (error, json) { + if (!error && (json.status !== 'success')) { + error = new Error("Bad response: " + JSON.stringify(json)); + } + if (error) { + error.message += ' for ' + url; + return cb(error); + } + var domains = json.data.domains; + asyncEach(domains, addPictureToDomain, function (error) { + if (json.current_page < json.total_pages) { + options.page++; + return getDomains(options, function (error, others) { + cb(error, domains.concat(others)); + }); + } + cb(null, domains); + }); + }); + } + + function fillDestinations () { + function fill1(target, data) { + if (!data) { + return; + } + console.log('suggestion:', JSON.stringify(data)); + target.userStory = data; + if (data.lobby) { + console.log('target', target); + console.log('target.image', target.image); + console.log('url', data.lobby); + target.image.source = data.lobby; + } + target.placeText = data.name; + target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); + } + getDomains({minUsers: 0, maxUsers: 20}, function (e, d) { + var withLobby = !e && d.filter(function (d1) { return d1.lobby; }); + console.log(e, d.length, withLobby.length); + withLobby.sort(function (a, b) { return b.online_users - a.online_users; }); + console.log(withLobby.map(function (d) { return d.online_users; })); + fill1(s0, withLobby[0]); + fill1(s1, withLobby[1]); + fill1(s2, withLobby[2]); + }); + } + onVisibleChanged: { if (visible) { addressLine.forceActiveFocus() + fillDestinations(); } else { addressLine.text = "" } diff --git a/interface/resources/qml/controls-uit/HorizontalSpacer.qml b/interface/resources/qml/controls-uit/HorizontalSpacer.qml new file mode 100644 index 0000000000..a9fac2bcf7 --- /dev/null +++ b/interface/resources/qml/controls-uit/HorizontalSpacer.qml @@ -0,0 +1,19 @@ +// +// HorizontalSpacer.qml +// +// Created by Howard Stearns on 12 July 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +import "../styles-uit" + +Item { + HifiConstants { id: hifi } + height: 1 // Must be non-zero + width: hifi.dimensions.controlInterlineHeight +} diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml new file mode 100644 index 0000000000..342c469303 --- /dev/null +++ b/interface/resources/qml/hifi/Card.qml @@ -0,0 +1,64 @@ +// +// Card.qml +// qml/hifi +// +// Displays a clickable card representing a user story or destination. +// +// Created by Howard Stearns on 7/13/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.5 +import "../styles-uit" + +Rectangle { + property var goFunction: null; + property var userStory: null; + property alias image: lobby; + property alias placeText: place.text; + property alias usersText: users.text; + HifiConstants { id: hifi } + Image { + id: lobby; + width: parent.width; + height: parent.height; + fillMode: Image.PreserveAspectCrop; + // source gets filled in later + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + onStatusChanged: { + if (status == Image.Error) { + console.log("source: " + source + ": failed to load " + JSON.stringify(userStory)); + // FIXME: let's get our own + source = "http://www.davidluke.com/wp-content/themes/david-luke/media/ims/placeholder720.gif" + } + } + } + RalewaySemiBold { + id: place; + color: hifi.colors.lightGrayText; + anchors { + leftMargin: 2 * hifi.layout.spacing; + topMargin: 2 * hifi.layout.spacing; + } + } + RalewayRegular { + id: users; + color: hifi.colors.lightGrayText; + anchors { + bottom: parent.bottom; + right: parent.right; + bottomMargin: 2 * hifi.layout.spacing; + rightMargin: 2 * hifi.layout.spacing; + } + } + MouseArea { + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + onClicked: goFunction(parent); + } +} From d84c7524bf76efd09a213d9557573d22232aa70b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 14 Jul 2016 14:56:58 -0700 Subject: [PATCH 1133/1237] Remove global HRTF headroom. The initial HRTF reduced overall gain by -6dB to avoid clipping at spectral peaks. With the addition of a peak limiter, this is no longer necessary. Changing to 0dB improves the loudness match between spatialized and unspatialized sounds. --- libraries/audio/src/AudioHRTF.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index 111409f80c..63d1712980 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -21,7 +21,7 @@ static const int HRTF_TABLES = 25; // number of HRTF subjects static const int HRTF_DELAY = 24; // max ITD in samples (1.0ms at 24KHz) static const int HRTF_BLOCK = 256; // block processing size -static const float HRTF_GAIN = 0.5f; // HRTF global gain adjustment +static const float HRTF_GAIN = 1.0f; // HRTF global gain adjustment class AudioHRTF { From 1c2d13d1eea38b5d88630decb110250fc8c79b37 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 14 Jul 2016 15:38:22 -0700 Subject: [PATCH 1134/1237] Don't show where you are now. --- interface/resources/qml/AddressBarDialog.qml | 8 ++++---- interface/src/ui/AddressBarDialog.cpp | 4 ++++ interface/src/ui/AddressBarDialog.h | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 8ac199b326..fe5c1d8030 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -283,11 +283,11 @@ Window { target.placeText = data.name; target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); } - getDomains({minUsers: 0, maxUsers: 20}, function (e, d) { - var withLobby = !e && d.filter(function (d1) { return d1.lobby; }); - console.log(e, d.length, withLobby.length); + getDomains({minUsers: 0, maxUsers: 20}, function (error, domains) { + var here = addressBarDialog.getHost(); // don't show where we are now. + var withLobby = !error && domains.filter(function (domain) { return domain.lobby && (domain.name !== here); }); + console.log(error, domains.length, withLobby.length); withLobby.sort(function (a, b) { return b.online_users - a.online_users; }); - console.log(withLobby.map(function (d) { return d.online_users; })); fill1(s0, withLobby[0]); fill1(s1, withLobby[1]); fill1(s2, withLobby[2]); diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index 6fb437e312..7a32381db3 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -40,6 +40,10 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare _forwardEnabled = !(DependencyManager::get()->getForwardStack().isEmpty()); } +QString AddressBarDialog::getHost() const { + return DependencyManager::get()->getHost(); +} + void AddressBarDialog::loadAddress(const QString& address) { qDebug() << "Called LoadAddress with address " << address; if (!address.isEmpty()) { diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index bbce52c67c..b5a21dd47a 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -34,6 +34,7 @@ protected: void displayAddressOfflineMessage(); void displayAddressNotFoundMessage(); + Q_INVOKABLE QString getHost() const; Q_INVOKABLE void loadAddress(const QString& address); Q_INVOKABLE void loadHome(); Q_INVOKABLE void loadBack(); From 776bcb6655e3842e39491b1d4eae4d1c130b75f0 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 14 Jul 2016 15:49:49 -0700 Subject: [PATCH 1135/1237] FIxing the web entity rendering --- libraries/render-utils/src/DeferredBufferWrite.slh | 4 ++-- libraries/render-utils/src/model_unlit.slf | 2 +- libraries/render/src/render/Task.h | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index dece8e9ce3..9adb7948df 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -70,8 +70,8 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { } _fragColor0 = vec4(color, packUnlit()); _fragColor1 = vec4(packNormal(normal), 1.0); - _fragColor2 = vec4(vec3(0.0), 1.0); - _fragColor3 = vec4(color * isUnlitEnabled(), 1.0); + // _fragColor2 = vec4(vec3(0.0), 1.0); + _fragColor3 = vec4(color, 1.0); } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, vec3 fresnel, float roughness) { diff --git a/libraries/render-utils/src/model_unlit.slf b/libraries/render-utils/src/model_unlit.slf index 3c2e4ae2e0..c64f9a07d7 100644 --- a/libraries/render-utils/src/model_unlit.slf +++ b/libraries/render-utils/src/model_unlit.slf @@ -40,5 +40,5 @@ void main(void) { packDeferredFragmentUnlit( normalize(_normal), opacity, - albedo); + albedo * isUnlitEnabled()); } diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 6a7b04198f..cc38830498 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -572,6 +572,12 @@ public: auto children = _config->children(); for (auto& child : children) { child->setParent(config.get()); + QObject::connect(child, SIGNAL(loaded()), config.get(), SLOT(refresh())); + static const char* DIRTY_SIGNAL = "dirty()"; + if (child->metaObject()->indexOfSignal(DIRTY_SIGNAL) != -1) { + // Connect dirty->refresh if defined + QObject::connect(child, SIGNAL(dirty()), config.get(), SLOT(refresh())); + } } } _config = config; @@ -588,6 +594,7 @@ public: void configure(const QObject& configuration) { for (auto& job : _jobs) { job.applyConfiguration(); + } } From 6094fb3de3eb90e40d8e137bf20e0e0cacb14f1e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Jul 2016 15:51:05 -0700 Subject: [PATCH 1136/1237] Remember object attach point offsets in user settings --- .../system/controllers/handControllerGrab.js | 76 +++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index c78cf4dbbb..fef58d28b4 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -197,7 +197,8 @@ CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { CONTROLLER_STATE_MACHINE[STATE_HOLD] = { name: "hold", enterMethod: "nearGrabbingEnter", - updateMethod: "nearGrabbing" + updateMethod: "nearGrabbing", + exitMethod: "holdExit" }; CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { name: "trigger", @@ -257,6 +258,48 @@ function propsArePhysical(props) { return isPhysical; } +var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; +function getAttachPointSettings() { + try { + var str = Settings.getValue(ATTACH_POINT_SETTINGS); + print("getAttachPointSettings = " + str); + if (str === "false") { + return {}; + } else { + return JSON.parse(str); + } + } catch (err) { + print("Error parsing attachPointSettings: " + err); + return {}; + } +} +function setAttachPointSettings(attachPointSettings) { + var str = JSON.stringify(attachPointSettings); + print("setAttachPointSettings = " + str); + Settings.setValue(ATTACH_POINT_SETTINGS, str); +} +function getAttachPointForHotspotFromSettings(hotspot, hand) { + var attachPointSettings = getAttachPointSettings(); + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = attachPointSettings[hotspot.key]; + if (joints) { + return joints[jointName]; + } else { + return undefined; + } +} +function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) { + var attachPointSettings = getAttachPointSettings(); + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = attachPointSettings[hotspot.key]; + if (!joints) { + joints = {}; + attachPointSettings[hotspot.key] = joints; + } + joints[jointName] = [offsetPosition, offsetRotation]; + setAttachPointSettings(attachPointSettings); +} + // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; @@ -1798,11 +1841,18 @@ function MyController(hand) { // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (this.grabbedHotspot.joints[handJointName]) { - this.offsetPosition = this.grabbedHotspot.joints[handJointName][0]; - this.offsetRotation = this.grabbedHotspot.joints[handJointName][1]; + var offsets = getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand); + if (offsets) { + this.offsetPosition = offsets[0]; + this.offsetRotation = offsets[1]; hasPresetPosition = true; + } else { + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (this.grabbedHotspot.joints[handJointName]) { + this.offsetPosition = this.grabbedHotspot.joints[handJointName][0]; + this.offsetRotation = this.grabbedHotspot.joints[handJointName][1]; + hasPresetPosition = true; + } } } else { this.ignoreIK = false; @@ -1996,6 +2046,22 @@ function MyController(hand) { } }; + this.holdExit = function () { + // store the offset attach points into preferences. + if (this.grabbedHotspot) { + entityPropertiesCache.addEntity(this.grabbedEntity); + var props = entityPropertiesCache.getProps(this.grabbedEntity); + var entityXform = new Xform(props.rotation, props.position); + var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); + var handRot = (this.hand === RIGHT_HAND) ? MyAvatar.getRightPalmRotation() : MyAvatar.getLeftPalmRotation(); + var avatarHandPos = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; + var palmXform = new Xform(handRot, avatarXform.xformPoint(avatarHandPos)); + var offsetXform = Xform.mul(palmXform.inv(), entityXform); + + storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, offsetXform.pos, offsetXform.rot); + } + }; + this.nearTriggerEnter = function () { this.clearEquipHaptics(); From 37d60c6c8575b3423c74755c320782ebc4fd9e38 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Jul 2016 15:58:13 -0700 Subject: [PATCH 1137/1237] eslint clean --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 93c8dbde76..d6bf5cdd5f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -2354,7 +2354,7 @@ function MyController(hand) { delayedDeactivateTimeout = null; _this.delayedDeactivateEntity(delayedEntityID, delayedCollidesWith); return delayedCollidesWith; - } + }; delayedDeactivateTimeout = Script.setTimeout(delayedDeactivateFunc, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY * MSECS_PER_SEC); delayedDeactivateEntityID = entityID; From aa70a38e79abe5204740dcce382a945c50227811 Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Thu, 14 Jul 2016 15:59:47 -0700 Subject: [PATCH 1138/1237] Removed button color declarations to make icons visible. --- scripts/tutorials/createDice.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/scripts/tutorials/createDice.js b/scripts/tutorials/createDice.js index 00ec1184bc..8975578c66 100644 --- a/scripts/tutorials/createDice.js +++ b/scripts/tutorials/createDice.js @@ -46,11 +46,6 @@ var offButton = toolBar.addOverlay("image", { width: BUTTON_SIZE, height: BUTTON_SIZE, imageURL: "http://hifi-production.s3.amazonaws.com/tutorials/dice/close.png", - color: { - red: 255, - green: 255, - blue: 255 - }, alpha: 1 }); @@ -60,11 +55,6 @@ var deleteButton = toolBar.addOverlay("image", { width: BUTTON_SIZE, height: BUTTON_SIZE, imageURL: "http://hifi-production.s3.amazonaws.com/tutorials/dice/delete.png", - color: { - red: 255, - green: 255, - blue: 255 - }, alpha: 1 }); @@ -75,11 +65,6 @@ var diceButton = toolBar.addOverlay("image", { width: BUTTON_SIZE, height: BUTTON_SIZE, imageURL: diceIconURL, - color: { - red: 255, - green: 255, - blue: 255 - }, alpha: 1 }); From c45f810622e356412b08a7bea66fb3a4f385619d Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 14 Jul 2016 16:43:56 -0700 Subject: [PATCH 1139/1237] Fixing the mac build --- libraries/render/src/render/Task.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index cc38830498..d5b41e539e 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -520,7 +520,7 @@ public: Model(const Varying& input, A&&... args) : Concept(nullptr), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { // Recreate the Config to use the templated type - _data.createConfiguration(); + _data.template createConfiguration(); _config = _data.getConfiguration(); applyConfiguration(); } From d69be5e4d281bf7f8177a0ed5cfedfa80fc3b109 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 14 Jul 2016 16:45:56 -0700 Subject: [PATCH 1140/1237] Fixing the default value for the lightingMOdel flag when using debugDeferredLighting --- libraries/render-utils/src/DeferredLightingEffect.cpp | 2 +- libraries/render-utils/src/model_translucent_unlit.slf | 3 ++- .../developer/utilities/render/deferredLighting.qml | 10 ++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 1299cb428f..4f5544bab3 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -612,7 +612,7 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, for (auto lightID : deferredLightingEffect->_spotLights) { auto light = deferredLightingEffect->_allocatedLights[lightID]; // IN DEBUG: - light->setShowContour(true); + // light->setShowContour(true); batch.setUniformBuffer(deferredLightingEffect->_spotLightLocations->lightBufferUnit, light->getSchemaBuffer()); auto eyeLightPos = eyePoint - light->getPosition(); diff --git a/libraries/render-utils/src/model_translucent_unlit.slf b/libraries/render-utils/src/model_translucent_unlit.slf index e2676636bf..b397cea5aa 100644 --- a/libraries/render-utils/src/model_translucent_unlit.slf +++ b/libraries/render-utils/src/model_translucent_unlit.slf @@ -16,6 +16,7 @@ <@include MaterialTextures.slh@> <$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$> +<@include LightingModel.slh@> in vec2 _texCoord0; in vec3 _color; @@ -35,5 +36,5 @@ void main(void) { <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; albedo *= _color; - _fragColor = vec4(albedo, opacity); + _fragColor = vec4(albedo * isUnlitEnabled(), opacity); } diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 9d92b79fd6..bc34423a9c 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -28,7 +28,7 @@ Column { ] CheckBox { text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1]) + checked: Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } } } @@ -46,7 +46,7 @@ Column { ] CheckBox { text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1]) + checked: Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } } } @@ -63,7 +63,7 @@ Column { ] CheckBox { text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1]) + checked: Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } } } @@ -76,7 +76,7 @@ Column { ] CheckBox { text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1]) + checked: Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } } } @@ -104,6 +104,7 @@ Column { } ComboBox { + anchors.right: root.right currentIndex: 1 model: ListModel { id: cbItems @@ -133,6 +134,7 @@ Column { } ComboBox { + anchors.right: root.right currentIndex: 0 model: ListModel { id: cbItemsFramebuffer From ffdaadf13c946eb8082d9ea39849ff2c71d7d6b4 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 15:48:14 -0700 Subject: [PATCH 1141/1237] Make toolbars work even when debugging a script --- .../src/scripting/ToolbarScriptingInterface.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp index 175a7fd539..0cb314615a 100644 --- a/interface/src/scripting/ToolbarScriptingInterface.cpp +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -8,6 +8,8 @@ #include "ToolbarScriptingInterface.h" +#include + #include class QmlWrapper : public QObject { @@ -79,7 +81,11 @@ public: Q_INVOKABLE QObject* addButton(const QVariant& properties) { QVariant resultVar; - bool invokeResult = QMetaObject::invokeMethod(_qmlObject, "addButton", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, properties)); + Qt::ConnectionType connectionType = Qt::AutoConnection; + if (QThread::currentThread() != _qmlObject->thread()) { + connectionType = Qt::BlockingQueuedConnection; + } + bool invokeResult = QMetaObject::invokeMethod(_qmlObject, "addButton", connectionType, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, properties)); if (!invokeResult) { return nullptr; } @@ -101,8 +107,12 @@ public: QObject* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { auto offscreenUi = DependencyManager::get(); auto desktop = offscreenUi->getDesktop(); + Qt::ConnectionType connectionType = Qt::AutoConnection; + if (QThread::currentThread() != desktop->thread()) { + connectionType = Qt::BlockingQueuedConnection; + } QVariant resultVar; - bool invokeResult = QMetaObject::invokeMethod(desktop, "getToolbar", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, toolbarId)); + bool invokeResult = QMetaObject::invokeMethod(desktop, "getToolbar", connectionType, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, toolbarId)); if (!invokeResult) { return nullptr; } From fdcbf7162395546c9507bc59cd7c6569e7936b1e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 15:49:32 -0700 Subject: [PATCH 1142/1237] Remove fade effect from toolbar buttons --- .../qml/hifi/toolbars/ToolbarButton.qml | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index 4356b18253..47a52c4c36 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -4,7 +4,7 @@ import QtQuick.Controls 1.4 Item { id: button property alias imageURL: image.source - property alias alpha: button.opacity + property alias alpha: image.opacity property var subImage; property int yOffset: 0 property int buttonState: 0 @@ -14,32 +14,11 @@ Item { width: size; height: size property bool pinned: false clip: true - - Behavior on opacity { - NumberAnimation { - duration: 150 - easing.type: Easing.InOutCubic - } - } - - property alias fadeTargetProperty: button.opacity - - onFadeTargetPropertyChanged: { - visible = (fadeTargetProperty !== 0.0); - } - - onVisibleChanged: { - if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { - var target = visible; - visible = !visible; - fadeTargetProperty = target ? 1.0 : 0.0; - return; - } - } - + function updateOffset() { yOffset = size * (buttonState + hoverOffset); } + onButtonStateChanged: { hoverOffset = 0; // subtle: show the new state without hover. don't wait for mouse to be moved away // The above is per UX design, but ALSO avoid a subtle issue that would be a problem because @@ -64,11 +43,19 @@ Item { width: parent.width } + Timer { + id: asyncClickSender + interval: 10 + repeat: false + running: false + onTriggered: button.clicked(); + } + MouseArea { id: mouseArea hoverEnabled: true anchors.fill: parent - onClicked: button.clicked(); + onClicked: asyncClickSender.start(); onEntered: { hoverOffset = 2; updateOffset(); From 1adf96c8df1d9ffde4d1708d45b22a46f965eff9 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 15:50:40 -0700 Subject: [PATCH 1143/1237] Move edit.js to new toolbar API --- scripts/system/edit.js | 439 +++++++++++++---------------------------- 1 file changed, 132 insertions(+), 307 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 4e87fcbf71..47d6fd4367 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -16,7 +16,6 @@ HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; Script.include([ "libraries/stringHelpers.js", "libraries/dataViewHelpers.js", - "libraries/toolBars.js", "libraries/progressDialog.js", "libraries/entitySelectionTool.js", @@ -50,11 +49,6 @@ selectionManager.addEventListener(function() { lightOverlayManager.updatePositions(); }); -var toolIconUrl = Script.resolvePath("assets/images/tools/"); -var toolHeight = 50; -var toolWidth = 50; -var TOOLBAR_MARGIN_Y = 0; - var DEGREES_TO_RADIANS = Math.PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / Math.PI; var epsilon = 0.001; @@ -104,6 +98,7 @@ IMPORTING_SVO_OVERLAY_WIDTH = 144; IMPORTING_SVO_OVERLAY_HEIGHT = 30; IMPORTING_SVO_OVERLAY_MARGIN = 5; IMPORTING_SVO_OVERLAY_LEFT_MARGIN = 34; + var importingSVOImageOverlay = Overlays.addOverlay("image", { imageURL: Script.resolvePath("assets") + "/images/hourglass.svg", width: 20, @@ -168,241 +163,19 @@ function toggleMarketplace() { } var toolBar = (function() { + var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts + var TOOL_ICON_URL = Script.resolvePath("assets/images/tools/"); var that = {}, toolBar, - activeButton, - newModelButton, - newCubeButton, - newSphereButton, - newLightButton, - newTextButton, - newWebButton, - newZoneButton, - newParticleButton - - function initialize() { - toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.edit.toolbar", function(windowDimensions, toolbar) { - return { - x: (windowDimensions.x / 2) + (Tool.IMAGE_WIDTH * 2), - y: windowDimensions.y - }; - }, { - x: toolWidth, - y: -TOOLBAR_MARGIN_Y - toolHeight - }); - - activeButton = toolBar.addTool({ - imageURL: toolIconUrl + "edit.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - visible: true - }, true, false); - - newModelButton = toolBar.addTool({ - imageURL: toolIconUrl + "model-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - newCubeButton = toolBar.addTool({ - imageURL: toolIconUrl + "cube-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - newSphereButton = toolBar.addTool({ - imageURL: toolIconUrl + "sphere-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - newLightButton = toolBar.addTool({ - imageURL: toolIconUrl + "light-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - newTextButton = toolBar.addTool({ - imageURL: toolIconUrl + "text-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - newWebButton = toolBar.addTool({ - imageURL: toolIconUrl + "web-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - newZoneButton = toolBar.addTool({ - imageURL: toolIconUrl + "zone-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - newParticleButton = toolBar.addTool({ - imageURL: toolIconUrl + "particle-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - showButtonDown: true, - visible: false - }); - - that.setActive(false); - } - - that.clearEntityList = function() { - entityListTool.clearEntityList(); - }; - - var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts - Settings.setValue(EDIT_SETTING, false); - that.setActive = function(active) { - if (active != isActive) { - if (active && !Entities.canRez() && !Entities.canRezTmp()) { - Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG); - } else { - Messages.sendLocalMessage("edit-events", JSON.stringify({ - enabled: active - })); - isActive = active; - Settings.setValue(EDIT_SETTING, active); - if (!isActive) { - entityListTool.setVisible(false); - gridTool.setVisible(false); - grid.setEnabled(false); - propertiesTool.setVisible(false); - selectionManager.clearSelections(); - cameraManager.disable(); - selectionDisplay.triggerMapping.disable(); - } else { - UserActivityLogger.enabledEdit(); - hasShownPropertiesTool = false; - entityListTool.setVisible(true); - gridTool.setVisible(true); - grid.setEnabled(true); - propertiesTool.setVisible(true); - // Not sure what the following was meant to accomplish, but it currently causes - selectionDisplay.triggerMapping.enable(); - // everybody else to think that Interface has lost focus overall. fogbugzid:558 - // Window.setFocus(); - } - that.showTools(isActive); - } - } - toolBar.selectTool(activeButton, isActive); - lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); - Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); - }; - - // Sets visibility of tool buttons, excluding the power button - that.showTools = function(doShow) { - toolBar.showTool(newModelButton, doShow); - toolBar.showTool(newCubeButton, doShow); - toolBar.showTool(newSphereButton, doShow); - toolBar.showTool(newLightButton, doShow); - toolBar.showTool(newTextButton, doShow); - toolBar.showTool(newWebButton, doShow); - toolBar.showTool(newZoneButton, doShow); - toolBar.showTool(newParticleButton, doShow); - }; - - var RESIZE_INTERVAL = 50; - var RESIZE_TIMEOUT = 120000; // 2 minutes - var RESIZE_MAX_CHECKS = RESIZE_TIMEOUT / RESIZE_INTERVAL; - - function addModel(url) { - var entityID = createNewEntity({ - type: "Model", - modelURL: url - }, false); - - if (entityID) { - print("Model added: " + url); - selectionManager.setSelections([entityID]); - } - } - + systemToolbar, + activeButton; + function createNewEntity(properties, dragOnCreate) { + Settings.setValue(EDIT_SETTING, false); + // Default to true if not passed in - dragOnCreate = dragOnCreate == undefined ? true : dragOnCreate; + // dragOnCreate = dragOnCreate == undefined ? true : dragOnCreate; + dragOnCreate = false; var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); @@ -426,35 +199,69 @@ var toolBar = (function() { return entityID; } - that.mousePressEvent = function(event) { - var clickedOverlay, - url, - file; - - if (!event.isLeftButton) { - // if another mouse button than left is pressed ignore it - return false; + function cleanup() { + that.setActive(false); + } + + function addButton(name, image, handler) { + var imageUrl = TOOL_ICON_URL + image; + var button = toolBar.addButton({ + objectName: name, + imageURL: imageUrl, + buttonState: 1, + alpha: 0.9, + visible: true, + }); + if (handler) { + button.clicked.connect(function(){ + Script.setTimeout(handler, 100); + }); } + return button; + } - clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y + function initialize() { + print("QQQ creating edit toolbar"); + Script.scriptEnding.connect(cleanup); + + Window.domainChanged.connect(function() { + that.setActive(false); + that.clearEntityList(); }); - if (activeButton === toolBar.clicked(clickedOverlay)) { - that.setActive(!isActive); - return true; - } - - if (newModelButton === toolBar.clicked(clickedOverlay)) { - url = Window.prompt("Model URL"); - if (url !== null && url !== "") { - addModel(url); + Entities.canAdjustLocksChanged.connect(function(canAdjustLocks) { + if (isActive && !canAdjustLocks) { + that.setActive(false); } - return true; - } + }); - if (newCubeButton === toolBar.clicked(clickedOverlay)) { + systemToolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + activeButton = systemToolbar.addButton({ + objectName: "activeButton", + imageURL: TOOL_ICON_URL + "edit.svg", + visible: true, + buttonState: 1, + alpha: 0.9, + }); + activeButton.clicked.connect(function(){ + that.setActive(!isActive); + activeButton.writeProperty("buttonState", isActive ? 0 : 1); + }); + + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.edit"); + toolBar.writeProperty("shown", false); + + addButton("newModelButton", "model-01.svg", function(){ + var url = Window.prompt("Model URL"); + if (url !== null && url !== "") { + createNewEntity({ + type: "Model", + modelURL: url + }, false); + } + }); + + addButton("newCubeButton", "cube-01.svg", function(){ createNewEntity({ type: "Box", dimensions: DEFAULT_DIMENSIONS, @@ -464,11 +271,9 @@ var toolBar = (function() { blue: 0 } }); + }); - return true; - } - - if (newSphereButton === toolBar.clicked(clickedOverlay)) { + addButton("newSphereButton", "sphere-01.svg", function(){ createNewEntity({ type: "Sphere", dimensions: DEFAULT_DIMENSIONS, @@ -478,11 +283,9 @@ var toolBar = (function() { blue: 0 } }); + }); - return true; - } - - if (newLightButton === toolBar.clicked(clickedOverlay)) { + addButton("newLightButton", "light-01.svg", function(){ createNewEntity({ type: "Light", dimensions: DEFAULT_LIGHT_DIMENSIONS, @@ -499,11 +302,9 @@ var toolBar = (function() { exponent: 0, cutoff: 180, // in degrees }); + }); - return true; - } - - if (newTextButton === toolBar.clicked(clickedOverlay)) { + addButton("newTextButton", "text-01.svg", function(){ createNewEntity({ type: "Text", dimensions: { @@ -524,11 +325,9 @@ var toolBar = (function() { text: "some text", lineHeight: 0.06 }); + }); - return true; - } - - if (newWebButton === toolBar.clicked(clickedOverlay)) { + addButton("newWebButton", "web-01.svg", function(){ createNewEntity({ type: "Web", dimensions: { @@ -538,11 +337,9 @@ var toolBar = (function() { }, sourceUrl: "https://highfidelity.com/", }); + }); - return true; - } - - if (newZoneButton === toolBar.clicked(clickedOverlay)) { + addButton("newZoneButton", "zone-01.svg", function(){ createNewEntity({ type: "Zone", dimensions: { @@ -551,11 +348,9 @@ var toolBar = (function() { z: 10 }, }); + }); - return true; - } - - if (newParticleButton === toolBar.clicked(clickedOverlay)) { + addButton("newParticleButton", "particle-01.svg", function(){ createNewEntity({ type: "ParticleEffect", isEmitting: true, @@ -577,33 +372,65 @@ var toolBar = (function() { emitRate: 100, textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png", }); - } + }); - return false; + that.setActive(false); + } + + that.clearEntityList = function() { + entityListTool.clearEntityList(); }; - that.mouseReleaseEvent = function(event) { - return false; - } - - Window.domainChanged.connect(function() { - that.setActive(false); - that.clearEntityList(); - }); - - Entities.canAdjustLocksChanged.connect(function(canAdjustLocks) { - if (isActive && !canAdjustLocks) { - that.setActive(false); + that.setActive = function(active) { + if (active == isActive) { + return; } - }); - - that.cleanup = function() { - toolBar.cleanup(); + if (active && !Entities.canRez() && !Entities.canRezTmp()) { + Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG); + return; + } + Messages.sendLocalMessage("edit-events", JSON.stringify({ + enabled: active + })); + isActive = active; + Settings.setValue(EDIT_SETTING, active); + if (!isActive) { + entityListTool.setVisible(false); + gridTool.setVisible(false); + grid.setEnabled(false); + propertiesTool.setVisible(false); + selectionManager.clearSelections(); + cameraManager.disable(); + selectionDisplay.triggerMapping.disable(); + } else { + UserActivityLogger.enabledEdit(); + hasShownPropertiesTool = false; + entityListTool.setVisible(true); + gridTool.setVisible(true); + grid.setEnabled(true); + propertiesTool.setVisible(true); + // Not sure what the following was meant to accomplish, but it currently causes + selectionDisplay.triggerMapping.enable(); + // everybody else to think that Interface has lost focus overall. fogbugzid:558 + // Window.setFocus(); + } + // Sets visibility of tool buttons, excluding the power button + toolBar.writeProperty("shown", active); + var visible = toolBar.readProperty("visible"); + if (active && !visible) { + toolBar.writeProperty("visible", false); + toolBar.writeProperty("shown", false); + toolBar.writeProperty("visible", true); + toolBar.writeProperty("shown", true); + } + //toolBar.selectTool(activeButton, isActive); + lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; initialize(); return that; -}()); +})(); function isLocked(properties) { @@ -712,7 +539,7 @@ function mousePressEvent(event) { mouseHasMovedSincePress = false; mouseCapturedByTool = false; - if (propertyMenu.mousePressEvent(event) || toolBar.mousePressEvent(event) || progressDialog.mousePressEvent(event)) { + if (propertyMenu.mousePressEvent(event) || progressDialog.mousePressEvent(event)) { mouseCapturedByTool = true; return; } @@ -796,8 +623,7 @@ function mouseReleaseEvent(event) { mouseMove(lastMouseMoveEvent); lastMouseMoveEvent = null; } - if (propertyMenu.mouseReleaseEvent(event) || toolBar.mouseReleaseEvent(event)) { - + if (propertyMenu.mouseReleaseEvent(event)) { return true; } if (placingEntityID) { @@ -1094,7 +920,6 @@ Script.scriptEnding.connect(function() { Settings.setValue(SETTING_SHOW_ZONES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); progressDialog.cleanup(); - toolBar.cleanup(); cleanupModelMenus(); tooltip.cleanup(); selectionDisplay.cleanup(); From a6d71f508ea6ad11d21e84b72d2b2fb5842d24c0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 17:20:08 -0700 Subject: [PATCH 1144/1237] Fix edit toolbar button vanishing, persistent edit button --- .../resources/qml/hifi/toolbars/Toolbar.qml | 3 +-- .../qml/hifi/toolbars/ToolbarButton.qml | 25 ++++++++----------- scripts/system/edit.js | 6 ++--- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index c5c15a6406..4e7eef62d5 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -19,7 +19,6 @@ Window { shown: true width: content.width height: content.height - visible: true // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' activator: MouseArea { width: frame.decoration ? frame.decoration.width : window.width @@ -38,7 +37,7 @@ Window { property real buttonSize: 50; property var buttons: [] property var container: horizontal ? row : column - + Settings { category: "toolbar/" + window.objectName property alias x: window.x diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index 47a52c4c36..f4693adec5 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -8,23 +8,16 @@ Item { property var subImage; property int yOffset: 0 property int buttonState: 0 - property int hoverOffset: 0 + property int hoverState: -1 + property int defaultState: -1 property var toolbar; property real size: 50 // toolbar ? toolbar.buttonSize : 50 width: size; height: size property bool pinned: false clip: true - - function updateOffset() { - yOffset = size * (buttonState + hoverOffset); - } - + onButtonStateChanged: { - hoverOffset = 0; // subtle: show the new state without hover. don't wait for mouse to be moved away - // The above is per UX design, but ALSO avoid a subtle issue that would be a problem because - // the hand controllers don't move the mouse when not triggered, so releasing the trigger would - // never show unhovered. - updateOffset(); + yOffset = size * buttonState; } Component.onCompleted: { @@ -57,12 +50,14 @@ Item { anchors.fill: parent onClicked: asyncClickSender.start(); onEntered: { - hoverOffset = 2; - updateOffset(); + if (hoverState > 0) { + buttonState = hoverState; + } } onExited: { - hoverOffset = 0; - updateOffset(); + if (defaultState > 0) { + buttonState = defaultState; + } } } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 47d6fd4367..883a804e4b 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -201,6 +201,7 @@ var toolBar = (function() { function cleanup() { that.setActive(false); + systemToolbar.removeButton("com.highfidelity.interface.system.editButton"); } function addButton(name, image, handler) { @@ -237,11 +238,10 @@ var toolBar = (function() { systemToolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); activeButton = systemToolbar.addButton({ - objectName: "activeButton", + objectName: "com.highfidelity.interface.system.editButton", imageURL: TOOL_ICON_URL + "edit.svg", visible: true, buttonState: 1, - alpha: 0.9, }); activeButton.clicked.connect(function(){ that.setActive(!isActive); @@ -418,9 +418,7 @@ var toolBar = (function() { toolBar.writeProperty("shown", active); var visible = toolBar.readProperty("visible"); if (active && !visible) { - toolBar.writeProperty("visible", false); toolBar.writeProperty("shown", false); - toolBar.writeProperty("visible", true); toolBar.writeProperty("shown", true); } //toolBar.selectTool(activeButton, isActive); From 43e045c22acae96d64f453dd74cd0d1a3f932a6b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 17:45:49 -0700 Subject: [PATCH 1145/1237] Restore mouseover events for items in windows --- interface/resources/qml/hifi/toolbars/Toolbar.qml | 14 +------------- interface/resources/qml/windows/Decoration.qml | 8 ++++++-- interface/resources/qml/windows/Window.qml | 4 ---- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 4e7eef62d5..30989be688 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -20,19 +20,7 @@ Window { width: content.width height: content.height // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' - activator: MouseArea { - width: frame.decoration ? frame.decoration.width : window.width - height: frame.decoration ? frame.decoration.height : window.height - x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 - y: frame.decoration ? frame.decoration.anchors.topMargin : 0 - propagateComposedEvents: true - acceptedButtons: Qt.AllButtons - enabled: window.visible - hoverEnabled: true - onPressed: mouse.accepted = false; - onEntered: window.mouseEntered(); - onExited: window.mouseExited(); - } + activator: Item {} property bool horizontal: true property real buttonSize: 50; property var buttons: [] diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml index edfb369c0f..0048c556eb 100644 --- a/interface/resources/qml/windows/Decoration.qml +++ b/interface/resources/qml/windows/Decoration.qml @@ -43,15 +43,19 @@ Rectangle { // Enable dragging of the window, // detect mouseover of the window (including decoration) MouseArea { + id: decorationMouseArea anchors.fill: parent drag.target: window + hoverEnabled: true + onEntered: window.mouseEntered(); + onExited: window.mouseExited(); } Connections { target: window onMouseEntered: { if (desktop.hmdHandMouseActive) { root.inflateDecorations() - } + } } onMouseExited: root.deflateDecorations(); } @@ -59,7 +63,7 @@ Rectangle { target: desktop onHmdHandMouseActiveChanged: { if (desktop.hmdHandMouseActive) { - if (window.activator.containsMouse) { + if (decorationMouseArea.containsMouse) { root.inflateDecorations(); } } else { diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index ca37c55f4d..c873872692 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -115,14 +115,10 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible - hoverEnabled: true onPressed: { - //console.log("Pressed on activator area"); window.raise(); mouse.accepted = false; } - onEntered: window.mouseEntered(); - onExited: window.mouseExited(); } // This mouse area serves to swallow mouse events while the mouse is over the window From cd6abadc41fb9531477724e22e79b55ff20e4be7 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Jul 2016 17:37:14 -0700 Subject: [PATCH 1146/1237] Make sure buttons get removed when shutting down scripts --- scripts/system/examples.js | 1 + scripts/system/goto.js | 1 + scripts/system/hmd.js | 1 + scripts/system/mute.js | 1 + 4 files changed, 4 insertions(+) diff --git a/scripts/system/examples.js b/scripts/system/examples.js index 0ec2644f9c..cea176f6b1 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -69,6 +69,7 @@ browseExamplesButton.clicked.connect(onClick); examplesWindow.visibleChanged.connect(onExamplesWindowVisibilityChanged); Script.scriptEnding.connect(function () { + toolBar.removeButton("examples"); browseExamplesButton.clicked.disconnect(onClick); examplesWindow.visibleChanged.disconnect(onExamplesWindowVisibilityChanged); }); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 24c402ab85..9cdf579d72 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -30,6 +30,7 @@ button.clicked.connect(onClicked); DialogsManager.addressBarShown.connect(onAddressBarShown); Script.scriptEnding.connect(function () { + toolBar.removeButton("goto"); button.clicked.disconnect(onClicked); DialogsManager.addressBarShown.disconnect(onAddressBarShown); }); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 305557b60c..9b749c306f 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -40,6 +40,7 @@ if (headset) { HMD.displayModeChanged.connect(onHmdChanged); Script.scriptEnding.connect(function () { + toolBar.removeButton("hmdToggle"); button.clicked.disconnect(onClicked); HMD.displayModeChanged.disconnect(onHmdChanged); }); diff --git a/scripts/system/mute.js b/scripts/system/mute.js index 1a575efa01..511c013d5e 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -34,6 +34,7 @@ button.clicked.connect(onClicked); AudioDevice.muteToggled.connect(onMuteToggled); Script.scriptEnding.connect(function () { + toolBar.removeButton("mute"); button.clicked.disconnect(onClicked); AudioDevice.muteToggled.disconnect(onMuteToggled); }); From 89ec5471617809473d5d8b91103f10bf35b91a44 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 14 Jul 2016 18:20:20 -0700 Subject: [PATCH 1147/1237] Trying to get the gpu timer to work correctly --- libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp | 11 +++++++---- libraries/gpu-gl/src/gpu/gl/GLQuery.h | 14 ++++++++++---- .../gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp | 2 +- .../gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp | 4 ++-- libraries/gpu/src/gpu/Query.cpp | 2 +- scripts/developer/utilities/render/stats.qml | 18 +++++++++++++++++- 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp index 344f380339..45e0d8d53f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp @@ -18,7 +18,7 @@ void GLBackend::do_beginQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { - glBeginQuery(GL_TIME_ELAPSED, glquery->_qo); + glQueryCounter(glquery->_beginqo, GL_TIMESTAMP); (void)CHECK_GL_ERROR(); } } @@ -27,7 +27,7 @@ void GLBackend::do_endQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { - glEndQuery(GL_TIME_ELAPSED); + glQueryCounter(glquery->_endqo, GL_TIMESTAMP); (void)CHECK_GL_ERROR(); } } @@ -36,9 +36,12 @@ void GLBackend::do_getQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { - glGetQueryObjectui64v(glquery->_qo, GL_QUERY_RESULT_AVAILABLE, &glquery->_result); + glGetQueryObjectui64v(glquery->_endqo, GL_QUERY_RESULT_AVAILABLE, &glquery->_result); if (glquery->_result == GL_TRUE) { - glGetQueryObjectui64v(glquery->_qo, GL_QUERY_RESULT, &glquery->_result); + GLuint64 start, end; + glGetQueryObjectui64v(glquery->_beginqo, GL_QUERY_RESULT, &start); + glGetQueryObjectui64v(glquery->_endqo, GL_QUERY_RESULT, &end); + glquery->_result = end - start; query->triggerReturnHandler(glquery->_result); } (void)CHECK_GL_ERROR(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLQuery.h b/libraries/gpu-gl/src/gpu/gl/GLQuery.h index 7b14b227ab..93f5ab20b3 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLQuery.h +++ b/libraries/gpu-gl/src/gpu/gl/GLQuery.h @@ -41,15 +41,21 @@ public: return 0; } - return object->_qo; + return object->_endqo; } - const GLuint& _qo { _id }; + const GLuint& _endqo = { _id }; + const GLuint _beginqo = { 0 }; GLuint64 _result { (GLuint64)-1 }; protected: - GLQuery(const Query& query, GLuint id) : Parent(query, id) {} - ~GLQuery() { if (_id) { glDeleteQueries(1, &_id); } } + GLQuery(const Query& query, GLuint endId, GLuint beginId) : Parent(query, endId), _beginqo(beginId){} + ~GLQuery() { + if (_id) { + GLuint ids[2] = { _endqo, _beginqo }; + glDeleteQueries(2, ids); + } + } }; } } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp index 478d210535..8cc47a43a4 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp @@ -25,7 +25,7 @@ public: } GL41Query(const Query& query) - : Parent(query, allocateQuery()) { } + : Parent(query, allocateQuery(), allocateQuery()) { } }; gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp index a7a7c26bed..9e808aeb82 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp @@ -19,12 +19,12 @@ class GL45Query : public gpu::gl::GLQuery { public: static GLuint allocateQuery() { GLuint result; - glCreateQueries(GL_TIME_ELAPSED, 1, &result); + glCreateQueries(GL_TIMESTAMP, 1, &result); return result; } GL45Query(const Query& query) - : Parent(query, allocateQuery()) { } + : Parent(query, allocateQuery(), allocateQuery()){} }; gl::GLQuery* GL45Backend::syncGPUObject(const Query& query) { diff --git a/libraries/gpu/src/gpu/Query.cpp b/libraries/gpu/src/gpu/Query.cpp index 2e28dcd061..76c239b1e0 100644 --- a/libraries/gpu/src/gpu/Query.cpp +++ b/libraries/gpu/src/gpu/Query.cpp @@ -25,7 +25,7 @@ Query::~Query() } double Query::getElapsedTime() const { - return ((double) _queryResult) * 0.000001; + return ((double)_queryResult) / 1000000.0; } void Query::triggerReturnHandler(uint64_t queryResult) { diff --git a/scripts/developer/utilities/render/stats.qml b/scripts/developer/utilities/render/stats.qml index 322af5389e..27f7580b57 100644 --- a/scripts/developer/utilities/render/stats.qml +++ b/scripts/developer/utilities/render/stats.qml @@ -241,7 +241,23 @@ Item { color: "#E2334D" } ] - } + } + PlotPerf { + title: "Timing" + height: parent.evalEvenHeight() + object: parent.drawOpaqueConfig + valueUnit: "ms" + valueScale: 1 + valueNumDigits: "4" + plots: [ + { + object: Render.getConfig("RenderDeferredTask"), + prop: "gpuTime", + label: "RenderFrameGPU", + color: "#00FFFF" + } + ] + } } } From 2674df6095382bc6894265e3810c4ceb1aad9824 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 14 Jul 2016 18:34:07 -0700 Subject: [PATCH 1148/1237] Fix the distance-attenuation model (for injectors only) The original attenuation model seems wrong, under-attenuating at close distance but completely muting after 45m. This uses a physics-based model of -6dB per doubling of distance in free space, for the injectors. The AudioMixer model and domain settings still need to be reworked in a future PR. --- libraries/audio-client/src/AudioClient.cpp | 32 +++++----------------- libraries/audio-client/src/AudioClient.h | 2 +- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 50039ba668..84d0146f3b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -885,7 +885,7 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { // calculate distance, gain and azimuth for hrtf glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = gainForSource(relativePosition, injector->getVolume()); + float gain = gainForSource(distance, injector->getVolume()); float azimuth = azimuthForSource(relativePosition); injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); @@ -1299,37 +1299,19 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { } } -float AudioClient::gainForSource(const glm::vec3& relativePosition, float volume) { - // TODO: put these in a place where we can share with AudioMixer! - const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.18f; +float AudioClient::gainForSource(float distance, float volume) { + const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f; - - //qDebug() << "initial gain is " << volume; - // I'm assuming that the AudioMixer's getting of the stream's attenuation // factor is basically same as getting volume float gain = volume; - float distanceBetween = glm::length(relativePosition); - if (distanceBetween < EPSILON ) { - distanceBetween = EPSILON; + + // attenuate based on distance + if (distance >= ATTENUATION_BEGINS_AT_DISTANCE) { + gain /= distance; // attenuation = -6dB * log2(distance) } - // audio mixer has notion of zones. Unsure how to map that across here... - - // attenuate based on distance now - if (distanceBetween >= ATTENUATION_BEGINS_AT_DISTANCE) { - float distanceCoefficient = 1.0f - (logf(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE) / logf(2.0f) - * DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE); - if (distanceCoefficient < 0.0f) { - distanceCoefficient = 0.0f; - } - - gain *= distanceCoefficient; - } - - //qDebug() << "calculated gain as " << gain; - return gain; } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index df7b10ab04..3e4aa931a6 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -217,7 +217,7 @@ private: void outputFormatChanged(); void mixLocalAudioInjectors(int16_t* inputBuffer); float azimuthForSource(const glm::vec3& relativePosition); - float gainForSource(const glm::vec3& relativePosition, float volume); + float gainForSource(float distance, float volume); QByteArray firstInputFrame; QAudioInput* _audioInput; From 22ac95d46396570a0c1dcb549a9788c84fa1b8b6 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 15 Jul 2016 09:12:16 -0700 Subject: [PATCH 1149/1237] REvisiting the RangeTimer to measure the real gpu duration --- .../gpu-gl/src/gpu/gl/GLBackendQuery.cpp | 26 ++++++++--- libraries/gpu/src/gpu/Query.h | 3 ++ .../src/DeferredLightingEffect.cpp | 12 ++++- .../render-utils/src/DeferredLightingEffect.h | 6 +++ .../render-utils/src/RenderDeferredTask.cpp | 34 +++++++++++--- .../render-utils/src/RenderDeferredTask.h | 44 +++++++++++++++++-- .../render-utils/src/SurfaceGeometryPass.cpp | 22 ++++++++-- 7 files changed, 129 insertions(+), 18 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp index 45e0d8d53f..5191b161cf 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp @@ -14,11 +14,17 @@ using namespace gpu; using namespace gpu::gl; +static bool timeElapsed = true; + void GLBackend::do_beginQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { - glQueryCounter(glquery->_beginqo, GL_TIMESTAMP); + if (timeElapsed) { + glBeginQuery(GL_TIME_ELAPSED, glquery->_endqo); + } else { + glQueryCounter(glquery->_beginqo, GL_TIMESTAMP); + } (void)CHECK_GL_ERROR(); } } @@ -27,7 +33,11 @@ void GLBackend::do_endQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { - glQueryCounter(glquery->_endqo, GL_TIMESTAMP); + if (timeElapsed) { + glEndQuery(GL_TIME_ELAPSED); + } else { + glQueryCounter(glquery->_endqo, GL_TIMESTAMP); + } (void)CHECK_GL_ERROR(); } } @@ -38,10 +48,14 @@ void GLBackend::do_getQuery(Batch& batch, size_t paramOffset) { if (glquery) { glGetQueryObjectui64v(glquery->_endqo, GL_QUERY_RESULT_AVAILABLE, &glquery->_result); if (glquery->_result == GL_TRUE) { - GLuint64 start, end; - glGetQueryObjectui64v(glquery->_beginqo, GL_QUERY_RESULT, &start); - glGetQueryObjectui64v(glquery->_endqo, GL_QUERY_RESULT, &end); - glquery->_result = end - start; + if (timeElapsed) { + glGetQueryObjectui64v(glquery->_endqo, GL_QUERY_RESULT, &glquery->_result); + } else { + GLuint64 start, end; + glGetQueryObjectui64v(glquery->_beginqo, GL_QUERY_RESULT, &start); + glGetQueryObjectui64v(glquery->_endqo, GL_QUERY_RESULT, &end); + glquery->_result = end - start; + } query->triggerReturnHandler(glquery->_result); } (void)CHECK_GL_ERROR(); diff --git a/libraries/gpu/src/gpu/Query.h b/libraries/gpu/src/gpu/Query.h index 8e4026fe0f..48b9d0a0d5 100644 --- a/libraries/gpu/src/gpu/Query.h +++ b/libraries/gpu/src/gpu/Query.h @@ -66,6 +66,9 @@ namespace gpu { int rangeIndex(int index) const { return (index % QUERY_QUEUE_SIZE); } }; + + using RangeTimerPointer = std::shared_ptr; + }; #endif diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 4f5544bab3..9d8c1ef200 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -694,12 +694,22 @@ void RenderDeferred::run(const SceneContextPointer& sceneContext, const RenderCo auto surfaceGeometryFramebuffer = inputs.get3(); auto lowCurvatureNormalFramebuffer = inputs.get4(); auto subsurfaceScatteringResource = inputs.get5(); + auto args = renderContext->args; - + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + _gpuTimer.begin(batch); + }); setupJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel, surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer, subsurfaceScatteringResource); lightsJob.run(sceneContext, renderContext, deferredTransform, deferredFramebuffer, lightingModel); cleanupJob.run(sceneContext, renderContext); + + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + _gpuTimer.end(batch); + }); + + auto config = std::static_pointer_cast(renderContext->jobConfig); + config->gpuTime = _gpuTimer.getAverage(); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index ff372aa5b4..b309299be9 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -163,9 +163,14 @@ public: class RenderDeferredConfig : public render::Job::Config { Q_OBJECT + Q_PROPERTY(double gpuTime READ getGpuTime) public: RenderDeferredConfig() : render::Job::Config(true) {} + double getGpuTime() { return gpuTime; } + + double gpuTime{ 0.0 }; + signals: void dirty(); }; @@ -188,6 +193,7 @@ public: RenderDeferredCleanup cleanupJob; protected: + gpu::RangeTimer _gpuTimer; }; #endif // hifi_DeferredLightingEffect_h diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 7959c371f9..6ad0ae4843 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -100,6 +100,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // GPU jobs: Start preparing the primary, deferred and lighting buffer const auto primaryFramebuffer = addJob("PreparePrimaryBuffer"); + const auto prepareDeferredInputs = SurfaceGeometryPass::Inputs(primaryFramebuffer, lightingModel).hasVarying(); const auto prepareDeferredOutputs = addJob("PrepareDeferred", prepareDeferredInputs); const auto deferredFramebuffer = prepareDeferredOutputs.getN(0); @@ -120,6 +121,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto curvatureFramebuffer = surfaceGeometryPassOutputs.getN(1); const auto linearDepthTexture = surfaceGeometryPassOutputs.getN(2); + const auto rangeTimer = addJob("BeginTimerRange"); + // TODO: Push this 2 diffusion stages into surfaceGeometryPass as they are working together const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture).hasVarying(); const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", diffuseCurvaturePassInputs); @@ -185,8 +188,11 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // AA job to be revisited addJob("Antialiasing", primaryFramebuffer); + addJob("RangeTimer", rangeTimer); // Blit! addJob("Blit", primaryFramebuffer); + + } void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { @@ -204,21 +210,39 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend RenderArgs* args = renderContext->args; auto config = std::static_pointer_cast(renderContext->jobConfig); - gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + /* gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { _gpuTimer.begin(batch); - }); + });*/ for (auto job : _jobs) { job.run(sceneContext, renderContext); } - gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + /*gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { _gpuTimer.end(batch); - }); + });*/ - config->gpuTime = _gpuTimer.getAverage(); +// config->gpuTime = _gpuTimer.getAverage(); + } +void BeginTimerGPU::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer) { + timer = _gpuTimer; + gpu::doInBatch(renderContext->args->_context, [&](gpu::Batch& batch) { + _gpuTimer->begin(batch); + }); +} + +void EndTimerGPU::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::RangeTimerPointer& timer) { + gpu::doInBatch(renderContext->args->_context, [&](gpu::Batch& batch) { + timer->end(batch); + }); + + auto config = std::static_pointer_cast(renderContext->jobConfig); + config->gpuTime = timer->getAverage(); +} + + void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 22a460cd88..032cd48b2e 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -17,6 +17,44 @@ #include "LightingModel.h" +class BeginTimerGPU { +public: + using JobModel = render::Job::ModelO; + + BeginTimerGPU() : _gpuTimer(std::make_shared()) {} + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer); + +protected: + gpu::RangeTimerPointer _gpuTimer; +}; + + +class EndTimerGPUConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(double gpuTime READ getGpuTime) +public: + double getGpuTime() { return gpuTime; } + +protected: + friend class EndTimerGPU; + double gpuTime; +}; + +class EndTimerGPU { +public: + using Config = EndTimerGPUConfig; + using JobModel = render::Job::ModelI; + + EndTimerGPU() {} + + void configure(const Config& config) {} + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::RangeTimerPointer& timer); + +protected: +}; + + class DrawConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY newStats) @@ -175,13 +213,13 @@ public: class RenderDeferredTaskConfig : public render::Task::Config { Q_OBJECT - Q_PROPERTY(double gpuTime READ getGpuTime) + Q_PROPERTY(quint64 gpuTime READ getGpuTime) public: - double getGpuTime() { return gpuTime; } + quint64 getGpuTime() { return gpuTime; } protected: friend class RenderDeferredTask; - double gpuTime; + quint64 gpuTime; }; class RenderDeferredTask : public render::Task { diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index c5d3f79bba..1d38a1bc4c 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -148,8 +148,14 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto linearDepthPipeline = getLinearDepthPipeline(); auto curvaturePipeline = getCurvaturePipeline(); - + + // gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + // _gpuTimer.begin(batch); + // }); + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + _gpuTimer.begin(batch); batch.enableStereo(false); batch.setViewportTransform(args->_viewport); @@ -166,7 +172,9 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setPipeline(linearDepthPipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); - + + _gpuTimer.end(batch); + // Curvature pass batch.setFramebuffer(curvatureFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); @@ -176,8 +184,16 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.draw(gpu::TRIANGLE_STRIP, 4); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, nullptr); - }); + // _gpuTimer.end(batch); + }); + + // gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + // _gpuTimer.end(batch); + // }); + + auto config = std::static_pointer_cast(renderContext->jobConfig); + config->gpuTime = _gpuTimer.getAverage(); } const gpu::PipelinePointer& SurfaceGeometryPass::getLinearDepthPipeline() { From 800d2db8f163a512ec1f1c50239a31cc42f4d4df Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 15 Jul 2016 09:13:55 -0700 Subject: [PATCH 1150/1237] New plots to watch the GPU load" " --- .../utilities/render/renderStatsGPU.js | 21 ++++++ .../developer/utilities/render/statsGPU.qml | 64 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 scripts/developer/utilities/render/renderStatsGPU.js create mode 100644 scripts/developer/utilities/render/statsGPU.qml diff --git a/scripts/developer/utilities/render/renderStatsGPU.js b/scripts/developer/utilities/render/renderStatsGPU.js new file mode 100644 index 0000000000..c5955b0d5d --- /dev/null +++ b/scripts/developer/utilities/render/renderStatsGPU.js @@ -0,0 +1,21 @@ +// +// renderStats.js +// examples/utilities/tools/render +// +// Sam Gateau, created on 3/22/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Set up the qml ui +var qml = Script.resolvePath('statsGPU.qml'); +var window = new OverlayWindow({ + title: 'Render Stats GPU', + source: qml, + width: 320, + height: 200 +}); +window.setPosition(50, 20); +window.closed.connect(function() { Script.stop(); }); \ No newline at end of file diff --git a/scripts/developer/utilities/render/statsGPU.qml b/scripts/developer/utilities/render/statsGPU.qml new file mode 100644 index 0000000000..0f529d2d7f --- /dev/null +++ b/scripts/developer/utilities/render/statsGPU.qml @@ -0,0 +1,64 @@ +// +// stats.qml +// examples/utilities/render +// +// Created by Zach Pomerantz on 2/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../lib/plotperf" + +Item { + id: statsUI + anchors.fill:parent + + Column { + id: stats + spacing: 8 + anchors.fill:parent + + property var config: Render.getConfig("Stats") + + function evalEvenHeight() { + // Why do we have to do that manually ? cannot seem to find a qml / anchor / layout mode that does that ? + return (height - spacing * (children.length - 1)) / children.length + } + + + PlotPerf { + title: "Timing" + height: parent.evalEvenHeight() + object: parent.drawOpaqueConfig + valueUnit: "ms" + valueScale: 1 + valueNumDigits: "4" + plots: [ + { + object: Render.getConfig("SurfaceGeometry"), + prop: "gpuTime", + label: "SurfaceGeometryGPU", + color: "#00FFFF" + }, + { + object: Render.getConfig("RenderDeferred"), + prop: "gpuTime", + label: "DeferredLighting", + color: "#FF00FF" + } + , + { + object: Render.getConfig("RangeTimer"), + prop: "gpuTime", + label: "FrameGPU", + color: "#FFFF00" + } + ] + } + } + +} + From e1212c54cbc5931030aefba4132d67e07db49f0f Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 15 Jul 2016 09:06:13 -0700 Subject: [PATCH 1151/1237] Updated to deal with streaming out of both buffers So gotta keep track of when finished streaming to network and locally separately. That means the State needed to be more of a bitflag and less of an enum. --- libraries/audio-client/src/AudioClient.cpp | 1 - libraries/audio/src/AudioInjector.cpp | 105 ++++++++++++++------- libraries/audio/src/AudioInjector.h | 34 +++++-- 3 files changed, 95 insertions(+), 45 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index dd72125d93..bec30edb4e 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -954,7 +954,6 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA if (hasReverb) { assert(_outputFormat.channelCount() == 2); updateReverbOptions(); - qDebug() << "handling reverb"; _listenerReverb.render(outputSamples, outputSamples, numDeviceOutputSamples/2); } } diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 08143b8491..f94890ea7d 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -28,6 +28,15 @@ int audioInjectorPtrMetaTypeId = qRegisterMetaType(); +AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +}; + +AudioInjectorState& operator&= (AudioInjectorState& lhs, AudioInjectorState rhs) { + lhs = static_cast(static_cast(lhs) & static_cast(rhs)); + return lhs; +}; + AudioInjector::AudioInjector(QObject* parent) : QObject(parent) { @@ -56,10 +65,26 @@ void AudioInjector::setOptions(const AudioInjectorOptions& options) { _options.stereo = currentlyStereo; } +void AudioInjector::finishNetworkInjection() { + _state &= AudioInjectorState::NetworkInjectionFinished; + + // if we are already finished with local + // injection, then we are finished + if((_state & AudioInjectorState::LocalInjectionFinished) == AudioInjectorState::LocalInjectionFinished) { + finish(); + } +} + +void AudioInjector::finishLocalInjection() { + _state &= AudioInjectorState::LocalInjectionFinished; + if(_options.localOnly || ((_state & AudioInjectorState::NetworkInjectionFinished) == AudioInjectorState::NetworkInjectionFinished)) { + finish(); + } +} void AudioInjector::finish() { - bool shouldDelete = (_state == State::NotFinishedWithPendingDelete); - _state = State::Finished; + bool shouldDelete = ((_state & AudioInjectorState::PendingDelete) == AudioInjectorState::PendingDelete); + _state &= AudioInjectorState::Finished; emit finished(); @@ -121,23 +146,31 @@ void AudioInjector::restart() { _hasSentFirstFrame = false; // check our state to decide if we need extra handling for the restart request - if (_state == State::Finished) { + if ((_state & AudioInjectorState::Finished) == AudioInjectorState::Finished) { // we finished playing, need to reset state so we can get going again _hasSetup = false; _shouldStop = false; - _state = State::NotFinished; + _state = AudioInjectorState::NotFinished; // call inject audio to start injection over again setupInjection(); - // if we're a local injector, just inject again - if (_options.localOnly) { - injectLocally(); - } else { - // wake the AudioInjectorManager back up if it's stuck waiting - if (!injectorManager->restartFinishedInjector(this)) { - _state = State::Finished; // we're not playing, so reset the state used by isPlaying. + // inject locally + if(injectLocally()) { + + // if not localOnly, wake the AudioInjectorManager back up if it is stuck waiting + if (!_options.localOnly) { + + if (!injectorManager->restartFinishedInjector(this)) { + // TODO: this logic seems to remove the pending delete, + // which makes me wonder about the deleteLater calls + _state = AudioInjectorState::Finished; // we're not playing, so reset the state used by isPlaying. + } } + } else { + // TODO: this logic seems to remove the pending delete, + // which makes me wonder about the deleteLater calls + _state = AudioInjectorState::Finished; // we failed to play, so we are finished again } } } @@ -183,7 +216,7 @@ static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1; static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0; int64_t AudioInjector::injectNextFrame() { - if (_state == AudioInjector::State::Finished) { + if ((_state & AudioInjectorState::NetworkInjectionFinished) == AudioInjectorState::NetworkInjectionFinished) { qDebug() << "AudioInjector::injectNextFrame called but AudioInjector has finished and was not restarted. Returning."; return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } @@ -234,8 +267,10 @@ int64_t AudioInjector::injectNextFrame() { // pack the stereo/mono type of the stream audioPacketStream << _options.stereo; - // pack the flag for loopback - uchar loopbackFlag = (uchar)true; + // pack the flag for loopback. Now, we don't loopback + // and _always_ play locally, so loopbackFlag should be + // false always. + uchar loopbackFlag = (uchar)false; audioPacketStream << loopbackFlag; // pack the position for injected audio @@ -333,7 +368,7 @@ int64_t AudioInjector::injectNextFrame() { } if (_currentSendOffset >= _audioData.size() && !_options.loop) { - finish(); + finishNetworkInjection(); return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } @@ -372,10 +407,10 @@ void AudioInjector::triggerDeleteAfterFinish() { return; } - if (_state == State::Finished) { + if (_state == AudioInjectorState::Finished) { stopAndDeleteLater(); } else { - _state = State::NotFinishedWithPendingDelete; + _state &= AudioInjectorState::PendingDelete; } } @@ -421,7 +456,7 @@ AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjector* sound = playSound(buffer, options, localInterface); if (sound) { - sound->_state = AudioInjector::State::NotFinishedWithPendingDelete; + sound->_state &= AudioInjectorState::PendingDelete; } return sound; @@ -438,21 +473,23 @@ AudioInjector* AudioInjector::playSound(const QByteArray& buffer, const AudioInj // setup parameters required for injection injector->setupInjection(); - if (options.localOnly) { - if (injector->injectLocally()) { - // local injection succeeded, return the pointer to injector - return injector; - } else { - // unable to inject locally, return a nullptr - return nullptr; - } - } else { - // attempt to thread the new injector - if (injectorManager->threadInjector(injector)) { - return injector; - } else { - // we failed to thread the new injector (we are at the max number of injector threads) - return nullptr; - } + // we always inject locally + // + if (!injector->injectLocally()) { + // failed, so don't bother sending to server + qDebug() << "AudioInjector::playSound failed to inject locally"; + return nullptr; } + // if localOnly, we are done, just return injector. + if (options.localOnly) { + return injector; + } + + // send off to server for everyone else + if (!injectorManager->threadInjector(injector)) { + // we failed to thread the new injector (we are at the max number of injector threads) + qDebug() << "AudioInjector::playSound failed to thread injector"; + } + return injector; + } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 1af733ace6..e13fc15c26 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -32,24 +32,35 @@ class AbstractAudioInterface; class AudioInjectorManager; + +enum class AudioInjectorState : uint8_t { + NotFinished = 1, + Finished = 2, + PendingDelete = 4, + LocalInjectionFinished = 8, + NetworkInjectionFinished = 16 +}; + +AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs); +AudioInjectorState& operator&= (AudioInjectorState& lhs, AudioInjectorState rhs); + // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object // until it dies. - class AudioInjector : public QObject { Q_OBJECT public: - enum class State : uint8_t { - NotFinished, - NotFinishedWithPendingDelete, - Finished - }; - + static const uint8_t NotFinished = 1; + static const uint8_t Finished = 2; + static const uint8_t PendingDelete = 4; + static const uint8_t LocalInjectionFinished = 8; + static const uint8_t NetworkInjectionFinished = 16; + AudioInjector(QObject* parent); AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); - bool isFinished() const { return _state == State::Finished; } + bool isFinished() const { return (_state & AudioInjectorState::Finished) == AudioInjectorState::Finished; } int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } @@ -78,8 +89,10 @@ public slots: void setOptions(const AudioInjectorOptions& options); float getLoudness() const { return _loudness; } - bool isPlaying() const { return _state == State::NotFinished || _state == State::NotFinishedWithPendingDelete; } + bool isPlaying() const { return (_state & AudioInjectorState::NotFinished) == AudioInjectorState::NotFinished; } void finish(); + void finishLocalInjection(); + void finishNetworkInjection(); signals: void finished(); @@ -92,7 +105,7 @@ private: QByteArray _audioData; AudioInjectorOptions _options; - State _state { State::NotFinished }; + AudioInjectorState _state { AudioInjectorState::NotFinished }; bool _hasSentFirstFrame { false }; bool _hasSetup { false }; bool _shouldStop { false }; @@ -111,4 +124,5 @@ private: friend class AudioInjectorManager; }; + #endif // hifi_AudioInjector_h From ce99146733deb50a425aa200ba213e056112a4b8 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 10:27:52 -0700 Subject: [PATCH 1152/1237] filter by typing --- interface/resources/qml/AddressBarDialog.qml | 53 +++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index fe5c1d8030..bcb55ba568 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -16,9 +16,6 @@ import "styles" import "windows" import "hifi" -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 - Window { id: root HifiConstants { id: hifi } @@ -48,12 +45,13 @@ Window { // Explicitly center in order to avoid warnings at shutdown anchors.centerIn = parent; } - + function goCard(card) { addressLine.text = card.userStory.name; toggleOrGo(); } - + property var suggestionChoices: null; + AddressBarDialog { id: addressBarDialog implicitWidth: backgroundImage.width @@ -63,8 +61,8 @@ Window { width: backgroundImage.width; anchors { left: backgroundImage.left; - leftMargin: backgroundImage.height + 2 * hifi.layout.spacing bottom: backgroundImage.top; + leftMargin: parent.height + 2 * hifi.layout.spacing; bottomMargin: 2 * hifi.layout.spacing; } Text { @@ -175,6 +173,7 @@ Window { } font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75 helperText: "Go to: place, @user, /path, network address" + onTextChanged: filterChoicesByText() } } @@ -184,7 +183,7 @@ Window { // TODO: make available to other .qml. var request = new XMLHttpRequest(); // QT bug: apparently doesn't handle onload. Workaround using readyState. - request.onreadystatechange = function () { + request.onreadystatechange = function () { var READY_STATE_DONE = 4; var HTTP_OK = 200; if (request.readyState >= READY_STATE_DONE) { @@ -221,7 +220,7 @@ Window { iterator(element, icb); }); } - + function addPictureToDomain(domainInfo, cb) { // asynchronously add thumbnail and lobby to domainInfo, if available, and cb(error) asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(function (x) { return x; }), function (name, icb) { var url = "https://metaverse.highfidelity.com/api/v1/places/" + name; @@ -266,34 +265,52 @@ Window { }); }); } - - function fillDestinations () { + + function filterChoicesByText() { function fill1(target, data) { if (!data) { + target.visible = false; return; } console.log('suggestion:', JSON.stringify(data)); target.userStory = data; if (data.lobby) { - console.log('target', target); - console.log('target.image', target.image); - console.log('url', data.lobby); target.image.source = data.lobby; } target.placeText = data.name; target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); + target.visible = true; } + if (!suggestionChoices) { + return; + } + var words = addressLine.text.toUpperCase().split(/\s+/); + var filtered = !words.length ? suggestionChoices : suggestionChoices.filter(function (domain) { + var text = domain.names.concat(domain.tags).join(' '); + if (domain.description) { + text += domain.description; + } + text = text.toUpperCase(); + return words.every(function (word) { + return text.indexOf(word) >= 0; + }); + }); + fill1(s0, filtered[0]); + fill1(s1, filtered[1]); + fill1(s2, filtered[2]); + } + + function fillDestinations() { + suggestionChoices = null; getDomains({minUsers: 0, maxUsers: 20}, function (error, domains) { var here = addressBarDialog.getHost(); // don't show where we are now. var withLobby = !error && domains.filter(function (domain) { return domain.lobby && (domain.name !== here); }); - console.log(error, domains.length, withLobby.length); withLobby.sort(function (a, b) { return b.online_users - a.online_users; }); - fill1(s0, withLobby[0]); - fill1(s1, withLobby[1]); - fill1(s2, withLobby[2]); + suggestionChoices = withLobby; + filterChoicesByText(); }); } - + onVisibleChanged: { if (visible) { addressLine.forceActiveFocus() From f0db7b9d39e5b2da679515aa3fe0a3c471d0055a Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 15 Jul 2016 11:42:30 -0600 Subject: [PATCH 1153/1237] Checkpoint --- cmake/externals/hifiAudioCodec/CMakeLists.txt | 8 +++----- cmake/macros/SetupHifiClientServerPlugin.cmake | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cmake/externals/hifiAudioCodec/CMakeLists.txt b/cmake/externals/hifiAudioCodec/CMakeLists.txt index 3a8c714d79..642a58b685 100644 --- a/cmake/externals/hifiAudioCodec/CMakeLists.txt +++ b/cmake/externals/hifiAudioCodec/CMakeLists.txt @@ -1,13 +1,13 @@ include(ExternalProject) include(SelectLibraryConfigurations) -set(EXTERNAL_NAME HiFiAudioCodec) +set(EXTERNAL_NAME hifiAudioCodec) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://s3.amazonaws.com/hifi-public/dependencies/codecSDK-1.zip + URL http://s3.amazonaws.com/hifi-public/dependencies/codecSDK-1.zip URL_MD5 23ec3fe51eaa155ea159a4971856fc13 CONFIGURE_COMMAND "" BUILD_COMMAND "" @@ -27,7 +27,5 @@ if (WIN32) elseif(APPLE) set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/libaudio.a CACHE TYPE INTERNAL) elseif(NOT ANDROID) - # FIXME need to account for different architectures - #set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/linux64/audio.so CACHE TYPE INTERNAL) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/linux64/audio.so CACHE TYPE INTERNAL) endif() - diff --git a/cmake/macros/SetupHifiClientServerPlugin.cmake b/cmake/macros/SetupHifiClientServerPlugin.cmake index 8ba38a09c3..74ceaeb0ab 100644 --- a/cmake/macros/SetupHifiClientServerPlugin.cmake +++ b/cmake/macros/SetupHifiClientServerPlugin.cmake @@ -37,7 +37,7 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) ${CLIENT_PLUGIN_FULL_PATH} ) # copy the client plugin binaries - add_custom_command(TARGET ${DIR} POST_BUILD + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy "$" ${CLIENT_PLUGIN_FULL_PATH} @@ -50,7 +50,7 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) ${SERVER_PLUGIN_FULL_PATH} ) # copy the server plugin binaries - add_custom_command(TARGET ${DIR} POST_BUILD + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy "$" ${SERVER_PLUGIN_FULL_PATH} From 53d524eb2fe40668424aac9feb1a0d0c1cb01e15 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 15 Jul 2016 12:04:45 -0600 Subject: [PATCH 1154/1237] Checkpoint --- cmake/externals/hifiAudioCodec/CMakeLists.txt | 2 +- .../macros/AddDependencyExternalProjects.cmake | 5 ++++- plugins/hifiCodec/CMakeLists.txt | 18 +++++++----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/cmake/externals/hifiAudioCodec/CMakeLists.txt b/cmake/externals/hifiAudioCodec/CMakeLists.txt index 642a58b685..8a1b07253a 100644 --- a/cmake/externals/hifiAudioCodec/CMakeLists.txt +++ b/cmake/externals/hifiAudioCodec/CMakeLists.txt @@ -23,7 +23,7 @@ ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include CACHE TYPE INTERNAL) if (WIN32) - set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/audio.lib CACHE TYPE INTERNAL) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/audio.lib CACHE TYPE INTERNAL) elseif(APPLE) set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/libaudio.a CACHE TYPE INTERNAL) elseif(NOT ANDROID) diff --git a/cmake/macros/AddDependencyExternalProjects.cmake b/cmake/macros/AddDependencyExternalProjects.cmake index e35ca98959..3cbb961308 100644 --- a/cmake/macros/AddDependencyExternalProjects.cmake +++ b/cmake/macros/AddDependencyExternalProjects.cmake @@ -17,10 +17,12 @@ macro(ADD_DEPENDENCY_EXTERNAL_PROJECTS) # has the user told us they specific don't want this as an external project? if (NOT USE_LOCAL_${_PROJ_NAME_UPPER}) + message(STATUS "LEODEBUG: Looking for dependency ${_PROJ_NAME}") # have we already detected we can't have this as external project on this OS? if (NOT DEFINED ${_PROJ_NAME_UPPER}_EXTERNAL_PROJECT OR ${_PROJ_NAME_UPPER}_EXTERNAL_PROJECT) # have we already setup the target? if (NOT TARGET ${_PROJ_NAME}) + message(STATUS "LEODEBUG: We dont have a target with that name ${_PROJ_NAME} adding directory ${EXTERNAL_PROJECT_DIR}/${_PROJ_NAME}") add_subdirectory(${EXTERNAL_PROJECT_DIR}/${_PROJ_NAME} ${EXTERNALS_BINARY_DIR}/${_PROJ_NAME}) # did we end up adding an external project target? @@ -35,6 +37,7 @@ macro(ADD_DEPENDENCY_EXTERNAL_PROJECTS) endif () if (TARGET ${_PROJ_NAME}) + message(STATUS "LEODEBUG: We no have the target ${_PROJ_NAME}") add_dependencies(${TARGET_NAME} ${_PROJ_NAME}) endif () @@ -43,4 +46,4 @@ macro(ADD_DEPENDENCY_EXTERNAL_PROJECTS) endforeach() -endmacro() \ No newline at end of file +endmacro() diff --git a/plugins/hifiCodec/CMakeLists.txt b/plugins/hifiCodec/CMakeLists.txt index 0af7e42ea1..b278e839e4 100644 --- a/plugins/hifiCodec/CMakeLists.txt +++ b/plugins/hifiCodec/CMakeLists.txt @@ -6,15 +6,11 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -if (WIN32 OR APPLE) - set(TARGET_NAME hifiCodec) - setup_hifi_client_server_plugin() - - link_hifi_libraries(audio shared plugins) - - add_dependency_external_projects(HiFiAudioCodec) - target_include_directories(${TARGET_NAME} PRIVATE ${HIFIAUDIOCODEC_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${HIFIAUDIOCODEC_LIBRARIES}) - install_beside_console() -endif() +set(TARGET_NAME hifiCodec) +setup_hifi_client_server_plugin() +link_hifi_libraries(audio shared plugins) +add_dependency_external_projects(hifiAudioCodec) +target_include_directories(${TARGET_NAME} PRIVATE ${HIFIAUDIOCODEC_INCLUDE_DIRS}) +target_link_libraries(${TARGET_NAME} ${HIFIAUDIOCODEC_LIBRARIES}) +install_beside_console() From 8faa961c8dfdb3f8586c73545708250bd6a4bca2 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 11:41:00 -0700 Subject: [PATCH 1155/1237] all domains, not just the 'active' ones, which turns out to be defined as having at least one person in it. --- interface/resources/qml/AddressBarDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index bcb55ba568..b9b6e58913 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -244,7 +244,7 @@ Window { options.page = 1; } // FIXME: really want places I'm allowed in, not just open ones - var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&active&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; + var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; getRequest(url, function (error, json) { if (!error && (json.status !== 'success')) { error = new Error("Bad response: " + JSON.stringify(json)); From 99e229467bdaec842c1aa58cb0b8adac97f72c3d Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 11:49:21 -0700 Subject: [PATCH 1156/1237] Don't show the word "suggesions" when there aren't any. --- interface/resources/qml/AddressBarDialog.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index b9b6e58913..220aa794b9 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -66,6 +66,7 @@ Window { bottomMargin: 2 * hifi.layout.spacing; } Text { + id: suggestionsLabel; text: "Suggestions" } Row { @@ -298,6 +299,7 @@ Window { fill1(s0, filtered[0]); fill1(s1, filtered[1]); fill1(s2, filtered[2]); + suggestionsLabel.visible = filtered.length; } function fillDestinations() { From b0c27a53e2b2d7c2dc0434484bb217638d1e4a50 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 15 Jul 2016 13:01:21 -0600 Subject: [PATCH 1157/1237] Linking to Linux only SDK --- cmake/externals/hifiAudioCodec/CMakeLists.txt | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/cmake/externals/hifiAudioCodec/CMakeLists.txt b/cmake/externals/hifiAudioCodec/CMakeLists.txt index 8a1b07253a..87b5044b42 100644 --- a/cmake/externals/hifiAudioCodec/CMakeLists.txt +++ b/cmake/externals/hifiAudioCodec/CMakeLists.txt @@ -5,15 +5,27 @@ set(EXTERNAL_NAME hifiAudioCodec) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -ExternalProject_Add( - ${EXTERNAL_NAME} - URL http://s3.amazonaws.com/hifi-public/dependencies/codecSDK-1.zip - URL_MD5 23ec3fe51eaa155ea159a4971856fc13 - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - LOG_DOWNLOAD 1 -) +if (WIN32 OR APPLE) + ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://s3.amazonaws.com/hifi-public/dependencies/codecSDK-1.zip + URL_MD5 23ec3fe51eaa155ea159a4971856fc13 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 + ) +elseif(NOT ANDROID) + ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://s3.amazonaws.com/hifi-public/dependencies/codecSDK-linux.zip + URL_MD5 7d37914a18aa4de971d2f45dd3043bde + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 + ) +endif() # Hide this external target (for ide users) set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") @@ -27,5 +39,5 @@ if (WIN32) elseif(APPLE) set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/libaudio.a CACHE TYPE INTERNAL) elseif(NOT ANDROID) - set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/linux64/audio.so CACHE TYPE INTERNAL) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/Release/libaudio.a CACHE TYPE INTERNAL) endif() From ce67d57279209593daa18c033c1e601450273f0f Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 15 Jul 2016 13:03:40 -0600 Subject: [PATCH 1158/1237] Removing Debugs --- cmake/macros/AddDependencyExternalProjects.cmake | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmake/macros/AddDependencyExternalProjects.cmake b/cmake/macros/AddDependencyExternalProjects.cmake index 3cbb961308..99b8317fd7 100644 --- a/cmake/macros/AddDependencyExternalProjects.cmake +++ b/cmake/macros/AddDependencyExternalProjects.cmake @@ -17,12 +17,10 @@ macro(ADD_DEPENDENCY_EXTERNAL_PROJECTS) # has the user told us they specific don't want this as an external project? if (NOT USE_LOCAL_${_PROJ_NAME_UPPER}) - message(STATUS "LEODEBUG: Looking for dependency ${_PROJ_NAME}") # have we already detected we can't have this as external project on this OS? if (NOT DEFINED ${_PROJ_NAME_UPPER}_EXTERNAL_PROJECT OR ${_PROJ_NAME_UPPER}_EXTERNAL_PROJECT) # have we already setup the target? if (NOT TARGET ${_PROJ_NAME}) - message(STATUS "LEODEBUG: We dont have a target with that name ${_PROJ_NAME} adding directory ${EXTERNAL_PROJECT_DIR}/${_PROJ_NAME}") add_subdirectory(${EXTERNAL_PROJECT_DIR}/${_PROJ_NAME} ${EXTERNALS_BINARY_DIR}/${_PROJ_NAME}) # did we end up adding an external project target? @@ -37,7 +35,6 @@ macro(ADD_DEPENDENCY_EXTERNAL_PROJECTS) endif () if (TARGET ${_PROJ_NAME}) - message(STATUS "LEODEBUG: We no have the target ${_PROJ_NAME}") add_dependencies(${TARGET_NAME} ${_PROJ_NAME}) endif () From d0846236622f32a76e4c15bed35f0f124cec0aab Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 12:15:23 -0700 Subject: [PATCH 1159/1237] PR feedback --- scripts/system/edit.js | 68 +++++++++++++----------------------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 883a804e4b..05d554393f 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -12,7 +12,10 @@ // HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; - +var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; +var SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; +var EDIT_TOOLBAR = "com.highfidelity.interface.toolbar.edit"; + Script.include([ "libraries/stringHelpers.js", "libraries/dataViewHelpers.js", @@ -92,8 +95,6 @@ var INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG = "You do not have the necessary p var mode = 0; var isActive = false; -var placingEntityID = null; - IMPORTING_SVO_OVERLAY_WIDTH = 144; IMPORTING_SVO_OVERLAY_HEIGHT = 30; IMPORTING_SVO_OVERLAY_MARGIN = 5; @@ -170,24 +171,16 @@ var toolBar = (function() { systemToolbar, activeButton; - function createNewEntity(properties, dragOnCreate) { + function createNewEntity(properties) { Settings.setValue(EDIT_SETTING, false); - // Default to true if not passed in - // dragOnCreate = dragOnCreate == undefined ? true : dragOnCreate; - dragOnCreate = false; - var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); var entityID = null; if (position != null) { position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions), properties.position = position; - entityID = Entities.addEntity(properties); - if (dragOnCreate) { - placingEntityID = entityID; - } } else { Window.alert("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); } @@ -201,7 +194,7 @@ var toolBar = (function() { function cleanup() { that.setActive(false); - systemToolbar.removeButton("com.highfidelity.interface.system.editButton"); + systemToolbar.removeButton(EDIT_TOGGLE_BUTTON); } function addButton(name, image, handler) { @@ -214,7 +207,7 @@ var toolBar = (function() { visible: true, }); if (handler) { - button.clicked.connect(function(){ + button.clicked.connect(function() { Script.setTimeout(handler, 100); }); } @@ -236,32 +229,32 @@ var toolBar = (function() { } }); - systemToolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + systemToolbar = Toolbars.getToolbar(SYSTEM_TOOLBAR); activeButton = systemToolbar.addButton({ - objectName: "com.highfidelity.interface.system.editButton", + objectName: EDIT_TOGGLE_BUTTON, imageURL: TOOL_ICON_URL + "edit.svg", visible: true, buttonState: 1, }); - activeButton.clicked.connect(function(){ + activeButton.clicked.connect(function() { that.setActive(!isActive); activeButton.writeProperty("buttonState", isActive ? 0 : 1); }); - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.edit"); + toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); toolBar.writeProperty("shown", false); - addButton("newModelButton", "model-01.svg", function(){ + addButton("newModelButton", "model-01.svg", function() { var url = Window.prompt("Model URL"); if (url !== null && url !== "") { createNewEntity({ type: "Model", modelURL: url - }, false); + }); } }); - addButton("newCubeButton", "cube-01.svg", function(){ + addButton("newCubeButton", "cube-01.svg", function() { createNewEntity({ type: "Box", dimensions: DEFAULT_DIMENSIONS, @@ -273,7 +266,7 @@ var toolBar = (function() { }); }); - addButton("newSphereButton", "sphere-01.svg", function(){ + addButton("newSphereButton", "sphere-01.svg", function() { createNewEntity({ type: "Sphere", dimensions: DEFAULT_DIMENSIONS, @@ -285,7 +278,7 @@ var toolBar = (function() { }); }); - addButton("newLightButton", "light-01.svg", function(){ + addButton("newLightButton", "light-01.svg", function() { createNewEntity({ type: "Light", dimensions: DEFAULT_LIGHT_DIMENSIONS, @@ -304,7 +297,7 @@ var toolBar = (function() { }); }); - addButton("newTextButton", "text-01.svg", function(){ + addButton("newTextButton", "text-01.svg", function() { createNewEntity({ type: "Text", dimensions: { @@ -327,7 +320,7 @@ var toolBar = (function() { }); }); - addButton("newWebButton", "web-01.svg", function(){ + addButton("newWebButton", "web-01.svg", function() { createNewEntity({ type: "Web", dimensions: { @@ -339,7 +332,7 @@ var toolBar = (function() { }); }); - addButton("newZoneButton", "zone-01.svg", function(){ + addButton("newZoneButton", "zone-01.svg", function() { createNewEntity({ type: "Zone", dimensions: { @@ -350,7 +343,7 @@ var toolBar = (function() { }); }); - addButton("newParticleButton", "particle-01.svg", function(){ + addButton("newParticleButton", "particle-01.svg", function() { createNewEntity({ type: "ParticleEffect", isEmitting: true, @@ -409,8 +402,8 @@ var toolBar = (function() { gridTool.setVisible(true); grid.setEnabled(true); propertiesTool.setVisible(true); - // Not sure what the following was meant to accomplish, but it currently causes selectionDisplay.triggerMapping.enable(); + // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); } @@ -580,16 +573,6 @@ function mouseMove(event) { mouseHasMovedSincePress = true; } - if (placingEntityID) { - var pickRay = Camera.computePickRay(event.x, event.y); - var distance = cameraManager.enabled ? cameraManager.zoomDistance : DEFAULT_ENTITY_DRAG_DROP_DISTANCE; - var offset = Vec3.multiply(distance, pickRay.direction); - var position = Vec3.sum(Camera.position, offset); - Entities.editEntity(placingEntityID, { - position: position, - }); - return; - } if (!isActive) { return; } @@ -624,16 +607,7 @@ function mouseReleaseEvent(event) { if (propertyMenu.mouseReleaseEvent(event)) { return true; } - if (placingEntityID) { - - if (isActive) { - - selectionManager.setSelections([placingEntityID]); - } - placingEntityID = null; - } if (isActive && selectionManager.hasSelection()) { - tooltip.show(false); } if (mouseCapturedByTool) { From 90299a98fec556ffc236b8eecb9c859b394887ed Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 12:19:02 -0700 Subject: [PATCH 1160/1237] PR feedback --- plugins/openvr/src/ViveControllerManager.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 4fccc624be..85feebda11 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -11,9 +11,6 @@ #include "ViveControllerManager.h" -#include -#include - #include #include #include @@ -288,9 +285,6 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u vr::VRControllerState_t controllerState = vr::VRControllerState_t(); if (_system->GetControllerState(deviceIndex, &controllerState)) { - //std::stringstream stream; - //stream << std::hex << controllerState.ulButtonPressed << " " << std::hex << controllerState.ulButtonTouched; - //qDebug() << deviceIndex << " " << stream.str().c_str() << controllerState.rAxis[1].x; // process each button for (uint32_t i = 0; i < vr::k_EButton_Max; ++i) { auto mask = vr::ButtonMaskFromId((vr::EVRButtonId)i); From 6fb2cedfe04b2248406718adaa4fe856b21542df Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 13:42:31 -0700 Subject: [PATCH 1161/1237] card default image: settable, use to start, and reset when needed --- interface/resources/qml/AddressBarDialog.qml | 4 +--- interface/resources/qml/hifi/Card.qml | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 220aa794b9..4cb550cd53 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -275,9 +275,7 @@ Window { } console.log('suggestion:', JSON.stringify(data)); target.userStory = data; - if (data.lobby) { - target.image.source = data.lobby; - } + target.image.source = data.lobby || ''; // should fail to load and thus use default target.placeText = data.name; target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); target.visible = true; diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 342c469303..8b2d1133da 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -21,11 +21,14 @@ Rectangle { property alias image: lobby; property alias placeText: place.text; property alias usersText: users.text; + // FIXME: let's get our own + property string defaultPicture: "http://www.davidluke.com/wp-content/themes/david-luke/media/ims/placeholder720.gif"; HifiConstants { id: hifi } Image { id: lobby; width: parent.width; height: parent.height; + source: defaultPicture; fillMode: Image.PreserveAspectCrop; // source gets filled in later anchors.verticalCenter: parent.verticalCenter; @@ -33,8 +36,7 @@ Rectangle { onStatusChanged: { if (status == Image.Error) { console.log("source: " + source + ": failed to load " + JSON.stringify(userStory)); - // FIXME: let's get our own - source = "http://www.davidluke.com/wp-content/themes/david-luke/media/ims/placeholder720.gif" + source = defaultPicture; } } } From 9ee64d114b3bca06a5f5ac9cbd6e8c828705a27c Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Fri, 15 Jul 2016 13:47:36 -0700 Subject: [PATCH 1162/1237] Sam's space. --- interface/src/FileLogger.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/FileLogger.cpp b/interface/src/FileLogger.cpp index 48bd881bfd..754fa7f474 100644 --- a/interface/src/FileLogger.cpp +++ b/interface/src/FileLogger.cpp @@ -94,6 +94,7 @@ void FilePersistThread::rollFileIfNecessary(QFile& file, bool notifyListenersIfR } } } + bool FilePersistThread::processQueueItems(const Queue& messages) { QFile file(_logger._fileName); rollFileIfNecessary(file); From c462b8c387559f14150d4d9416eab2d8c3d89e15 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 13:51:14 -0700 Subject: [PATCH 1163/1237] Make search line termination a circle, like 2D UI --- .../system/controllers/handControllerGrab.js | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 13d71dca1c..e1c4c9e757 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -229,6 +229,14 @@ function getTag() { return "grab-" + MyAvatar.sessionUUID; } +function colorPow(color, power) { + return { + red: Math.pow(color.red / 255.0, power) * 255, + green: Math.pow(color.green / 255.0, power) * 255, + blue: Math.pow(color.blue / 255.0, power) * 255, + }; +} + function entityHasActions(entityID) { return Entities.getActionIDs(entityID).length > 0; } @@ -547,23 +555,34 @@ function MyController(hand) { var SEARCH_SPHERE_ALPHA = 0.5; this.searchSphereOn = function (location, size, color) { + + var rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); + var brightColor = colorPow(color, 0.1); + print("bright color " + brightColor.red + " " + brightColor.green + " " + brightColor.blue); if (this.searchSphere === null) { var sphereProperties = { position: location, - size: size, - color: color, - alpha: SEARCH_SPHERE_ALPHA, + rotation: rotation, + outerRadius: size * 3.0, + innerColor: brightColor, + outerColor: color, + innerAlpha: 0.9, + outerAlpha: 0.0, solid: true, ignoreRayIntersection: true, drawInFront: true, // Even when burried inside of something, show it. visible: true }; - this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); + this.searchSphere = Overlays.addOverlay("circle3d", sphereProperties); } else { Overlays.editOverlay(this.searchSphere, { position: location, - size: size, - color: color, + rotation: rotation, + innerColor: brightColor, + outerColor: color, + innerAlpha: 1.0, + outerAlpha: 0.0, + outerRadius: size * 3.0, visible: true }); } From 0d811c489aa3464f9a75761581078ad5c0a50b29 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 14:05:16 -0700 Subject: [PATCH 1164/1237] Removing debug logging --- scripts/system/controllers/handControllerGrab.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index e1c4c9e757..4f0578369f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -558,7 +558,6 @@ function MyController(hand) { var rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); var brightColor = colorPow(color, 0.1); - print("bright color " + brightColor.red + " " + brightColor.green + " " + brightColor.blue); if (this.searchSphere === null) { var sphereProperties = { position: location, From 87e54482dda406b1853ffb826dd3b9115b7d5d2e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 14:30:28 -0700 Subject: [PATCH 1165/1237] check pictures only for the domains that have someone in them, but use all domains when filtering (occupied or no, picture or no). Also, make darn sure we use default picture if no data. --- interface/resources/qml/AddressBarDialog.qml | 52 ++++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 4cb550cd53..d87cfe1f48 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -50,7 +50,8 @@ Window { addressLine.text = card.userStory.name; toggleOrGo(); } - property var suggestionChoices: null; + property var allDomains: []; + property var suggestionChoices: []; AddressBarDialog { id: addressBarDialog @@ -223,12 +224,14 @@ Window { } function addPictureToDomain(domainInfo, cb) { // asynchronously add thumbnail and lobby to domainInfo, if available, and cb(error) + // This requests data for all the names at once, and just uses the first one to come back. + // We might change this to check one at a time, which would be less requests and more latency. asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(function (x) { return x; }), function (name, icb) { var url = "https://metaverse.highfidelity.com/api/v1/places/" + name; getRequest(url, function (error, json) { var previews = !error && json.data.place.previews; if (previews) { - if (!domainInfo.thumbnail) { // just grab tghe first one + if (!domainInfo.thumbnail) { // just grab the first one domainInfo.thumbnail = previews.thumbnail; } if (!domainInfo.lobby) { @@ -255,15 +258,13 @@ Window { return cb(error); } var domains = json.data.domains; - asyncEach(domains, addPictureToDomain, function (error) { - if (json.current_page < json.total_pages) { - options.page++; - return getDomains(options, function (error, others) { - cb(error, domains.concat(others)); - }); - } - cb(null, domains); - }); + if (json.current_page < json.total_pages) { + options.page++; + return getDomains(options, function (error, others) { + cb(error, domains.concat(others)); + }); + } + cb(null, domains); }); } @@ -275,16 +276,13 @@ Window { } console.log('suggestion:', JSON.stringify(data)); target.userStory = data; - target.image.source = data.lobby || ''; // should fail to load and thus use default + target.image.source = data.lobby || target.defaultPicture; target.placeText = data.name; target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); target.visible = true; } - if (!suggestionChoices) { - return; - } var words = addressLine.text.toUpperCase().split(/\s+/); - var filtered = !words.length ? suggestionChoices : suggestionChoices.filter(function (domain) { + var filtered = !words.length ? suggestionChoices : allDomains.filter(function (domain) { var text = domain.names.concat(domain.tags).join(' '); if (domain.description) { text += domain.description; @@ -301,13 +299,25 @@ Window { } function fillDestinations() { - suggestionChoices = null; + allDomains = suggestionChoices = []; getDomains({minUsers: 0, maxUsers: 20}, function (error, domains) { + if (error) { + console.log('domain query failed:', error); + return filterChoicesByText(); + } var here = addressBarDialog.getHost(); // don't show where we are now. - var withLobby = !error && domains.filter(function (domain) { return domain.lobby && (domain.name !== here); }); - withLobby.sort(function (a, b) { return b.online_users - a.online_users; }); - suggestionChoices = withLobby; - filterChoicesByText(); + allDomains = domains.filter(function (domain) { return domain.name !== here; }); + allDomains.sort(function (a, b) { return b.online_users - a.online_users; }); + // Whittle down suggestions to those that have at least one user, and try to get pictures. + suggestionChoices = allDomains.filter(function (domain) { return domain.online_users; }); + asyncEach(domains, addPictureToDomain, function (error) { + if (error) { + console.log('place picture query failed:', error); + } + // Whittle down more by requiring a picture. + suggestionChoices = suggestionChoices.filter(function (domain) { return domain.lobby; }); + filterChoicesByText(); + }); }); } From 562446e7b86659712823df026512f27973f5d89a Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 14:44:27 -0700 Subject: [PATCH 1166/1237] use AddressManager, rather than adding a trampoline to hostname in AddressBarDialog. --- interface/resources/qml/AddressBarDialog.qml | 2 +- interface/src/ui/AddressBarDialog.cpp | 4 ---- interface/src/ui/AddressBarDialog.h | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index d87cfe1f48..43a1c10c0f 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -305,7 +305,7 @@ Window { console.log('domain query failed:', error); return filterChoicesByText(); } - var here = addressBarDialog.getHost(); // don't show where we are now. + var here = AddressManager.hostname; // don't show where we are now. allDomains = domains.filter(function (domain) { return domain.name !== here; }); allDomains.sort(function (a, b) { return b.online_users - a.online_users; }); // Whittle down suggestions to those that have at least one user, and try to get pictures. diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index 7a32381db3..6fb437e312 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -40,10 +40,6 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare _forwardEnabled = !(DependencyManager::get()->getForwardStack().isEmpty()); } -QString AddressBarDialog::getHost() const { - return DependencyManager::get()->getHost(); -} - void AddressBarDialog::loadAddress(const QString& address) { qDebug() << "Called LoadAddress with address " << address; if (!address.isEmpty()) { diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index b5a21dd47a..bbce52c67c 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -34,7 +34,6 @@ protected: void displayAddressOfflineMessage(); void displayAddressNotFoundMessage(); - Q_INVOKABLE QString getHost() const; Q_INVOKABLE void loadAddress(const QString& address); Q_INVOKABLE void loadHome(); Q_INVOKABLE void loadBack(); From cc367f7b49b635a10cc3a3cb7c6587364a52846f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 14:56:31 -0700 Subject: [PATCH 1167/1237] We don't need HorizontalSpacer.qml. --- interface/resources/qml/AddressBarDialog.qml | 4 +--- .../qml/controls-uit/HorizontalSpacer.qml | 19 ------------------- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 interface/resources/qml/controls-uit/HorizontalSpacer.qml diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 43a1c10c0f..8764639972 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -11,7 +11,6 @@ import Hifi 1.0 import QtQuick 2.4 import "controls" -import "controls-uit" as HifiControls import "styles" import "windows" import "hifi" @@ -71,20 +70,19 @@ Window { text: "Suggestions" } Row { + spacing: hifi.layout.spacing; Card { id: s0; width: 200; height: 200; goFunction: goCard } - HifiControls.HorizontalSpacer { } Card { id: s1; width: 200; height: 200; goFunction: goCard } - HifiControls.HorizontalSpacer { } Card { id: s2; width: 200; diff --git a/interface/resources/qml/controls-uit/HorizontalSpacer.qml b/interface/resources/qml/controls-uit/HorizontalSpacer.qml deleted file mode 100644 index a9fac2bcf7..0000000000 --- a/interface/resources/qml/controls-uit/HorizontalSpacer.qml +++ /dev/null @@ -1,19 +0,0 @@ -// -// HorizontalSpacer.qml -// -// Created by Howard Stearns on 12 July 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 - -import "../styles-uit" - -Item { - HifiConstants { id: hifi } - height: 1 // Must be non-zero - width: hifi.dimensions.controlInterlineHeight -} From e4794450c202e38b13b837131bbb4f85de89843f Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Fri, 15 Jul 2016 15:36:20 -0700 Subject: [PATCH 1168/1237] Disable attach point saving --- scripts/system/controllers/handControllerGrab.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index d6bf5cdd5f..1e83fca024 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -264,6 +264,9 @@ function propsArePhysical(props) { return isPhysical; } +// currently disabled. +var USE_ATTACH_POINT_SETTINGS = false; + var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; function getAttachPointSettings() { try { @@ -1858,7 +1861,7 @@ function MyController(hand) { // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; - var offsets = getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand); + var offsets = USE_ATTACH_POINT_SETTINGS && getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand); if (offsets) { this.offsetPosition = offsets[0]; this.offsetRotation = offsets[1]; @@ -2065,7 +2068,7 @@ function MyController(hand) { this.holdExit = function () { // store the offset attach points into preferences. - if (this.grabbedHotspot) { + if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedEntity) { entityPropertiesCache.addEntity(this.grabbedEntity); var props = entityPropertiesCache.getProps(this.grabbedEntity); var entityXform = new Xform(props.rotation, props.position); From 289617d4ce5a4add7e9e3921dc49a55247d84c9a Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 16:14:29 -0700 Subject: [PATCH 1169/1237] styling --- interface/resources/qml/AddressBarDialog.qml | 31 ++++++++-------- interface/resources/qml/hifi/Card.qml | 37 ++++++++++++++++---- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 8764639972..b739842d1c 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -51,6 +51,8 @@ Window { } property var allDomains: []; property var suggestionChoices: []; + property int cardWidth: 200; + property int cardHeight: 152; AddressBarDialog { id: addressBarDialog @@ -60,33 +62,29 @@ Window { Column { width: backgroundImage.width; anchors { - left: backgroundImage.left; bottom: backgroundImage.top; - leftMargin: parent.height + 2 * hifi.layout.spacing; bottomMargin: 2 * hifi.layout.spacing; - } - Text { - id: suggestionsLabel; - text: "Suggestions" + right: backgroundImage.right; + rightMargin: -104; // FIXME } Row { spacing: hifi.layout.spacing; Card { id: s0; - width: 200; - height: 200; + width: cardWidth; + height: cardHeight; goFunction: goCard } Card { id: s1; - width: 200; - height: 200; + width: cardWidth; + height: cardHeight; goFunction: goCard } Card { id: s2; - width: 200; - height: 200; + width: cardWidth; + height: cardHeight; goFunction: goCard } } @@ -221,10 +219,14 @@ Window { }); } + function identity(x) { + return x; + } + function addPictureToDomain(domainInfo, cb) { // asynchronously add thumbnail and lobby to domainInfo, if available, and cb(error) // This requests data for all the names at once, and just uses the first one to come back. // We might change this to check one at a time, which would be less requests and more latency. - asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(function (x) { return x; }), function (name, icb) { + asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(identity), function (name, icb) { var url = "https://metaverse.highfidelity.com/api/v1/places/" + name; getRequest(url, function (error, json) { var previews = !error && json.data.place.previews; @@ -279,7 +281,7 @@ Window { target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); target.visible = true; } - var words = addressLine.text.toUpperCase().split(/\s+/); + var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity); var filtered = !words.length ? suggestionChoices : allDomains.filter(function (domain) { var text = domain.names.concat(domain.tags).join(' '); if (domain.description) { @@ -293,7 +295,6 @@ Window { fill1(s0, filtered[0]); fill1(s1, filtered[1]); fill1(s2, filtered[2]); - suggestionsLabel.visible = filtered.length; } function fillDestinations() { diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 8b2d1133da..3a2a52e3c0 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -13,6 +13,7 @@ import Hifi 1.0 import QtQuick 2.5 +import QtGraphicalEffects 1.0 import "../styles-uit" Rectangle { @@ -21,6 +22,8 @@ Rectangle { property alias image: lobby; property alias placeText: place.text; property alias usersText: users.text; + property int textPadding: 20; + property int textSize: 24; // FIXME: let's get our own property string defaultPicture: "http://www.davidluke.com/wp-content/themes/david-luke/media/ims/placeholder720.gif"; HifiConstants { id: hifi } @@ -40,22 +43,44 @@ Rectangle { } } } + DropShadow { + source: place; + anchors.fill: place; + horizontalOffset: 0; + radius: 2; + samples: 9; + color: hifi.colors.black; + verticalOffset: 1; + spread: 0; + } + DropShadow { + source: users; + anchors.fill: users; + horizontalOffset: 0; + radius: 2; + samples: 9; + color: hifi.colors.black; + verticalOffset: 1; + spread: 0; + } RalewaySemiBold { id: place; - color: hifi.colors.lightGrayText; + color: hifi.colors.white; + size: textSize; anchors { - leftMargin: 2 * hifi.layout.spacing; - topMargin: 2 * hifi.layout.spacing; + top: parent.top; + left: parent.left; + margins: textPadding; } } RalewayRegular { id: users; - color: hifi.colors.lightGrayText; + size: textSize; + color: hifi.colors.white; anchors { bottom: parent.bottom; right: parent.right; - bottomMargin: 2 * hifi.layout.spacing; - rightMargin: 2 * hifi.layout.spacing; + margins: textPadding; } } MouseArea { From 317dee1b235c12ddbdb316d3c36ef229e456d4a7 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 16:15:53 -0700 Subject: [PATCH 1170/1237] Fix decoration inflation --- .../resources/qml/windows/Decoration.qml | 22 +++++++++++++++++-- .../src/display-plugins/CompositorHelper.cpp | 9 ++++++++ .../src/display-plugins/CompositorHelper.h | 6 ++--- .../controllers/handControllerPointer.js | 9 +++++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml index 0048c556eb..843ae25596 100644 --- a/interface/resources/qml/windows/Decoration.qml +++ b/interface/resources/qml/windows/Decoration.qml @@ -48,7 +48,23 @@ Rectangle { drag.target: window hoverEnabled: true onEntered: window.mouseEntered(); - onExited: window.mouseExited(); + onExited: { + if (!containsMouseGlobal()) { + window.mouseExited(); + } + } + + function containsMouseGlobal() { + var reticlePos = Reticle.position; + var globalPosition = decorationMouseArea.mapToItem(desktop, 0, 0); + var localPosition = { + x: reticlePos.x - globalPosition.x, + y: reticlePos.y - globalPosition.y, + }; + return localPosition.x >= 0 && localPosition.x <= width && + localPosition.y >= 0 && localPosition.y <= height; + } + } Connections { target: window @@ -57,7 +73,9 @@ Rectangle { root.inflateDecorations() } } - onMouseExited: root.deflateDecorations(); + onMouseExited: { + root.deflateDecorations(); + } } Connections { target: desktop diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 89ff2e0c8d..56be8e1cf9 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -442,3 +442,12 @@ glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const } return result; } + + +QVariant ReticleInterface::getPosition() const { + return vec2toVariant(_compositor->getReticlePosition()); +} + +void ReticleInterface::setPosition(QVariant position) { + _compositor->setReticlePosition(vec2FromVariant(position)); +} diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index 2a3dd0c852..b0b96d86be 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -174,7 +174,7 @@ private: // Scripting interface available to control the Reticle class ReticleInterface : public QObject { Q_OBJECT - Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(QVariant position READ getPosition WRITE setPosition) Q_PROPERTY(bool visible READ getVisible WRITE setVisible) Q_PROPERTY(float depth READ getDepth WRITE setDepth) Q_PROPERTY(glm::vec2 maximumPosition READ getMaximumPosition) @@ -198,8 +198,8 @@ public: Q_INVOKABLE float getDepth() { return _compositor->getReticleDepth(); } Q_INVOKABLE void setDepth(float depth) { _compositor->setReticleDepth(depth); } - Q_INVOKABLE glm::vec2 getPosition() { return _compositor->getReticlePosition(); } - Q_INVOKABLE void setPosition(glm::vec2 position) { _compositor->setReticlePosition(position); } + Q_INVOKABLE QVariant getPosition() const; + Q_INVOKABLE void setPosition(QVariant position); Q_INVOKABLE glm::vec2 getMaximumPosition() { return _compositor->getReticleMaximumPosition(); } diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 9ff82b3767..dab6438efa 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -32,6 +32,7 @@ function setupHandler(event, handler) { event.disconnect(handler); }); } + // If some capability is not available until expiration milliseconds after the last update. function TimeLock(expiration) { var last = 0; @@ -42,6 +43,7 @@ function TimeLock(expiration) { return ((optionalNow || Date.now()) - last) > expiration; }; } + var handControllerLockOut = new TimeLock(2000); function Trigger(label) { @@ -118,6 +120,10 @@ function ignoreMouseActivity() { if (!Reticle.allowMouseCapture) { return true; } + var pos = Reticle.position; + if (pos.x == -1 && pos.y == -1) { + return true; + } // Only we know if we moved it, which is why this script has to replace depthReticle.js if (!weMovedReticle) { return false; @@ -433,11 +439,12 @@ function clearSystemLaser() { } HMD.disableHandLasers(BOTH_HUD_LASERS); systemLaserOn = false; + weMovedReticle = true; + Reticle.position = { x: -1, y: -1 }; } function setColoredLaser() { // answer trigger state if lasers supported, else falsey. var color = (activeTrigger.state === 'full') ? LASER_TRIGGER_COLOR_XYZW : LASER_SEARCH_COLOR_XYZW; return HMD.setHandLasers(activeHudLaser, true, color, SYSTEM_LASER_DIRECTION) && activeTrigger.state; - } // MAIN OPERATIONS ----------- From b31300406a63fd39c34c570cc554f4ceadc87161 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 16:19:19 -0700 Subject: [PATCH 1171/1237] Don't extend glow line length --- libraries/render-utils/src/glowLine.slg | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/render-utils/src/glowLine.slg b/libraries/render-utils/src/glowLine.slg index 429cb8ee37..9af8eaa4d0 100644 --- a/libraries/render-utils/src/glowLine.slg +++ b/libraries/render-utils/src/glowLine.slg @@ -71,28 +71,24 @@ void main() { lineOrthogonal *= 0.02; gl_Position = gl_PositionIn[0]; - gl_Position.xy -= lineNormal; gl_Position.xy -= lineOrthogonal; outColor = inColor[0]; outLineDistance = vec3(-1.02, -1, gl_Position.z); EmitVertex(); gl_Position = gl_PositionIn[0]; - gl_Position.xy -= lineNormal; gl_Position.xy += lineOrthogonal; outColor = inColor[0]; outLineDistance = vec3(-1.02, 1, gl_Position.z); EmitVertex(); gl_Position = gl_PositionIn[1]; - gl_Position.xy += lineNormal; gl_Position.xy -= lineOrthogonal; outColor = inColor[1]; outLineDistance = vec3(1.02, -1, gl_Position.z); EmitVertex(); gl_Position = gl_PositionIn[1]; - gl_Position.xy += lineNormal; gl_Position.xy += lineOrthogonal; outColor = inColor[1]; outLineDistance = vec3(1.02, 1, gl_Position.z); From 8df4ed01d92b26411f1d7c2e7e565792aa411dc3 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 15 Jul 2016 16:22:41 -0700 Subject: [PATCH 1172/1237] fixed typo which oddly I was sure I already did. Seems I didn't push it? OK now it is pushed. --- libraries/audio/src/AudioInjector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index f94890ea7d..0b0be9353b 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -29,7 +29,7 @@ int audioInjectorPtrMetaTypeId = qRegisterMetaType(); AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { - return static_cast(static_cast(lhs) | static_cast(rhs)); + return static_cast(static_cast(lhs) & static_cast(rhs)); }; AudioInjectorState& operator&= (AudioInjectorState& lhs, AudioInjectorState rhs) { From f00114e53e3e0e8f93459670e950f6dac28b4ddb Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 16:46:20 -0700 Subject: [PATCH 1173/1237] Make it work with laser pointers (selection at least), and fix magic numbers. --- interface/resources/qml/hifi/Card.qml | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 3a2a52e3c0..b4a709215f 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -43,25 +43,30 @@ Rectangle { } } } + property int dropHorizontalOffset: 0; + property int dropVerticalOffset: 1; + property int dropRadius: 2; + property int dropSamples: 9; + property int dropSpread: 0; DropShadow { source: place; anchors.fill: place; - horizontalOffset: 0; - radius: 2; - samples: 9; + horizontalOffset: dropHorizontalOffset; + verticalOffset: dropVerticalOffset; + radius: dropRadius; + samples: dropSamples; color: hifi.colors.black; - verticalOffset: 1; - spread: 0; + spread: dropSpread; } DropShadow { source: users; anchors.fill: users; - horizontalOffset: 0; - radius: 2; - samples: 9; + horizontalOffset: dropHorizontalOffset; + verticalOffset: dropVerticalOffset; + radius: dropRadius; + samples: dropSamples; color: hifi.colors.black; - verticalOffset: 1; - spread: 0; + spread: dropSpread; } RalewaySemiBold { id: place; @@ -87,5 +92,6 @@ Rectangle { anchors.fill: parent; acceptedButtons: Qt.LeftButton; onClicked: goFunction(parent); + hoverEnabled: true; } } From b106dee3e9cb876cbead876246f79fcbb825d947 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 16:52:13 -0700 Subject: [PATCH 1174/1237] Provide our own default image. --- interface/resources/images/default-domain.gif | Bin 0 -> 4714 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 interface/resources/images/default-domain.gif diff --git a/interface/resources/images/default-domain.gif b/interface/resources/images/default-domain.gif new file mode 100644 index 0000000000000000000000000000000000000000..1418a54cddf29bcbf4e3e32ec8b903a7d1ac88a4 GIT binary patch literal 4714 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 literal 0 HcmV?d00001 From f73c2b884dfa3ea4e3eb657d56ed19983f79bef0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Jul 2016 17:01:37 -0700 Subject: [PATCH 1175/1237] Remove now-superflous Column. --- interface/resources/qml/AddressBarDialog.qml | 40 ++++++++++---------- interface/resources/qml/hifi/Card.qml | 3 +- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index b739842d1c..edc69d7c7d 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -59,7 +59,7 @@ Window { implicitWidth: backgroundImage.width implicitHeight: backgroundImage.height - Column { + Row { width: backgroundImage.width; anchors { bottom: backgroundImage.top; @@ -67,26 +67,24 @@ Window { right: backgroundImage.right; rightMargin: -104; // FIXME } - Row { - spacing: hifi.layout.spacing; - Card { - id: s0; - width: cardWidth; - height: cardHeight; - goFunction: goCard - } - Card { - id: s1; - width: cardWidth; - height: cardHeight; - goFunction: goCard - } - Card { - id: s2; - width: cardWidth; - height: cardHeight; - goFunction: goCard - } + spacing: hifi.layout.spacing; + Card { + id: s0; + width: cardWidth; + height: cardHeight; + goFunction: goCard + } + Card { + id: s1; + width: cardWidth; + height: cardHeight; + goFunction: goCard + } + Card { + id: s2; + width: cardWidth; + height: cardHeight; + goFunction: goCard } } diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index b4a709215f..7758c5800a 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -24,8 +24,7 @@ Rectangle { property alias usersText: users.text; property int textPadding: 20; property int textSize: 24; - // FIXME: let's get our own - property string defaultPicture: "http://www.davidluke.com/wp-content/themes/david-luke/media/ims/placeholder720.gif"; + property string defaultPicture: "../../images/default-domain.gif"; HifiConstants { id: hifi } Image { id: lobby; From fb99828e30302f2c615e74926dbe62a647def972 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 15 Jul 2016 17:09:27 -0700 Subject: [PATCH 1176/1237] PR feedback Code a bit more readable. Sadly (and I guess it makes sense), a enum class XXX is not a class, so you cannot have member functions for it. I can imagine no way to have a vtable if you are really representing it as a uint8_t or whatever. So, I put a stateHas function in the AudioInjector instead. Definite improvement. --- libraries/audio/src/AudioInjector.cpp | 15 +++++++++------ libraries/audio/src/AudioInjector.h | 5 +++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 0b0be9353b..e992e3c541 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -57,6 +57,10 @@ AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOpt } +bool AudioInjector::stateHas(AudioInjectorState state) const { + return (_state & state) == state; +} + void AudioInjector::setOptions(const AudioInjectorOptions& options) { // since options.stereo is computed from the audio stream, // we need to copy it from existing options just in case. @@ -70,20 +74,19 @@ void AudioInjector::finishNetworkInjection() { // if we are already finished with local // injection, then we are finished - if((_state & AudioInjectorState::LocalInjectionFinished) == AudioInjectorState::LocalInjectionFinished) { + if(stateHas(AudioInjectorState::LocalInjectionFinished)) { finish(); } } void AudioInjector::finishLocalInjection() { _state &= AudioInjectorState::LocalInjectionFinished; - if(_options.localOnly || ((_state & AudioInjectorState::NetworkInjectionFinished) == AudioInjectorState::NetworkInjectionFinished)) { + if(_options.localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { finish(); } } void AudioInjector::finish() { - bool shouldDelete = ((_state & AudioInjectorState::PendingDelete) == AudioInjectorState::PendingDelete); _state &= AudioInjectorState::Finished; emit finished(); @@ -94,7 +97,7 @@ void AudioInjector::finish() { _localBuffer = NULL; } - if (shouldDelete) { + if (stateHas(AudioInjectorState::PendingDelete)) { // we've been asked to delete after finishing, trigger a deleteLater here deleteLater(); } @@ -146,7 +149,7 @@ void AudioInjector::restart() { _hasSentFirstFrame = false; // check our state to decide if we need extra handling for the restart request - if ((_state & AudioInjectorState::Finished) == AudioInjectorState::Finished) { + if (stateHas(AudioInjectorState::Finished)) { // we finished playing, need to reset state so we can get going again _hasSetup = false; _shouldStop = false; @@ -216,7 +219,7 @@ static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1; static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0; int64_t AudioInjector::injectNextFrame() { - if ((_state & AudioInjectorState::NetworkInjectionFinished) == AudioInjectorState::NetworkInjectionFinished) { + if (stateHas(AudioInjectorState::NetworkInjectionFinished)) { qDebug() << "AudioInjector::injectNextFrame called but AudioInjector has finished and was not restarted. Returning."; return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index e13fc15c26..ce0c88247e 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -60,7 +60,7 @@ public: AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); - bool isFinished() const { return (_state & AudioInjectorState::Finished) == AudioInjectorState::Finished; } + bool isFinished() const { return (stateHas(AudioInjectorState::Finished)); } int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } @@ -74,6 +74,7 @@ public: bool isStereo() const { return _options.stereo; } void setLocalAudioInterface(AbstractAudioInterface* localAudioInterface) { _localAudioInterface = localAudioInterface; } + bool stateHas(AudioInjectorState state) const ; static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface); static AudioInjector* playSound(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface); static AudioInjector* playSound(SharedSoundPointer sound, const float volume, const float stretchFactor, const glm::vec3 position); @@ -89,7 +90,7 @@ public slots: void setOptions(const AudioInjectorOptions& options); float getLoudness() const { return _loudness; } - bool isPlaying() const { return (_state & AudioInjectorState::NotFinished) == AudioInjectorState::NotFinished; } + bool isPlaying() const { return stateHas(AudioInjectorState::NotFinished); } void finish(); void finishLocalInjection(); void finishNetworkInjection(); From 9ebc0c28c846b309716b8313e4e9bff9f5cbddb8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 17:12:49 -0700 Subject: [PATCH 1177/1237] Fix hover states for toolbar buttons --- interface/resources/qml/hifi/toolbars/ToolbarButton.qml | 4 ++-- scripts/system/edit.js | 5 +++++ scripts/system/examples.js | 4 ++++ scripts/system/goto.js | 4 ++++ scripts/system/hmd.js | 4 ++++ scripts/system/ignore.js | 4 ++++ scripts/system/mute.js | 8 +++++++- 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index f4693adec5..aed90cd433 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -50,12 +50,12 @@ Item { anchors.fill: parent onClicked: asyncClickSender.start(); onEntered: { - if (hoverState > 0) { + if (hoverState >= 0) { buttonState = hoverState; } } onExited: { - if (defaultState > 0) { + if (defaultState >= 0) { buttonState = defaultState; } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 05d554393f..f7e44933d7 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -234,11 +234,16 @@ var toolBar = (function() { objectName: EDIT_TOGGLE_BUTTON, imageURL: TOOL_ICON_URL + "edit.svg", visible: true, + alpha: 0.9, buttonState: 1, + hoverState: 3, + defaultState: 1, }); activeButton.clicked.connect(function() { that.setActive(!isActive); activeButton.writeProperty("buttonState", isActive ? 0 : 1); + activeButton.writeProperty("defaultState", isActive ? 0 : 1); + activeButton.writeProperty("hoverState", isActive ? 2 : 3); }); toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); diff --git a/scripts/system/examples.js b/scripts/system/examples.js index cea176f6b1..6fdb2a2874 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -56,11 +56,15 @@ var browseExamplesButton = toolBar.addButton({ imageURL: toolIconUrl + "market.svg", objectName: "examples", buttonState: 1, + defaultState: 1, + hoverState: 3, alpha: 0.9 }); function onExamplesWindowVisibilityChanged() { browseExamplesButton.writeProperty('buttonState', examplesWindow.visible ? 0 : 1); + browseExamplesButton.writeProperty('defaultState', examplesWindow.visible ? 0 : 1); + browseExamplesButton.writeProperty('hoverState', examplesWindow.visible ? 2 : 3); } function onClick() { toggleExamples(); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 9cdf579d72..2ed98c689b 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -17,11 +17,15 @@ var button = toolBar.addButton({ imageURL: Script.resolvePath("assets/images/tools/directory.svg"), visible: true, buttonState: 1, + defaultState: 1, + hoverState: 3, alpha: 0.9, }); function onAddressBarShown(visible) { button.writeProperty('buttonState', visible ? 0 : 1); + button.writeProperty('defaultState', visible ? 0 : 1); + button.writeProperty('hoverState', visible ? 2 : 3); } function onClicked(){ DialogsManager.toggleAddressBar(); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 9b749c306f..ac1918b001 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -22,6 +22,8 @@ var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var button; function onHmdChanged(isHmd) { button.writeProperty('buttonState', isHmd ? 0 : 1); + button.writeProperty('defaultState', isHmd ? 0 : 1); + button.writeProperty('hoverState', isHmd ? 2 : 3); } function onClicked(){ var isDesktop = Menu.isOptionChecked(desktopMenuItemName); @@ -32,6 +34,8 @@ if (headset) { objectName: "hmdToggle", imageURL: Script.resolvePath("assets/images/tools/switch.svg"), visible: true, + hoverState: 2, + defaultState: 0, alpha: 0.9, }); onHmdChanged(HMD.active); diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js index 39405a2e57..1c996a7fcc 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/ignore.js @@ -18,6 +18,8 @@ var button = toolbar.addButton({ imageURL: Script.resolvePath("assets/images/tools/ignore.svg"), visible: true, buttonState: 1, + defaultState: 2, + hoverState: 3, alpha: 0.9 }); @@ -46,6 +48,8 @@ function buttonClicked(){ } button.writeProperty('buttonState', isShowingOverlays ? 0 : 1); + button.writeProperty('defaultState', isShowingOverlays ? 0 : 1); + button.writeProperty('hoverState', isShowingOverlays ? 2 : 3); } button.clicked.connect(buttonClicked); diff --git a/scripts/system/mute.js b/scripts/system/mute.js index 511c013d5e..4ea8aee546 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -17,13 +17,19 @@ var button = toolBar.addButton({ imageURL: Script.resolvePath("assets/images/tools/mic.svg"), visible: true, buttonState: 1, + defaultState: 1, + hoverState: 3, alpha: 0.9 }); function onMuteToggled() { // We could just toggle state, but we're less likely to get out of wack if we read the AudioDevice. // muted => button "on" state => 1. go figure. - button.writeProperty('buttonState', AudioDevice.getMuted() ? 0 : 1); + var state = AudioDevice.getMuted() ? 0 : 1; + var hoverState = AudioDevice.getMuted() ? 2 : 3; + button.writeProperty('buttonState', state); + button.writeProperty('defaultState', state); + button.writeProperty('hoverState', hoverState); } onMuteToggled(); function onClicked(){ From 1145c3b59006e2bf3378f80a8d85e5f48b9858dc Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Jul 2016 17:45:16 -0700 Subject: [PATCH 1178/1237] Smaller and hotter circle, per Philip --- scripts/system/controllers/handControllerGrab.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4f0578369f..8020163d32 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -557,12 +557,12 @@ function MyController(hand) { this.searchSphereOn = function (location, size, color) { var rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); - var brightColor = colorPow(color, 0.1); + var brightColor = colorPow(color, 0.06); if (this.searchSphere === null) { var sphereProperties = { position: location, rotation: rotation, - outerRadius: size * 3.0, + outerRadius: size * 1.2, innerColor: brightColor, outerColor: color, innerAlpha: 0.9, @@ -581,7 +581,7 @@ function MyController(hand) { outerColor: color, innerAlpha: 1.0, outerAlpha: 0.0, - outerRadius: size * 3.0, + outerRadius: size * 1.2, visible: true }); } From 076b8cd2973c5956d9938eb4d58a835b742a1cd6 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 15 Jul 2016 17:47:54 -0700 Subject: [PATCH 1179/1237] Instrumenting the GPU git status --- .../gpu-gl/src/gpu/gl/GLBackendQuery.cpp | 3 +- .../render-utils/src/DebugDeferredBuffer.cpp | 6 ++- .../render-utils/src/DebugDeferredBuffer.h | 11 ++-- .../render-utils/src/RenderDeferredTask.cpp | 35 +++++++------ .../render-utils/src/RenderDeferredTask.h | 24 ++++----- .../render-utils/src/SurfaceGeometryPass.cpp | 50 +++++++++++++------ .../render-utils/src/SurfaceGeometryPass.h | 10 +++- .../utilities/lib/plotperf/PlotPerf.qml | 23 +++++++++ .../utilities/render/debugDeferredLighting.js | 8 +-- .../utilities/render/deferredLighting.qml | 7 ++- .../utilities/render/renderStatsGPU.js | 4 +- scripts/developer/utilities/render/stats.qml | 16 ------ .../developer/utilities/render/statsGPU.qml | 21 +++++++- .../utilities/render/surfaceGeometryPass.qml | 9 +++- 14 files changed, 145 insertions(+), 82 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp index 5191b161cf..09556d8e60 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp @@ -14,7 +14,8 @@ using namespace gpu; using namespace gpu::gl; -static bool timeElapsed = true; +// Eventually, we want to test with TIME_ELAPSED instead of TIMESTAMP +static bool timeElapsed = false; void GLBackend::do_beginQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 8c12beeaad..e32c0c2884 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -32,7 +32,7 @@ void DebugDeferredBufferConfig::setMode(int newMode) { if (newMode == mode) { return; } else if (newMode > DebugDeferredBuffer::CustomMode || newMode < 0) { - mode = DebugDeferredBuffer::CustomMode; + mode = 0; } else { mode = newMode; } @@ -347,6 +347,10 @@ void DebugDeferredBuffer::configure(const Config& config) { } void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& inputs) { + if (_mode == Off) { + return; + } + assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 10f7dad330..1053779577 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -50,7 +50,8 @@ protected: enum Mode : uint8_t { // Use Mode suffix to avoid collisions - DepthMode = 0, + Off = 0, + DepthMode, AlbedoMode, NormalMode, RoughnessMode, @@ -70,18 +71,20 @@ protected: ScatteringDebugMode, AmbientOcclusionMode, AmbientOcclusionBlurredMode, - CustomMode // Needs to stay last + CustomMode, // Needs to stay last + + NumModes, }; private: - Mode _mode; + Mode _mode{ Off }; glm::vec4 _size; struct CustomPipeline { gpu::PipelinePointer pipeline; mutable QFileInfo info; }; - using StandardPipelines = std::array; + using StandardPipelines = std::array; using CustomPipelines = std::unordered_map; bool pipelineNeedsUpdate(Mode mode, std::string customFile = std::string()) const; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 6ad0ae4843..0c47768bcf 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -100,7 +100,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // GPU jobs: Start preparing the primary, deferred and lighting buffer const auto primaryFramebuffer = addJob("PreparePrimaryBuffer"); - + const auto fullFrameRangeTimer = addJob("BeginRangeTimer"); + const auto opaqueRangeTimer = addJob("BeginOpaqueRangeTimer"); + const auto prepareDeferredInputs = SurfaceGeometryPass::Inputs(primaryFramebuffer, lightingModel).hasVarying(); const auto prepareDeferredOutputs = addJob("PrepareDeferred", prepareDeferredInputs); const auto deferredFramebuffer = prepareDeferredOutputs.getN(0); @@ -113,6 +115,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Once opaque is all rendered create stencil background addJob("DrawOpaqueStencil", deferredFramebuffer); + addJob("OpaqueRangeTimer", opaqueRangeTimer); + + const auto curvatureRangeTimer = addJob("BeginCurvatureRangeTimer"); // Opaque all rendered, generate surface geometry buffers const auto surfaceGeometryPassInputs = SurfaceGeometryPass::Inputs(deferredFrameTransform, deferredFramebuffer).hasVarying(); @@ -121,7 +126,6 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto curvatureFramebuffer = surfaceGeometryPassOutputs.getN(1); const auto linearDepthTexture = surfaceGeometryPassOutputs.getN(2); - const auto rangeTimer = addJob("BeginTimerRange"); // TODO: Push this 2 diffusion stages into surfaceGeometryPass as they are working together const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture).hasVarying(); @@ -130,6 +134,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto scatteringResource = addJob("Scattering"); + addJob("CurvatureRangeTimer", curvatureRangeTimer); + // AO job addJob("AmbientOcclusion"); @@ -150,6 +156,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto transparentsInputs = DrawDeferred::Inputs(transparents, lightingModel).hasVarying(); addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); + const auto toneAndPostRangeTimer = addJob("BeginToneAndPostRangeTimer"); + // Lighting Buffer ready for tone mapping const auto toneMappingInputs = render::Varying(ToneMappingDeferred::Inputs(lightingFramebuffer, primaryFramebuffer)); addJob("ToneMapping", toneMappingInputs); @@ -188,11 +196,13 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // AA job to be revisited addJob("Antialiasing", primaryFramebuffer); - addJob("RangeTimer", rangeTimer); + addJob("ToneAndPostRangeTimer", toneAndPostRangeTimer); + // Blit! addJob("Blit", primaryFramebuffer); - - + + addJob("RangeTimer", fullFrameRangeTimer); + } void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { @@ -210,30 +220,19 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend RenderArgs* args = renderContext->args; auto config = std::static_pointer_cast(renderContext->jobConfig); - /* gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - _gpuTimer.begin(batch); - });*/ - for (auto job : _jobs) { job.run(sceneContext, renderContext); } - - /*gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - _gpuTimer.end(batch); - });*/ - -// config->gpuTime = _gpuTimer.getAverage(); - } -void BeginTimerGPU::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer) { +void BeginGPURangeTimer::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer) { timer = _gpuTimer; gpu::doInBatch(renderContext->args->_context, [&](gpu::Batch& batch) { _gpuTimer->begin(batch); }); } -void EndTimerGPU::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::RangeTimerPointer& timer) { +void EndGPURangeTimer::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::RangeTimerPointer& timer) { gpu::doInBatch(renderContext->args->_context, [&](gpu::Batch& batch) { timer->end(batch); }); diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 032cd48b2e..749cc09edc 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -17,11 +17,11 @@ #include "LightingModel.h" -class BeginTimerGPU { +class BeginGPURangeTimer { public: - using JobModel = render::Job::ModelO; + using JobModel = render::Job::ModelO; - BeginTimerGPU() : _gpuTimer(std::make_shared()) {} + BeginGPURangeTimer() : _gpuTimer(std::make_shared()) {} void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer); @@ -30,23 +30,23 @@ protected: }; -class EndTimerGPUConfig : public render::Job::Config { +class GPURangeTimerConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(double gpuTime READ getGpuTime) public: double getGpuTime() { return gpuTime; } protected: - friend class EndTimerGPU; + friend class EndGPURangeTimer; double gpuTime; }; -class EndTimerGPU { +class EndGPURangeTimer { public: - using Config = EndTimerGPUConfig; - using JobModel = render::Job::ModelI; + using Config = GPURangeTimerConfig; + using JobModel = render::Job::ModelI; - EndTimerGPU() {} + EndGPURangeTimer() {} void configure(const Config& config) {} void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::RangeTimerPointer& timer); @@ -213,13 +213,13 @@ public: class RenderDeferredTaskConfig : public render::Task::Config { Q_OBJECT - Q_PROPERTY(quint64 gpuTime READ getGpuTime) + Q_PROPERTY(double gpuTime READ getGpuTime) public: - quint64 getGpuTime() { return gpuTime; } + double getGpuTime() { return gpuTime; } protected: friend class RenderDeferredTask; - quint64 gpuTime; + double gpuTime; }; class RenderDeferredTask : public render::Task { diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 1d38a1bc4c..66a1c89e77 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -45,14 +45,17 @@ void SurfaceGeometryFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& d } if (reset) { - _linearDepthFramebuffer.reset(); - _linearDepthTexture.reset(); - _curvatureFramebuffer.reset(); - _curvatureTexture.reset(); - + clear(); } } +void SurfaceGeometryFramebuffer::clear() { + _linearDepthFramebuffer.reset(); + _linearDepthTexture.reset(); + _curvatureFramebuffer.reset(); + _curvatureTexture.reset(); +} + void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; @@ -62,12 +65,12 @@ void SurfaceGeometryFramebuffer::allocate() { _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); - // _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); + _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); - _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width >> getResolutionLevel(), height >> getResolutionLevel(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - // _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); +// _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); } gpu::FramebufferPointer SurfaceGeometryFramebuffer::getLinearDepthFramebuffer() { @@ -98,6 +101,12 @@ gpu::TexturePointer SurfaceGeometryFramebuffer::getCurvatureTexture() { return _curvatureTexture; } +void SurfaceGeometryFramebuffer::setResolutionLevel(int resolutionLevel) { + if (resolutionLevel != getResolutionLevel()) { + clear(); + _resolutionLevel = resolutionLevel; + } +} SurfaceGeometryPass::SurfaceGeometryPass() { @@ -106,7 +115,7 @@ SurfaceGeometryPass::SurfaceGeometryPass() { } void SurfaceGeometryPass::configure(const Config& config) { - + if (config.depthThreshold != getCurvatureDepthThreshold()) { _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold; } @@ -118,8 +127,14 @@ void SurfaceGeometryPass::configure(const Config& config) { if (config.curvatureScale != getCurvatureScale()) { _parametersBuffer.edit().curvatureInfo.w = config.curvatureScale; } + + if (!_surfaceGeometryFramebuffer) { + _surfaceGeometryFramebuffer = std::make_shared(); + } + _surfaceGeometryFramebuffer->setResolutionLevel(config.resolutionLevel); } + void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& curvatureAndDepth) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -153,15 +168,17 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c // _gpuTimer.begin(batch); // }); - + auto depthViewport = args->_viewport; + auto curvatureViewport = depthViewport >> _surfaceGeometryFramebuffer->getResolutionLevel(); + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { _gpuTimer.begin(batch); batch.enableStereo(false); - batch.setViewportTransform(args->_viewport); + batch.setViewportTransform(depthViewport); batch.setProjectionTransform(glm::mat4()); batch.setViewTransform(Transform()); - batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getFrameSize(), args->_viewport)); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getDepthFrameSize(), depthViewport)); batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); @@ -172,9 +189,10 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setPipeline(linearDepthPipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); - - _gpuTimer.end(batch); - + + batch.setViewportTransform(curvatureViewport); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getCurvatureFrameSize(), curvatureViewport)); + // Curvature pass batch.setFramebuffer(curvatureFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); @@ -185,7 +203,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, nullptr); - // _gpuTimer.end(batch); + _gpuTimer.end(batch); }); // gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 30fb6a53ef..bdd3997536 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -34,9 +34,14 @@ public: // Update the depth buffer which will drive the allocation of all the other resources according to its size. void updatePrimaryDepth(const gpu::TexturePointer& depthBuffer); gpu::TexturePointer getPrimaryDepthTexture(); - const glm::ivec2& getFrameSize() const { return _frameSize; } + const glm::ivec2& getDepthFrameSize() const { return _frameSize; } + glm::ivec2 getCurvatureFrameSize() const { return _frameSize >> _resolutionLevel; } + + void setResolutionLevel(int level); + int getResolutionLevel() const { return _resolutionLevel; } protected: + void clear(); void allocate(); gpu::TexturePointer _primaryDepthTexture; @@ -48,6 +53,7 @@ protected: gpu::TexturePointer _curvatureTexture; glm::ivec2 _frameSize; + int _resolutionLevel{ 0 }; }; using SurfaceGeometryFramebufferPointer = std::shared_ptr; @@ -57,6 +63,7 @@ class SurfaceGeometryPassConfig : public render::Job::Config { Q_PROPERTY(float depthThreshold MEMBER depthThreshold NOTIFY dirty) Q_PROPERTY(float basisScale MEMBER basisScale NOTIFY dirty) Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) + Q_PROPERTY(int resolutionLevel MEMBER resolutionLevel NOTIFY dirty) Q_PROPERTY(double gpuTime READ getGpuTime) public: SurfaceGeometryPassConfig() : render::Job::Config(true) {} @@ -64,6 +71,7 @@ public: float depthThreshold{ 0.02f }; // meters float basisScale{ 1.0f }; float curvatureScale{ 10.0f }; + int resolutionLevel{ 0 }; double getGpuTime() { return gpuTime; } diff --git a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml index 6871ffe6a6..c6b95dca30 100644 --- a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml +++ b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml @@ -16,6 +16,8 @@ Item { width: parent.width height: 100 + property int hitboxExtension : 20 + // The title of the graph property string title @@ -71,6 +73,11 @@ Item { Component.onCompleted: { createValues(); } + function resetMax() { + for (var i = 0; i < _values.length; i++) { + _values[i].valueMax *= 0.25 // Fast reduce the max value as we click + } + } function pullFreshValues() { // Wait until values are created to begin pulling @@ -125,6 +132,7 @@ Item { Canvas { id: mycanvas anchors.fill:parent + onPaint: { var lineHeight = 12; @@ -199,4 +207,19 @@ Item { displayTitle(ctx, title, valueMax) } } + + MouseArea { + id: hitbox + anchors.fill:mycanvas + + anchors.topMargin: -hitboxExtension + anchors.bottomMargin: -hitboxExtension + anchors.leftMargin: -hitboxExtension + anchors.rightMargin: -hitboxExtension + + onClicked: { + print("PerfPlot clicked!") + resetMax(); + } + } } diff --git a/scripts/developer/utilities/render/debugDeferredLighting.js b/scripts/developer/utilities/render/debugDeferredLighting.js index 852e7e7f5c..b32a924c10 100644 --- a/scripts/developer/utilities/render/debugDeferredLighting.js +++ b/scripts/developer/utilities/render/debugDeferredLighting.js @@ -13,14 +13,15 @@ var qml = Script.resolvePath('deferredLighting.qml'); var window = new OverlayWindow({ title: 'Lighting', source: qml, - width: 400, height:250, + width: 400, height:220, }); -window.setPosition(250, 800);a +window.setPosition(Window.innerWidth - 420, 50); window.closed.connect(function() { Script.stop(); }); var DDB = Render.RenderDeferredTask.DebugDeferredBuffer; DDB.enabled = true; +DDB.mode = 0; // Debug buffer sizing var resizing = false; @@ -36,7 +37,7 @@ Script.scriptEnding.connect(function () { DDB.enabled = false; }); function shouldStartResizing(eventX) { var x = Math.abs(eventX - Window.innerWidth * (1.0 + DDB.size.x) / 2.0); var mode = DDB.mode; - return mode !== -1 && x < 20; + return mode !== 0 && x < 20; } function setDebugBufferSize(x) { @@ -45,3 +46,4 @@ function setDebugBufferSize(x) { DDB.size = { x: x, y: -1, z: 1, w: 1 }; } + diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index bc34423a9c..3f1b1ac010 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -13,8 +13,7 @@ import "configSlider" Column { spacing: 8 - - + Row { spacing: 8 Column { @@ -129,7 +128,7 @@ Column { property var config: Render.getConfig("DebugDeferredBuffer") function setDebugMode(mode) { - framebuffer.config.enabled = (mode != -1); + framebuffer.config.enabled = (mode != 0); framebuffer.config.mode = mode; } @@ -162,7 +161,7 @@ Column { ListElement { text: "Custom"; color: "White" } } width: 200 - onCurrentIndexChanged: { framebuffer.setDebugMode(currentIndex - 1) } + onCurrentIndexChanged: { framebuffer.setDebugMode(currentIndex) } } } } diff --git a/scripts/developer/utilities/render/renderStatsGPU.js b/scripts/developer/utilities/render/renderStatsGPU.js index c5955b0d5d..fc35cb053a 100644 --- a/scripts/developer/utilities/render/renderStatsGPU.js +++ b/scripts/developer/utilities/render/renderStatsGPU.js @@ -14,8 +14,8 @@ var qml = Script.resolvePath('statsGPU.qml'); var window = new OverlayWindow({ title: 'Render Stats GPU', source: qml, - width: 320, + width: 400, height: 200 }); -window.setPosition(50, 20); +window.setPosition(Window.innerWidth - 420, 50 + 250 + 50); window.closed.connect(function() { Script.stop(); }); \ No newline at end of file diff --git a/scripts/developer/utilities/render/stats.qml b/scripts/developer/utilities/render/stats.qml index 27f7580b57..d20efc1f10 100644 --- a/scripts/developer/utilities/render/stats.qml +++ b/scripts/developer/utilities/render/stats.qml @@ -242,22 +242,6 @@ Item { } ] } - PlotPerf { - title: "Timing" - height: parent.evalEvenHeight() - object: parent.drawOpaqueConfig - valueUnit: "ms" - valueScale: 1 - valueNumDigits: "4" - plots: [ - { - object: Render.getConfig("RenderDeferredTask"), - prop: "gpuTime", - label: "RenderFrameGPU", - color: "#00FFFF" - } - ] - } } } diff --git a/scripts/developer/utilities/render/statsGPU.qml b/scripts/developer/utilities/render/statsGPU.qml index 0f529d2d7f..7282b753cc 100644 --- a/scripts/developer/utilities/render/statsGPU.qml +++ b/scripts/developer/utilities/render/statsGPU.qml @@ -36,12 +36,23 @@ Item { valueUnit: "ms" valueScale: 1 valueNumDigits: "4" - plots: [ + plots: [ + { + object: Render.getConfig("OpaqueRangeTimer"), + prop: "gpuTime", + label: "Opaque", + color: "#0000FF" + }, { object: Render.getConfig("SurfaceGeometry"), prop: "gpuTime", - label: "SurfaceGeometryGPU", + label: "SurfaceGeometry", color: "#00FFFF" + },{ + object: Render.getConfig("CurvatureRangeTimer"), + prop: "gpuTime", + label: "Curvature", + color: "#00FF00" }, { object: Render.getConfig("RenderDeferred"), @@ -51,6 +62,12 @@ Item { } , { + object: Render.getConfig("ToneAndPostRangeTimer"), + prop: "gpuTime", + label: "tone and post", + color: "#FF0000" + } + ,{ object: Render.getConfig("RangeTimer"), prop: "gpuTime", label: "FrameGPU", diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index 061402b80d..0021ecc96c 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -19,10 +19,15 @@ Column { Column{ Repeater { - model: [ "Depth Threshold:depthThreshold:0.1", "Basis Scale:basisScale:2.0", "Curvature Scale:curvatureScale:100.0" ] + model: [ + "Depth Threshold:depthThreshold:0.1:false", + "Basis Scale:basisScale:2.0:false", + "Curvature Scale:curvatureScale:100.0:false", + "Downscale:resolutionLevel:4:true" + ] ConfigSlider { label: qsTr(modelData.split(":")[0]) - integral: false + integral: (modelData.split(":")[3] == 'true') config: Render.getConfig("SurfaceGeometry") property: modelData.split(":")[1] max: modelData.split(":")[2] From 59ac2a789ff9bd406bc20c4605754f64f9effe3c Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Jul 2016 18:07:19 -0700 Subject: [PATCH 1180/1237] Fix the AudioMixer distance attenuation. The zone settings are still used, and still match the documentation where 0 = no attenuation and 1 = max attenuation. The default is now 0.5 which corresponds to -6dB per doubling of distance. This is the attenuation for a spherical wave in the free field. --- assignment-client/src/audio/AudioMixer.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 08e6e029a0..40e22f855a 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -62,7 +62,7 @@ #include "AudioMixer.h" const float LOUDNESS_TO_DISTANCE_RATIO = 0.00001f; -const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.18f; +const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.5f; // attenuation = -6dB * log2(distance) const float DEFAULT_NOISE_MUTING_THRESHOLD = 0.003f; const QString AUDIO_MIXER_LOGGING_TARGET_NAME = "audio-mixer"; const QString AUDIO_ENV_GROUP_KEY = "audio_env"; @@ -141,13 +141,14 @@ float AudioMixer::gainForSource(const PositionalAudioStream& streamToAdd, } if (distanceBetween >= ATTENUATION_BEGINS_AT_DISTANCE) { - // calculate the distance coefficient using the distance to this node - float distanceCoefficient = 1.0f - (logf(distanceBetween / ATTENUATION_BEGINS_AT_DISTANCE) / logf(2.0f) - * attenuationPerDoublingInDistance); - if (distanceCoefficient < 0) { - distanceCoefficient = 0; - } + // translate the zone setting to gain per log2(distance) + float g = 1.0f - attenuationPerDoublingInDistance; + g = (g < EPSILON) ? EPSILON : g; + g = (g > 1.0f) ? 1.0f : g; + + // calculate the distance coefficient using the distance to this node + float distanceCoefficient = exp2f(log2f(g) * log2f(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE)); // multiply the current attenuation coefficient by the distance coefficient gain *= distanceCoefficient; From 1ee608ad2cb13ce7f23374ea93f7f040a717d16b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Jul 2016 18:23:12 -0700 Subject: [PATCH 1181/1237] Cleanup --- libraries/audio-client/src/AudioClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 84d0146f3b..1ec2f40928 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1309,7 +1309,7 @@ float AudioClient::gainForSource(float distance, float volume) { // attenuate based on distance if (distance >= ATTENUATION_BEGINS_AT_DISTANCE) { - gain /= distance; // attenuation = -6dB * log2(distance) + gain /= (distance/ATTENUATION_BEGINS_AT_DISTANCE); // attenuation = -6dB * log2(distance) } return gain; From d446e1bcf54eee13320b071910e689b5be910993 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 15 Jul 2016 19:15:58 -0700 Subject: [PATCH 1182/1237] Phone home with suggestions activity usage. --- interface/resources/qml/AddressBarDialog.qml | 6 +++--- interface/src/ui/AddressBarDialog.cpp | 4 ++-- interface/src/ui/AddressBarDialog.h | 2 +- libraries/networking/src/AddressManager.cpp | 4 ++-- libraries/networking/src/AddressManager.h | 5 +++-- libraries/networking/src/UserActivityLogger.cpp | 3 +++ 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index edc69d7c7d..e8318ef1b8 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -47,7 +47,7 @@ Window { function goCard(card) { addressLine.text = card.userStory.name; - toggleOrGo(); + toggleOrGo(true); } property var allDomains: []; property var suggestionChoices: []; @@ -327,9 +327,9 @@ Window { } } - function toggleOrGo() { + function toggleOrGo(fromSuggestions) { if (addressLine.text !== "") { - addressBarDialog.loadAddress(addressLine.text) + addressBarDialog.loadAddress(addressLine.text, fromSuggestions) } root.shown = false; } diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index 6fb437e312..a4ef8a913f 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -40,10 +40,10 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare _forwardEnabled = !(DependencyManager::get()->getForwardStack().isEmpty()); } -void AddressBarDialog::loadAddress(const QString& address) { +void AddressBarDialog::loadAddress(const QString& address, bool fromSuggestions) { qDebug() << "Called LoadAddress with address " << address; if (!address.isEmpty()) { - DependencyManager::get()->handleLookupString(address); + DependencyManager::get()->handleLookupString(address, fromSuggestions); } } diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index bbce52c67c..6c7620164b 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -34,7 +34,7 @@ protected: void displayAddressOfflineMessage(); void displayAddressNotFoundMessage(); - Q_INVOKABLE void loadAddress(const QString& address); + Q_INVOKABLE void loadAddress(const QString& address, bool fromSuggestions = false); Q_INVOKABLE void loadHome(); Q_INVOKABLE void loadBack(); Q_INVOKABLE void loadForward(); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 7ed3888be0..b3a022cc3a 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -221,7 +221,7 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { return false; } -void AddressManager::handleLookupString(const QString& lookupString) { +void AddressManager::handleLookupString(const QString& lookupString, bool fromSuggestions) { if (!lookupString.isEmpty()) { // make this a valid hifi URL and handle it off to handleUrl QString sanitizedString = lookupString.trimmed(); @@ -236,7 +236,7 @@ void AddressManager::handleLookupString(const QString& lookupString) { lookupURL = QUrl(lookupString); } - handleUrl(lookupURL); + handleUrl(lookupURL, fromSuggestions ? Suggestions : UserInput); } } diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index a3aaee3ba2..c013da3a72 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -49,7 +49,8 @@ public: StartupFromSettings, DomainPathResponse, Internal, - AttemptedRefresh + AttemptedRefresh, + Suggestions }; bool isConnected(); @@ -77,7 +78,7 @@ public: std::function localSandboxNotRunningDoThat); public slots: - void handleLookupString(const QString& lookupString); + void handleLookupString(const QString& lookupString, bool fromSuggestions = false); // we currently expect this to be called from NodeList once handleLookupString has been called with a path bool goToViewpointForPath(const QString& viewpointString, const QString& pathString) diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index eba4d31167..75e15db2a4 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -178,6 +178,9 @@ void UserActivityLogger::wentTo(AddressManager::LookupTrigger lookupTrigger, QSt case AddressManager::StartupFromSettings: trigger = "StartupFromSettings"; break; + case AddressManager::Suggestions: + trigger = "Suggesions"; + break; default: return; } From 8c0eb1e4d27f4d925317a09fedf7632138f9172e Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 15 Jul 2016 19:48:15 -0700 Subject: [PATCH 1183/1237] Fixed some bad logic When I "fixed" my or instead of and issue, I did it in the wrong direction. But it looked right :) Now it is. Sigh. Long story how it got there, but it seems good now. --- libraries/audio/src/AudioInjector.cpp | 14 +++++++------- libraries/audio/src/AudioInjector.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index e992e3c541..9c49ce66d8 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -32,8 +32,8 @@ AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { return static_cast(static_cast(lhs) & static_cast(rhs)); }; -AudioInjectorState& operator&= (AudioInjectorState& lhs, AudioInjectorState rhs) { - lhs = static_cast(static_cast(lhs) & static_cast(rhs)); +AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs) { + lhs = static_cast(static_cast(lhs) | static_cast(rhs)); return lhs; }; @@ -70,7 +70,7 @@ void AudioInjector::setOptions(const AudioInjectorOptions& options) { } void AudioInjector::finishNetworkInjection() { - _state &= AudioInjectorState::NetworkInjectionFinished; + _state |= AudioInjectorState::NetworkInjectionFinished; // if we are already finished with local // injection, then we are finished @@ -80,14 +80,14 @@ void AudioInjector::finishNetworkInjection() { } void AudioInjector::finishLocalInjection() { - _state &= AudioInjectorState::LocalInjectionFinished; + _state |= AudioInjectorState::LocalInjectionFinished; if(_options.localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { finish(); } } void AudioInjector::finish() { - _state &= AudioInjectorState::Finished; + _state |= AudioInjectorState::Finished; emit finished(); @@ -413,7 +413,7 @@ void AudioInjector::triggerDeleteAfterFinish() { if (_state == AudioInjectorState::Finished) { stopAndDeleteLater(); } else { - _state &= AudioInjectorState::PendingDelete; + _state |= AudioInjectorState::PendingDelete; } } @@ -459,7 +459,7 @@ AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjector* sound = playSound(buffer, options, localInterface); if (sound) { - sound->_state &= AudioInjectorState::PendingDelete; + sound->_state |= AudioInjectorState::PendingDelete; } return sound; diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index ce0c88247e..9bdfcacb5c 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -42,7 +42,7 @@ enum class AudioInjectorState : uint8_t { }; AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs); -AudioInjectorState& operator&= (AudioInjectorState& lhs, AudioInjectorState rhs); +AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs); // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object // until it dies. From 03cb6175ab79d143eecc015e3dee21375e70326d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Jul 2016 09:10:21 -0700 Subject: [PATCH 1184/1237] Support triangle and hexagon shapes, add shape support to overlays. --- interface/src/ui/overlays/Overlays.cpp | 3 + interface/src/ui/overlays/Shape3DOverlay.cpp | 130 ++++++++++++++++++ interface/src/ui/overlays/Shape3DOverlay.h | 46 +++++++ .../src/RenderableShapeEntityItem.cpp | 6 +- libraries/entities/src/ShapeEntityItem.cpp | 8 +- libraries/entities/src/ShapeEntityItem.h | 2 + libraries/networking/src/udt/PacketHeaders.h | 3 +- libraries/render-utils/src/GeometryCache.cpp | 93 ++++++++++++- libraries/render-utils/src/GeometryCache.h | 16 ++- scripts/system/html/entityProperties.html | 3 + 10 files changed, 298 insertions(+), 12 deletions(-) create mode 100644 interface/src/ui/overlays/Shape3DOverlay.cpp create mode 100644 interface/src/ui/overlays/Shape3DOverlay.h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 9ff7f6268f..e99ca3a9e0 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -22,6 +22,7 @@ #include "Image3DOverlay.h" #include "Circle3DOverlay.h" #include "Cube3DOverlay.h" +#include "Shape3DOverlay.h" #include "ImageOverlay.h" #include "Line3DOverlay.h" #include "LocalModelsOverlay.h" @@ -157,6 +158,8 @@ unsigned int Overlays::addOverlay(const QString& type, const QVariant& propertie thisOverlay = std::make_shared(); } else if (type == Text3DOverlay::TYPE) { thisOverlay = std::make_shared(); + } else if (type == Shape3DOverlay::TYPE) { + thisOverlay = std::make_shared(); } else if (type == Cube3DOverlay::TYPE) { thisOverlay = std::make_shared(); } else if (type == Sphere3DOverlay::TYPE) { diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp new file mode 100644 index 0000000000..cd07385aab --- /dev/null +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -0,0 +1,130 @@ +// +// Shape3DOverlay.cpp +// interface/src/ui/overlays +// +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "Shape3DOverlay.h" + +#include +#include +#include +#include + +QString const Shape3DOverlay::TYPE = "shape"; + +Shape3DOverlay::Shape3DOverlay(const Shape3DOverlay* Shape3DOverlay) : + Volume3DOverlay(Shape3DOverlay) +{ +} + +void Shape3DOverlay::render(RenderArgs* args) { + if (!_visible) { + return; // do nothing if we're not visible + } + + float alpha = getAlpha(); + xColor color = getColor(); + const float MAX_COLOR = 255.0f; + glm::vec4 cubeColor(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha); + + // TODO: handle registration point?? + glm::vec3 position = getPosition(); + glm::vec3 dimensions = getDimensions(); + glm::quat rotation = getRotation(); + + auto batch = args->_batch; + + if (batch) { + Transform transform; + transform.setTranslation(position); + transform.setRotation(rotation); + auto geometryCache = DependencyManager::get(); + auto pipeline = args->_pipeline; + if (!pipeline) { + pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline(); + } + + transform.setScale(dimensions); + batch->setModelTransform(transform); + if (_isSolid) { + geometryCache->renderSolidShapeInstance(*batch, _shape, cubeColor, pipeline); + } else { + geometryCache->renderWireShapeInstance(*batch, _shape, cubeColor, pipeline); + } + } +} + +const render::ShapeKey Shape3DOverlay::getShapeKey() { + auto builder = render::ShapeKey::Builder(); + if (getAlpha() != 1.0f) { + builder.withTranslucent(); + } + if (!getIsSolid()) { + builder.withUnlit().withDepthBias(); + } + return builder.build(); +} + +Shape3DOverlay* Shape3DOverlay::createClone() const { + return new Shape3DOverlay(this); +} + + +static const std::array shapeStrings { { + "Line", + "Triangle", + "Quad", + "Hexagon", + "Octagon", + "Circle", + "Cube", + "Sphere", + "Tetrahedron", + "Octahedron", + "Dodecahedron", + "Icosahedron", + "Torus", + "Cone", + "Cylinder" +} }; + + +void Shape3DOverlay::setProperties(const QVariantMap& properties) { + Volume3DOverlay::setProperties(properties); + + auto shape = properties["shape"]; + if (shape.isValid()) { + const QString shapeStr = shape.toString(); + for (size_t i = 0; i < shapeStrings.size(); ++i) { + if (shapeStr == shapeStrings[i]) { + this->_shape = static_cast(i); + break; + } + } + } + + auto borderSize = properties["borderSize"]; + + if (borderSize.isValid()) { + float value = borderSize.toFloat(); + setBorderSize(value); + } +} + +QVariant Shape3DOverlay::getProperty(const QString& property) { + if (property == "borderSize") { + return _borderSize; + } + + if (property == "shape") { + return shapeStrings[_shape]; + } + + return Volume3DOverlay::getProperty(property); +} diff --git a/interface/src/ui/overlays/Shape3DOverlay.h b/interface/src/ui/overlays/Shape3DOverlay.h new file mode 100644 index 0000000000..2361001721 --- /dev/null +++ b/interface/src/ui/overlays/Shape3DOverlay.h @@ -0,0 +1,46 @@ +// +// Shape3DOverlay.h +// interface/src/ui/overlays +// +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Shape3DOverlay_h +#define hifi_Shape3DOverlay_h + +#include "Volume3DOverlay.h" + +#include + +class Shape3DOverlay : public Volume3DOverlay { + Q_OBJECT + +public: + static QString const TYPE; + virtual QString getType() const override { return TYPE; } + + Shape3DOverlay() {} + Shape3DOverlay(const Shape3DOverlay* Shape3DOverlay); + + virtual void render(RenderArgs* args) override; + virtual const render::ShapeKey getShapeKey() override; + + virtual Shape3DOverlay* createClone() const override; + + float getBorderSize() const { return _borderSize; } + + void setBorderSize(float value) { _borderSize = value; } + + void setProperties(const QVariantMap& properties) override; + QVariant getProperty(const QString& property) override; + +private: + float _borderSize; + GeometryCache::Shape _shape { GeometryCache::Hexagon }; +}; + + +#endif // hifi_Shape3DOverlay_h diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index ec07e10ccf..48ad05a714 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -23,9 +23,11 @@ // 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; -static GeometryCache::Shape MAPPING[entity::NUM_SHAPES] = { +static std::array MAPPING { { GeometryCache::Triangle, GeometryCache::Quad, + GeometryCache::Hexagon, + GeometryCache::Octagon, GeometryCache::Circle, GeometryCache::Cube, GeometryCache::Sphere, @@ -36,7 +38,7 @@ static GeometryCache::Shape MAPPING[entity::NUM_SHAPES] = { GeometryCache::Torus, GeometryCache::Cone, GeometryCache::Cylinder, -}; +} }; RenderableShapeEntityItem::Pointer RenderableShapeEntityItem::baseFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 84208cc6f1..141526643c 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -20,17 +20,19 @@ #include "ShapeEntityItem.h" namespace entity { - static const std::vector shapeStrings { { + static const std::array shapeStrings { { "Triangle", "Quad", - "Circle", + "Hexagon", + "Octagon", + "Circle", "Cube", "Sphere", "Tetrahedron", "Octahedron", "Dodecahedron", "Icosahedron", - "Torus", + "Torus", "Cone", "Cylinder" } }; diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index 2ae4ae2ca1..122fc98dc0 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -15,6 +15,8 @@ namespace entity { enum Shape { Triangle, Quad, + Hexagon, + Octagon, Circle, Cube, Sphere, diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 9581f3ca20..85030135a1 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -97,7 +97,8 @@ public: ICEServerHeartbeatACK, NegotiateAudioFormat, SelectedAudioFormat, - LAST_PACKET_TYPE = SelectedAudioFormat + MoreEntityShapes, + LAST_PACKET_TYPE = MoreEntityShapes }; }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 4558b68af9..50a93d2200 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -60,6 +60,18 @@ static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3); static const gpu::Type SHAPE_INDEX_TYPE = gpu::UINT32; static const uint SHAPE_INDEX_SIZE = sizeof(gpu::uint32); +template +std::vector polygon() { + std::vector result; + result.reserve(SIDES); + double angleIncrement = 2.0 * M_PI / SIDES; + for (size_t i = 0; i < SIDES; ++i) { + double angle = (double)i * angleIncrement; + result.push_back(vec3{ cos(angle) * 0.5, 0.0, sin(angle) * 0.5 }); + } + return result; +} + void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const geometry::VertexVector& vertices) { vertexBuffer->append(vertices); @@ -239,6 +251,75 @@ void setupSmoothShape(GeometryCache::ShapeData& shapeData, const geometry::Solid shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); } +template +void extrudePolygon(GeometryCache::ShapeData& shapeData, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + using namespace geometry; + Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); + VertexVector vertices; + IndexVector solidIndices, wireIndices; + + // Top and bottom faces + std::vector shape = polygon(); + for (const vec3& v : shape) { + vertices.push_back(vec3(v.x, 0.5f, v.z)); + vertices.push_back(vec3(0, 1, 0)); + } + for (const vec3& v : shape) { + vertices.push_back(vec3(v.x, -0.5f, v.z)); + vertices.push_back(vec3(0, -1, 0)); + } + for (uint32_t i = 2; i < N; ++i) { + solidIndices.push_back(baseVertex + 0); + solidIndices.push_back(baseVertex + i); + solidIndices.push_back(baseVertex + i - 1); + solidIndices.push_back(baseVertex + N); + solidIndices.push_back(baseVertex + i + N - 1); + solidIndices.push_back(baseVertex + i + N); + } + for (uint32_t i = 1; i <= N; ++i) { + wireIndices.push_back(baseVertex + (i % N)); + wireIndices.push_back(baseVertex + i - 1); + wireIndices.push_back(baseVertex + (i % N) + N); + wireIndices.push_back(baseVertex + (i - 1) + N); + } + + // Now do the sides + baseVertex += 2 * N; + + for (uint32_t i = 0; i < N; ++i) { + vec3 left = shape[i]; + vec3 right = shape[(i + 1) % N]; + vec3 normal = glm::normalize(left + right); + vec3 topLeft = vec3(left.x, 0.5f, left.z); + vec3 topRight = vec3(right.x, 0.5f, right.z); + vec3 bottomLeft = vec3(left.x, -0.5f, left.z); + vec3 bottomRight = vec3(right.x, -0.5f, right.z); + + vertices.push_back(topLeft); + vertices.push_back(normal); + vertices.push_back(bottomLeft); + vertices.push_back(normal); + vertices.push_back(topRight); + vertices.push_back(normal); + vertices.push_back(bottomRight); + vertices.push_back(normal); + + solidIndices.push_back(baseVertex + 0); + solidIndices.push_back(baseVertex + 2); + solidIndices.push_back(baseVertex + 1); + solidIndices.push_back(baseVertex + 1); + solidIndices.push_back(baseVertex + 2); + solidIndices.push_back(baseVertex + 3); + wireIndices.push_back(baseVertex + 0); + wireIndices.push_back(baseVertex + 1); + wireIndices.push_back(baseVertex + 3); + wireIndices.push_back(baseVertex + 2); + baseVertex += 4; + } + + shapeData.setupVertices(vertexBuffer, vertices); + shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); +} // FIXME solids need per-face vertices, but smooth shaded // components do not. Find a way to support using draw elements @@ -285,10 +366,13 @@ void GeometryCache::buildShapes() { // Not implememented yet: //Triangle, + extrudePolygon<3>(_shapes[Triangle], _shapeVertices, _shapeIndices); + //Hexagon, + extrudePolygon<6>(_shapes[Hexagon], _shapeVertices, _shapeIndices); + //Octagon, + extrudePolygon<8>(_shapes[Octagon], _shapeVertices, _shapeIndices); //Quad, //Circle, - //Octahetron, - //Dodecahedron, //Torus, //Cone, //Cylinder, @@ -1757,6 +1841,11 @@ void GeometryCache::renderSolidShapeInstance(gpu::Batch& batch, GeometryCache::S renderInstances(batch, color, false, pipeline, shape); } +void GeometryCache::renderWireShapeInstance(gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { + renderInstances(batch, color, true, pipeline, shape); +} + + void GeometryCache::renderSolidSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) { renderInstances(batch, color, false, pipeline, GeometryCache::Sphere); } diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 385f2c6fa4..bab0942672 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -132,6 +132,8 @@ public: Line, Triangle, Quad, + Hexagon, + Octagon, Circle, Cube, Sphere, @@ -139,10 +141,9 @@ public: Octahedron, Dodecahedron, Icosahedron, - Torus, - Cone, - Cylinder, - + Torus, // not yet implemented + Cone, // not yet implemented + Cylinder, // not yet implemented NUM_SHAPES, }; @@ -170,6 +171,13 @@ public: renderSolidShapeInstance(batch, shape, glm::vec4(color, 1.0f), pipeline); } + void renderWireShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), + const render::ShapePipelinePointer& pipeline = _simplePipeline); + void renderWireShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec3& color, + const render::ShapePipelinePointer& pipeline = _simplePipeline) { + renderWireShapeInstance(batch, shape, glm::vec4(color, 1.0f), pipeline); + } + void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline = _simplePipeline); void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec3& color, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 54c79b1d9f..f2ade39144 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1354,6 +1354,9 @@ + + +
From b03686c37f53c8362595cb9b65fe75bc2890d4b9 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Jul 2016 22:02:56 -0700 Subject: [PATCH 1185/1237] Change the default domain attenuation level to 0.5 --- domain-server/resources/describe-settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 948c6ddc18..e1334ee46f 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -589,8 +589,8 @@ "name": "attenuation_per_doubling_in_distance", "label": "Default Domain Attenuation", "help": "Factor between 0 and 1.0 (0: No attenuation, 1.0: extreme attenuation)", - "placeholder": "0.18", - "default": "0.18", + "placeholder": "0.5", + "default": "0.5", "advanced": false }, { @@ -686,7 +686,7 @@ "name": "coefficient", "label": "Attenuation coefficient", "can_set": true, - "placeholder": "0.18" + "placeholder": "0.5" } ] }, From cf025072e16f2fee538af06b7b10b6715ac463a8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sat, 16 Jul 2016 09:50:56 -0700 Subject: [PATCH 1186/1237] Only consider "active" domains for all purposes (which on the back end means at least one person connected). --- interface/resources/qml/AddressBarDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index e8318ef1b8..f08ef5c608 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -246,7 +246,7 @@ Window { options.page = 1; } // FIXME: really want places I'm allowed in, not just open ones - var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; + var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&active&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; getRequest(url, function (error, json) { if (!error && (json.status !== 'success')) { error = new Error("Bad response: " + JSON.stringify(json)); From 4b2fb546eb1615ad68200d441c556594e56f9644 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Sat, 16 Jul 2016 12:21:08 -0700 Subject: [PATCH 1187/1237] Re-enable storing of attach points in user settings * bug fix, use localOffset property of grabbed entity, which is dynamically updated as the near grab action moves the entity. --- .../system/controllers/handControllerGrab.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 66c9e10795..36b0725f45 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -271,8 +271,7 @@ function propsArePhysical(props) { return isPhysical; } -// currently disabled. -var USE_ATTACH_POINT_SETTINGS = false; +var USE_ATTACH_POINT_SETTINGS = true; var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; function getAttachPointSettings() { @@ -2091,16 +2090,10 @@ function MyController(hand) { this.holdExit = function () { // store the offset attach points into preferences. if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedEntity) { - entityPropertiesCache.addEntity(this.grabbedEntity); - var props = entityPropertiesCache.getProps(this.grabbedEntity); - var entityXform = new Xform(props.rotation, props.position); - var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); - var handRot = (this.hand === RIGHT_HAND) ? MyAvatar.getRightPalmRotation() : MyAvatar.getLeftPalmRotation(); - var avatarHandPos = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; - var palmXform = new Xform(handRot, avatarXform.xformPoint(avatarHandPos)); - var offsetXform = Xform.mul(palmXform.inv(), entityXform); - - storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, offsetXform.pos, offsetXform.rot); + var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + if (props && props.localPosition && props.localRotation) { + storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, props.localPosition, props.localRotation); + } } }; From c7c804623166b5ed64a96103053bf8d5271c4e83 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Sat, 16 Jul 2016 14:58:21 -0700 Subject: [PATCH 1188/1237] De-equip goes into a near grab instead of dropping the object. --- .../system/controllers/handControllerGrab.js | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 36b0725f45..f622bf9217 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -228,7 +228,7 @@ function colorPow(color, power) { return { red: Math.pow(color.red / 255.0, power) * 255, green: Math.pow(color.green / 255.0, power) * 255, - blue: Math.pow(color.blue / 255.0, power) * 255, + blue: Math.pow(color.blue / 255.0, power) * 255 }; } @@ -764,9 +764,8 @@ function MyController(hand) { } }; - var SEARCH_SPHERE_ALPHA = 0.5; this.searchSphereOn = function (location, size, color) { - + var rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); var brightColor = colorPow(color, 0.06); if (this.searchSphere === null) { @@ -789,7 +788,7 @@ function MyController(hand) { position: location, rotation: rotation, innerColor: brightColor, - outerColor: color, + outerColor: color, innerAlpha: 1.0, outerAlpha: 0.0, outerRadius: size * 1.2, @@ -1960,12 +1959,12 @@ function MyController(hand) { this.currentObjectRotation = grabbedProperties.rotation; this.currentVelocity = ZERO_VEC; this.currentAngularVelocity = ZERO_VEC; + + this.prevDropDetected = false; }; this.nearGrabbing = function (deltaTime, timestamp) { - var dropDetected = this.dropGestureProcess(deltaTime); - if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); @@ -1974,6 +1973,16 @@ function MyController(hand) { if (this.state == STATE_HOLD) { + var dropDetected = this.dropGestureProcess(deltaTime); + + if (this.triggerSmoothedReleased()) { + this.waitForTriggerRelease = false; + } + + if (dropDetected && this.prevDropDetected != dropDetected) { + this.waitForTriggerRelease = true; + } + // highlight the grabbed hotspot when the dropGesture is detected. if (dropDetected) { entityPropertiesCache.addEntity(this.grabbedHotspot.entityID); @@ -1981,17 +1990,15 @@ function MyController(hand) { equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); } - if (dropDetected && this.triggerSmoothedGrab()) { + if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) { this.callEntityMethodOnGrabbed("releaseEquip"); - this.setState(STATE_OFF, "drop gesture detected"); - return; - } - - if (this.thumbPressed()) { - this.callEntityMethodOnGrabbed("releaseEquip"); - this.setState(STATE_OFF, "drop via thumb press"); + var grabbedEntity = this.grabbedEntity; + this.release(); + this.grabbedEntity = grabbedEntity; + this.setState(STATE_NEAR_GRABBING, "drop gesture detected"); return; } + this.prevDropDetected = dropDetected; } this.heartBeat(this.grabbedEntity); From 9332f82d54a70cf9347aa855c15921844c0ebb32 Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 17 Jul 2016 17:13:07 -0700 Subject: [PATCH 1189/1237] Carrefully improving the curvature generation performances --- .../render-utils/src/DebugDeferredBuffer.cpp | 12 +- .../render-utils/src/DebugDeferredBuffer.h | 3 +- .../render-utils/src/DeferredTransform.slh | 5 + .../render-utils/src/RenderDeferredTask.cpp | 18 +- .../render-utils/src/SurfaceGeometryPass.cpp | 260 ++++++++++++------ .../render-utils/src/SurfaceGeometryPass.h | 90 +++++- .../src/surfaceGeometry_makeCurvature.slf | 5 + libraries/render/src/render/BlurTask.cpp | 9 + libraries/render/src/render/BlurTask.h | 5 + libraries/render/src/render/BlurTask.slh | 11 +- .../utilities/render/deferredLighting.qml | 1 + .../developer/utilities/render/statsGPU.qml | 7 +- 12 files changed, 321 insertions(+), 105 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index e32c0c2884..73aa614233 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -144,7 +144,13 @@ static const std::string DEFAULT_SHADOW_SHADER { static const std::string DEFAULT_PYRAMID_DEPTH_SHADER { "vec4 getFragmentColor() {" " return vec4(vec3(1.0 - texture(pyramidMap, uv).x * 0.01), 1.0);" - //" return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 3).x * 0.01), 1.0);" + " }" +}; + +static const std::string DEFAULT_LINEAR_DEPTH_2_SHADER{ + "vec4 getFragmentColor() {" + // " return vec4(vec3(1.0 - texture(pyramidMap, uv).x * 0.01), 1.0);" + " return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 1).x * 0.01), 1.0);" " }" }; @@ -252,8 +258,10 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_LIGHTING_SHADER; case ShadowMode: return DEFAULT_SHADOW_SHADER; - case PyramidDepthMode: + case LinearDepthMode: return DEFAULT_PYRAMID_DEPTH_SHADER; + case LinearDepth2Mode: + return DEFAULT_LINEAR_DEPTH_2_SHADER; case CurvatureMode: return DEFAULT_CURVATURE_SHADER; case NormalCurvatureMode: diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 1053779577..2f919930d3 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -63,7 +63,8 @@ protected: ScatteringMode, LightingMode, ShadowMode, - PyramidDepthMode, + LinearDepthMode, + LinearDepth2Mode, CurvatureMode, NormalCurvatureMode, DiffusedCurvatureMode, diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 814a59a407..b3881c4c71 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -61,6 +61,11 @@ float getProjectionNear() { return planeD / planeC; } +// positive far distance of the projection +float getPosLinearDepthFar() { + return -frameTransform._depthInfo.z; +} + mat4 getViewInverse() { return frameTransform._viewInverse; } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 0c47768bcf..d54cc4f7ce 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -103,7 +103,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto fullFrameRangeTimer = addJob("BeginRangeTimer"); const auto opaqueRangeTimer = addJob("BeginOpaqueRangeTimer"); - const auto prepareDeferredInputs = SurfaceGeometryPass::Inputs(primaryFramebuffer, lightingModel).hasVarying(); + const auto prepareDeferredInputs = PrepareDeferred::Inputs(primaryFramebuffer, lightingModel).hasVarying(); const auto prepareDeferredOutputs = addJob("PrepareDeferred", prepareDeferredInputs); const auto deferredFramebuffer = prepareDeferredOutputs.getN(0); const auto lightingFramebuffer = prepareDeferredOutputs.getN(1); @@ -117,15 +117,23 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("OpaqueRangeTimer", opaqueRangeTimer); - const auto curvatureRangeTimer = addJob("BeginCurvatureRangeTimer"); - // Opaque all rendered, generate surface geometry buffers - const auto surfaceGeometryPassInputs = SurfaceGeometryPass::Inputs(deferredFrameTransform, deferredFramebuffer).hasVarying(); + // Opaque all rendered + + // Linear Depth Pass + const auto linearDepthPassInputs = LinearDepthPass::Inputs(deferredFrameTransform, deferredFramebuffer).hasVarying(); + const auto linearDepthPassOutputs = addJob("LinearDepth", linearDepthPassInputs); + const auto linearDepthTarget = linearDepthPassOutputs.getN(0); + const auto linearDepthTexture = linearDepthPassOutputs.getN(2); + + + // Curvature pass + const auto surfaceGeometryPassInputs = SurfaceGeometryPass::Inputs(deferredFrameTransform, deferredFramebuffer, linearDepthTarget).hasVarying(); const auto surfaceGeometryPassOutputs = addJob("SurfaceGeometry", surfaceGeometryPassInputs); const auto surfaceGeometryFramebuffer = surfaceGeometryPassOutputs.getN(0); const auto curvatureFramebuffer = surfaceGeometryPassOutputs.getN(1); - const auto linearDepthTexture = surfaceGeometryPassOutputs.getN(2); + const auto curvatureRangeTimer = addJob("BeginCurvatureRangeTimer"); // TODO: Push this 2 diffusion stages into surfaceGeometryPass as they are working together const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture).hasVarying(); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 66a1c89e77..369341403f 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -10,10 +10,15 @@ // #include "SurfaceGeometryPass.h" +#include + #include #include +const int DepthLinearPass_FrameTransformSlot = 0; +const int DepthLinearPass_DepthMapSlot = 0; + const int SurfaceGeometryPass_FrameTransformSlot = 0; const int SurfaceGeometryPass_ParamsSlot = 1; const int SurfaceGeometryPass_DepthMapSlot = 0; @@ -25,11 +30,11 @@ const int SurfaceGeometryPass_NormalMapSlot = 1; -SurfaceGeometryFramebuffer::SurfaceGeometryFramebuffer() { +LinearDepthFramebuffer::LinearDepthFramebuffer() { } -void SurfaceGeometryFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& depthBuffer) { +void LinearDepthFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& depthBuffer) { //If the depth buffer or size changed, we need to delete our FBOs bool reset = false; if ((_primaryDepthTexture != depthBuffer)) { @@ -49,42 +54,173 @@ void SurfaceGeometryFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& d } } -void SurfaceGeometryFramebuffer::clear() { +void LinearDepthFramebuffer::clear() { _linearDepthFramebuffer.reset(); _linearDepthTexture.reset(); +} + +void LinearDepthFramebuffer::allocate() { + + auto width = _frameSize.x; + auto height = _frameSize.y; + + // For Linear Depth: + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _linearDepthTexture->autoGenerateMips(1); + _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); + _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); +} + +gpu::FramebufferPointer LinearDepthFramebuffer::getLinearDepthFramebuffer() { + if (!_linearDepthFramebuffer) { + allocate(); + } + return _linearDepthFramebuffer; +} + +gpu::TexturePointer LinearDepthFramebuffer::getLinearDepthTexture() { + if (!_linearDepthTexture) { + allocate(); + } + return _linearDepthTexture; +} + + +LinearDepthPass::LinearDepthPass() { +} + +void LinearDepthPass::configure(const Config& config) { +} + +void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + + RenderArgs* args = renderContext->args; + + const auto frameTransform = inputs.get0(); + const auto deferredFramebuffer = inputs.get1(); + + if (!_linearDepthFramebuffer) { + _linearDepthFramebuffer = std::make_shared(); + } + _linearDepthFramebuffer->updatePrimaryDepth(deferredFramebuffer->getPrimaryDepthTexture()); + + auto depthBuffer = deferredFramebuffer->getPrimaryDepthTexture(); + auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); + + auto linearDepthFBO = _linearDepthFramebuffer->getLinearDepthFramebuffer(); + auto linearDepthTexture = _linearDepthFramebuffer->getLinearDepthTexture(); + + outputs.edit0() = _linearDepthFramebuffer; + outputs.edit1() = linearDepthFBO; + outputs.edit2() = linearDepthTexture; + + auto linearDepthPipeline = getLinearDepthPipeline(); + + auto depthViewport = args->_viewport; + auto furtherDepth = std::numeric_limits::infinity(); + furtherDepth = args->getViewFrustum().getFarClip() + 10.0; + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + _gpuTimer.begin(batch); + batch.enableStereo(false); + + batch.setViewportTransform(depthViewport); + batch.setProjectionTransform(glm::mat4()); + batch.setViewTransform(Transform()); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_linearDepthFramebuffer->getDepthFrameSize(), depthViewport)); + + batch.setUniformBuffer(DepthLinearPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); + + // Pyramid pass + batch.setFramebuffer(linearDepthFBO); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(furtherDepth, 0.0f, 0.0f, 0.0f)); + batch.setPipeline(linearDepthPipeline); + batch.setResourceTexture(DepthLinearPass_DepthMapSlot, depthBuffer); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + // batch.setResourceTexture(DepthLinearPass_DepthMapSlot, nullptr); + batch.generateTextureMips(linearDepthFBO->getRenderBuffer(0)); + + _gpuTimer.end(batch); + }); + + auto config = std::static_pointer_cast(renderContext->jobConfig); + config->gpuTime = _gpuTimer.getAverage(); +} + + +const gpu::PipelinePointer& LinearDepthPass::getLinearDepthPipeline() { + if (!_linearDepthPipeline) { + auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(surfaceGeometry_makeLinearDepth_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DepthLinearPass_FrameTransformSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), DepthLinearPass_DepthMapSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + // Stencil test the curvature pass for objects pixels only, not the background + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + + state->setColorWriteMask(true, false, false, false); + + // Good to go add the brand new pipeline + _linearDepthPipeline = gpu::Pipeline::create(program, state); + } + + return _linearDepthPipeline; +} + + + +SurfaceGeometryFramebuffer::SurfaceGeometryFramebuffer() { +} + +void SurfaceGeometryFramebuffer::updateLinearDepth(const gpu::TexturePointer& linearDepthBuffer) { + //If the depth buffer or size changed, we need to delete our FBOs + bool reset = false; + if ((_linearDepthTexture != linearDepthBuffer)) { + _linearDepthTexture = linearDepthBuffer; + reset = true; + } + if (_linearDepthTexture) { + auto newFrameSize = glm::ivec2(_linearDepthTexture->getDimensions()); + if (_frameSize != newFrameSize) { + _frameSize = newFrameSize; + reset = true; + } + } + + if (reset) { + clear(); + } +} + +void SurfaceGeometryFramebuffer::clear() { _curvatureFramebuffer.reset(); _curvatureTexture.reset(); } +gpu::TexturePointer SurfaceGeometryFramebuffer::getLinearDepthTexture() { + return _linearDepthTexture; +} + void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); - _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); - _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); - _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width >> getResolutionLevel(), height >> getResolutionLevel(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); -// _curvatureFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); -} - -gpu::FramebufferPointer SurfaceGeometryFramebuffer::getLinearDepthFramebuffer() { - if (!_linearDepthFramebuffer) { - allocate(); - } - return _linearDepthFramebuffer; -} - -gpu::TexturePointer SurfaceGeometryFramebuffer::getLinearDepthTexture() { - if (!_linearDepthTexture) { - allocate(); - } - return _linearDepthTexture; } gpu::FramebufferPointer SurfaceGeometryFramebuffer::getCurvatureFramebuffer() { @@ -108,7 +244,6 @@ void SurfaceGeometryFramebuffer::setResolutionLevel(int resolutionLevel) { } } - SurfaceGeometryPass::SurfaceGeometryPass() { Parameters parameters; _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); @@ -135,7 +270,7 @@ void SurfaceGeometryPass::configure(const Config& config) { } -void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& curvatureAndDepth) { +void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -143,31 +278,28 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c const auto frameTransform = inputs.get0(); const auto deferredFramebuffer = inputs.get1(); + const auto linearDepthFramebuffer = inputs.get2(); + + auto linearDepthTexture = linearDepthFramebuffer->getLinearDepthTexture(); if (!_surfaceGeometryFramebuffer) { _surfaceGeometryFramebuffer = std::make_shared(); } - _surfaceGeometryFramebuffer->updatePrimaryDepth(deferredFramebuffer->getPrimaryDepthTexture()); + _surfaceGeometryFramebuffer->updateLinearDepth(linearDepthTexture); - auto depthBuffer = deferredFramebuffer->getPrimaryDepthTexture(); auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); - auto linearDepthFBO = _surfaceGeometryFramebuffer->getLinearDepthFramebuffer(); - auto linearDepthTexture = _surfaceGeometryFramebuffer->getLinearDepthTexture(); auto curvatureFBO = _surfaceGeometryFramebuffer->getCurvatureFramebuffer(); - auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); + if (curvatureFBO->getDepthStencilBuffer() != deferredFramebuffer->getPrimaryDepthTexture()) { + curvatureFBO->setDepthStencilBuffer(deferredFramebuffer->getPrimaryDepthTexture(), deferredFramebuffer->getPrimaryDepthTexture()->getTexelFormat()); + } + auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); - curvatureAndDepth.edit0() = _surfaceGeometryFramebuffer; - curvatureAndDepth.edit1() = curvatureFBO; - curvatureAndDepth.edit2() = linearDepthTexture; + outputs.edit0() = _surfaceGeometryFramebuffer; + outputs.edit1() = curvatureFBO; - auto linearDepthPipeline = getLinearDepthPipeline(); auto curvaturePipeline = getCurvaturePipeline(); - - // gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - // _gpuTimer.begin(batch); - // }); - + auto depthViewport = args->_viewport; auto curvatureViewport = depthViewport >> _surfaceGeometryFramebuffer->getResolutionLevel(); @@ -175,70 +307,34 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c _gpuTimer.begin(batch); batch.enableStereo(false); - batch.setViewportTransform(depthViewport); batch.setProjectionTransform(glm::mat4()); batch.setViewTransform(Transform()); - batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getDepthFrameSize(), depthViewport)); - - batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); - batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); - - // Pyramid pass - batch.setFramebuffer(linearDepthFBO); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(args->getViewFrustum().getFarClip(), 0.0f, 0.0f, 0.0f)); - batch.setPipeline(linearDepthPipeline); - batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, depthBuffer); - batch.draw(gpu::TRIANGLE_STRIP, 4); batch.setViewportTransform(curvatureViewport); batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getCurvatureFrameSize(), curvatureViewport)); + batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); + batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); + // Curvature pass batch.setFramebuffer(curvatureFBO); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + batch.setPipeline(curvaturePipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, linearDepthTexture); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, normalTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, nullptr); _gpuTimer.end(batch); }); - - // gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - // _gpuTimer.end(batch); - // }); auto config = std::static_pointer_cast(renderContext->jobConfig); config->gpuTime = _gpuTimer.getAverage(); } -const gpu::PipelinePointer& SurfaceGeometryPass::getLinearDepthPipeline() { - if (!_linearDepthPipeline) { - auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(surfaceGeometry_makeLinearDepth_frag)); - gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), SurfaceGeometryPass_FrameTransformSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), SurfaceGeometryPass_DepthMapSlot)); - gpu::Shader::makeProgram(*program, slotBindings); - - - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - // Stencil test the curvature pass for objects pixels only, not the background - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - - state->setColorWriteMask(true, false, false, false); - - // Good to go add the brand new pipeline - _linearDepthPipeline = gpu::Pipeline::create(program, state); - } - - return _linearDepthPipeline; -} const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { if (!_curvaturePipeline) { diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index bdd3997536..8137deda35 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -19,23 +19,19 @@ #include "DeferredFramebuffer.h" -// SurfaceGeometryFramebuffer is a helper class gathering in one place theframebuffers and targets describing the surface geometry linear depth and curvature generated -// from a z buffer and a normal buffer -class SurfaceGeometryFramebuffer { +// SurfaceGeometryFramebuffer is a helper class gathering in one place theframebuffers and targets describing the surface geometry linear depth +// from a z buffer +class LinearDepthFramebuffer { public: - SurfaceGeometryFramebuffer(); + LinearDepthFramebuffer(); gpu::FramebufferPointer getLinearDepthFramebuffer(); gpu::TexturePointer getLinearDepthTexture(); - gpu::FramebufferPointer getCurvatureFramebuffer(); - gpu::TexturePointer getCurvatureTexture(); - // Update the depth buffer which will drive the allocation of all the other resources according to its size. void updatePrimaryDepth(const gpu::TexturePointer& depthBuffer); gpu::TexturePointer getPrimaryDepthTexture(); const glm::ivec2& getDepthFrameSize() const { return _frameSize; } - glm::ivec2 getCurvatureFrameSize() const { return _frameSize >> _resolutionLevel; } void setResolutionLevel(int level); int getResolutionLevel() const { return _resolutionLevel; } @@ -48,6 +44,76 @@ protected: gpu::FramebufferPointer _linearDepthFramebuffer; gpu::TexturePointer _linearDepthTexture; + + glm::ivec2 _frameSize; + int _resolutionLevel{ 0 }; +}; + +using LinearDepthFramebufferPointer = std::shared_ptr; + + +class LinearDepthPassConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(double gpuTime READ getGpuTime) +public: + LinearDepthPassConfig() : render::Job::Config(true) {} + + double getGpuTime() { return gpuTime; } + + double gpuTime{ 0.0 }; + +signals: + void dirty(); +}; + +class LinearDepthPass { +public: + using Inputs = render::VaryingSet2; + using Outputs = render::VaryingSet3; + using Config = LinearDepthPassConfig; + using JobModel = render::Job::ModelIO; + + LinearDepthPass(); + + void configure(const Config& config); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs); + +private: + typedef gpu::BufferView UniformBufferView; + + LinearDepthFramebufferPointer _linearDepthFramebuffer; + + const gpu::PipelinePointer& getLinearDepthPipeline(); + + gpu::PipelinePointer _linearDepthPipeline; + + gpu::RangeTimer _gpuTimer; +}; + + +// SurfaceGeometryFramebuffer is a helper class gathering in one place theframebuffers and targets describing the surface geometry linear depth and curvature generated +// from a z buffer and a normal buffer +class SurfaceGeometryFramebuffer { +public: + SurfaceGeometryFramebuffer(); + + gpu::FramebufferPointer getCurvatureFramebuffer(); + gpu::TexturePointer getCurvatureTexture(); + + // Update the source framebuffer size which will drive the allocation of all the other resources. + void updateLinearDepth(const gpu::TexturePointer& linearDepthBuffer); + gpu::TexturePointer getLinearDepthTexture(); + const glm::ivec2& getSourceFrameSize() const { return _frameSize; } + glm::ivec2 getCurvatureFrameSize() const { return _frameSize >> _resolutionLevel; } + + void setResolutionLevel(int level); + int getResolutionLevel() const { return _resolutionLevel; } + +protected: + void clear(); + void allocate(); + + gpu::TexturePointer _linearDepthTexture; gpu::FramebufferPointer _curvatureFramebuffer; gpu::TexturePointer _curvatureTexture; @@ -83,15 +149,15 @@ signals: class SurfaceGeometryPass { public: - using Inputs = render::VaryingSet2; - using Outputs = render::VaryingSet3; + using Inputs = render::VaryingSet3; + using Outputs = render::VaryingSet2; using Config = SurfaceGeometryPassConfig; using JobModel = render::Job::ModelIO; SurfaceGeometryPass(); void configure(const Config& config); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& curvatureAndDepth); + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs); float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } float getCurvatureBasisScale() const { return _parametersBuffer.get().curvatureInfo.y; } @@ -114,10 +180,8 @@ private: SurfaceGeometryFramebufferPointer _surfaceGeometryFramebuffer; - const gpu::PipelinePointer& getLinearDepthPipeline(); const gpu::PipelinePointer& getCurvaturePipeline(); - gpu::PipelinePointer _linearDepthPipeline; gpu::PipelinePointer _curvaturePipeline; diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index c5cb12aeb8..552481304f 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -109,6 +109,11 @@ void main(void) { // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); + if (Zeye <= -getPosLinearDepthFar()) { + // outFragColor = vec4(0.0); + // return; + discard; + } float nearPlaneScale = 0.5 * getProjectionNear(); diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 041d69370a..64f960f957 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -78,6 +78,14 @@ void BlurParams::setDepthThreshold(float threshold) { } } +void BlurParams::setLinearDepthPosFar(float farPosDepth) { + auto linearDepthInfo = _parametersBuffer.get().linearDepthInfo; + if (farPosDepth != linearDepthInfo.x) { + _parametersBuffer.edit().linearDepthInfo.x = farPosDepth; + } +} + + BlurInOutResource::BlurInOutResource(bool generateOutputFramebuffer) : _generateOutputFramebuffer(generateOutputFramebuffer) { @@ -220,6 +228,7 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); glm::ivec2 textureSize(blurringResources.sourceTexture->getDimensions()); _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, args->_viewport)); + _parameters->setLinearDepthPosFar(args->getViewFrustum().getFarClip()); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 88228d1d86..c6e4e76ca5 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -29,6 +29,8 @@ public: void setDepthPerspective(float oneOverTan2FOV); void setDepthThreshold(float threshold); + void setLinearDepthPosFar(float farPosDepth); + // Class describing the uniform buffer with all the parameters common to the blur shaders class Params { public: @@ -47,6 +49,9 @@ public: // stereo info if blurring a stereo render glm::vec4 stereoInfo{ 0.0f }; + // LinearDepth info is { f } + glm::vec4 linearDepthInfo{ 0.0f }; + Params() {} }; gpu::BufferView _parametersBuffer; diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index c66eaff00b..8ba1126413 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -27,6 +27,7 @@ struct BlurParameters { vec4 filterInfo; vec4 depthInfo; vec4 stereoInfo; + vec4 linearDepthInfo; }; uniform blurParamsBuffer { @@ -54,6 +55,10 @@ float getDepthPerspective() { return parameters.depthInfo.w; } +float getPosLinearDepthFar() { + return parameters.linearDepthInfo.x; +} + <@endfunc@> @@ -92,8 +97,12 @@ uniform sampler2D depthMap; vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep) { texcoord = evalTexcoordTransformed(texcoord); - float sampleDepth = texture(depthMap, texcoord).x; + float sampleDepth = -texture(depthMap, texcoord).x; vec4 sampleCenter = texture(sourceMap, texcoord); + if (sampleDepth <= getPosLinearDepthFar()) { + discard; + //return sampleCenter; + } // Calculate the width scale. float distanceToProjectionWindow = getDepthPerspective(); diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 3f1b1ac010..098f101b6d 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -151,6 +151,7 @@ Column { ListElement { text: "Lighting"; color: "White" } ListElement { text: "Shadow"; color: "White" } ListElement { text: "Linear Depth"; color: "White" } + ListElement { text: "Linear Depth LOD2"; color: "White" } ListElement { text: "Mid Curvature"; color: "White" } ListElement { text: "Mid Normal"; color: "White" } ListElement { text: "Low Curvature"; color: "White" } diff --git a/scripts/developer/utilities/render/statsGPU.qml b/scripts/developer/utilities/render/statsGPU.qml index 7282b753cc..03a17d93a6 100644 --- a/scripts/developer/utilities/render/statsGPU.qml +++ b/scripts/developer/utilities/render/statsGPU.qml @@ -41,9 +41,14 @@ Item { object: Render.getConfig("OpaqueRangeTimer"), prop: "gpuTime", label: "Opaque", - color: "#0000FF" + color: "#FFFFFF" }, { + object: Render.getConfig("LinearDepth"), + prop: "gpuTime", + label: "LinearDepth", + color: "#00FF00" + },{ object: Render.getConfig("SurfaceGeometry"), prop: "gpuTime", label: "SurfaceGeometry", From cfa51ae1f53bfcda1f52d8e600f7ff823c7439db Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 17 Jul 2016 18:32:43 -0700 Subject: [PATCH 1190/1237] REmoving problematic timers for mac --- libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp | 4 ++++ libraries/render-utils/src/RenderDeferredTask.cpp | 4 ++-- scripts/developer/utilities/render/statsGPU.qml | 6 ------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp index 09556d8e60..463cff9a6c 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp @@ -15,7 +15,11 @@ using namespace gpu; using namespace gpu::gl; // Eventually, we want to test with TIME_ELAPSED instead of TIMESTAMP +#ifdef Q_OS_MAC +static bool timeElapsed = true; +#else static bool timeElapsed = false; +#endif void GLBackend::do_beginQuery(Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d54cc4f7ce..76e5f56714 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -100,7 +100,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // GPU jobs: Start preparing the primary, deferred and lighting buffer const auto primaryFramebuffer = addJob("PreparePrimaryBuffer"); - const auto fullFrameRangeTimer = addJob("BeginRangeTimer"); + // const auto fullFrameRangeTimer = addJob("BeginRangeTimer"); const auto opaqueRangeTimer = addJob("BeginOpaqueRangeTimer"); const auto prepareDeferredInputs = PrepareDeferred::Inputs(primaryFramebuffer, lightingModel).hasVarying(); @@ -209,7 +209,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { // Blit! addJob("Blit", primaryFramebuffer); - addJob("RangeTimer", fullFrameRangeTimer); + // addJob("RangeTimer", fullFrameRangeTimer); } diff --git a/scripts/developer/utilities/render/statsGPU.qml b/scripts/developer/utilities/render/statsGPU.qml index 03a17d93a6..44ea4c4597 100644 --- a/scripts/developer/utilities/render/statsGPU.qml +++ b/scripts/developer/utilities/render/statsGPU.qml @@ -72,12 +72,6 @@ Item { label: "tone and post", color: "#FF0000" } - ,{ - object: Render.getConfig("RangeTimer"), - prop: "gpuTime", - label: "FrameGPU", - color: "#FFFF00" - } ] } } From 378f4576fac407012a4bf4bcbe130f35a9ac18ad Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 17 Jul 2016 20:18:06 -0700 Subject: [PATCH 1191/1237] a bit better making the curvature and diffusion passes --- .../render-utils/src/SurfaceGeometryPass.cpp | 8 +++--- .../src/surfaceGeometry_makeCurvature.slf | 27 +++++++++---------- libraries/render/src/render/BlurTask.cpp | 2 +- libraries/render/src/render/BlurTask.slh | 4 +-- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 369341403f..d34d9108d4 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -143,7 +143,7 @@ void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const batch.draw(gpu::TRIANGLE_STRIP, 4); // batch.setResourceTexture(DepthLinearPass_DepthMapSlot, nullptr); - batch.generateTextureMips(linearDepthFBO->getRenderBuffer(0)); + // batch.generateTextureMips(linearDepthFBO->getRenderBuffer(0)); _gpuTimer.end(batch); }); @@ -293,7 +293,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c if (curvatureFBO->getDepthStencilBuffer() != deferredFramebuffer->getPrimaryDepthTexture()) { curvatureFBO->setDepthStencilBuffer(deferredFramebuffer->getPrimaryDepthTexture(), deferredFramebuffer->getPrimaryDepthTexture()->getTexelFormat()); } - auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); + auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); outputs.edit0() = _surfaceGeometryFramebuffer; outputs.edit1() = curvatureFBO; @@ -318,7 +318,9 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c // Curvature pass batch.setFramebuffer(curvatureFBO); - // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + + // We can avoid the clear by drawing the same clear vallue from the makeCUrvature shader. slightly better than adding the clear + // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); batch.setPipeline(curvaturePipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, linearDepthTexture); diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index 552481304f..bb3a073a2e 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -110,9 +110,8 @@ void main(void) { // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); if (Zeye <= -getPosLinearDepthFar()) { - // outFragColor = vec4(0.0); - // return; - discard; + outFragColor = vec4(0.0); + return; } float nearPlaneScale = 0.5 * getProjectionNear(); @@ -121,13 +120,13 @@ void main(void) { // The position of the pixel fragment in Eye space then in world space vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); - vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; + // vec3 worldPos = (frameTransform._viewInverse * vec4(eyePos, 1.0)).xyz; - if (texcoordPos.y > 0.5) { + /* if (texcoordPos.y > 0.5) { outFragColor = vec4(fract(10.0 * worldPos.xyz), 1.0); } else { outFragColor = vec4(fract(10.0 * eyePos.xyz), 1.0); - } + }*/ // return; // Calculate the perspective scale. @@ -160,11 +159,11 @@ void main(void) { vec4 pz = vec4(eyePos + az, 0.0); - if (texcoordPos.y > 0.5) { + /* if (texcoordPos.y > 0.5) { outFragColor = vec4(fract(px.xyz), 1.0); } else { outFragColor = vec4(fract(eyePos.xyz), 1.0); - } + }*/ // return; @@ -201,14 +200,14 @@ void main(void) { vec4 clipPos = getProjectionMono() * vec4(eyePos, 1.0); nclipPos = clipPos.xy / clipPos.w; - if (texcoordPos.y > 0.5) { + /* if (texcoordPos.y > 0.5) { // outFragColor = vec4(fract(10.0 * worldPos.xyz), 1.0); outFragColor = vec4(fract(10.0 * (nclipPos)), 0.0, 1.0); } else { outFragColor = vec4(fract(10.0 * (clipPos.xy / clipPos.w)), 0.0, 1.0); // outFragColor = vec4(nclipPos * 0.5 + 0.5, 0.0, 1.0); - } + }*/ //return; @@ -217,14 +216,14 @@ void main(void) { py.xy = (py.xy - nclipPos) * pixPerspectiveScaleInv; pz.xy = (pz.xy - nclipPos) * pixPerspectiveScaleInv; - if (texcoordPos.y > 0.5) { + /* if (texcoordPos.y > 0.5) { // outFragColor = vec4(fract(10.0 * worldPos.xyz), 1.0); outFragColor = vec4(fract(10.0 * (px.xy)), 0.0, 1.0); } else { outFragColor = vec4(fract(10.0 * (py.xy)), 0.0, 1.0); // outFragColor = vec4(nclipPos * 0.5 + 0.5, 0.0, 1.0); - } + }*/ // return; // Calculate dF/dx, dF/dy and dF/dz using chain rule @@ -234,10 +233,10 @@ void main(void) { vec3 trace = vec3(dFdx.x, dFdy.y, dFdz.z); - if (dot(trace, trace) > params.curvatureInfo.w) { + /*if (dot(trace, trace) > params.curvatureInfo.w) { outFragColor = vec4(dFdx.x, dFdy.y, dFdz.z, 1.0); return; - } + }*/ // Calculate the mean curvature float meanCurvature = ((trace.x + trace.y + trace.z) * 0.33333333333333333) * params.curvatureInfo.w; diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 64f960f957..4a3118d0d3 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -228,7 +228,6 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); glm::ivec2 textureSize(blurringResources.sourceTexture->getDimensions()); _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, args->_viewport)); - _parameters->setLinearDepthPosFar(args->getViewFrustum().getFarClip()); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); @@ -341,6 +340,7 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const glm::ivec2 textureSize(blurringResources.sourceTexture->getDimensions()); _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, args->_viewport)); _parameters->setDepthPerspective(args->getViewFrustum().getProjection()[1][1]); + _parameters->setLinearDepthPosFar(args->getViewFrustum().getFarClip()); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index 8ba1126413..de40740c19 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -97,9 +97,9 @@ uniform sampler2D depthMap; vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep) { texcoord = evalTexcoordTransformed(texcoord); - float sampleDepth = -texture(depthMap, texcoord).x; + float sampleDepth = texture(depthMap, texcoord).x; vec4 sampleCenter = texture(sourceMap, texcoord); - if (sampleDepth <= getPosLinearDepthFar()) { + if (sampleDepth >= getPosLinearDepthFar()) { discard; //return sampleCenter; } From 5a0ce815167746aac6ade94275eea0cb613f063c Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 18 Jul 2016 03:54:05 -0700 Subject: [PATCH 1192/1237] Performing the curvature generation and diffusion at half the resolution --- .../render-utils/src/DebugDeferredBuffer.cpp | 41 ++++--- .../render-utils/src/DebugDeferredBuffer.h | 5 +- .../render-utils/src/RenderDeferredTask.cpp | 6 +- .../render-utils/src/SurfaceGeometryPass.cpp | 109 ++++++++++++++++-- .../render-utils/src/SurfaceGeometryPass.h | 16 ++- .../src/debug_deferred_buffer.slf | 4 +- .../src/surfaceGeometry_makeCurvature.slf | 4 +- libraries/render/src/render/BlurTask.cpp | 37 +++--- libraries/render/src/render/BlurTask.slh | 41 +++++-- libraries/render/src/render/Task.h | 15 +++ .../utilities/render/deferredLighting.qml | 3 +- 11 files changed, 223 insertions(+), 58 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 73aa614233..3a8e6a822c 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -46,7 +46,9 @@ enum Slot { Depth, Lighting, Shadow, - Pyramid, + LinearDepth, + HalfLinearDepth, + HalfNormal, Curvature, DiffusedCurvature, Scattering, @@ -141,16 +143,21 @@ static const std::string DEFAULT_SHADOW_SHADER { " }" }; -static const std::string DEFAULT_PYRAMID_DEPTH_SHADER { +static const std::string DEFAULT_LINEAR_DEPTH_SHADER { "vec4 getFragmentColor() {" - " return vec4(vec3(1.0 - texture(pyramidMap, uv).x * 0.01), 1.0);" + " return vec4(vec3(1.0 - texture(linearDepthMap, uv).x * 0.01), 1.0);" " }" }; -static const std::string DEFAULT_LINEAR_DEPTH_2_SHADER{ +static const std::string DEFAULT_HALF_LINEAR_DEPTH_SHADER{ "vec4 getFragmentColor() {" - // " return vec4(vec3(1.0 - texture(pyramidMap, uv).x * 0.01), 1.0);" - " return vec4(vec3(1.0 - textureLod(pyramidMap, uv, 1).x * 0.01), 1.0);" + " return vec4(vec3(1.0 - texture(halfLinearDepthMap, uv).x * 0.01), 1.0);" + " }" +}; + +static const std::string DEFAULT_HALF_NORMAL_SHADER{ + "vec4 getFragmentColor() {" + " return vec4(vec3(texture(halfNormalMap, uv).xyz), 1.0);" " }" }; @@ -259,9 +266,11 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust case ShadowMode: return DEFAULT_SHADOW_SHADER; case LinearDepthMode: - return DEFAULT_PYRAMID_DEPTH_SHADER; - case LinearDepth2Mode: - return DEFAULT_LINEAR_DEPTH_2_SHADER; + return DEFAULT_LINEAR_DEPTH_SHADER; + case HalfLinearDepthMode: + return DEFAULT_HALF_LINEAR_DEPTH_SHADER; + case HalfNormalMode: + return DEFAULT_HALF_NORMAL_SHADER; case CurvatureMode: return DEFAULT_CURVATURE_SHADER; case NormalCurvatureMode: @@ -325,7 +334,9 @@ const gpu::PipelinePointer& DebugDeferredBuffer::getPipeline(Mode mode, std::str slotBindings.insert(gpu::Shader::Binding("obscuranceMap", AmbientOcclusion)); slotBindings.insert(gpu::Shader::Binding("lightingMap", Lighting)); slotBindings.insert(gpu::Shader::Binding("shadowMap", Shadow)); - slotBindings.insert(gpu::Shader::Binding("pyramidMap", Pyramid)); + slotBindings.insert(gpu::Shader::Binding("linearDepthMap", LinearDepth)); + slotBindings.insert(gpu::Shader::Binding("halfLinearDepthMap", HalfLinearDepth)); + slotBindings.insert(gpu::Shader::Binding("halfNormalMap", HalfNormal)); slotBindings.insert(gpu::Shader::Binding("curvatureMap", Curvature)); slotBindings.insert(gpu::Shader::Binding("diffusedCurvatureMap", DiffusedCurvature)); slotBindings.insert(gpu::Shader::Binding("scatteringMap", Scattering)); @@ -364,8 +375,9 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren RenderArgs* args = renderContext->args; auto& deferredFramebuffer = inputs.get0(); - auto& surfaceGeometryFramebuffer = inputs.get1(); - auto& diffusedCurvatureFramebuffer = inputs.get2(); + auto& linearDepthTarget = inputs.get1(); + auto& surfaceGeometryFramebuffer = inputs.get2(); + auto& diffusedCurvatureFramebuffer = inputs.get3(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); @@ -395,7 +407,10 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setResourceTexture(Depth, deferredFramebuffer->getPrimaryDepthTexture()); batch.setResourceTexture(Lighting, deferredFramebuffer->getLightingTexture()); batch.setResourceTexture(Shadow, lightStage.lights[0]->shadow.framebuffer->getDepthStencilBuffer()); - batch.setResourceTexture(Pyramid, surfaceGeometryFramebuffer->getLinearDepthTexture()); + batch.setResourceTexture(LinearDepth, linearDepthTarget->getLinearDepthTexture()); + batch.setResourceTexture(HalfLinearDepth, linearDepthTarget->getHalfLinearDepthTexture()); + batch.setResourceTexture(HalfNormal, linearDepthTarget->getHalfNormalTexture()); + batch.setResourceTexture(Curvature, surfaceGeometryFramebuffer->getCurvatureTexture()); batch.setResourceTexture(DiffusedCurvature, diffusedCurvatureFramebuffer->getRenderBuffer(0)); if (DependencyManager::get()->isAmbientOcclusionEnabled()) { diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 2f919930d3..d6005de349 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -36,7 +36,7 @@ signals: class DebugDeferredBuffer { public: - using Inputs = render::VaryingSet3; + using Inputs = render::VaryingSet4; using Config = DebugDeferredBufferConfig; using JobModel = render::Job::ModelI; @@ -64,7 +64,8 @@ protected: LightingMode, ShadowMode, LinearDepthMode, - LinearDepth2Mode, + HalfLinearDepthMode, + HalfNormalMode, CurvatureMode, NormalCurvatureMode, DiffusedCurvatureMode, diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 76e5f56714..343e9fbed0 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -125,6 +125,8 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto linearDepthPassOutputs = addJob("LinearDepth", linearDepthPassInputs); const auto linearDepthTarget = linearDepthPassOutputs.getN(0); const auto linearDepthTexture = linearDepthPassOutputs.getN(2); + const auto halfLinearDepthTexture = linearDepthPassOutputs.getN(3); + const auto halfNormalTexture = linearDepthPassOutputs.getN(4); // Curvature pass @@ -136,7 +138,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto curvatureRangeTimer = addJob("BeginCurvatureRangeTimer"); // TODO: Push this 2 diffusion stages into surfaceGeometryPass as they are working together - const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture).hasVarying(); + const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, halfLinearDepthTexture).hasVarying(); const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", diffuseCurvaturePassInputs); const auto lowCurvatureNormalFramebuffer = addJob("DiffuseCurvatureLow", diffuseCurvaturePassInputs, true); // THis blur pass generates it s render resource @@ -182,7 +184,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DebugScattering", deferredLightingInputs); // Debugging Deferred buffer job - const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer)); + const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, linearDepthTarget, surfaceGeometryFramebuffer, lowCurvatureNormalFramebuffer)); addJob("DebugDeferredBuffer", debugFramebuffers); // Scene Octree Debuging job diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index d34d9108d4..57b0b7e08d 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -18,6 +18,7 @@ const int DepthLinearPass_FrameTransformSlot = 0; const int DepthLinearPass_DepthMapSlot = 0; +const int DepthLinearPass_NormalMapSlot = 1; const int SurfaceGeometryPass_FrameTransformSlot = 0; const int SurfaceGeometryPass_ParamsSlot = 1; @@ -25,6 +26,7 @@ const int SurfaceGeometryPass_DepthMapSlot = 0; const int SurfaceGeometryPass_NormalMapSlot = 1; #include "surfaceGeometry_makeLinearDepth_frag.h" +#include "surfaceGeometry_downsampleDepthNormal_frag.h" #include "surfaceGeometry_makeCurvature_frag.h" @@ -45,6 +47,8 @@ void LinearDepthFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& depth auto newFrameSize = glm::ivec2(_primaryDepthTexture->getDimensions()); if (_frameSize != newFrameSize) { _frameSize = newFrameSize; + _halfFrameSize = newFrameSize >> 1; + reset = true; } } @@ -67,10 +71,21 @@ void LinearDepthFramebuffer::allocate() { // For Linear Depth: _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _linearDepthTexture->autoGenerateMips(1); + // _linearDepthTexture->autoGenerateMips(1); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); + + // For Downsampling: + _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + + _halfNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + + _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _downsampleFramebuffer->setRenderBuffer(0, _halfLinearDepthTexture); + _downsampleFramebuffer->setRenderBuffer(1, _halfNormalTexture); } gpu::FramebufferPointer LinearDepthFramebuffer::getLinearDepthFramebuffer() { @@ -87,6 +102,27 @@ gpu::TexturePointer LinearDepthFramebuffer::getLinearDepthTexture() { return _linearDepthTexture; } +gpu::FramebufferPointer LinearDepthFramebuffer::getDownsampleFramebuffer() { + if (!_downsampleFramebuffer) { + allocate(); + } + return _downsampleFramebuffer; +} + +gpu::TexturePointer LinearDepthFramebuffer::getHalfLinearDepthTexture() { + if (!_halfLinearDepthTexture) { + allocate(); + } + return _halfLinearDepthTexture; +} + +gpu::TexturePointer LinearDepthFramebuffer::getHalfNormalTexture() { + if (!_halfNormalTexture) { + allocate(); + } + return _halfNormalTexture; +} + LinearDepthPass::LinearDepthPass() { } @@ -114,13 +150,21 @@ void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const auto linearDepthFBO = _linearDepthFramebuffer->getLinearDepthFramebuffer(); auto linearDepthTexture = _linearDepthFramebuffer->getLinearDepthTexture(); + auto downsampleFBO = _linearDepthFramebuffer->getDownsampleFramebuffer(); + auto halfLinearDepthTexture = _linearDepthFramebuffer->getHalfLinearDepthTexture(); + auto halfNormalTexture = _linearDepthFramebuffer->getHalfNormalTexture(); + outputs.edit0() = _linearDepthFramebuffer; outputs.edit1() = linearDepthFBO; outputs.edit2() = linearDepthTexture; - + outputs.edit3() = halfLinearDepthTexture; + outputs.edit4() = halfNormalTexture; + auto linearDepthPipeline = getLinearDepthPipeline(); + auto downsamplePipeline = getDownsamplePipeline(); auto depthViewport = args->_viewport; + auto halfViewport = depthViewport >> 1; auto furtherDepth = std::numeric_limits::infinity(); furtherDepth = args->getViewFrustum().getFarClip() + 10.0; @@ -135,15 +179,21 @@ void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const batch.setUniformBuffer(DepthLinearPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); - // Pyramid pass + // LinearDepth batch.setFramebuffer(linearDepthFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(furtherDepth, 0.0f, 0.0f, 0.0f)); batch.setPipeline(linearDepthPipeline); batch.setResourceTexture(DepthLinearPass_DepthMapSlot, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); - // batch.setResourceTexture(DepthLinearPass_DepthMapSlot, nullptr); - // batch.generateTextureMips(linearDepthFBO->getRenderBuffer(0)); + // Downsample + batch.setViewportTransform(halfViewport); + + batch.setFramebuffer(downsampleFBO); + batch.setResourceTexture(DepthLinearPass_DepthMapSlot, linearDepthTexture); + batch.setResourceTexture(DepthLinearPass_NormalMapSlot, normalTexture); + batch.setPipeline(downsamplePipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); _gpuTimer.end(batch); }); @@ -180,6 +230,32 @@ const gpu::PipelinePointer& LinearDepthPass::getLinearDepthPipeline() { } +const gpu::PipelinePointer& LinearDepthPass::getDownsamplePipeline() { + if (!_downsamplePipeline) { + auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(surfaceGeometry_downsampleDepthNormal_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DepthLinearPass_FrameTransformSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), DepthLinearPass_DepthMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), DepthLinearPass_NormalMapSlot)); + gpu::Shader::makeProgram(*program, slotBindings); + + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setColorWriteMask(true, true, true, false); + + // Good to go add the brand new pipeline + _downsamplePipeline = gpu::Pipeline::create(program, state); + } + + return _downsamplePipeline; +} + + +//#define USE_STENCIL_TEST SurfaceGeometryFramebuffer::SurfaceGeometryFramebuffer() { } @@ -280,19 +356,22 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c const auto deferredFramebuffer = inputs.get1(); const auto linearDepthFramebuffer = inputs.get2(); - auto linearDepthTexture = linearDepthFramebuffer->getLinearDepthTexture(); + auto linearDepthTexture = linearDepthFramebuffer->getHalfLinearDepthTexture(); if (!_surfaceGeometryFramebuffer) { _surfaceGeometryFramebuffer = std::make_shared(); } _surfaceGeometryFramebuffer->updateLinearDepth(linearDepthTexture); - auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); + // auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); + auto normalTexture = linearDepthFramebuffer->getHalfNormalTexture(); auto curvatureFBO = _surfaceGeometryFramebuffer->getCurvatureFramebuffer(); +#ifdef USE_STENCIL_TEST if (curvatureFBO->getDepthStencilBuffer() != deferredFramebuffer->getPrimaryDepthTexture()) { curvatureFBO->setDepthStencilBuffer(deferredFramebuffer->getPrimaryDepthTexture(), deferredFramebuffer->getPrimaryDepthTexture()->getTexelFormat()); } +#endif auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); outputs.edit0() = _surfaceGeometryFramebuffer; @@ -301,7 +380,8 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto curvaturePipeline = getCurvaturePipeline(); auto depthViewport = args->_viewport; - auto curvatureViewport = depthViewport >> _surfaceGeometryFramebuffer->getResolutionLevel(); + auto curvatureViewport = depthViewport >> 1; + // >> _surfaceGeometryFramebuffer->getResolutionLevel(); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { _gpuTimer.begin(batch); @@ -318,9 +398,13 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c // Curvature pass batch.setFramebuffer(curvatureFBO); - - // We can avoid the clear by drawing the same clear vallue from the makeCUrvature shader. slightly better than adding the clear - // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + + // We can avoid the clear by drawing the same clear vallue from the makeCurvature shader. same performances or no worse + +#ifdef USE_STENCIL_TEST + // Except if stenciling out + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); +#endif batch.setPipeline(curvaturePipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, linearDepthTexture); @@ -354,9 +438,10 @@ const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); +#ifdef USE_STENCIL_TEST // Stencil test the curvature pass for objects pixels only, not the background state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - +#endif // Good to go add the brand new pipeline _curvaturePipeline = gpu::Pipeline::create(program, state); } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 8137deda35..5b9b64a11c 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -28,6 +28,10 @@ public: gpu::FramebufferPointer getLinearDepthFramebuffer(); gpu::TexturePointer getLinearDepthTexture(); + gpu::FramebufferPointer getDownsampleFramebuffer(); + gpu::TexturePointer getHalfLinearDepthTexture(); + gpu::TexturePointer getHalfNormalTexture(); + // Update the depth buffer which will drive the allocation of all the other resources according to its size. void updatePrimaryDepth(const gpu::TexturePointer& depthBuffer); gpu::TexturePointer getPrimaryDepthTexture(); @@ -44,8 +48,14 @@ protected: gpu::FramebufferPointer _linearDepthFramebuffer; gpu::TexturePointer _linearDepthTexture; + + gpu::FramebufferPointer _downsampleFramebuffer; + gpu::TexturePointer _halfLinearDepthTexture; + gpu::TexturePointer _halfNormalTexture; + glm::ivec2 _frameSize; + glm::ivec2 _halfFrameSize; int _resolutionLevel{ 0 }; }; @@ -69,7 +79,7 @@ signals: class LinearDepthPass { public: using Inputs = render::VaryingSet2; - using Outputs = render::VaryingSet3; + using Outputs = render::VaryingSet5; using Config = LinearDepthPassConfig; using JobModel = render::Job::ModelIO; @@ -84,9 +94,11 @@ private: LinearDepthFramebufferPointer _linearDepthFramebuffer; const gpu::PipelinePointer& getLinearDepthPipeline(); - gpu::PipelinePointer _linearDepthPipeline; + const gpu::PipelinePointer& getDownsamplePipeline(); + gpu::PipelinePointer _downsamplePipeline; + gpu::RangeTimer _gpuTimer; }; diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index 4b8e8d48ce..079f79c06e 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -14,7 +14,9 @@ <@include DeferredBufferRead.slh@> -uniform sampler2D pyramidMap; +uniform sampler2D linearDepthMap; +uniform sampler2D halfLinearDepthMap; +uniform sampler2D halfNormalMap; uniform sampler2D occlusionMap; uniform sampler2D occlusionBlurredMap; uniform sampler2D curvatureMap; diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index bb3a073a2e..09836f2ab0 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -78,8 +78,10 @@ vec3 getRawNormal(vec2 texcoord) { } vec3 getWorldNormal(vec2 texcoord) { + // vec3 rawNormal = getRawNormal(texcoord); + // return unpackNormal(rawNormal); vec3 rawNormal = getRawNormal(texcoord); - return unpackNormal(rawNormal); + return normalize((rawNormal - vec3(0.5)) * 2.0); } vec3 getWorldNormalDiff(vec2 texcoord, vec2 delta) { diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 4a3118d0d3..597df37b7a 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -101,9 +101,9 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra _blurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); // attach depthStencil if present in source - if (sourceFramebuffer->hasDepthStencil()) { - _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } + //if (sourceFramebuffer->hasDepthStencil()) { + // _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + //} auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); _blurredFramebuffer->setRenderBuffer(0, blurringTarget); @@ -111,9 +111,9 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra // it would be easier to just call resize on the bluredFramebuffer and let it work if needed but the source might loose it's depth buffer when doing so if ((_blurredFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_blurredFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { _blurredFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); - if (sourceFramebuffer->hasDepthStencil()) { - _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } + //if (sourceFramebuffer->hasDepthStencil()) { + // _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); + //} } } @@ -128,18 +128,18 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); // attach depthStencil if present in source - if (sourceFramebuffer->hasDepthStencil()) { + /* if (sourceFramebuffer->hasDepthStencil()) { _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } + }*/ auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); _outputFramebuffer->setRenderBuffer(0, blurringTarget); } else { if ((_outputFramebuffer->getWidth() != sourceFramebuffer->getWidth()) || (_outputFramebuffer->getHeight() != sourceFramebuffer->getHeight())) { _outputFramebuffer->resize(sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), sourceFramebuffer->getNumSamples()); - if (sourceFramebuffer->hasDepthStencil()) { + /* if (sourceFramebuffer->hasDepthStencil()) { _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); - } + }*/ } } @@ -173,7 +173,7 @@ gpu::PipelinePointer BlurGaussian::getBlurVPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); // Stencil test the curvature pass for objects pixels only, not the background - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); _blurVPipeline = gpu::Pipeline::create(program, state); } @@ -195,7 +195,7 @@ gpu::PipelinePointer BlurGaussian::getBlurHPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); // Stencil test the curvature pass for objects pixels only, not the background - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + // state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); _blurHPipeline = gpu::Pipeline::create(program, state); } @@ -336,22 +336,25 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const auto blurVPipeline = getBlurVPipeline(); auto blurHPipeline = getBlurHPipeline(); - _parameters->setWidthHeight(args->_viewport.z, args->_viewport.w, args->_context->isStereo()); + auto sourceViewport = args->_viewport; + auto blurViewport = sourceViewport >> 1; + + _parameters->setWidthHeight(blurViewport.z, blurViewport.w, args->_context->isStereo()); glm::ivec2 textureSize(blurringResources.sourceTexture->getDimensions()); - _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, args->_viewport)); + _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, blurViewport)); _parameters->setDepthPerspective(args->getViewFrustum().getProjection()[1][1]); _parameters->setLinearDepthPosFar(args->getViewFrustum().getFarClip()); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); - batch.setViewportTransform(args->_viewport); + batch.setViewportTransform(blurViewport); batch.setUniformBuffer(BlurTask_ParamsSlot, _parameters->_parametersBuffer); batch.setResourceTexture(BlurTask_DepthSlot, depthTexture); batch.setFramebuffer(blurringResources.blurringFramebuffer); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); batch.setPipeline(blurVPipeline); batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.sourceTexture); @@ -359,7 +362,7 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const batch.setFramebuffer(blurringResources.finalFramebuffer); if (_inOutResources._generateOutputFramebuffer) { - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); } batch.setPipeline(blurHPipeline); diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index de40740c19..d40aca4a76 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -95,14 +95,22 @@ vec4 pixelShaderGaussian(vec2 texcoord, vec2 direction, vec2 pixelStep) { uniform sampler2D sourceMap; uniform sampler2D depthMap; +#define NUM_HALF_TAPS 4 + +const float gaussianDistributionCurveHalf[NUM_HALF_TAPS] = float[]( + 0.383f, 0.242f, 0.061f, 0.006f +); +const float gaussianDistributionOffsetHalf[NUM_HALF_TAPS] = float[]( + 0.0f, 1.0f, 2.0f, 3.0f +); + vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep) { texcoord = evalTexcoordTransformed(texcoord); float sampleDepth = texture(depthMap, texcoord).x; - vec4 sampleCenter = texture(sourceMap, texcoord); if (sampleDepth >= getPosLinearDepthFar()) { discard; - //return sampleCenter; } + vec4 sampleCenter = texture(sourceMap, texcoord); // Calculate the width scale. float distanceToProjectionWindow = getDepthPerspective(); @@ -117,21 +125,40 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep // Accumulate the center sample vec4 srcBlurred = gaussianDistributionCurve[0] * sampleCenter; - - for(int i = 1; i < NUM_TAPS; i++) { + +/* for(int i = 1; i < NUM_TAPS; i++) { // Fetch color and depth for current sample. vec2 sampleCoord = texcoord + (gaussianDistributionOffset[i] * finalStep); float srcDepth = texture(depthMap, sampleCoord).x; vec4 srcSample = texture(sourceMap, sampleCoord); - - + // If the difference in depth is huge, we lerp color back. float s = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); - // float s = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepth - sampleDepth), 0.0, 1.0); srcSample = mix(srcSample, sampleCenter, s); // Accumulate. srcBlurred += gaussianDistributionCurve[i] * srcSample; + } +*/ + + for(int i = 1; i < NUM_HALF_TAPS; i++) { + // Fetch color and depth for current sample. + vec2 texcoordOffset = (gaussianDistributionOffsetHalf[i] * finalStep); + + float srcDepthN = texture(depthMap, texcoord - texcoordOffset).x; + float srcDepthP = texture(depthMap, texcoord + texcoordOffset).x; + vec4 srcSampleN = texture(sourceMap, texcoord - texcoordOffset); + vec4 srcSampleP = texture(sourceMap, texcoord + texcoordOffset); + + // If the difference in depth is huge, we lerp color back. + float sN = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepthN - sampleDepth), 0.0, 1.0); + float sP = clamp(depthThreshold * distanceToProjectionWindow * filterScale * abs(srcDepthP - sampleDepth), 0.0, 1.0); + + srcSampleN = mix(srcSampleN, sampleCenter, sN); + srcSampleP = mix(srcSampleP, sampleCenter, sP); + + // Accumulate. + srcBlurred += gaussianDistributionCurveHalf[i] * (srcSampleP + srcSampleN); } return srcBlurred; diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index d5b41e539e..8268090997 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -206,6 +206,21 @@ public: const T4& get4() const { return std::get<4>((*this)).template get(); } T4& edit4() { return std::get<4>((*this)).template edit(); } + virtual Varying operator[] (uint8_t index) const { + if (index == 4) { + return std::get<4>((*this)); + } else if (index == 3) { + return std::get<3>((*this)); + } else if (index == 2) { + return std::get<2>((*this)); + } else if (index == 1) { + return std::get<1>((*this)); + } else { + return std::get<0>((*this)); + } + } + virtual uint8_t length() const { return 5; } + Varying hasVarying() const { return Varying((*this)); } }; diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 098f101b6d..12f21ed366 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -151,7 +151,8 @@ Column { ListElement { text: "Lighting"; color: "White" } ListElement { text: "Shadow"; color: "White" } ListElement { text: "Linear Depth"; color: "White" } - ListElement { text: "Linear Depth LOD2"; color: "White" } + ListElement { text: "Half Linear Depth"; color: "White" } + ListElement { text: "Half Normal"; color: "White" } ListElement { text: "Mid Curvature"; color: "White" } ListElement { text: "Mid Normal"; color: "White" } ListElement { text: "Low Curvature"; color: "White" } From 86d9ee56d2ddedd977f3d3f1ad3fd75fb2946267 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 18 Jul 2016 03:56:11 -0700 Subject: [PATCH 1193/1237] Downsampling shader pass --- .../surfaceGeometry_downsampleDepthNormal.slf | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf diff --git a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf new file mode 100644 index 0000000000..049a357776 --- /dev/null +++ b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf @@ -0,0 +1,70 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 6/3/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + + + +uniform sampler2D linearDepthMap; +uniform sampler2D normalMap; + + +vec2 signNotZero(vec2 v) { + return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); +} + +vec3 oct_to_float32x3(in vec2 e) { + vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); + if (v.z < 0) { + v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); + } + return normalize(v); +} + +vec2 unorm8x3_to_snorm12x2(vec3 u) { + u *= 255.0; + u.y *= (1.0 / 16.0); + vec2 s = vec2( u.x * 16.0 + floor(u.y), + fract(u.y) * (16.0 * 256.0) + u.z); + return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); +} +vec3 unpackNormal(in vec3 p) { + return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); +} + +in vec2 varTexCoord0; + +out vec4 outLinearDepth; +out vec4 outNormal; + +void main(void) { + float Zeye = texture(linearDepthMap, varTexCoord0).x; + + ivec2 texpos = ivec2(gl_FragCoord.xy) * 2; + + vec3 rawNormals[4]; + rawNormals[0] = texelFetch(normalMap, texpos, 0).xyz; + rawNormals[1] = texelFetch(normalMap, texpos + ivec2(0, 1), 0).xyz; + rawNormals[2] = texelFetch(normalMap, texpos + ivec2(1, 0), 0).xyz; + rawNormals[3] = texelFetch(normalMap, texpos + ivec2(1, 1), 0).xyz; + + vec3 normal = vec3(0.0); + + normal += unpackNormal( rawNormals[0] ); + normal += unpackNormal( rawNormals[1] ); + normal += unpackNormal( rawNormals[2] ); + normal += unpackNormal( rawNormals[3] ); + + normal = normalize(normal); + + outLinearDepth = vec4(Zeye, 0.0, 0.0, 0.0); + outNormal = vec4((normal + vec3(1.0)) * 0.5, 0.0); +} + From a4ad4659c4a0429ad5f7cfb784604c9089e248ac Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 18 Jul 2016 11:17:06 -0700 Subject: [PATCH 1194/1237] Fixing build warnings --- libraries/render-utils/src/DebugDeferredBuffer.cpp | 2 ++ libraries/render-utils/src/RenderDeferredTask.cpp | 2 +- libraries/render-utils/src/SurfaceGeometryPass.cpp | 5 ++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 3a8e6a822c..672bb00cc8 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -287,6 +287,8 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_AMBIENT_OCCLUSION_BLURRED_SHADER; case CustomMode: return getFileContent(customFile, DEFAULT_CUSTOM_SHADER); + default: + return DEFAULT_ALBEDO_SHADER; } Q_UNREACHABLE(); return std::string(); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 343e9fbed0..32c6cab40d 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -227,7 +227,7 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend if (!(renderContext->args && renderContext->args->hasViewFrustum())) { return; } - RenderArgs* args = renderContext->args; + auto config = std::static_pointer_cast(renderContext->jobConfig); for (auto job : _jobs) { diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 57b0b7e08d..a30d8524da 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -165,8 +165,7 @@ void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const auto depthViewport = args->_viewport; auto halfViewport = depthViewport >> 1; - auto furtherDepth = std::numeric_limits::infinity(); - furtherDepth = args->getViewFrustum().getFarClip() + 10.0; + float clearLinearDepth = args->getViewFrustum().getFarClip() * 2.0f; gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { _gpuTimer.begin(batch); @@ -181,7 +180,7 @@ void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const // LinearDepth batch.setFramebuffer(linearDepthFBO); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(furtherDepth, 0.0f, 0.0f, 0.0f)); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(clearLinearDepth, 0.0f, 0.0f, 0.0f)); batch.setPipeline(linearDepthPipeline); batch.setResourceTexture(DepthLinearPass_DepthMapSlot, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); From 0f240d39b61905313bd76127752d393cf65b6ff4 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 18 Jul 2016 11:21:09 -0700 Subject: [PATCH 1195/1237] Actually delete textures we're not using --- libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index ed931437b7..74428b53f5 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -187,7 +187,11 @@ GLTexture::~GLTexture() { } } - Backend::decrementTextureGPUCount(); + if (_id) { + glDeleteTextures(1, &_id); + const_cast(_id) = 0; + Backend::decrementTextureGPUCount(); + } Backend::updateTextureGPUMemoryUsage(_size, 0); Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); } From 5a01bf406e267d7a90fd43792f2761f0197f781d Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 18 Jul 2016 11:29:41 -0700 Subject: [PATCH 1196/1237] remove obsolete handControllerMouse.js to avoid confusion --- .../system/controllers/handControllerMouse.js | 131 ------------------ 1 file changed, 131 deletions(-) delete mode 100644 scripts/system/controllers/handControllerMouse.js diff --git a/scripts/system/controllers/handControllerMouse.js b/scripts/system/controllers/handControllerMouse.js deleted file mode 100644 index 921999f96a..0000000000 --- a/scripts/system/controllers/handControllerMouse.js +++ /dev/null @@ -1,131 +0,0 @@ -// -// handControllerMouse.js -// examples/controllers -// -// Created by Brad Hefta-Gaub on 2015/12/15 -// 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 -// - -var DEBUGGING = false; -var angularVelocityTrailingAverage = 0.0; // Global trailing average used to decide whether to move reticle at all -var lastX = 0; -var lastY = 0; - -Math.clamp=function(a,b,c) { - return Math.max(b,Math.min(c,a)); -} - -function length(posA, posB) { - var dx = posA.x - posB.x; - var dy = posA.y - posB.y; - var length = Math.sqrt((dx*dx) + (dy*dy)) - return length; -} - -function moveReticleAbsolute(x, y) { - var globalPos = Reticle.getPosition(); - globalPos.x = x; - globalPos.y = y; - Reticle.setPosition(globalPos); -} - -var MAPPING_NAME = "com.highfidelity.testing.reticleWithHandRotation"; -var mapping = Controller.newMapping(MAPPING_NAME); -if (Controller.Hardware.Hydra !== undefined) { - mapping.from(Controller.Hardware.Hydra.L3).peek().to(Controller.Actions.ReticleClick); - mapping.from(Controller.Hardware.Hydra.R4).peek().to(Controller.Actions.ReticleClick); -} -if (Controller.Hardware.Vive !== undefined) { - mapping.from(Controller.Hardware.Vive.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick); - mapping.from(Controller.Hardware.Vive.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick); -} - -mapping.enable(); - -function debugPrint(message) { - if (DEBUGGING) { - print(message); - } -} - -var leftRightBias = 0.0; -var filteredRotatedLeft = Vec3.UNIT_NEG_Y; -var filteredRotatedRight = Vec3.UNIT_NEG_Y; -var lastAlpha = 0; - -Script.update.connect(function(deltaTime) { - - // avatar frame - var poseRight = Controller.getPoseValue(Controller.Standard.RightHand); - var poseLeft = Controller.getPoseValue(Controller.Standard.LeftHand); - - // NOTE: hack for now - var screenSize = Reticle.maximumPosition; - var screenSizeX = screenSize.x; - var screenSizeY = screenSize.y; - - // transform hand facing vectors from avatar frame into sensor frame. - var worldToSensorMatrix = Mat4.inverse(MyAvatar.sensorToWorldMatrix); - var rotatedRight = Mat4.transformVector(worldToSensorMatrix, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.multiplyQbyV(poseRight.rotation, Vec3.UNIT_NEG_Y))); - var rotatedLeft = Mat4.transformVector(worldToSensorMatrix, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.multiplyQbyV(poseLeft.rotation, Vec3.UNIT_NEG_Y))); - - lastRotatedRight = rotatedRight; - - // Decide which hand should be controlling the pointer - // by comparing which one is moving more, and by - // tending to stay with the one moving more. - if (deltaTime > 0.001) { - // leftRightBias is a running average of the difference in angular hand speed. - // a positive leftRightBias indicates the right hand is spinning faster then the left hand. - // a negative leftRightBias indicates the left hand is spnning faster. - var BIAS_ADJUST_PERIOD = 1.0; - var tau = Math.clamp(deltaTime / BIAS_ADJUST_PERIOD, 0, 1); - newLeftRightBias = Vec3.length(poseRight.angularVelocity) - Vec3.length(poseLeft.angularVelocity); - leftRightBias = (1 - tau) * leftRightBias + tau * newLeftRightBias; - } - - // add a bit of hysteresis to prevent control flopping back and forth - // between hands when they are both mostly stationary. - var alpha; - var HYSTERESIS_OFFSET = 0.25; - if (lastAlpha > 0.5) { - // prefer right hand over left - alpha = leftRightBias > -HYSTERESIS_OFFSET ? 1 : 0; - } else { - alpha = leftRightBias > HYSTERESIS_OFFSET ? 1 : 0; - } - lastAlpha = alpha; - - // Velocity filter the hand rotation used to position reticle so that it is easier to target small things with the hand controllers - var VELOCITY_FILTER_GAIN = 0.5; - filteredRotatedLeft = Vec3.mix(filteredRotatedLeft, rotatedLeft, Math.clamp(Vec3.length(poseLeft.angularVelocity) * VELOCITY_FILTER_GAIN, 0.0, 1.0)); - filteredRotatedRight = Vec3.mix(filteredRotatedRight, rotatedRight, Math.clamp(Vec3.length(poseRight.angularVelocity) * VELOCITY_FILTER_GAIN, 0.0, 1.0)); - var rotated = Vec3.mix(filteredRotatedLeft, filteredRotatedRight, alpha); - - var absolutePitch = rotated.y; // from 1 down to -1 up ... but note: if you rotate down "too far" it starts to go up again... - var absoluteYaw = -rotated.x; // from -1 left to 1 right - - var x = Math.clamp(screenSizeX * (absoluteYaw + 0.5), 0, screenSizeX); - var y = Math.clamp(screenSizeX * absolutePitch, 0, screenSizeY); - - // don't move the reticle with the hand controllers unless the controllers are actually being moved - // take a time average of angular velocity, and don't move mouse at all if it's below threshold - - var AVERAGING_INTERVAL = 0.95; - var MINIMUM_CONTROLLER_ANGULAR_VELOCITY = 0.03; - var angularVelocityMagnitude = Vec3.length(poseLeft.angularVelocity) * (1.0 - alpha) + Vec3.length(poseRight.angularVelocity) * alpha; - angularVelocityTrailingAverage = angularVelocityTrailingAverage * AVERAGING_INTERVAL + angularVelocityMagnitude * (1.0 - AVERAGING_INTERVAL); - - if ((angularVelocityTrailingAverage > MINIMUM_CONTROLLER_ANGULAR_VELOCITY) && ((x != lastX) || (y != lastY))) { - moveReticleAbsolute(x, y); - lastX = x; - lastY = y; - } -}); - -Script.scriptEnding.connect(function(){ - mapping.disable(); -}); From be4839d46c2ae373ab4a17493f22cf2ad543abd1 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 18 Jul 2016 12:26:04 -0700 Subject: [PATCH 1197/1237] a better depth filter --- .../surfaceGeometry_downsampleDepthNormal.slf | 37 +++++++++++++++---- .../src/surfaceGeometry_makeCurvature.slf | 2 +- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf index 049a357776..6bd9850d1a 100644 --- a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf +++ b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf @@ -45,23 +45,46 @@ out vec4 outLinearDepth; out vec4 outNormal; void main(void) { - float Zeye = texture(linearDepthMap, varTexCoord0).x; +#if __VERSION__ == 450 + // Gather 2 by 2 quads from texture + vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); + vec4 rawNormalsX = textureGather(normalMap, varTexCoord0, 0); + vec4 rawNormalsY = textureGather(normalMap, varTexCoord0, 1); + vec4 rawNormalsZ = textureGather(normalMap, varTexCoord0, 2); + + float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); + + vec3 normal = vec3(0.0); + normal += unpackNormal(vec3(rawNormalsX[0], rawNormalsY[0], rawNormalsZ[0])); + normal += unpackNormal(vec3(rawNormalsX[1], rawNormalsY[1], rawNormalsZ[1])); + normal += unpackNormal(vec3(rawNormalsX[2], rawNormalsY[2], rawNormalsZ[2])); + normal += unpackNormal(vec3(rawNormalsX[3], rawNormalsY[3], rawNormalsZ[3])); +#else ivec2 texpos = ivec2(gl_FragCoord.xy) * 2; - + + vec4 Zeyes; + Zeyes[0] = texelFetch(linearDepthMap, texpos, 0).x; + Zeyes[1] = texelFetch(linearDepthMap, texpos + ivec2(0, 1), 0).x; + Zeyes[2] = texelFetch(linearDepthMap, texpos + ivec2(1, 0), 0).x; + Zeyes[3] = texelFetch(linearDepthMap, texpos + ivec2(1, 1), 0).x; + vec3 rawNormals[4]; rawNormals[0] = texelFetch(normalMap, texpos, 0).xyz; rawNormals[1] = texelFetch(normalMap, texpos + ivec2(0, 1), 0).xyz; rawNormals[2] = texelFetch(normalMap, texpos + ivec2(1, 0), 0).xyz; rawNormals[3] = texelFetch(normalMap, texpos + ivec2(1, 1), 0).xyz; + float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); + vec3 normal = vec3(0.0); - normal += unpackNormal( rawNormals[0] ); - normal += unpackNormal( rawNormals[1] ); - normal += unpackNormal( rawNormals[2] ); - normal += unpackNormal( rawNormals[3] ); - + normal += unpackNormal(rawNormals[0]); + normal += unpackNormal(rawNormals[1]); + normal += unpackNormal(rawNormals[2]); + normal += unpackNormal(rawNormals[3]); +#endif + normal = normalize(normal); outLinearDepth = vec4(Zeye, 0.0, 0.0, 0.0); diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index 09836f2ab0..bf8ca6abf3 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -112,7 +112,7 @@ void main(void) { // Fetch the z under the pixel (stereo or not) float Zeye = getZEye(framePixelPos); if (Zeye <= -getPosLinearDepthFar()) { - outFragColor = vec4(0.0); + outFragColor = vec4(1.0, 0.0, 0.0, 0.0); return; } From 80587ca8a355c78563f7cade7e84d055f97a9766 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 18 Jul 2016 12:52:21 -0700 Subject: [PATCH 1198/1237] using the textureGather lookup --- libraries/render-utils/src/SurfaceGeometryPass.h | 2 +- .../src/surfaceGeometry_downsampleDepthNormal.slf | 5 ++--- .../utilities/render/deferredLighting.qml | 14 +------------- .../utilities/render/surfaceGeometryPass.qml | 2 +- 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 5b9b64a11c..14c8316d9e 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -146,7 +146,7 @@ class SurfaceGeometryPassConfig : public render::Job::Config { public: SurfaceGeometryPassConfig() : render::Job::Config(true) {} - float depthThreshold{ 0.02f }; // meters + float depthThreshold{ 0.005f }; // meters float basisScale{ 1.0f }; float curvatureScale{ 10.0f }; int resolutionLevel{ 0 }; diff --git a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf index 6bd9850d1a..af371d53a8 100644 --- a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf +++ b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf @@ -45,7 +45,6 @@ out vec4 outLinearDepth; out vec4 outNormal; void main(void) { -#if __VERSION__ == 450 // Gather 2 by 2 quads from texture vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); @@ -60,7 +59,7 @@ void main(void) { normal += unpackNormal(vec3(rawNormalsX[1], rawNormalsY[1], rawNormalsZ[1])); normal += unpackNormal(vec3(rawNormalsX[2], rawNormalsY[2], rawNormalsZ[2])); normal += unpackNormal(vec3(rawNormalsX[3], rawNormalsY[3], rawNormalsZ[3])); -#else + /* ivec2 texpos = ivec2(gl_FragCoord.xy) * 2; vec4 Zeyes; @@ -83,7 +82,7 @@ void main(void) { normal += unpackNormal(rawNormals[1]); normal += unpackNormal(rawNormals[2]); normal += unpackNormal(rawNormals[3]); -#endif +*/ normal = normalize(normal); diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 12f21ed366..fedb99fc74 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -58,19 +58,7 @@ Column { "Ambient:LightingModel:enableAmbientLight", "Directional:LightingModel:enableDirectionalLight", "Point:LightingModel:enablePointLight", - "Spot:LightingModel:enableSpotLight" - ] - CheckBox { - text: modelData.split(":")[0] - checked: Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] - onCheckedChanged: { Render.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } - } - } - } - Column { - spacing: 10 - Repeater { - model: [ + "Spot:LightingModel:enableSpotLight", "Light Contour:LightingModel:showLightContour" ] CheckBox { diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index 0021ecc96c..23a846ee79 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -20,7 +20,7 @@ Column { Column{ Repeater { model: [ - "Depth Threshold:depthThreshold:0.1:false", + "Depth Threshold:depthThreshold:0.05:false", "Basis Scale:basisScale:2.0:false", "Curvature Scale:curvatureScale:100.0:false", "Downscale:resolutionLevel:4:true" From 959a2f9915e6ef06816135cf4d25b2b9d2b68c46 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 18 Jul 2016 13:10:16 -0700 Subject: [PATCH 1199/1237] far-grabbed objects no longer fly away when the avatar body shifts Before this fix, a hand vector was being tracked that was the difference between the avatar's hand in avatar space with the avatar's hand in avatar space the previous frame. This hand vector was used to move the grabbed object position. However, when the body shifts, objects in the avatar's space will change rapidly, this would cause this hand vector to be incorrect and cause the object to shoot off in the distance. Now, we track this hand delta in sensor a.k.a. room space. This is immune to the shifts caused by body shifting. --- .../system/controllers/handControllerGrab.js | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 66c9e10795..50d5d644ac 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1592,13 +1592,16 @@ function MyController(hand) { this.clearEquipHaptics(); // controller pose is in avatar frame - var avatarControllerPose = - Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var device = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + var avatarControllerPose = Controller.getPoseValue(device); // transform it into world frame - var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); - var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar); - var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); + var worldControllerPosition = Vec3.sum(MyAvatar.position, + Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation)); + + // also transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); var now = Date.now(); @@ -1609,7 +1612,7 @@ function MyController(hand) { this.currentObjectTime = now; this.currentCameraOrientation = Camera.orientation; - this.grabRadius = Vec3.distance(this.currentObjectPosition, controllerPosition); + this.grabRadius = Vec3.distance(this.currentObjectPosition, worldControllerPosition); this.grabRadialVelocity = 0.0; // compute a constant based on the initial conditions which we use below to exagerate hand motion onto the held object @@ -1644,8 +1647,7 @@ function MyController(hand) { this.turnOffVisualizations(); - this.previousControllerPositionVSAvatar = controllerPositionVSAvatar; - this.previousControllerRotation = controllerRotation; + this.previousRoomControllerPosition = roomControllerPosition; }; this.distanceHolding = function (deltaTime, timestamp) { @@ -1658,13 +1660,17 @@ function MyController(hand) { this.heartBeat(this.grabbedEntity); // controller pose is in avatar frame - var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); + var device = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + var avatarControllerPose = Controller.getPoseValue(device); // transform it into world frame - var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); - var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar); - var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); + var worldControllerPosition = Vec3.sum(MyAvatar.position, + Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation)); + var worldControllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); + + // also transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); @@ -1673,26 +1679,15 @@ function MyController(hand) { this.currentObjectTime = now; // the action was set up when this.distanceHolding was called. update the targets. - var radius = Vec3.distance(this.currentObjectPosition, controllerPosition) * + var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) * this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR; if (radius < 1.0) { radius = 1.0; } - // scale delta controller hand movement by radius. - var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), - radius); - - /// double delta controller rotation - // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did - // var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, - // controllerRotation, - // DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - // Quat.inverse(this.previousControllerRotation)); - - // update the currentObject position and rotation. + var handDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); + var handMoved = Vec3.multiply(handDelta, radius); this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); - // this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); this.callEntityMethodOnGrabbed("continueDistantGrab"); @@ -1703,10 +1698,9 @@ function MyController(hand) { var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); // Update radialVelocity - var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); - lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); - var newRadialVelocity = Vec3.dot(lastVelocity, - Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); + var lastVelocity = Vec3.multiply(handDelta, 1.0 / deltaObjectTime); + var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); + var newRadialVelocity = Vec3.dot(lastVelocity, delta); var VELOCITY_AVERAGING_TIME = 0.016; this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + @@ -1718,9 +1712,8 @@ function MyController(hand) { this.grabRadius * RADIAL_GRAB_AMPLIFIER); } - var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); - newTargetPosition = Vec3.sum(newTargetPosition, controllerPosition); - + var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position); if (handControllerData.disableMoveWithHead !== true) { @@ -1776,8 +1769,7 @@ function MyController(hand) { print("continueDistanceHolding -- updateAction failed"); } - this.previousControllerPositionVSAvatar = controllerPositionVSAvatar; - this.previousControllerRotation = controllerRotation; + this.previousRoomControllerPosition = roomControllerPosition; }; this.setupHoldAction = function () { From 3df373252f8cbe7ae7e227e5bd545a7472085886 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 18 Jul 2016 14:00:41 -0700 Subject: [PATCH 1200/1237] Several minor things We could only partially fill the _scratchBuffer - .wav files may not be exactly N frames long. Doh. While at it, I needed to call finishLocalInjection() after local injectors are done, and the access to the injector vector needs to be locked, given that we do a QtDirectConnection with the networking and thus the outputLocalInjectors is on a different thread. The clicking was just 0-ing out the _scratchBuffer. --- libraries/audio-client/src/AudioClient.cpp | 9 +++++++-- libraries/audio-client/src/AudioClient.h | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b5a7c8c0cf..7cf8574529 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -862,6 +862,9 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { static const float INT16_TO_FLOAT_SCALE_FACTOR = 1/32768.0f; bool injectorsHaveData = false; + + // lock the injector vector + Lock lock(_injectorsMutex); for (AudioInjector* injector : getActiveLocalAudioInjectors()) { if (injector->getLocalBuffer()) { @@ -871,6 +874,7 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector (mono or stereo) + memset(_scratchBuffer, 0, sizeof(_scratchBuffer)); if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, samplesToRead)) { injectorsHaveData = true; @@ -894,14 +898,14 @@ void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { } else { qDebug() << "injector has no more data, marking finished for removal"; - injector->finish(); + injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { qDebug() << "injector has no local buffer, marking as finished for removal"; - injector->finish(); + injector->finishLocalInjection(); injectorsToRemove.append(injector); } } @@ -1003,6 +1007,7 @@ void AudioClient::setIsStereoInput(bool isStereoInput) { bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { + Lock lock(_injectorsMutex); if (injector->getLocalBuffer() && _audioInput ) { // just add it to the vector of active local injectors, if // not already there. diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3e4aa931a6..472092163b 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -83,6 +84,9 @@ public: using AudioPositionGetter = std::function; using AudioOrientationGetter = std::function; + using Mutex = std::mutex; + using Lock = std::unique_lock; + class AudioOutputIODevice : public QIODevice { public: AudioOutputIODevice(MixedProcessedAudioStream& receivedAudioStream, AudioClient* audio) : @@ -219,6 +223,7 @@ private: float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); + Mutex _injectorsMutex; QByteArray firstInputFrame; QAudioInput* _audioInput; QAudioFormat _desiredInputFormat; From 659e1ff9845b80a9b0da64019adc77ff8015dc95 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 18 Jul 2016 14:17:06 -0700 Subject: [PATCH 1201/1237] remove debug prints --- scripts/system/controllers/handControllerGrab.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f622bf9217..43152f763f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -277,7 +277,6 @@ var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; function getAttachPointSettings() { try { var str = Settings.getValue(ATTACH_POINT_SETTINGS); - print("getAttachPointSettings = " + str); if (str === "false") { return {}; } else { @@ -290,7 +289,6 @@ function getAttachPointSettings() { } function setAttachPointSettings(attachPointSettings) { var str = JSON.stringify(attachPointSettings); - print("setAttachPointSettings = " + str); Settings.setValue(ATTACH_POINT_SETTINGS, str); } function getAttachPointForHotspotFromSettings(hotspot, hand) { From 69500643f3592ab3a669f12ed39879941b7ee87d Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 18 Jul 2016 14:17:32 -0700 Subject: [PATCH 1202/1237] bugfix for saving attach points The feature to add transition from equip -> near-grab, in inadvertently broke attach point saving. --- .../system/controllers/handControllerGrab.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 43152f763f..cf8146fba9 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -202,8 +202,7 @@ CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { CONTROLLER_STATE_MACHINE[STATE_HOLD] = { name: "hold", enterMethod: "nearGrabbingEnter", - updateMethod: "nearGrabbing", - exitMethod: "holdExit" + updateMethod: "nearGrabbing" }; CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { name: "trigger", @@ -1990,6 +1989,15 @@ function MyController(hand) { if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) { this.callEntityMethodOnGrabbed("releaseEquip"); + + // store the offset attach points into preferences. + if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedEntity) { + var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + if (props && props.localPosition && props.localRotation) { + storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, props.localPosition, props.localRotation); + } + } + var grabbedEntity = this.grabbedEntity; this.release(); this.grabbedEntity = grabbedEntity; @@ -2092,16 +2100,6 @@ function MyController(hand) { } }; - this.holdExit = function () { - // store the offset attach points into preferences. - if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedEntity) { - var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); - if (props && props.localPosition && props.localRotation) { - storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, props.localPosition, props.localRotation); - } - } - }; - this.nearTriggerEnter = function () { this.clearEquipHaptics(); From 1a66574adb44542157af46d04167645ef6533bb0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 18 Jul 2016 14:29:49 -0700 Subject: [PATCH 1203/1237] add protocol version signature to metaverse heartbeat --- domain-server/src/DomainServer.cpp | 4 +++- libraries/networking/src/udt/PacketHeaders.cpp | 14 +++++++++++--- libraries/networking/src/udt/PacketHeaders.h | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8b3f09d1f7..bcfdd42c34 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1083,9 +1083,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // Setup the domain object to send to the data server QJsonObject domainObject; - // add the version + // add the versions static const QString VERSION_KEY = "version"; domainObject[VERSION_KEY] = BuildInfo::VERSION; + static const QString PROTOCOL_KEY = "protocol"; + domainObject[PROTOCOL_KEY] = protocolVersionsSignatureBase64(); // add networking if (!networkAddress.isEmpty()) { diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index fca006ae87..8542e9f60e 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -99,8 +99,9 @@ void sendWrongProtocolVersionsSignature(bool sendWrongVersion) { } #endif -QByteArray protocolVersionsSignature() { - static QByteArray protocolVersionSignature; +static QByteArray protocolVersionSignature; +static QString protocolVersionSignatureBase64; +static void ensureProtocolVersionsSignature() { static std::once_flag once; std::call_once(once, [&] { QByteArray buffer; @@ -114,8 +115,11 @@ QByteArray protocolVersionsSignature() { QCryptographicHash hash(QCryptographicHash::Md5); hash.addData(buffer); protocolVersionSignature = hash.result(); + protocolVersionSignatureBase64 = protocolVersionSignature.toBase64(); }); - +} +QByteArray protocolVersionsSignature() { + ensureProtocolVersionsSignature(); #if (PR_BUILD || DEV_BUILD) if (sendWrongProtocolVersion) { return QByteArray("INCORRECTVERSION"); // only for debugging version checking @@ -124,3 +128,7 @@ QByteArray protocolVersionsSignature() { return protocolVersionSignature; } +QString protocolVersionsSignatureBase64() { + ensureProtocolVersionsSignature(); + return protocolVersionSignatureBase64; +} diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 85030135a1..0e10a8fd76 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -113,6 +113,7 @@ extern const QSet NON_SOURCED_PACKETS; PacketVersion versionForPacketType(PacketType packetType); QByteArray protocolVersionsSignature(); /// returns a unqiue signature for all the current protocols +QString protocolVersionsSignatureBase64(); #if (PR_BUILD || DEV_BUILD) void sendWrongProtocolVersionsSignature(bool sendWrongVersion); /// for debugging version negotiation From 2d73b23c5693d48cee876752b61dd63e4095b59f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 18 Jul 2016 14:44:41 -0700 Subject: [PATCH 1204/1237] when deciding on the release velocity of something thrown, don't include a zero velocity caused by seeing the same controller data as last frame --- interface/src/avatar/AvatarActionHold.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index bac3b1e02f..5acee052f2 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -207,8 +207,10 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { } withWriteLock([&]{ - if (_previousSet) { + if (_previousSet && + _positionalTarget != _previousPositionalTarget) { // don't average in a zero velocity if we get the same data glm::vec3 oneFrameVelocity = (_positionalTarget - _previousPositionalTarget) / deltaTimeStep; + _measuredLinearVelocities[_measuredLinearVelocitiesIndex++] = oneFrameVelocity; if (_measuredLinearVelocitiesIndex >= AvatarActionHold::velocitySmoothFrames) { _measuredLinearVelocitiesIndex = 0; @@ -228,9 +230,9 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { // 3 -- ignore i of 0 1 2 // 4 -- ignore i of 1 2 3 // 5 -- ignore i of 2 3 4 - if ((i + 1) % 6 == _measuredLinearVelocitiesIndex || - (i + 2) % 6 == _measuredLinearVelocitiesIndex || - (i + 3) % 6 == _measuredLinearVelocitiesIndex) { + if ((i + 1) % AvatarActionHold::velocitySmoothFrames == _measuredLinearVelocitiesIndex || + (i + 2) % AvatarActionHold::velocitySmoothFrames == _measuredLinearVelocitiesIndex || + (i + 3) % AvatarActionHold::velocitySmoothFrames == _measuredLinearVelocitiesIndex) { continue; } measuredLinearVelocity += _measuredLinearVelocities[i]; From 98f76924c5867307e14ac0329ef626a7542c56b2 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 18 Jul 2016 14:49:56 -0700 Subject: [PATCH 1205/1237] log hearbeat like we do updates --- domain-server/src/DomainServer.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index bcfdd42c34..88f4b94883 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1121,7 +1121,12 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())), + QString path = DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())); +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata sent to" << path; + qDebug() << "Domain metadata update:" << domainUpdateJSON; +#endif + DependencyManager::get()->sendRequest(path, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, JSONCallbackParameters(nullptr, QString(), this, "handleMetaverseHeartbeatError"), From df615b1503ab554682cf6f383461b0008745d90b Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 18 Jul 2016 16:02:02 -0700 Subject: [PATCH 1206/1237] NotFinished never should have had its own bit Since there is a Finished flag too. So now, it is just 0, used as a starting point, and we check for !hasState(Finished). --- libraries/audio/src/AudioInjector.h | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 9bdfcacb5c..b872800e15 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -34,11 +34,11 @@ class AudioInjectorManager; enum class AudioInjectorState : uint8_t { - NotFinished = 1, - Finished = 2, - PendingDelete = 4, - LocalInjectionFinished = 8, - NetworkInjectionFinished = 16 + NotFinished = 0, + Finished = 1, + PendingDelete = 2, + LocalInjectionFinished = 4, + NetworkInjectionFinished = 8 }; AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs); @@ -50,12 +50,6 @@ class AudioInjector : public QObject { Q_OBJECT public: - static const uint8_t NotFinished = 1; - static const uint8_t Finished = 2; - static const uint8_t PendingDelete = 4; - static const uint8_t LocalInjectionFinished = 8; - static const uint8_t NetworkInjectionFinished = 16; - AudioInjector(QObject* parent); AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); @@ -90,7 +84,7 @@ public slots: void setOptions(const AudioInjectorOptions& options); float getLoudness() const { return _loudness; } - bool isPlaying() const { return stateHas(AudioInjectorState::NotFinished); } + bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } void finish(); void finishLocalInjection(); void finishNetworkInjection(); From 2a89fa25bbc64f69bd5717e84af9bee3e5120962 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 18 Jul 2016 16:22:12 -0700 Subject: [PATCH 1207/1237] Removing comments Since I looked into it, seems ok --- libraries/audio/src/AudioInjector.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 9c49ce66d8..58122fee3c 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -165,14 +165,10 @@ void AudioInjector::restart() { if (!_options.localOnly) { if (!injectorManager->restartFinishedInjector(this)) { - // TODO: this logic seems to remove the pending delete, - // which makes me wonder about the deleteLater calls _state = AudioInjectorState::Finished; // we're not playing, so reset the state used by isPlaying. } } } else { - // TODO: this logic seems to remove the pending delete, - // which makes me wonder about the deleteLater calls _state = AudioInjectorState::Finished; // we failed to play, so we are finished again } } From 11b461a73033867d00763d4c60f7532adae0aba4 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 19 Jul 2016 01:46:29 +0200 Subject: [PATCH 1208/1237] Fixes duplicate target overlays for teleporter, make sure to delete overlay before creating one --- scripts/system/controllers/teleport.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index da0b4cb576..854e23315e 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -88,6 +88,7 @@ function Teleporter() { this.createTargetOverlay = function() { + _this.deleteTargetOverlay(); var targetOverlayProps = { url: TARGET_MODEL_URL, dimensions: TARGET_MODEL_DIMENSIONS, @@ -191,6 +192,9 @@ function Teleporter() { }; this.deleteTargetOverlay = function() { + if (this.targetOverlay === null) { + return; + } Overlays.deleteOverlay(this.targetOverlay); this.intersection = null; this.targetOverlay = null; From 17e64cf5e6e4cc46660590f2e50d5e09273fcf20 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 18 Jul 2016 16:58:29 -0700 Subject: [PATCH 1209/1237] suggest only unrestricted domains, and also use server to sort --- interface/resources/qml/AddressBarDialog.qml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index f08ef5c608..b806bbbca2 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -245,8 +245,10 @@ Window { if (!options.page) { options.page = 1; } - // FIXME: really want places I'm allowed in, not just open ones - var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&active&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; + // FIXME: really want places I'm allowed in, not just open ones. + // FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. + // FIXME: add maturity + var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&active&restriction=open&sort_by=users&sort_order=desc&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; getRequest(url, function (error, json) { if (!error && (json.status !== 'success')) { error = new Error("Bad response: " + JSON.stringify(json)); @@ -304,7 +306,6 @@ Window { } var here = AddressManager.hostname; // don't show where we are now. allDomains = domains.filter(function (domain) { return domain.name !== here; }); - allDomains.sort(function (a, b) { return b.online_users - a.online_users; }); // Whittle down suggestions to those that have at least one user, and try to get pictures. suggestionChoices = allDomains.filter(function (domain) { return domain.online_users; }); asyncEach(domains, addPictureToDomain, function (error) { From 7fd40a5d10747425a3c1ca03f82862fb06f91c05 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 18 Jul 2016 17:08:17 -0700 Subject: [PATCH 1210/1237] MacOS: fix crash when touchscreen device is nullptr. --- interface/src/Application.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fb48472b14..eb0fbc4e13 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2746,7 +2746,7 @@ void Application::touchUpdateEvent(QTouchEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchUpdateEvent(event); } - if (_touchscreenDevice->isActive()) { + if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchUpdateEvent(event); } } @@ -2767,7 +2767,7 @@ void Application::touchBeginEvent(QTouchEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchBeginEvent(event); } - if (_touchscreenDevice->isActive()) { + if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchBeginEvent(event); } @@ -2787,7 +2787,7 @@ void Application::touchEndEvent(QTouchEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchEndEvent(event); } - if (_touchscreenDevice->isActive()) { + if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchEndEvent(event); } @@ -2795,7 +2795,7 @@ void Application::touchEndEvent(QTouchEvent* event) { } void Application::touchGestureEvent(QGestureEvent* event) { - if (_touchscreenDevice->isActive()) { + if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchGestureEvent(event); } } From ad7fa971aa601ad1427694160d48409d9a07c095 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 18 Jul 2016 17:09:10 -0700 Subject: [PATCH 1211/1237] copy scripts into build directory --- interface/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index cf5a2b60ad..414fafe705 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -58,6 +58,7 @@ set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") # qt5_create_translation_custom(${QM} ${INTERFACE_SRCS} ${QT_UI_FILES} ${TS}) if (APPLE) + # configure CMake to use a custom Info.plist set_target_properties(${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in) @@ -229,6 +230,13 @@ if (APPLE) set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_APP_PATH}/Contents/Resources") + # copy script files beside the executable + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${CMAKE_SOURCE_DIR}/scripts" + $/../Resources/scripts + ) + # call the fixup_interface macro to add required bundling commands for installation fixup_interface() @@ -263,6 +271,7 @@ else (APPLE) endif (APPLE) if (SCRIPTS_INSTALL_DIR) + # setup install of scripts beside interface executable install( DIRECTORY "${CMAKE_SOURCE_DIR}/scripts/" From 34f2a96888552aebf7e0143adaeab629b277964c Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 18 Jul 2016 18:11:51 -0700 Subject: [PATCH 1212/1237] Adding the check for Obscurrance --- libraries/render-utils/src/LightAmbient.slh | 4 ++++ libraries/render-utils/src/LightingModel.cpp | 11 ++++++++++- libraries/render-utils/src/LightingModel.h | 13 +++++++++++-- libraries/render-utils/src/LightingModel.slh | 8 ++++++-- libraries/render-utils/src/SurfaceGeometryPass.cpp | 4 ++-- libraries/render-utils/src/SurfaceGeometryPass.h | 2 +- libraries/render/src/render/BlurTask.cpp | 2 ++ libraries/render/src/render/BlurTask.h | 1 - .../developer/utilities/render/deferredLighting.qml | 1 + .../utilities/render/surfaceGeometryPass.qml | 9 ++++++++- 10 files changed, 45 insertions(+), 10 deletions(-) diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index d40bd921af..ec665e6deb 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -98,6 +98,10 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, Light light, vec3 } <@endif@> + if (!(isObscuranceEnabled() > 0.0)) { + obscurance = 1.0; + } + float lightEnergy = obscurance * getLightAmbientIntensity(light); if (isAlbedoEnabled() > 0.0) { diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 7f05daac51..5a251fc5e9 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -49,7 +49,14 @@ void LightingModel::setBackground(bool enable) { bool LightingModel::isBackgroundEnabled() const { return (bool)_parametersBuffer.get().enableBackground; } - +void LightingModel::setObscurance(bool enable) { + if (enable != isObscuranceEnabled()) { + _parametersBuffer.edit().enableObscurance = (float)enable; + } +} +bool LightingModel::isObscuranceEnabled() const { + return (bool)_parametersBuffer.get().enableObscurance; +} void LightingModel::setScattering(bool enable) { if (enable != isScatteringEnabled()) { @@ -136,6 +143,8 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setLightmap(config.enableLightmap); _lightingModel->setBackground(config.enableBackground); + _lightingModel->setObscurance(config.enableObscurance); + _lightingModel->setScattering(config.enableScattering); _lightingModel->setDiffuse(config.enableDiffuse); _lightingModel->setSpecular(config.enableSpecular); diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 82e674db7b..87a6703dc6 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -36,6 +36,9 @@ public: void setBackground(bool enable); bool isBackgroundEnabled() const; + void setObscurance(bool enable); + bool isObscuranceEnabled() const; + void setScattering(bool enable); bool isScatteringEnabled() const; void setDiffuse(bool enable); @@ -46,6 +49,7 @@ public: void setAlbedo(bool enable); bool isAlbedoEnabled() const; + void setAmbientLight(bool enable); bool isAmbientLightEnabled() const; void setDirectionalLight(bool enable); @@ -77,13 +81,16 @@ protected: float enableSpecular{ 1.0f }; float enableAlbedo{ 1.0f }; + float enableAmbientLight{ 1.0f }; float enableDirectionalLight{ 1.0f }; float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; float showLightContour{ 0.0f }; // false by default - glm::vec3 spares{ 0.0f }; + float enableObscurance{ 1.0f }; + + glm::vec2 spares{ 0.0f }; Parameters() {} }; @@ -103,6 +110,8 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enableLightmap MEMBER enableLightmap NOTIFY dirty) Q_PROPERTY(bool enableBackground MEMBER enableBackground NOTIFY dirty) + Q_PROPERTY(bool enableObscurance MEMBER enableObscurance NOTIFY dirty) + Q_PROPERTY(bool enableScattering MEMBER enableScattering NOTIFY dirty) Q_PROPERTY(bool enableDiffuse MEMBER enableDiffuse NOTIFY dirty) Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty) @@ -122,7 +131,7 @@ public: bool enableEmissive{ true }; bool enableLightmap{ true }; bool enableBackground{ true }; - + bool enableObscurance{ true }; bool enableScattering{ true }; bool enableDiffuse{ true }; diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index b246516df9..c0722945bc 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -17,7 +17,7 @@ struct LightingModel { vec4 _UnlitEmissiveLightmapBackground; vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; - vec4 _ShowContour; + vec4 _ShowContourObscuranceSpare2; }; uniform lightingModelBuffer { @@ -36,6 +36,9 @@ float isLightmapEnabled() { float isBackgroundEnabled() { return lightingModel._UnlitEmissiveLightmapBackground.w; } +float isObscuranceEnabled() { + return lightingModel._ShowContourObscuranceSpare2.y; +} float isScatteringEnabled() { return lightingModel._ScatteringDiffuseSpecularAlbedo.x; @@ -64,9 +67,10 @@ float isSpotEnabled() { } float isShowLightContour() { - return lightingModel._ShowContour.x; + return lightingModel._ShowContourObscuranceSpare2.x; } + <@endfunc@> <$declareLightingModel()$> diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index a30d8524da..f60fd3f3f8 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -326,8 +326,8 @@ SurfaceGeometryPass::SurfaceGeometryPass() { void SurfaceGeometryPass::configure(const Config& config) { - if (config.depthThreshold != getCurvatureDepthThreshold()) { - _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold; + if ((config.depthThreshold * 100.0f) != getCurvatureDepthThreshold()) { + _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold * 100.0f; } if (config.basisScale != getCurvatureBasisScale()) { diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 14c8316d9e..3f42a7ab1f 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -146,7 +146,7 @@ class SurfaceGeometryPassConfig : public render::Job::Config { public: SurfaceGeometryPassConfig() : render::Job::Config(true) {} - float depthThreshold{ 0.005f }; // meters + float depthThreshold{ 5.0f }; // centimeters float basisScale{ 1.0f }; float curvatureScale{ 10.0f }; int resolutionLevel{ 0 }; diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 597df37b7a..862c08bcbb 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -374,3 +374,5 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const batch.setUniformBuffer(BlurTask_ParamsSlot, nullptr); }); } + + diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index c6e4e76ca5..7d8fb62fbc 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -156,7 +156,6 @@ protected: BlurInOutResource _inOutResources; }; - } #endif // hifi_render_BlurTask_h diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index fedb99fc74..635d8b1471 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -38,6 +38,7 @@ Column { spacing: 10 Repeater { model: [ + "Obscurance:LightingModel:enableObscurance", "Scattering:LightingModel:enableScattering", "Diffuse:LightingModel:enableDiffuse", "Specular:LightingModel:enableSpecular", diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index 23a846ee79..ad5a9dd03d 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -18,9 +18,16 @@ Column { spacing: 10 Column{ + ConfigSlider { + label: qsTr("Depth Threshold [cm]") + integral: false + config: Render.getConfig("SurfaceGeometry") + property: "depthThreshold" + max: 5.0 + min: 0.0 + } Repeater { model: [ - "Depth Threshold:depthThreshold:0.05:false", "Basis Scale:basisScale:2.0:false", "Curvature Scale:curvatureScale:100.0:false", "Downscale:resolutionLevel:4:true" From 57955a2b5612d287ad33dac1b96cf33fd1b33fec Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 18 Jul 2016 18:12:27 -0700 Subject: [PATCH 1213/1237] Expose protocol version to qml, and use it in query. --- interface/resources/qml/AddressBarDialog.qml | 19 +++++++++++++++---- libraries/networking/src/AddressManager.cpp | 5 +++++ libraries/networking/src/AddressManager.h | 1 + 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index b806bbbca2..0c9e5c9dbc 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -51,6 +51,7 @@ Window { } property var allDomains: []; property var suggestionChoices: []; + property var domainsBaseUrl: null; property int cardWidth: 200; property int cardHeight: 152; @@ -245,10 +246,20 @@ Window { if (!options.page) { options.page = 1; } - // FIXME: really want places I'm allowed in, not just open ones. - // FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. - // FIXME: add maturity - var url = "https://metaverse.highfidelity.com/api/v1/domains/all?open&active&restriction=open&sort_by=users&sort_order=desc&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; + if (!domainsBaseUrl) { + var domainsOptions = [ + 'open', // published hours handle now + 'active', // has at least one person connected. FIXME: really want any place that is verified accessible. + // FIXME: really want places I'm allowed in, not just open ones. + 'restriction=open', // Not by whitelist, etc. FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. + // FIXME add maturity + 'protocol=' + AddressManager.protocolVersion(), + 'sort_by=users', + 'sort_order=desc', + ]; + domainsBaseUrl = "https://metaverse.highfidelity.com/api/v1/domains/all?" + domainsOptions.join('&'); + } + var url = domainsBaseUrl + "&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; getRequest(url, function (error, json) { if (!error && (json.status !== 'success')) { error = new Error("Bad response: " + JSON.stringify(json)); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index b3a022cc3a..ae6aad3c4f 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -24,6 +24,7 @@ #include "NodeList.h" #include "NetworkLogging.h" #include "UserActivityLogger.h" +#include "udt/PacketHeaders.h" const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; @@ -37,6 +38,10 @@ AddressManager::AddressManager() : } +QString AddressManager::protocolVersion() { + return protocolVersionsSignatureBase64(); +} + bool AddressManager::isConnected() { return DependencyManager::get()->getDomainHandler().isConnected(); } diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index c013da3a72..2e9f177137 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -39,6 +39,7 @@ class AddressManager : public QObject, public Dependency { Q_PROPERTY(QString hostname READ getHost) Q_PROPERTY(QString pathname READ currentPath) public: + Q_INVOKABLE QString protocolVersion(); using PositionGetter = std::function; using OrientationGetter = std::function; From a8dd06ad38914045f282a63bcf1be1b5552f3ae9 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 18 Jul 2016 19:24:43 -0700 Subject: [PATCH 1214/1237] Better groupint of the diffuseion of the curvature for easier fix for mini mirror --- .../render-utils/src/RenderDeferredTask.cpp | 9 +++++--- .../render-utils/src/SurfaceGeometryPass.cpp | 22 +++++++++++++++---- .../render-utils/src/SurfaceGeometryPass.h | 5 ++++- libraries/render/src/render/BlurTask.cpp | 6 ++--- libraries/render/src/render/BlurTask.h | 4 +++- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 32c6cab40d..aa226e60bc 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -134,18 +134,21 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto surfaceGeometryPassOutputs = addJob("SurfaceGeometry", surfaceGeometryPassInputs); const auto surfaceGeometryFramebuffer = surfaceGeometryPassOutputs.getN(0); const auto curvatureFramebuffer = surfaceGeometryPassOutputs.getN(1); + const auto midCurvatureNormalFramebuffer = surfaceGeometryPassOutputs.getN(2); + const auto lowCurvatureNormalFramebuffer = surfaceGeometryPassOutputs.getN(3); - const auto curvatureRangeTimer = addJob("BeginCurvatureRangeTimer"); + const auto scatteringResource = addJob("Scattering"); + +/* const auto curvatureRangeTimer = addJob("BeginCurvatureRangeTimer"); // TODO: Push this 2 diffusion stages into surfaceGeometryPass as they are working together const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, halfLinearDepthTexture).hasVarying(); const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", diffuseCurvaturePassInputs); const auto lowCurvatureNormalFramebuffer = addJob("DiffuseCurvatureLow", diffuseCurvaturePassInputs, true); // THis blur pass generates it s render resource - const auto scatteringResource = addJob("Scattering"); addJob("CurvatureRangeTimer", curvatureRangeTimer); - +*/ // AO job addJob("AmbientOcclusion"); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index f60fd3f3f8..3e1930f6ba 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -319,7 +319,10 @@ void SurfaceGeometryFramebuffer::setResolutionLevel(int resolutionLevel) { } } -SurfaceGeometryPass::SurfaceGeometryPass() { +SurfaceGeometryPass::SurfaceGeometryPass() : + _firstBlurPass(false), + _secondBlurPass(true, _firstBlurPass.getParameters()) +{ Parameters parameters; _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); } @@ -365,7 +368,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c // auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); auto normalTexture = linearDepthFramebuffer->getHalfNormalTexture(); - auto curvatureFBO = _surfaceGeometryFramebuffer->getCurvatureFramebuffer(); + auto curvatureFramebuffer = _surfaceGeometryFramebuffer->getCurvatureFramebuffer(); #ifdef USE_STENCIL_TEST if (curvatureFBO->getDepthStencilBuffer() != deferredFramebuffer->getPrimaryDepthTexture()) { curvatureFBO->setDepthStencilBuffer(deferredFramebuffer->getPrimaryDepthTexture(), deferredFramebuffer->getPrimaryDepthTexture()->getTexelFormat()); @@ -374,7 +377,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); outputs.edit0() = _surfaceGeometryFramebuffer; - outputs.edit1() = curvatureFBO; + outputs.edit1() = curvatureFramebuffer; auto curvaturePipeline = getCurvaturePipeline(); @@ -396,7 +399,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); // Curvature pass - batch.setFramebuffer(curvatureFBO); + batch.setFramebuffer(curvatureFramebuffer); // We can avoid the clear by drawing the same clear vallue from the makeCurvature shader. same performances or no worse @@ -416,6 +419,17 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c _gpuTimer.end(batch); }); + const auto diffuseCurvaturePassInputs = render::BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture); + gpu::FramebufferPointer midBlurredFramebuffer; + _firstBlurPass.run(sceneContext, renderContext, diffuseCurvaturePassInputs, midBlurredFramebuffer); + + gpu::FramebufferPointer lowBlurredFramebuffer; + _secondBlurPass.run(sceneContext, renderContext, diffuseCurvaturePassInputs, lowBlurredFramebuffer); + + + outputs.edit2() = midBlurredFramebuffer; + outputs.edit3() = lowBlurredFramebuffer; + auto config = std::static_pointer_cast(renderContext->jobConfig); config->gpuTime = _gpuTimer.getAverage(); } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 3f42a7ab1f..d0f79dad76 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -15,6 +15,7 @@ #include #include "render/DrawTask.h" +#include "render/BlurTask.h" #include "DeferredFrameTransform.h" #include "DeferredFramebuffer.h" @@ -162,7 +163,7 @@ signals: class SurfaceGeometryPass { public: using Inputs = render::VaryingSet3; - using Outputs = render::VaryingSet2; + using Outputs = render::VaryingSet4; using Config = SurfaceGeometryPassConfig; using JobModel = render::Job::ModelIO; @@ -196,6 +197,8 @@ private: gpu::PipelinePointer _curvaturePipeline; + render::BlurGaussianDepthAware _firstBlurPass; + render::BlurGaussianDepthAware _secondBlurPass; gpu::RangeTimer _gpuTimer; }; diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 862c08bcbb..ddaaa82e31 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -258,10 +258,10 @@ void BlurGaussian::run(const SceneContextPointer& sceneContext, const RenderCont -BlurGaussianDepthAware::BlurGaussianDepthAware(bool generateOutputFramebuffer) : - _inOutResources(generateOutputFramebuffer) +BlurGaussianDepthAware::BlurGaussianDepthAware(bool generateOutputFramebuffer, const BlurParamsPointer& params) : + _inOutResources(generateOutputFramebuffer), + _parameters((params ? params : std::make_shared())) { - _parameters = std::make_shared(); } gpu::PipelinePointer BlurGaussianDepthAware::getBlurVPipeline() { diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index 7d8fb62fbc..b54e494a80 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -138,11 +138,13 @@ public: using Config = BlurGaussianDepthAwareConfig; using JobModel = Job::ModelIO; - BlurGaussianDepthAware(bool generateNewOutput = false); + BlurGaussianDepthAware(bool generateNewOutput = false, const BlurParamsPointer& params = BlurParamsPointer()); void configure(const Config& config); void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& SourceAndDepth, gpu::FramebufferPointer& blurredFramebuffer); + const BlurParamsPointer& getParameters() const { return _parameters; } + protected: BlurParamsPointer _parameters; From cbbb2a7975b7276170f5fd85c7473fdc9d8eedf5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 18 Jul 2016 19:04:17 -0700 Subject: [PATCH 1215/1237] Properly delete GL context and offscreen surfaces --- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 8 ++++++++ libraries/gl/src/gl/OffscreenGLCanvas.h | 4 ++-- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 15 ++++++++------- libraries/gl/src/gl/QOpenGLContextWrapper.h | 2 ++ 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index eec3d2bf6b..a6b5a03ff6 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -36,7 +36,15 @@ OffscreenGLCanvas::~OffscreenGLCanvas() { delete _logger; _logger = nullptr; } + _context->doneCurrent(); + delete _context; + _context = nullptr; + + _offscreenSurface->destroy(); + delete _offscreenSurface; + _offscreenSurface = nullptr; + } bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h index 69210f638d..8def09796d 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.h +++ b/libraries/gl/src/gl/OffscreenGLCanvas.h @@ -34,8 +34,8 @@ public: protected: std::once_flag _reportOnce; - QOpenGLContext* _context; - QOffscreenSurface* _offscreenSurface; + QOpenGLContext* _context{ nullptr }; + QOffscreenSurface* _offscreenSurface{ nullptr }; QOpenGLDebugLogger* _logger{ nullptr }; }; diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index bc8afc3927..0b153a5ae8 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -28,16 +28,17 @@ QOpenGLContext* QOpenGLContextWrapper::currentContext() { return QOpenGLContext::currentContext(); } - QOpenGLContextWrapper::QOpenGLContextWrapper() : -_context(new QOpenGLContext) -{ -} - + _ownContext(true), _context(new QOpenGLContext) { } QOpenGLContextWrapper::QOpenGLContextWrapper(QOpenGLContext* context) : - _context(context) -{ + _context(context) { } + +QOpenGLContextWrapper::~QOpenGLContextWrapper() { + if (_ownContext) { + delete _context; + _context = nullptr; + } } void QOpenGLContextWrapper::setFormat(const QSurfaceFormat& format) { diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index d097284e68..b09ad1a4c3 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -23,6 +23,7 @@ class QOpenGLContextWrapper { public: QOpenGLContextWrapper(); QOpenGLContextWrapper(QOpenGLContext* context); + virtual ~QOpenGLContextWrapper(); void setFormat(const QSurfaceFormat& format); bool create(); void swapBuffers(QSurface* surface); @@ -40,6 +41,7 @@ public: private: + bool _ownContext { false }; QOpenGLContext* _context { nullptr }; }; From 20824f038c67b63aedf38481c359a9bc668b7822 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 18 Jul 2016 21:59:44 -0700 Subject: [PATCH 1216/1237] include codec in audio stream packets so that each side can discard packets that don't match --- assignment-client/src/audio/AudioMixer.cpp | 17 ++++++++++--- .../src/audio/AudioMixerClientData.cpp | 2 +- .../src/audio/AudioMixerClientData.h | 3 +++ libraries/audio-client/src/AudioClient.cpp | 5 ++-- .../audio/src/AbstractAudioInterface.cpp | 18 ++++++++++--- libraries/audio/src/AbstractAudioInterface.h | 3 ++- libraries/audio/src/AudioInjector.cpp | 12 +++++++++ libraries/audio/src/InboundAudioStream.cpp | 25 ++++++++++++++++--- libraries/audio/src/InjectedAudioStream.cpp | 1 + libraries/networking/src/udt/BasePacket.cpp | 5 ++-- .../networking/src/udt/PacketHeaders.cpp | 7 ++++++ libraries/networking/src/udt/PacketHeaders.h | 5 ++++ 12 files changed, 84 insertions(+), 19 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 40e22f855a..f4b80f55b4 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -768,28 +768,39 @@ void AudioMixer::broadcastMixes() { std::unique_ptr mixPacket; + const int MAX_CODEC_NAME = 30; // way over estimate + if (mixHasAudio) { - int mixPacketBytes = sizeof(quint16) + AudioConstants::NETWORK_FRAME_BYTES_STEREO; + int mixPacketBytes = sizeof(quint16) + MAX_CODEC_NAME+ AudioConstants::NETWORK_FRAME_BYTES_STEREO; mixPacket = NLPacket::create(PacketType::MixedAudio, mixPacketBytes); // pack sequence number quint16 sequence = nodeData->getOutgoingSequenceNumber(); mixPacket->writePrimitive(sequence); + // write the codec + QString codecInPacket = nodeData->getCodecName(); + mixPacket->writeString(codecInPacket); + QByteArray decodedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); QByteArray encodedBuffer; nodeData->encode(decodedBuffer, encodedBuffer); // pack mixed audio samples mixPacket->write(encodedBuffer.constData(), encodedBuffer.size()); - } else { - int silentPacketBytes = sizeof(quint16) + sizeof(quint16); + } + else { + int silentPacketBytes = sizeof(quint16) + sizeof(quint16) + MAX_CODEC_NAME; mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes); // pack sequence number quint16 sequence = nodeData->getOutgoingSequenceNumber(); mixPacket->writePrimitive(sequence); + // write the codec + QString codecInPacket = nodeData->getCodecName(); + mixPacket->writeString(codecInPacket); + // pack number of silent audio samples quint16 numSilentSamples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; mixPacket->writePrimitive(numSilentSamples); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 5c2ce8bf57..dc481e1c59 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -128,7 +128,6 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { isMicStream = true; } else if (packetType == PacketType::InjectAudio) { // this is injected audio - // grab the stream identifier for this injected audio message.seek(sizeof(quint16)); QUuid streamIdentifier = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID)); @@ -167,6 +166,7 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { // check the overflow count before we parse data auto overflowBefore = matchingStream->getOverflowCount(); + auto parseResult = matchingStream->parseData(message); if (matchingStream->getOverflowCount() > overflowBefore) { diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index da2bf8997c..85bf3fa3a1 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -78,6 +78,9 @@ public: } } + QString getCodecName() { return _selectedCodecName; } + + signals: void injectorStreamFinished(const QUuid& streamIdentifier); diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 7cf8574529..ac42de903d 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -834,7 +834,7 @@ void AudioClient::handleAudioInput() { encodedBuffer = decocedBuffer; } - emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, packetType); + emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, packetType, _selectedCodecName); _stats.sentPacket(); } } @@ -852,7 +852,7 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { } // FIXME check a flag to see if we should echo audio? - emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho); + emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho, _selectedCodecName); } void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { @@ -1015,7 +1015,6 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { // no reason to lock access to the vector of injectors. if (!_activeLocalAudioInjectors.contains(injector)) { qDebug() << "adding new injector"; - _activeLocalAudioInjectors.append(injector); } else { qDebug() << "injector exists in active list already"; diff --git a/libraries/audio/src/AbstractAudioInterface.cpp b/libraries/audio/src/AbstractAudioInterface.cpp index b347d57450..aa5c85283c 100644 --- a/libraries/audio/src/AbstractAudioInterface.cpp +++ b/libraries/audio/src/AbstractAudioInterface.cpp @@ -19,7 +19,8 @@ #include "AudioConstants.h" -void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, const Transform& transform, PacketType packetType) { +void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, + const Transform& transform, PacketType packetType, QString codecName) { static std::mutex _mutex; using Locker = std::unique_lock; auto nodeList = DependencyManager::get(); @@ -27,10 +28,19 @@ void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes if (audioMixer && audioMixer->getActiveSocket()) { Locker lock(_mutex); auto audioPacket = NLPacket::create(packetType); + + // FIXME - this is not a good way to determine stereoness with codecs.... quint8 isStereo = bytes == AudioConstants::NETWORK_FRAME_BYTES_STEREO ? 1 : 0; // write sequence number - audioPacket->writePrimitive(sequenceNumber++); + auto sequence = sequenceNumber++; + audioPacket->writePrimitive(sequence); + + // write the codec - don't include this for injected audio + if (packetType != PacketType::InjectAudio) { + auto stringSize = audioPacket->writeString(codecName); + } + if (packetType == PacketType::SilentAudioFrame) { // pack num silent samples quint16 numSilentSamples = isStereo ? @@ -49,8 +59,8 @@ void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes if (audioPacket->getType() != PacketType::SilentAudioFrame) { // audio samples have already been packed (written to networkAudioSamples) - audioPacket->setPayloadSize(audioPacket->getPayloadSize() + bytes); - static const int leadingBytes = sizeof(quint16) + sizeof(glm::vec3) + sizeof(glm::quat) + sizeof(quint8); + int leadingBytes = audioPacket->getPayloadSize(); + audioPacket->setPayloadSize(leadingBytes + bytes); memcpy(audioPacket->getPayload() + leadingBytes, audioData, bytes); } nodeList->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendAudioPacket); diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index ee52622d7e..223421a7ab 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -28,7 +28,8 @@ class AbstractAudioInterface : public QObject { public: AbstractAudioInterface(QObject* parent = 0) : QObject(parent) {}; - static void emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, const Transform& transform, PacketType packetType); + static void emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, const Transform& transform, + PacketType packetType, QString codecName = QString("")); public slots: virtual bool outputLocalInjector(bool isStereo, AudioInjector* injector) = 0; diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 9c49ce66d8..5064686565 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -218,6 +218,14 @@ const uchar MAX_INJECTOR_VOLUME = 0xFF; static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1; static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0; +qint64 writeStringToStream(const QString& string, QDataStream& stream) { + QByteArray data = string.toUtf8(); + uint32_t length = data.length(); + stream << static_cast(length); + stream << data; + return length + sizeof(uint32_t); +} + int64_t AudioInjector::injectNextFrame() { if (stateHas(AudioInjectorState::NetworkInjectionFinished)) { qDebug() << "AudioInjector::injectNextFrame called but AudioInjector has finished and was not restarted. Returning."; @@ -264,6 +272,10 @@ int64_t AudioInjector::injectNextFrame() { // pack some placeholder sequence number for now audioPacketStream << (quint16) 0; + // pack some placeholder sequence number for now + //QString noCodecForInjectors(""); + //writeStringToStream(noCodecForInjectors, audioPacketStream); + // pack stream identifier (a generated UUID) audioPacketStream << QUuid::createUuid(); diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index c9b9363b1b..0a207acba0 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -58,6 +58,7 @@ void InboundAudioStream::reset() { _isStarved = true; _hasStarted = false; resetStats(); + //cleanupCodec(); // FIXME??? } void InboundAudioStream::resetStats() { @@ -99,12 +100,17 @@ void InboundAudioStream::perSecondCallbackForUpdatingStats() { } int InboundAudioStream::parseData(ReceivedMessage& message) { - + PacketType packetType = message.getType(); + // parse sequence number and track it quint16 sequence; message.readPrimitive(&sequence); SequenceNumberStats::ArrivalInfo arrivalInfo = _incomingSequenceNumberStats.sequenceNumberReceived(sequence, message.getSourceID()); + QString codecInPacket(""); + if (packetType != PacketType::InjectAudio) { + codecInPacket = message.readString(); + } packetReceivedUpdateTimingStats(); @@ -112,9 +118,10 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { // parse the info after the seq number and before the audio data (the stream properties) int prePropertyPosition = message.getPosition(); - int propertyBytes = parseStreamProperties(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead()), networkSamples); + auto afterHeader = message.readWithoutCopy(message.getBytesLeftToRead()); + int propertyBytes = parseStreamProperties(message.getType(), afterHeader, networkSamples); message.seek(prePropertyPosition + propertyBytes); - + // handle this packet based on its arrival status. switch (arrivalInfo._status) { case SequenceNumberStats::Early: { @@ -129,9 +136,19 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { case SequenceNumberStats::OnTime: { // Packet is on time; parse its data to the ringbuffer if (message.getType() == PacketType::SilentAudioFrame) { + // FIXME - do some codecs need to know about these silen frames? writeDroppableSilentSamples(networkSamples); } else { - parseAudioData(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead())); + // note: PCM and no codec are identical + bool selectedPCM = _selectedCodecName == "pcm" || _selectedCodecName == ""; + bool packetPCM = codecInPacket == "pcm" || codecInPacket == ""; + if (codecInPacket == _selectedCodecName || (packetPCM && selectedPCM)) { + auto afterProperties = message.readWithoutCopy(message.getBytesLeftToRead()); + parseAudioData(message.getType(), afterProperties); + } else { + qDebug() << __FUNCTION__ << "codec mismatch: expected" << _selectedCodecName << "got" << codecInPacket << "writing silence"; + writeDroppableSilentSamples(networkSamples); + } } break; } diff --git a/libraries/audio/src/InjectedAudioStream.cpp b/libraries/audio/src/InjectedAudioStream.cpp index 54e0f92bea..ccd581959f 100644 --- a/libraries/audio/src/InjectedAudioStream.cpp +++ b/libraries/audio/src/InjectedAudioStream.cpp @@ -33,6 +33,7 @@ const uchar MAX_INJECTOR_VOLUME = 255; int InjectedAudioStream::parseStreamProperties(PacketType type, const QByteArray& packetAfterSeqNum, int& numAudioSamples) { + // setup a data stream to read from this packet QDataStream packetStream(packetAfterSeqNum); diff --git a/libraries/networking/src/udt/BasePacket.cpp b/libraries/networking/src/udt/BasePacket.cpp index 18552ca966..c6501943c7 100644 --- a/libraries/networking/src/udt/BasePacket.cpp +++ b/libraries/networking/src/udt/BasePacket.cpp @@ -154,8 +154,8 @@ qint64 BasePacket::writeString(const QString& string) { QByteArray data = string.toUtf8(); uint32_t length = data.length(); writePrimitive(length); - writeData(data.constData(), data.length()); - seek(pos() + length); + write(data.constData(), data.length()); + //seek(pos() + length); return length + sizeof(uint32_t); } @@ -176,7 +176,6 @@ bool BasePacket::reset() { } qint64 BasePacket::writeData(const char* data, qint64 maxSize) { - Q_ASSERT_X(maxSize <= bytesAvailableForWrite(), "BasePacket::writeData", "not enough space for write"); // make sure we have the space required to write this block diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index fca006ae87..eb8739dd49 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -72,6 +72,13 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::DomainServerAddedNode: return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); + case PacketType::MixedAudio: + case PacketType::SilentAudioFrame: + case PacketType::InjectAudio: + case PacketType::MicrophoneAudioNoEcho: + case PacketType::MicrophoneAudioWithEcho: + return static_cast(AudioVersion::CodecNameInAudioPackets); + default: return 17; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 85030135a1..e723ea38eb 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -213,4 +213,9 @@ enum class DomainListVersion : PacketVersion { PermissionsGrid }; +enum class AudioVersion : PacketVersion { + HasCompressedAudio = 17, + CodecNameInAudioPackets +}; + #endif // hifi_PacketHeaders_h From 2e63aba8c96b84b76a892a4787f5ea1152784d7f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 18 Jul 2016 22:29:05 -0700 Subject: [PATCH 1217/1237] when getting unexpected codec in the mixer, send a message to the client to select a different codec --- assignment-client/src/audio/AudioMixer.cpp | 10 +--------- assignment-client/src/audio/AudioMixerClientData.cpp | 12 ++++++++++++ assignment-client/src/audio/AudioMixerClientData.h | 3 +++ libraries/audio/src/InboundAudioStream.cpp | 6 ++++++ libraries/audio/src/InboundAudioStream.h | 4 ++++ 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index f4b80f55b4..489f9afa2d 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -525,7 +525,6 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess } } - auto clientData = dynamic_cast(sendingNode->getLinkedData()); // FIXME - why would we not have client data at this point?? @@ -539,14 +538,7 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess clientData->setupCodec(selectedCodec, selectedCodecName); qDebug() << "selectedCodecName:" << selectedCodecName; - - auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); - - // write them to our packet - replyPacket->writeString(selectedCodecName); - - auto nodeList = DependencyManager::get(); - nodeList->sendPacket(std::move(replyPacket), *sendingNode); + clientData->sendSelectAudioFormat(sendingNode, selectedCodecName); } void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index dc481e1c59..f055fded33 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -113,6 +113,8 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { avatarAudioStream->setupCodec(_codec, _selectedCodecName, AudioConstants::MONO); qDebug() << "creating new AvatarAudioStream... codec:" << _selectedCodecName; + connect(avatarAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioMixerClientData::sendSelectAudioFormat); + auto emplaced = _audioStreams.emplace( QUuid(), std::unique_ptr { avatarAudioStream } @@ -344,6 +346,16 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { return result; } +void AudioMixerClientData::sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName) { + auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); + + // write them to our packet + replyPacket->writeString(selectedCodecName); + auto nodeList = DependencyManager::get(); + nodeList->sendPacket(std::move(replyPacket), *node); +} + + void AudioMixerClientData::setupCodec(CodecPluginPointer codec, const QString& codecName) { cleanupCodec(); // cleanup any previously allocated coders first _codec = codec; diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 85bf3fa3a1..f4f190bd47 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -84,6 +84,9 @@ public: signals: void injectorStreamFinished(const QUuid& streamIdentifier); +public slots: + void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName); + private: QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 0a207acba0..6b6ce0ad07 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "InboundAudioStream.h" @@ -147,7 +148,12 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { parseAudioData(message.getType(), afterProperties); } else { qDebug() << __FUNCTION__ << "codec mismatch: expected" << _selectedCodecName << "got" << codecInPacket << "writing silence"; + writeDroppableSilentSamples(networkSamples); + + // inform others of the mismatch + auto sendingNode = DependencyManager::get()->nodeWithUUID(message.getSourceID()); + emit mismatchedAudioCodec(sendingNode, _selectedCodecName); } } break; diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index 5da63f96c2..af79ff6164 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -12,6 +12,7 @@ #ifndef hifi_InboundAudioStream_h #define hifi_InboundAudioStream_h +#include #include #include #include @@ -180,6 +181,9 @@ public: void setupCodec(CodecPluginPointer codec, const QString& codecName, int numChannels); void cleanupCodec(); +signals: + void mismatchedAudioCodec(SharedNodePointer sendingNode, const QString& desiredCodec); + public slots: /// This function should be called every second for all the stats to function properly. If dynamic jitter buffers /// is enabled, those stats are used to calculate _desiredJitterBufferFrames. From c6ffd81c4bfcb2022d4eeaf61c02a1b1c5af10d1 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 18 Jul 2016 22:42:38 -0700 Subject: [PATCH 1218/1237] some cleanup --- assignment-client/src/audio/AudioMixer.cpp | 10 ++++------ assignment-client/src/audio/AudioMixerClientData.cpp | 3 --- assignment-client/src/audio/AudioMixerClientData.h | 1 - libraries/audio/src/AudioConstants.h | 2 ++ libraries/audio/src/AudioInjector.cpp | 6 +++--- libraries/audio/src/InboundAudioStream.cpp | 7 ++----- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 489f9afa2d..8f752e70d0 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -760,10 +760,9 @@ void AudioMixer::broadcastMixes() { std::unique_ptr mixPacket; - const int MAX_CODEC_NAME = 30; // way over estimate - if (mixHasAudio) { - int mixPacketBytes = sizeof(quint16) + MAX_CODEC_NAME+ AudioConstants::NETWORK_FRAME_BYTES_STEREO; + int mixPacketBytes = sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE + + AudioConstants::NETWORK_FRAME_BYTES_STEREO; mixPacket = NLPacket::create(PacketType::MixedAudio, mixPacketBytes); // pack sequence number @@ -780,9 +779,8 @@ void AudioMixer::broadcastMixes() { // pack mixed audio samples mixPacket->write(encodedBuffer.constData(), encodedBuffer.size()); - } - else { - int silentPacketBytes = sizeof(quint16) + sizeof(quint16) + MAX_CODEC_NAME; + } else { + int silentPacketBytes = sizeof(quint16) + sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE; mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes); // pack sequence number diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index f055fded33..85491537a2 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -168,7 +168,6 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { // check the overflow count before we parse data auto overflowBefore = matchingStream->getOverflowCount(); - auto parseResult = matchingStream->parseData(message); if (matchingStream->getOverflowCount() > overflowBefore) { @@ -348,8 +347,6 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { void AudioMixerClientData::sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName) { auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat); - - // write them to our packet replyPacket->writeString(selectedCodecName); auto nodeList = DependencyManager::get(); nodeList->sendPacket(std::move(replyPacket), *node); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index f4f190bd47..babfae3539 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -80,7 +80,6 @@ public: QString getCodecName() { return _selectedCodecName; } - signals: void injectorStreamFinished(const QUuid& streamIdentifier); diff --git a/libraries/audio/src/AudioConstants.h b/libraries/audio/src/AudioConstants.h index dbbe434915..9271323498 100644 --- a/libraries/audio/src/AudioConstants.h +++ b/libraries/audio/src/AudioConstants.h @@ -26,6 +26,8 @@ namespace AudioConstants { inline const char* getAudioFrameName() { return "com.highfidelity.recording.Audio"; } + const int MAX_CODEC_NAME_LENGTH = 30; + const int MAX_CODEC_NAME_LENGTH_ON_WIRE = MAX_CODEC_NAME_LENGTH + sizeof(uint32_t); const int NETWORK_FRAME_BYTES_STEREO = 1024; const int NETWORK_FRAME_SAMPLES_STEREO = NETWORK_FRAME_BYTES_STEREO / sizeof(AudioSample); const int NETWORK_FRAME_BYTES_PER_CHANNEL = 512; diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 5064686565..527be70a59 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -272,9 +272,9 @@ int64_t AudioInjector::injectNextFrame() { // pack some placeholder sequence number for now audioPacketStream << (quint16) 0; - // pack some placeholder sequence number for now - //QString noCodecForInjectors(""); - //writeStringToStream(noCodecForInjectors, audioPacketStream); + // current injectors don't use codecs, so pack in the unknown codec name + QString noCodecForInjectors(""); + writeStringToStream(noCodecForInjectors, audioPacketStream); // pack stream identifier (a generated UUID) audioPacketStream << QUuid::createUuid(); diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 6b6ce0ad07..1fb908c1d0 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -59,7 +59,7 @@ void InboundAudioStream::reset() { _isStarved = true; _hasStarted = false; resetStats(); - //cleanupCodec(); // FIXME??? + cleanupCodec(); } void InboundAudioStream::resetStats() { @@ -108,10 +108,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { message.readPrimitive(&sequence); SequenceNumberStats::ArrivalInfo arrivalInfo = _incomingSequenceNumberStats.sequenceNumberReceived(sequence, message.getSourceID()); - QString codecInPacket(""); - if (packetType != PacketType::InjectAudio) { - codecInPacket = message.readString(); - } + QString codecInPacket = message.readString(); packetReceivedUpdateTimingStats(); From c484fec51d3e2137132427a484db41f107db491c Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 18 Jul 2016 22:45:28 -0700 Subject: [PATCH 1219/1237] cleanup --- libraries/audio/src/InboundAudioStream.cpp | 9 +++------ libraries/networking/src/udt/BasePacket.cpp | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 1fb908c1d0..ff177e9a93 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -116,8 +116,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { // parse the info after the seq number and before the audio data (the stream properties) int prePropertyPosition = message.getPosition(); - auto afterHeader = message.readWithoutCopy(message.getBytesLeftToRead()); - int propertyBytes = parseStreamProperties(message.getType(), afterHeader, networkSamples); + int propertyBytes = parseStreamProperties(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead()), networkSamples); message.seek(prePropertyPosition + propertyBytes); // handle this packet based on its arrival status. @@ -134,7 +133,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { case SequenceNumberStats::OnTime: { // Packet is on time; parse its data to the ringbuffer if (message.getType() == PacketType::SilentAudioFrame) { - // FIXME - do some codecs need to know about these silen frames? + // FIXME - Some codecs need to know about these silent frames... and can produce better output writeDroppableSilentSamples(networkSamples); } else { // note: PCM and no codec are identical @@ -144,10 +143,8 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { auto afterProperties = message.readWithoutCopy(message.getBytesLeftToRead()); parseAudioData(message.getType(), afterProperties); } else { - qDebug() << __FUNCTION__ << "codec mismatch: expected" << _selectedCodecName << "got" << codecInPacket << "writing silence"; - + qDebug() << "Codec mismatch: expected" << _selectedCodecName << "got" << codecInPacket << "writing silence"; writeDroppableSilentSamples(networkSamples); - // inform others of the mismatch auto sendingNode = DependencyManager::get()->nodeWithUUID(message.getSourceID()); emit mismatchedAudioCodec(sendingNode, _selectedCodecName); diff --git a/libraries/networking/src/udt/BasePacket.cpp b/libraries/networking/src/udt/BasePacket.cpp index c6501943c7..8a4b98de87 100644 --- a/libraries/networking/src/udt/BasePacket.cpp +++ b/libraries/networking/src/udt/BasePacket.cpp @@ -155,7 +155,6 @@ qint64 BasePacket::writeString(const QString& string) { uint32_t length = data.length(); writePrimitive(length); write(data.constData(), data.length()); - //seek(pos() + length); return length + sizeof(uint32_t); } From ff132fa7128acf44d73136f8e348f13524052d90 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Jul 2016 02:11:22 -0700 Subject: [PATCH 1220/1237] Gathering the diffusion passes in the SUrfaceGEometryPAss job --- interface/src/Application.cpp | 2 + .../render-utils/src/SurfaceGeometryPass.cpp | 145 ++++++++++++++---- .../render-utils/src/SurfaceGeometryPass.h | 24 ++- libraries/render/src/render/BlurTask.cpp | 11 +- libraries/render/src/render/BlurTask.h | 6 +- .../developer/utilities/render/statsGPU.qml | 5 - .../utilities/render/surfaceGeometryPass.qml | 19 +-- 7 files changed, 146 insertions(+), 66 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bc65977beb..a29ac19f97 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -305,6 +305,8 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG + + deadlockDetectionCrash(); #endif } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 3e1930f6ba..95557c99e3 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -282,6 +282,10 @@ void SurfaceGeometryFramebuffer::updateLinearDepth(const gpu::TexturePointer& li void SurfaceGeometryFramebuffer::clear() { _curvatureFramebuffer.reset(); _curvatureTexture.reset(); + _lowCurvatureFramebuffer.reset(); + _lowCurvatureTexture.reset(); + _blurringFramebuffer.reset(); + _blurringTexture.reset(); } gpu::TexturePointer SurfaceGeometryFramebuffer::getLinearDepthTexture() { @@ -293,9 +297,17 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width >> getResolutionLevel(), height >> getResolutionLevel(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); + + _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); + + _blurringTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create()); + _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } gpu::FramebufferPointer SurfaceGeometryFramebuffer::getCurvatureFramebuffer() { @@ -312,6 +324,34 @@ gpu::TexturePointer SurfaceGeometryFramebuffer::getCurvatureTexture() { return _curvatureTexture; } +gpu::FramebufferPointer SurfaceGeometryFramebuffer::getLowCurvatureFramebuffer() { + if (!_lowCurvatureFramebuffer) { + allocate(); + } + return _lowCurvatureFramebuffer; +} + +gpu::TexturePointer SurfaceGeometryFramebuffer::getLowCurvatureTexture() { + if (!_lowCurvatureTexture) { + allocate(); + } + return _lowCurvatureTexture; +} + +gpu::FramebufferPointer SurfaceGeometryFramebuffer::getBlurringFramebuffer() { + if (!_blurringFramebuffer) { + allocate(); + } + return _blurringFramebuffer; +} + +gpu::TexturePointer SurfaceGeometryFramebuffer::getBlurringTexture() { + if (!_blurringTexture) { + allocate(); + } + return _blurringTexture; +} + void SurfaceGeometryFramebuffer::setResolutionLevel(int resolutionLevel) { if (resolutionLevel != getResolutionLevel()) { clear(); @@ -320,8 +360,7 @@ void SurfaceGeometryFramebuffer::setResolutionLevel(int resolutionLevel) { } SurfaceGeometryPass::SurfaceGeometryPass() : - _firstBlurPass(false), - _secondBlurPass(true, _firstBlurPass.getParameters()) + _diffusePass(false) { Parameters parameters; _parametersBuffer = gpu::BufferView(std::make_shared(sizeof(Parameters), (const gpu::Byte*) ¶meters)); @@ -345,6 +384,10 @@ void SurfaceGeometryPass::configure(const Config& config) { _surfaceGeometryFramebuffer = std::make_shared(); } _surfaceGeometryFramebuffer->setResolutionLevel(config.resolutionLevel); + + _diffusePass.getParameters()->setFilterRadiusScale(config.diffuseFilterScale); + _diffusePass.getParameters()->setDepthThreshold(config.diffuseDepthThreshold); + } @@ -358,33 +401,52 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c const auto deferredFramebuffer = inputs.get1(); const auto linearDepthFramebuffer = inputs.get2(); - auto linearDepthTexture = linearDepthFramebuffer->getHalfLinearDepthTexture(); + + auto linearDepthTexture = linearDepthFramebuffer->getLinearDepthTexture(); + auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); + auto sourceViewport = args->_viewport; + auto curvatureViewport = sourceViewport; + + if (_surfaceGeometryFramebuffer->getResolutionLevel() > 0) { + linearDepthTexture = linearDepthFramebuffer->getHalfLinearDepthTexture(); + normalTexture = linearDepthFramebuffer->getHalfNormalTexture(); + curvatureViewport = curvatureViewport >> _surfaceGeometryFramebuffer->getResolutionLevel(); + } if (!_surfaceGeometryFramebuffer) { _surfaceGeometryFramebuffer = std::make_shared(); } _surfaceGeometryFramebuffer->updateLinearDepth(linearDepthTexture); - // auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); - auto normalTexture = linearDepthFramebuffer->getHalfNormalTexture(); - auto curvatureFramebuffer = _surfaceGeometryFramebuffer->getCurvatureFramebuffer(); + auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); #ifdef USE_STENCIL_TEST - if (curvatureFBO->getDepthStencilBuffer() != deferredFramebuffer->getPrimaryDepthTexture()) { - curvatureFBO->setDepthStencilBuffer(deferredFramebuffer->getPrimaryDepthTexture(), deferredFramebuffer->getPrimaryDepthTexture()->getTexelFormat()); + if (curvatureFramebuffer->getDepthStencilBuffer() != deferredFramebuffer->getPrimaryDepthTexture()) { + curvatureFramebuffer->setDepthStencilBuffer(deferredFramebuffer->getPrimaryDepthTexture(), deferredFramebuffer->getPrimaryDepthTexture()->getTexelFormat()); } #endif - auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); + + auto lowCurvatureFramebuffer = _surfaceGeometryFramebuffer->getLowCurvatureFramebuffer(); + auto lowCurvatureTexture = _surfaceGeometryFramebuffer->getLowCurvatureTexture(); + + auto blurringFramebuffer = _surfaceGeometryFramebuffer->getBlurringFramebuffer(); + auto blurringTexture = _surfaceGeometryFramebuffer->getBlurringTexture(); outputs.edit0() = _surfaceGeometryFramebuffer; outputs.edit1() = curvatureFramebuffer; + outputs.edit2() = curvatureFramebuffer; + outputs.edit3() = lowCurvatureFramebuffer; auto curvaturePipeline = getCurvaturePipeline(); + auto diffuseVPipeline = _diffusePass.getBlurVPipeline(); + auto diffuseHPipeline = _diffusePass.getBlurHPipeline(); - auto depthViewport = args->_viewport; - auto curvatureViewport = depthViewport >> 1; - // >> _surfaceGeometryFramebuffer->getResolutionLevel(); + glm::ivec2 textureSize(curvatureTexture->getDimensions()); + _diffusePass.getParameters()->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, curvatureViewport)); + _diffusePass.getParameters()->setDepthPerspective(args->getViewFrustum().getProjection()[1][1]); + _diffusePass.getParameters()->setLinearDepthPosFar(args->getViewFrustum().getFarClip()); + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { _gpuTimer.begin(batch); batch.enableStereo(false); @@ -393,43 +455,58 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setViewTransform(Transform()); batch.setViewportTransform(curvatureViewport); - batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getCurvatureFrameSize(), curvatureViewport)); - - batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); - batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getSourceFrameSize(), curvatureViewport)); // Curvature pass + batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); + batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, _parametersBuffer); batch.setFramebuffer(curvatureFramebuffer); - - // We can avoid the clear by drawing the same clear vallue from the makeCurvature shader. same performances or no worse - + // We can avoid the clear by drawing the same clear vallue from the makeCurvature shader. same performances or no worse #ifdef USE_STENCIL_TEST // Except if stenciling out batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); #endif - batch.setPipeline(curvaturePipeline); batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, linearDepthTexture); batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, normalTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); - batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); - batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, nullptr); + // Diffusion pass + const int BlurTask_ParamsSlot = 0; + const int BlurTask_SourceSlot = 0; + const int BlurTask_DepthSlot = 1; + batch.setUniformBuffer(BlurTask_ParamsSlot, _diffusePass.getParameters()->_parametersBuffer); + + batch.setResourceTexture(BlurTask_DepthSlot, linearDepthTexture); + + batch.setFramebuffer(blurringFramebuffer); + batch.setPipeline(diffuseVPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, curvatureTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setFramebuffer(curvatureFramebuffer); + batch.setPipeline(diffuseHPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, blurringTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setFramebuffer(blurringFramebuffer); + batch.setPipeline(diffuseVPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, curvatureTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setFramebuffer(lowCurvatureFramebuffer); + batch.setPipeline(diffuseHPipeline); + batch.setResourceTexture(BlurTask_SourceSlot, blurringTexture); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setResourceTexture(BlurTask_SourceSlot, nullptr); + batch.setResourceTexture(BlurTask_DepthSlot, nullptr); + batch.setUniformBuffer(BlurTask_ParamsSlot, nullptr); _gpuTimer.end(batch); }); - - const auto diffuseCurvaturePassInputs = render::BlurGaussianDepthAware::Inputs(curvatureFramebuffer, linearDepthTexture); - gpu::FramebufferPointer midBlurredFramebuffer; - _firstBlurPass.run(sceneContext, renderContext, diffuseCurvaturePassInputs, midBlurredFramebuffer); - - gpu::FramebufferPointer lowBlurredFramebuffer; - _secondBlurPass.run(sceneContext, renderContext, diffuseCurvaturePassInputs, lowBlurredFramebuffer); - + - outputs.edit2() = midBlurredFramebuffer; - outputs.edit3() = lowBlurredFramebuffer; - auto config = std::static_pointer_cast(renderContext->jobConfig); config->gpuTime = _gpuTimer.getAverage(); } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index d0f79dad76..9a7ff7d964 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -112,12 +112,17 @@ public: gpu::FramebufferPointer getCurvatureFramebuffer(); gpu::TexturePointer getCurvatureTexture(); + + gpu::FramebufferPointer getLowCurvatureFramebuffer(); + gpu::TexturePointer getLowCurvatureTexture(); + + gpu::FramebufferPointer getBlurringFramebuffer(); + gpu::TexturePointer getBlurringTexture(); // Update the source framebuffer size which will drive the allocation of all the other resources. void updateLinearDepth(const gpu::TexturePointer& linearDepthBuffer); gpu::TexturePointer getLinearDepthTexture(); const glm::ivec2& getSourceFrameSize() const { return _frameSize; } - glm::ivec2 getCurvatureFrameSize() const { return _frameSize >> _resolutionLevel; } void setResolutionLevel(int level); int getResolutionLevel() const { return _resolutionLevel; } @@ -131,6 +136,12 @@ protected: gpu::FramebufferPointer _curvatureFramebuffer; gpu::TexturePointer _curvatureTexture; + gpu::FramebufferPointer _blurringFramebuffer; + gpu::TexturePointer _blurringTexture; + + gpu::FramebufferPointer _lowCurvatureFramebuffer; + gpu::TexturePointer _lowCurvatureTexture; + glm::ivec2 _frameSize; int _resolutionLevel{ 0 }; }; @@ -143,6 +154,10 @@ class SurfaceGeometryPassConfig : public render::Job::Config { Q_PROPERTY(float basisScale MEMBER basisScale NOTIFY dirty) Q_PROPERTY(float curvatureScale MEMBER curvatureScale NOTIFY dirty) Q_PROPERTY(int resolutionLevel MEMBER resolutionLevel NOTIFY dirty) + + Q_PROPERTY(float diffuseFilterScale MEMBER diffuseFilterScale NOTIFY dirty) + Q_PROPERTY(float diffuseDepthThreshold MEMBER diffuseDepthThreshold NOTIFY dirty) + Q_PROPERTY(double gpuTime READ getGpuTime) public: SurfaceGeometryPassConfig() : render::Job::Config(true) {} @@ -151,6 +166,8 @@ public: float basisScale{ 1.0f }; float curvatureScale{ 10.0f }; int resolutionLevel{ 0 }; + float diffuseFilterScale{ 0.2f }; + float diffuseDepthThreshold{ 1.0f }; double getGpuTime() { return gpuTime; } @@ -191,14 +208,15 @@ private: }; gpu::BufferView _parametersBuffer; + SurfaceGeometryFramebufferPointer _surfaceGeometryFramebuffer; const gpu::PipelinePointer& getCurvaturePipeline(); gpu::PipelinePointer _curvaturePipeline; + + render::BlurGaussianDepthAware _diffusePass; - render::BlurGaussianDepthAware _firstBlurPass; - render::BlurGaussianDepthAware _secondBlurPass; gpu::RangeTimer _gpuTimer; }; diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index ddaaa82e31..3849adf588 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -337,24 +337,23 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const auto blurHPipeline = getBlurHPipeline(); auto sourceViewport = args->_viewport; - auto blurViewport = sourceViewport >> 1; - _parameters->setWidthHeight(blurViewport.z, blurViewport.w, args->_context->isStereo()); + _parameters->setWidthHeight(sourceViewport.z, sourceViewport.w, args->_context->isStereo()); glm::ivec2 textureSize(blurringResources.sourceTexture->getDimensions()); - _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, blurViewport)); + _parameters->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, sourceViewport)); _parameters->setDepthPerspective(args->getViewFrustum().getProjection()[1][1]); _parameters->setLinearDepthPosFar(args->getViewFrustum().getFarClip()); gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { batch.enableStereo(false); - batch.setViewportTransform(blurViewport); + batch.setViewportTransform(sourceViewport); batch.setUniformBuffer(BlurTask_ParamsSlot, _parameters->_parametersBuffer); batch.setResourceTexture(BlurTask_DepthSlot, depthTexture); batch.setFramebuffer(blurringResources.blurringFramebuffer); - // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); batch.setPipeline(blurVPipeline); batch.setResourceTexture(BlurTask_SourceSlot, blurringResources.sourceTexture); @@ -362,7 +361,7 @@ void BlurGaussianDepthAware::run(const SceneContextPointer& sceneContext, const batch.setFramebuffer(blurringResources.finalFramebuffer); if (_inOutResources._generateOutputFramebuffer) { - // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); + // batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(0.0)); } batch.setPipeline(blurHPipeline); diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index b54e494a80..f820947d98 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -144,6 +144,9 @@ public: void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& SourceAndDepth, gpu::FramebufferPointer& blurredFramebuffer); const BlurParamsPointer& getParameters() const { return _parameters; } + + gpu::PipelinePointer getBlurVPipeline(); + gpu::PipelinePointer getBlurHPipeline(); protected: @@ -152,9 +155,6 @@ protected: gpu::PipelinePointer _blurVPipeline; gpu::PipelinePointer _blurHPipeline; - gpu::PipelinePointer getBlurVPipeline(); - gpu::PipelinePointer getBlurHPipeline(); - BlurInOutResource _inOutResources; }; diff --git a/scripts/developer/utilities/render/statsGPU.qml b/scripts/developer/utilities/render/statsGPU.qml index 44ea4c4597..74bd376a00 100644 --- a/scripts/developer/utilities/render/statsGPU.qml +++ b/scripts/developer/utilities/render/statsGPU.qml @@ -53,11 +53,6 @@ Item { prop: "gpuTime", label: "SurfaceGeometry", color: "#00FFFF" - },{ - object: Render.getConfig("CurvatureRangeTimer"), - prop: "gpuTime", - label: "Curvature", - color: "#00FF00" }, { object: Render.getConfig("RenderDeferred"), diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index ad5a9dd03d..3f06e63719 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -41,16 +41,11 @@ Column { min: 0.0 } } - } - - Column{ - CheckBox { - text: "Diffuse Curvature Mid" - checked: true - onCheckedChanged: { Render.getConfig("DiffuseCurvatureMid").enabled = checked } - } + Repeater { - model: [ "Blur Scale:DiffuseCurvatureMid:filterScale:2.0", "Blur Depth Threshold:DiffuseCurvatureMid:depthThreshold:1.0", "Blur Scale2:DiffuseCurvatureLow:filterScale:2.0", "Blur Depth Threshold 2:DiffuseCurvatureLow:depthThreshold:1.0"] + model: [ "Diffusion Scale:SurfaceGeometry:diffuseFilterScale:2.0", + "Diffusion Depth Threshold:SurfaceGeometry:diffuseDepthThreshold:1.0" + ] ConfigSlider { label: qsTr(modelData.split(":")[0]) integral: false @@ -60,12 +55,6 @@ Column { min: 0.0 } } - - CheckBox { - text: "Diffuse Curvature Low" - checked: true - onCheckedChanged: { Render.getConfig("DiffuseCurvatureLow").enabled = checked } - } } } } From d98ae1ba9ce229df8d2ec8f329f439384abc0ccd Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 19 Jul 2016 07:10:34 -0700 Subject: [PATCH 1221/1237] Ensure the pause overlay is unlit (emissive) --- scripts/system/away.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/away.js b/scripts/system/away.js index 04263d4223..25c330738f 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -34,6 +34,7 @@ var OVERLAY_DATA_HMD = { color: {red: 255, green: 255, blue: 255}, alpha: 1, scale: 2, + emissive: true, isFacingAvatar: true, drawInFront: true }; From 1ee5023f6df63d816e4b8ec29c8530a91b7318ad Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 19 Jul 2016 08:33:54 -0700 Subject: [PATCH 1222/1237] fix warnings --- libraries/audio/src/AbstractAudioInterface.cpp | 6 ++---- libraries/audio/src/InboundAudioStream.cpp | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/libraries/audio/src/AbstractAudioInterface.cpp b/libraries/audio/src/AbstractAudioInterface.cpp index aa5c85283c..bf43c35cb9 100644 --- a/libraries/audio/src/AbstractAudioInterface.cpp +++ b/libraries/audio/src/AbstractAudioInterface.cpp @@ -36,10 +36,8 @@ void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes auto sequence = sequenceNumber++; audioPacket->writePrimitive(sequence); - // write the codec - don't include this for injected audio - if (packetType != PacketType::InjectAudio) { - auto stringSize = audioPacket->writeString(codecName); - } + // write the codec + audioPacket->writeString(codecName); if (packetType == PacketType::SilentAudioFrame) { // pack num silent samples diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index ff177e9a93..d781a1991b 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -101,8 +101,6 @@ void InboundAudioStream::perSecondCallbackForUpdatingStats() { } int InboundAudioStream::parseData(ReceivedMessage& message) { - PacketType packetType = message.getType(); - // parse sequence number and track it quint16 sequence; message.readPrimitive(&sequence); From 212175bdaabf4db973a978f3ebba487a7987d4e1 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 19 Jul 2016 10:23:00 -0700 Subject: [PATCH 1223/1237] bug fix for far-grab pulling the object in the wrong direction --- scripts/system/controllers/handControllerGrab.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 50d5d644ac..832346a5fd 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1685,8 +1685,9 @@ function MyController(hand) { radius = 1.0; } - var handDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); - var handMoved = Vec3.multiply(handDelta, radius); + var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); + var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta); + var handMoved = Vec3.multiply(worldHandDelta, radius); this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); this.callEntityMethodOnGrabbed("continueDistantGrab"); @@ -1698,7 +1699,7 @@ function MyController(hand) { var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); // Update radialVelocity - var lastVelocity = Vec3.multiply(handDelta, 1.0 / deltaObjectTime); + var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); var newRadialVelocity = Vec3.dot(lastVelocity, delta); From 36d58a2b823e37542c45381d61c422d3ddfcfdde Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Jul 2016 10:34:06 -0700 Subject: [PATCH 1224/1237] Finish the integration of the diffusion pass in the SurfaceGeometryPass job, clean up ui --- libraries/render-utils/src/DeferredBuffer.slh | 51 ++----------------- .../render-utils/src/RenderDeferredTask.cpp | 11 +--- .../render-utils/src/SurfaceGeometryPass.cpp | 4 ++ .../render-utils/src/SurfaceGeometryPass.h | 6 ++- .../src/surfaceGeometry_makeCurvature.slf | 37 ++++---------- .../utilities/render/surfaceGeometryPass.qml | 6 ++- 6 files changed, 29 insertions(+), 86 deletions(-) diff --git a/libraries/render-utils/src/DeferredBuffer.slh b/libraries/render-utils/src/DeferredBuffer.slh index 5f80354585..a4b69bd70e 100755 --- a/libraries/render-utils/src/DeferredBuffer.slh +++ b/libraries/render-utils/src/DeferredBuffer.slh @@ -11,6 +11,8 @@ <@if not DEFERRED_BUFFER_SLH@> <@def DEFERRED_BUFFER_SLH@> +<@include gpu/PackedNormal.slh@> + // Unpack the metallic-mode value const float FRAG_PACK_SHADED_NON_METALLIC = 0.0; const float FRAG_PACK_SHADED_METALLIC = 0.1; @@ -63,44 +65,7 @@ float packUnlit() { return FRAG_PACK_UNLIT; } - -vec2 signNotZero(vec2 v) { - return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); -} - -vec2 float32x3_to_oct(in vec3 v) { - vec2 p = v.xy * (1.0 / (abs(v.x) + abs(v.y) + abs(v.z))); - return ((v.z <= 0.0) ? ((1.0 - abs(p.yx)) * signNotZero(p)) : p); -} - - -vec3 oct_to_float32x3(in vec2 e) { - vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); - if (v.z < 0) { - v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); - } - return normalize(v); -} - -vec3 snorm12x2_to_unorm8x3(vec2 f) { - vec2 u = vec2(round(clamp(f, -1.0, 1.0) * 2047.0 + 2047.0)); - float t = floor(u.y / 256.0); - - return floor(vec3( - u.x / 16.0, - fract(u.x / 16.0) * 256.0 + t, - u.y - t * 256.0 - )) / 255.0; -} - -vec2 unorm8x3_to_snorm12x2(vec3 u) { - u *= 255.0; - u.y *= (1.0 / 16.0); - vec2 s = vec2( u.x * 16.0 + floor(u.y), - fract(u.y) * (16.0 * 256.0) + u.z); - return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); -} - + <@endif@> diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index aa226e60bc..bb7adf3f80 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -137,18 +137,9 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { const auto midCurvatureNormalFramebuffer = surfaceGeometryPassOutputs.getN(2); const auto lowCurvatureNormalFramebuffer = surfaceGeometryPassOutputs.getN(3); + // Simply update the scattering resource const auto scatteringResource = addJob("Scattering"); -/* const auto curvatureRangeTimer = addJob("BeginCurvatureRangeTimer"); - - // TODO: Push this 2 diffusion stages into surfaceGeometryPass as they are working together - const auto diffuseCurvaturePassInputs = BlurGaussianDepthAware::Inputs(curvatureFramebuffer, halfLinearDepthTexture).hasVarying(); - const auto midCurvatureNormalFramebuffer = addJob("DiffuseCurvatureMid", diffuseCurvaturePassInputs); - const auto lowCurvatureNormalFramebuffer = addJob("DiffuseCurvatureLow", diffuseCurvaturePassInputs, true); // THis blur pass generates it s render resource - - - addJob("CurvatureRangeTimer", curvatureRangeTimer); -*/ // AO job addJob("AmbientOcclusion"); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 95557c99e3..26063ba30c 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -383,7 +383,11 @@ void SurfaceGeometryPass::configure(const Config& config) { if (!_surfaceGeometryFramebuffer) { _surfaceGeometryFramebuffer = std::make_shared(); } + _surfaceGeometryFramebuffer->setResolutionLevel(config.resolutionLevel); + if (config.resolutionLevel != getResolutionLevel()) { + _parametersBuffer.edit().resolutionInfo.w = config.resolutionLevel; + } _diffusePass.getParameters()->setFilterRadiusScale(config.diffuseFilterScale); _diffusePass.getParameters()->setDepthThreshold(config.diffuseDepthThreshold); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 9a7ff7d964..24f0c56cdd 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -165,7 +165,7 @@ public: float depthThreshold{ 5.0f }; // centimeters float basisScale{ 1.0f }; float curvatureScale{ 10.0f }; - int resolutionLevel{ 0 }; + int resolutionLevel{ 1 }; float diffuseFilterScale{ 0.2f }; float diffuseDepthThreshold{ 1.0f }; @@ -189,9 +189,11 @@ public: void configure(const Config& config); void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs); + float getCurvatureDepthThreshold() const { return _parametersBuffer.get().curvatureInfo.x; } float getCurvatureBasisScale() const { return _parametersBuffer.get().curvatureInfo.y; } float getCurvatureScale() const { return _parametersBuffer.get().curvatureInfo.w; } + int getResolutionLevel() const { return (int)_parametersBuffer.get().resolutionInfo.w; } private: typedef gpu::BufferView UniformBufferView; @@ -200,7 +202,7 @@ private: class Parameters { public: // Resolution info - glm::vec4 resolutionInfo { -1.0f, 0.0f, 0.0f, 0.0f }; + glm::vec4 resolutionInfo { 0.0f, 0.0f, 0.0f, 1.0f }; // Default Curvature & Diffusion is running half res // Curvature algorithm glm::vec4 curvatureInfo{ 0.0f }; diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index bf8ca6abf3..e96ac60b45 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -12,6 +12,8 @@ <@include DeferredTransform.slh@> <$declareDeferredFrameTransform()$> +<@include gpu/PackedNormal.slh@> + struct SurfaceGeometryParams { // Resolution info vec4 resolutionInfo; @@ -35,6 +37,10 @@ float getCurvatureScale() { return params.curvatureInfo.w; } +bool isFullResolution() { + return params.resolutionInfo.w == 0.0; +} + uniform sampler2D linearDepthMap; float getZEye(ivec2 pixel) { @@ -44,29 +50,6 @@ float getZEyeLinear(vec2 texcoord) { return -texture(linearDepthMap, texcoord).x; } -vec2 signNotZero(vec2 v) { - return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); -} - -vec3 oct_to_float32x3(in vec2 e) { - vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); - if (v.z < 0) { - v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); - } - return normalize(v); -} - -vec2 unorm8x3_to_snorm12x2(vec3 u) { - u *= 255.0; - u.y *= (1.0 / 16.0); - vec2 s = vec2( u.x * 16.0 + floor(u.y), - fract(u.y) * (16.0 * 256.0) + u.z); - return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); -} -vec3 unpackNormal(in vec3 p) { - return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); -} - vec2 sideToFrameTexcoord(vec2 side, vec2 texcoordPos) { return vec2((texcoordPos.x + side.x) * side.y, texcoordPos.y); } @@ -78,10 +61,12 @@ vec3 getRawNormal(vec2 texcoord) { } vec3 getWorldNormal(vec2 texcoord) { - // vec3 rawNormal = getRawNormal(texcoord); - // return unpackNormal(rawNormal); vec3 rawNormal = getRawNormal(texcoord); - return normalize((rawNormal - vec3(0.5)) * 2.0); + if (isFullResolution()) { + return unpackNormal(rawNormal); + } else { + return normalize((rawNormal - vec3(0.5)) * 2.0); + } } vec3 getWorldNormalDiff(vec2 texcoord, vec2 delta) { diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index 3f06e63719..1ff0efa15d 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -30,7 +30,6 @@ Column { model: [ "Basis Scale:basisScale:2.0:false", "Curvature Scale:curvatureScale:100.0:false", - "Downscale:resolutionLevel:4:true" ] ConfigSlider { label: qsTr(modelData.split(":")[0]) @@ -41,6 +40,11 @@ Column { min: 0.0 } } + CheckBox { + text: "Half Resolution" + checked: Render.getConfig("SurfaceGeometry")["resolutionLevel"] + onCheckedChanged: { Render.getConfig("SurfaceGeometry")["resolutionLevel"] = checked } + } Repeater { model: [ "Diffusion Scale:SurfaceGeometry:diffuseFilterScale:2.0", From f28f3d7fcf9adeb8ee841754b63e48b4f41b55ba Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 19 Jul 2016 12:02:37 -0700 Subject: [PATCH 1225/1237] force removal of an old solo node when added new one --- libraries/networking/src/LimitedNodeList.cpp | 30 ++++++++++++++++++-- libraries/networking/src/LimitedNodeList.h | 3 +- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index e2d6b277a7..d83046bc1b 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -34,7 +34,7 @@ #include "NetworkLogging.h" #include "udt/Packet.h" -const char SOLO_NODE_TYPES[2] = { +const std::set SOLO_NODE_TYPES = { NodeType::AvatarMixer, NodeType::AudioMixer }; @@ -534,7 +534,7 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t if (it != _nodeHash.end()) { SharedNodePointer& matchingNode = it->second; - + matchingNode->setPublicSocket(publicSocket); matchingNode->setLocalSocket(localSocket); matchingNode->setPermissions(permissions); @@ -551,7 +551,33 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t SharedNodePointer newNodePointer(newNode, &QObject::deleteLater); + // if this is a solo node type, we assume that the DS has replaced its assignment and we should kill the previous node + if (SOLO_NODE_TYPES.count(newNode->getType())) { + // while we still have the read lock, see if there is a previous solo node we'll need to remove + auto previousSoloIt = std::find_if(_nodeHash.cbegin(), _nodeHash.cend(), [newNode](const UUIDNodePair& nodePair){ + return nodePair.second->getType() == newNode->getType(); + }); + + if (previousSoloIt != _nodeHash.cend()) { + // we have a previous solo node, switch to a write lock so we can remove it + readLocker.unlock(); + + QWriteLocker writeLocker(&_nodeMutex); + + auto oldSoloNode = previousSoloIt->second; + + _nodeHash.unsafe_erase(previousSoloIt); + handleNodeKill(oldSoloNode); + + // convert the current lock back to a read lock for insertion of new node + writeLocker.unlock(); + readLocker.relock(); + } + } + + // insert the new node and release our read lock _nodeHash.insert(UUIDNodePair(newNode->getUUID(), newNodePointer)); + readLocker.unlock(); qCDebug(networking) << "Added" << *newNode; diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 03e82f053f..d599fbcc37 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #ifndef _WIN32 @@ -46,7 +47,7 @@ const quint64 NODE_SILENCE_THRESHOLD_MSECS = 5 * 1000; -extern const char SOLO_NODE_TYPES[2]; +extern const std::set SOLO_NODE_TYPES; const char DEFAULT_ASSIGNMENT_SERVER_HOSTNAME[] = "localhost"; From 4742f40128235ac35815fcdb1ba4becfc92fac6f Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Jul 2016 12:23:57 -0700 Subject: [PATCH 1226/1237] Separating the normal packing into it s own file and make sure to sclae the filter radius correctly dpeending on the resolution of diffusion --- libraries/gpu/src/gpu/PackedNormal.slh | 61 +++++++++++++++++++ .../render-utils/src/DebugDeferredBuffer.cpp | 2 +- .../src/DeferredLightingEffect.cpp | 3 +- .../render-utils/src/SurfaceGeometryPass.cpp | 15 ++++- libraries/render-utils/src/simple.slf | 2 +- .../surfaceGeometry_downsampleDepthNormal.slf | 57 ++--------------- libraries/render/src/render/BlurTask.slh | 6 +- 7 files changed, 86 insertions(+), 60 deletions(-) create mode 100644 libraries/gpu/src/gpu/PackedNormal.slh diff --git a/libraries/gpu/src/gpu/PackedNormal.slh b/libraries/gpu/src/gpu/PackedNormal.slh new file mode 100644 index 0000000000..ecb383fc30 --- /dev/null +++ b/libraries/gpu/src/gpu/PackedNormal.slh @@ -0,0 +1,61 @@ + +<@if not PACKED_NORMAL_SLH@> +<@def PACKED_NORMAL_SLH@> + +vec2 signNotZero(vec2 v) { + return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); +} + +vec2 float32x3_to_oct(in vec3 v) { + vec2 p = v.xy * (1.0 / (abs(v.x) + abs(v.y) + abs(v.z))); + return ((v.z <= 0.0) ? ((1.0 - abs(p.yx)) * signNotZero(p)) : p); +} + + +vec3 oct_to_float32x3(in vec2 e) { + vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); + if (v.z < 0) { + v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); + } + return normalize(v); +} + +vec3 snorm12x2_to_unorm8x3(vec2 f) { + vec2 u = vec2(round(clamp(f, -1.0, 1.0) * 2047.0 + 2047.0)); + float t = floor(u.y / 256.0); + + return floor(vec3( + u.x / 16.0, + fract(u.x / 16.0) * 256.0 + t, + u.y - t * 256.0 + )) / 255.0; +} + +vec2 unorm8x3_to_snorm12x2(vec3 u) { + u *= 255.0; + u.y *= (1.0 / 16.0); + vec2 s = vec2( u.x * 16.0 + floor(u.y), + fract(u.y) * (16.0 * 256.0) + u.z); + return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); +} + + +// Recommended function to pack/unpack vec3 normals to vec3 rgb with best efficiency +vec3 packNormal(in vec3 n) { + return snorm12x2_to_unorm8x3(float32x3_to_oct(n)); +} + +vec3 unpackNormal(in vec3 p) { + return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); +} + +<@endif@> diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 672bb00cc8..9d0e38027e 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -414,7 +414,7 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren batch.setResourceTexture(HalfNormal, linearDepthTarget->getHalfNormalTexture()); batch.setResourceTexture(Curvature, surfaceGeometryFramebuffer->getCurvatureTexture()); - batch.setResourceTexture(DiffusedCurvature, diffusedCurvatureFramebuffer->getRenderBuffer(0)); + batch.setResourceTexture(DiffusedCurvature, surfaceGeometryFramebuffer->getLowCurvatureTexture()); if (DependencyManager::get()->isAmbientOcclusionEnabled()) { batch.setResourceTexture(AmbientOcclusion, framebufferCache->getOcclusionTexture()); } else { diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 9d8c1ef200..6f202f6200 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -448,7 +448,8 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c batch.setResourceTexture(DEFERRED_BUFFER_CURVATURE_UNIT, surfaceGeometryFramebuffer->getCurvatureTexture()); } if (lowCurvatureNormalFramebuffer) { - batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, lowCurvatureNormalFramebuffer->getRenderBuffer(0)); + // batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, lowCurvatureNormalFramebuffer->getRenderBuffer(0)); + batch.setResourceTexture(DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, surfaceGeometryFramebuffer->getLowCurvatureTexture()); } if (subsurfaceScatteringResource) { batch.setUniformBuffer(SCATTERING_PARAMETERS_BUFFER_SLOT, subsurfaceScatteringResource->getParametersBuffer()); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 26063ba30c..fd778d30be 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -367,9 +367,10 @@ SurfaceGeometryPass::SurfaceGeometryPass() : } void SurfaceGeometryPass::configure(const Config& config) { + const float CM_TO_M = 0.01f; - if ((config.depthThreshold * 100.0f) != getCurvatureDepthThreshold()) { - _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold * 100.0f; + if ((config.depthThreshold * CM_TO_M) != getCurvatureDepthThreshold()) { + _parametersBuffer.edit().curvatureInfo.x = config.depthThreshold * CM_TO_M; } if (config.basisScale != getCurvatureBasisScale()) { @@ -389,7 +390,8 @@ void SurfaceGeometryPass::configure(const Config& config) { _parametersBuffer.edit().resolutionInfo.w = config.resolutionLevel; } - _diffusePass.getParameters()->setFilterRadiusScale(config.diffuseFilterScale); + auto filterRadius = (getResolutionLevel() > 0 ? config.diffuseFilterScale / 2.0f : config.diffuseFilterScale); + _diffusePass.getParameters()->setFilterRadiusScale(filterRadius); _diffusePass.getParameters()->setDepthThreshold(config.diffuseDepthThreshold); } @@ -445,6 +447,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c auto diffuseVPipeline = _diffusePass.getBlurVPipeline(); auto diffuseHPipeline = _diffusePass.getBlurHPipeline(); + _diffusePass.getParameters()->setWidthHeight(curvatureViewport.z, curvatureViewport.w, args->_context->isStereo()); glm::ivec2 textureSize(curvatureTexture->getDimensions()); _diffusePass.getParameters()->setTexcoordTransform(gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(textureSize, curvatureViewport)); _diffusePass.getParameters()->setDepthPerspective(args->getViewFrustum().getProjection()[1][1]); @@ -475,6 +478,12 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, normalTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); + + batch.setResourceTexture(SurfaceGeometryPass_DepthMapSlot, nullptr); + batch.setResourceTexture(SurfaceGeometryPass_NormalMapSlot, nullptr); + batch.setUniformBuffer(SurfaceGeometryPass_ParamsSlot, nullptr); + batch.setUniformBuffer(SurfaceGeometryPass_FrameTransformSlot, nullptr); + // Diffusion pass const int BlurTask_ParamsSlot = 0; const int BlurTask_SourceSlot = 0; diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index 4a16c1c46a..dd5faf40db 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -41,7 +41,7 @@ void main(void) { #ifdef PROCEDURAL_V1 specular = getProceduralColor().rgb; // Procedural Shaders are expected to be Gamma corrected so let's bring back the RGB in linear space for the rest of the pipeline - specular = pow(specular, vec3(2.2)); + //specular = pow(specular, vec3(2.2)); emissiveAmount = 1.0; #else emissiveAmount = getProceduralColors(diffuse, specular, shininess); diff --git a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf index af371d53a8..533073b224 100644 --- a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf +++ b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf @@ -10,35 +10,11 @@ // - +<@include gpu/PackedNormal.slh@> uniform sampler2D linearDepthMap; uniform sampler2D normalMap; - -vec2 signNotZero(vec2 v) { - return vec2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0); -} - -vec3 oct_to_float32x3(in vec2 e) { - vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y)); - if (v.z < 0) { - v.xy = (1.0 - abs(v.yx)) * signNotZero(v.xy); - } - return normalize(v); -} - -vec2 unorm8x3_to_snorm12x2(vec3 u) { - u *= 255.0; - u.y *= (1.0 / 16.0); - vec2 s = vec2( u.x * 16.0 + floor(u.y), - fract(u.y) * (16.0 * 256.0) + u.z); - return clamp(s * (1.0 / 2047.0) - 1.0, vec2(-1.0), vec2(1.0)); -} -vec3 unpackNormal(in vec3 p) { - return oct_to_float32x3(unorm8x3_to_snorm12x2(p)); -} - in vec2 varTexCoord0; out vec4 outLinearDepth; @@ -46,43 +22,22 @@ out vec4 outNormal; void main(void) { // Gather 2 by 2 quads from texture - vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); + + // Try different filters for Z +// vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); + // float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); + float Zeye = texture(linearDepthMap, varTexCoord0).x; vec4 rawNormalsX = textureGather(normalMap, varTexCoord0, 0); vec4 rawNormalsY = textureGather(normalMap, varTexCoord0, 1); vec4 rawNormalsZ = textureGather(normalMap, varTexCoord0, 2); - float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); vec3 normal = vec3(0.0); normal += unpackNormal(vec3(rawNormalsX[0], rawNormalsY[0], rawNormalsZ[0])); normal += unpackNormal(vec3(rawNormalsX[1], rawNormalsY[1], rawNormalsZ[1])); normal += unpackNormal(vec3(rawNormalsX[2], rawNormalsY[2], rawNormalsZ[2])); normal += unpackNormal(vec3(rawNormalsX[3], rawNormalsY[3], rawNormalsZ[3])); - /* - ivec2 texpos = ivec2(gl_FragCoord.xy) * 2; - - vec4 Zeyes; - Zeyes[0] = texelFetch(linearDepthMap, texpos, 0).x; - Zeyes[1] = texelFetch(linearDepthMap, texpos + ivec2(0, 1), 0).x; - Zeyes[2] = texelFetch(linearDepthMap, texpos + ivec2(1, 0), 0).x; - Zeyes[3] = texelFetch(linearDepthMap, texpos + ivec2(1, 1), 0).x; - - vec3 rawNormals[4]; - rawNormals[0] = texelFetch(normalMap, texpos, 0).xyz; - rawNormals[1] = texelFetch(normalMap, texpos + ivec2(0, 1), 0).xyz; - rawNormals[2] = texelFetch(normalMap, texpos + ivec2(1, 0), 0).xyz; - rawNormals[3] = texelFetch(normalMap, texpos + ivec2(1, 1), 0).xyz; - - float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); - - vec3 normal = vec3(0.0); - - normal += unpackNormal(rawNormals[0]); - normal += unpackNormal(rawNormals[1]); - normal += unpackNormal(rawNormals[2]); - normal += unpackNormal(rawNormals[3]); -*/ normal = normalize(normal); diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index d40aca4a76..de2614eb51 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -126,7 +126,7 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep // Accumulate the center sample vec4 srcBlurred = gaussianDistributionCurve[0] * sampleCenter; -/* for(int i = 1; i < NUM_TAPS; i++) { + for(int i = 1; i < NUM_TAPS; i++) { // Fetch color and depth for current sample. vec2 sampleCoord = texcoord + (gaussianDistributionOffset[i] * finalStep); float srcDepth = texture(depthMap, sampleCoord).x; @@ -139,8 +139,8 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep // Accumulate. srcBlurred += gaussianDistributionCurve[i] * srcSample; } -*/ + /* for(int i = 1; i < NUM_HALF_TAPS; i++) { // Fetch color and depth for current sample. vec2 texcoordOffset = (gaussianDistributionOffsetHalf[i] * finalStep); @@ -159,7 +159,7 @@ vec4 pixelShaderGaussianDepthAware(vec2 texcoord, vec2 direction, vec2 pixelStep // Accumulate. srcBlurred += gaussianDistributionCurveHalf[i] * (srcSampleP + srcSampleN); - } + }*/ return srcBlurred; } From 2adff24a058ab596bae5b427d55b03e72caaf99e Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 19 Jul 2016 21:34:42 +0200 Subject: [PATCH 1227/1237] silly workaround- workaround --- scripts/system/controllers/teleport.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 854e23315e..3d40bfb9eb 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -88,11 +88,13 @@ function Teleporter() { this.createTargetOverlay = function() { - _this.deleteTargetOverlay(); + if (_this.targetOverlay !== null) { + return; + } var targetOverlayProps = { url: TARGET_MODEL_URL, dimensions: TARGET_MODEL_DIMENSIONS, - visible: true, + visible: true }; _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); @@ -620,4 +622,4 @@ function cleanup() { if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } -} \ No newline at end of file +} From 3e7126f5bea5a41601fe8b984832218843c20741 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Jul 2016 13:06:26 -0700 Subject: [PATCH 1228/1237] Removed reference to LIghtingModel.slh in teh DefereedBufferWrite.slh to avoid side effects --- libraries/render-utils/src/DeferredBufferWrite.slh | 10 ++-------- libraries/render-utils/src/LightingModel.slh | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 9adb7948df..3153a851fb 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -12,7 +12,6 @@ <@def DEFERRED_BUFFER_WRITE_SLH@> <@include DeferredBuffer.slh@> -<@include LightingModel.slh@> layout(location = 0) out vec4 _fragColor0; @@ -40,7 +39,6 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness if (alpha != 1.0) { discard; } - emissive *= isEmissiveEnabled(); _fragColor0 = vec4(albedo, ((scattering > 0.0) ? packScatteringMetallic(metallic) : packShadedMetallic(metallic))); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(((scattering > 0.0) ? vec3(scattering) : emissive), occlusion); @@ -48,7 +46,6 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness _fragColor3 = vec4(emissive, 1.0); } - void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 fresnel, vec3 lightmap) { if (alpha != 1.0) { discard; @@ -57,11 +54,8 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float r _fragColor0 = vec4(albedo, packLightmappedMetallic(metallic)); _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); _fragColor2 = vec4(lightmap, 1.0); - - _fragColor3 = vec4(lightmap * isLightmapEnabled(), 1.0); - if (isAlbedoEnabled() > 0.0) { - _fragColor3.rgb *= albedo; - } + + _fragColor3 = vec4(lightmap * albedo, 1.0); } void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index c0722945bc..f36b2d8131 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -20,7 +20,7 @@ struct LightingModel { vec4 _ShowContourObscuranceSpare2; }; -uniform lightingModelBuffer { +uniform lightingModelBuffer{ LightingModel lightingModel; }; From 7f169b8e4f79bf3efc6e827517c9e7ae951b75fa Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Jul 2016 13:20:19 -0700 Subject: [PATCH 1229/1237] Adjust qml positions and fix one shader bug --- libraries/render-utils/src/model_unlit.slf | 1 + .../developer/utilities/render/debugSurfaceGeometryPass.js | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/model_unlit.slf b/libraries/render-utils/src/model_unlit.slf index c64f9a07d7..750b51fe8c 100644 --- a/libraries/render-utils/src/model_unlit.slf +++ b/libraries/render-utils/src/model_unlit.slf @@ -13,6 +13,7 @@ // <@include DeferredBufferWrite.slh@> +<@include LightingModel.slh@> <@include model/Material.slh@> <@include MaterialTextures.slh@> diff --git a/scripts/developer/utilities/render/debugSurfaceGeometryPass.js b/scripts/developer/utilities/render/debugSurfaceGeometryPass.js index b28bb5269e..44df10e690 100644 --- a/scripts/developer/utilities/render/debugSurfaceGeometryPass.js +++ b/scripts/developer/utilities/render/debugSurfaceGeometryPass.js @@ -13,8 +13,7 @@ var qml = Script.resolvePath('surfaceGeometryPass.qml'); var window = new OverlayWindow({ title: 'Surface Geometry Pass', source: qml, - width: 400, height: 300, + width: 400, height: 170, }); -window.setPosition(250, 400); +window.setPosition(Window.innerWidth - 420, 50 + 550 + 50); window.closed.connect(function() { Script.stop(); }); - From 8f663f20e0c2cb30a867ad85894dbccef73261cd Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Jul 2016 13:24:03 -0700 Subject: [PATCH 1230/1237] FIx warnings --- libraries/render-utils/src/DebugDeferredBuffer.cpp | 1 - libraries/render/src/render/BlurTask.h | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 9d0e38027e..8118df5435 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -379,7 +379,6 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren auto& deferredFramebuffer = inputs.get0(); auto& linearDepthTarget = inputs.get1(); auto& surfaceGeometryFramebuffer = inputs.get2(); - auto& diffusedCurvatureFramebuffer = inputs.get3(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); diff --git a/libraries/render/src/render/BlurTask.h b/libraries/render/src/render/BlurTask.h index f820947d98..a00a444af7 100644 --- a/libraries/render/src/render/BlurTask.h +++ b/libraries/render/src/render/BlurTask.h @@ -149,13 +149,11 @@ public: gpu::PipelinePointer getBlurHPipeline(); protected: - - BlurParamsPointer _parameters; - gpu::PipelinePointer _blurVPipeline; gpu::PipelinePointer _blurHPipeline; BlurInOutResource _inOutResources; + BlurParamsPointer _parameters; }; } From 4a6132874f57b8ff463ee9117cd48ad9a6a15904 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 19 Jul 2016 13:43:28 -0700 Subject: [PATCH 1231/1237] encodeURIComponent of the protocol signature --- interface/resources/qml/AddressBarDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 0c9e5c9dbc..792410c59d 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -253,7 +253,7 @@ Window { // FIXME: really want places I'm allowed in, not just open ones. 'restriction=open', // Not by whitelist, etc. FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. // FIXME add maturity - 'protocol=' + AddressManager.protocolVersion(), + 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), 'sort_by=users', 'sort_order=desc', ]; From 7b2d5e7fabafbd88d2ee09a607092c367abdc2b4 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 20 Jul 2016 10:41:16 -0700 Subject: [PATCH 1232/1237] Potential cause of that buzzsaw when resetting --- libraries/audio/src/InboundAudioStream.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index d781a1991b..a1a5915a8c 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -59,7 +59,9 @@ void InboundAudioStream::reset() { _isStarved = true; _hasStarted = false; resetStats(); - cleanupCodec(); + // FIXME: calling cleanupCodec() seems to be the cause of the buzzsaw -- we get an assert + // after this is called in AudioClient. Ponder and fix... + // cleanupCodec(); } void InboundAudioStream::resetStats() { From 1c4742e7101d0ce10de82eb9ca2aed846930bd4d Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 20 Jul 2016 11:02:27 -0700 Subject: [PATCH 1233/1237] Fixing comments from review --- libraries/render-utils/src/DeferredFrameTransform.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/DeferredFrameTransform.h b/libraries/render-utils/src/DeferredFrameTransform.h index 1abd912f15..df47396a38 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.h +++ b/libraries/render-utils/src/DeferredFrameTransform.h @@ -36,12 +36,12 @@ protected: // It s changing every frame class FrameTransform { public: - // Pixel info is { viemport width height and stereo on off} + // Pixel info is { viewport width height} glm::vec4 pixelInfo; glm::vec4 invpixelInfo; // Depth info is { n.f, f - n, -f} glm::vec4 depthInfo; - // Stereo info + // Stereo info is { isStereoFrame, halfWidth } glm::vec4 stereoInfo{ 0.0 }; // Mono proj matrix or Left and Right proj matrix going from Mono Eye space to side clip space glm::mat4 projection[2]; From 406b08ffa13bce3536cbde7917d33790c68601c2 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Jul 2016 11:29:00 -0700 Subject: [PATCH 1234/1237] when cmake is told SERVER_ONLY, still build server-side plugins --- CMakeLists.txt | 2 +- plugins/CMakeLists.txt | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0922779bc6..0d42be3d95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,8 +226,8 @@ if (NOT ANDROID) add_subdirectory(interface) set_target_properties(interface PROPERTIES FOLDER "Apps") add_subdirectory(tests) - add_subdirectory(plugins) endif() + add_subdirectory(plugins) add_subdirectory(tools) endif() diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 55b18b122c..7185fda3f7 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -10,9 +10,26 @@ file(GLOB PLUGIN_SUBDIRS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/*") list(REMOVE_ITEM PLUGIN_SUBDIRS "CMakeFiles") -foreach(DIR ${PLUGIN_SUBDIRS}) - if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}") - add_subdirectory(${DIR}) - endif() -endforeach() +# client-side plugins +if (NOT SERVER_ONLY) + set(DIR "oculus") add_subdirectory(${DIR}) + set(DIR "hifiSdl2") add_subdirectory(${DIR}) + set(DIR "openvr") add_subdirectory(${DIR}) + set(DIR "oculusLegacy") add_subdirectory(${DIR}) + set(DIR "hifiSixense") add_subdirectory(${DIR}) + set(DIR "hifiSpacemouse") add_subdirectory(${DIR}) + set(DIR "hifiNeuron") add_subdirectory(${DIR}) +endif() + +# server-side plugins +set(DIR "pcmCodec") add_subdirectory(${DIR}) +set(DIR "hifiCodec") add_subdirectory(${DIR}) + + +# foreach(DIR ${PLUGIN_SUBDIRS}) +# if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}") +# add_subdirectory(${DIR}) +# message("XXXXXXXXXXXXXXXXX" ${DIR}) +# endif() +# endforeach() From c36d0d91add46811587d96943adf3f0e79753024 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Jul 2016 11:29:57 -0700 Subject: [PATCH 1235/1237] when cmake is told SERVER_ONLY, still build server-side plugins --- plugins/CMakeLists.txt | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7185fda3f7..f766f32efe 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -12,18 +12,27 @@ list(REMOVE_ITEM PLUGIN_SUBDIRS "CMakeFiles") # client-side plugins if (NOT SERVER_ONLY) - set(DIR "oculus") add_subdirectory(${DIR}) - set(DIR "hifiSdl2") add_subdirectory(${DIR}) - set(DIR "openvr") add_subdirectory(${DIR}) - set(DIR "oculusLegacy") add_subdirectory(${DIR}) - set(DIR "hifiSixense") add_subdirectory(${DIR}) - set(DIR "hifiSpacemouse") add_subdirectory(${DIR}) - set(DIR "hifiNeuron") add_subdirectory(${DIR}) + set(DIR "oculus") + add_subdirectory(${DIR}) + set(DIR "hifiSdl2") + add_subdirectory(${DIR}) + set(DIR "openvr") + add_subdirectory(${DIR}) + set(DIR "oculusLegacy") + add_subdirectory(${DIR}) + set(DIR "hifiSixense") + add_subdirectory(${DIR}) + set(DIR "hifiSpacemouse") + add_subdirectory(${DIR}) + set(DIR "hifiNeuron") + add_subdirectory(${DIR}) endif() # server-side plugins -set(DIR "pcmCodec") add_subdirectory(${DIR}) -set(DIR "hifiCodec") add_subdirectory(${DIR}) +set(DIR "pcmCodec") +add_subdirectory(${DIR}) +set(DIR "hifiCodec") +add_subdirectory(${DIR}) # foreach(DIR ${PLUGIN_SUBDIRS}) From 5485b3ee13244ef4b8973b33e55ef79a42a3c678 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Jul 2016 11:34:44 -0700 Subject: [PATCH 1236/1237] when cmake is told SERVER_ONLY, still build server-side plugins --- cmake/macros/SetupHifiClientServerPlugin.cmake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmake/macros/SetupHifiClientServerPlugin.cmake b/cmake/macros/SetupHifiClientServerPlugin.cmake index 74ceaeb0ab..0ce7796756 100644 --- a/cmake/macros/SetupHifiClientServerPlugin.cmake +++ b/cmake/macros/SetupHifiClientServerPlugin.cmake @@ -8,7 +8,9 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN) set(${TARGET_NAME}_SHARED 1) setup_hifi_library(${ARGV}) - add_dependencies(interface ${TARGET_NAME}) + if (NOT DEFINED SERVER_ONLY) + add_dependencies(interface ${TARGET_NAME}) + endif() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Plugins") if (APPLE) From 4e308914b2efa62a5dfcc6f5a6e3d8d074dbe7bd Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Jul 2016 11:37:30 -0700 Subject: [PATCH 1237/1237] when cmake is told SERVER_ONLY, still build server-side plugins --- plugins/CMakeLists.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f766f32efe..1e2a672107 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -33,12 +33,3 @@ set(DIR "pcmCodec") add_subdirectory(${DIR}) set(DIR "hifiCodec") add_subdirectory(${DIR}) - - -# foreach(DIR ${PLUGIN_SUBDIRS}) -# if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}") -# add_subdirectory(${DIR}) -# message("XXXXXXXXXXXXXXXXX" ${DIR}) -# endif() -# endforeach() -

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 From fd467caf86ea55b2558575cf6ebe7e1390e415c8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 30 May 2016 14:19:25 +1200 Subject: [PATCH 0261/1237] Add "Open Folder" button to file browser dialog --- .../resources/qml/dialogs/FileDialog.qml | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 00a66c01cc..b10917222d 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -203,6 +203,7 @@ ModalWindow { var row = fileTableView.currentRow; if (row === -1) { + openFolderButton.enabled = false; return; } @@ -213,6 +214,7 @@ ModalWindow { } else { currentSelection.text = ""; } + openFolderButton.enabled = currentSelectionIsFolder } function navigateUp() { @@ -389,6 +391,13 @@ ModalWindow { rows++; } + + fileTableView.selection.clear(); + if (model.count > 0 && fileTableView.activeFocus) { + fileTableView.currentRow = 0; + fileTableView.selection.select(0); + d.update(); + } } } @@ -617,6 +626,16 @@ ModalWindow { Keys.onReturnPressed: okAction.trigger() KeyNavigation.up: selectionType KeyNavigation.left: selectionType + KeyNavigation.right: openFolderButton + } + + Button { + id: openFolderButton + text: "Open Folder" + enabled: false + action: openFolderAction + KeyNavigation.up: selectionType + KeyNavigation.left: openButton KeyNavigation.right: cancelButton } @@ -624,7 +643,7 @@ ModalWindow { id: cancelButton action: cancelAction KeyNavigation.up: selectionType - KeyNavigation.left: openButton + KeyNavigation.left: openFolderButton KeyNavigation.right: fileTableView.contentItem Keys.onReturnPressed: { canceled(); root.enabled = false } } @@ -694,6 +713,12 @@ ModalWindow { } } + Action { + id: openFolderAction + text: "Open Folder" + onTriggered: { fileTableView.navigateToCurrentRow(); } + } + Action { id: cancelAction text: "Cancel" From 437607998f3e777cc9e645762bdf3b16b746a9c9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 30 May 2016 14:20:13 +1200 Subject: [PATCH 0262/1237] Adjust minimum size of file browser dialog to keep all buttons visible --- interface/resources/qml/dialogs/FileDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index b10917222d..df609d2432 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -29,7 +29,7 @@ ModalWindow { implicitWidth: 640 implicitHeight: 480 - minSize: Qt.vector2d(300, 240) + minSize: Qt.vector2d(388, 240) draggable: true HifiConstants { id: hifi } From 1eb0b6a2315da8b1954b9bffcf8a60ac049222d9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 31 May 2016 11:38:58 -0700 Subject: [PATCH 0263/1237] apply FBXGeometry.scaleOffset to mesh vertices --- tools/vhacd-util/src/VHACDUtil.cpp | 18 +++++++----------- tools/vhacd-util/src/VHACDUtil.h | 4 +--- tools/vhacd-util/src/VHACDUtilApp.cpp | 26 +------------------------- 3 files changed, 9 insertions(+), 39 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 7acb26d81e..9e28d33120 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -87,19 +87,12 @@ void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& trian } } - -void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, - uint32_t& meshPartCount, - uint32_t startMeshIndex, uint32_t endMeshIndex) const { +void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometryOffset, FBXMesh& result) const { // this is used to make meshes generated from a highfield collidable. each triangle // is converted into a tetrahedron and made into its own mesh-part. std::vector triangleIndices; foreach (const FBXMeshPart &meshPart, mesh.parts) { - if (meshPartCount < startMeshIndex || meshPartCount >= endMeshIndex) { - meshPartCount++; - continue; - } getTrianglesInMeshPart(meshPart, triangleIndices); } @@ -107,10 +100,13 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, return; } + int indexStartOffset = result.vertices.size(); + // new mesh gets the transformed points from the original + glm::mat4 totalTransform = geometryOffset * mesh.modelTransform; for (int i = 0; i < mesh.vertices.size(); i++) { // apply the source mesh's transform to the points - glm::vec4 v = mesh.modelTransform * glm::vec4(mesh.vertices[i], 1.0f); + glm::vec4 v = totalTransform * glm::vec4(mesh.vertices[i], 1.0f); result.vertices += glm::vec3(v); } @@ -118,7 +114,6 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, const uint32_t TRIANGLE_STRIDE = 3; const float COLLISION_TETRAHEDRON_SCALE = 0.25f; - int indexStartOffset = result.vertices.size(); for (uint32_t i = 0; i < triangleIndices.size(); i += TRIANGLE_STRIDE) { int index0 = triangleIndices[i] + indexStartOffset; int index1 = triangleIndices[i + 1] + indexStartOffset; @@ -341,8 +336,9 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, // each mesh has its own transform to move it to model-space std::vector vertices; + glm::mat4 totalTransform = geometry.offset * mesh.modelTransform; foreach (glm::vec3 vertex, mesh.vertices) { - vertices.push_back(glm::vec3(mesh.modelTransform * glm::vec4(vertex, 1.0f))); + vertices.push_back(glm::vec3(totalTransform * glm::vec4(vertex, 1.0f))); } uint32_t numVertices = (uint32_t)vertices.size(); diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 04e8dab92d..8f82c4e4e4 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -29,9 +29,7 @@ namespace vhacd { bool loadFBX(const QString filename, FBXGeometry& result); - void fattenMeshes(const FBXMesh& mesh, FBXMesh& result, - unsigned int& meshPartCount, - unsigned int startMeshIndex, unsigned int endMeshIndex) const; + void fattenMesh(const FBXMesh& mesh, const glm::mat4& gometryOffset, FBXMesh& result) const; bool computeVHACD(FBXGeometry& geometry, VHACD::IVHACD::Parameters params, diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index c7257bd5c2..cae184a49c 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -126,12 +126,6 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : const QCommandLineOption outputCentimetersOption("c", "output units are centimeters"); parser.addOption(outputCentimetersOption); - const QCommandLineOption startMeshIndexOption("s", "start-mesh index", "0"); - parser.addOption(startMeshIndexOption); - - const QCommandLineOption endMeshIndexOption("e", "end-mesh index", "0"); - parser.addOption(endMeshIndexOption); - const QCommandLineOption minimumMeshSizeOption("m", "minimum mesh (diagonal) size to consider", "0"); parser.addOption(minimumMeshSizeOption); @@ -230,16 +224,6 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : Q_UNREACHABLE(); } - int startMeshIndex = -1; - if (parser.isSet(startMeshIndexOption)) { - startMeshIndex = parser.value(startMeshIndexOption).toInt(); - } - - int endMeshIndex = -1; - if (parser.isSet(endMeshIndexOption)) { - endMeshIndex = parser.value(endMeshIndexOption).toInt(); - } - float minimumMeshSize = 0.0f; if (parser.isSet(minimumMeshSizeOption)) { minimumMeshSize = parser.value(minimumMeshSizeOption).toFloat(); @@ -417,17 +401,9 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : meshCount += mesh.parts.size(); } - if (startMeshIndex < 0) { - startMeshIndex = 0; - } - if (endMeshIndex < 0) { - endMeshIndex = meshCount; - } - - unsigned int meshPartCount = 0; result.modelTransform = glm::mat4(); // Identity matrix foreach (const FBXMesh& mesh, fbx.meshes) { - vUtil.fattenMeshes(mesh, result, meshPartCount, startMeshIndex, endMeshIndex); + vUtil.fattenMesh(mesh, fbx.offset, result); } newFbx.meshes.append(result); From 9ad19a2eaf031d95e9be1a6059aafb4790bed4ff Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 31 May 2016 14:57:31 -0700 Subject: [PATCH 0264/1237] Draw grab balls in front of entities (not just the lines as before). Don't intersect with grab balls (not just lasers as before). Don't run grab lasers through (2d or 3d) overlays. Don't try to manage reticle in handControllerGrab because other scripts do. --- .../system/controllers/handControllerGrab.js | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 06549a38b5..be4bffe58c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -249,6 +249,18 @@ function entityIsGrabbedByOther(entityID) { return false; } +// If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, +// and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. +var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; +function isIn2DMode() { + return EXTERNALLY_MANAGED_2D_MINOR_MODE && Reticle.visible; +} +function restore2DMode() { + if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { + Reticle.setVisible(true); + } +} + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -302,7 +314,10 @@ function MyController(hand) { this.update = function() { this.updateSmoothedTrigger(); - + if (isIn2DMode()) { + _this.turnOffVisualizations(); + return; + } switch (this.state) { case STATE_OFF: this.off(); @@ -425,6 +440,8 @@ function MyController(hand) { color: color, alpha: SEARCH_SPHERE_ALPHA, solid: true, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. visible: true } this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); @@ -447,6 +464,8 @@ function MyController(hand) { color: color, alpha: 0.1, solid: true, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. visible: true } this.grabSphere = Overlays.addOverlay("sphere", sphereProperties); @@ -477,6 +496,7 @@ function MyController(hand) { end: farPoint, color: color, ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. visible: true, alpha: 1 }; @@ -490,6 +510,7 @@ function MyController(hand) { color: color, visible: true, ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. alpha: 1 }); } @@ -759,8 +780,7 @@ function MyController(hand) { this.particleBeamOff(); } this.searchSphereOff(); - - Reticle.setVisible(true); + restore2DMode(); }; @@ -901,9 +921,16 @@ function MyController(hand) { } else { intersection = Entities.findRayIntersection(pickRayBacked, true); } + var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); + if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { + intersection = overlayIntersection; + } + // If we want to share results with other scripts, this is where we would do it. if (intersection.intersects) { - rayPickedCandidateEntities.push(intersection.entityID); + if (intersection.entityID) { + rayPickedCandidateEntities.push(intersection.entityID); + } this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); } } From 7810ed631392d4597af7076a6e93e82e6fbe332c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 31 May 2016 16:56:08 -0700 Subject: [PATCH 0265/1237] working toward a permissions grid in domain-server settings --- .../resources/describe-settings.json | 71 ++++++++-------- .../resources/web/settings/js/settings.js | 81 ++++++++++++++++--- .../src/DomainServerSettingsManager.cpp | 7 +- 3 files changed, 109 insertions(+), 50 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 53d062d4bd..e0767b0256 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -87,27 +87,6 @@ "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.", "value-hidden": true }, - { - "name": "restricted_access", - "type": "checkbox", - "label": "Restricted Access", - "default": false, - "help": "Only users listed in \"Allowed Users\" can enter your domain." - }, - { - "name": "allowed_users", - "type": "table", - "label": "Allowed Users", - "help": "You can always connect from the domain-server machine.", - "numbered": false, - "columns": [ - { - "name": "username", - "label": "Username", - "can_set": true - } - ] - }, { "name": "maximum_user_capacity", "label": "Maximum User Capacity", @@ -117,25 +96,47 @@ "advanced": false }, { - "name": "allowed_editors", + "name": "permissions", "type": "table", - "label": "Allowed Editors", - "help": "List the High Fidelity names for people you want to be able lock or unlock entities in this domain.
An empty list means everyone.", - "numbered": false, + "label": "Domain-Wide User Permissions", + "help": "Indicate which users or groups can have which domain-wide permissions.", + "columns": [ { - "name": "username", - "label": "Username", - "can_set": true + "name": "permissions_id", + "label": "User/Group" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "default": true + }, + { + "name": "id_can_adjust_locks", + "label": "Lock/Unlock", + "type": "checkbox", + "default": false + }, + { + "name": "id_can_rez", + "label": "Rez", + "type": "checkbox", + "default": false + }, + { + "name": "id_can_rez_tmp", + "label": "Rez Temp", + "type": "checkbox", + "default": false + }, + { + "name": "id_can_write_to_asset_server", + "label": "Write Assets", + "type": "checkbox", + "default": false } ] - }, - { - "name": "editors_are_rezzers", - "type": "checkbox", - "label": "Only Editors Can Create Entities", - "help": "Only users listed in \"Allowed Editors\" can create new entites.", - "default": false } ] }, diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index e17a886e10..1628844d15 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -875,6 +875,7 @@ function saveSettings() { } } + console.log("----- SAVING ------"); console.log(formJSON); // re-enable all inputs @@ -957,7 +958,11 @@ function makeTable(setting, keypath, setting_value, isLocked) { colValue = rowIsObject ? row[col.name] : row; colName = keypath + "[" + rowIndexOrName + "]" + (rowIsObject ? "." + col.name : ""); } else { - colValue = row[col.name]; + if (col.type === "checkbox") { + colValue = (row[col.name] ? "X" : "") + } else { + colValue = row[col.name]; + } colName = keypath + "." + rowIndexOrName + "." + col.name; } @@ -1012,10 +1017,22 @@ function makeTableInputs(setting) { } _.each(setting.columns, function(col) { - html += "\ - \ - " + if (col.type === "checkbox") { + html += "" + // html += "

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 literal 0 HcmV?d00001 diff --git a/script-archive/tests/audio/testPeakLimiter.js b/script-archive/tests/audio/testPeakLimiter.js new file mode 100644 index 0000000000..e2315fb498 --- /dev/null +++ b/script-archive/tests/audio/testPeakLimiter.js @@ -0,0 +1,18 @@ +var audioOptions = { + volume: 1.0, + loop: true, + position: MyAvatar.position +} + +var sineWave = Script.resolvePath("./1760sine.wav"); +var sound = SoundCache.getSound(sineWave); +var injectorCount = 0; +var MAX_INJECTOR_COUNT = 40; + +Script.update.connect(function() { + if (sound.downloaded && injectorCount < MAX_INJECTOR_COUNT) { + injectorCount++; + print("stating injector:" + injectorCount); + Audio.playSound(sound, audioOptions); + } +}); \ No newline at end of file From 2ed95542a9070ae20ebe095184e743f00abee2c5 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 29 May 2016 13:56:09 -0700 Subject: [PATCH 0259/1237] use sound file from S3 --- script-archive/tests/audio/testPeakLimiter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script-archive/tests/audio/testPeakLimiter.js b/script-archive/tests/audio/testPeakLimiter.js index e2315fb498..d56126b912 100644 --- a/script-archive/tests/audio/testPeakLimiter.js +++ b/script-archive/tests/audio/testPeakLimiter.js @@ -4,7 +4,8 @@ var audioOptions = { position: MyAvatar.position } -var sineWave = Script.resolvePath("./1760sine.wav"); +//var sineWave = Script.resolvePath("./1760sine.wav"); // use relative file +var sineWave = "https://s3-us-west-1.amazonaws.com/highfidelity-dev/1760sine.wav"; // use file from S3 var sound = SoundCache.getSound(sineWave); var injectorCount = 0; var MAX_INJECTOR_COUNT = 40; From b3a43c40a969d313edb775ec878d5312fc518a00 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 29 May 2016 14:16:30 -0700 Subject: [PATCH 0260/1237] remove audio test file --- script-archive/tests/audio/1760sine.wav | Bin 2880044 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 script-archive/tests/audio/1760sine.wav diff --git a/script-archive/tests/audio/1760sine.wav b/script-archive/tests/audio/1760sine.wav deleted file mode 100644 index fd2701ffef5d68f771e81f1dde5ecbb5d875257a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2880044 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

+i_@>H*?kwIuLd6#YSGeQ!@EuA1R7 z!Tk!~t0J2oJ$T3klxO&v=2zCtapYZUHu>n$#|ie@9<(#6K=-`VfA9^-w>PE)Cx%-+ zR#{tdikN?eTpxRvJV#Us4;h^8KU2QFO>&0j_2E5eCw&~8E8aU#B0huirQ+W1Y8{*O z;5z5*1*P3ie0+5-9 zc4-%xiyjj0hw6EG>;E9`SKH?8nARv>fctx1{2v7O1NU~Gi^9W;{~-3x?YrjJ{w(}< zc;W)&T(N(U`B&=Rjy%KmpjloI4ZGW8XT!#`Q;zxXzM%Wg;A}ImuT$5Y!BykD{Jsxg zt3AB%(c^x_e1P^7I%Um_&8QP?TN*|}bep>f!|MNfSu06a)fAC4kw_}eJ&uw(SV!kN6mOO7~o($(1INy$(4Evqo zU4qvVdC`YYyk6Qa`{8w?liXx3YTU29$84D1b$<1dmn{yK+uG;FUQ6bF;612(^ob{f z#Jhw%1N#TpEc-5eqIdx?XW;iBdjY`xn7R8Enu{9$LCi%}ALqegceLMG<=dHm1)fY^ zZRf7pgTEU$-tWioRm+YM4;h{~&kytcIFhRx#))J zUSl@U{R;0v^d{h=$NkFuAN*zbCfPe9*N5JOi%qWYde)0V zE9Cknh9nEWU7aiYwVx2Dh`p9PXFy(*&lR|8_QGd?kKW)ws(HI#Zl1~S%sqN|;@FRa zIfIdtc}(&Qc5Cq;gntloQOmQ*!js`1J@+Q2nfUGSr5>>MrF=Vh$p6%yxFHeaOdLq{ zy@J;gIb_3Yi9U|OlVJ`daxw+;a;70!h!s~Ce)>e?46Nk$WL%2A3b<7@R-~@({kG;a;C?kn_q;F{Jy&pB=IzFQ(A@JY<8r0% z{5pBVUs|=;>L>Yj&Wkn+XS;;HSK!HP4+^5WXmLXZackM{tUPhx$rRMK@0w4ZION+u zpqxy4SYaz(-;mV*>b^6)OSXNS<^)g6MH63??^n3DV?U_4wMHLjBXNp&eg$41=Ax=c zA0@qsHk5Bi&x`lY;C_IshWyn~{T{?zbnyB8$7k$Wyrr4E;V~=SGu|$I`cPoy?V4FM zzXG?GeG^W?>kFs8Gjr9jcXqV;uke!|eVeQPPILR0j@&`_cFY-&L&jXxcn_8nuMb=` z?hhg-1HLHk?R>w2#{_-n7lgCz_126#+9pgj(JsUswZtnv)5)&t4k zrvFe-fAGiC-q+vhuy)XtF>9yyl>Xoy@rE}J=;T~{ZT4sXCO-~y)t;g~j^c}=mx{Tl z;)_o7s~VR*xNFyJ@(-pJC1w6&RV3{P;iJdAy@+^yAzlSRyIOG|Z8=4F4>GThy#QV4 zKL{_tnq@~V&uR}Z{)39M4W119ym&6kc~K+J0FOy`;%tMf#^(xi2J{D2-&yh7@gKZJ zKKg3QKgCDS`Sz%)iX-90zZ0j(cFr)hdp_kE(3`kz8BUyS_QZiNy6<@4p71SBYfmKp z-MX0kgWQ`4aOovHnGcq(wd_c6r2U}s0)VrPoDBYh=%w<15OaokHonw9^2A}zpu9@~ z;`73P5P1gfdEF&m-$=c8#`ns$M{mp-Ts$5jpVvjp(fD6t?uKU3Tom4=Ui7_U9y0UW ze>-^P^!V%VbV%2G9B)fQLR0M4d4(s%lO3&JB)O+n-c8eP^CuVb0*}Y(KDR)Q)M{(s$;$DEFNwT)%w!_k))U zzDPfu+{^kh`Mi*409WmAu2&_`!2B!haXNZDD*ZvlXQ;5YSix@(6~3tQ(X0HG@-A)f z?bWL1#rdnp$?pu`1p0%^#XpE#AM-{1sE-3Lz;yavvB$)iiz@C1=IzYc&eB{p_Tzwu zY|ANP?gxAmoWEK~97u4A@E)|2{~+>L{pCN%Ts3e%kiSyCRP=Fd^P=ED!jJQ{@EMo` zi9QbBuUg0(u6(JyA4DIgO>%q6A@8Si6;NF+Juh&!IoEeKe7xWHzF;Sw1@WNZ1t(t-z#`{nfo!JE~2WO{)6Cb zJ0!N#z0|t;ncX)H9qoB3a3OuKo;CGSCpA5Ne(&)>x?la5I7OIWvBw1eLA(d8)W>1p z#4h<>At#fgzgOUXz(`O)889ABu9s>d%i|`^o!*4OSLbsacSA1_*KCcS}H>gJs-^3)!A)lxI zAm&&3)9i`gZg}F57v=et*%QZmXPhhcO>nLc->cSr9N*oqk{94^j|Yd%^4cC$LVlbr z%lAZWqx_Z1i%zHg;H<EY${MDm2dZ8=OF$p6(J{7UBS6Ry8YdVCy} zhJ4~PfPbZWsmyP;owqA)t@4<39o8s$QTV*ndvImunxgQ-m&NCWe0x>+`elbLFC^Tf z@6{xF4+d9TPg*30jQdr(DJKI?Q7-vX?JSs!GEW9QFZ>5<4qh(!D*a&cBg83cpFK}F zMf@LxZ{of9%NCEgCnEk7&|Y$V$TOff@#yMunO|{_p66F1B!_J9WQ=p=r+umT4{{&p zPK}>SU-vh(-?{bo>i*tD0p&%(RZ~4L^yrmuLUGlU&&%)vpg%aNDUa?~>|HX?ucp(v zvZuLd8{urjyM&y~1mPhot{UIl)!zA|i@nGj&i5<42hktAWSTQPMBl4M@-E?f)kIvi zxt|@aaML^)TmBXE`Z&*kyeQ^Zic>UC_a@YSaK^C>XLp(AqWLtx!ki(*%Vn5dPxx_^ zC(e&@GWZY96CZu9c*E7X+7MPW#dE|rZl!c@Urs#aPsIzseP`rFk&`KuyeMDbR-U*>nO`9<%3QU(bZ=LE zob{G`nv0f9-K{;m1?4ky{nq=hXk+zT;lF++oh$eU;o&v%SNIPihup_KK)4_5$3frO zer+Ui)zEilUZ43r$ef~?)bnaO8d4Hy;%qDKM>+9i_&tcZDDv&#i(=jmpVxxYcH;9o zB>O==SL=y?g?u}B$jCEb?`%J?t?)(ROKp?vZ2dH8aB4-)(z2DrL;gy0Apb)B!SD2( zAz$w8t20NfT56r1=s-MV{0EU|=r(MYSE2S=V$PsA+nHyl9rG3b)gsCDaSmC{MbFXv z>Mznu#oih7c6_h)>K;Ac+e0OX3=U+T=E=ZEkNXw;INW#sBK`g3p44|ne-M6WN8!mZ z5BVp{*tj7Pt~75q&l$`-WWzV{qIfOu(VT&KeYrY+g?u~bkikPX{|7M_J-jrP{)7Ff z?;K9^cI=&JLH14b5dIbWyufF`yq$esxL=`1|FXr=ax1oAULM`A zI#Ry6ujg~t!X8^YrTs6hNLf@JFIN(5nhYTM*I7PO;)SrjH;A(Go{9>;Yp`~53 zJ5kRIeP?jhxId`!4Clp{nl~?Rn#1VE0WQu(*XHV+403(!ON~D{yLykq*KS@Tc1+no zerMhf`uKf0Zu{U4UGpS|3}0%D-VaVCAAM7gt|m_$pQ~W6F4Ch1PX_Nn&NIMAKTo{j z>RhF0&NlA{F>g;enJ9i7#VKO1Whdum8}HHu;)^n$LFKPF&%kq0y!>g+qZ|i1IqxQqi7{tj{#DY+ zAn8r87hrh8VN06M^?|E)m3pbj$wXL#6Yr#4+w`jRof~^RKs*_hXHefOb#GVyL2w{f zW~LU+Is7Z-ukr?0j+^XP6~2bPSKu?Kd3!MJagc9EAIBj%Gp%p_hZXZaE4puwb4h=% z-t91+=2yHQgx?u?QO@;&&j61JIFLu9_1&Krok@FV@cMko z8;eYeV;sWJijtLap1|IKd8Q{vdKPqdYea_3wUzxV5-n zVef2wulPNvdh`=y&Tu<)ljf@RIx)M{MfQW}dHqOx9C+ezt~N}6Nb*r=VD z*@v%D-V;vR3s*~T6R|3L?x>$5X?Ou{4YQ6IZzr|{2* zW>z-T1lV{j6;};B8TfIopUS5`4)+Jq^TPa!{myR_SMB~@Y8~CL&>vi^{W!jqlQDd$ zxL@7T`$6sx+V<$-1%TfJKg%|9zEs^>`T2Sy$Q}Cchz%Io?jt-yaBoMxJx6$b@R*qQgGR0o`F8dX^8ISnvX3l76Py;W2`irBrSqc9L&m(ltLVqx*@>Wf-OLzC|v5~$B3*I+*0rp5A z=XdIPO{YB$=A!<&dF3^yoXBg*|G_rIZ|D5grm;Qg-X2VIQTBNue}(tpJ{w-2!TtD& z@>k6LXgL~K5-L6VM|2;D`_3-9m-;^C`nLF78~(iPal%SnvbzvZ=Dp;~^a}-b2hX0K za=olW)*#O@8M;T`7@IS%fb#9Qx6l3T!-{_S>Dp^)%h@iJa|NCZIFQWi=#67d3XZvt}$cmZsAGHTAi z=L#O)82uj%s(j*5Md6<^mYRC>2Pl7q{@@DjwQSAVZaErEa|S!fA!B|ODff04kNyFV zMNEu4NBzP2&`mzqhX2jAS@>7*4{}ZhUI4rY!Rt#K*M^D=ZNAd>%#11?`+IPIe&$7rSeyogx|iSp-FNw+#lRVeP?jC;k5*>Z@Tp8 zIfrcaTB`5WG4V}opEA+ZqvxCq{LbKh{HG$A`0WmKuCO2EdAsuC9PaS)^(&`;J9wqw z3+9 z;hRu<9OhpsuO)kzxQ{chzdvyxb2YDz`3%VQng4?hl)vIV4({!NF1Kv*qTF}J_v$I? z(Z}mP4)P3~lfirN7UlXvO9D1_pmSyGcgB0r;ES?%iM^KWcjo+6Z~0!?z6W{Uj{P7w zkdC7p2Xxf=cI=&PdtS(2O&U=!B_pg!czx*6oBM;9w<9mg-f(!A(3?JNtSGM1)nUcQ(PsZXsMdnx7ofkKXW@fY-O9 zA?xh)V^eqkvZ+2LI5E;XRy=Xl9R|_bAg`5nyY5^-8t${0M$a@eunK_id zVsCgFan&Mxv&VLK4|b`mnWgtQ#=M<5kmz|Ocux)aH2OK>Uop29y$SSE6%QHjLB*|& zCf~#(I^Pa&IKEdnSDbHGJQOow=-ya)DG!+DX-ydw=B3L|e&7<;0iu;3Qg@YxBZ1AtZ>vM|jHMfL3yzqwey!|!eK*BdMD|egrnCv27s-y0u zM*D6ipBH-cgNggWJ$i5;@gE#aUI5&$yh6T?{xkKR*<)hnUorPXdBb_$UeV2MNJo!9 zX>M(8Ol{~^@_B(<+k-gUF6&-S>ZAQQ=T7@v-`63NJaOITU!?aS?{OSPHx1}O|G^U# zJ&51l-ug+>GpT!WlFH&whDwjVaLNYy4g9L-83pbS(LS|{h00nMWdr{zdahI4%?e|6sGJqUjL?9$$?-h<5R zy#UA|_i}%O{5YIL zM!wzfS{nIw_VB7X1MXM5M!q?<7x84^;YGe3y;SC3fdgrrE9?j1A5^`G2_fG^KNa6% zxfK@^c`+6*Lz`DODJ!0LT z{B8P=#Am?$3Y=|lAQkr`SN4PWUa^OFLt5Kb{C0SFJxA;!zUWXJ{~-Ikkn6*L5Ps)- z#ESrS4a@9uxNPA}@;lAo2{JaZf}%B7D(* zl7CR;WWbYwCrdv9;bx8psC{FUM%gU?V~6XMdtedE~0 zzRSoPzMcGooM-TNvL_B?aPHLgvsc(zXRnyHK0vr?zQWmdwedTv?-lmWYA&kg?d%QD zq5PG1$XC%riGLLxc_Hv6&rL&TcE45cNcSuB=*{38xzXGSIv&mz^ zIT`kOF|QAN2Kc4>6sqpxkWf557)0Wb~c+KbWu}O8BC8YWypQ9D19)OJl7c6So%r!NHWjx<4=a zH_xp@XLhfxcly5hXkf|gjgBd8bq<-m0NhJ8zE?M>?+jiabGGMe9x{BX#vUh~crsn* zUs>|9#m;h<=I!j^eUtpo{qj@OI<)E!?scdX-vsXMM~Tm1K38)NrxzvCf6zbCZq0>F zekPw6-h(`ER~{3*2e&tDrW~?yuE3MQT$JYw=+Re&uUmGMJaOy8c9?Q9-1AaCuilzl z%RMjT8C2gH{Pv7Nu zw8Xz%$GuKGnY^J>yEoJ~f9Lq2e@Q6u`poxs z=6=A#i~rzd%VF{^DG#rjGd#HNrKCr!70FfU=L&8fyeRp0@EO3_o*?=5ylHl$?FKr_ zx#~;&cHG<9=Y{tmd=qMZHJm&q_+C}gdyx58;K__xwOI47kZ;ExNAX3mALKlP@|dW6 z`#tK>k6#s^nOPKi_?I)eltVW6o$VHzhD)QFtv6=yQd=^S{Ysf(#IztZGR z_bc4n`CL7gv0u2gfi8WtkKV{LyhZ2AN#+bQh^xjt8Qu?a9|yfu#Y5(t4DWG5Djz>o zQP@8tZsm)@>x0)4-z)Y4s9YcVgVo}RGkWwYFZz2{IPtGM>JE16Go-6WzX11$332Bv zAIAS0Qy03`=cnN>x;E)~J9-oF4>I>d{RiQjczFJgOD0(w5*lN(rAOah<_vxFGt->z zAIdj@zBA9;L%s5Yc1d2eC~AkCtDOy*XQv-~mHart zi4oRu^d3at863zw$%|rsg`O9Gub6+8OnK3#v^N~{cHZOI4{XtM2Ii`*o&L(0wS(T3 zUMlkK*bk1AJ`T9G$`glNpW;oj0AGDn2jVugpB;&g3zfB%ZkE z6B-w7AWvMC+itmE?I8{%_Bb2TTq!5B!0F_Y^9$wM9f@1Zy;S@MbB2x}USA`5!#|6jqJ0xj z(tc3guVPmY$T(0q@X(yf8#O^Ly=^?a$`gkn;UycYJ^N5rjVA3gHzxVM8RQ|0C{ zV%L|E z=wGLKyP99!-}3^u7Cm~*MGa2Tar$0ie&skihk6tC7EkI;tk-GU4E()Fu_zZK2 ztJW;@E8Y)Y)c!%7E6(+)zBA5M5as&VM-Pw5PjYX^Ty#RnYVWvb3n+(d<`gZH-b7&K z9r6Nf92-OVs|}WI3C`3Xd{fTV?lGX?;9oGj=eMQ2XSxbzB78M@TKy9@V2Rsqj-I)=XH+!IA)It zdR|R*uE0ZfpgvBh$?yDG(qI#38~;Ibo&j^wuV{XSo)>&x$TMKh&>&nj+^>xOVBzvc zOX2b&$su#DkNtz-Kt@bME`y2x3Kd5*z%Zg<6`Lgy}eqr+-#9UN;uNG1MYRI90%G)(Tn$KX~J!zBX6=oaXf} z9a%WlZLVExq3}g9zd{aK<*y#kh_UgQU@oe9sq8Unq+B0z$TP$f7fAa-yTpT5SLzQc zPaHhFk5b=Rc`dj4Tp2#fwMpjf$n}j)aisJ|KPP2j`!szOy+eQ%v70 z<>AF1r&PQEd$ivf|G|G*QkH!$=ZblK^M%i_$m(z66zyub+v5S79zA%-*gM;DKLQe? ztm7yrqx{a`GoY7h%YjthrG&D*#Qng35czh@uXa)X3Y;SJ2OAT-EoYbXoWFMZUm+0 zgI+58CK47z_~wj#h~9(XA!B|OOW!N-ka_Pssi~lMJo#~yFBP0^=3m_wJ_FCM!0Y>s z^6in-^Kvug8Mu$LmGV~(cE^cR)IPi0y0J;Fvd6heIpl*KQpxY!eg4HI<1O~$1wfvm z6ZIzGwdDDg!GSb#$Z28ri=Rn2ZdtkP2hCN>Gu^K+Z)dI=^RHB%fqfIqDGJhg(P_tC z+kItIv-s$dLk9N)-f-rAV9vnzcDx5U&k#}_e=>>YqWBNOm)cu$GVN(Dns4%$pg*X3 zso3NESWrdh3f?8;kRLp8>f*hYk>ZK_E#|O^&%k?}@T!AH7E=DIT;H!8K5YK3p?)fH zKa9Pz${}y-J*DY!@(*Gz`iXcgk?VtpH-LKdF}?}JzcPGY@bL0`PCrcmkN#Ed zo8bIay8aI;J_CE=*p~|L65p>9$P4h9_;KJ}nk_sTNAkpBe#LXq^y!nvqz-zw!vwt_ z1g{T$XYLQ4yx83G0`&*)5U)>pm*53}CoZaLAN3}%cSgQFSbVAAKyDd2%JX7iOk`bL zvt>y98SyTG*T?tvs4aHd!&@$1fMEF#CM-B?;$Q97ob7zgDGH*#^Vag4I^X{0)IM|V zV~Y~jSUz03bV00mOpw1qj~;nZ^t|j$dC`L2%b#88?HzJD`Z;a{9f*Gg?^1H+*j16%Alc(gKbCQ}Sl_RT zhy%G@<_r%EYclPf;RS%-nfo|b&J@$-jLSL3|>eju)z$}>zDQRTKrd=vX~o=p9m z&J}ol|Eicr??JOS96aQ1#8pd|JcGeiQ+x)+lQI7XUzYbEyy0GT3#$$sSx|f;c?Qhe@x5B%9Zx+k_wxsjzot1w$X}I74jDNa^ys-afw?I5 zgUBKC{fd1Pm@@>CC$6CU*Hg|{UvrA=zjNf9Q~S=fi!Dl6Yx1Rn*9Sg>!L8-q1bgD( zF-i2EWXd74e{g#D`uY~?;{;333pp9&uVSiRHtli#5%;s@VEnC^8=+gIH*ws=Rl}SC z^DBI>aIVaGQSLkEk_J+d?z&V*R{T}3=S2}rkx#u;>?`!H0=G6WoeH`SF;iHGo z3my~p#Nj=Na|J(+x?dURO3mBN`@v4s$C=`HCOmD~VaxLgO~hxwxq6HIIOx$UUn=(p zncvrhhTr*v`12O8xIq#913HS=((vOP zu(nt`qy%jY)Lb>*<1CE4NB1k$n?Ns>xgUn#8C*5+WY{;cN&D#0qvvz=I`P|)zY5HK zjd;lT4=N5M@>krOfHxdH`uGJglyAqqJ-G6TL;DK{5?}Nr@!R2}=bVheXShq8BHlZz zyeM-&&`WitxhQ)~&`U*L)R?#P-uVT(U#Xtg*rdKR7v+5W|HKo)`Xu=ntxMWz0pHCo}(WN>NJY7`b1u#{_dxaMi%s zM$e1q?K^@3g)a)<1h^j!DM2Rw6?4@JB-aP;lJXD2!&~m)U%m3=va$|b>G6!x!lw?+qTYn!^{M_~-?_V{ zmJs)&-PK=D6_oqSyghJ*-+JHN?d8AGd+>D=|7yMEu*qwwd|r>4d=mkC%BVM??(O^@ zY@$8RbDjl5Um-tE%h8Y$|BaneTCCMJqF3TD3@WGR#%Oyd6C+#r?=9k4dQbojK2NX;YK@2UV_5uD~ETwlg?&oLQ;4haA1V!_w?-mY@Un74CY zRPm6(DZ;${0QCp?e#M-kwkfkW&a}z5W8PjLcTadS%0K9C@&cfb1HP!bw_}e3j|rbE zaJE(78TaORmC9c^ z4s>=droOZ4rGiuRw)PKJ5oeqII3H-gbFSR4z!zPe`O>NwQ-4tX2Lu0*&#+VbaX8Ol zc$e~P+naL8I9JV!)`t~O8Bh1CQ1Zlq&+uRIhGXx{ejIzv*=`oUGxpAWzha&Yb8Bty zSICR9-}(LcYRf<3hD7uwZ}`D(mA+RC`@iPoO#MNB**mMA7ke%Fd&T_rRLlF6XV@Y>FU1!% z_vo3ky-hs4Ul0!&y@_h_JJ0A|XTyOs`<;#N)e{jOvUk2oc~QN^@r2m7=feiJ^ z3))S+3EZ!cLss)E?$I-!0scYN^Kv2I1p88Pu5iC9qB+CcbZ<{6+o%778)$wNB76qy z2YG&F+}oLltnycfDBtd~u1ogp(w>yRdVxG~cS3)O`B2YAJIfviob5>Ai>f^i<_vd< zvkhydWLZ;^LtvTz`glYwtS@njZLzTLORboq3Ogo>$4# zp2EL6Y{O^Zy)*Z`l+O!2FJmss=L(!{-VY+*-XeP(K||*(Mv6CXzAf%${}xGz9VXf&HRe_4D7W8|7zjk3ulUJow^p%dr-|q;o&Wr zGG6$iNyOP6B3^(j`VT4&WHb4l@5MIKy!{~?z9_hAm|wYB$C`Yp_H=KDZ{qj+@}ls> z?K=`t{Cid(%E`1%2_b&FqlwR;@>jTDajtJ-(^KTNjMkhYQ#~)<T=5Zh1A z)l5@=ko!33(IemPM84GBBX{cWmEvsoa1WuJ4F3nw9|Wf;it?iTA2j^VcP;m%H?dXb z?M8pFLV6RW#AkpXr-MiT0QZQOMLgP4or-p;+$Ue?3po2a4r)u9fl z(jP=#v@zixO zuy{;nIEB->>PzoI&ND>$ZW-IdeU?joO+e*SlxN`mAh;jMA%nAR>$L>8);5QHzt0OC zNPZ8(H^Dx7K3B{`{`g|I6XE1b9i8-u_IX`6J>mMH4(n(x`T+Tz?Jc)r3+LrbYcsk@ zya4~ExhT870zc#lv!WNjI9erw`iS zVe<9wPd6O=w%}~~zT`)2zE_-+F}wiCGxVeT72n(0yW~oI(e^0;8)ug^9Bupk&H5RX zXTZGuZd{`UoNeSqt$Hqs-h|`Yj}!kc{XuwmZ<@}P@|dXa73UfFKgjbd@UL>{Kj9r!)|oTfcBJczn`Eb0FQuA1ah$(G9|!p>e6QdIu;pK|-`StIAMiV~cM0<=><7OW z9Gsb z;K>|1KFww>%3QVj8h;m8_YCT#9+Ceb&#&g|{vh@^ftuF`PLawp6j6VWIgsd0u;1CX zKgjuZKjJe?G%)SXVXXqk(XWru&`F6D*gx~p_abA8u&>m-)aJJb$sCa$o55kwq zew?t+c8P}<`77Sz{84}K>+}P}7lj{3%|+qiRqsKSzv}4Ge@JP!Np%aV4jhTlzKOOe zp&R{5+R(lI)$X}NpZEOP)OY55JNh_k&fu{21nr%l$k=OhZ%1!}xwV)xppRqT4|0Ey z=c4L%X0H2WGne(Ff55i-@`@uW`0y6FAjSLx>q>JFZx_aHpH>C?N<|8a@8r7594c02tC zdG8#ibA3h*nZ4ocqhGK6&YWjppBM9x(W8goS@C2xp55Ni(xc0;Ag`jJLh=t5h=&)R zxMJO#fR7$|25aU9*$?uZVUpi>;VGJb1s*c`IOwIaALmozs)1XJ`4#(d8b=jP+cK{? z_HM!?%ef^F5eG6&czwuUsdLqxczy6%a{g+w&+X9PV&1noS9`lntXpXF9>l#JzEtkf zbAJ&0cJ_JskcXFjUI&iPAiwjci7#0ftsIoGkGz&Qr9a62!E_sLE$^KTPn?mH0ap$A zcHG+?Vha+|Dc2Y2yOlg~zt@CS4mwm`I4C1#400wW_EWIs4L#A?Gs2A|;*;y@0}*k|hF@E(Wz zgKc8_%-wn4esC6XwiUnK-jr`weP`s``9Fx>1UzvAGh$c1bl>+%y$9LnwSc_gTd3#d zpn1qniRD(e)iqfQir+c1pvp@)+x-Hbr1{l*@wG8GLUY9DW%zM07gb(>3#MG3nls$i z-z&xa!2HT7*-7RMsz(p+lG@{7F8V|G1XB)qo^U^S@60*mtg|~BT6%Pn`xSV|=uL3G z9rt$S4LAP>Uq5`sR$|13w?pNqL zt2~3@A3P*I`fJ07yE@wNWVknhzBA4h@14;fys%^#w)Uq6l zc*DPO8$V(faf<$$@B!@yz5J@idZAo^G#lW3F04oE}>3gBJk*LH6TtUKD%h zUo@}p_0sm)4{9Frg@RkOAKXuS=QQEBa}F7tZ9B?eslKxl-LJ66Nu>GJUin_J*AjdN ze6PTh=}5Ug?hi6wlzsG5k8RTTD|k!_=|8xO_zazOZ-P0H{&cQVGE<5c9KLcUpL+CP z6Q?L`*?Z&#$P6o(GHJw@;&+ZGUn=i$RIU&Fb_ba=)WlxnjRF{5Zz_s^8p}#YGA0D9?cT)x+)~#Qjhn z6aEiIZtOPL0e6LgvIhwrT9^wtR%^_#|R8tNa_jdFq z9+dsyH|ggKYPH7%ejMbGC(>LrN8W?z53=7`?VX>Z?^VLd+0}13d`rCvHD_R6-Lhej-D5LOfVPyn7AK5Yp$Bg_1Svje#`2+`W?#2 zphw@V=b|G$eJ?%=dOL%Q?+wK)(Ib>00sdqVKHy&d7^0 zrwDx<g$24w=16$jQ8D{XFsBn%FHf$Uo>q z{44ke6;CEI@^auv;$NXZX!yMLTK}~AT2^#%x!kYZhIH}hD}J2!$w$vzH3zw0Aup=- zgSPzkH0ckje7ggAmzev}oAx;Etn9Vqdpo$blSb^Ak{af?c&PYN!BqpdHm~+4%3rO? z9JMOeIy14w3Z)dM1b8EB68-8Jv!CD9;)29=s%bXLy$sp8@x)hp0z?Ao-j0bJQQ4r0-XlUrn;y zBOdbhX)UABqc4$OY6kTO;W1(F((LNp4&S+XXm9vYOUkkz=v?JWe~{mUW^OI#+c|$# zD0xxkOXYI~j|tD);fXW+&V0W*GtS5F>+tkthb%8hUKIIu+}rtkWj%wWyP%C?9r@&f#o=Az7jM1K%DnU62(P&F zmJi~8qkH?W^d3BRvGzzE^5~Wq9IBu7%Lv`HeJ3@el6P zIb`$)*%S91<&eJ#_tE@z>~WY=1m6ViS2JWk*hqey?Rt+>7*!nP@6~TB+VH(&%mBIp11#gYFl|g?#xzvQTFh14%s+YydUKL zAm7`uAN*M7ke|A5?|i?1Fj&4<;C}o{erNQ&%DO!|q?5b{t7*<~gS<uG6NYyQ@4>gO1$}mc@(gV$-;TLxqIgWI+{TY6o{~zQIJ1ASeb)jxS1K>6 z?(N{WgR2%y-z)S|x6^)*xgR^_KL{UvAoV7asPD}D_FVFY!;kZ!<++5$MVVpwQ+%{1 zE}nW5r<2;qxk4}1xL@VXyB%w98835DgR6%B;2E92V(-$wDbIlY;LXq*F^A%>SpE_B z=ZM}m9LQ*zU%_L7K90FZ&pvvbtH08FFwxs0JQ?O}vwtv_ax&mw-Kd%E@{s%1u~G6K zM82JQ$ggYuRZ#9U;y`lGE1T|D{=#Q4_BhKHEFy3CY?qpvIhBtcs?hm%2l07x1JLizU zNk1#SROChZJqVr*&Q%`yQtu6LA;0rS;-hCj4)_f0cdn$I4CjzBZ~q~DqUMWYkHhn; zMdS^?r1_%oc?HsY5cziHAM8TD3Fh@(*kn&RnK9&}M}IJ{wo}(_9pyy}=G`U^B=fJ37lnWD;^~{zOI6=1&dGdC9uxNP z^0{KaGje?u-8{$>XU_HApOZmv;*Op(C{9uNao;_Qw=}N#Jn==zw?8d?XXe&|`++$F zyy3g27LSbRKf}rK>aA0o%R_Ugt`C(t1J6adms&h>H}wY#spl0(->aUKlL;hl?ZAv> zEB|KwJh5p_jOO)$TWck+CHBrOaq}X7rag|q{ZM@ze6P^QVXm6%kS^rmeL3#3>~T1M z#eV0o(oWgkWZn+{;4SgdV?VfI`UCSXE%CD4Bc6i9GNO*X`zhdsk z9-VKuTRcoS+vs&GG~C_S@lwH)%$keJoF{c3&bgE)O(zG z@`eu~j|sd0|4kfaT`FFHAj-)oP7(9l+s5{o`_|Orkqi6JaB`yimFhd2bI8bF8D2~D zaV*5yepqsSPai5Rd|cnJA~dfr-{b{=FO~O$DlcmPVStUt1Ucl0DlfWUb)>nd%8N1& z`9;@8y?4f3w4e5+!Z*R*CH9zjS(@ZO=rFovfSQY3h5K zF0pR{bB5`}DFP3<3-Pbaxjx*lIM??Y_2|L-7fGVFKWHu#%y6Nx8d&h@E0Lp=4J zRnH6atGVLy0>3?P-W}7t9rt$SAG}oXCHd(8ne@QA_Sy4GyPfz%JaHR+?u7mt^MMUd zrlQ-+b&IR^99b+LUWdf?vLCenUcCpA7rh&IPdp|oy%Thv!FNyO7DvrhgD*9(vX1fS5=8TM+W7Ckv6N%-xG1Bo6z&l!-vf_Dk~LF5^bze*$D1o-XDfrO7fyF55|8tt9g zN6#F{nUWX9eh{1@^t=|4A1A=2t|p{%5Z&9=esJNIz&-m+_x8ylpJ^|^6%%KhzgJEv zPQn+BF5ag-amIV_-T0f7XSgD~z8)uLmv+hSOu4@IDBu43!HdGbf+tSxaT@5}j=3oQ zgLn`2%Fod6L3o$2$JyiXjoSpuGax5p_D%49Fzj$fQ9|bURST@anp>MmerH=>Dtmal z3~T98(6Hs~lzTjQA$-9t8jDEA1b|oMEkSAl00KJ-qFR1KE?j0Jyi?OK+kf zwk5&Ca%D+p@};7eI*H~C?4vjHMURos3q5-FrE;#XAN2>p*^a0>cw~O@jjTSa-?cVd zJEiz<3@K^WK6>W2D;_fLS5}%|xzm17aX*k}KyMLiV^l`q79!7qgaPgQRe+B+k za7pk+C*js!qx%)+qToP2;NhxsGT_#3@%fp)SO31)lk%eA6!nw2sLJ)F5Aq(fetLKD zrE-6;ag_bQ4&?LtS4AH^Z|AuvzXv%dvp&pW@iPg>EGg9U`eNLU!R<`^E9Cluh<^o7 z9CJT-&cJ&da}F7EhF58igPaWb4Ctl8mkJIf@}it?XP!(Ny$3npZshvVn@H1nh7RP% z0awlTJ(xyb%j(lL2QL+zNk5qEW_>B?p>-Xy=MV?dj(qg+S|Zn{d|ocnqmQgA7w!jo z6ZjAEyuFU@SED^IQvOQi`glL6?(I=q9M*oEIF>j?Zx;?Z4|GmGX|m^1Lboq2uC zzjC-*OMYk0$-p;(J`SHNzF*zno8WT=zUTnzrJhQ(qkQ|p~VO`V9VL& zxo8{8$zXnEo{JiLXZAa@&+Frh4^l6c`-41huRC~NaxxQXes#&j>$9i()o$t0!yAsg zD7==I|G|^_Hr(6qYuXQ@=Vj!tz}ZIr3fvFeuQ=DoIb@tGc*D^jRP(Eg^d3a6kN3{Z zDdHaex5U5tYeMs)44Pl5JOh06=6U-u$&2#$%G}4vkpCcZeYfb|{;v23ale9xx3T3# zoo_!B|C8k(aZgL%S$P5AiF{Hq zJNKRO9}IDM$USH5Lf^y%hivkq$Y0^!&fE``>w_;9`$2!F`INs>oFecUqJ1~%-o%rK z_EB$QjP(=Ai{f0p+O)NI(z8k46UpyR$@4QRq4F89(v+?Ws-v57>BqDYUk<8hS zt=Y`xm>n~YH5*BiG?FfqWJHRP`g}@=uEbbMx{zGVNOCpnFmq;(&1_~fJI)eeHbW$$ z`rRJS=kxV`zd!N&{txfl`}KM~pO44=o|V}q#nyaU`h)mi?Y8+|INOcFDdO)+c-}`r02zRhS9XgX*<$Q zn_t0e*>ib0`Mko{hG=<)kNOX_;PrWjrFbV02lBdS2imCsMv0=OFu?v3Jg+UaA#+ zSM0~pxgT?gtEPB;@5#I!{lVV{z2RzOb+o$s@u@pI<~pT~NOsloqSf@jVy;@3nZGU= zZL*@iGkji+#8tE8i*nD4`J%|Tm-?=Zu%mmso|9?qys7r`*;$8HmZZoz$bFpgM?bny zMBESXuhjesxjt}f;qyXXG+=x6I_nhyNpX_vn{}w>Y*DSFp*Qil=~eMfwCqi&JOlFW zsziygt;H!)~@%tP27(wruX9Ag(tIHe5sg=_FBH1 zzN=}JXRsmum8Bo&OpH&M$$N>$*`7juXZ){_zry`0jPh5^DNsUYP;}O8LCt zwS?bU=QBi|8&g+CJ}>lf@Lh4f9rvrj`%CHNd5LAF6X9}iFA+Y&XQm7bZ}^q7GY_pONu@kPU{WJ_OyD0ZX=v)=G$6t& zKXfbQ8I&KVJMmb!X{<}oqm+h0#?oR>|xzB6uRHX$`j zPQ(*WhI#f?eK=P$E3dwBWYuJTvtJHx}v zejLo(;a&Pxa>%;ZvQEoi>E0#G8Rn562lIC1uilmK3UdbT;~;+(N!}&qY-8ST?9uD@ zcI9TNsBH1@gtMJRy$SSj z!0Us@1UY2BUlqvzs$g-++>+1`uZ{+8ZA}XfB<~0D9`q5fr)$kJN8rBJ3l|?uZFoO zJaOy|=XpElMeo)7cg`n1!_4SL$@Ptue7ld~9Ay6>ya0bY79C8ft+jpDo zr00d+1iuG)kHd2YM)!yLqns;s_?VWi)$p0($ zc_H84tobJ3cXlx!O8GA9a^daAd=uOsEY$L%>fWAB@4=JFFQ@MjuG%i)Y~#BcC_Q@g zoeTRqI2YAkI~#FmRY{WY`m9z|lOJcTcuc~?H_>D&UR*qPb7+uPC*k$^9etnnIPmZy zCzGZ5=$VI%9{myFY=c|No;dzry_)`2ayzqC%DT)h!r9jM&UJA%rgu$O$v=p_GyYc> zX+QV_y$6v)*8R@#@UEvk1HP*f@h_9#S@m(sDA(7EdK0bb9JD!NMY+D*{zE*kXdFoH zO>mw8@4?9iUn>4r&mWws`3L6)n52)x_p48Y*9Q+T&cWa%wwi|*oT4fAk;K{V71=x= zJmi0A-_=y7yYyY*J*YU_*bidfuJY|Z!hu9i2K~YO{(hYw5MNZy87eIPS9?0S_Uky( zCFqqo@5Em*eM(%lT%Rk0hPgg${_3%OJ92&8$3fqjIYsQ@#sA8hcrvBer=9vYaklMg z-p+j-H5c_9yxBKH_Ri^x&RNVEKF~PZ{2rW0ejMdXEeYK;w}?Ev%xCz^fch@m8dlN& zs%BlI@ML&D2)?M=52EJiA&eDp;$zhYmi>ZK~K8uuo6el^K{PVdb_KKAPxX-^zT-aDf| z80K{Q<}~sy@ppweLrl(O&AVh8m`r&FH$@%^7|e zQAj))_5!faD_G`NJQr=57gc?nrY^;FZ-Yx8#8uX>s*QhuiV)vu?=Q2xqic-q9L zwRtzQEc##6n7}h5 zHuY`gTuk3p2Kl^{7a(A}@6ntKg$-6++tGV)JLMU4KaN$`f`;`Me2z{b4kY{N;fael z^vBuU+78YeD2J>(aW?bQD9^w-WZh$;`Z!-!Or`gr>JMUmh=uaFm2`77|-E&Vv? z(fc;$(!AX_?3DBeqlo*lzW@6c_bWS^UnxF=C*>JbFO|LFb+X54BHsk~SJ>m=f5jX~ z_V8jZx_Y9s%-i92F7$64YD*py^t`$jtj)AeX=^?~^LF&;;k9J%61Zvu5|7CIiaorl z=fyc0p0~qCkG$w3@}(9^o?+u-oFd$>8pP+N@}kJMBhSFSiO)^1C0fraBF8sAE#;lX5nlj&w%d=|0{5|xknEk@*LvUzNC3AdGCySJ9skeF+u(c z@4?U(zSOytL+(xfLCmj|e-LxgL9TXIpI5sapGM!+J8}+!`|-ci<8JJB%+~H#@B-Aw z6=)t_b#G@6FZwv;d5bB}pn9p`ArB;eJLhBy482tI>UWn#KhYD1xo95sotXpKaX>?t zEe)$KOgien{mQyVZSTzIU}3F;b8+AANB9Jur+ho^S3Gb3$!(`iXiXCN=($G^o(#CP z(@&LXy@{3_NaRJ)OI3Ru7xR1M^O{b+RP>#(cSa7G=M1`s_dVkE@tk1~<=Z3H25fD0 zq~Uy1y>I9I{%?9-3Z5I?KptMa2a#tebn$9@rE1r)VDdXF|DgI`!9VyRaki0@DM@Ua z-z&11?(MqQlDTRjOD0mTukK{?)ga0tm&$uE!&JF&(X535tB5CqJ`VexjqgFsui&*r zPG*dFc<;y0iM|p%*mFaFf9el55%+_+wZ?q=Zt|Fbs|K$ndS3U#KlJ%S-h<%Qvd4ru z+u*lfK0S(fePf4bPVA_80XAy(vhwhPt7gn!ac=_o z_BeAulJ$yeiyU%@=1ax@sw6asINRXKyiYxP=E=Of%tSmH>~Zw_)zU>@(7c^1DdKpu^0y6W<-awojSkIO1Pj5U(ZsCg6$V9{qL6$tYi{ zoAgrQALPArPkIk>{;E;#SGoNMdtTG#SKr5sG3;^BA7sw63W-`l|{noqveFsHjWL(aTJxjuN}{*z?8GAbu@$1dTw z>pA3l)5G}a=!?O_JwNO}MLZ_p$vC9iYJ3LVui$rf>(`EWGMF8s?nlE1?Is#Ls7%z6E z7I_Bd^`Y;~-xcqj@xS7pR}c3$HDvx{C3RS*~6>2AIyP- ze=vVW5#6uY3!rky?8i}g2AqTVU#-$OMJ;)KX(dZn50{=7@(keC8u{&(duR1sS-uBX zXnY3Fi}IWS_bZi?Q9K#lN3ZX3;2%UUwTR|d?+FJIUdsbAXW%`Kp1Dna8j)ZUzS z=5NApSKJTf4KH+wAGq9mblBOL{=#Rdk#mrJsZ+L2qg)?2+t=63BW^8wOqlzD_aJ!4 ziUZj!p1Ah8?bF^(?oRtb-5ZX)DE!XgY%5$vXnzd|494)GZ#S@h^@Wk0xW$Tq(&kq&Vi5?7f%)pC8IPWOmYw6oYfJ4W_6 z;EQtpD$m7i_+PQl>n-7aG@rMz@DCy#eN+0yx>a(rwH>ao-@Fgs(M~H2V3&nJ>}k>Onc{-(!WV=Meo5L z!WV68@g8LE2YYxwbsTx4=Jc(7)!N>fJ#osr#J!1|22Wgj;WOa7;(NQ!XRtb=@(k*J zbzge)OT0}3H@J)!PaN{?%&p~lJLeg?xv%qx(e79F)OX%KWESy7F>hCXoX{5hEA%FG zzcc4WJG)O6?-F}>b^evwJDXd~MW1wU$Nh@WLG&g(Wsk#LH6y18IT`o|dG9>Y`#9@)DhxewWjZ?pVpgDeVnX_NAv!X_^Byt(OL5F7SSGuIot5j ztN9i0o#Ba7dC@}Q6k$KuUUD+<4_;mOkUS>u(jEugTKFc=#{o|Udz{x2ZRf9y*do3O z#esY!{Y0`O-LKSlrMMsHO~5x%ML8KC@>*g)$i380Q@WZ*CAUrMm>agUJDr2CyWSYI zmb~HEmXoQFxcs{Ay6&znSX+MaZ4Bms_Y`YnFGT?q( z*Sz8AO*BjXYJLB=XfDcJHM|Gmqc3#vk?$&oa>(zP=PrFAyR6uqd|shW-P}L)m@mCl z@EMS2fOl!4y~DLXP8Mi;oRB2}89`cJ6uiDIe%&K&g;TUh@>krOcwXjL!_8kM*{zJC z-UM@sz^(P7yyzp%8*b!_g8PB|73K`FrJq#Bof}cNw^Pr4?KJNa`%?9s47`?@iy|j; zPsTl8a;Z{V2`1%2CzcPWeZIP9YbpW#V)QS@;(giiNrM_jd!F8Ce|++Iy{hFG%) z&D*Dw&kMd(&dGo;`gfX(vM==$gC7UpC0p`M92L&C>ZLYlbI~k$52_q8`zAQo#~es2 z@#EBMJump^!9(8F$*V5fuy?kmUg}hui`G+4=E*!{+}kUT1ronq&99U%6?29)9`geh z&pNa)OSm7F`73Zgc-|h6F@<Xce zecT_+?LV>ey?PsMex>FNe81vz&`I`#mOXm(Qkh%Zk@5`S)>hGb(389XKJ;Dvx?rrS zA+a%T{fr{vw{O$(?Ywt>iMSso;;NyKqjG(f)OSX{y}-a{V6WxWqz9?j=sgH;IQR_V zUIn40+Wjg&G=g${xL+YJdTCv=aBGo6W`28Hy0>G_kV@PSoP(v}O9fBHq<5v*;(WW#{ZQU;cmediGv}}P9E3Mq?|Io;aJGBW|7sWQ2a#{b z9tS=zaEktny-VID?ho?)3jB87JM(v?da3ZGURm&-$y)q4*29{fZA*CuqaUYl`fllY zeQfAEGY^^X?FE#-LSA&Gya%U9-`Q82Uxm^hXM@b!;SER6t4Q<2VJ;d&`SwhW*M~k1 ze^GSp-ECqWT_3<&gC}1M;HiO>Fn;LU~d4O~4BP-vstJFCHu}epc>R z%`0NX6X)Gn*^NJn?t21p=9L--P4@mzwFIjjp z{J#RfoxR}~qP!^A*VehPwnp>B;oh#l2l;-5?~3OP`8012_UbsGiTZ=S+WiXOmFjsF ziZ^^r;D5z8ft*Z#bEwvLHs%>7@2DUiGJE2%$9XlOLHan%>w8e&c;4>Fl&xXJRpXot_zXtw$IIl$ zL0+_$JiN?9?s9wvachSs_cB)qUsSyZy~$$&?-K4;8)-k-{H&uxDc!G_hipfE9L~4H zyTtsf49WE=-vl_2-1EvPNm~7`_;D^#u8(^Y>OBZvpYp_Y9Pm)%i&n2|Obtz%ZGKOC z4`Pp_<_z%T4AgpFXN0SUa}d4>2gxBHp&osEheyv^4P)Pgm1&fzT6|u}$#_zp0skxH z8JM%J=dajnse1wV-frYyvDcD)sqhch6IV^~uQ<;Dt{VI}$RY2M@5`S#tkcg{C>c<*bz)B@u5@%<`ztz)XS^d`{rLT>^-ddHEyEb{Fed@c_f>e@s( znd!uV#CNrq@>jR^T{%7S#sTrscOtGD`*C!SN%*Ow6+OxC3_b(zos%ff@PnJz;LS7_ zeJ$~@DP8MLz`KMw19HgkE}LZzCmu5UQk5r;?^jLa4aXj*gXFLLj!xQsh4>6}XfB#Y zd`T>s6F3LCH^Dq)^`nLfT;f0?f3^8~+Z0x1tyLxBYTmugooT8>K&a@vyUR3d~rfR(jd{@{HDj$7TNd)bk zjou~X`p`>d&Ngy=$tM!Zc1vEA@9oSNMc>(OSgwE3jQeqq633b@5WjsaaUi{j&!GFA zorF{LZ@IU#AE(m5{Q$4e^RfLPbJdtr^bz@;;hO-Tf%EMuhg^1S%8rz%=XN!fexq#MZL}vr9Pxa`nWiHD3E5(xmSFMSBsm#A(9x}eG*2I(P zzWnx@sL~3Ww<9kKpO^cbQSlc|hZFvYy=UO{@%>8Y*7APP$Kazk^4obo2!8vQ72OPc zhP`=lWvM47*HqY?cJmorPjXwcRmw-1oeRFIn0jjK^|lU; z&)U#j6x>?Q^~_CI29Nsfyno zLY}z$ly7J52j@l4#CQ`Ad83PW=bu_H72J;y%D2xANRa)YRh(<&X1|Y# z1Iam=sreJA=Y>6v;?}-zKbyE8u8~%8n~1XwPaJw)mhVCMQf&;mz5*B1Kr`hxe#Zlw+C7uW;U+UPwg}$pItmeNa{C4&as{UX`$&%F`%VNw^Xzv`b zeS*f53Dtb`d~Xl+dTPM^E?XP2g@1)}a4yY7aSkFU!{;D;^eZSY$~j~;Z&x{F-D}DD zcKok!ze27L-<5HX!?`|{7kwwWy@A&^MDwL$j{^=Q^ZJyJo^vvL40~tx52|@PbBex+ z8Bh1Ce3!A}1yDSh|BHu=|J8QCEyQO~JQ>wX4a^A0FE0Q6Wb11I!b8S6h&+SdADrUU za5M1C%LjLnA7_|wKX`t{d{O4NTkdfINOT*0ZtL~`iy;?O}=9W|3H0b zoP&GvUQGWk+1l)wl9}1F;7G-cQ$^Q1I^2J@)v#Ru;u&>u&GKDk5C>Avi=saWUZ3(> zeo6eRKL&m1b3gosc;YZ;Q2oIenzySx4&H;oat==Dyn%YDv!Wlx-=`ch^6k3Eg!?$$ zqema-Me5_I{}rEuDu=v%$OpZHh_jtj9wz)NaBDdy!}%-TJFA=w`hzNmtb3R6UEv&r ze~|N6iw7BSzl=p+FB~gd25Vsa{hOyL}U`|mw z@vr#1LJk@HD`URhvX9e1z0}JKIuKWFmH52$x#-);ouuy!z9{}z>wWH8aEg#;DDN~% z@(gO;uK4YZ=N|~aoq2tnZ%2~oI`$JaxyXIx0j`ehqvIdxhQ%Qn~D1Y zz9{EKc`mAY^buMPxk%#_@i{nd_{u&9Dc8r}75G=fspp0J)xX8(#T>{=nu}uI-f?92 ze)~GTNB1k`iBtJ2?49A8VD3j$bYpy->5YW1&$g;C@^&9Z0wvdoz54&y_(#G%tXfU#WRJdJ~v4D6SfN z!}Dis^tT}oFZ#~hOT`|CdB_Q6W{oGa$#+#mx{@n2W0U)kS&_>U;*(qi0?p_vqmT;Cs8JcgdJ<$DE=0e0_aD=XJzGo=aYU*Al+h z{LbhPvUiEUE1q8&`3&I6fU8zOJ$hqrf^*0^Po}W^26@9jpq^Jx;uLX@9`C{K?txAX zH>aHGEBnErbjsC#`;a|0-KF$}^$8jJJuio=Q{tDa=z30XM zE8MU0YOhOP6ggz%uiyoMe-QU8C(W12^Q)rJP}<|L-+3bOWZ37Wyq4S_)W0k4(X-zf zduQgi_b09zJSO0avTwrbxyGTJ{PQ% z>%%#iUp^&2Fe7k@ojGX9Wa1Rze}#Fw@;iekb7*1Wtk{4LJ$krLc51j8B=;-KMZtlb zn=@rc*|C?YHv#??d&3`SeP{G0?um~cz6m?xeqhe9t1?#jqR!+6XvzJ+yuGEz+w}USGzdL#8*xyTsg&X*G!_ z63fc-UQ9ntejJr&P~6&L|C||C3+fZc(Y!r<;<({yefCpM=8iURe~&!8)5$-`duO%B z!FSb{IFLr34BmtAr6Mm1zccoO6J?L1INK`EkT3bG$uws;NxVMno%;%(0o+>7^{Kol zxN6gT2isd;yG`E}d*Y@n38e3eJ-i=ioFd$>&>xJWc{_44s^`VumHr+aOZ!2dUkyyS zZ)%8-ralh)QjwDh*&0UNT6h6|TQi^XqTtDZQ`AfH?T55^`#-dNJACx$O&m!1G3(dD zJ958T*=L;GuXyjwc~N+HvBx=Lu^)uT1p7hGibemmyv`kdi6+BE9&VncJjf5eE|I zU^|CL&)QI)LG|eIzv6R{bA1;Je;_~3yUEVvF;V$;BMQr0(mW`I(2bhr}-xI95TF1*gNM@e~|g2mbpH7;+U(J zZaT6sZq~wp)gIloxhUt`F=wzL&Ne)8@DJ+VaQ(Yt{*`(Uwx<6T^6lVkqvyrF3D=;P zr010FK+qU}aL)31y;kj%egP7&r;@TFq!Jel@`7q$DpeQVhdb5Y3_Ig~a96Q=_S~iKkjI2Qyn5f6`-Ar-*Eg6rMGf^$=WULJ zkT;w?yo&pw{#VRHMvtC($nd2$(Y$?z#(_LQ9uxSTy{VU~?pJCq+LBufKThi>^ys;d z!@0f-QJ#V4N38GLLFS_H@M3-iuG*D#leUjPnscF`!K!O}@rE;3jd^{}1Dd)NHGFho zy!cWBHNNOf@zH~aJWhKLTFuWgya$b(ZJsk6CBA4I;y|i=JAA2KX0Dt#Zg_g1ed37& zzg>AP)x90}EA(;higyY3t7l|Cs627H-#LW#IL+tp8_q%Ood?mpJ(2!b>fX+MXM9)S zA%g?Sxjy`_;>p9y+z;$=RBwX6EA9`zuk|JZh0lO{JGix<890!Nf5qGnm6Or?IOwG+ zemn1R^7>Ec{ILEJ<*&lU3xIQw`-AV+?IRDb8~JgnO`p;K3f^$uI~)0;2P|^PmggYm zqVVuq8GIA)4>~;dU2!iJe1;>Yy@H<3@iRQ_M-97zA8*%!7o)OUHxkn4NAHxanK zYMu3pDa2I+_haUv%V#$U{|b8? z`h(!ffG>)k7kGW_iDRzXt=P{Js%g%E9J1mRfhY63_P;WE;xKPVUX-~X;ERGM1MbHQ zl)u6`i2dOFrG2t@kRbWQY5^M3}s2nox?cg(j zTg!f&=ztuLF2el)XB*!Yya3HBVsZi{f3=Zv$m57Fs{gORXF#scR^xu`G54Z#uw3II zI}x`QzEpfycn@A5^oA>ZUR`OA(=qKGL(l6norCzUaBt^xupw@PbeQrCz0Q|LA$qG_MH`H zoBM;<55B!@Ddn%gzjAag>igY@@qs@^rBVJ0bA}Sy;~4wS=y};`z0@u8zk(m$N24{N-<&Xy^)LZ1+F>h}^97^Y)4S6kZtcfly7p~ef{o0ItHt272#>D?B zzEs?=(DVAY_8uIO+|7JQ`Z$+}FRJ+M7q$Eqczw*Rokspa&dHoieuesj=%woOcIH5) zEjkx9Ht@$08~S!|&Z)h5HvCXlNiyx7LnzlbX}j;y+zT5TY`eA{5Fxp~MpNvOADN5W^4Yz%cuX`S5x?X)~6}miPzU5H*9B*<43A(sW*Z9mGV13PjiOe+WkuJ zrScxfmN<};4DUhZ;RU}PIpn@HznYt4EnWb8SMJ0^23HL{WE1rViw%9}iT3t%Zx5k8 z4)d>&Lsogw?bJ)<{UGL|^8-Hg=uVt%wZ~EUE8Y(pxoW3lUQei_oXl0}58kb}{l)4? z(AH_>iPL-ZvFAqBmD78$chK{5Mr(bX9G|O$hPpm%{!jH&$7j$U=iOu%^5gKF!JGWf z@TH>fjC=dTxDC`x#k?Ik8O4EgFn^u=Qu>~}MP*A*1l5!Y|0*-0Nj$vp0-U8jPLc5X zc+M~-si}oG9DGsbiQ~RA?+4MN_uqb%I7P9BJr2BZHv@y>I;QbRE41aSmcX*iPeLDQ@je+B>s{mwR4g z>Smv-sGKeTE1RTNso})g#{BAi@=aXP^6i*2q);za{jcVdCl1^XoP+FLnyUHeKV6uq z@%pxq*OK3ZA=IPC{EGiqwHmip`RMr^oNMq+a1I%M9F-T{d#q2Df8$3k%LXp?_6s|u zaX*lgfhVq>_BiOJDqm{tnpvfL4f!kVagdWasP*X4$3c(YlCupT{l(LxZ|tZ3AaXLu zU-8~K&)-hw?d-Kwd*@fuKcU`4IPG!JOV#-ds*lsgxk&oX$%Z_G;`Qk~PZpqeF;7scM$K4nd2H_FKIYe&OzH>?$if%UPpc$ z%-eAe@*ao10PM9?ew^5)eYE~ykW)AJj|_es<+X%|S8DD?NvdzO&_= z;RD$Z_K+U^oqbiO$K2TOnBK={csg-E_AUKm_=Rb?1)o9jub3yp^LF%7H#a<>dpmseMRX2sn!8E-gOBf>m#t1M znSJOIaX!);HM;u6acpu0f z2R(ZDar#y5Iu^1cD#uRv4EV0xHJ=xIOb#zxY1))%t>yYA+qb&*$I0UIN%>Rg-j2RA zyZ}5GMUQ^7Ur);QalRerAok9Bk6!(+!2PJ8d^_?C$=+j$Q^efb%fu;~B=dHiTZ&!u+by zu*ZRK0^ASq8C1UgjPxehW5PZ!^qt{}<2*wO-P_S0*KsA@(g$nDozpngW$I_S54j9kr!p249`VXo&obKJ^;5eF#^1S^& zNyE+a$-|4eXdU$jb?!%DqD@@S$YQ@;Ln3J2uDG?BGlVUfkP()@p3Xt=Me$v64mrzo zaG{xeshx@6K9%n6&yjzSy-Up59^aVjl0rVOZ{^<39LV+kClX)O_Q;g2;gmz>Ji{FN zUyZEWD{}_sUm?#>NBpbTU2Tc`fqZ+Xw6~Mp%oQo;w0S$uL2vOpgHr?!B=$I}Hvv!F znoL`R&nxLfi1r=?_XBg$^hM{1&%kq0=6)<8KaTF3V7{pGE}`dDxVS|ASKu?ik7Mjj zaDNc*LFC)P7uC6HI0upI+hr3b^Y;GfCpB+4di0wmFN&PZ{WvSrNb#6}hrG|C?+gy4 zQ(A}I@SQHlztDOUD$js(5Oane{q|9RaA)Pb((A&%Vop($a6c5UuPOeXX`s9ZF=rT0 zduQ|qn^%0J&95*Qy({x}crA}CTuJjQ^t|{zXnl?QgY1b5U*ewuZ@A*O@6h_r%vFn% zd3)W>sb|~|mKDE{9kbL!{LZRB2yU&}du-U5nEnPnLw^4Wly66#0ec+moq5iH{vi9E zRgeCn=`-TDBY&m$o#Dq}{~&WelqU}TLF7fpm>Q`+2#*Qw?OQZHg9GKS`daWs)%;53 z8T5HO`*Gk)uDLGb!s z5uaC<=HYdsJ&u|)j3I6<&cSry6v02pdmMaM@P>CieoXT1ANo89|2_6d!v9QO^d4;c zME{^eYS`K-;swB*L3sgi4yt@R@>j?+pzmzE@;Grn+_QICvq74JcKOoGVo{6hA;rNhkMBsH#x%kkfF;Mnt38_?#0JMU()VZHor6yuaA3P@Wg?$&AdLhC(f_1AH?4I1B+hjMe2EhFADDx=2z@p zsuw>F->;YhnVH#!@(l0-B+CB^JY;xGnk+nV8~V1Fc{}IZW9b|OU(}y+eGe`CgWzm~ zQ-nT_nzy6pg}f-AgWxlOTU(^{2i3j3M0{Q$)bnD#sIA#i_Ji2tv?l+c;){ZR1)mrD zokx?;tE%u^R(VQq^FJxqH-q*#)>f~(-W;@AxN6|ZfQPK^SJ8&|Am&&2u577~gB&t? z^m?8FdC`=C>uD|uPh70L2Qe4rdAsh3V?U1a(SJN7lIEh=I|t=YBu>#(>P_H&rTWfW z{JM#EiTMmX7uD}q%qfD;OY!=cTgyHA2gFrVy@`9|AA~oY--A7x?zS+ZpH@MZJOlas7hTx`MXL(i*d@gviw#l;5i(mLAXfcwGut0k*5 zOQs9AHn;CDBSr=OFKYRs!=~4%kF&{l^x$)DAKQf2n8_Ost{ObNA7*wD4{sac$>fRO z8S{4JuhJ+lS}i=}C+%^r8|GJ@@*V_V)T?gJx$?@nu>?7$MWUdmo_rFoB31XGvGbwt??N;68{R`@Rqzj_~>=6nu$Dd$cr*h#>#?! z#rgIdXTv4mjy?{2^pl7KxtY#E&dC(gdyv1YPIABUJ33i>sVdK)?(N_dA>R&85qk7l zl)w5(zAN@xVvo~G^LdpLSFL$yp}Yq@>E6Cd^W$*O3;RLz=-a0`UUho1a9x~<(;B3Q3&v{Yoao`02w>C_8GW;Gi=8$#n2YOz35As};--F1D zE?Yf(Sq#nFudVYJAAQSn5bwe1UTp_hcP*qm19OT3ljfPhf3Gmxn@{o0}CC)+Set_4fa>yNshpcn9ncsd$_Jhx8=OFh7|6J3P zeDsc#zp^%*gUBHx*N1yMJaPQLdcJD!vB^84bL>`rndD)fNAE$*8TdW8R`~6i!b4Vh z2F~@N?~M20v?YF89|!M2#n~>9Jr4RfPs@AoMdBgPm;2THoJl(>D9->-9Oew))Boze z)|*h@Rm&bd_i;FXg*=1KtzGN$$DlW~9(~)~PQv{-m-RpKhJ&;1CGSBm;fu0&34NS! zjV}t$HqRMadM(kT#~#Ng@CTYRpqC1tmsy*OG7otTapz94oo<7Vb-)-)#Y~WeRx8u7~`@!I(hpE4<8^3+> z(X0zw8y?VmurPFsd{^q;4sW>83xIPFJmi@MAH7%Lg{Y;Aj+tI1&NhEnn2Rbt1A6rA zn_!QL%E=VYs29E{@(haKj(j`w+udl7!+8eYJEK3y`77?Fg4f4A`WWIssyu`0(IY4G zK6&D5$dAMOLG%ZcsF(UO`6lK_o&o$Tcua!m-tJQ?Fxa-Ko)MbRH*?-IVN z8CqTxIT_|5=Xi8=4<@gr+T*-s;dk~CKEsp#SH}AlcrrK#=Mw)4oNc}5Rc$()@VkX? z0y$(gzhWOf_fp~Wf*+^D%*&d`B%k;UoNq^OVio1vPba&l@6B6C`@wRX@7%@>F7VB! z-o#<*rH&5#c|;z)2Y-`uaM-f>+J3NQz8!f6ozK8LWZnKIx3=&Pa=sn&EBL&aTg%)J^aqh|H}>d}lK}_v zM3Rl%+uf^t8Vg(!sF(VE%-@O6@Imx%!Gk^5_n+MPc75w#9v%+g8Z5pE#c$`mGx!Xy zK`+mFS9pE!F7fI^ZJyJ{y#}?%l!&G zi z&d}Om}pI& zIM2a9YWqPwCxh<_d7317$fg_-0We5tBj{5W4H4L8RweO}`s8}khR zzx-8T#*`(tnuqrm`6g!f-lF*?*grUzdi0^<4KJtvmFnXJ?$~3oAH=<#`_7jshm3i< zp1-oDJw@;mGE_IAQU=DaB9koVF3DvP*kd~fHR4CYsylVP3= z_RjreE~@@l+?#kU@pFSGu8ng6p8~aq-;J&_J)J2i)(nS>tT$ z-xc_x*Ph5{V4oMyuaZtIE-NRl8utgg6ns??D*nNyp_}}3W;Dm$Ck~|Xer5EABQM(0 z?|dixPVD}KtELfj4q}h9x6{bF=yQ83XO-Sw(_?vs*^c~!legMYe-J);?xpIUxMbqi zGEZj8j@X<=@ed;3?oPc4aMf}=qNP8m_JcczME0IyZ!5hCf4aA0@BAblGUr8c4zBU& z=^o(pkhmW^Y2L2<&iJl4-`;Y5g*-zp&97{KX%_w!{La0W-&ixdbYErExzXg|Wgk6z z;%>&?3tubz_J0$%mhbK8JO7~Z892`X&i1bhyk+mqJumEWn5&kS>6l_q@4=nqA7lgj*p~|a6?_wKFPo>$MJLdE5OW6nueK4lHec@T&YCwIp17veDZ;H? zRT6&akF)v2>%;u2WlrXda6b~t%H%z0PdsFB)i7_jdQS0V9*K`W%P<$cBljye^RVPD zX?D4Dc6L1eRdq9YmpIqQ-X-2UyY;IeZtZT$i^At+mt=3qUvZBf^LFNb7<;Mg$6@{z za>&fTia2xYX1G%q_YXZ51T2_!WMP`ADA6vi8|??9$TxB0WTE6mnFAS|zp?yJ;q@sm zK%vYT&>ut}2lIA!?SI9-RJ9+>b4ePQ?CmXcQTEXf^~|Lla`Sm>$;se<#XOnamC>4i z5OY!ef0gHRWzbv1{QwVH|E|ttT`Ih-y$3PBLJk@JLFS8co}siwABQ>H>Ks%Y$XDrq z#q%rlotZC+`PHyx3(NsY&8ao(-k156o8ucc?two-J z`#5TkgZve~E1omxeH>$duvxgZ=y~yekl%ygi`GzH^o+q9&i+C60_6JC$~mYwke2UlA@7P&s~8Q`_FwfaKx?Hwd93LY|i^eWevKJjV!U%?B2_h93* zZO9Y%MMaN-tjtz)4qDDdb#M5;Oe+@sSIaXL)LuIqsr7M!lNvRi%qBVqr+O8IZlV6* z#>J&`iz$DF9{q;~kBQ33L>l(acn_{1USH$9?1&=YF@t|}+d+Gr#bp(FFG>zseOKVB zA%~nNKCh9c%L_Wr%+mbMm&n6=ChH*O`oJk-53e=lMVSMM`_)M5r8?6&II3>;x&4(h zOK-1n6qEZ1yX3Dp z-;Vqh_fl1l{_lyOn^G735;aESi{d?)Ua~~yq7PHAYMwaU+nEE2Twfu1!^4Cp122He z$?$i@??Ln?*f)W>DF3fkMHKmtr5rNvopiI*?;%bRdo9^F0UtejUar)mH|9k-C&L`bJo0%>Cja0TzaHerNvC{!T)>AOUBx%? z{K1Oizh*BauO+eDv(Ke0t{pD2I%55IGs<^+gDu!P0Aq?~47-nZ#A&c{})` z*gF?WUNmL(JJd^kC^?xAFKlV3@A8)cp} z{-A?%p?Km}kQbnNUPeTz@2J5)yM1gEMtM=(uej&6ioPrCaoD@0yq1as>81Id@6!Eh zugpckXXt$VIQ8hUcSa7G=U4FKfPV$A<(zXBm2*mO&>n|#eIdkEd!TV3RnJR(SM0S^ z+z-z6A>aOz#@Sxy5gm{;>i}_zR1R77arAjRJiNj5zp|13V2PhA`RLKdVV@T`+jnk; zoq6G4xt8lwK6=g}t8?%b$zNRy9^|=E@>lRXgHvQH`@y+}oDB1?JgGm3Uh0QFHwFzN zpI2A$F6my&T8-a6X80-tXB$2GCh{&ZrwF`0d+JRvug|hak3LSNDPz&uDBr*z=zgVm zedy!NHHRiWNWH$!Z~H_oCo`3N6MD}J{y}&xvB%-P^W>yB^U!6f#8ta|wy3tP%te0@ zZY|D1aEiVo9`X`;5B5#}HrdYXkg_V%rQpko&{H29?(OP-1+JPGZ#)uT?#&wd^_$}No9$|zfybWX3F((&+7+y4_-Mt zgE-sFf#h=#J$mKGQTsvWejtC<*ku#tkOQ_~rn%@`;$I;r^Bd(wInRLiV5MnL;-h)1 zBMN=J2cOlvOFP9IZc8~Cea^t|L3~%t7fqWuZg__5o$rdr1m9Jsv^KdjcRqFec(t|F zAlE+zeMG+00~VYjd{@XbsQDFq^v3&@;)}wUiu)COUhxArxr}anrE2G~VDgxpNP62G zD?Tsm2ZNV(b@>EiPMf zBAE6#;B03KR}J44yh|I2&v5x{)S(rW7X`n)DYbgtlim=CF zU+R<{b8_rf9@p{=FOVmWxgYZb7R>rwa>zZj@2b_co0RJd$Ou|uXAWEvDD!sSWf zm){azA98&?C5Oy>(Oa>1sP7E!hcPeeX5J@$=ZoYGR~}yGA@g^IIRoF@6<4jT`MCIT z@V{b@3HCUR^9Ch;X38-5Ca}kuPPsnpakw`ziSnXbWbcgrAkM)8;XtaM7xUYhhm7ya z(wD0IIIkw!%+Dlm_-VI21~0(Z$qw3nF#OcE>#ZE@o_jd7*nflezruF~zcVgJ!@Mdx4-;q^^j8?@E-NaOj3^?sc<^dI7RiSi8a zn4HnLYTTPZo}qEYw@E|H3zt5dU0&Q*?pN@{%@beh#zgx#m&mPtTZhat@UNIt6r}la zT3wrHKc{!;knMim$Un$jHRiX2`!V%QUz&@~U;38$+oVS;;&R^KvH#fLswRksm;E@* zzk)9{&oF10y0(qxn?N6DHhE0g=k*f#aa14YHvvJ@0Ef6Pk=v*@G1=b!+Mg#2D9^w=nMqIVJ7azY zAN{<5wbb*1-x(eg=6?K#_RhK|4!k~aKlnY!d{O50F$Yq40eWcrLG&h!`F422**Bs8 zuawVgKjo0`?ECe!&yC%VtNVBj&lKN;k+Y3GPB{Iqz>|T`>$UXlc}vR{ortX2uK5R% z7d>K16>qqavyJbHIYnyDfZjwW3%+RS+(N^51rKix&D-b6dk}l)4YeJdbNimN=y~z~ z3itL9=?@~;2fyP<{PReHS*<=goje3p!t`UA-M~AVb!+k^P|RO{h6TkDz{YM#NthkI6^$ zzrww}>+uN6^@02GY0BxWtA&4(#{}MR_;KJ}@}!;@&l#9oi(DV}gRN5R&EJxL5PN5E zYkzVZIe44ziU`~J0}}sbN*6wZ>Um+0gZb4Qb12>0IWM}Up`lC10ij;SlyArU3g1Q&!X78UHJMSDa^H z&NlZ4(Z@OHIEs8;?8k92k4Wxl;6SP#{Vnl3qvr*FdxiKW(05iGNX3)6M|~W4c+pG6 zyq({J%#%T06uk-d<1n{Y?FYLOr)cr4%7tl^lW~jOV5@&JNAQ|XMk^ldlSfCsl4cGbPlTdm0#xu+T#S%y`A}2;2~F3&OYZ=w_Ed= zc*a+m4$J>ae-E+R&8^uM}TcxT^5;WL1L^?&(svgtjDUMe`-%>6j;HgfP5-<1)z z^IuK;!nBMyML(0@S@(JA{C2)y!9S??yl}tTGIwL>G|e0CcQiorF5w)^kluvly?p@r zCfLKf(xhMSDXRDT_HC~H zezLecK;DC@N3Ze>dJdU6kP8B`JzU)bogUnrLjSA2+0jeiF(0Qn1H1s>Z0k80^&J3PE?47@(%kT0J0y0Kq+UY%!F893WIPe$F_ zmB$11$5?9T~!eas-{ZHau!rr+b@sO=kKFWNm;ABPcsgJKa(0&m8 zLGD z@PpplhHN7a`T=-MLcf@r-*y>!LoN|ujS^%CdrGc_h4T6)O^2; zh$U9$@Ff$qJ`VDtYVTZ_m~J|}a3Sq+x*2?_d~g4nxN1CSn9%r<%hG{~-s8i*i5Wzk zqNwOg!TUyL8R8KcSZ947_(fTHWpV zbS=-YUz;;{S$IsqXLuNwJ7bf7qs-fxe}$eGdJ{Zv$Nh@u3`LSd{ulKJ*$dE)_Rcd7 zt)xBy3`4#V-b-t*% z?327#)4xh?OI$V1^=;95Uin&&{&A1VkF>{8dBK2Qe-M4=$<*^& zUmlk4mobg_4E`A*!t0xCKcn}~A;o@fk=BxvF;T9si+d=&2XVi8UgKZQ&IuOIHuL&0 zZ%1$9q2?dNyd6B`slsn(|6mN|+d0?gL_M$3@xPjmkS7i~kO3{1x(| zDu2bizUD+@-x)cXV-;Ns)@E9#IBK~*&dHo5PEk7bCg2}rj|t~wcz&gOE!mF)o{TXk z1Fxm-G1=0v>O#QL3F05*|CR1b#axv8gP)n+NPIYNO+D8DoA?J?wu!yc!?Y%5$f z`y-FeH&B0Yu)#O+UCbEj;}pAiOJ0<@YMj3^@{pNRbZB9gX=7q5@_F(6%I4agley9# zWd9)cgYe@#sh7%}ZFu6?Yl;8WKOf6qB@_1pb5ZU)UkV=T`BDGL^uMwqZmnZ#vw1)H z2WOS;tDJRiOkG*0XZp3JzO!fiMN?(M&DcBPxjt71z3FN#IT`f4_`5nw=O8#m$cv60 zo-wgA#^z8G3*AhK1kns>FSl_n*a|vpE%q9jY?f~*fhxST`{ju^-^1F+*){- z^qdU(IN%g@qFyR{moCzN@F)6T{WH0xk3L7{R~JpA4RcX=!*^B2h;Kseaj+kJp!Mk4 zV={pF3_QQm`J%Qfze*Y|y@?kNhDeW|dlQw^^Rm}+GOF(!NI7IX;kP5#*ZSH-Er*Pr z7xp;VJEKRBo|o=R1)t&3iUm0nb{stRw<a3+WJ<8-h}c^AkTpL zRWIpzc@g(R@vpe&g*k(hmP1C59yuA6zd~O0BHi1SH@xv#Cx;RPzrB^b2XSxb9J1nm z3?x5}>UqKMeD&;1;?^qe#})CIlt>O)c}y(50BbJz3ttqTINiGho(#N}=y@%tIRm)0 ziYGIbc*wY4ol3ShJCG;tbK(^7{0iQ1=0H|2=tw>>V7y&e0h@EQIZ z)Mn&g`;}?$!9UmZ5U-`Ww;R3T*bfdA4=?6d;~L#%?`*a5MAAFv=%p`a?<{`tV3_bl z*L%bew>FdZ&aRQ0{B{iafW9mEanN_>{vh%U_^!Zbi0r*{$R?RzseJo7@t7$8pz=+` zXdaV%;)`-#^pWXad~Ec^;9&+YfZlfoPbRi>XXQMt=LIhS_RhQ?#Qci$?cjbm~_5GgSgUnTfcM18c z@7(s-OsQFRVxi>v9BA(hKMv2YkQe3mU^DgTJI-81^DEMb z3%_OI$LT5i!CjTH)SK9Cwx=92@>jga$tVBd%;@I$yQV=2r^vemULSknuph+#3VobD z+2z#ps=FEN)ZKlxN1X7Gk!L{uYR8Znl5fwY`&GEiMZs^crJM|UUd;W-Ph4#}Aor`T z#6#wJ`*Xro(|r?|i!!$s-X(Po4)a{sKd^Icy}kHxT5CS9Iixedml2U+Nvo zi@xD%ZS|$*1yFuxT)V>UpUiy?$@U-Z`XZm(7pF>r1D-^AYk* z@V&jgb6)N5a1ZuXghyp z#1`_UA}^YFBB^Yr!9Uoo;5hA_TSt2c$S&HpQ& zGr+q9?#JwNdn=<#Z%R&vd#TK4@E5))=2!5BpNbi$&99!X+J9`~j@TS)$}{M>KJGiO z*7EI&-_GBa&9&RYLk?PEV-C^wINVExe~^8t>C*Fp-#M&B{tA8^?s>gfHI90z`Z@R@ zer|Mi@KBjESke7TnFHA?$bHVp_{*kG6KbiCb5-;3f&&T8Hgh0gTl(n1De92hE^SnDSM$M?A1NpEtMEnn-d;t1XZDyNC&N8@j~k zd;M%~ZAa&vzUN1b5%1C<;?|bIJN0|Tj3Pt&!~^H zqIvsYW~NW{A>YK<8~;0v{1yIJJ;iG|gSa2)@X%$zVSi{1{(!D(#)0cFz%REpo_D=BnWwoVH|w z^yqnyqx^%sA50Jruj&u(DSl3R6SlI)LC=f(&ZEPAjOmxqWU7<zki5amU_cFA@(qabPm} zCg25lNc=0z83H=j)msY(5;^1wv+MHPYi5@2tDH^TTKK%cRRh2MNWyjE6#YKvAFehu zXPCCLy~TSFc~Q(oF=t4h*hTW~?02>s*6?fxhvMrqC?|s)avSo|D<1MEHb0P$p7{*; zu6~K~(!5LXdBJPRTs5l|m)1?9`_-0)`(6GrAjqqPc*x9E+Z-B1zEtLHV?X%#IcS+f z24|bSOJ~S80Uolk@7!H_6D{9^=sSZ0iTf3JeYf{rlKmii!y|TfJASm<&gym7KeYc< zjcG)Dzd7B5oHdWhp2|6oaf+;H@BER*LvAp=lJHZ^sIX<;iQ0Zp=hia!1D?1#>O13q zl|XyvO^G(dlR?jmeO?=gQxq(H=O=psFmGQpYhJ)QxwnUq$AtaP;K{HTKykL2hm3ps zqxhN8*Mnd8+}Ph=+YjP@WtUVg{43;;IWO9d&cRXS4d-4eI7RSH7ba(& zMKo{6dyqNX@Wf%xP(}X1@3Ri2bThw4Ts7_wT9J22@kM_%dCL9DRpz3K-(F^RNU}+t zwl;Wc(_#BxYU=~(yFwp_b298PNg}=|dZ_^he*5#}1qha$3^>~hXMMgf)l`yb6Xz1S z-LG`W?B0{??a6Bi{+0S)={_&quap-6_x8nk znCgiyI>a+i>!l*k&?dFne8AjI_Jc7rXW+c(3));1ejMcb^!=c^w_DDyR89up75al4 zC5OCC%(px2Z>e&Gb(EOLN>b4=*^|%41?pT(zlN zn-1H^{OV1MbI_aiIN*M`S5?UW>O_)~2d~eCczyU^*%JqH zN`3(Oae|fvW=tjzuf5jC=^oiCZbM?W@UP&BKyTPKwf2XcPtj>-k+-l^NCd3f2ognRo(7Cw4?-j45zy#P1|eTJt|FO_{0 z8>x?DL%vkz$z*0aq}ZEJiGL6|nH@G~-Mj}E`mT<6MBG}wU;Q{DpZd<2i*o*IwmB^6 zLF(0Y6Sq&Gd%Nb+Fve!w@ehIq&|>zY#ok`|hWE?Y)>oGWLGYg;=PX?thzWY}Zk zHTVbOx1*24JujZOcQ5#N#Z=)lASbhdd=uzRu+J;I&u+(YcnqR>;!8sY_ zALRWYpMw>|{RmkbwzcuF-7j~DQ-nN&;>nzi86CFN+cYrG#jmkX)jsMEavx{m(&xlC zq0Ye=`mVS~??Svj?hop|ROR#1_c)&n*+e{<&50|tbCB;>LG&JcK09`)2jyfqf8}1~ zOLK-4@6lmrHLlu~;D2~-?C;n4LA^EY2brtJ^LFr%Vo)`1_bWfblRRd?6bA8`u zT`s&uy;SrE**~cA495SJ^5e83&Nlbx!RrIJ7T;BNL=*K=k(2QXJSTb4>sr2D_lD!U zDsE`hc*vY@=lm7-=;0qkZ{lHUh|Jr0Kgd1$pEPecyi4iSA3R6%EACBz*9YIkYU1@p zoZ6~!ijZ$_P8?+&aAIIq7 z1rHf>2INI8J-n9lt3a8Hx3Z)YDp`*GGwzWqzuJF6V>KIwVEM-R^SIQd_BX?y2p z$ul5_j2^u{7iAAGxV2*j7yD*KG|wB5c+?_)g?T&g2R(#a%f1QY-gyi4ariw5etUD5 zV)0FI4q4C1a32TWCGZ(|-p-sN%o)mTf@+qYh!>uW1LZ~8W3tV_>$?|cWdgSr^L8)U z4_0e)QS`hx*VpCv7ut8lz6sphUz*c1sQt)Z`j#>JS`O75xm9r_=XY_e}M}3?}l5bbsTAnjt-j4U+tu;~PU4n{7!iFKQ+EtG7Ha1<#3oMEonBUva+u zMd7OH^LEaQD((k(eU%F{gx`)n4s$=y^9s&?KO>CtR}+X^tM74C4jK9O#j|4RyJA1i z3)G`mJ+HYMrzqbgX`sp5FYN1>K?(PShm5=^_@Xx2IcP`sEBNTkT6h88i@##ppKw>^ z?Qaqfx!dt+JKN{u)(IQi`{r~U6 zm?29^jUnkMDaSdIazv77W=1Y%c4>2!&8`?``+PRT*cDqeo7v1PX$>6@*QNqGyH?xn_%zK7c^fPK6($zw}XFWdmn@^mHF-HO~4zDxgC5_ zGfzg%R|UbdX)jvXu<5M#5&xal;$3=8{5ZHfGpA^R_Tzxxj(z*L(sxcIA3Z#A=;L62 zHAj4@cEo467uz6QHO`CjeGuLy8eed9J{d;e9)}AM+2S+4#J0$H9E1dS1#m zu{G-5U_Z~UgYR@JA^#wH^o=ROi3_dcR$27^ioKRz3r z#eML7okPYQ$6j-akVD4Y&U;aCir`Ddd^Nx9;iLblZfbei{qoRF-nVpr5cfg!arP4T zgMAZyR`0Po(%c?Qy$R&o!P&-l(CAGh(*BBjsqo{#m->q2ulfmB4f9ouZ#>-x(M!eL zZg|5BOk6e2x3kyMc&_+=FfJ^H?#`;03jWoSl`o6mnYn6wAH;KYBKn2++m=Sjx8sgu z{th})UbHqlLh@Jpx^$=c>RHX}GyH>?+vm}q0eu|woi7w#tGsk_Z0!M;wdBV^{>qc? zIQhhrv466y^ZQqWPaM^p?PE!QOZ%V|uMf|atrviM6Rq=C(}~Zpy`ibw19ZoEk8*wR zhVyrjeO}X!OxyX(#ycsYiQ(4QX)ikGz^|t_OCJY&(X?fsSYAqKki01P3?bs9M{feY z3B0eWDrTMd;A$J{J8zR7{dnr7s_)>$TKJveqepL|^&RI|;eLD?e_q~K;9qfG6n7lO z*@kZ-nC^q>xw;q_9@$Je8Sc@WeG}0+cJy4SeY^4kaDVVq;vw6`x)Tps<&e=IvzAWm|KSaIM;;16sADrd+pl~4J^SVrX2ILu%WzVqXMkmw% zpn6|%&kN6$Mf>QnzXDH&J-pzz8yrY@m&|^g3wkfAeDvTU|4a7m8@%s?{-*a=k86Ir z>ZQ6LN68fLFOTso4$kCGa!Ej9&&IeH_2apyfn*_m(VWu zG4W0Ki7$0l{#40dao_oxcuc@&=q2Al_B(UmxsJYryNTDwJ};kC_QvCoV9gYW_%hm7}Cp*&Z`#I5z)d48ka z8s>|_Kghhk2V8Rpd_QJ9<&c#pu8260@DCc#mD)4>oA#pcd8xgq*+;*=U%87n?M1;U z`cR%L%va|7;4M>Lv`>HwdBd?6-Ampj@Y~VHG51o{eUN)zc&?23YB%+rk?R|6javE4 z`VWep+aDpB#m^I;;oI=ZeqWC%lsp4Ckcq84ChWE3JcHtFzZaDs zJe&CK{9G~r$}e%Tb>ynmS!>8Qf%nzs`}m?fx2yj__y-M-$v-P*%m1L`$b8LbK;Lz z`*!zvj{m9(_os8`nZj|AWZ&@ty(ucEiKVdC};|bAhjTyfetB>)raM zAKD!D+cA4Xn-nMM<3y1k2YzRG0eS`ukogL|RP05~`F7@u;(diX4)X1l80*V?oIwd_~>yTbg+Ag_zbsnfADGYE@6MgyuR>~>)G9fC!^*o*YLT00-PML zT$8!ooEKH!LDe5*uO;@P@95qH?#>3c7I}tp?Ojsb59ArbBCiC#bl?68b31$!*k8ey zYWrM$692pSCWecTelB?d)OYYg;UC1U{m3P?-{etid>&nJcF9CbBjx(oV*>BeZ0S40 z8_s@b>+bktv$cN^`zyn{bV`2*=j-(YMcMC+{T050;1nU(H}#0`&hr}` z*7zpIT3-3u6pzV(oD96- zimS%nCF~iL-x+yP@I}${nk;_jCc5Li@nfFo$t=zss(u#2Ny*ZFK@7HTV5Qs z)x_(2ljbY?HNJ^UtRv~}JdeDV;4^^Vj$9w^&Mk}9&ncKZY1r2^Ut!vpn0sFE0?a25FYg&t{%S()KFyQ4uwwLLh{^FiW^ zzH@f6X>K30$|~PM>_yQZRNnCC>F&IpdK0{7h~K@3yi4t^j>79ho`L7~0?NstN00wO zaMctC68=Hni@uiBIrV|u+2mbT{7NR)5W`l zI}YadkQLJ8FD&$kbDO(({siC zLFC&5Hn^nNC+@XAzWUef$dZbK;ig_{|A?`1mo1fa$Jyk4RdRjUU$Mu;-MTON`^>Y2 zw<+I_`HH>aKa+>IF*Z+feavq^B0Vp)XJAhp=NS~AfqhsT+yU0RJHJ z?f4%AuTS}%`R=T^wTm?m8Q($d+i}N1FZHnHdFl^>Q-q#ZAn_T%{lLCmaf&dvcW`dj zUI6Ud!N1C+yK{x)`p(n;U=PWQs$3uT?Z{u@J9x?RI`y5IQ#7_NMtl?QtM`%LS@A{D zo5;~QnMTXN`0t`UC?^Ac`_t$4O3&+9;wb9TGY|O=y{|SBS53|B%f>CYzBBhG(8uW=&`I<9jJ~tMDdO)Sd|udJnRzntLkj3Sh~C6P-5-34{DW%Gz+M3M zTB7g#9^D6Lw&KZPzA`+#;eB?9hqp-hqTuymzN)AGAb2v!GaQv1^6sjE=f*c}b6YlK zwb%HNZ?rEJJui4n?&$fdO-ftxnYE$*VIepeft*bO|FUoVf zdS9VO->7{Pyce|yrwIG2^^$J~SFNQlxV4f9(i7~{1RC|UO$!mEx z^w*d}@#ih$;+~D@L34YB_);TnINOSUHI43rmjWZnmkO>LdoB4pxVOr$G%Ck#?SILa z$~oj<`3`QRI}YCmdC!0xGW(s4{T1`C^2@Igw>C_3wwbHO`78G0@c!z_^@~=%N^?7W z6XjLUotw~9;AW-w)mPDfCr?~tMfP_5y6bI#;{^oa=jze5v>!#Qq9%JMTr&^ZGV? zlHWID@&|Nw-E^b&^lW-xjad~Xz0{fH4TmSL)8ILtn}gq{{M8m4{#Dl5siyDXE2~yz zr4d(c^Nk0{6USal@cPi3K!5Q2Uhg@~sj-NU9yuA?xgFm@#TRAn2YM6u9~@tM@#G(s z7Ye`0tfc=zzB}`t0i11fuCHIjlL5}-o*1~R%Xk~Vv%Tq#!+VBdlh@41L*FocrqibCledi zEY6u(x~J;7a}%5L+>(bRcug|#+rh2fEbl9DKZ1!{`=Qls^^NSXl8S>-XU5i5cIhEL zuh-))>0F=crKG8S@gcjH06+$$K*8S z`qohn`H=8Mztugjc?U9!Em`AMg;_%r8&YbhH(})Zil~p%WGPy{EoxhEkj(9W=6AN` zU;PxGyzF0=za}^?PH)9$P(Clr?d*5nuIDTE43zk$3G@eh5Ko5t&i@jx zCH@B$pP_$5?|=uWHvzvh_i+@jZx-!E9e=z{@2eL*ehCa~<%wfH1KwAW`a9TDoV+yq4g%gD3N-_R%Y@n)yBmKMtO&x1{fkyr}Y+WRG_1AJpmAwa`<4*}qSFOqk!k zi~P=t1Bre69ZN%8WaI_?zQX@t0PRKF(Es4I>A43o zH4hp5c0cJmH+SnyJY>~NRec7ljwV#dYg|Q@TI+d_ptr8N7#m-D|h( zMbS$=p486jEWL^OCyK9jbZ(~aVBY*j@_F%n5brD1$AO1e-Eoj-V2{agny-+*!gufu z>ErYyKaSC(M-G|y?cm99P6nJJb<>c+KM!s}CB0Pq>M zO(~PU^DWu82d!vhow*_~eMWv^`8DkgFA+Zu=lT}=W@!#2`@CYwyQKIG$RUH@4zAjX z|Kqt*y;S9!fX@s3cEj&{f%5IlDH=|mxNfu;b){Y^xN3M`VSlCWI1dR=1|HrmHXKOg zWDJjq;xn)x2j4;D`VSLFTtBo=j_gJM(0~DSC@|$n2vB z|05lVVuMZq%2@jd`3~h)n3hoDU)sVk3yZ~Jnr24!$YIVPZF0a>KI{8Os zwfIuODRRl3OW#5E@NV?JBlDHYU%3mv9lnWQ$P)(-FWy(^JJ;*^3cZQDmLc(9L{AM_ z?v*?wSG-H;4{~pUxwQlK2THzugXW83&%j;)mLQeIkxqXrNQrYLFdK1Vq@cybf?yblx^grm^^=7@(kB#DYMh>|rach-# z33I#QA7oFQ!L5bIwi z4*C8(19HfPl)pMX#@p}P@Qh`jSYDtW{kF-T#AldW6L&0W_XpxJf!7i_8F&HI-5H#s zCfc_@MEUm2ewE}Oye4^3wHJjS2l*?!uQqw#481OUhG!!B1UQfDEAtgNkhl+4h<6G3 zE8~4tLvwqR<*)Ilqdh}bdMzKa)y_(>&?p*vA%8P9`BN$~R~9Blq10&HJl~@*VUF`6l|W)T4*rd4xwE@sM#J z^xqLm|AXjxVc)*zV3hcsI}*1R-X-wcm3PU}?nt%U(NO6-yHidEy;Q~Po2c`mP2zXv zUaEO7iaXA;X?yeH$YZiw^ZM);KTkd{&dG4ki}UTPvR+vgLw;xQulRqEJ-o=b=Pqxt zY+C+))Ry2`ly7GbFZZ2iB}T}7kiUZ+Z1xP`ejqREai52m_o8lecjo^=d%8P=TZ?_W z;$MB}@q=GiSR(dS0!0eef=o zT%AjM(e#Y=P>(Hsr-Zd9aQ~6?mIstz6q5>Hhbcjt9HTUqi25m`ND7MfAE#0uBq*E z!^$2$`mbsSyP@v2L*MqU53QyA)mXaYz!O(aJ+Id9EA+hD{CJD{gQ|~HPrN?-55jB7 zdrKa`s8nw>nLGfBD4{xLJ`rt8Hr+Z$z@)Bjwfc~JdZwFu0 z=sW+S`3&geG*fQ^?<>U@g%_Zp6;B5ItLaB(?7Sd6WN;uAU)0R4HFF@@!wVjAneau8 zI}UnYyl-b-pXzzxK8Srg@}kPSG%l$_>VvrJr&V6pxwB?3@q*9 zRTn{dQT`6XYdL#^-wwyac6#3qeml?Y@B&yRCu8i};ayrxcbtCvLr>kh7Szd&di2ZW zI|#2OxV4zCxbMt9FZ2h&>&q*@b-d+rkki~grP9YK6z&IlUaCir`yluX>h3(Fw6f|c z>7}YXLj!quF9r@54=?V{;A}gkI3@0;eY@cw>`DKF=nt|l75gi2KU9yN{mwnD`{=ns zAIHo?Hs5jVM>h5CK>4eGRk#(b%V?kMVEroTnY2B5*4@jG&8^w#aHiLI{XdAg9r^a} zXfL|u#wFtQt;u?s{Lc1ksyEKqIi2RK2Hi_7T;5D`yYlcBHM~Q7QI+fC_tk;}mrfVl zmv6UQZ0pBS_d(=EGctM>9H#%l4tg)DIFJv?{>qjEiM%NG?V)AekA6lu8RU>}x8f8% zPW{11$+!15%~#-G-6n7NaF2_Dk;G^CyU91fzEopwXaC^SI)}`2`yqN?Inka$aX(Z} zCQ$SG_>MEfDZI~n^jtZ`<|nMTe6-XmJmguOZd{v9-@(4b>-!?HMS7|5JEP~t`F3N^ zpnCN7`ngiRRJ^ZLFBR`A<}-X6UmH^wy2<;}&=Ky9E&r+RdUP&*2VWOos+zCB>r;FN zm2cldcO1?$R8juQ$jKzw@UPOyH-TR2kKtZ^r^yosz9{%t$uzg)J9vpWknA7C-5LHt z#jWM%3iP)q^(o&(Z{oLuha6sVBin8Dhr+)Cp8@J=|g$Zr->&6ULW^ykn6kuKB#>3`&`!b^CbV^xg}o2L*6{UK=usp z5&sG~8TR8mleSm)Qoknupz?X83QuM;@!RoSG516HCcr7;JI+Ic13XKD3!{pcH|o7; z7kwY}r8~~d#6{Lu$z#I&c3V!-3*t)!XS<9zkhtTpcd1=+rg)dOU+w7JM0aO{Tib=a z;VOTn?t{2HbDjbBLFS9X!+R~}O`cSbK&`BK&YAof>$cWy&GnT;lXJN8#*UZ3jG ze@-5gzUwQB{<42IdBed~i}baIeXM(4>VFVC8FPQoCp|!V6WEI?zGy*0y4;;Nk#FMW zwP4~DF;C_=?XSRZxAmBS*LUYnIgs3=N1j3b53<)1cbu_t)s{mh-vqd7_#Z@n@HM?} z$K0+s+sa1|&bIMfAuq}v6UBktMY+DYHSalmPj?)6Es?)US@uKtM9u3%kKX)TfddI% zUvWc2w}%J&du|WjCcO!8YeR|8P|%QL;xjN`6z?na=j^ytCAN~hk$cfCrbk5f)_ zdt1pf{Ig>2iS1Wi=zXq9=qTlOG2@FZ3p2avIm1O#Fv+*~-4+ z4QHOrrqK(16T|i|&9oGXH=KFMj>IVn%=e+W9d{h?udYi@#^|N46P}F9GXztA5MF>0 z))R@%Yoc@fO83xr5ZqeuMZb)Gfw*c5BQMf-5c{h(KeilFcO3KwuL<{qbA9H0JLdLm z@85^M;O=PmPo3)LRaDwpquos1Yke@3vpJBI&v(5W<=C`k* z95T2c@J%RwyYgD1M-Tp$^5bw0nf-&{$)Jy;@}ijA{lpWOQ9MWI`rw;DUi1jv2boiJ zy4NoH4)T1(9uvH;4p+Q&V(V2`=Z3xxBay$te8qG7dh$DSP6pm^b89DuBzUbHlH)d|sXyfzBB(zI?g#ET z=%wDi7S!o6+Fvar?-K7t-_`RKa(zMhKIuUwpVxJ| zfSMJ@;`Mx``h)1DG7lMf26%YK)1Cp|aQG(vte2{MUf?0a3!ri`1BlPSo;diO7nfYg z?z?)I)rovw$RW4ro|oRKvBI!huW(J0N+{KmQ-Jrye~# zCbh@gUzz3<*{6h_EA;5q`-=Hj+?%K2Obl1{_4G`&B60M+Yh!UUn;zomo_$~1SCdVN3TlC%AonGkT~1l zjPWL}8hCx!i(=o-J$j4wO}JPclCxETr8 z+_y~mb|Z(3{FUNN zg;@7#ktaRKB@1;2fy?Azg^ z#~p|B?eH$WpzqFRUSH6Tpbaj%=M`O2M*YDLyY$fCLFR1#5xUv?67?pOHyn58p4JMT z7xk*Gpt+s*SI8mXirqRtpT2|6&O5ILpZHHjH@f4rwVn|E74FW={n)1a&dk61j`r=y zUorP%H1YbBk3O4tGKyQfX?cq!clmo!g~7p|4-LLcJQ?GT!+TM~=Y_kom*4-WH}N;& z*1jS9E9S`m&Lq^Z*OlC!LZ{c4pD0}eef2;4dyy9Lnbdz^o zXl=})_@6DFaZgbn2YXTO4@Q>!s_%m#x;Me|75X^fK=Ry<=gQ34=KCP;8L+>)Dc*4G zMGuoV9K1d=PX_nFY1B*Q`yjY#$RXo9sP2P`C&PUl&WmC%iswr0MfVC{G@kwkk?U)) zG{h~WoDBO?*}D`dejNAJwY0yAJmXont4p7O9me&x@d6ke$cK-Hm9@?7mO3KoaoLO3 zRsKxe55tec^Hsj)_3gMi=fr0fJqt25x3-LOGVrA`p8@}aoNvdTLHThw&w&0QxF2^@ ze%Uy6r_Yfr;WGq!76)&O+Ddy-?&E;p4xS9YgR1BC`YKD-`r^<7wc<cjuxT&aPVrd^2W(-`Vh$%l>KkdxHI9{13h{ z?E79jTX_MTO?w9HMbV>2{))Yp;EUpY1s*bU)lOv|NbY8RBdKGmYi?+n+tJUe9qnFn zzcO^Q_wS)~F(1ZXrac4y5AN;aQ5SKh;^4xP-?MuY|H?6C_J)8Rc848*yrp{+%qimU zAal0OejJ{!(4%MnAiS374<_h*yYj@%AP=v)5At`=kGueFWiR^f6t{V8Vhd%@0G^Do zZ{JCe;4D*o9yuMW0x5HzC?;!h9(VJk-Hu4Pcm|(tQ?-IP0nHfC_ zj*vIpMfUARPKG(#e8)LQ{lPPtdy^lhUMltsT_w+8c;XDM8vhUSp5YpKmzu2|Q~WoC z?r0((J>FN`$3b4y{9LI$1Lu%!_o9LFKgfRPZTEQrwg|5eJ})(2IbHE_is%7(w}V+vf_L?GGj2j$9u&knX1U)xr4RV{V0RlJ6k<=sV?hraS|4Al3US zv)}kpSw5W?Tv{@g=Julbo2S{2w0p9h<`lJlU%?Z{xxO^vU#UJ$;eZaVTj)EOMjS}= z=+R3>&kOsjmTnyf&-L6Kyi|eZ^t>J|I7+=#=E*1?GWHDMi%uAoPW&s*w=@3=UVu)ybIQ6Mt)lxN{Lai5yoJpve+7Pf%OdcQJ;)m#WRsJ@e1*9kxjyh2 z-~|8=+3;G9AC*O%?XxFuSN3sSQ`HKYa%+I?r%d^V`{D0>86yAH>`qQ}V&V*fV45 z_I2qs&}H0{0Z&Fui2KQMi1_Urynh|~qPv6LzpA?)olp68aJCiq19?%-UuBwd$c6Ls zrnM1oIM3~^`J%x!dt@)Vj(qf=319RZeRpQAn&N&iUley70d3`%(qThGn8OuJlyh!^i_$E&GDsu=SPaOQt z$CBE&>UqJBgZI_lSUbyD%jG2xELd;y58kAFduy(m9r>N%;l*>sxjy%R2PxP0I`vZV zT%nh$o-2FWw@>f7nf!zJAH;L@W%QdN30^CPOOSV`6YW z@IUBRy4N&csT^`sTp0D}!9%Xo-f(z$E3G|O*UJ9t^|}i2r9MS>=Y#RTQT_@Z6ZYe@ z_N87Y&i2PHYx_+cwa&+NK@IKO*^h(y%0>PMAEW20J@NWViBmMIW;gW*w@%KOW54)s z300Pr%YF*?pgRuygDTfIhURwWiyC|ecuY8dr8wKJl4oE(gWVdwjALl@_oAtG%E|h0@gua8|e(?JW zJ$euAqX(~#=PTxmvWGX2{Da_rY@)vNLgFEFkDm8ecwbdkO)rhjX`Mh_`$lMv_~@&uyGpK)?>Oku*OAZ5 z+#dwD7P-Cx;a|N=bGv&%Mn*g8(GQaS75@*i-JPU3;9$!2 z^`ZYkaEiv&?d#HGpeym)Und?ib3c9^`ht5~yHBg#kA_gbUG)c**K)7RI_;a_`3lb! z=lb5IJp=mAxZ`}TxoWfY+@3L~dC?2n6Nf!R2l9r89Y`r&E#E<&+m$c13vnQ~OAZ;n zROS?6zM4vYXZXDEKe(dRzFm3ZxaU=<_oB#)?xNm=(RT)CTkRRpcW$J7J9|uQ{mvgH zcbA+D_E-O`w%0u5*1lBE$-s~En{c+-H-SD5_@a2Ouy4OZy@^x8Rm*dmM0p0j<9tDV z=YGUzKp!WlQ#bJs8a!m~r7G@+dakfI&b>u1(q8ob z-o#4rhBrx0W+UYp(02yE9eW1$JBN5aFu0k#OPuSwmoh7Hp>-_rWH^82;##QpSD#t_ zme6MLTAABR9cI_WYfh2M$yCw%3O+CR2Mg!ligmQSZmC()ML65wA*;SK_Xj!8fLvco z%S-OR4bAqx+lnu$a(yb_zSruI*ghqY{5TFK&Nld>yl+QNCPDb3;C_J5@Ok1$>k^u; z4F6#E=*8L#;1t_U-d8@Bq$~k0kwsWcP+|+VZ=Jto^JBS>z$}{Y@$&2!H zwRKvv_B;PeczrI!7gcxXT_)eeP{=z2b{* zZ^)ASV8j36;Z?l}da6*GRcuVyzI4Po(%Kb4PGDTWYD9(aPp7J zi-o7;JIH&6M~_y~-2Mygudc-$j6YADqQ`{;3Eu?vSD3GiUaHC=W4^i&7-f^c!gDpb zso%K|t7h1E0rnL2r@7t8w;z=G3jIOMSALYg+EVV9@0T91!k+xj$hY&J0bI2mrhPm3 z3>!vwBcInS&Fg!m6$dhxda2A+yKQNSn;&^KaHz+YK~uZlskfItj&q8=@I{#ei5`9T zfv)3v%D&y`dA&&a_D3j(+==>w9}@=>Ud!<^U!h0;6ZyP&zG^0)m+B8<&j7#kb7_0? zmhDbH7DzoW%vYQjWnZfD532bJ9+MdH(NEtwO?Z7B2ha1&ll_(Q@OG9yj@mPDo?(UN z)`F{M^l{XD#k{`In)qW$yG!#1rTv%o?TROZ-UPp|;E6+?p_=%j*k8d5z;nCWi-ONE zm-biarJD2Y@P@PBd7t#?7nWR;|3SqUMUUQ=135(Vuee8#|3TGvX8x7pA#;CldDy42 zX8>p0%&le4Ht$8*Kj=XBL3{_@bpA^D2eEHgz6so&j|oo(Ts6)!U@!VL`6ifCgg(x+ zu6OI3e{iJ!pt+9&--P+@Z0xVz@%}~bgXWw}>*vZ_@}jsqRAB_Q+sAQ(m-$d=n20ZX!?I zG~yIBQQw(!GIJ%@cmKYHQk-T zfpksnoEuWs{pb;zuYRHY73Ueo$MvWFAolItALREHxV3F5&){g{Y$JbVa6j-J%$NT` z_~`BAKKSJNilTn|Luk+Nm}!5tHNlbYgSb2UJ2_staXeS&`T|$XNDmZGoImjyc;Aj* zDmdFqeAkceK^_x)2g{58B6B-@^fNWT9UMsTui)WD&r9);ad*ZY2RRwFZ;zz8oq5Q7 z$5DI+zB>b~<3-J5_fHJJQ3iigacBEAoHaBdVHGJ2`Vi*ml5xgX$PAusxo4Zj^cyuLk@Z&y5IyssK4-+nQ081Y5T zo;ZiZeO7l+$e2Q|&=T1(nEqBT8H*Qq6 zk1O>D?JRY%MZ|$b&kOfK@I|x9=XES;P}(k?L&hCv8|B;azQUeiqG{jGe&<)EM-QIN zn@8R`TiDRj?Sa7|#FJ^=O9ihFxjufblsA0tfnQJO-FUz?UvhoSX8`wu{|A}hZjt8- zxxTkE9+MtD_U%n0isbHW@I_T#6uwk&Ao+d8J+C9x4tjruyr{b4bQsrbU`3a)@?0rD zjuYjO?WspUz3ZkyBRtLr#zfY~-P7C;=8HaiZZ~n&+N?dA_^NfWO`d_@S8?>e`WLsH;nGy->*}{HNR5>_m>s*TOVu6U;W>`)R#Oq51MZBm~dW{^XAE1pXTqp;{q|~)3HK&A-;SQwzpL%(xl;GRi_zvA*K)75jp@0e;F$bK@nBiEL3! z2j`V_67C0h$mb~6H<9*Np0Z~!?t|vLb2pmXU1Z;$xZB#w?>t2JQsFV__nyy0%vF-Z@mwkIAk_6+~f{C4E}#>e%Gc!J(noWI(4FiiLic&`0sF_I}Y#L zdA_RGJmkb7dD2Vu)6Z2b^}PD+pG}-1-e0kgzL@+t+;{eKY9o75&NBq9m|^06%$Gh+ zTj_b>e^BKan9rbksm*H^<;*DEC!B5Y88EkVe{f-B4ehUR$7%Cp3JNBJwxx8pmw zZCWn<4^FWByyTGuYkekfkr!3IRP^XyC2u(2ah^Rlp=qmI{E!toCxiZ=!Bs;J8F_}b zDQ$>b`>Xa^DqkvmUic3F7IQZ=+xxen!z9;-_Z4~*k690p-}zc)4S9Ie`b{8y`^6=1 zSnMqIa>sFavMv1&nmxSFrj_R{C*OqPKr*lIQ_C9iJL7!?{uSSGcrO|te1>U9)}P(c zaHreDg9AN_#0!A^RRP^`%v`kw;mN2x!&c#J8-5&k!?zI+x!fU0_nqN)=J|^KgUBI+ zC$n|_ZStieCxiVJ_foOHsww<3^I-C0^uA(VAGozIyI;~C6MP5R!`q8|6UP3EJtoSJ zW84Sfo8VlZ>ZN)&J$J6GDyVcJ<&Zgl1s*c??ZL#8VgDfeQo$)wJ$j?(h5eO}_;HX! zp4X>rcoBJ*wk4!n4v=pG+*&IlJStuQ-d}B{9zFNGkiYUG z|KRxA^C#;n&lY}9y$R%G;G1AxAHIXgGyD;=KmM}iHQh_acM!Zj&LQu%wo7zKnY+P{ zzJs^w{iyF;O?(D$if|vq`^w;p@_i5x<r&x1}RvcTtdynw?RIYEY+;>qB1D(e4Esp3K;|{?f-mFST_JIe_jsjwYTAINNvhcM$W{Wb!Tr5?4*} z+snx3#rHwxY#-EIHS@k5erN7G<2#6aJN!5^oEG-kKD=~Fw|Q-a&tN6~75lt`PxYt! zpz`plJj2AMEpAqwZ%3}r?4xJjMCb;`ly)YM3H&&i+rioH7coArT6|u)-dBbW6MnnO z^}!Q|`yhJs=ntlmkG?B$Ae$5J(*CMtgvz(8=L%dkBj0Z8oA}g{y6lW_wjXfaeB+nX zZyi`uoRT$SRjhSpqWzll#21B!7ru!(o_WEg)bq-t`AYS?kY|7=t|cXq@>lRp;6A9H ztBHO;iWi_|QHJy;-lu#!?-{@sZA&>B?61IQXx9Eg<(t5BW#rqz7e)RG|AXf5;AOoR zg>M3LJA2}|KNv$CNHt&C_Hh(nbTH+w;=PjT?hLLP_U%n;PG}x7?l^VSA7rm3o~xn+ z2NMSpoFc;;9INP{8bFL5H!6@>11$VlAE#TC%`}fe@8NO8PMSbOe zkn`>QzPd*_UVBm)lA_bV{XTH@Y($Vr|w?!>(ssfhSAZ! z*0BA;{pd08-6`)i!_|?X$}|W{3=)($BF8$x$99&-Bersnmjyy!{ILsmIt+;NUmp5cl0`*bfAJQ>Va*tZuZw2@va z{s)<}9VC7n=C^}?#k~pkrP{?7C1hCkFHHc(;EuDMdK2J_rY-v}+}rOo^_{^(M&DWW z2ifO^{T1e`hjcHsSv+wep032Lg(q$%an=4TFM5jd?Wv}{DE16{90F;+D$jeC_E!cE zd58E13#Jv!ucMp{a>!Xe<43L2+>f6%XPfh)nA@9LMz~)bn(uu*^bg%j#ogJRd=pV; zc9SRWiPdFR=R}8;IUD?U*o(&m-tddmcfMyCMBG|%AoHo`#k~pS`p`?|-UQzVyHS4- zJ$m-hqwlQvSJ#hkruoY7rRJ935^gQ$uN=fz#UsADa%%-Z68-11XKx{nqZ3lfnO>(Z^BwD}G;H6|ZHtKk<1nx7O%8 zdx?LLc{0wlzxpTjyquCN1+;;Q+OcL_Y1&Vy%Z{~&X=)&Jl|Qy+(W6B)F>LLZ0k&Oe!YscL_PoD4XSgVK(X zk3K8IP2X|so^0)RPWPEKYE8dSgtN^(FXZ}M#Ycav`d-UO_nM)3-Z%7pkhve|(T}J9 z!LX9+**)k!*ghp_Ly+cv;En^HOk><_$|0lY#eC7HDK8q6)4b-3#Fwp0iO*p4QrTky zJ_FBJ+ozNcpVueQ$?nR{nYdP`GWc;v;vksccc`O$Z<=}_nnzYU#InkCv+;2>JP>rn^W_i!~c4X8TQ`fRJr4TQ^Y!1=X!P+Y$x20X*;iMyeD3OWSx_NhxfZN6KHN{ zUSEcdZ^A;)mGbc7x#AqM@|fJAxgESd^W1(@b3f1@#J(LlWaRpgZ?6yijePVT!YS%G zu(Hc*b+Ko5={}CiGk_-p?gx6Q>`S%1JF8x5bWZb{6NxVqza9QTf8xow_s#E&!EhEs$b za%b9KaSmDe=;0q!dVEiJ>{FVkKWdsvs*i5{|V zKS(*5tkK>32T}e?@sRO92)<}Q>8`53p7U%fbW70tc6|NqK z!w)9DDDF7y4TpEBg}7?mo4DWSwJkVM{s$Fbv^fRdaQM8~V}d)*R_YJpj)NRB_U*<0 zpSv@Ac+pF>AGvwj*7fA>Eos(&Qh3ATU^ym+A{z~;y4L=Th!*R!X>%b+s$Ng zJNgbz?V3Mmn2A%Q_U+7X=lm7sD}&F#K6*FtTJEQPd)wH@=9Lf!5}fVY#dGY(| zWsgmRe7fGD?;!XL&g8Z1v-+3p#U&pcTy$n!T{+!xZ1>1kpSf@EfMsGs( zyf9xep8?O+NZ~UW-@%Qeqvd^7BwV%9;R}c-^V{*Q<$n1y=znnbiYe(K`J2jb+{eF4 zvm9*2fmHi;=3jyPf!>5yQ=wZtaX&cM2R_54z?bBI5Z}Q-y>IWk`bzeql3fR*X)g*d z0Pcfh<7zDX<8R!zzv@i+_Q$Lr6R+wvJeplC`FI z!GX*4KZxhb%kO*QUm5xKZ+n$G%&Cbxwqkc_-rs0{)t35$*k6GI$^U~}#A9OiJ5Q)R zM_zzanI9*+S;rPI{$0EP2jZ*s+;09K1ZVprYl{{CgV?vXk^L2XsRsY* ze&0mPniGk`iO;aN=;{3-r*2B$ImS1EJaGj!J}>(#*N^9y`{(k-Y`mOL!pP9d< z{Q7Zw;%t8~d^^oo@TGFkYZLJqRL|=#>ldvYZ9OV{QTB%Cx+U4<+mVy`$9?&Bhr~+q z4~7$86h1HTMcHG*&(-zNJnu_GUv_WOcW3zMdkFV~bA7nuAm83q^JEIBm&$ul|zIva$mL-$d&S|swuL=LMtfaXe_d(t>;5&%t3S6~@luH|ZcTPT%bGD$t z{?U$u=g__#{PukET4HX;`wDaW^qp5sIb`s!E|E99V8H1yUVcA@uO=V8;$QvHYlp); z{ahJ6`qrGH2Ff8{T+*5LSL^$gyLi|Bq26=Zf>~<7z*4NhSUj_@X?w7idlq zdZ~D>%yT>b2f0Vj^OfRk^PXV~eFra|UU*=&^iuuhe{kxNth4Vn+!em)d*n+sdw7d! zFX~I&kATD&>#N$20}n4a+xQ>k=L&N>xN2v}3y^dyZuj220cl?*wb9%UW6z*CkhnX; zYw1m#qH(nsDTfUIAbZ2vKgfAe%;*u+-N?!8B~FpS>-$~%yd3PRDA$MQYD`jh>&MAoWnL`&qw>PZ3Bni6p!d~yn%mX; zs-5KedK6@2z-#$cQh(ve%%Z!q$FS{_)917huMc+|FUnu>d^OdLq$ z+s*eu#b@}P_6+0V21K}v&ud&=B)zZx$adF0FZK_z*YYKLU%?yBJuib>Ys)F>tG!Fe z$uO_)z|u@hKF#gCzXJd2cgZtg-;O+kzvN_moEDNdocqqmA;*O+BA!fer#k7S@*M|U zwM98TrIl6v&rQ551V0Y?&b?RHYJNNZ2Ok*MJD|Vt`fNFnj~ty}=A8RT z>WHL1*1e`X4!oAkRa5uD&GU<y)k!J`MemmcBkn2O=d8_bbs)*$)V`-6uqgA?o*r_L#vJf5Db zfSOgumh7&~>!0?e^qn7*{FMuF)wWFALiu)2ny-w#=+DF{`YyAQ`Zy1zI_HL!bvb&h zy18W-^#={FY zHD=}0l#}s4b@!S-@vjyUuMhind*O?6ZvuCmfP7!cx1*1fOLM!*Ga%oNp4UOii^9VT zkIB;}?-Kq8{iTls997sYeseZ*fsSIQG-_$JhMQ2BAj+xSwSBc2R$GH&ueXy#wx zeZ_uf_D!Jg%=@eTG`E8t`_jH$eFq(gv#t1|zT}B3 zp5m_WgUhJz{3v;s;K$k9iqF8DZS=fWczK4Ljvf?$PjhSYOdLqnAH;JNR#JH|{ER2{ z=xsf5cSEzhYlpr_`>RKfhL*YJc1(RE=~3%}NQN9Ub2bZQ=wk5PI?j>_Odi2O4&&r=doNe3(gIf8#@Et4`{#A@`j(B*v zN6&fD>sSO(+j<2+i$w(xW%{?dZ{Sp5d}(zvLOz-T6`BUyT;ugxa?= z_an>4Rp%L)Q{?cZlXL0Spc6-_H_?Xf&VNt)AkVTp(S`%*Kzz~F%YGnl__hIUspkdX z1m~}U6B|;lY@AN~E5+-}4}PC~srk!GXnzHd34C5}9`WD#^G1g?zKOBcQLB=*HyqzV z%eKsyQTbh;`Om7&V8Ppey%WIfhU9iLF`5GTp9kseU!i2EF8$M z=()OUnyjXx1Ri8zpXZd026a&CXsG|g4R`>Kq36ZL<} zLoT7-#7rlLE4Pm4=sr&9imAjGy?NY$a(#+_#eE$3ao`1DKEwSzdNWsT67farD9?cW z)s{hTc5SG)6VA3%N?Y+-DqkwNwd`GD&Nk+D=8GaPin-k-w@d14Np9AUsgHyILEBuP z;uIB4Ya*V^`&WZc9Hsss`*FZ;SN_4TdQBwXgyEw{PR7W$vmZzC8T`qQvz7Sm%z@N^W1)&?t`3XfOiS}c4L2a|8r%p_gCKX zzEXFbyV?_n{gvwD;Euz+3H83()8%oRulRouJmlM<8>mO`Xji45EA%GdG5Lvj$l%G~ ze=uGA&W$=R>iDFS^ZVo<#9p+m@cNju9YXwek6|U6hnzw^FL+E;&&%wI178$ewUUO0 zZVwI)A#eC*x(~t=r}9_t&>e@lwd#KmUduxA@M16eL--mKzn%Mo0X508zvB6d`-AgN zY`fY?_zc*$wh3cZOw0qw^p6|;`WGtkb7Ps z^uA*65_2FUBJb%v1Gu%=i#{beWaGJl&uf0D|nX-4kW*?jGPSkqAJgz`Z(aXGpFbc z%D1;4_XK?hm5(0WT6it1C?~^r95?HJ>e1h#|3UOpv1fQ_K{atdaCZi`_Fmsk&fBlf zIZ;*dXu*aI`{WMR&yxny`^sv{$-qCzd4@CLlW5NX&bIQ=k6ION4bt<~M&dK@eUSO0 z1)6`=(5=MOn?N6j{mzQ}v1!1MW8NSRB)kCdrD8AoL$937V%(ax!za_348d6@eZ;$Pi}*&knH851{9di3ad;eW6-r-=Di z;MNYK@1U(0V6*n{qUUv-?#_Rq|3Q9Vah?HQ0N%HEZ^i5L&!3s@zoLybK=)GHkdJ=n z@aLj+$?SE>>gPdo`$hU6L@%{)TGI#z%D2xx@lUxAI$2L9^-tTEw`_Nk%kBfyuN1%i)R@VB--f3x z`;>YU_xqiVzH@iVU)lOno2c&$K7%K5wkOtB)BEb{%>Bd{HRoh-$9acxeQMv{E1Tfa%6;%1?X^@MlP!9_!d~>9vu_^p)p-VRAk&NAI#6?Z z+l_XVznbiKIy{p&+f9qsQBDT=_WSn?#{P;q+wfW{PaM1e=sUv`2OmA=cFrLi`77oW zv3E)3uh5%l?Z;_D{44B5`8&vYQRZL4V=_^C^xjR+p4(LwB>h3|JMSS+9DJ$#zKRPw zv^14?GUz+AkDmEg=I5JQ#MytFEj|ADOQ>FKV9Kk5oTK-lfiT zcm62(+svN}Z&qG7=^@^5c;e8TXijK|&7-;9f$lgVC;m>msv z{#BW9ww2d1ROhdF-;Vti=a4;VZl55%RLt!?t)unaj=S?q?pNu55FQiGGjL7@^Hubj zT?gT{d}8%(Ya8L#Hn;N8-;uua#1O03iXpkC{T1iimzdsHL7lqy&l~4*QdUN@R&SilW%`}boc)Lo$gScLG7=OC$^B^*_hkazP-WHNPTDYyrxRNopXKg zc|Cq#o&h--=0IMHxka8hc$fZD-Sy}^ooCpe{B>qc;qR3-Cq3vp=t+JYcmeRf+C0Bd zJiPB-oqOVNMX!SN3}?;jgC`F8_V0SVG3?#RX>@l6U-ZxSSJ<~Vrd--MeWwrYui%?7 zczyT|vfmkV`}(s!^uAI#54L_^dC_-}dtU6L$J`D-4)aAfl7|=h_Hl9jBYMks@D1XA;JJF7@(j#X zvpa16eh+zg(Mtut9X#Yu zm#(#JPiR`)XI|lyUBl<~@pp0{4y5`Ha;`7O#Op)fS@pbZzk~fLCxh?cmx;}5qUe7R zb9?^(;q#i-wV}T82dBe+J3>wO!SIp_^28ZFdhpu~-^5Fl7kya#&d7`6K8X1W-tY>| z*@oX4JQ>AR>!^KRnbaRVku)G}Z(iK)6~}^1+z)lf;e30+02ku7FF24&-$8Z9!TT!f zY)QktZVwKgrS}X4%ZsCmg9AMu8r;xrd&Apjr_%n4bA2OMt;$L*ev9@D&g92|Ck~t< z?oFtCJHM~k6X&FJGRltwPSK|M&9U_fZ(4p>^4Nm4K9feJ(R>9@oa)i@zFl!YaCbI8 zSID<>FLi9);xiu{j47$jcGnz8?5`Z??reBWIL|OS#3Ed^{`&6BUVtYl*M}ax!EfiD zm%2M6FN(f1INO{<1_#o7$JsIbUCA?~Sw3Ex6c*;Yd30Cl(GT4J0r6zODRPirD&KMN zKj;TI#2u$Kzuh^vYw9c1$3ZXkHudP)kHa1l_`Gh@ z{~$Qq?InkdzBBe${}pdIc*x+%C?Eav37=^X?`Az;fwOJqK*Hy>t>K?OED&N5Z;;Lb8S6%?4KdAaRO?~Z!`vLFLdHN12U+TG&w}~fH zp8SOM<)m(@ZF9rQ9ySoQ8;XV$(uR4)`ko(T;#{ploeM&I#MbYz`DtiWa0l<^_ zCVE0h5_xzhH}yYPPJ2<~e-J&d0J=N737_Fp^2Bl98Fw7D7iEtLyq1bv3y;ZL)Jt`n z*H(MOdEbtljOq`_zRhZ(@)52hkq{_XBxR_ke*B zqvNhvKB8VKIFK*9H@Ez&`mv*-Wu3{FihVo&2X9vXd~zZ^S6;$Z^RnEd@1WYZ@4OmH zdj`&n;(b+~7q7pA*o&^61=M%Op5c)8TB1jf`3iR&@Q}Zd`HFpB=uKdMwQX9?{ATKTRSW+r zllblMm@r=ydr|D$*=vdC3O;)Hybg+YiMeX<<3yj?eK1Pr+f`1+PB@Umsqfqn*I@Zu z{5Li^8P%KM{vf>JPwx*sb^ThX&R>1JbiHLW-3JYxjOsgMzM8dST6!??8QR$R2USi+ z@!M~Ze{hef@63Bq_~^lJkN27qaw2*N`BE3s9Y^s+TMh-$+>U)adJ~*)=bjhxS5HTH zkS`VU75fLlfy8}KaUl7Bkne-4KiF--IpH%Pe}(;3TiUnde^BxI{;Iu}%xA#dZb$j{ zRm;wXPY}QJR^p4UDPEoR3h~>)7v&r>b8ACw_6(S>*q8b?<@(Oc-Ff7y)mdwb!w%G( z-b$RJZ^R3L=Zg2Dc3j22Uc`tf0dU8m-*Qy~oZg0wW5WQ6PF7b1P z9P%vc(Z`T)BEwQ3UQ6`oeVv@H{2_UU*_0Q>bCpZ?LG``@_oE;o)AI4sgs>RhcV_<8 zUu4hFOmn;KUR3drqa&-yyQF%lJh!tK;Lkn0M$hZA$w%LzRo@x&7544eU$Ni$p7c`j zT(zVBLGICWzWvYl?SmyJgS=?I^qtMU)aH~L)Bhm$qFafp)?_Kv`*zMV{F&EM-EqKY zK!1>*tIRoDCr=pmo%ULm=KVeGXi`V3LvnV;V+H@J2qm6OQ(p(#GvJQ%hNWgn=LOk5 zZ;Z+^@npV{{T1^?nOi%Q_*d*7JWsue-t-+T7vBVWUIqtJaf%F2+&y|@j(XeDt@;6Q}+ME66_>CEVJ5 zNrE!Tz4b!d2T${42f>7D<0lan<;a zgZ^Nl$&Q%>f}&|L59p*LebioawT8~1cX-vH-veFm1(+>RbS`zFwLHaJC`7gb(>!SN@F z1DQM|m%K}Rssc(Q@DcJ4f-kCa$UCO&96rBKpi|4`I#Zq@MBZ2Ed2wD8 z&lSAk8N_dgH(c?MpV3^kV~K6nhUEm8?y7oLJaL%Y;iG4s411T@KgfGg#VG<`lzDv> zHaz45+K)4w_zVxx{~+=VJ!F4%i{>l#d4VT`o)>e9(07J+src$#x#Kt`x3zvr^A-4_ zoa<9_`w-%)!Mk*NOo4cK(RW^vHH!Q=imPV6i>|qP(xva|KUa-XNc@x9XjKY^3~^ONw1$rHvPWb22<%;X7Ds>P^6x z3SM99zB9gq;1r=Z@iXy7@5r8^xo>;t9mH=pd|u4IvK*Uj;(lP?t~f>C$a8f?e+N~* z9o{9(?f0lZm>;}TdK2Iwhmao!{Hw{t>$6+qpBPPihRou52QG_`{u`S-1Gpc|DZ>9C z`p)oLwzWDZr)TskIBfEU^S=FFg16=Dl1CS;p*_Qg)E{L3;C}6iW3C!|;&NN%WRPdr z)y1XOI&_2dovSH-)iiD(`@FChHF^`^ zw+9HH!Ov-apAU$K>=0W(zEo>ilGcNXuhjeM+wjSLUz3kMpLl&LhYSwnMd1`}m%XU+O|ah?^OfDB zh4jAiB`*NHmakFIi}P3D*5Z!C^A)~>;J35iS?xuiOFK?}XY{-dSIj$6LOn0ui*k=% zacjXBomhMBW%|W5%vr!*XMX8z-exu4~7>{=}9@{6w8sNOXcny(&+~ECd!H5K8o%*;B4=ydhVQ8 zQ-PaBJiIT&*NKmw^9(oYorL=VuO;?ZiUY~sC6{r1h_hX7`858wnEKFnysuC%75OXX z^}U|tX0vC&UX*)Y*fS`8yWL1|ijcp89|zuW_)^)!tL}rCuh1-$6WA;J35aQthwcUHXo^;c3ebT82`dVXOGOIERe=)p7Er z!efH_Al_FFt$g&%XGr^he4UMd&t?1ncVnzEq%_GWDJ7C}$#qFdk~K3U4KthBtjT7x z*)aRu%rG`%lf`CsV-}KTMoA(mNy(KK)ul8hDand4yZJp{$8ntJ`~4Z;-~Vtv-tY4~ zj@Rq?B%jw=;xq8x`9;cKfrosk+QsQ`5>5paz-+vlw>B|m1mUoj6E97v<*1^(4_ z;$JCWD*JK3Z?{ukwC|aUL&4I=frqzOb|rb@YRS8VT%See?M_MeM-@+acW7kKQ2icc z4=>-_4Gtvxon2exki$K%T#h{P(4O5T50WR&+?#;k8GYvo;a|oNPN^dw{rMLB_RuwL z)0=I(Y(3=OuKYOMAH?^{>>os~&+Bj$%^BcJMGpBa@kRY~u8;3mhS!qw448{{E6mPv zO>0ZMKICL<+Dv<#sq`K+{Lb8?52v{(=2!dYFSl;AxKPgvygvBo zac>Wn{1xwUnAbPiEtWXj_mIaV%le_@+wbWU;d$lqj1#^0RF*tQoT4wu!)y3)IM-)* z;&84C22ALDLvqOA6t&WOoE^k(2cN-{?pMrjzeM*dcubsH+}n4jeMkSnmK;ds1z?Yf zF=tT!!6!1y^Hb=4g}w9WAse-CVmY0wZ$}o?(N7ksP7f{ zqTKU3PWdbApu({&@Z}^5b-mDxL7Q!yab}<=Y*76Y!XD ze~^0<%>7$eaQ9EA|f#UouzxgWRKM4kYI7%xA#4+7LFw&y#q4x#S%{nIoetUvLPNtmrSKt(}HyrbJ&R;F^4+uXN z|L>H4tWC+$dVZzySKwc%c{{w8>%v-7fQ?RRdZ_aOW@oa@7!fqfImAx}OyVe4~S3PSLi=8hPTdAB>B;L34)x{vSEy=WO#epTTk7jvTVV*$$@nV17lY^yu*(gfCU` z`uKjO_Bd8~4~A)8-$R;*%)SZa8H`+?;(n;PsOnAp|2UA3$G#}NROA^h4Ib?6qW^=K zi!%2E`K$BCU%vF7$I9NLhi6afH0zgnW2`qVHXi^&D(h{`UCk=^T{^>e*3NO!PUZ(F?fA=4;Bmms+4jv@Wh$t3^VthuPUJ&a@9z` z;GbgB797$XNX}oumx})20L|;`Mx3J3i=L8aC~T4IyY+iD(sXY}{t9!^tK_v*{C4Eq z6`z6g47=n%X!ugW>r=i7^&YejTCaIBb12t0%$Bscx8(ZN_iAp?0?HvP9=>#zpO zU%_i>ya#c=vXtA7Ot0B4IT^DbhxdcXw;Sh5<@!|q>W0_d14H~u!%D@Mx{3Irm|wj` zoTABFYjSUlazUT?b$uI|!b28ak-H0dimheRj0*hu| zOLDUM%A5iJLH5yK(VXoUg@=6C;lttuP<&C$uQ=DoerI@ieD12V<x8?iRv_rZg8fA9|SJEK2%O8U-YF72kCm*R^yxA5bra|Q3x z5&91@{|fwe<}-lTcTo0&=Dl-EULWS7C6X7#-noe83}thQG*9MD>7_QNhmdzEHD_h% zjJ)++B3_fl`YU%gM<56_Ga z+GE1~L2$Mm`#8Hhjl7y9xxQ}XoA99g6+FC#CvGjUQH+8gpqH@{c4ov)|$NloRfiXLiy-NrFOA>DEuqT+i|XpJVS$} zUOaKlz3y<|LY_EqYdP1a`p(m8%B7Ek@6}V5gOcm}j_z0ZUPbO*RcfRBRha!I<&eR@ zVxJfOgS;Q)yl9BuT>~4viiA^yUaIO3g4g$}=BoJxpN?6w;Gpni6lZ%n&D+gBFO_F# z=YF$St6}Q{3tMm?F~55GQqA$pyK87J%Dz`e5C;;RBIIPaH(~fv;kD#*g?u~vak%Hj zUQ5S31J0G2x5FEb_h1hPKaTm{4j(=DQeDOGY;YjmY{!I$jQ=2fsR2U@$7e@2%^gJE zaP(3+*H>73v1$gLtCyC<%DFOn6P^QK^IIRbMRI-cT4F!Q+*;N1!kod;yM+Is$}^bf z?eDnEs9AUB_+75@iUQ6FcQ$7s_Yr@PEWRWFtMIJmcC z-i~`a^6lRJcXb+dH9_y4;U9EOk0k#f&lwzZ$n3{~#{|4S#o1;*4!;L+uEIS#_gQP= zKswW$VcXExC4Yq;z1j~NIhiT?J*e`cd~d%!p8@&ya+$Y-hdeI)c>F-hi!!&iaKQM^ zH|rXIa25WQ3;Db*QBKA@X8>p0=sV+n1s*c{2Rn){6}-MHyQ`1;$@gld?mJ&iDxO&o z=rqh(czv9|f)_yj2hkracd62T9OSRy4To=neW~!{Fuz@KKLY7{Ra)QRb?3k^^6-MQ z&3@+yzYYT%q&M;E*57h(q=%7@Uh!lKYCSc#mOZ>q+BdP?)bql;y$zkKgOxLmZMo>~ z-rUQDyh~S;oUBHV-k7)lRP?>}E-4NqdS1x4bN;H@>Z9}R>>orQ=i=%IX)daK^vyri z*S$i$iQht_Dc8q)=QMxIpbcFE5x8r^V?~-{xxQ*`ZZb=&~E5#GHR=!u^!oNydJlyuVz18vpoNds=4`d@zLLtF)a0d+b&bz zne*-N0{l!l5>;k z&kKDV_Aa51W9*%a^m~x|gMrfX0uPzxqvw1(di2~&ZTYY82(K zl-CmHO7Z%*M-Okf;)|j`c(&-T-RGp|#U5Vtoq3N_6zEL(tFns`$3B(!U~BPt*~^oU zMAd9@`Jso;kTT(m^0~tI3Vj^q^IAlnIP|<~awn5_3B6QL@|cv-y}fYZX5y-Gk3Q%0 ztJIrlru)_Nrk)qhmD)QqSB?FHCwsgjduQ;kj#0iHyuL%y$Kk&720dpm&qdErPNqud zuhhKV#p&PFOZ_|I3iVRedk{V^AL7X*wCE42zH{DyXMN9w&Tf&DF}_#uE+L1k-h*M( zcZQE1erM+Oxh368J}>aEoG$!xG>_(2A)1F=Sn=mkw+q2;Q9a)sS~TH-sHVhHOD64c zl-Cly3BwEUCG`h+kHh(Pi~soW*=%D#m;34B&#oJz7lA5!^#FOD1 zGWyP#i)Kca(R*+k`BE+A75R^49@ji%#o1=>Qqjz-oLV;Z=KQa6c9- zejxk3l85%pkiB!>h}a-&^oR2^XpiF^RVJP|84J!y(_^gS-IT zo8Y`C`0YdWdl30{%thhhh1U}IEAZRL5>E!-t48UiHWU9!^_>-G8~Ju?x#h?V;5Q$1wP@M+}?;mI^V$2sICi?93# z;a%cfANJ1pUUjz(Pwkx1IxlAXorjNz&udLUo$llC-dXtv`F;gIj;sBS^w2faH#hHh z{;^K{ILvQXKCi|83kMZ-9ozVrsgE<1JaOoy!bk7u$0;Tcukz6wbB1uuLq=W{+>e-^ zo2Bo}zSNZHxwOX#@w`TU=N*)jL4VNsHXdG;Z)dM1_i@;d!`eX2a$h)Na&fsjnPriwNcK<^BtBl^`hG**h6}TVpm^AkCq#QEyMcva{NeUqV*Rfi7qT}$^X=Bn|2u+r9@yq4x(Ds#4xL%y8wx1KW?`74|&_J-F|p5cA^US&@5 znss{KSlZ(#?uW`VV1C7O(I@qP@EzJavoDqBS3ky#4L(76hC=aqfiH^x;7#gH1Ri`t zJaL9^LY*u42iZ5FzE{j|=l!7iUMa2`{)5Q%aSj=K96ncHqxZ1>0Dv&3~nv+kdf;f zLf79c;R{W6rRF=2zLJmYkQD%(6w=uccqieRb<3 z`CfthQ5g1??47sh{1tli%olA;52ZN+ya33Hntc=CA*&ue_Jf!+SdOHYZrvuFuH{z0`FyzuK$!gNjoWcdEkV zclJo1wkFJ!lexXuavYs2F!{jmffbQ+c$$)lo|76mpx=kyRfyy5yi z2%e1E4~Ch%OU{V{Ee9yqHSIm=9y$SZjP4l~RV53(_eJ;&K zYjPV*`77iZ_5N1E>MqLl;oc5! zID1U+y~6woJQ=lj#(r=P&D+tVH~P+;zv6RsGx?fzaLQNluTWmp?00rr{w4AHI4=sn zGjp~vzhcg|v+M`q1;BpL;K_t9dUZuOaX(xnC-auxJMYo?cIH6xdyxIk{Jp9?^yr!K zjqAIn4zl`>BOY>t^;&X#+*wmE)#boc;i~Plb#L*#0>2%6(R0>4DOVEyiO3DOG?g!^Z*^jeHd=o3=eudtI`CJ)& z9B?3!lPTzGrTrlHaTI6!%>lubZ|9y@+w>;e`?mXZAIC>}shqz$M{|a25xJBX<^3S$ zSL!`@LUPF9A;agz|3Nipz<-eYgENoquk2p9N^&w!WbTmu;CA9{bFNQ$czuJvA6cON zINJ;647$rAF!U8pLkZ7+OLoU4Pf z$0^nSL42>6&j2p~b8BBaw&`M9;x+G8^NlrQa_JJR=R6!j+lNVp!6+rl>iz9{kx$HilU-h}!O*39z} z{uTcR(Z_+;61@q$2fs*tlKeRAiTjqkOW5NKmOhTkUm-8bUdtDk#G5>E$TKki3g0XA zanK)Ro{Y-3BQIK?9%hfDxoB2tl-#e1geQ}=;6v*m6ZZq>3OQsoZ^xVgUVup1<1{^2 z9GFjeQ6ndVJ&vE`MNh7*OuOH~H^KWs_L%VZisz!Y&aYb8+X%0Z`B%#C+?agbv>)U= zL&c#f+Y;6`FFUIFqU@UprSBDTeFx?*mt3FX$tcctAl=*H1qjo5QREr6PVkCqlT=_? zNqmOHpqvrjeS(G8hrTn<8TedPP%qW+4}vFiGkJF0IpIKFB|bw-{?*QXvrmn=`fjJ5 z)bqkz^o(^s^}KMd;NiubLG5w6+V-Y>v+@_>$&9)5o}OP-&wJVGY`vOPG;;&J2R%&p zE6&Lnz6to~N7H|hdB{uYdxiaAJK}7E`>}}l49LlVC&P15=Jgc`4;lO`=E=Y}!T&+< z8Tfu>ClBuh>ZP(b{3P|flrPorn3!{Y$RU4H`5NU#8+y6QdyxIkMlTgS8TOcfTl-RK z`;7K^kuqm^*1Kl#`hdS8{wB`$OY|S)J`R7ckdslp3Gmxn9ccRDdfmj%>!^>zIb?8( z{K6NLcggS%x-9=(_JccQ?~MQ88^r4aXS;}U$m(3VUHIc@NkvdW@QM)HJ0s7)b5Y!{ zkZt{or}K;uNX-mA~Y#R9+N&oPX+CQGXELC3U}wCGH2m2aP$y-jqKR zu1DkqTpm1#a(&njw#@_g<9=I3+7ByF7hT(ZhUTK`esxCoad^&vbH)3?RWuhpmil<+ zyX2#fFmZ~S=Q6+DyvO;iYR2B|QftnrC9#@+1r8+l=;3$fxv0T!S9@o^Uny_+3i2)? z&#=D5Toil;=3imX0R9zx6X1(xn>=yY;}iz2CyxnpKUNC27Vkma+rL@)VcI=5-_&-L z>%*LZbI7@r>-*?7{401&F0JlCUVzrbXTV(aTHR#w0-)zrm)u}|ia13UxwnJQfF8Xw zWa`1@(d)_X2 zEIVHF`rwI!k6!tm;fdq#754|h7d5zQuH@nUd-dyO?}^U~a|ZB5eS}kVmH491=siw5 znlrF3RlNr*bsqoY-_&7 zJ;^I-3-M$^WbZt!VA6_di(G7>izcs_rukQ9&h~Qa0h)_~&oE8AmKEfQ!}kh(9CfbH zcScTz{e#NG+w$J-O8db-R>upkkLOqFT;V@>J>rdk-|0X2X|)&io$t&TK|C4E8LsaB z>9{}r2geUzHmS?3pXglONGc$1Z4-U3B9DDs*}ZT@mPeYa?d#Njv>&t{38!2ia>y&` zd&T}i_;J8zFgV*Imc(mrt)nj$_bbKg<30|2Uf@8&M~@ux^Tbs%=S9Io##|KsLF{q- z9C$L^cZN5-JN4*GFSe7tGkZ*MuI`?dIcePRjNb3de^AXC*kgiob+Fo1drU4Rd?fQL z^irAMj=8Auy>gd(d#fM+A^#wsE5%hqFV#--E9?g+Z%bU;xa>>&aQa?tFX<~Dle_!m zlNVr*?$K|Y@D6cn*%KF5@G5cDA{I?tF}0w$;`gKO7bcT`5Z-WC;uL)#xxRb*gm_-R zJoQBXJ=;qj$xbBxRTKH>@gBtdihHTbN3ZVf%-MFOoQ(3Ng0qc1j=}5e6YE3stG^Sj zMC4Ik6ungZ2btH`#kNoG?bRlp4EPMl$>4jXeDtLkqlo(fUZ0!h6fHUuT2n#ZC7dg8 zwvm(hp5~(b9z+fqoT3+KKj^gV7x6BY)Hiu~${uHf+}k(OT(p7a4C-9*f6z`|0Pf=y zIpjrM$d}6bcFrMJ2(Qmg_vks_?li2B<_zGf-OAbK_aJ+Cl^1|LagIJOK3CwX#h==~ zFRAR;)jf&N&^A4ozE^JKqhCAVIqIb{U$iRTk8*tlU0-SJedhf`6SvJZ-P_rZQ%fAk z+!2XE^XcBck@BMIT(zP;4)aC1H-Y=rpGR9?nCv!N^6lUuV}8YX2JWT8H^F;n~WC4>fXNt`3IRVTANTC zu`b}Z!T*syj=Ep5cj?^ z`T{72>|xsD_$--in`UoHug(qDJQ;Waz>_H!53k}B8J;-iY%>oTKCj|h_m0Js>tinf zIFS6k!haClj}*zb-(hp1-o&BG$YW)CkE8lH*gNOY9tSz(vvhCATvYL|UZek@>Ul9= z6mtf%hc{8Ymdq(aUKBiJ=E>v^80LFcdzaW_g1o58A!9Cjg!l~LKt8r7lss`=TI8>g z7u`HGn!MpxBqsy@75aly#YaEMExPC1#Qkt4{+0S(v40SpqH^(Cera!%o)>#eEYx>q zZ+KJkEZrZ(dk|bT89J9T(<{^K-@_$9w zcK>qR_tGAZjNW5~tH$3e><69n{pwIN|f&ejMX_1%5j`aqT)5*PgAK zy*Isd3C%@=?M}<8bEj;baPSSuAy4xwrE>+}1onfRZ)cCmWX+Qq>5#utK6>Q+@Gfb%`F1tGYC~Rtd+B?nyh}xa zc{7`mu3N@geB;InqS?S(LOI?d)LDUt6j9uOZ6rm(H;}#sLpXpYgF<%ooBd6y;L=CcjQ3w-Wj|;^d>mh$Gkol>ZM{X%A6whP zFUVXJ-f-2UXOBtCoD9#e)SLmFZT35x^X-_67Dx{H9Q_Am z4)MN7bB5m&4y61dIb_9ehu3m$*@e};bZ>%t^ym-ber5Qb!BtcDb~E<_Ib?V(;U83a zhWH>W?FS1fC$mX&YmsLNlRXZ46FhHsz7XOzllbl6As1195WZCSox@xBypGYmopUnd z8uLx__8Y{lRXuw4@P=;=S<{yM&fapr;yw;|GT=atPX5LEVai_#wGsINznbO@oEPOe z13bK#U+wW&)_cP6Ws|&S{WNc^wVrs$oNve8+30z-wtY$WEA~5MKM1bc3YoWOE;to4 zHu$@d>xGA`czxi0pf?dnbB4FbYstP;@EO*VH++U)`+<#KrS+>#oT9n57nUr_$)dh9 z{5bdz`UihcbB4i|rnxI3%f`Pv)*udW8cJd>ZPu)xN_7*&K2H+t&%nn zug@MmCuq%x`zSApoXorAn^0Ue#gpN4HD37boI`%r_ss!QI$x!HJMs+t9yI5W;Y&py zr%&vtG5^eucjCPLq$G z=U0#J2@~!|E}bjnMGL9th5HqH6DlXeduRR+8hHkAw&BNthxeoT3+a1xcb`blzb;46 zdr)y}m3K+`=$VHM4&;sG7~vF|d-Tkc@m_j)_3W};G8es{=IzMIfK$XCUd3mCZ=z$K zXU0pW@6}Jo$6eatvAXxmn){*VqR3w_+G&q4!-Elv>#XgSX8(BXX;JB@2oi6=uPl@5P8uuy0@#nGdyv~$tdp<`Z$?% zZwC+gR7?Qnkl~2~2NJ%CVN2$kdS2+`7<*^@2XD)_IepKJveT4&w{8go`mAzGZ1bHpNXK>`VKc&wV^JLI>h9~YT`*ZRhe2jWtq4FLC zUo?w)6W&pqCTtlxyXPdg))y`xT~Bk-2+flzkRJW(!c}vX-h{!wx^a2xiO2W6Tk>%B zyv5HE|0-^6$hIBiF~NIKdE#zTo}oJQdEZ?64?6wmav+pAMb0`e%5%}3o$l}7)~BcN zka53K`F2PDAkGzg!qAYm?iV} zWea|c@gr||yN-qAF+p!)_$_^$2Cq&7Lut-Xvapfz4Dg1tkDll4;K?A*Fk;DK;WL1L zrM#Aznum=2pgLEWU%|uM+U7={xWkp<$F^K-fM!*iv9c{6W_$E4G&Ea4$z&Y_b=}>=IZQI75nCt{jvIf?J>dpitktG(Vr*p zQls?=>UqJ#TiA6>V}J2YfUAakyP7kYJ#qW!KiG;mkk}7Sc56)@6Z0Mi{43*rRVLo0 zZ0mdT7e&Ve<&U_#PdM?f`pEo>?^h~+g?T&gahTUvpBx)kL!2Ub;?Sd4c~SQ8a=u;l z2fai3(tD75Ud;XAc{{kZ9i=z%4fW{H3TIn!)eJrZyy5LkduMn|*q4ev4toLMwM4%C z-h-yl9d5aqu7fm-Gku-X2k76)(V%)VAa?xv%i!$|#3iANZnb-mc~hMjr`68A&(osmPvyuF|zv|u8AuaJ|OUXU+d%NXiSz#9%Pzz6ef z(Xm16M%>e9I`vYO7vOo@5xeX1Sn?0@{R%nc@!?;a`p*0x{HM!*QY$Z*yDh!hTa7B&d4*s=jC^)`uLUI)kRg57iABx@_8ZG_eMZ{#BZiK zLpz@bgfCjYFQM$B^atH(e$}|&jri?6Z%0n%hV@@2Z+JjspEJ7-P2M(FJiN?rH_jFI zI198V4)?3gLt}bQc58Ftl5il=A4Faha|Z6w8=koJmK^K*^R2q)RY$&5%-g|l|J?54 z&`Y%rO7r&%uZn*nrGY$g$X~6c{h+hlufVNkK0|ET&V6w@Cu8)y_*{WoiyX4o;ThzK z8z~+WcrDKyAA4!1M>hHB@0?XV&(C_((wMYvW=UZ4bIsJp0cZO@>UpU>j=kJMdz_Z< z!T-hhi_Z)9E6(+?ALmB;Z@Q18?pK@V-H%@gq+hd&A%J@HO4r z4R3gt!w0KdNuGiE?TXjOUQ5G2XymUjzhaMxIe+yO@fnbBhu`^;Gw&S=+%|u0{j%fs zXKV{3hs^Im_y^|(EhJxRsl};9-x+z)NyI5)?gx8J&>sZ%!|>YP%jPZl_E_>et2qPq&L0q8RP~*) zcaG9IWcE9o{W$QLV15PvAbP3H>r;8rRm5-qihQXH%9k7ouBmkSy2tn-Ma0>D%JK<$ z;zkFb)I1sGA5^?P=GJbkZ}w_Od*`imuJSau)~U;e`n=QQ4hCvCVuWq-;QJy&L2RVOLKt3<>5AJ>}vb1fL;c?PTI1^BzZWijYHA_p6D- zLw2GbJ#)5kuHfP2?-lmW;Ps6(^#?DChqv?LnWi2+dK2*QqDP<6d;IX_G#7=}l6f-C z&nYhexV5XZTBWto9$x$h%W2M_@>lz;8IosU@6v@9eH`V9;~X+`iqxE8qj*dd2NF3M z?oFiStSEhL@5QPD;r02EHyr&z)l0>_9bQYTya(IZ+$4X6y|agVb1$c1MS=M<8>pGdNX*;O$8nYZ zAoKdrOJ)Bc=Iv4Py@D4YgXUL?tEPMtk7UQwf6%yJjn{LAuj2=$)X`kjn73o^%=at& z2f3HJW8a)pFJG;o`BnFjex@FMZou!tley>cOqnxu7e5Zp)mWKdt)A48{(}ae!I)p& zN4Y*TS1nvTCg34EC#t-tfAC3~w_|=4MI6W%CD#Y;$E&8k^M$`O0{JLDg1YjaJ@xy>HuI{gRFP!5?r zaYkNrZ`!@KQK{|48;+irv3FMUcDx6B^skUyAM@LpQv@Hq;>qOGynQzHyaqX(tA1z7 z#S@2nJI_VIzcT0gK9qCi9aTzPHSRm-YYrs(gSfY&?>ve442lDpVckD}L39%3`a(Ug z691~A)Gb3GzC$oIP z$(V7$C**teoABGY=cRh7@P?y5i2WdQwpA~+qx=U04h9nkGK6|wJikKUxt-*Y{};1- z!H3rWSZ>bEi7X=i75vWZA9TF8W6t32?kpaYdegie+>Z}DMqN68{4d?}QvSithd-}w zY<|}JJo&t86F#6GeLwQ>_UK>U$^UBfsondc%l=y3U2@3bruh{(MQ20f7}wK&N-=+ zdi2ROZ}$?vGy4bUQO`^9+i|~A^DD#WrS{I?Y=c{i_aJzE_+G`8?a;iw_S!e`k)F4M zTMHhtk-t)0HU1A8^Q$qMFKX@&_9)EKa|Zb6Q;$s7z6m2I^MeDg@3pDm603AkEZt^axx3cEAt=; zNX=)c)N=;zO@N1N&NJ}6JvQ!Ua-IGUrVc9TI=1ogGw&V>-8Oq|E9%k5FYZlwhR73t ziQhRv_q4`SZV9uxLj)?E%c@%WzYC67|R z{iwcQ!3)5>)Un~m#k({s?n3B)d<$rgfGN%YRWaey_ck0={t?~|o7XYp#nkS$%CzsPH}Ny^klB|CKMw9!2EU#A&Ug=Uz8$<7Vt1P^&E`JIt(=lj)|;O|GS>)WAYLG7X)KQ`ae^Fp41 z^P)}ZA@m;vp8<0Q?oFID%^8{{CsU=pmOIGj1+OJ@)p$S1ew=)V-ozND!dAk?+ad^&fBjWdj{h9-bxoAbFmxX@?&Nll8@x4MX74vqp*OGnoiYL>LdR}VY zK2P&SuU?+!*|ks3h-S&J7;s-9US1+}km~>TcVWc53BMMb{|bzFVIweh=PE%BNl`yi4#} zf-ky~`Z&i^AIq%Bx0G9tL{QHQ`77)P!IObE+)wlReiI%txV6d?_sYRs$|1KWj|u17 z@gGEgkmv2pX8@-NJ+BeO+2;Heax&nmos3zr;6w3wZ5;1AFeGkle>v>_0lZiuTUeWzGQZ2j@jw z-rJe8UD@gW{&)C16!LKF=;UgexBnfn*7P34eh|G0=JlN^sud2T+7F&3J_GJo$hTvU z545+c6jAIm7tj%X)v{@iOs{ z&lY{V^1ZYkw&&?R$h%6thtmO+s;FM>0DVT-~Ltnzf-PS8_jVVW zUxln`NBzOBOaC(EMZKx-jGot7>t4CH!@Jbnd|2G{FU#i(RVdkotdC17gu!k3OhF2Z@gM7bI`73yr*c;CLE55fYJ_GiH zsz11N!2#msJQr15HO^nD@6}|T>tjETajwQS_B-?Lp|EYyYg;Y<-2MV_ z)wY*BzGv!*`pZ*1z5C>hND4|OZ@9|!fwRpyWaJrwgfAK-UI6C{q2#sPB>O@1ossK% zQ||54#Ww*CB=g&+ZmW_b3=NmLwwgu4!N1;qV75`%HDA0^HTFG@J0WL zSQqdsaUi*mV{mKH^ZJMKqWmAsn$&65xp_WTXX`Zw4=-}a-Doa~9=-BA!@C4eoZ^1$ zr@b?{wFP}WJFc(&UGv*Ekf3?{V5Y^IY^ZOicvM48m`h(!fC||0& zUuDyr0i2@9nuYpYalXAv;b)Z*$I32xy4Uw|8&*I*ubY-Jracb!gUB=dOukgZYk3d( zQkjPg&i1~PU&sr9{8f+sm7QLqc{})5-Ingw-X-OmSWmn@_?@p>|DEy``JIjX73QKh zmmRkcwar`Ho4!}zi!z@OVlH=mehyL4l1NEJoe_%h3`z_pB zfGFPpeZ8!M`Yj>YL?n@j>^ziU1?-Kfh{^TEoFBSVi$8!a4?HkmafOlzQSQ+h|;U8Qd7D-+Jl|yE( z8aPEfzry{>L%1IXp8qGCPq$%_c%Of@X-6g7|M$>_XFpupZ0lOmH%K< z^Wo~wltb>EF)Z}~TV>jJ^u4M%?j!xdOyU&r{R&|q9=8}9dm{+ zQ`_isrEqDLae1=ZqF{u;|BzzMUX+3SjB~q-z6VCIFK3E zeR982=SuOfuy;1j72bpFqdyTpkoGv>$vj8-cFY-?h$n;ZRVC$+(epxY0(+cxlyCPT zZ}=tRs$q}g>U2d~9M0ETaZ|AWj``;7KDe+*t7P#&Rt?53PdBjrWs)BKA0qSHO^>9dY{ z^eQh3PaOE7oNwnoj(P8loDBF^$~V#STwNs|GWIy&^`Y+^FTDxOMY%`+U-Eh3d&M~! z!4y9VGohcub5v&J&qmq~1X}8TL&eC)3!=b=Z39 zrE(4#_jV)KcXjtqMJHu0$~~{n;swC>>L22Ms5t}k8D6Gy6)(OC&NINni=51>oeKv1 z$M=Hxao8Jf_qPq&Kz$t5oA^q4spaB{3z5DvIFRtA8or5O;WHpFin%B}yga|cxnhqA z<_sm|o4}l5-H6Wmex>;BpVQtMIpn@)#>)33dl6PrL@c+nbc{|Rw;uOK> z#oUig7d_ma$w%)vZ}%I%s`oC*^__L-J2UqK`Kvz$=aMh=!<4fQ-X-6wvBc{`z8!mK z<{>xl=lK=#qMVayunwRcGWRB4A-*X3&de#goqrI#KKPy0yd6HT)))RfT1Y;6_$H<< znz|yWpt$0a^isJ;&phN0>0H6P#CcKlydKG(t9z;7K=OVtb&$<}th@)WC&$H|6V5h# zsVaX3zcc!S*bgR9FZI6uoqW26^w)U?&WjE)^}N*m3iB)W0-!(m%JAisZ|6M@I7QsY zc}x1v_+GWL9Z&6>xr2PE$jO}OF?LAF_)M9Lf~$sn`-Z-EbS$R(Rfhf#GFOe~?eIIJ zKWOCoz=6C|xF6sY{hC{!KHWasHfo7AXBExck&{9G3UdbbS~?}Jh%BZ3;8vH{YLbtn z(zyb+wpH32Szd*QiQnE@dS1wja*v+#qN{pWN}k~i^_?HEJ(qe<#vSAZF!)#3BQ7R< zOx#-JWH1+HP7(8D;9YXDx0XE)&XvKxx}IEb{a4D@4)4JV;cTn<)w|kj*{#p&5p&7s zh5Qx#&dk{kaclnd+R-;F!V3cFKL{UvsEIGiK6>n(ms$7EUl=_nC~w4FlRs7SElc}M+=#Zxwa3BtigSJF4=P?CINQu`2WK1qLHJVD9tZcU z(ZT%sqPeQjuribH(#3mFr`^D0||VQW{c4}`S!d7yvAZM{qIJ9A!??^n^(qeuRV`_AyCKB{xb$L!D87A}4udsoRL zd%{lCwcs=0KiDN|i})tkyELs}@`{i}t!yFW4bQKrJ=#hBoB`b0H?myQ+-=8FAIsdCpHgn4xv1iPFs~1s?Xh%k_vlzsd$uZe z?~>B=oMB6nY*Xyc%g*FZ-8$Yh7cB|fMDM}kg&XITQf~sBBIJ3gh&Q-{X z$B2jQIBz#N+hgh8UT?il`KzJ6`7&o{HoXU#`vG6-Xnk)t`h!;~*Y_Wj*Al+e`^E3< zm_r8t%G}3k=+)MJ^Tp}M4p(-eyr?n10$1%F;%vj`<(fFq@{x7Lf~uJDl)qB*tE^IM zj?a=9+jM&)-P^|zKQnEE%~B%n(poJ@EUwk^}RyRD}eUSd#(SrGzzcpTj3Nb zKTaE4o3s^K_ZRN3oOx`Mya$FVy4PGBO+sI$-@>nH3uU{yC1)dE4gUB-=*T+73#r=TC#CBvx%~qEmdc3Uf z?H}nqPGMj7jwR$>QoU5=wVZe`oA?aoTp#jRMUrP=4x}+}e`$%`#6yOEaI8L8O>^11 z#6Eh?^*xb!B(K?tF`0eaVg@5orz8eNikoO>RGT<}7H*uNfqOsD)X(zo@!`%v-Z(%n0bSE$Y*FSiu@J&ILP&#FZz1rM`>QRmxzBglX|I~li@jo z@_BJD75DboQ#<#?NiWslY%A^udzaWZ@!$Bd)Jp|d&A4A-F3SGFhv|F89uxRd_s>ta zzGbeswR+MYodQ*;%~d{2sh}){031!e=n|CSK4y8J>$8^LBi%`p0$+ zxy$F?{_l5siT;DJ(xb>^u~_ZI1Nlk?X4{d6fJ(Vbq%d zPlmZ4t&+Owe7o||EA9vP2a!X@{EFX$u1STKRo3_C+oR*iV*;O71ic4)XD2Lv(e|0$ zZF$Vv5b1g0KZyRId4Bbb+^?{A#(&U_=2y&TsMr^K%J1sCoqF|m_j!!GL9z0JxwG)+aWEHcChmuEZ%--Tncr79MXKkuxkbL+-})1IO!!>2%ro5169?bK zFV@krAM8#(dfcznxl)`W_QYY%p!|c#Gn^p*pk+{jya#XPe&F5?J_GuL${UVzwQ0h( zp;0|UDA$L1JNI!o*9Sg>+l3&vSv|K6EtznCRC6Nd+ZRM92CW&1$k;Y&r2 z9_I?*EB1yfo{Zu%a4(g8sb8cH(EdU2WcWXbb7joi^ZR<5ya1eMXq288{DYWZ!5dym z=c-W88TdUo@!-U*HPoX|puDKzn^4~?_;J|7yNS59@4AH6q#a2XZ#a5htFyccKd%fs zw&h}5_nW=khUEp8h=-Rs+uX;&-no|MqTq{iUi5X@4;t@5?48-~%zd1edmQ+@R4;W> z=QjsD@B0h&2OF$Uq@0X@Iee*q@}T0bV;lRN*{yr2$TO(-putsxH+;FZ)Y6>TJ*sTN z=Ap4Y1NDC}h&(1d7cDaJ`pPs{?IW2pfLn`PUmxOZBhT=q=4@l$4$gL}e}L|JVL!;d zROa=Ci5Fmx?cSwVR>zi=@0)w-C5Qi@@-Dq$;)_OaZ=?BF-wJ0NoT41+JG0NL*>Z#O zqToP+`@tMY%tgUfgKxqn9$x$hOKC3pB>A0<-URl};I|ujQSN!ckJGGk$jp-|qn?-1 zo0z!uJmp1a+kBR!LzVDk(DTCm3Vj^pkg<0*zE|*hb*Fnf@>k%hAt&Q(Z$tY*_5uWT z&K)q^_e^Mf+;!=tjv*eh*<;c~`$7H>_7QLRJ$>><%nq_gzdt|Qx`DVK_+Ej};CkV* z%-cg3IqCNxd&BuXh`#ei@mgZe0I%gg#6vDG=}mo{WA?^n2@c;YoBu1}$KwB$a)Wrt z)sz=yP7&v1_C-dh;n*?BKU+Ua`Ac)QT{NEoduOv3!0*y-xwm(i zb$;F`s|)4&;NdmAme>!rq5KtceU|dnBcU}rTux9<20R(#dv$ByIZpovRZd2|2jRyl zlJ6CAec-o`KN!6Amt5y%!LlDj{_1>H5%ERgA4I-=FZH~#bxtNs-h+mB>Au2GE2nEt z(FV!Mz~{x>TJAeL=CAI~a5v@Kf79pctaTLiahT8WGI1bV=G(EysjCa5{8bJ4CT==# zijYIbyghEM%km@k7i{s1`((dU(tpoX;>p}6+}eHfms#JkG$!_lDwSR;d&5H{C!^*J z;B1@!gV;Os_bPY9J$)iPYcGdWZ{i{PUa@yc@%jq7S_Y;0j}acS;`OZ?FtPIuy&uH6 z+GXpu^vdd(vI^oM8}oML8TO@IO1K`eM)R+bzf$kP-Dy?i4L^7MWfKn>`F8caQu%iF zn3(zPn2Tb5g**duYr)x0U+|;DynPn!orgR0alk`XUdt`?y@JPNL)dHN;VmZLM35;j zs``WIrNWOBAbV$Uw&9}(Pv#vuSL_Wp=VUl9+T5#IdzZjxKu%`#@a*0@J;sXH@|%?( zq}@k+QS|8H;YEM&*TL)b{0jH>ZZa3e-Wh%8zho{7UZ3GhMXrxIMc5C18$Tv|5%mXg zuK3;#-^2*poW+l2Z*RdDRetAINnN7eo=`S4#>Cmie-OS2)thK_VG8+ixHkcBxN&a> zR}Jq$&R;z*dX^9d3qz|MW_1}Q~rv5Uho1aA3g3@_zzCA$BM_~ z^}RJ!8*5v4+}QWOBgX}QFZ;pf7CB_}od<}=q@{nbIB-4naroZ8f^x{aJp4^LneN1s z;d7;OGH(RbQ4YDrIx6`w={xV#{Xy)VciPTABW$A%>4kT z2z?xIYq59!o4!{&B`^A=a6eSOojq|UbRWlM4$ThC%Zj_VZbA@?3JSNx= zvTx#pl)nfUbjQg3>d)n}iceDGDtGr(iAgt%&s z@4@s12dx7vtrC|Luwr8y=H6Wxtabz?HbQydSJH%^B)7rwDllx7L-n1J7X_#2kE6vEQwpY#7XUe#;DSPjdpqt|#WWYSM8^^TD$KJ^czu{(p+Cs^ zcJ`%$TWg`-#K4prsp#b0|4yH7A^l=UC0C0lZcV`NlD}e} zOefvP;r<|Vip<|D_?4;xlkBwJ-T5aIWyZLLUd*TAnk!E%$c#=*^rWd_-7b6Grb>OY7+ z&Zy*Htou`bzm3-t|G{p=DFXir^Y-1+qwh@qK_}}q@ei_xm-C{SGhp7{kGN{^wOM7^gA7`}qov|N;7r=vf$W3%_H}l);TI5CHcV@omm-gpv z33P9Nd`}4ZospAC3`!>dV6nwT=2tCyUM_m?d|C2W@DJV_)jFxbl5O2X-X-wcXL$Z~ zIr7BAdv=xd%w8aT2KWczi8JyHUlX^sIr;Ut->5e+fjn{84@RtMmF{NWY04op2NK^a zgHwdLD7b1(%}1*5JRC!Fh7mHq`i1t+{&cR!4bPa=nS2w>*=GNsBe&MW_Ic`4neXTx zy?PHCUQ55=Q>M8naxx=mE~@5NIj7&EJq|cU+?!axaO<1`>3Nk>4*8X>zvo`395TEB z%qd!5>q&cO@cNLy;`vqCc;6x4ktdG7R|fav-dj8-SCd?b z_~_9eyfAoOKrQjF&Ra(&_oF;RxBk02`CN@VRk<&w?8@qHODk#K9ZNVygpYsS2ImHWcI{OT@McR&!Clw==gE{Hq}Gd4Y!vt{T2q*R74hXIM{p2JU%<31=HV zdT=0_Q^fv3)tkV35c8|2iTiQy;ThXK^X??C<<7KIl6rMP|2frIxD7ilK{EF{alMiNV{uSS^FlX4LeO};;V$N_j zw~_Ac$n|BHM(q8gD!;aU$Mt=`8#$J|OPF8bJ-CUy;qSUktFhDfiv5Fa#KQ~jhlhJp zFPC8jfrZq^!Td_~CU(;Iiuvv655hOWKKjq9o12GvpC6nba3$jJg!fa9|Zp@bj|e5&HJ5xye@MF@V9KB+{(i!(g@+g1+Vulo6>cqf$mmT}*m^F#DBKTU$}_On5*`!PcSa7G zJ-jovx6kX4@lxu2I^TYV=2uz0$58%?Jtpk6RQ&evW1m%e73O9+r?u7I@DvlD!I)qD z5HmLT#K>athDYh|)iirU`Z@Uz;yrk$aMjp9h&>M8gY3sat`B>h>C~fNl(W1vdhfZa z(%RM?OZ1#UeXk5}xSi$<{aW}YN*(wNO%`ABm}KiY1O9`ji+)(SBke)Ti<)`J;Hv!{ zaYgR!edRp}{?%;CU)8R@PtQd|Tkx;o$3gxIduO|U>YzO0x4(C2l6hxJh0VTs7rg!uJZiKIGdKXPf;v_eQlz%D3cL_sqAL`p(l%^b?-Ui?+k| zRu0?`NB&iuK36yDntyOQ0Kap~{-7Vt8Q8;%a|I40_Ri?hJ2`!(=U3dL2e+1U$nR^v zGrUXuA1n%Ndd}sc*6wd#obKTB>MPt2?xp$-*))E6WUIshmP6LXn)|^yWPGpqKRAti z^vYv$!|N{LY!_>OJ2*u*z22%{c{)hH2UTxk=H6eciYVWHa^&dX)3kT~7xgB_&5jPbwpbf2Dl%$RQ)wcPE`Il|x1^^*ix9duDWy{vfz&Dlf`jOUxNM_;l;P zyVFR`f$Y9?x2@UMR(O5R#Ai@`9Mz-umwWqI;`L$gjPDiq2P>uLr98YTbid->#9R8_ zj+_kkgW%S}3lJteWO$e0n=t!vxHqBvIF7xEskC>V(73K^+8~Sn`0(TL12qS7sBhkY ziJh<4HB)aQjQFCvb-vx~$Kg2x`<<01?z8H94!^d&eV%8=sMN01n*jF%`$3gwK(3GT z3K84E!Ga zRJb3iN3XbQcS(N`bJ3rUU*G+|qEjn((fkS?lbO`V;hc=}J6BtMlKYzOS95N=cgDP3 zc>!YOT%Djj4)^HQ{0ip^^Q#B5D@z{TGfnuS@R+DM1HM=AhR^I7;^snqXLYXBydD2R z^yty^QhgkF!{M9gNpc!Q+uv(1z$$tVf`0{mJM(0Y3a6-Z;TM&m$F@mtqDZ*4oI_qQY25JT z#FJr<$v47*>_G3qPKUp!ZlHNPa((d8pR>N4{3z`Q!6`!DdHcS(Wf#fsjNZhIHNn*L zGS9D6PR86zHTx#!(Oi`0456N0+7pMK7rX%Ai$+tf@6V$J`hKN2+nkeO|6o^|Gpsb- z+p%{>UR3pQFu$6!c8bie))T*dT=;+EpCUek>JOqf;r3(0{^{}`*fliEkWNBdH1=DlQfv0kVA)y?Nz$s0bM_zXER zXW+iGac|!!yuN+b6$`#24kSFh$hXJrU0%9~{Da{2A=iiR752{X@U9PgoA?aiY;Vxq z4|w7xQ;#10!55dL$^EK?zE@S$^IE##WBCulH^JxX|IYRCyLBYJ*?_uvO9XRW@}OGR&@oN|4Y`(laLx0C!hs+X#`YUhaGUQhc$ z{0GfBnOnJP*K}_J9uu4^eh;#Lu=&@kN7oVu5z8&CTju zc%*XLvA1=uufW9XGv^tu+=jD__aJj?>mvSW!L8k?^X=$O@cb%d&5X@A_qY1-T3sOZ zQn4RY_bcUHdWH6bm|rQr=p)%XB-h6|WboVf5x3Skse9DM3ENFx%g}*`ZzIVmx)`8 zd^>ZB(DTB%`nT_Ip|8g^Ctnp`D!x}6q>qC<1JB#p!^?df+eDS{tI z<&eQ=XrjF6D+hzLcM1Q&Wu?*je)a9hvB5vke())aQ{u|V;_+jNQ=~j`Ph@_T>Sl9K zTS++?#Y0wn25>*lkblsB_zLn`VlIjv{d*3+RD7?72*3SL={xt0?Jj&##p`2kt&6=) zdKl#ykZ*sL{La6J&WvkJzD~V~G2yBH_Ca}FUnNi64b>E6!X zCG2sgNDdi(XZD!jJ(x%TLHM2dd&OQ$W6lsxz6r$_jR(RPV$DA%qh`6di0&^y~<1;6ZQgt*T?Tc5AlZop!p2oA%j!&j!QVb2Y2Q_ zChtMcA#-nnIYkAUtH#_9K3BgGU-TsP2ZyJ2%4nAtxxMS*kA;Vf9zFB=_}<=*xV1P} z;HrUJ3*Q8L!;$NQhj$%uw&zga8GMHECNIDa;kUyZ4o=ZdQ?75VK3AAu;oiP&=uFD> zDG#r~{owf(=IxuQmkJ*8O7hXGz4KK)ze-sAyzMJ{m>N{_&UrjlgUvqEL zxq5y{dd||)*?Z4b73*{LKhwP({42#NTBy%eo3w1*n_y4erocCavyJ%``*DzyIdlAP z;q@u6CHtM7o&K#k+j}S{gT6EKub2bLUI64|T+<`vJ$PO7kY|(6>q+qsrjo}5`@yN( zX0LTy{;B;%&1VQXapUq-@(<#Eg}w6zOPeIGsIB6oM}N@hrNWmwmGTU&FHCZa?O8T- zEA>*Dhs-^%u6;s0>&U~qy`+!K8H~O&b8FK~-tcJRY=Z-dTp#w%;cMLK-rjX-?ds^V z%6)TAjS_Eo58@#s*N6NSdK1hS?M(Bl_B3yYAE&zL3eB%D7Y!J`g8qZPG#A}4voNsv zIcItgh95gbobA@KcSg_4E`6NQ)T1~0I5=1QAH@6$Ib`Je(08uR4HBQ1@-Dq4xxP&_ zZ)fk)8>e59yyz6-x2KWU^4F>j!t3*=JfpUd~_PT#ZgXWBo|@SAPtC+S|?PKy}x{k=xs8{uSrj`F@qD`#1(ytx3)m`p#a3 zYiN)2Me5_?iJL~;4}Z#wHq9Mu*=t?7peklu@V6s3kSC6L$jm7M2eO9v3~vfoZHC{5 zur1^T;GS26<`hjhIA!Zu@d7AbpW#b2_@atid$PxN$&2pD@1J=z)t&l-@Wk=`3j8bN z`rse*vDVD%NStl-2a&%*FO~VC*blOI3EU6%(O-?Yta&osO`M|geRIoxTitVMIr#_G z{EEL<+{Z!wY9RT%EY$N-_bcZ0!DG^!xF5J*@g65OC^dTb{MFQVzBg*KgC`C-WZbX# zdsS5N$I&*#XV{{96YvjOP5(i)A50?tm5cTdvLDCrc{L=@CLXeS4=SDv<_ukzUXi(| zuk`5QONHNA&9A^!L%tn*XY2<#FZ%7uvqe`VFPhVPwD6Gqtj!kWWC{YEhdDijylD8b zPs#6$zVny#A5>mT=63c3c+9%l-jtCk)UwI9U1 zU3r(lRb%hcm7@g}Qwye0FEw~k$co7YB^AF@-x*#@qeuUd_TxWbe2(yB zz`sfvv`D-F%I7uIcb)hLoezYN$AtUNf13P*-COXlu0^aNZ#d@d%Dcq-!BZ>$S9E>% zPxKyC9+T7a##yghnv(Kn7CG=ml^5Xa)P9-eltZ2&{C2g+L4Oc?9P{@Id{Nxn`FjQL z5_}Vme1-y=iz<)FYT^{B{M8G3k5f|Hx?^eI<0D@Q{w^kS!9MWj8LjVEsz+a|IYr<=wiW)Bt8i=oj`&OSMIQ{g!{@>N<(*!p zd3yr=2f^#RBRzCY_~wTFt%OrFROX`WOGUojYU2O(4&JIfLW9UFDD~sFylI_RgW(%Jp0nedljYIb?NjS9wuzw!!OD zIb`%wA2jih*&7bd_OsrN&7W0wF@3Mp9_J@hUKBkqUuzTbkl}a6_iF3K>GZwI&T5n9 zX8YRYn^2r>aBH8Gy)*Kn8~S?c_h1@%0q`C~4tX-oMQ?cBNqZdZ2OF(LnzM~P4)^HK zan>z8&*+aEerqzMp;%b|L;1d&8UPKdAPDchmO@ z@4?XyzKP+){fOS)>F^QS4|4wMpM+g07p*TO_mA~%;RX0p`zEHDdK39{uHfNSzEtq~ z9DAvF4{~0#-=5$T^_PP^z58U-{OZ8}|_&WvxAvK5;HR-kzAKtx+Ke?T#B(sp~j5a;dgsHpU>Cp z{r-%8|HJF{dcWS!=i_m|&s%15uKb-G#1qFknaRYhwe^^=e^AY@R_b><_Xmxf%!I8a zho66dFIpcnQ#^6wg)a)PC4BVoF8#J5YU8&3bI!h9T}t_OgIf#Ewz{vF1KCEu+nL{f zQTEOws7KFvh8p?~;#^_Qz}yeykntTDe} z_;Gm7aGK^i#)u2 zVo%b$9emMIGQWa{7kSayi+W~kE9@oxLCmja2?w%(dJ{#o#|d((zjEhTR!Ok-E@6+8 zOL@_8@*Tvv;vT(K_zdU|hLk_McPDw`jCp&B_R)j;q2}$#U*S6lJ_C9aiuV0MQO(>t& zS@)s+H~6K|yZv+85Bm9^Z1TPuyg170AK#dCY1IVk52_q;q3%2L`>HL?MOU7GpFF(G zfqZlE!pt=Bd9|XxGjcME=YM7Sdz{0Zlqp5>9pql>wwxDJzDZ~vSRKMvkk0lDLdFA9G9B+0kqTx}-)Rg37g zltZ@CyZv$D_4S~0#ay+*pD;x`X1qaJWd`Hc*DUds(anx8D~8gU7qHg=xF^e z!6T)VI7Qqa#9Wj;aRv_=c~Q>EFt0C)^6f^R0r_^`I)y>YFlJ7TC`p)r3V~R_O&%nOaE^@Bm$Kg4H@f`&B zBe};Omtog0+Hh;ZXMi{SNJRtnCbEQEdx_?v-GzVkPIc7T9s9#|f6yf}n1qC)MXSH4vAyvCXOgW#&M$7I`p^`qQ$A7_Ey zJHt1DIm0vZKWMiMIb_3+6YuL6ay;_o*gNz;i2I83R}DWlqxluj88C03CB9Vn=$X#| zKhEdmiCaDLs>x&0D*LhIK?&~GT{_o?zBBGC=E)fID?92tqeqXP7xE0v>3s$MRs7N5 z%54s(X)c-~^LFHr(eqN=TJBAF5x2I1&K0<|_zr?Es(5{C7kn~nZO~-#0>Bf8IRiYr z@OiN(?ke?Cm4A@mR}JEcQ|~MECU9RV4=;0yZ11Zfl5YnO8C0B{aZII5le_hca zTs8a;ei3`YGAyQNc(;Hyl9N%~kFHBftj*&clBTVku-WmT-MQ+T2_07Te#5(h{DXIe ztH$1NzxrO%A8c6qZTzd&81XKFe-$j>LCeg~<}H^VJ@QxFqgQ<#_AX6Goshdu&lxap zSG@`JQp3k*i5~~O)a`{H84-)T=-nQcHEHXohkIY}sV{Jilm9{9JF~|``Ed-M47h44 zhm3hUd&7H%drRLLdz>5nUUqY^J6zH6$W-!8z(0s{gjLyRV7Q0G}7+{X!$UTVhqv4_U!{M8`& z4qhWZLk{(ExHs`qlU^$L42n~P_Z2wX-1Dk`+J0cc=!~R_th@peki}Q z;uM*EsrVno!8xDTEzs`#y&j4Ou?95qot|pP!68Wp18M797S^pj1 zY*_^5`aEbqh#WFFkT219a7M(hf&IMmdXMgKr^fMIy?8B|hm5)C&i#?p#{r+=DLPjd zq(8W7-$`mnfUFPUt#YIK7-+j%S;3Rig_~Z zwN&14%&(Zw@MD;-|7owhUM}s{RaHul{!N*;UzGRN2J-NtkAwLY^BKVFQyfV22icd( zxxQrLZ0A!BnY{qalTltv#eqaG6?4(1^LBgMJLgZo6KzlT74|s3gOYpfb9wvv74q{Uxx#(*TfdJ+-Xm_Udbfkmu$?@-yY`1~yiWT;sc<3x_P&fG)y750Pd1@PB=2F}SSUf)gXJ0sV(c~qxqEuwSD z=Y{Ve@}lgc@3ptM@Y#$gnlm^qo2B^-YiZu@N1nLbl)vKd;ArYin7@Oz`$6_i;J&&j z^LEwqGCU@)Cv>Mi4&GN4HXKO!CV0+}H`-CW0GvZs=gKpsBqyeL;n7KwlfnGTPJ6>g z_D`XLGxDOCGbm3S^RKu! zkt#WvVuv8zOZ_&XndBL8U#a<3zG?5Q`Z(Zh^LOxv^qq;_=sW0=J#E_)lxI+!B6ydO zLw-KIbHHOmI(hDG|CV@{eqYf|_fi8lJ07h6v9@Nk_)@_a1qbq^=Jk!Nf9^sl`6ier z!#Nq`Mep34M0wG0;>j$RT;E3V9b-`T@C&a zrF!ijkn00ql=%$cY@hgZn&P)DWF>_2m>*t9l(kmq=1HLHd zWPX}E(sEDp+u6fA_4t9(Zuu!`ErovtKMs6e@R)G^3g1Eg9~|jlPI(6W56&RJGxHh1 zLk4Gi<@pV@_c}h_Cy;V|n2Q!JSWo@I$NJo(oQ%pL!8m9aMdsZuA{Q z4tdtuV(HN{w-&iR`-Ar9?vQtBfOne zw`m*169*1tUWtFMKkacQn(nIrr|H5&2A_f7SMXYzIYo*C*-Z9>%vD2w(AHz}s&_Wc zMIFhH10TI({7!2(nu{WTHB`UbJ;H~^RM5N~z6tI-e_ipo&79$f^#2vqklz{SYPjTN zeDs_FUH~;`z#gZCsgJYZ=+w$B4re9LP&fOPxc!!8+UNBM@noi&_zcW}wA~LH{Py)h zLE@X>J`Vc_nFGo5c6EagSbkc%KXJrg?o4XG>@<>P{YB z?xofra3Y?}>fWz;|04YMKGerCI7K*DJimHPzpqrTk9}TWNS>jy_QW;iY}?MSxaZ{< zT|hh;_;K(*$huA+spQc>HgqT0nbvN;Xv%~rg=N>2U})8ru&2NE?qp~P5dk5WFDjU75sz9 z$uI|!{mxGkUliOA=0L_Col^OU!&&NiVb1VU+yU_qDsJsnQ-2VAQT8sa5>8QwPl4=l z)-BlZkbL`n+T#paY{^U~FM#5zsk|ujWH$K?)j1h+&nr{*IELRjZ+Z>A+b@xCg88E8 z(cdOd+|I=A*4Gm{2v-ezXXOp&e7iUGaX5bkUuv;s)>5eKme-RF<9auRJog7d}VxcIF{RjJJx<%bxD5PX^4OJ`Vp6j!&ILyuQ(? z!FrD~lg^dvv}Sa!;G<_wQHXHWz$uEh;mP2C5Ih-+@4F%2()_9+W=6yny00)7RlW(# zMVYhB`F8cb;yn((uMGYb=lWc-A4`5Cp}TcQ;<@zy72Kh|GxrBM*XLuYm;InU@vk6H!qqkgL zRhu*^KGHg9@e<)|8~#D?ufDYWoj674(SJ|xc6h_V{rEbemDN%9&bY7mJGhSc?f2t+ zEk9HKisx6zU-A2j^9+ya{$PFM>u#0(){gvxdh}N`pP{qo9{q0T|3Ub?kV9@b(Cpl; zn(-a7d-wOg8aRVIy#J8 zX0Dp*JL6n!9Ob6HOXgf(V0kaiRb#)izw8IW7v;HV-`Kk*PSH3$Z&$rk+*jNm^w#_< z!xLxZ`u1oG|Ri-uIjX|5XQ8IZpUZSp@@PhLxSmyB}-?g!^&!2JLR67P1* z+pkD}5PXJrVtQ&{D*GnDLuQ@~a((=6zg;t){Lbik-4$M+Psk$Q1$1Ba5>K4^A5?q> z&h;VRu6kbZ<7^qVaX@(Yar)iPdmOXJ#NTNq^}L>#)*?DDF4b~i-rSk<#%B_r!N|$X zUi7BsUzOPSQcpzojjhq=3iI|F+T*|z=PdIp)yGly73W1UZ@(%2LF@;=s^~=dcFfzm zTK7o4oxPTLw}Xd#f$l5jGpKnx`*BvLIs7G8O>sZ|7dDLigWw^ncl(<*oFeWIwh?|i z_RhGk@`9%LJkrOmQ$g)&>P=i09x`$=$$HL^M|{z+>e)IPiXGPNZJqLJJ@K-UwP~eE&uaoXzM3DR?~j)3-YDnJ6I&#TFgb+N8gkK zsl1k5r#Up?w}S&2T<#(BEA*W?hm1S}bBd6^I!XRPwI5XZE4zb}sF&&}^Y$q6m>Bm} z&v2iZpDYJse~-E&{C0RPdGDKWEv)}Z_P^7s)!F(spXH_9b)&WqnA$x+c*qlS{q?+^y_Vp& z^ZTl^ez&XdAp7XKj}yIUAbH{xr|4bX^EwsTn|fXm5tjmAll#iyT>XL2%>lZP1OH&L z^l`e9hgW%*Zb+_==L{W>OqX{%xN6AB+#)Z)gM5ay(+h3>2hqoYH=N(?@xqfiL;r)k zcZP35an-PQR(Vn68PH2rduP0_;G@rzzH|11Ez);pkI7h?i{7Wc^V{SfoVxF-%-hfC z`4#(8ZMh%V52_q;XT2Zfc{|?ib{20Fr-;3lRr-I>oM+&3wTAXMpT+)Sc`N3{aMyrG zg@0x68FpFiCD(`f)$N*b^uD?x`SurM%Ol^V9CEg+AI%xS{V;nipC_(bKR-M)`_GOMVUT0`$Ep10#WXn1(h^Rnt(U%llH z`MlJ*vKOCMi=<|hlQHsF@R+E*GdM*zqN<4}1Fo9#O(+f|_JesE_$Cy;9eD=ikbfiZ5_4-Oihq#% z&fq}S+3cOyP`-WFVgIe+Svbzgqsd{MmH;SFa$4!lb} z!hLLd6a6UP-bp;XZzgm#d6zJ6hsOl-_Mhhtr~g6B+d0=~^t_N~K#v|dWc285=c33V zpOv{N_nkS<@b=+xQzw->*557dXKID*dUEh_tsBPaFa|V@X z;QW=!i>f{jxV7*uji>w-xE}$z`6btd-;Q3Ynll*pRRDP{J@;-kc}$|R#%(P-+~dOg z^=n)gKH%Y1z6s_OsXmVK4`PqQc?R?*xR1m46>`XRjmIh;Ju+jP3-xi_Wbe%TLHOu> z2BnXEjPh5QGvHj=OHO9$@h?sH6}TVZY=@E`M|n&PPEom6KHXQ`$61v52Jso{DK9!& zdrX+K&FAXlS-H}8UQhWe<>AG=UFBr*t6bU@^eXonN%z$OOP@H0Imwz+gmaZpyfvpc z<@(@vcC*R18}BQ40l=+APDafcPHS&Cdi3lKf0h0Rt3v;b+Gpc;23O7SdF>QlUkIJ6 z+r-(1Ck}ju`{c(N5n`qOAp4zpe)XUDLDt!eo~1s{B-#&ht`B~kG|Rd;`)GHY_f_6Q za(&>pbH2SR%^8#z0Dhd{)>StrQ;+^{8S@qmv>qjIIP)3&>hoM<`z|8ylFG@jA7@;L z+cnKhc?RT=c|RB~bJ4cM7kxYCvdr685f7QUABKnbv&8Sxe<`?8R(Znv`flR&VSaUK z?lAJD=1eankI94ILG)7L4F|XOd-q}eH~B3kuO+^N=KbKcQ&V-{8Qfa+zEby+~egXpDV-hTHXb5YfIX3jReOTpA1MBkbDSHA>? zX}+kzf#kjOoUD4uA%pwzEbRx8zf$vd_)?oiKPhvDu})6Jzk-iGaA7m6zj(ucKjwUO zj8k~`O#?n2_2jf>vd39KUQ3?0bI9|v;= z&dG$*-WeQ7!@D$u^6ihvyxs5*8oa*g(xV5TLFL<#>oap8Ro~gR=f&@Kqvxe^ee9!G zJ};HOg4gl`%D2BoUVu8{Y`;(U6*xuc4|1LX+>e5w5WROEb!fcw2NnNn>b~D-ex>-L zDf2(Kyb@O@zSJ|~4PRLNX^v;gamh1u$v<2gBK&srCUU0V*K<+k$#8$Ls_dfh+nG~@ z_thcs(XXan>ORWJJQv9w!|^|e`>K}SSKmdBG~HJ_ zHTMI&zGpIu3!mRRk$MyGrGm4~dmQj&FlR8hwIR9hrA}FBXAL1=D$hm1Rcq>rL#_`z z8O#};-Mg#sZ+hbPD!+}ZUiEG-UFK*x6vz7LS zg9G_z=-QFLn7o$oO?VLxxt#ig?DJx;<)bqyWZtg6gD$!^(KO$#eDr27z&YtVBY*Xa z-aF5+PLO>2dxut^&#$%b)T+-^pS++={%-j$As{u~ax^IQqmRRT9B?4*&Ver)xUyAJqji_neaY{_{TO2Mh9f7lFZO0sb?8dsAvd!-MDKRY z+ZA6_d3cp44*NmQA?Hpn5N<8r?O&32sbyj_&A$RyZ7b~ui~Q2Xm%3E%om;glsH!{_ zwlAq@iFm{9mR?#l&XhwAB_I7-;kOskyIpZVkn6*_in0zO-^BEN*G}bBwQIMg*AFyj zNSgnpc*6_*M)W`FzJqwk>~}tz(8_9`xGL?5{C`Vc^#0S$2Cf~wjy!RkL&kk&?s?%m z$oZ>>c>yqI0B5^vKpXOT8N9xymXzpsJNWI@~s=;uKXKTU# zS4&y!?WjLObBKpr-}p6gKlt6wUI2BjcIzHJa((QhUnlRYP1mL#KUmr|KRwM!xF0Il z_wc+ZxF76y{$O7!`6eQ*{^Co8j~=~LaEc0S_zavw-ax%n^&OmT>d}K+3vW1cwhxeZ z37)uN{m;^OaKX{|2mS|DkKT@asdu9r;)anIAY-hzc$dui_BY5kVQ@cw6P^sdgWfUE z(p>Z@&mDR$dVNLLCA-KA5U?_o-t9P7{2dI7Xox}n>f6YX#AjfBd&%K|tsiDNE;}0k zhUEH?7p=Y--1-U1A#)$cmVae;_4ctfCBd3gG$u8W_*c!Zj&qu+eG`TkK;`<_!;8GA zvB$xE#q+BK={v(?5=Q;OEaE`&yFGyJE9TbD+PHoH?6dFa_m#?v-jKaB^RHAd)r)vC z@Gh}8oPDX>^YSIHCHK4JpJumQ(!BxY4Fpb{rmxTkli{9<+lG|iY-u7e@?gw)qpQZgE z`v>9kLJqmxk{wnj-5&%G8NSp3^lmrjMbS&;9zD2f%tM~Ps5^0MgJ|CFN}f3I`cmjz zb)L3))F$oo;=Oa=LVIhFP zc;d*6ZOyYgCcmE0&AOZ3S5@Q%*eUx#<2%T6QRLgffdu#CBjPh0qjLqXC37IbDMC(0 zan;Pe33L9+$RTsCFTiI*(0V#o?3+L@mG?OB(fkT?QRI+So`Li2m|vkk_@$+9T>b2f zDFwt;+opT;y;F`RxJaIX-|a1kCu4X4F3oK_V+Gw;rFt%UD!okRqL0YAQr@Mzq1TD~ zVeqeZwD+kFJG*QDhZ}FmxniG}dbcCba7FkG;C{dxj&s#?eg$sr&cdE@uHXegFLlqn zOv_ryw{suI;p(kpYf3_LM-flP=nwLof&Do6A569!m^WYhasIeDS^Unk$r}#e1kXjW z$HBXuz2V3qM?_o}FTlMTJI#T_e(}{D4!Sn&a32H{i~wGkq^k{^-e-Z z>lcaNrdJmHQFe*)?VRhw{EF`@a3CE9)(HplE6QJ??+icAj-0q+EBWY8>2rnr6~C|G zF=^c{pWf}9lbLPx7Y-!mSH^d6qj&*|W)%em`aIs}ZpTgJ^BT3~*H!n#3y_eRS~N}e zILIL@o=m^Ey4e}jqc1nTuT(E}xoMB%Ky%TxrvE{8U%~GTe)}2OJEQMBMb8<2CqE8& zeV57S#eN*a6NhtEY;}rno)kco%_zul5axIMVV8? zd{Ovu6!$~rkj;Ei_`I5lCvH7)ir}@x`)bbk%)wne0$SgpcRTp)$n||g=L$LGo)<>d zXOqVS`Kx}^n^5m|?xiYUD)&;at(d)W+x{qex5E?npnnkGLFBJK*w!-p@#KLC-K`~w zKc=5AxNX`Gs<|k7sX5aNh%d_g_QR#F@;~@(f=9}(oO#8G)SKYFGxF{5T7t7Z(*LAa zVXqeL3dO^#^6i`#egDu3^29NpfzK5@Cj4$kZvxzp30p3%vRgVaewJx|1wMoF@EY&- z^(O8IxV0WBCleg4EhyKAJybw*4j7TcVx&3 z@jK%?X!Pir&(NH>YCLBs-apHflR>_nJ#n?6nInHU@%mbuJaLx_{?Hy?Gq)D}D{yN$ z*EjhAULU@LYR-W7l>_l)66gOM=IwvRE1!B^m#IIvFw<*sxaPONcPLBGuaJ|$9%r5S zCYaZ^g8GBteoUhMAh;j!#2N3a|B=Up^9%)kBm19pFE;TRRGy(r{+FfGj&Hc;;%xU! z!|R1K7gh5s4shy|wh6v3F+f2j1;?Ut#a8 z_@d}dFi!?~QSccoeb>6aSO4sV-G|3)olW-@drWo`pW*J!iR86Z97voiSK_LDNV&c$ z`X3yVI*B}SM!uanMc5C*Ke*iT*+cRS*gJz$1mg8^ zFSS)tgLSvbV=`2Gm)PfpKF-5($b%BPS$8J>kp2_-aoCrNy|eO|C||1bwBcn6yo*4KL|bp z&qevZ0uPz_4BVrKZ{muaEB1L|-VRO?{|}xbA3f)gxkukZ?*}&%Po`16gWw@wr2ZiP z5Ayr!0_CqBuQ+=cS(Es}y)PTFjD(C*y5no%9{djInw{Pwl3 zBkNzduw8s!|E1mp-tD}{32L$*M9*ves82Ny8GKRp#08kVOFVDiFsk!3rw8T?X5Yjc z)^Fk)i2JeYaL)_x)n`k-9eL3^mWG%QBCb-t9dl9e8PJ<(mef4HnBMKi|6ph7dBMBH z{43^*KFsgT`77o#_?moPMh@9_-rj;ZMcnf;yi3XCcgFh)eP{Gim3PVD6e+*+n)3y< zb;AANK28D6+u^lr?6|IW75Ti-AB1lLIb`?;e>;_3^=P{+;pppf3}h1v_io;dJDkr(Zm5hXd9NXfSkuV3f7pl_n@NaB9nwcMlpmGQn(?{@g; z-TptkzCS333{PBx_??|~ABX)oX096cINW#6Gx3loA3s#;o}Wy9oc|{Drd}#Mad=;0 z&VYA2_JeBgoG0gM#=d1miL@UKj=!7q>#EUP#+drf$hQ|rk3K(WvQO(ijU5YXKRQ2} zdJ~-MWBwI#GV4sX)c;NrO4EquoiF4`A5o8;$N{hd<3$scj@1&O$2YQJK!X_KD@8Y zduQ<5b12uh<8a{C53?MW9n(Iqp2Yp&cl#Wgx2KwT$TNtu&D;-uUrk>4UTSD=7V+ER z4TpD0c>$Dn=}C_u@rIYsydD0*#-)+kkCW?aG3{}{7rm@=eeAUiUg?zNq~{FqT7px= zy;RJvE?PdP{UGM;ubS>FB^5w9is&ioxL)xHVbSIma)jiAKW4{+KfSR}4`X&?)If#4{d|#zn z3ga5*bepzbyi1NWzrwto{W#!2n&%Ao4l0j{;SE>scD%1_=k4#+=Lq-Xq~1GU4g9Bx zCu8QSVL!;czB2tC#Je3lWbpc~NnRBEcC!}%erM*%7(8V3yo~o%3*xF_-p>Dnn2T=9 zv6_7J`F@$SAB1=5RM>mMRl|PJ=y^?yw_934+z*~JwC@wFbI7^$zCx~#=L}7~;oQeD z_JcoaP7%)4i2mQX?{o;LOpyKHe-fOn&Gmh?k-V1dF=74{e+N}g=JJV~WtR)esqZ{E zpSTw#?{nIWiJ5xCW04^Bfco+SL}%cUzGQQ;MUHu;cS5DraEg>~V!7sv?xY-YFumIq2a>(v{JvuE68;C( z`%2}nZ2g0nUp44FLp$mZGXJV#Rbx^Bz1tHq)5VVi5APX07e&ttb5U?0;fb@77r-Os zy97sTOM17XH}Ubc=FX1K)C&jF>@ndz&ZQH7lvNg-NdG+XN$ERdKWOVQ;qM^(2l2k* z9P+Gl_3Kz-eDp@X9bN$RCe(h=-19Q$ugtyF zTDq?k_XGU)T-xK{-M;#PoQ&ehv~HKv>jy7y|MQeXW#+t4{_@6AmQ!3=?EOh&cm1 zarRdyI(^Xn;{jVnJu%HWIyWxObYF3g{`tM1l5b+L^(gWBkVAg-TpA1 zjN&1KFKXpkDj?|D%bb%tQ_hODxVj= zgSfBQKNu7rVSUr&$7xBtK40+BAiUtunaKF*|-!PLjOrE|#4DFU|^oNe9@ z;(ri6`ue5E<6pDRUG#q$JIIfNo)>&x$jQJPzCO-D^BMe|?62OUc{{(ajQyaK_5yUV z;XuL@XXdwKe)TnZEyv580X};4=rL!wOP;t(fp2)P>OH$iW zJ&y8uVeedXb0YC%W=p;ubI~rs>l-iML9@>*I5(^0Hr-e7J9CeILtK*OAib{)t{QlK zJ)}R_uyjt=n610zz5=JnFXY?EKCyL{>X3{I63fCi{+uCNgO&*-kRre+?(p;4H&d9e9B~M&|$-|3zJGdXl-r49) z3>OdYi*bi7i|3!0?;vtAiu-~43O;(y$?zVhZJ)_Lxj~=ITC~2RYx)-$C|Z^Y(kuc9wT6SLU{vkvVqQpykvb zymaDb*)Ihr()S5ZrWNIozY?F<$_MhI=;MI+)G{Ed!Y9vnYSa)z&T{h zMW4S=Oz(EbWna@CM|mxc9{mE!A+L|KqZ~50AGSVvysvoPe)BK9OW+iBH1#H)&X_}- zZT9eTt`Bnt_DvYQiT?6Gh&~Sf2YD_EzGxKXuihr_5^^%w<6IISJWG@kk|4Zi-V<_&Q+t#+k<33=#=P0{y|%AExv>B<9u#Ok^SIW>e2Il z5FV2V?F}D&C`0B9xUbk_!rYHw;)~)t2(DUFemi=p=;NI9^7B8Vd-UM-4JYmgdh}+W z7xE0YJ$iWJ)cguOh?_{~+G&ubSoz7v;WcJEW`Uj`qW=XP(`)KSKJ> zm|v;h1iY5)1yG#r?}g7$K;EUD)W>O9dOZGh>zqZ;W^5~bL3n+tj{}bhzps$L3hzF~ zsm0Zs$8t)7O#F7$A5{Ey{12`ZPEjOzcrm{UE${I_zI})8(I@zhpdLMZsUJ!o2VToY zlh3P6@0}xPkJHW5WysS3{}=8<^DCZ<@_nUx^zD=1wBffaUn=qp@bH@RqAt!Gu1%-; zRce}ZqLa=cD-Z85)458We^=_(N+D&D)h92R-`9KKYUt#dokU zC{X*nRBwWNsobN--nmt~weoJ~xoE@ej4Ank@Af}IbB4sD^EGE1_mx}zLD~>e07OZYwz%JDqO_4;g$>a6epzJSF!P_fpk&Fhu$| zI9F>af5rX5zQk2q=o{BJpXLn3G#5o)6g-*#qyNFl<$vA`Y28V3$jHeQ#@R)?Pg_sD zi2%xrX3HMOp12=@#OqT%uMP6PN~8WD^RI$ve&w-uXJPk@1&dyl|3SR3lz-67znUf9 zCG2t3xnh2Mw&{&@F7p!yE110 zudhx#yvX(8ef3qTTYhSqOJXzYe-mCz`7|fK*h0OD@7;a+Z}eL_rP-Xn#~rdPk-Vsy zxAXs?^3lT!@SY8?kM}sLkJC=iuf|ayXJUM$^{vGTru`t^SMV;ukMlCkMd7u?esEId z!lN<8JB8o=U@z6^<1nAWcTfs>!+)Xq)yed|P2TOyzrws7dmMw`uD*lHy9BQ#I7R$! zM~@y}%gdTCS{~^Wve-AKZ=tJCz2}AE!~WuT9!U4qcAFl3gyxGnXuop+_2}6Pz;g!v zAGGz+gMXE3`C{I@nbG4vvYEH{$cS9zW&Kxt^JQV=iDT{ub0FcP$9-k^Qfq4(|ZbUm-8LwpT0SAzLoQ(gL3h7ONC$qFDY~N*?x99ab<2B6xC*mQ41BpG(u>PmX6SvTGUtvFp z95VNDkY`|C-+g+wBZrKf%pRAa*Dvcm4(1GPDcASJk*_Q6H@@L^x!>B6f9iL8PwLUT zdG6BlD|k$>$7xRY73W3AcUax~b>gbsi@9s*8~bhKyCDg_aeWJ1hu3>v*mgK*YnbHQ z*^k3K% zL{l#{TzlfwcQ91WRX3gMLvO;}bgn*&t%~|HG)JGS$7Rm&mT*7#T#dY5;*vq{E6lHk zS?WxC92e~c_=x<2M-yJ8UaIwIDEYjG^xvet0E@}Pd)6zL`Z&Lc*HY!%nX6{@#PNN_ zJ}>MC8>sI*=FsRZzpSdKeEXY=6DcQS@Y|6W<^MtSoxxSZeHA3VRCt#htuBeFXd52UUNN=k3D=WoRDq&-5L1vkp#ZPkU#cU#VW|Z&3$hf42C<^w9jPp~Qj2yxna{ znYGdCLf)mu0}i@JuX4zWhivV;*40mQiom~ufAAUcJJ%3bZN=cI@s^o;=4Frmd4}x{C*9xepYN9;J+Gws=fk}HfACsM{lP1zrcocqi@X5$l73zF z{+6+aR%lMqTH(p$kS8uwda0|r82)A zdz|#NuK5Q_r`zPOupb<5`FZXWGm^*p4O&JX6YK{uXF#5Td#T{oekJ*<2mc43B5p1C zq7k|`(M&wN%z?!FO8IgAA^CRfosqxVb$EjCWQ;xzczw)ok23Y>yVCngaUhw06|m6J z8nAGDYM^+-v3FM9aK5kR&x{_wVsK~brE=dHJumc~k#C33OZieQzJ8SJ!~BZh?Q46F z5`H^+6YMd0*nRaJc}(_;FO|KP=uLb;d4{(VT&r6Yfhpe(P7(7N#*!!Qt_=^_mM^+W_a?B% z!CZ71<*)LH*T;Kj2Jd#{WOC$OVSZ)iepHv8FZeNi zC-rfR|3T%)xfA+()B)`=F}zD^?_6fJk9Q_sU*iGQOMR7i$mkEgq%+MM|LW`b zH;K=%t?+ruAqTa7!efo(MN`NV2d>&D17>y~=hWir^<#NDFRJDYf9T%CM!K&Gd_ZVUUVD1ui#y3oQ-@t{5Zq?Pg4##m-r0nMOL}5?k8Q*IT`MGnfEw(`o3Bz z^LG9YKIq{EPX_;kDla;s|0&_gpyy?tU-A12d{N{@mB)nnqIh3{&v1!)^gL&P9|!X* zgU_IR^p(0d!TBrXWFj|S*Zx7~i#nS8&MGH^_Z7Gw$TN6c*eSl$Z{lAk4{xz>YlB*M z^vI$;&faW!AdmURPoU}CbpvY)%KjY;)J6?m7kI)Zlm*Mzo?{R|5t0jLGH9n4bGMww%M7{~WuL?>g(0r5VA+*i$_^WrkJcgcLN*u$&1wWWujz3{$pwlQyiDYl04qSpfZdFRsmiuvuh zuaIYe&kH%3=(BHC@0Q-g3*lbGL;f@B7ID?sKgb?lO3y$SH!v3JJ1JzwTm?BP{;2IQ|c)YfUQCGuAV3)aut5EMZ3 zD{$4uH1S%318JUL{S>xn{$b0DaZYoV5>E#IgXX=n;o;@)Al~h`uZ%s8;a$2QKCfMg zZsfIWOZoOg6%ND~1qbrFdpf}j z%(*`JQqBEA^itVt36BYK$ncnibswkqIAbJ#HLgi-ViWni%v?3h+i#KAvh*RGB9*@~ z_a?xBgxB&`jnldN2SR0!11|tPygQ^f(bco<5VwFAXg|0o_EyxNO}v)iY@4~Y7v(z$ z{#E-K6?2DM>WF`pt8+5EcUFBIaEca)ABXdzX~aVYr-(gq<~|P3+u7$eX3H4z#39eX z{=sz%>gj#8PWPQx()$WInZWpP>pP3BnW;K2%KWPzq>lqnT%n(jK3B~BIG)g)=Az7j zRDEakCTgP_;=E}t$~@%1-aDK9gYe^A)ckgyU-7xZ{~)-v{JsL8p+puBXU-QoTOK*6|{-`E<9K$zp zt=~V%Ysono#gq9y{esRjEbrk%Iho6IhdgwT(@}WHxUUQjZ|6P^`zFjena5|Oj2$^Bjd(KWPt*u!Tg@5Fe1_E{Z-@S=d-U*z zcc*!KQ@*J3JJ*oM%! zPrSZKGH-_`j_0EMKZra-A#v5vA6!M>!8PAle|l}L;r}{XW|qw2a?}c*H-*($#!dV z`VIyNUvv!RMa}c8ksVDA7^GnW#H?SXSh>ich2G9 z#Lbf_*T?TG^l>8fxq@%PE9Q!2ckJz`KSS4yyw>j(H+%9;V9wAXd0@g*)>8T(tSJtb~$uk@plmC3OpI?owp0GZ+ng> z@%pgG=}f#n@Z0Y{?P%gNTr|xYyovj9=>cx7dbfkChIc#u2hI5_cubgEtM)kXO@J?| z_JcDc>d8OIJQ;Paiph@y9x~=v&zSfOt{xw05AVbBqG~QWWuaecXzrSls$)*lciyNu zknmb^o`Ls+@TH=U1AaTtuXfV^U@rLwk-zFg{lQtnXMoqT*}1v{A)A9%HmBYM-dB;D zCj%Zb^F`5{Kn_{uuWWh9;PokOy#0}+i7P|seTAOa8u|{-h-i$d zw)BlXLGO0Ot;L)nGOKave<{zEJMlDmm(+cw{Lak3VlM#t&YP}<5}yHnXZG+i_XGVwdkz#@ZW+DXBgd~9{J2MG>)SU2%U_@zGWvu6A+8$yIQ$(n z`{?UqW=J0g9uw|)1yQaK|AU-oz+4pjL6vV;-f+CH`n%cL9hSW4qm+}`n|Lz)GWiGD zYY9)B%E{cLJx;^xj?XwbZ@D&E-dE1rKZtqzKK9^-j$5)W_-K zSxWoCo%FIU447YGj{~3AY|0@A3Qxv7Z=Ypzu5e%3@)?-d$2pnMl}^GJ#hjspINLZ^ zZHIIYcrko1<&bY^Pn@0fCLYOdOZoQC5>FCe6dqp1tyR4V-Vd7nI8Ta)_gI2wN-^<} zCsponIO#ru_BhOwK~9F}qOIwFkUb{gY){ad}KIgZpYb@nq1Oc#^nkd|w%JQFX3V{>nl3=-~~YVDgwK z{*}4s14?C+gR`@zN^!52ji8J;-B{fHu7AJ4BiC&T$G_R%}i zeh?m$$7Ozn`%3YzjJ)U&$|0|to=g08c*CdA`zk%HWuk-iWJ1rB(wqgw@$|j|PX_NR zzOUFf!FwFVzv6lO_;?5H4QI|ab09GnMV=vF=2x6UMqU(q9P9_-T>_s0`$5(7Vo#i- zu`;$PK--q8PpPX)LP>F&9s{gCR}XSeN-+W0%oMVrSvN)Fk~XFzY_F6{^TJ6PcA zTmOROknNXYkAvO>d*W(t2525KzJuVmgNKa$;8-VT+7GHd&ZLE7P5*-*bpJ&D2aDp8 zEuTv-Re3Evq(AtY^&69iS8;27L&_un5qnp-wf$+%aHq!poW1@ZwC#D7ws$AaHn?i+ z^FqEIdz{9`zv>STyIw~5cJ_I(Z-V_e?BRts9DZj9+T*Cb^VG_Z9eyCMC3{Tp9mIa{ zl8o%zSJL7Ts7oHm4_Fc zZTt`R6RsL^eFhKt;kiC=KN=4>>OKxUyzB+QeU&JA(O#N|%;)ME^6&WL0rw+lp`A5o;aJLx8ut}ECY--g-$7T8iR7DjUOXmmT93p#EeoSwDsp}NZpXR$ zd+dG7-I!Ss6@de3&cJ;f+*d74{C3RSnJ0sJJ9sk6Yq^^84DflWKF*s7-Du8mvEYxg z$`eDc?{P^rdEy%5z$wCg)kuCEoMGhQRdZ4P zAN*ITtL{5LpRzqCUi{8yh*O0B!GBOr2E0Ca!&T1zxs_dB-l6X;VE zw0>6Mf_mchtqThEX+!^mA5pFkzEpU_*=sp%-<4AZRV|5I`>t@+UW#j+ywvUbBB0L%wEgjl@m8R9&G%vMtCxnlxM(q5Oaoj&Dlo2JuIsk<=ba3>Os9!^yt|~ zj~+d|0IH9}UI67E97p@XV7agUpgcnu`6l4U$&X97d^XQAbJqA(gIzrWh+E4&uQ|dO zWnN!Tlg|s^!CK2b8&4d3Ucr*@>7S?!WVUz^N+ZLmSywL zhP~^5(raBW=XUF=uE_j~J#mc>@jDwF$aVA`wCj{#yXO4JLlY^7437!VuMGbnd*awX z$lQJujZl)svJ%4lI9eZ;5zJj)^C(l=xThJIDHt3OP=^zFM2UGxj(kn*&$2OtOnd zPUgDiZ0~CC=Gn%?7lnTibB0%m-_AK?>~R#o9X)#A>pNXCdUy}Y9NS*{I6P-)eBJJu zmf{U})&GOscV2YVzjCL;Nq1k#|XE6=|#JQ?f<2Q6NxJ-ltGM?XxsAM7z<{*~e3eKF;~nzN1m;O1*Cl;9Ww#oqH3u@AeVIXPB@x zN_%+G$1yx{D}~pGJ`Q`sw@9AB>_P6qia!8T6fP-|eHf zj6SrIc*yMGWey~`YU?#m26G1Gn?N5YJ~NFtkh#QX_|a>q^d{hmv;7Xnnda>)(>mt= z+s5x~@Q~s28bNbW-s6C)_L=7OssBOt@ZQ#(B6Gf-_s;AwL4OcEdd@S9>5xtNtFVar zm}<)_vHy+q3$fC7@ZI{J7j_*UPy50DNKU4dd=uz-v3Ch`2BSa7c?PGe*N+vHOw1ii ze9=(t#{pLje1?xlb)4oLy;eAoQKlZf$KLIQ9vQRbeHES+xOLlM&kMeCu8e&9hvFY( zzq9!}s625hFWPFzQvn{~?@+F!^axgT%QxdKTqPnHfNb% zm6>>bhS&0fez(IvsPYVlh%f4#*i3x%3n(wT&Ebsu(EgkJmQ8V#_m#mzZaTkmT>3NZ zomXm49DEZhe^n4P+2@fy_MP%-v*mqNpA;1TA?2^qiaywP`PBL<=XUwj^Ew}vL~{n@ zui!Dk`wE<*SmGfcOK53zp!@1bX|U#Ob03HK43EuNF1$V;{l3~Gy$LnHYLt09^N_jk ztoWi{)pIC^jB^EUZFA`lI-RSk@$ay*_aN_z!0?FsF*ULu#Je5;gUE}H+d3z!L36eh zPlo4LbH~pU&NlWq@Z-$rKG8I10Jk=fxN6+H9_y1!{#+5hv3UQSvu{=JY2R7+4BjypECi!_$utukQfmWVr9#OY)*2)bsMzJQ;Ad z@ovZeAp4!c*+yOzy@|%8i%s$jYJSB%dh~Jbi-&jomW!(#mX3>`XMJsP66KIpe~{-4 z?4!4vlTPm|~R{OwjY=$^LB7+`M$!teK`4Xz^%pn z3O%of=i3b*J?2;JwPYVX`*E-zRJp#jyn zo;u$SP7(6$mYJW;OP6{3hEbmmnA$zi&;E3ZVZ% zaJC1L*OIwv&tDj+`-6su7aU0V=-F!-EF8!Z=}mwG$vheMJA+%xy;StPUU6$?cd(+% zk&ta|sON?E6}W2bOWh^;E9?is7rjgTD?7;{!{_BDa|U>Lv3F(sZ1 zmN}5{TBfDB=N~AYMxHqIaqw=3cZuKa%z@;*D0~y}rFIp*sG5tuY{S_eDqOWLp1ax) zG37?}(0aq>6k{{>^!Oi~zvwmkAB>PZgW}fmyd54B^itpOUfFwM zhpL)p=Nb+K={}A*hph4p%DZ%3ya3ocgR6%A;G@z@RsKP6KalH79s9(LpXQFR)W+S{ zc?MfQ4(Az=7mYs}q?koP)1PJB`JJEKRRPxnubu3A}14f)N0BzFt>Ipy{~Yt@`?Mwd{OkgMh;4*Ji|rld6gtSWgTSd zc`4rndi2O&8U8_o*T+6D^t^&s25fdb*zn_BI#>O@uLgb)(GYW2`0e1=8NW5wQ5(; z>pQRa{ZEE1pTCdz?cj?lo(%X5CDilc-h?aVuP_(obH#ZEA4`SKGr*TxapD%e+u1+Z zp5~(P(c9U*>UOnXmh=ZNT82oT0lw6?X+Jod_Ria_Eo4849=+iWpD8(Hcmb4m2|X`= z@(*H3=Yg_Jg-;#+&jC z_nP>;)VWeQ-rLrFfUQ2iZc<&rfzKOoEHS})(HSiVhT*~!1oNExS z8oW#9@1V+Gaer{H?s>6)kbCsVU%At{LN66O8FjAA^Q-*Pjsx$D&kI~N^t@VX9x^;} z=y`dI#{_$v#qY^pZV6}NNa%Z4`M&aeVju14x&eoT%XFxjMkiOvpr>3d%EJkAv?Z`0elyvM&|<_HM*OMjvO=X8VJUKi1Z~N4|+GfgeWH z(z!ye51b;X`98 zbdR3>gI`rVNgPPzuew-wC!R?^Ur?j@qR91Ok2At@FRmea&GhwjUpYB%y%u!*-=$CH zuSj!Dbg>>y=uN!|Pke5$cN4Y-sJ1cIj`VR6Q2f04x)*^p3-13v`2MrEn zh49-IXB*!^_~_w@yBqo&@vnwRz8!f{_??y465LwurGopx{Xw%I=cM=tmEW0tscJ5| zlg^dur5b)5 zyTsg&63R2=x+e6E^?g6&o5+6Rcm5@C0G%u3WYD7zl08mS--OCDT&8nX9hybFzQ#r) z&+t}4XUX+}e+54dIFM;$JIwg03I7T{FU7xVDO@#!TZ{RX>O1qi9X@)scUE%-+*iEE zL0%O9gYEhR`>fOc!Hu&D^xm2GIF+mJ(_GYRaeQVf{SOw9FSXojr2h|LN%Oz5^tQ=g zF;50LWZvU|FUnjs_Aad-y=HnHafDwsmqA#Kb@#4J6~{yc*t)_{)#!y~@6?REOUi4BxhS}5e;#w7c{{w8PflwdofnsG*(Z5XSC7Efw@ti0 zd8OJ&C8&e=4Xc zJ1?9f%-eZC$nPuoomC$P@Ah=^O&m@5N6OZm_~JP6E-7y9KjS{vp13^S^GX(<7xz-f zZ5egwqw^bT?~{KJ^DFe|F=xQuxow~NCOKr~OGR%2JQRjb$j|u(=|`M5UYmUUU-Z7>-oy(j#pHKJZ$jl6?8%p^{5ar?g8MOL zU%KS_m|KhQpxGP#SALu^Tds)T`AvGab56#kU0$!V!f*dl=i7Z{E~?Jev4j@V9|RBC zoI^Ih+l@ZX&9Vye(Yx#Kpuzp%p4Xn(UxfS7j`Ho^)!}DL$?xoL>Ur^Zka>NV0%t|s zjk$06yZ#Qs?+h;h_Rft$eHdrIhB(FJ5#spmK*pIr%0q zzj`@tza?e&M z<`gN93BRvC8XP@--pqsZmRgF4FIqTi+kk1^CrbVb?{+nB=R5=VyfzK^*p$DDv)Ma8 zEB}M+;f>6iC_OLSSL`3e|6m>E`d+0RGSAzQlW`HA%=Hx!8%y?wpB-Mkz5P?3kLbSh z7qP!Z-3whY@~WT#A9rZe9c<+q;;92)A4pS>*jxH?Tn$uf*Eiq@nchFgUsoe7dxArlai-KFr97yE_;CwrB zGT1vKCxgB7=hB-{`@tddZoff!QHS^zNg>49X8slO443J?suf<}Lf=^WAAIq`r-y^L zevs8-*}vllTW1qb26It(;=ujj|G{K>Uop29oFaI5bA(&_AZL4%-aDsgUn<^L)q2i= zTp#jRm@}Z~h5td7lj#>*O?lBu;xjP!gZb?(lA6Wuuy$Ya8+pS&JUhI)xP3R;@JAuq~Yrg<{9=ZfbH z-u+J#w>E+HIQ(vJNgPP_0)WrJT(vsNi~dY|XXZe1{tEwt*blx*`$3*Fphtg=&K2$} zysvtNchmDL#b@ZUq|9m;-zsV1%FxaACT}?R=)o5~F5Yl_2l2j|xHVk(qS11$CYk1< zF_afAB;Ulxqc#kf)qS*63+bgYPX^wliMe^gzv8^8%D3;E7f1Xn<9~2_;j`3tJ{s@1 z>_g(#GFOfJgUE}5f5o0S%o(^x4_+U<0O${5&VU|0d*a{)K#%?x@=e?cT{-f1%3m4( zgUSm4uOb~XLH-Wk6=q=-f*9V_hf%e21dh=MCg{ z{={Lj%^t@oaYb6k{I5%cs6WViXXN_Wmx`VjJaO=bKgd;^wygtsOa{7D>bxl4S00+P zJw*El-L=n4c`e5h4;j4)BPWyKn;<-yGRia5Q=Y--rB-WRpW{`J$W`<(^lD_J*TBX!N|m z>r?0Ie_=lUXNV`0Q}z3)$@|iZ5;F%;e-OP?|1EwdJ_Gj71;i;TnpH?SWc9vMdf^vS zQEB3Aw-)Y)dEO3A(Xf~v;eQi;J9=K9?vLJhWyRmbYl;0J{Da`htfB8<1m%$X#C}V; zzIgEuA}0es&bRRcXpdveudpBFIRoBT*yC)XTpxc2*Omn2j-x(K$imU7lXTx1Juma# z+3>_w6HkWU?d*w*(C3Ohal=EtkL(?LUvswC(z$XtXLm4gbI8gTNp_TzxxM0pjl0MT zfIZG5l}MJ9AFvlF3KUyuM1hukLE@2lM(k&)}r_46!@e>JKva13j+>?Z;ss{k@nvi)ZYa$YGRkSNW^)Tcfh^>L4$&lPeq z@P>n13tk`R86Nx}bf$be=AyGUZkPO(tLLLb+)Vs-o-@4S=0NlI&~5FqTT5@^G~HL= zA#;Ba-$C$X_&W%$+Wn{7IIkzZXlLQ|aSqv99CtLhvY7Y`;MQ_4b*T+k?N{=KCy*~y z@!Lns|Df47@!)=N!j_Ax?3YfEd^^uY^Qn*Xo!2P;@59pPe`a|lu3>iClns7E`=6!! z6`!jY^}C%tCYUqSOMmcAw4FtH;?#WwJ_Gl>IM1N^IF5ENyZtWwtG$wE!2h7TXKDMl ztE10u-9KmJuPeH14=?X=?vW=B+*dEZHIIT@C<*8-dF6k#CNcP z?ko1CqUVMGLAR8P=%A-OKvy%tr~eN^j1_^>{W|b z%=6Uq;v6#1MX?{;DZIX>=PDwiCZ>-1ILI>~*T;EL+*j~<8NP|Y^6Hzxt)KA7qMQuo zqHE(?L_axg^Qg_#n`nOZ=CQRULAmdzPF`qdowD%#)JeIyvLEE$1o-U*n$G|)0Osw% z#BWEgZ=m(RHvD#Q)e`AD2=5a2yk7UtBA*xdqJf(yuXIRqj^8f-gQfdFJUgs!eLN9gg=w<`!C?}(O6Yx7TpMlR6yy5Uo z;C%%iGWZO>G{0)qE|2D->U}jn-hSyN$;oUG9&!QYkQ*&|3pSJ2vW?8!-#av6%VptT zDgWRv!c_xj`+uf919Kqp9kk6eDBpzHYl$Acx8?E!^LFf=;kA5A{DaDuivPiClh^X8 zfHwL&xI_AbyERY7ma~of3Ukr&$l<2GGv@8^(WB=Tv#5JUDS6^>t~z*R4~`fgJM#xtI~5AS+38I`{Zpgqpw%;chJw0CYr?VDiv%m3zfr2b$E<*(Qqj=89-spo|}!^>_r`h6sR z9PW9cH^F;ncrDorP(mIPyszNl<+-Th$=oL|0DIyT2a>%2n76ZU0v;11e}%lLdEWjZ z<2j~V=sH>^`qRUHO^uGAo!xlUj?+jNB@J{$Un$Fdhle}!;Ai4oa}M# zneMA`x$@&Tldd7`%d-F_T4;N30Lg`^}N_?sq*dUwJ}>4oActH}{Hym4jn{i;c$YZWx0d>YXT65|{}7fu|A3_r zc}zC?4eNi3xF3m>>w7-sxb|8aJ};GTuQT;h(Z^{z7iC}S6G#3>5* z(%ExY`=PSO`F%xq&4C1;LFE}F(DQcqQiG}Q%$_*T$$pM3C%X<-@VUFoLxPyB1 z>s({|Ci?p79{tRSE7Zr~_Z52q*f(MDMc=93P4D(D0WXBV9dp_8Ir-?hmx}jQ$0Jj= zwaIQPIppuP$7BzktCX>i�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