diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index c8ab489311..5864cadc15 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -57,6 +57,7 @@ Agent::Agent(ReceivedMessage& message) : ThreadedAssignment(message), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES) { + _entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT); DependencyManager::get()->setPacketSender(&_entityEditSender); ResourceManager::init(); diff --git a/assignment-client/src/AssignmentDynamic.cpp b/assignment-client/src/AssignmentDynamic.cpp index 026bc120bb..7adbd55c39 100644 --- a/assignment-client/src/AssignmentDynamic.cpp +++ b/assignment-client/src/AssignmentDynamic.cpp @@ -53,31 +53,3 @@ QVariantMap AssignmentDynamic::getArguments() { qDebug() << "UNEXPECTED -- AssignmentDynamic::getArguments called in assignment-client."; return QVariantMap(); } - -glm::vec3 AssignmentDynamic::getPosition() { - qDebug() << "UNEXPECTED -- AssignmentDynamic::getPosition called in assignment-client."; - return glm::vec3(0.0f); -} - -glm::quat AssignmentDynamic::getRotation() { - qDebug() << "UNEXPECTED -- AssignmentDynamic::getRotation called in assignment-client."; - return glm::quat(); -} - -glm::vec3 AssignmentDynamic::getLinearVelocity() { - qDebug() << "UNEXPECTED -- AssignmentDynamic::getLinearVelocity called in assignment-client."; - return glm::vec3(0.0f); -} - -void AssignmentDynamic::setLinearVelocity(glm::vec3 linearVelocity) { - qDebug() << "UNEXPECTED -- AssignmentDynamic::setLinearVelocity called in assignment-client."; -} - -glm::vec3 AssignmentDynamic::getAngularVelocity() { - qDebug() << "UNEXPECTED -- AssignmentDynamic::getAngularVelocity called in assignment-client."; - return glm::vec3(0.0f); -} - -void AssignmentDynamic::setAngularVelocity(glm::vec3 angularVelocity) { - qDebug() << "UNEXPECTED -- AssignmentDynamic::setAngularVelocity called in assignment-client."; -} diff --git a/assignment-client/src/AssignmentDynamic.h b/assignment-client/src/AssignmentDynamic.h index 35db8b1524..d0fd72533f 100644 --- a/assignment-client/src/AssignmentDynamic.h +++ b/assignment-client/src/AssignmentDynamic.h @@ -24,6 +24,8 @@ public: AssignmentDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); virtual ~AssignmentDynamic(); + virtual void remapIDs(QHash& map) override {}; + virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } virtual void setOwnerEntity(const EntityItemPointer ownerEntity) override { _ownerEntity = ownerEntity; } @@ -37,13 +39,6 @@ private: QByteArray _data; protected: - virtual glm::vec3 getPosition() override; - virtual glm::quat getRotation() override; - virtual glm::vec3 getLinearVelocity() override; - virtual void setLinearVelocity(glm::vec3 linearVelocity) override; - virtual glm::vec3 getAngularVelocity() override; - virtual void setAngularVelocity(glm::vec3 angularVelocity) override; - bool _active; EntityItemWeakPointer _ownerEntity; }; diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index b95c429b2d..bb03a6ec93 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -38,13 +38,14 @@ #include "AudioMixer.h" static const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.5f; // attenuation = -6dB * log2(distance) +static const int DISABLE_STATIC_JITTER_FRAMES = -1; static const float DEFAULT_NOISE_MUTING_THRESHOLD = 1.0f; static const QString AUDIO_MIXER_LOGGING_TARGET_NAME = "audio-mixer"; static const QString AUDIO_ENV_GROUP_KEY = "audio_env"; static const QString AUDIO_BUFFER_GROUP_KEY = "audio_buffer"; static const QString AUDIO_THREADING_GROUP_KEY = "audio_threading"; -int AudioMixer::_numStaticJitterFrames{ -1 }; +int AudioMixer::_numStaticJitterFrames{ DISABLE_STATIC_JITTER_FRAMES }; float AudioMixer::_noiseMutingThreshold{ DEFAULT_NOISE_MUTING_THRESHOLD }; float AudioMixer::_attenuationPerDoublingInDistance{ DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE }; std::map> AudioMixer::_availableCodecs{ }; @@ -56,7 +57,12 @@ QVector AudioMixer::_zoneReverbSettings; AudioMixer::AudioMixer(ReceivedMessage& message) : ThreadedAssignment(message) { + // Always clear settings first + // This prevents previous assignment settings from sticking around + clearDomainSettings(); + // hash the available codecs (on the mixer) + _availableCodecs.clear(); // Make sure struct is clean auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); std::for_each(codecPlugins.cbegin(), codecPlugins.cend(), [&](const CodecPluginPointer& codec) { @@ -232,7 +238,7 @@ void AudioMixer::sendStatsPacket() { } // general stats - statsObject["useDynamicJitterBuffers"] = _numStaticJitterFrames == -1; + statsObject["useDynamicJitterBuffers"] = _numStaticJitterFrames == DISABLE_STATIC_JITTER_FRAMES; statsObject["threads"] = _slavePool.numThreads(); @@ -490,6 +496,16 @@ int AudioMixer::prepareFrame(const SharedNodePointer& node, unsigned int frame) return data->checkBuffersBeforeFrameSend(); } +void AudioMixer::clearDomainSettings() { + _numStaticJitterFrames = DISABLE_STATIC_JITTER_FRAMES; + _attenuationPerDoublingInDistance = DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE; + _noiseMutingThreshold = DEFAULT_NOISE_MUTING_THRESHOLD; + _codecPreferenceOrder.clear(); + _audioZones.clear(); + _zoneSettings.clear(); + _zoneReverbSettings.clear(); +} + void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { qDebug() << "AVX2 Support:" << (cpuSupportsAVX2() ? "enabled" : "disabled"); @@ -525,7 +541,7 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { qDebug() << "Static desired jitter buffer frames:" << _numStaticJitterFrames; } else { qDebug() << "Disabling dynamic jitter buffers."; - _numStaticJitterFrames = -1; + _numStaticJitterFrames = DISABLE_STATIC_JITTER_FRAMES; } // check for deprecated audio settings diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 0641c04a6c..18e754016e 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -79,6 +79,7 @@ private: QString percentageForMixStats(int counter); void parseSettingsObject(const QJsonObject& settingsObject); + void clearDomainSettings(); float _trailingMixRatio { 0.0f }; float _throttlingRatio { 0.0f }; diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 05dbfee912..870149f1bc 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -665,12 +665,12 @@ void AvatarMixer::sendStatsPacket() { void AvatarMixer::run() { qCDebug(avatars) << "Waiting for connection to domain to request settings from domain-server."; - + // wait until we have the domain-server settings, otherwise we bail DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::settingsReceived, this, &AvatarMixer::domainSettingsRequestComplete); connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &AvatarMixer::domainSettingsRequestFailed); - + ThreadedAssignment::commonInit(AVATAR_MIXER_LOGGING_NAME, NodeType::AvatarMixer); } @@ -695,7 +695,7 @@ void AvatarMixer::domainSettingsRequestComplete() { // parse the settings to pull out the values we need parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject()); - + // start our tight loop... start(); } @@ -745,7 +745,7 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { } else { qCDebug(avatars) << "Avatar mixer will automatically determine number of threads to use. Using:" << _slavePool.numThreads() << "threads."; } - + const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_SCALE_OPTION = "min_avatar_scale"; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 1449005246..76519466b5 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -145,7 +145,7 @@ private: std::unordered_map> _lastOtherAvatarSentJoints; uint64_t _identityChangeTimestamp; - bool _avatarSessionDisplayNameMustChange{ false }; + bool _avatarSessionDisplayNameMustChange{ true }; int _numAvatarsSentLastFrame = 0; int _numFramesSinceAdjustment = 0; diff --git a/assignment-client/src/scripts/EntityScriptServer.h b/assignment-client/src/scripts/EntityScriptServer.h index 687641d6be..696b082467 100644 --- a/assignment-client/src/scripts/EntityScriptServer.h +++ b/assignment-client/src/scripts/EntityScriptServer.h @@ -24,9 +24,6 @@ #include #include -static const int DEFAULT_MAX_ENTITY_PPS = 9000; -static const int DEFAULT_ENTITY_PPS_PER_SCRIPT = 900; - class EntityScriptServer : public ThreadedAssignment { Q_OBJECT diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index d4d4b42e10..1bf195fc84 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip - URL_MD5 bc2861e50852dd590cdc773a14a041a7 + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi8.zip + URL_MD5 b01510437ea15527156bc25cdf733bd9 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/cmake/modules/FindFBX.cmake b/cmake/modules/FindFBX.cmake new file mode 100644 index 0000000000..7f6a424aa1 --- /dev/null +++ b/cmake/modules/FindFBX.cmake @@ -0,0 +1,114 @@ +# Locate the FBX SDK +# +# Defines the following variables: +# +# FBX_FOUND - Found the FBX SDK +# FBX_VERSION - Version number +# FBX_INCLUDE_DIRS - Include directories +# FBX_LIBRARIES - The libraries to link to +# +# Accepts the following variables as input: +# +# FBX_VERSION - as a CMake variable, e.g. 2017.0.1 +# FBX_ROOT - (as a CMake or environment variable) +# The root directory of the FBX SDK install + +# adapted from https://github.com/ufz-vislab/VtkFbxConverter/blob/master/FindFBX.cmake +# which uses the MIT license (https://github.com/ufz-vislab/VtkFbxConverter/blob/master/LICENSE.txt) + +if (NOT FBX_VERSION) + if (WIN32) + set(FBX_VERSION 2017.1) + else() + set(FBX_VERSION 2017.0.1) + endif() +endif() + +string(REGEX REPLACE "^([0-9]+).*$" "\\1" FBX_VERSION_MAJOR "${FBX_VERSION}") +string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_MINOR "${FBX_VERSION}") +string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_PATCH "${FBX_VERSION}") + +set(FBX_MAC_LOCATIONS "/Applications/Autodesk/FBX\ SDK/${FBX_VERSION}") + +if (WIN32) + string(REGEX REPLACE "\\\\" "/" WIN_PROGRAM_FILES_X64_DIRECTORY $ENV{ProgramW6432}) +endif() + +set(FBX_WIN_LOCATIONS "${WIN_PROGRAM_FILES_X64_DIRECTORY}/Autodesk/FBX/FBX SDK/${FBX_VERSION}") + +set(FBX_SEARCH_LOCATIONS $ENV{FBX_ROOT} ${FBX_ROOT} ${FBX_MAC_LOCATIONS} ${FBX_WIN_LOCATIONS}) + +function(_fbx_append_debugs _endvar _library) + if (${_library} AND ${_library}_DEBUG) + set(_output optimized ${${_library}} debug ${${_library}_DEBUG}) + else() + set(_output ${${_library}}) + endif() + + set(${_endvar} ${_output} PARENT_SCOPE) +endfunction() + +if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + set(fbx_compiler clang) +elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") + set(fbx_compiler gcc4) +endif() + +function(_fbx_find_library _name _lib _suffix) + if (MSVC12) + set(VS_PREFIX vs2013) + endif() + + if (MSVC11) + set(VS_PREFIX vs2012) + endif() + + if (MSVC10) + set(VS_PREFIX vs2010) + endif() + + if (MSVC90) + set(VS_PREFIX vs2008) + endif() + + find_library(${_name} + NAMES ${_lib} + HINTS ${FBX_SEARCH_LOCATIONS} + PATH_SUFFIXES lib/${fbx_compiler}/${_suffix} lib/${fbx_compiler}/ub/${_suffix} lib/${VS_PREFIX}/x64/${_suffix} + ) + + mark_as_advanced(${_name}) +endfunction() + +find_path(FBX_INCLUDE_DIR fbxsdk.h + PATHS ${FBX_SEARCH_LOCATIONS} + PATH_SUFFIXES include +) +mark_as_advanced(FBX_INCLUDE_DIR) + +if (WIN32) + _fbx_find_library(FBX_LIBRARY libfbxsdk-md release) + _fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk-md debug) +elseif (APPLE) + find_library(CARBON NAMES Carbon) + find_library(SYSTEM_CONFIGURATION NAMES SystemConfiguration) + _fbx_find_library(FBX_LIBRARY libfbxsdk.a release) + _fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk.a debug) +endif() + +include(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(FBX DEFAULT_MSG FBX_LIBRARY FBX_INCLUDE_DIR) + +if (FBX_FOUND) + set(FBX_INCLUDE_DIRS ${FBX_INCLUDE_DIR}) + _fbx_append_debugs(FBX_LIBRARIES FBX_LIBRARY) + add_definitions(-DFBXSDK_NEW_API) + + if (WIN32) + add_definitions(-DK_PLUGIN -DK_FBXSDK -DK_NODLL) + set(CMAKE_EXE_LINKER_FLAGS /NODEFAULTLIB:\"LIBCMT\") + set(FBX_LIBRARIES ${FBX_LIBRARIES} Wininet.lib) + elseif (APPLE) + set(FBX_LIBRARIES ${FBX_LIBRARIES} ${CARBON} ${SYSTEM_CONFIGURATION}) + endif() +endif() diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 9efe3dd29b..b18599d8a9 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -49,48 +49,71 @@ "id": "ik", "type": "inverseKinematics", "data": { + "solutionSource": "relaxToUnderPoses", + "solutionSourceVar": "solutionSource", "targets": [ { "jointName": "Hips", "positionVar": "hipsPosition", "rotationVar": "hipsRotation", - "typeVar": "hipsType" + "typeVar": "hipsType", + "weightVar": "hipsWeight", + "weight": 1.0, + "flexCoefficients": [1] }, { "jointName": "RightHand", "positionVar": "rightHandPosition", "rotationVar": "rightHandRotation", - "typeVar": "rightHandType" + "typeVar": "rightHandType", + "weightVar": "rightHandWeight", + "weight": 1.0, + "flexCoefficients": [1, 0.5, 0.5, 0.25, 0.1, 0.05, 0.01, 0.0, 0.0] }, { "jointName": "LeftHand", "positionVar": "leftHandPosition", "rotationVar": "leftHandRotation", - "typeVar": "leftHandType" + "typeVar": "leftHandType", + "weightVar": "leftHandWeight", + "weight": 1.0, + "flexCoefficients": [1, 0.5, 0.5, 0.25, 0.1, 0.05, 0.01, 0.0, 0.0] }, { "jointName": "RightFoot", "positionVar": "rightFootPosition", "rotationVar": "rightFootRotation", - "typeVar": "rightFootType" + "typeVar": "rightFootType", + "weightVar": "rightFootWeight", + "weight": 1.0, + "flexCoefficients": [1, 0.45, 0.45] }, { "jointName": "LeftFoot", "positionVar": "leftFootPosition", "rotationVar": "leftFootRotation", - "typeVar": "leftFootType" + "typeVar": "leftFootType", + "weightVar": "leftFootWeight", + "weight": 1.0, + "flexCoefficients": [1, 0.45, 0.45] }, { "jointName": "Spine2", "positionVar": "spine2Position", "rotationVar": "spine2Rotation", - "typeVar": "spine2Type" + "typeVar": "spine2Type", + "weightVar": "spine2Weight", + "weight": 1.0, + "flexCoefficients": [0.45, 0.45] }, { "jointName": "Head", "positionVar": "headPosition", "rotationVar": "headRotation", - "typeVar": "headType" + "typeVar": "headType", + "weightVar": "headWeight", + "weight": 4.0, + "flexCoefficients": [1, 0.5, 0.5, 0.5, 0.5] } ] }, diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 4fbdb37abf..4491507a9c 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -35,6 +35,11 @@ { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, { "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] }, - { "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] } + { "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }, + { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", "when": [ "Application.InHMD"] }, + { "from": "Vive.RightFoot", "to" : "Standard.RightFoot", "when": [ "Application.InHMD"] }, + { "from": "Vive.Hips", "to" : "Standard.Hips", "when": [ "Application.InHMD"] }, + { "from": "Vive.Spine2", "to" : "Standard.Spine2", "when": [ "Application.InHMD"] }, + { "from": "Vive.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] } ] } diff --git a/interface/resources/icons/tablet-icons/raise-hand-a.svg b/interface/resources/icons/tablet-icons/raise-hand-a.svg new file mode 100644 index 0000000000..fd35073332 --- /dev/null +++ b/interface/resources/icons/tablet-icons/raise-hand-a.svg @@ -0,0 +1,70 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/raise-hand-i.svg b/interface/resources/icons/tablet-icons/raise-hand-i.svg new file mode 100644 index 0000000000..50a6aa2606 --- /dev/null +++ b/interface/resources/icons/tablet-icons/raise-hand-i.svg @@ -0,0 +1,60 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/interface/resources/images/Default-Sky-9-ambient.jpg b/interface/resources/images/Default-Sky-9-ambient.jpg deleted file mode 100644 index 8fb383c5e8..0000000000 Binary files a/interface/resources/images/Default-Sky-9-ambient.jpg and /dev/null differ diff --git a/interface/resources/images/Default-Sky-9-cubemap.jpg b/interface/resources/images/Default-Sky-9-cubemap.jpg deleted file mode 100644 index 697fd9aeea..0000000000 Binary files a/interface/resources/images/Default-Sky-9-cubemap.jpg and /dev/null differ diff --git a/interface/resources/images/Default-Sky-9-cubemap.ktx b/interface/resources/images/Default-Sky-9-cubemap.ktx new file mode 100644 index 0000000000..476d381a8c Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap.ktx differ diff --git a/interface/resources/qml/controls/TabletWebButton.qml b/interface/resources/qml/controls/TabletWebButton.qml index a5876d08dd..d016f71f2d 100644 --- a/interface/resources/qml/controls/TabletWebButton.qml +++ b/interface/resources/qml/controls/TabletWebButton.qml @@ -17,26 +17,26 @@ Rectangle { property alias pixelSize: label.font.pixelSize; property bool selected: false property bool hovered: false - property bool enabled: false property int spacing: 2 property var action: function () {} property string enabledColor: hifi.colors.blueHighlight property string disabledColor: hifi.colors.blueHighlight - property string highlightColor: hifi.colors.blueHighlight; width: label.width + 64 height: 32 color: hifi.colors.white + enabled: false + HifiConstants { id: hifi } + RalewaySemiBold { id: label; - color: enabledColor + color: enabled ? enabledColor : disabledColor font.pixelSize: 15; anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } } - Rectangle { id: indicator diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml index d288872289..d939e088a8 100644 --- a/interface/resources/qml/controls/TabletWebView.qml +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -8,6 +8,7 @@ import "../styles" as HifiStyles import "../styles-uit" import "../" import "." + Item { id: web HifiConstants { id: hifi } @@ -22,17 +23,14 @@ Item { property bool keyboardRaised: false property bool punctuationMode: false property bool isDesktop: false - property string initialPage: "" - property bool startingUp: true property alias webView: webview property alias profile: webview.profile property bool remove: false - property var urlList: [] - property var forwardList: [] - - property int currentPage: -1 // used as a model for repeater - property alias pagesModel: pagesModel + // Manage own browse history because WebEngineView history is wiped when a new URL is loaded via + // onNewViewRequested, e.g., as happens when a social media share button is clicked. + property var history: [] + property int historyIndex: -1 Rectangle { id: buttons @@ -51,21 +49,22 @@ Item { TabletWebButton { id: back - enabledColor: hifi.colors.baseGray - enabled: false + enabledColor: hifi.colors.darkGray + disabledColor: hifi.colors.lightGrayText + enabled: historyIndex > 0 text: "BACK" MouseArea { anchors.fill: parent onClicked: goBack() - hoverEnabled: true - } } TabletWebButton { id: close enabledColor: hifi.colors.darkGray + disabledColor: hifi.colors.lightGrayText + enabled: true text: "CLOSE" MouseArea { @@ -75,7 +74,6 @@ Item { } } - RalewaySemiBold { id: displayUrl color: hifi.colors.baseGray @@ -90,7 +88,6 @@ Item { } } - MouseArea { anchors.fill: parent preventStealing: true @@ -98,29 +95,10 @@ Item { } } - ListModel { - id: pagesModel - onCountChanged: { - currentPage = count - 1; - if (currentPage > 0) { - back.enabledColor = hifi.colors.darkGray; - } else { - back.enabledColor = hifi.colors.baseGray; - } - } - } - function goBack() { - if (webview.canGoBack) { - forwardList.push(webview.url); - webview.goBack(); - } else if (web.urlList.length > 0) { - var url = web.urlList.pop(); - loadUrl(url); - } else if (web.forwardList.length > 0) { - var url = web.forwardList.pop(); - loadUrl(url); - web.forwardList = []; + if (historyIndex > 0) { + historyIndex--; + loadUrl(history[historyIndex]); } } @@ -137,19 +115,12 @@ Item { } function goForward() { - if (currentPage < pagesModel.count - 1) { - currentPage++; + if (historyIndex < history.length - 1) { + historyIndex++; + loadUrl(history[historyIndex]); } } - function gotoPage(url) { - urlAppend(url) - } - - function isUrlLoaded(url) { - return (pagesModel.get(currentPage).webUrl === url); - } - function reloadPage() { view.reloadAndBypassCache() view.setActiveFocusOnPress(true); @@ -161,36 +132,8 @@ Item { web.url = webview.url; } - function onInitialPage(url) { - return (url === webview.url); - } - - - function urlAppend(url) { - var lurl = decodeURIComponent(url) - if (lurl[lurl.length - 1] !== "/") { - lurl = lurl + "/" - } - web.urlList.push(url); - setBackButtonStatus(); - } - - function setBackButtonStatus() { - if (web.urlList.length > 0 || webview.canGoBack) { - back.enabledColor = hifi.colors.darkGray; - back.enabled = true; - } else { - back.enabledColor = hifi.colors.baseGray; - back.enabled = false; - } - } - onUrlChanged: { loadUrl(url); - if (startingUp) { - web.initialPage = webview.url; - startingUp = false; - } } QtObject { @@ -258,6 +201,17 @@ Item { grantFeaturePermission(securityOrigin, feature, true); } + onUrlChanged: { + // Record history, skipping null and duplicate items. + var urlString = url + ""; + urlString = urlString.replace(/\//g, "%2F"); // Consistent representation of "/"s to avoid false differences. + if (urlString.length > 0 && (historyIndex === -1 || urlString !== history[historyIndex])) { + historyIndex++; + history = history.slice(0, historyIndex); + history.push(urlString); + } + } + onLoadingChanged: { keyboardRaised = false; punctuationMode = false; @@ -277,17 +231,11 @@ Item { } if (WebEngineView.LoadSucceededStatus == loadRequest.status) { - if (startingUp) { - web.initialPage = webview.url; - startingUp = false; - } webview.forceActiveFocus(); } } onNewViewRequested: { - var currentUrl = webview.url; - urlAppend(currentUrl); request.openIn(webview); } } diff --git a/interface/resources/qml/hifi/Audio.qml b/interface/resources/qml/hifi/Audio.qml index d0c3122100..66760ff290 100644 --- a/interface/resources/qml/hifi/Audio.qml +++ b/interface/resources/qml/hifi/Audio.qml @@ -35,11 +35,6 @@ Rectangle { property string title: "Audio Options" signal sendToScript(var message); - //set models after Components is shown - Component.onCompleted: { - refreshTimer.start() - refreshTimerOutput.start() - } Component { id: separator @@ -84,7 +79,7 @@ Rectangle { } Connections { - target: AvatarInputs + target: AvatarInputs !== undefined ? AvatarInputs : null onShowAudioToolsChanged: { audioTools.checkbox.checked = showAudioTools } @@ -105,10 +100,12 @@ Rectangle { id: audioTools width: parent.width anchors { left: parent.left; right: parent.right; leftMargin: 30 } - checkbox.checked: AvatarInputs.showAudioTools + checkbox.checked: AvatarInputs !== undefined ? AvatarInputs.showAudioTools : false text.text: qsTr("Show audio level meter") onCheckBoxClicked: { - AvatarInputs.showAudioTools = checked + if (AvatarInputs !== undefined) { + AvatarInputs.showAudioTools = checked + } } } @@ -138,30 +135,34 @@ Rectangle { } ListView { - Timer { - id: refreshTimer - interval: 1 - repeat: false - onTriggered: { - //refresh model - inputAudioListView.model = undefined - inputAudioListView.model = AudioDevice.inputAudioDevices - } - } id: inputAudioListView anchors { left: parent.left; right: parent.right; leftMargin: 70 } height: 125 - spacing: 16 + spacing: 0 clip: true snapMode: ListView.SnapToItem - delegate: AudioCheckbox { + model: AudioDevice + delegate: Item { width: parent.width - checkbox.checked: (modelData === AudioDevice.getInputDevice()) - text.text: modelData - onCheckBoxClicked: { - if (checked) { - AudioDevice.setInputDevice(modelData) - refreshTimer.start() + visible: devicemode === 0 + height: visible ? 36 : 0 + + AudioCheckbox { + id: cbin + anchors.verticalCenter: parent.verticalCenter + Binding { + target: cbin.checkbox + property: 'checked' + value: devicechecked + } + + width: parent.width + cbchecked: devicechecked + text.text: devicename + onCheckBoxClicked: { + if (checked) { + AudioDevice.setInputDeviceAsync(devicename) + } } } } @@ -191,31 +192,33 @@ Rectangle { text: qsTr("CHOOSE OUTPUT DEVICE") } } + ListView { id: outputAudioListView - Timer { - id: refreshTimerOutput - interval: 1 - repeat: false - onTriggered: { - //refresh model - outputAudioListView.model = undefined - outputAudioListView.model = AudioDevice.outputAudioDevices - } - } anchors { left: parent.left; right: parent.right; leftMargin: 70 } height: 250 - spacing: 16 + spacing: 0 clip: true snapMode: ListView.SnapToItem - delegate: AudioCheckbox { + model: AudioDevice + delegate: Item { width: parent.width - checkbox.checked: (modelData === AudioDevice.getOutputDevice()) - text.text: modelData - onCheckBoxClicked: { - if (checked) { - AudioDevice.setOutputDevice(modelData) - refreshTimerOutput.start() + visible: devicemode === 1 + height: visible ? 36 : 0 + AudioCheckbox { + id: cbout + width: parent.width + anchors.verticalCenter: parent.verticalCenter + Binding { + target: cbout.checkbox + property: 'checked' + value: devicechecked + } + text.text: devicename + onCheckBoxClicked: { + if (checked) { + AudioDevice.setOutputDeviceAsync(devicename) + } } } } diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index fc108f47e3..c1bd35f49d 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -238,8 +238,25 @@ Column { stackShadowNarrowing: root.stackShadowNarrowing; shadowHeight: root.stackedCardShadowHeight; - hoverThunk: function () { scroll.currentIndex = index; } - unhoverThunk: function () { scroll.currentIndex = -1; } + hoverThunk: function () { scrollToIndex(index); } + unhoverThunk: function () { scrollToIndex(-1); } } } + NumberAnimation { + id: anim; + target: scroll; + property: "contentX"; + duration: 250; + } + function scrollToIndex(index) { + anim.running = false; + var pos = scroll.contentX; + var destPos; + scroll.positionViewAtIndex(index, ListView.Contain); + destPos = scroll.contentX; + anim.from = pos; + anim.to = destPos; + scroll.currentIndex = index; + anim.running = true; + } } diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 0927d46fdf..a86defdfd7 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -132,62 +132,16 @@ Item { color: hifi.colors.textFieldLightBackground border.color: hifi.colors.blueHighlight border.width: 0 - TextInput { - id: myDisplayNameText - // Properties - text: thisNameCard.displayName - maximumLength: 256 - clip: true - // Size - width: parent.width - height: parent.height - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: parent.right - anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin - // Style - color: hifi.colors.darkGray - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } - font.family: firaSansSemiBold.name - font.pixelSize: displayNameTextPixelSize - selectionColor: hifi.colors.blueAccent - selectedTextColor: "black" - // Text Positioning - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignLeft - autoScroll: false; - // Signals - onEditingFinished: { - if (MyAvatar.displayName !== text) { - MyAvatar.displayName = text; - UserActivityLogger.palAction("display_name_change", text); - } - cursorPosition = 0 - focus = false - myDisplayName.border.width = 0 - color = hifi.colors.darkGray - pal.currentlyEditingDisplayName = false - autoScroll = false; - } - } MouseArea { anchors.fill: parent hoverEnabled: true onClicked: { - myDisplayName.border.width = 1 - myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); - myDisplayNameText.focus = true - myDisplayNameText.color = "black" - pal.currentlyEditingDisplayName = true - myDisplayNameText.autoScroll = true; + myDisplayNameText.focus = true; + myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX - myDisplayNameText.anchors.leftMargin, mouseY, TextInput.CursorOnCharacter); } onDoubleClicked: { myDisplayNameText.selectAll(); myDisplayNameText.focus = true; - pal.currentlyEditingDisplayName = true - myDisplayNameText.autoScroll = true; } onEntered: myDisplayName.color = hifi.colors.lightGrayText; onExited: myDisplayName.color = hifi.colors.textFieldLightBackground; @@ -207,6 +161,54 @@ Item { verticalAlignment: Text.AlignVCenter color: hifi.colors.baseGray } + TextInput { + id: myDisplayNameText + // Properties + text: thisNameCard.displayName + maximumLength: 256 + clip: true + // Size + width: parent.width + height: parent.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin + // Style + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: displayNameTextPixelSize + selectionColor: hifi.colors.blueAccent + selectedTextColor: "black" + // Text Positioning + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + autoScroll: false; + // Signals + onEditingFinished: { + if (MyAvatar.displayName !== text) { + MyAvatar.displayName = text; + UserActivityLogger.palAction("display_name_change", text); + } + focus = false; + } + onFocusChanged: { + if (focus === true) { + myDisplayName.border.width = 1 + color = "black" + autoScroll = true; + pal.currentlyEditingDisplayName = true + } else { + myDisplayName.border.width = 0 + color: hifi.colors.darkGray + cursorPosition = 0; + autoScroll = false; + pal.currentlyEditingDisplayName = false + } + } + } } // DisplayName container for others' cards Item { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 1755d2fbec..8f6b00f459 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -844,7 +844,7 @@ Rectangle { boxSize: 24; onClicked: { var newValue = model.connection !== "friend"; - connectionsUserModel.setProperty(model.userIndex, styleData.role, newValue); + connectionsUserModel.setProperty(model.userIndex, styleData.role, (newValue ? "friend" : "connection")); connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName}); diff --git a/interface/resources/qml/hifi/components/AudioCheckbox.qml b/interface/resources/qml/hifi/components/AudioCheckbox.qml index a8e0441e0a..b037fe4c7d 100644 --- a/interface/resources/qml/hifi/components/AudioCheckbox.qml +++ b/interface/resources/qml/hifi/components/AudioCheckbox.qml @@ -8,6 +8,7 @@ Row { id: row spacing: 16 property alias checkbox: cb + property alias cbchecked: cb.checked property alias text: txt signal checkBoxClicked(bool checked) diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml index 85377aaeda..17d3f1b959 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml @@ -33,6 +33,6 @@ StackView { TabletPreferencesDialog { id: root objectName: "TabletGeneralPreferences" - showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect", "Vive Pucks Configuration"] } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 2c8f6d9ea0..3e497b053e 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -136,8 +136,8 @@ Item { for (var i = 0; i < sections.length; i++) { totalHeight += sections[i].height + sections[i].getPreferencesHeight(); } - console.log(totalHeight); - return totalHeight; + var bottomPadding = 100; + return (totalHeight + bottomPadding); } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e882bad7e1..c39f7294c0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -563,11 +563,8 @@ const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; -Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) : +Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), - _shouldRunServer(runServer), - _runServerPath(runServerPathOption), - _runningMarker(this, RUNNING_MARKER_FILENAME), _window(new MainWindow(desktop())), _sessionRunTimer(startupTimer), _previousSessionCrashed(setupEssentials(argc, argv)), @@ -622,8 +619,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // make sure the debug draw singleton is initialized on the main thread. DebugDraw::getInstance().removeMarker(""); - _runningMarker.startRunningMarker(); - PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care PluginManager::getInstance()->setContainer(pluginContainer); @@ -675,38 +670,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo static const QString OCULUS_STORE_ARG = "--oculus-store"; setProperty(hifi::properties::OCULUS_STORE, arguments().indexOf(OCULUS_STORE_ARG) != -1); - static const QString NO_UPDATER_ARG = "--no-updater"; - static const bool noUpdater = arguments().indexOf(NO_UPDATER_ARG) != -1; - static const bool wantsSandboxRunning = shouldRunServer(); - static bool determinedSandboxState = false; - static bool sandboxIsRunning = false; - SandboxUtils sandboxUtils; - // updateHeartbeat() because we are going to poll shortly... updateHeartbeat(); - sandboxUtils.ifLocalSandboxRunningElse([&]() { - qCDebug(interfaceapp) << "Home sandbox appears to be running....."; - determinedSandboxState = true; - sandboxIsRunning = true; - }, [&]() { - qCDebug(interfaceapp) << "Home sandbox does not appear to be running...."; - if (wantsSandboxRunning) { - QString contentPath = getRunServerPath(); - SandboxUtils::runLocalSandbox(contentPath, true, RUNNING_MARKER_FILENAME, noUpdater); - sandboxIsRunning = true; - } - determinedSandboxState = true; - }); - - // SandboxUtils::runLocalSandbox currently has 2 sec delay after spawning sandbox, so 4 - // sec here is ok I guess. TODO: ping sandbox so we know it is up, perhaps? - quint64 MAX_WAIT_TIME = USECS_PER_SECOND * 4; - auto startWaiting = usecTimestampNow(); - while (!determinedSandboxState && (usecTimestampNow() - startWaiting <= MAX_WAIT_TIME)) { - QCoreApplication::processEvents(); - // updateHeartbeat() while polling so we don't scare the deadlock watchdog - updateHeartbeat(); - usleep(USECS_PER_MSEC * 50); // 20hz - } // start the nodeThread so its event loop is running QThread* nodeThread = new QThread(this); @@ -941,10 +905,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // 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. + static const QString TESTER = "HIFI_TESTER"; auto gpuIdent = GPUIdent::getInstance(); auto glContextData = getGLContextData(); QJsonObject properties = { { "version", applicationVersion() }, + { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) }, { "previousSessionCrashed", _previousSessionCrashed }, { "previousSessionRuntime", sessionRunTime.get() }, { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, @@ -1221,6 +1187,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo #endif // If launched from Steam, let it handle updates + const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; + bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; if (!noUpdater) { auto applicationUpdater = DependencyManager::get(); connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); @@ -1436,15 +1404,17 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool))); qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); - auto textureCache = DependencyManager::get(); + { + PROFILE_RANGE(render, "Process Default Skybox"); + auto textureCache = DependencyManager::get(); - QString skyboxUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-cubemap.jpg" }; - QString skyboxAmbientUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-ambient.jpg" }; + auto skyboxUrl = PathUtils::resourcesPath().toStdString() + "images/Default-Sky-9-cubemap.ktx"; - _defaultSkyboxTexture = textureCache->getImageTexture(skyboxUrl, image::TextureUsage::CUBE_TEXTURE, { { "generateIrradiance", false } }); - _defaultSkyboxAmbientTexture = textureCache->getImageTexture(skyboxAmbientUrl, image::TextureUsage::CUBE_TEXTURE, { { "generateIrradiance", true } }); + _defaultSkyboxTexture = gpu::Texture::unserialize(skyboxUrl); + _defaultSkyboxAmbientTexture = _defaultSkyboxTexture; - _defaultSkybox->setCubemap(_defaultSkyboxTexture); + _defaultSkybox->setCubemap(_defaultSkyboxTexture); + } EntityItem::setEntitiesShouldFadeFunction([this]() { SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); @@ -1461,110 +1431,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo const auto testScript = property(hifi::properties::TEST).toUrl(); scriptEngines->loadScript(testScript, false); } else { - enum HandControllerType { - Vive, - Oculus - }; - static const std::map MIN_CONTENT_VERSION = { - { Vive, 1 }, - { Oculus, 27 } - }; - - // Get sandbox content set version - auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; - auto contentVersionPath = acDirPath + "content-version.txt"; - qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; - int contentVersion = 0; - QFile contentVersionFile(contentVersionPath); - if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString line = contentVersionFile.readAll(); - contentVersion = line.toInt(); // returns 0 if conversion fails - } - - // Get controller availability - bool hasHandControllers = false; - HandControllerType handControllerType = Vive; - if (PluginUtils::isViveControllerAvailable()) { - hasHandControllers = true; - handControllerType = Vive; - } else if (PluginUtils::isOculusTouchControllerAvailable()) { - hasHandControllers = true; - handControllerType = Oculus; - } - - // Check tutorial content versioning - bool hasTutorialContent = contentVersion >= MIN_CONTENT_VERSION.at(handControllerType); - - // Check HMD use (may be technically available without being in use) - bool hasHMD = PluginUtils::isHMDAvailable(); - bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd(); - - Setting::Handle tutorialComplete { "tutorialComplete", false }; - Setting::Handle firstRun { Settings::firstRun, true }; - - bool isTutorialComplete = tutorialComplete.get(); - bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete; - - qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD; - qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent << - ", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial; - - // when --url in command line, teleport to location - const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; - int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); - QString addressLookupString; - if (urlIndex != -1) { - addressLookupString = arguments().value(urlIndex + 1); - } - - const QString TUTORIAL_PATH = "/tutorial_begin"; - - if (shouldGoToTutorial) { - if (sandboxIsRunning) { - qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; - DependencyManager::get()->goToLocalSandbox(TUTORIAL_PATH); - } else { - qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; - if (firstRun.get()) { - showHelp(); - } - if (addressLookupString.isEmpty()) { - DependencyManager::get()->goToEntry(); - } else { - DependencyManager::get()->loadSettings(addressLookupString); - } - } - } else { - - bool isFirstRun = firstRun.get(); - - if (isFirstRun) { - showHelp(); - } - - // If this is a first run we short-circuit the address passed in - if (isFirstRun) { - if (isUsingHMD) { - if (sandboxIsRunning) { - qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; - DependencyManager::get()->goToLocalSandbox(); - } else { - qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; - DependencyManager::get()->goToEntry(); - } - } else { - DependencyManager::get()->goToEntry(); - } - } else { - qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString); - DependencyManager::get()->loadSettings(addressLookupString); - } - } - - _connectionMonitor.init(); - - // After all of the constructor is completed, then set firstRun to false. - firstRun.set(false); + PROFILE_RANGE(render, "GetSandboxStatus"); + auto reply = SandboxUtils::getStatus(); + connect(reply, &QNetworkReply::finished, this, [=] { + handleSandboxStatus(reply); + }); } // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. @@ -1688,7 +1559,6 @@ void Application::updateHeartbeat() const { void Application::aboutToQuit() { emit beforeAboutToQuit(); - DependencyManager::get()->beforeAboutToQuit(); foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { if (inputPlugin->isActive()) { @@ -1789,14 +1659,13 @@ void Application::cleanupBeforeQuit() { _snapshotSoundInjector->stop(); } - // stop audio after QML, as there are unexplained audio crashes originating in qtwebengine - - // stop the AudioClient, synchronously + // FIXME: something else is holding a reference to AudioClient, + // so it must be explicitly synchronously stopped here QMetaObject::invokeMethod(DependencyManager::get().data(), - "stop", Qt::BlockingQueuedConnection); - + "cleanupBeforeQuit", Qt::BlockingQueuedConnection); // destroy Audio so it and its threads have a chance to go down safely + // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine DependencyManager::destroy(); DependencyManager::destroy(); @@ -2053,6 +1922,8 @@ void Application::initializeUi() { rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); + rootContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { rootContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); } @@ -2470,6 +2341,118 @@ void Application::resizeGL() { } } +void Application::handleSandboxStatus(QNetworkReply* reply) { + PROFILE_RANGE(render, "HandleSandboxStatus"); + + bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll()); + qDebug() << "HandleSandboxStatus" << sandboxIsRunning; + + enum HandControllerType { + Vive, + Oculus + }; + static const std::map MIN_CONTENT_VERSION = { + { Vive, 1 }, + { Oculus, 27 } + }; + + // Get sandbox content set version + auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto contentVersionPath = acDirPath + "content-version.txt"; + qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; + int contentVersion = 0; + QFile contentVersionFile(contentVersionPath); + if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString line = contentVersionFile.readAll(); + contentVersion = line.toInt(); // returns 0 if conversion fails + } + + // Get controller availability + bool hasHandControllers = false; + HandControllerType handControllerType = Vive; + if (PluginUtils::isViveControllerAvailable()) { + hasHandControllers = true; + handControllerType = Vive; + } else if (PluginUtils::isOculusTouchControllerAvailable()) { + hasHandControllers = true; + handControllerType = Oculus; + } + + // Check tutorial content versioning + bool hasTutorialContent = contentVersion >= MIN_CONTENT_VERSION.at(handControllerType); + + // Check HMD use (may be technically available without being in use) + bool hasHMD = PluginUtils::isHMDAvailable(); + bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd(); + + Setting::Handle tutorialComplete{ "tutorialComplete", false }; + Setting::Handle firstRun{ Settings::firstRun, true }; + + bool isTutorialComplete = tutorialComplete.get(); + bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete; + + qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD; + qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent << + ", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial; + + // when --url in command line, teleport to location + const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; + int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); + QString addressLookupString; + if (urlIndex != -1) { + addressLookupString = arguments().value(urlIndex + 1); + } + + const QString TUTORIAL_PATH = "/tutorial_begin"; + + if (shouldGoToTutorial) { + if (sandboxIsRunning) { + qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; + DependencyManager::get()->goToLocalSandbox(TUTORIAL_PATH); + } else { + qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; + if (firstRun.get()) { + showHelp(); + } + if (addressLookupString.isEmpty()) { + DependencyManager::get()->goToEntry(); + } else { + DependencyManager::get()->loadSettings(addressLookupString); + } + } + } else { + + bool isFirstRun = firstRun.get(); + + if (isFirstRun) { + showHelp(); + } + + // If this is a first run we short-circuit the address passed in + if (isFirstRun) { + if (isUsingHMD) { + if (sandboxIsRunning) { + qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; + DependencyManager::get()->goToLocalSandbox(); + } else { + qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; + DependencyManager::get()->goToEntry(); + } + } else { + DependencyManager::get()->goToEntry(); + } + } else { + qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString); + DependencyManager::get()->loadSettings(addressLookupString); + } + } + + _connectionMonitor.init(); + + // After all of the constructor is completed, then set firstRun to false. + firstRun.set(false); +} + 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 @@ -4331,13 +4314,6 @@ void Application::update(float deltaTime) { if (nearbyEntitiesAreReadyForPhysics()) { _physicsEnabled = true; getMyAvatar()->updateMotionBehaviorFromMenu(); - } else { - auto characterController = getMyAvatar()->getCharacterController(); - if (characterController) { - // if we have a character controller, disable it here so the avatar doesn't get stuck due to - // a non-loading collision hull. - characterController->setEnabled(false); - } } } } else if (domainLoadingInProgress) { @@ -5282,9 +5258,8 @@ void Application::nodeActivated(SharedNodePointer node) { } if (node->getType() == NodeType::AvatarMixer) { - // new avatar mixer, send off our identity packet right away + // new avatar mixer, send off our identity packet on next update loop getMyAvatar()->markIdentityDataChanged(); - getMyAvatar()->sendIdentityPacket(); getMyAvatar()->resetLastSent(); } } @@ -6453,7 +6428,7 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa if (!includeAnimated) { // Tell the dependency manager that the capture of the still snapshot has taken place. emit DependencyManager::get()->stillSnapshotTaken(path, notify); - } else { + } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { // Get an animated GIF snapshot and save it SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 5027c58349..5cf3580c09 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -112,17 +112,7 @@ class Application : public QApplication, // TODO? Get rid of those friend class OctreePacketProcessor; -private: - bool _shouldRunServer { false }; - QString _runServerPath; - RunningMarker _runningMarker; - public: - // startup related getter/setters - bool shouldRunServer() const { return _shouldRunServer; } - bool hasRunServerPath() const { return !_runServerPath.isEmpty(); } - QString getRunServerPath() const { return _runServerPath; } - // virtual functions required for PluginContainer virtual ui::Menu* getPrimaryMenu() override; virtual void requestReset() override { resetSensors(true); } @@ -146,7 +136,7 @@ public: static void initPlugins(const QStringList& arguments); static void shutdownPlugins(); - Application(int& argc, char** argv, QElapsedTimer& startup_time, bool runServer, QString runServerPathOption); + Application(int& argc, char** argv, QElapsedTimer& startup_time); ~Application(); void postLambdaEvent(std::function f) override; @@ -452,6 +442,8 @@ private slots: void addAssetToWorldInfoTimeout(); void addAssetToWorldErrorTimeout(); + void handleSandboxStatus(QNetworkReply* reply); + private: static void initDisplay(); void init(); diff --git a/interface/src/InterfaceDynamicFactory.cpp b/interface/src/InterfaceDynamicFactory.cpp index 5acc0700c0..e51f63d01b 100644 --- a/interface/src/InterfaceDynamicFactory.cpp +++ b/interface/src/InterfaceDynamicFactory.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -32,7 +32,9 @@ EntityDynamicPointer interfaceDynamicFactory(EntityDynamicType type, const QUuid case DYNAMIC_TYPE_OFFSET: return std::make_shared(id, ownerEntity); case DYNAMIC_TYPE_SPRING: - return std::make_shared(id, ownerEntity); + qDebug() << "The 'spring' Action is deprecated. Replacing with 'tractor' Action."; + case DYNAMIC_TYPE_TRACTOR: + return std::make_shared(id, ownerEntity); case DYNAMIC_TYPE_HOLD: return std::make_shared(id, ownerEntity); case DYNAMIC_TYPE_TRAVEL_ORIENTED: diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9688694287..f5f248602d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -197,7 +197,7 @@ Menu::Menu() { 0, // QML Qt::Key_Apostrophe, qApp, SLOT(resetSensors())); - addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true, + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableAvatarCollisions, 0, true, avatar.get(), SLOT(updateMotionBehaviorFromMenu())); // Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here. @@ -523,6 +523,8 @@ Menu::Menu() { avatar.get(), SLOT(setEnableDebugDrawSensorToWorldMatrix(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderIKTargets, 0, false, avatar.get(), SLOT(setEnableDebugDrawIKTargets(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderIKConstraints, 0, false, + avatar.get(), SLOT(setEnableDebugDrawIKConstraints(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ActionMotorControl, Qt::CTRL | Qt::SHIFT | Qt::Key_K, true, avatar.get(), SLOT(updateMotionBehaviorFromMenu()), diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 250d2241ac..b1f69a28d3 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -96,7 +96,7 @@ namespace MenuOption { const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene"; const QString EchoLocalAudio = "Echo Local Audio"; const QString EchoServerAudio = "Echo Server Audio"; - const QString EnableCharacterController = "Collide with world"; + const QString EnableAvatarCollisions = "Enable Avatar Collisions"; const QString EnableInverseKinematics = "Enable Inverse Kinematics"; const QString EntityScriptServerLog = "Entity Script Server Log"; const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation"; @@ -161,6 +161,7 @@ namespace MenuOption { const QString RenderResolutionQuarter = "1/4"; const QString RenderSensorToWorldMatrix = "Show SensorToWorld Matrix"; const QString RenderIKTargets = "Show IK Targets"; + const QString RenderIKConstraints = "Show IK Constraints"; const QString ResetAvatarSize = "Reset Avatar Size"; const QString ResetSensors = "Reset Sensors"; const QString RunningScripts = "Running Scripts..."; diff --git a/interface/src/avatar/AvatarActionFarGrab.cpp b/interface/src/avatar/AvatarActionFarGrab.cpp index afa21e58d7..1144591d09 100644 --- a/interface/src/avatar/AvatarActionFarGrab.cpp +++ b/interface/src/avatar/AvatarActionFarGrab.cpp @@ -12,7 +12,7 @@ #include "AvatarActionFarGrab.h" AvatarActionFarGrab::AvatarActionFarGrab(const QUuid& id, EntityItemPointer ownerEntity) : - ObjectActionSpring(id, ownerEntity) { + ObjectActionTractor(id, ownerEntity) { _type = DYNAMIC_TYPE_FAR_GRAB; #if WANT_DEBUG qDebug() << "AvatarActionFarGrab::AvatarActionFarGrab"; @@ -32,7 +32,7 @@ QByteArray AvatarActionFarGrab::serialize() const { dataStream << DYNAMIC_TYPE_FAR_GRAB; dataStream << getID(); - dataStream << ObjectActionSpring::springVersion; + dataStream << ObjectActionTractor::tractorVersion; serializeParameters(dataStream); @@ -55,7 +55,7 @@ void AvatarActionFarGrab::deserialize(QByteArray serializedArguments) { uint16_t serializationVersion; dataStream >> serializationVersion; - if (serializationVersion != ObjectActionSpring::springVersion) { + if (serializationVersion != ObjectActionTractor::tractorVersion) { assert(false); return; } diff --git a/interface/src/avatar/AvatarActionFarGrab.h b/interface/src/avatar/AvatarActionFarGrab.h index 46c9f65dcf..bcaf7f2f3c 100644 --- a/interface/src/avatar/AvatarActionFarGrab.h +++ b/interface/src/avatar/AvatarActionFarGrab.h @@ -13,9 +13,9 @@ #define hifi_AvatarActionFarGrab_h #include -#include +#include -class AvatarActionFarGrab : public ObjectActionSpring { +class AvatarActionFarGrab : public ObjectActionTractor { public: AvatarActionFarGrab(const QUuid& id, EntityItemPointer ownerEntity); virtual ~AvatarActionFarGrab(); diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 627dc2ba02..c1d2f903f3 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -21,7 +21,7 @@ const int AvatarActionHold::velocitySmoothFrames = 6; AvatarActionHold::AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntity) : - ObjectActionSpring(id, ownerEntity) + ObjectActionTractor(id, ownerEntity) { _type = DYNAMIC_TYPE_HOLD; _measuredLinearVelocities.resize(AvatarActionHold::velocitySmoothFrames); @@ -224,12 +224,12 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm:: void AvatarActionHold::updateActionWorker(float deltaTimeStep) { if (_kinematic) { - if (prepareForSpringUpdate(deltaTimeStep)) { + if (prepareForTractorUpdate(deltaTimeStep)) { doKinematicUpdate(deltaTimeStep); } } else { forceBodyNonStatic(); - ObjectActionSpring::updateActionWorker(deltaTimeStep); + ObjectActionTractor::updateActionWorker(deltaTimeStep); } } diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index 7eeda53e06..6acc71b45c 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -16,12 +16,12 @@ #include #include -#include +#include #include "avatar/MyAvatar.h" -class AvatarActionHold : public ObjectActionSpring { +class AvatarActionHold : public ObjectActionTractor { public: AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntity); virtual ~AvatarActionHold(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1306ce03ea..d47e4cfd10 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -189,6 +189,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { btCollisionShape* shape = const_cast(ObjectMotionState::getShapeManager()->getShape(shapeInfo)); if (shape) { AvatarMotionState* motionState = new AvatarMotionState(avatar, shape); + motionState->setMass(avatar->computeMass()); avatar->setPhysicsCallback([=] (uint32_t flags) { motionState->addDirtyFlags(flags); }); _motionStates.insert(avatar.get(), motionState); _motionStatesToAddToPhysics.insert(motionState); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 9df1639853..f1e71f7367 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -21,9 +21,9 @@ #include #include #include -#include #include +#include "AvatarMotionState.h" #include "MyAvatar.h" class AudioInjector; diff --git a/libraries/avatars-renderer/src/avatars-renderer/AvatarMotionState.cpp b/interface/src/avatar/AvatarMotionState.cpp similarity index 98% rename from libraries/avatars-renderer/src/avatars-renderer/AvatarMotionState.cpp rename to interface/src/avatar/AvatarMotionState.cpp index 0305634400..91c83afcbd 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/AvatarMotionState.cpp +++ b/interface/src/avatar/AvatarMotionState.cpp @@ -19,9 +19,6 @@ AvatarMotionState::AvatarMotionState(AvatarSharedPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) { assert(_avatar); _type = MOTIONSTATE_TYPE_AVATAR; - if (_shape) { - _mass = 100.0f; // HACK - } } AvatarMotionState::~AvatarMotionState() { diff --git a/libraries/avatars-renderer/src/avatars-renderer/AvatarMotionState.h b/interface/src/avatar/AvatarMotionState.h similarity index 98% rename from libraries/avatars-renderer/src/avatars-renderer/AvatarMotionState.h rename to interface/src/avatar/AvatarMotionState.h index f8801ddf2f..90bd2a60ac 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/AvatarMotionState.h +++ b/interface/src/avatar/AvatarMotionState.h @@ -14,10 +14,10 @@ #include +#include #include #include -#include "Avatar.h" class AvatarMotionState : public ObjectMotionState { public: diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp old mode 100644 new mode 100755 index 3f3ce7d9e9..9cf8e7747b --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,9 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) : _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), + _smoothOrientationTimer(std::numeric_limits::max()), + _smoothOrientationInitial(), + _smoothOrientationTarget(), _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), @@ -150,8 +154,6 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) : // when we leave a domain we lift whatever restrictions that domain may have placed on our scale connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::clearScaleRestriction); - _characterController.setEnabled(true); - _bodySensorMatrix = deriveBodyFromHMDSensor(); using namespace recording; @@ -165,12 +167,14 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) : if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } - _wasCharacterControllerEnabled = _characterController.isEnabled(); - _characterController.setEnabled(false); + _previousCollisionGroup = _characterController.computeCollisionGroup(); + _characterController.setCollisionless(true); } else { clearRecordingBasis(); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); - _characterController.setEnabled(_wasCharacterControllerEnabled); + if (_previousCollisionGroup != BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.setCollisionless(false); + } } auto audioIO = DependencyManager::get(); @@ -235,6 +239,7 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) : }); connect(rig.get(), SIGNAL(onLoadComplete()), this, SIGNAL(onLoadComplete())); + _characterController.setDensity(_density); } MyAvatar::~MyAvatar() { @@ -264,6 +269,17 @@ QVariant MyAvatar::getOrientationVar() const { return quatToVariant(Avatar::getOrientation()); } +glm::quat MyAvatar::getOrientationOutbound() const { + // Allows MyAvatar to send out smoothed data to remote agents if required. + if (_smoothOrientationTimer > SMOOTH_TIME_ORIENTATION) { + return (getLocalOrientation()); + } + + // Smooth the remote avatar movement. + float t = _smoothOrientationTimer / SMOOTH_TIME_ORIENTATION; + float interp = Interpolate::easeInOutQuad(glm::clamp(t, 0.0f, 1.0f)); + return (slerp(_smoothOrientationInitial, _smoothOrientationTarget, interp)); +} // virtual void MyAvatar::simulateAttachments(float deltaTime) { @@ -290,6 +306,11 @@ QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { } void MyAvatar::resetSensorsAndBody() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "resetSensorsAndBody"); + return; + } + qApp->getActiveDisplayPlugin()->resetSensors(); reset(true, false, true); } @@ -387,6 +408,11 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; _hmdSensorFacingMovingAverage = lerp(_hmdSensorFacingMovingAverage, _hmdSensorFacing, tau); + if (_smoothOrientationTimer < SMOOTH_TIME_ORIENTATION) { + _rotationChanged = usecTimestampNow(); + _smoothOrientationTimer += deltaTime; + } + #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE glm::vec3 p = transformPoint(getSensorToWorldMatrix(), _hmdSensorPosition + glm::vec3(_hmdSensorFacingMovingAverage.x, 0.0f, _hmdSensorFacingMovingAverage.y)); DebugDraw::getInstance().addMarker("facing-avg", getOrientation(), p, glm::vec4(1.0f)); @@ -504,6 +530,7 @@ void MyAvatar::simulate(float deltaTime) { if (_rig) { _rig->setEnableDebugDrawIKTargets(_enableDebugDrawIKTargets); + _rig->setEnableDebugDrawIKConstraints(_enableDebugDrawIKConstraints); } _skeletonModel->simulate(deltaTime); @@ -552,12 +579,12 @@ void MyAvatar::simulate(float deltaTime) { EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { bool flyingAllowed = true; - bool ghostingAllowed = true; + bool collisionlessAllowed = true; entityTree->withWriteLock([&] { std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); if (zone) { flyingAllowed = zone->getFlyingAllowed(); - ghostingAllowed = zone->getGhostingAllowed(); + collisionlessAllowed = zone->getGhostingAllowed(); } auto now = usecTimestampNow(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); @@ -588,9 +615,7 @@ void MyAvatar::simulate(float deltaTime) { } }); _characterController.setFlyingAllowed(flyingAllowed); - if (!_characterController.isEnabled() && !ghostingAllowed) { - _characterController.setEnabled(true); - } + _characterController.setCollisionlessAllowed(collisionlessAllowed); } updateAvatarEntities(); @@ -929,6 +954,10 @@ void MyAvatar::setEnableDebugDrawIKTargets(bool isEnabled) { _enableDebugDrawIKTargets = isEnabled; } +void MyAvatar::setEnableDebugDrawIKConstraints(bool isEnabled) { + _enableDebugDrawIKConstraints = isEnabled; +} + void MyAvatar::setEnableMeshVisible(bool isEnabled) { _skeletonModel->setVisibleInScene(isEnabled, qApp->getMain3DScene()); } @@ -1449,7 +1478,8 @@ void MyAvatar::updateMotors() { _characterController.clearMotors(); glm::quat motorRotation; if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { - if (_characterController.getState() == CharacterController::State::Hover) { + if (_characterController.getState() == CharacterController::State::Hover || + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { motorRotation = getMyHead()->getCameraOrientation(); } else { // non-hovering = walking: follow camera twist about vertical but not lift @@ -1495,6 +1525,7 @@ void MyAvatar::prepareForPhysicsSimulation() { qDebug() << "Warning: getParentVelocity failed" << getID(); parentVelocity = glm::vec3(); } + _characterController.handleChangedCollisionGroup(); _characterController.setParentVelocity(parentVelocity); _characterController.setPositionAndOrientation(getPosition(), getOrientation()); @@ -1806,8 +1837,10 @@ void MyAvatar::updateOrientation(float deltaTime) { // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. + bool snapTurn = false; if (getDriveKey(STEP_YAW) != 0.0f) { totalBodyYaw += getDriveKey(STEP_YAW); + snapTurn = true; } // use head/HMD orientation to turn while flying @@ -1840,10 +1873,17 @@ void MyAvatar::updateOrientation(float deltaTime) { totalBodyYaw += (speedFactor * deltaAngle * (180.0f / PI)); } - // update body orientation by movement inputs + glm::quat initialOrientation = getOrientationOutbound(); setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); + if (snapTurn) { + // Whether or not there is an existing smoothing going on, just reset the smoothing timer and set the starting position as the avatar's current position, then smooth to the new position. + _smoothOrientationInitial = initialOrientation; + _smoothOrientationTarget = getOrientation(); + _smoothOrientationTimer = 0.0f; + } + getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime); if (qApp->isHMDMode()) { @@ -1883,8 +1923,9 @@ void MyAvatar::updateActionMotor(float deltaTime) { glm::vec3 direction = forward + right; CharacterController::State state = _characterController.getState(); - if (state == CharacterController::State::Hover) { - // we're flying --> support vertical motion + if (state == CharacterController::State::Hover || + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + // we can fly --> support vertical motion glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; direction += up; } @@ -1906,7 +1947,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { float finalMaxMotorSpeed = getUniformScale() * MAX_ACTION_MOTOR_SPEED; float speedGrowthTimescale = 2.0f; float speedIncreaseFactor = 1.8f; - motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale , 0.0f, 1.0f) * speedIncreaseFactor; + motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor; const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED; if (_isPushing) { @@ -1949,9 +1990,17 @@ void MyAvatar::updatePosition(float deltaTime) { measureMotionDerivatives(deltaTime); _moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED; } else { - // physics physics simulation updated elsewhere float speed2 = glm::length2(velocity); _moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED; + + if (_moving) { + // scan for walkability + glm::vec3 position = getPosition(); + MyCharacterController::RayShotgunResult result; + glm::vec3 step = deltaTime * (getRotation() * _actionMotorVelocity); + _characterController.testRayShotgun(position, step, result); + _characterController.setStepUpEnabled(result.walkable); + } } // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. @@ -2188,30 +2237,33 @@ void MyAvatar::updateMotionBehaviorFromMenu() { } else { _motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; } - - setCharacterControllerEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController)); + setCollisionsEnabled(menu->isOptionChecked(MenuOption::EnableAvatarCollisions)); } -void MyAvatar::setCharacterControllerEnabled(bool enabled) { +void MyAvatar::setCollisionsEnabled(bool enabled) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setCharacterControllerEnabled", Q_ARG(bool, enabled)); + QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled)); return; } - bool ghostingAllowed = true; - auto entityTreeRenderer = qApp->getEntities(); - if (entityTreeRenderer) { - std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); - if (zone) { - ghostingAllowed = zone->getGhostingAllowed(); - } - } - _characterController.setEnabled(ghostingAllowed ? enabled : true); + _characterController.setCollisionless(!enabled); +} + +bool MyAvatar::getCollisionsEnabled() { + // may return 'false' even though the collisionless option was requested + // because the zone may disallow collisionless avatars + return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS; +} + +void MyAvatar::setCharacterControllerEnabled(bool enabled) { + qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead."; + setCollisionsEnabled(enabled); } bool MyAvatar::getCharacterControllerEnabled() { - return _characterController.isEnabled(); + qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead."; + return getCollisionsEnabled(); } void MyAvatar::clearDriveKeys() { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7c510f0556..394d6b2ac7 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -96,7 +96,7 @@ class MyAvatar : public Avatar { * @property rightHandTipPose {Pose} READ-ONLY. Returns a pose offset 30 cm from MyAvatar.rightHandPose * @property hmdLeanRecenterEnabled {bool} This can be used disable the hmd lean recenter behavior. This behavior is what causes your avatar * to follow your HMD as you walk around the room, in room scale VR. Disabling this is useful if you desire to pin the avatar to a fixed location. - * @property characterControllerEnabled {bool} This can be used to disable collisions between the avatar and the world. + * @property collisionsEnabled {bool} This can be used to disable collisions between the avatar and the world. * @property useAdvancedMovementControls {bool} Stores the user preference only, does not change user mappings, this is done in the defaultScript * "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js". */ @@ -125,9 +125,10 @@ class MyAvatar : public Avatar { Q_PROPERTY(controller::Pose rightHandTipPose READ getRightHandTipPose) Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) - Q_PROPERTY(float isAway READ getIsAway WRITE setAway) + Q_PROPERTY(bool isAway READ getIsAway WRITE setAway) Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) + Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) @@ -189,6 +190,8 @@ public: Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); Q_INVOKABLE QVariant getOrientationVar() const; + // A method intended to be overriden by MyAvatar for polling orientation for network transmission. + glm::quat getOrientationOutbound() const override; // Pass a recent sample of the HMD to the avatar. // This can also update the avatar's position to follow the HMD @@ -470,8 +473,10 @@ public: bool hasDriveInput() const; - Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); - Q_INVOKABLE bool getCharacterControllerEnabled(); + Q_INVOKABLE void setCollisionsEnabled(bool enabled); + Q_INVOKABLE bool getCollisionsEnabled(); + Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated + Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; @@ -518,6 +523,7 @@ public slots: void setEnableDebugDrawHandControllers(bool isEnabled); void setEnableDebugDrawSensorToWorldMatrix(bool isEnabled); void setEnableDebugDrawIKTargets(bool isEnabled); + void setEnableDebugDrawIKConstraints(bool isEnabled); bool getEnableMeshVisible() const { return _skeletonModel->isVisible(); } void setEnableMeshVisible(bool isEnabled); void setUseAnimPreAndPostRotations(bool isEnabled); @@ -614,7 +620,7 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; - bool _wasCharacterControllerEnabled { true }; + int16_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -629,6 +635,14 @@ private: Setting::Handle _realWorldFieldOfView; Setting::Handle _useAdvancedMovementControls; + // Smoothing. + const float SMOOTH_TIME_ORIENTATION = 0.5f; + + // Smoothing data for blending from one position/orientation to another on remote agents. + float _smoothOrientationTimer; + glm::quat _smoothOrientationInitial; + glm::quat _smoothOrientationTarget; + // private methods void updateOrientation(float deltaTime); void updateActionMotor(float deltaTime); @@ -703,6 +717,7 @@ private: bool _enableDebugDrawHandControllers { false }; bool _enableDebugDrawSensorToWorldMatrix { false }; bool _enableDebugDrawIKTargets { false }; + bool _enableDebugDrawIKConstraints { false }; AudioListenerMode _audioListenerMode; glm::vec3 _customListenPosition; diff --git a/interface/src/avatar/MyCharacterController.cpp b/interface/src/avatar/MyCharacterController.cpp old mode 100644 new mode 100755 index 6e52f4a949..3d98a0e604 --- a/interface/src/avatar/MyCharacterController.cpp +++ b/interface/src/avatar/MyCharacterController.cpp @@ -15,11 +15,15 @@ #include "MyAvatar.h" -// TODO: improve walking up steps -// TODO: make avatars able to walk up and down steps/slopes // TODO: make avatars stand on steep slope // TODO: make avatars not snag on low ceilings + +void MyCharacterController::RayShotgunResult::reset() { + hitFraction = 1.0f; + walkable = true; +} + MyCharacterController::MyCharacterController(MyAvatar* avatar) { assert(avatar); @@ -30,39 +34,33 @@ MyCharacterController::MyCharacterController(MyAvatar* avatar) { MyCharacterController::~MyCharacterController() { } +void MyCharacterController::setDynamicsWorld(btDynamicsWorld* world) { + CharacterController::setDynamicsWorld(world); + if (world) { + initRayShotgun(world); + } +} + void MyCharacterController::updateShapeIfNecessary() { if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) { _pendingFlags &= ~PENDING_FLAG_UPDATE_SHAPE; - // compute new dimensions from avatar's bounding box - float x = _boxScale.x; - float z = _boxScale.z; - _radius = 0.5f * sqrtf(0.5f * (x * x + z * z)); - _halfHeight = 0.5f * _boxScale.y - _radius; - float MIN_HALF_HEIGHT = 0.1f; - if (_halfHeight < MIN_HALF_HEIGHT) { - _halfHeight = MIN_HALF_HEIGHT; - } - // NOTE: _shapeLocalOffset is already computed - if (_radius > 0.0f) { // create RigidBody if it doesn't exist if (!_rigidBody) { - - // HACK: use some simple mass property defaults for now - const float DEFAULT_AVATAR_MASS = 100.0f; - const btVector3 DEFAULT_AVATAR_INERTIA_TENSOR(30.0f, 8.0f, 30.0f); - - btCollisionShape* shape = new btCapsuleShape(_radius, 2.0f * _halfHeight); - _rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR); + btCollisionShape* shape = computeShape(); + btScalar mass = 1.0f; + btVector3 inertia(1.0f, 1.0f, 1.0f); + _rigidBody = new btRigidBody(mass, nullptr, shape, inertia); } else { btCollisionShape* shape = _rigidBody->getCollisionShape(); if (shape) { delete shape; } - shape = new btCapsuleShape(_radius, 2.0f * _halfHeight); + shape = computeShape(); _rigidBody->setCollisionShape(shape); } + updateMassProperties(); _rigidBody->setSleepingThresholds(0.0f, 0.0f); _rigidBody->setAngularFactor(0.0f); @@ -72,12 +70,282 @@ void MyCharacterController::updateShapeIfNecessary() { if (_state == State::Hover) { _rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f)); } else { - _rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp); + _rigidBody->setGravity(_gravity * _currentUp); } - //_rigidBody->setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT); + _rigidBody->setCollisionFlags(_rigidBody->getCollisionFlags() & + ~(btCollisionObject::CF_KINEMATIC_OBJECT | btCollisionObject::CF_STATIC_OBJECT)); } else { // TODO: handle this failure case } } } +bool MyCharacterController::testRayShotgun(const glm::vec3& position, const glm::vec3& step, RayShotgunResult& result) { + btVector3 rayDirection = glmToBullet(step); + btScalar stepLength = rayDirection.length(); + if (stepLength < FLT_EPSILON) { + return false; + } + rayDirection /= stepLength; + + // get _ghost ready for ray traces + btTransform transform = _rigidBody->getWorldTransform(); + btVector3 newPosition = glmToBullet(position); + transform.setOrigin(newPosition); + _ghost.setWorldTransform(transform); + btMatrix3x3 rotation = transform.getBasis(); + _ghost.refreshOverlappingPairCache(); + + CharacterRayResult rayResult(&_ghost); + CharacterRayResult closestRayResult(&_ghost); + btVector3 rayStart; + btVector3 rayEnd; + + // compute rotation that will orient local ray start points to face step direction + btVector3 forward = rotation * btVector3(0.0f, 0.0f, -1.0f); + btVector3 adjustedDirection = rayDirection - rayDirection.dot(_currentUp) * _currentUp; + btVector3 axis = forward.cross(adjustedDirection); + btScalar lengthAxis = axis.length(); + if (lengthAxis > FLT_EPSILON) { + // we're walking sideways + btScalar angle = acosf(lengthAxis / adjustedDirection.length()); + if (rayDirection.dot(forward) < 0.0f) { + angle = PI - angle; + } + axis /= lengthAxis; + rotation = btMatrix3x3(btQuaternion(axis, angle)) * rotation; + } else if (rayDirection.dot(forward) < 0.0f) { + // we're walking backwards + rotation = btMatrix3x3(btQuaternion(_currentUp, PI)) * rotation; + } + + // scan the top + // NOTE: if we scan an extra distance forward we can detect flat surfaces that are too steep to walk on. + // The approximate extra distance can be derived with trigonometry. + // + // minimumForward = [ (maxStepHeight + radius / cosTheta - radius) * (cosTheta / sinTheta) - radius ] + // + // where: theta = max angle between floor normal and vertical + // + // if stepLength is not long enough we can add the difference. + // + btScalar cosTheta = _minFloorNormalDotUp; + btScalar sinTheta = sqrtf(1.0f - cosTheta * cosTheta); + const btScalar MIN_FORWARD_SLOP = 0.12f; // HACK: not sure why this is necessary to detect steepest walkable slope + btScalar forwardSlop = (_maxStepHeight + _radius / cosTheta - _radius) * (cosTheta / sinTheta) - (_radius + stepLength) + MIN_FORWARD_SLOP; + if (forwardSlop < 0.0f) { + // BIG step, no slop necessary + forwardSlop = 0.0f; + } + + const btScalar backSlop = 0.04f; + for (int32_t i = 0; i < _topPoints.size(); ++i) { + rayStart = newPosition + rotation * _topPoints[i] - backSlop * rayDirection; + rayEnd = rayStart + (backSlop + stepLength + forwardSlop) * rayDirection; + if (_ghost.rayTest(rayStart, rayEnd, rayResult)) { + if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) { + closestRayResult = rayResult; + } + if (result.walkable) { + if (rayResult.m_hitNormalWorld.dot(_currentUp) < _minFloorNormalDotUp) { + result.walkable = false; + // the top scan wasn't walkable so don't bother scanning the bottom + // remove both forwardSlop and backSlop + result.hitFraction = glm::min(1.0f, (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop) - backSlop) / stepLength); + return result.hitFraction < 1.0f; + } + } + } + } + if (_state == State::Hover) { + // scan the bottom just like the top + for (int32_t i = 0; i < _bottomPoints.size(); ++i) { + rayStart = newPosition + rotation * _bottomPoints[i] - backSlop * rayDirection; + rayEnd = rayStart + (backSlop + stepLength + forwardSlop) * rayDirection; + if (_ghost.rayTest(rayStart, rayEnd, rayResult)) { + if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) { + closestRayResult = rayResult; + } + if (result.walkable) { + if (rayResult.m_hitNormalWorld.dot(_currentUp) < _minFloorNormalDotUp) { + result.walkable = false; + // the bottom scan wasn't walkable + // remove both forwardSlop and backSlop + result.hitFraction = glm::min(1.0f, (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop) - backSlop) / stepLength); + return result.hitFraction < 1.0f; + } + } + } + } + } else { + // scan the bottom looking for nearest step point + // remove forwardSlop + result.hitFraction = (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop)) / (backSlop + stepLength); + + for (int32_t i = 0; i < _bottomPoints.size(); ++i) { + rayStart = newPosition + rotation * _bottomPoints[i] - backSlop * rayDirection; + rayEnd = rayStart + (backSlop + stepLength) * rayDirection; + if (_ghost.rayTest(rayStart, rayEnd, rayResult)) { + if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) { + closestRayResult = rayResult; + } + } + } + // remove backSlop + // NOTE: backSlop removal can produce a NEGATIVE hitFraction! + // which means the shape is actually in interpenetration + result.hitFraction = ((closestRayResult.m_closestHitFraction * (backSlop + stepLength)) - backSlop) / stepLength; + } + return result.hitFraction < 1.0f; +} + +btConvexHullShape* MyCharacterController::computeShape() const { + // HACK: the avatar collides using convex hull with a collision margin equal to + // the old capsule radius. Two points define a capsule and additional points are + // spread out at chest level to produce a slight taper toward the feet. This + // makes the avatar more likely to collide with vertical walls at a higher point + // and thus less likely to produce a single-point collision manifold below the + // _maxStepHeight when walking into against vertical surfaces --> fixes a bug + // where the "walk up steps" feature would allow the avatar to walk up vertical + // walls. + const int32_t NUM_POINTS = 6; + btVector3 points[NUM_POINTS]; + btVector3 xAxis = btVector3(1.0f, 0.0f, 0.0f); + btVector3 yAxis = btVector3(0.0f, 1.0f, 0.0f); + btVector3 zAxis = btVector3(0.0f, 0.0f, 1.0f); + points[0] = _halfHeight * yAxis; + points[1] = -_halfHeight * yAxis; + points[2] = (0.75f * _halfHeight) * yAxis - (0.1f * _radius) * zAxis; + points[3] = (0.75f * _halfHeight) * yAxis + (0.1f * _radius) * zAxis; + points[4] = (0.75f * _halfHeight) * yAxis - (0.1f * _radius) * xAxis; + points[5] = (0.75f * _halfHeight) * yAxis + (0.1f * _radius) * xAxis; + btConvexHullShape* shape = new btConvexHullShape(reinterpret_cast(points), NUM_POINTS); + shape->setMargin(_radius); + return shape; +} + +void MyCharacterController::initRayShotgun(const btCollisionWorld* world) { + // In order to trace rays out from the avatar's shape surface we need to know where the start points are in + // the local-frame. Since the avatar shape is somewhat irregular computing these points by hand is a hassle + // so instead we ray-trace backwards to the avatar to find them. + // + // We trace back a regular grid (see below) of points against the shape and keep any that hit. + // ___ + // + / + \ + + // |+ +| + // +| + | + + // |+ +| + // +| + | + + // |+ +| + // + \ + / + + // --- + // The shotgun will send rays out from these same points to see if the avatar's shape can proceed through space. + + // helper class for simple ray-traces against character + class MeOnlyResultCallback : public btCollisionWorld::ClosestRayResultCallback { + public: + MeOnlyResultCallback (btRigidBody* me) : btCollisionWorld::ClosestRayResultCallback(btVector3(0.0f, 0.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)) { + _me = me; + m_collisionFilterGroup = BULLET_COLLISION_GROUP_DYNAMIC; + m_collisionFilterMask = BULLET_COLLISION_MASK_DYNAMIC; + } + virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult,bool normalInWorldSpace) override { + if (rayResult.m_collisionObject != _me) { + return 1.0f; + } + return ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); + } + btRigidBody* _me; + }; + + const btScalar fullHalfHeight = _radius + _halfHeight; + const btScalar divisionLine = -fullHalfHeight + _maxStepHeight; // line between top and bottom + const btScalar topHeight = fullHalfHeight - divisionLine; + const btScalar slop = 0.02f; + + const int32_t NUM_ROWS = 5; // must be odd number > 1 + const int32_t NUM_COLUMNS = 5; // must be odd number > 1 + btVector3 reach = (2.0f * _radius) * btVector3(0.0f, 0.0f, 1.0f); + + { // top points + _topPoints.clear(); + _topPoints.reserve(NUM_ROWS * NUM_COLUMNS); + btScalar stepY = (topHeight - slop) / (btScalar)(NUM_ROWS - 1); + btScalar stepX = 2.0f * (_radius - slop) / (btScalar)(NUM_COLUMNS - 1); + + btTransform transform = _rigidBody->getWorldTransform(); + btVector3 position = transform.getOrigin(); + btMatrix3x3 rotation = transform.getBasis(); + + for (int32_t i = 0; i < NUM_ROWS; ++i) { + int32_t maxJ = NUM_COLUMNS; + btScalar offsetX = -(btScalar)((NUM_COLUMNS - 1) / 2) * stepX; + if (i % 2 == 1) { + // odd rows have one less point and start a halfStep closer + maxJ -= 1; + offsetX += 0.5f * stepX; + } + for (int32_t j = 0; j < maxJ; ++j) { + btVector3 localRayEnd(offsetX + (btScalar)(j) * stepX, divisionLine + (btScalar)(i) * stepY, 0.0f); + btVector3 localRayStart = localRayEnd - reach; + MeOnlyResultCallback result(_rigidBody); + world->rayTest(position + rotation * localRayStart, position + rotation * localRayEnd, result); + if (result.m_closestHitFraction < 1.0f) { + _topPoints.push_back(localRayStart + result.m_closestHitFraction * reach); + } + } + } + } + + { // bottom points + _bottomPoints.clear(); + _bottomPoints.reserve(NUM_ROWS * NUM_COLUMNS); + + btScalar steepestStepHitHeight = (_radius + 0.04f) * (1.0f - DEFAULT_MIN_FLOOR_NORMAL_DOT_UP); + btScalar stepY = (_maxStepHeight - slop - steepestStepHitHeight) / (btScalar)(NUM_ROWS - 1); + btScalar stepX = 2.0f * (_radius - slop) / (btScalar)(NUM_COLUMNS - 1); + + btTransform transform = _rigidBody->getWorldTransform(); + btVector3 position = transform.getOrigin(); + btMatrix3x3 rotation = transform.getBasis(); + + for (int32_t i = 0; i < NUM_ROWS; ++i) { + int32_t maxJ = NUM_COLUMNS; + btScalar offsetX = -(btScalar)((NUM_COLUMNS - 1) / 2) * stepX; + if (i % 2 == 1) { + // odd rows have one less point and start a halfStep closer + maxJ -= 1; + offsetX += 0.5f * stepX; + } + for (int32_t j = 0; j < maxJ; ++j) { + btVector3 localRayEnd(offsetX + (btScalar)(j) * stepX, (divisionLine - slop) - (btScalar)(i) * stepY, 0.0f); + btVector3 localRayStart = localRayEnd - reach; + MeOnlyResultCallback result(_rigidBody); + world->rayTest(position + rotation * localRayStart, position + rotation * localRayEnd, result); + if (result.m_closestHitFraction < 1.0f) { + _bottomPoints.push_back(localRayStart + result.m_closestHitFraction * reach); + } + } + } + } +} + +void MyCharacterController::updateMassProperties() { + assert(_rigidBody); + // the inertia tensor of a capsule with Y-axis of symmetry, radius R and cylinder height H is: + // Ix = density * (volumeCylinder * (H^2 / 12 + R^2 / 4) + volumeSphere * (2R^2 / 5 + H^2 / 2 + 3HR / 8)) + // Iy = density * (volumeCylinder * (R^2 / 2) + volumeSphere * (2R^2 / 5) + btScalar r2 = _radius * _radius; + btScalar h2 = 4.0f * _halfHeight * _halfHeight; + btScalar volumeSphere = 4.0f * PI * r2 * _radius / 3.0f; + btScalar volumeCylinder = TWO_PI * r2 * 2.0f * _halfHeight; + btScalar cylinderXZ = volumeCylinder * (h2 / 12.0f + r2 / 4.0f); + btScalar capsXZ = volumeSphere * (2.0f * r2 / 5.0f + h2 / 2.0f + 6.0f * _halfHeight * _radius / 8.0f); + btScalar inertiaXZ = _density * (cylinderXZ + capsXZ); + btScalar inertiaY = _density * ((volumeCylinder * r2 / 2.0f) + volumeSphere * (2.0f * r2 / 5.0f)); + btVector3 inertia(inertiaXZ, inertiaY, inertiaXZ); + + btScalar mass = _density * (volumeCylinder + volumeSphere); + + _rigidBody->setMassProps(mass, inertia); +} diff --git a/interface/src/avatar/MyCharacterController.h b/interface/src/avatar/MyCharacterController.h index 265406bc6f..fd9caface2 100644 --- a/interface/src/avatar/MyCharacterController.h +++ b/interface/src/avatar/MyCharacterController.h @@ -24,10 +24,38 @@ public: explicit MyCharacterController(MyAvatar* avatar); ~MyCharacterController (); - virtual void updateShapeIfNecessary() override; + void setDynamicsWorld(btDynamicsWorld* world) override; + void updateShapeIfNecessary() override; + + // Sweeping a convex shape through the physics simulation can be expensive when the obstacles are too + // complex (e.g. small 20k triangle static mesh) so instead we cast several rays forward and if they + // don't hit anything we consider it a clean sweep. Hence this "Shotgun" code. + class RayShotgunResult { + public: + void reset(); + float hitFraction { 1.0f }; + bool walkable { true }; + }; + + /// return true if RayShotgun hits anything + bool testRayShotgun(const glm::vec3& position, const glm::vec3& step, RayShotgunResult& result); + + void setDensity(btScalar density) { _density = density; } + +protected: + void initRayShotgun(const btCollisionWorld* world); + void updateMassProperties() override; + +private: + btConvexHullShape* computeShape() const; protected: MyAvatar* _avatar { nullptr }; + + // shotgun scan data + btAlignedObjectArray _topPoints; + btAlignedObjectArray _bottomPoints; + btScalar _density { 1.0f }; }; #endif // hifi_MyCharacterController_h diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index c41fff3bb5..793fbb79c4 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -44,14 +44,17 @@ glm::quat MyHead::getCameraOrientation() const { void MyHead::simulate(float deltaTime) { auto player = DependencyManager::get(); // Only use face trackers when not playing back a recording. - if (!player->isPlaying()) { + if (player->isPlaying()) { + Parent::simulate(deltaTime); + } else { + computeAudioLoudness(deltaTime); + FaceTracker* faceTracker = qApp->getActiveFaceTracker(); - _isFaceTrackerConnected = faceTracker != NULL && !faceTracker->isMuted(); + _isFaceTrackerConnected = faceTracker && !faceTracker->isMuted(); if (_isFaceTrackerConnected) { - _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); + _transientBlendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); if (typeid(*faceTracker) == typeid(DdeFaceTracker)) { - if (Menu::getInstance()->isOptionChecked(MenuOption::UseAudioForMouth)) { calculateMouthShapes(deltaTime); @@ -60,17 +63,27 @@ void MyHead::simulate(float deltaTime) { const int FUNNEL_BLENDSHAPE = 40; const int SMILE_LEFT_BLENDSHAPE = 28; const int SMILE_RIGHT_BLENDSHAPE = 29; - _blendshapeCoefficients[JAW_OPEN_BLENDSHAPE] += _audioJawOpen; - _blendshapeCoefficients[SMILE_LEFT_BLENDSHAPE] += _mouth4; - _blendshapeCoefficients[SMILE_RIGHT_BLENDSHAPE] += _mouth4; - _blendshapeCoefficients[MMMM_BLENDSHAPE] += _mouth2; - _blendshapeCoefficients[FUNNEL_BLENDSHAPE] += _mouth3; + _transientBlendshapeCoefficients[JAW_OPEN_BLENDSHAPE] += _audioJawOpen; + _transientBlendshapeCoefficients[SMILE_LEFT_BLENDSHAPE] += _mouth4; + _transientBlendshapeCoefficients[SMILE_RIGHT_BLENDSHAPE] += _mouth4; + _transientBlendshapeCoefficients[MMMM_BLENDSHAPE] += _mouth2; + _transientBlendshapeCoefficients[FUNNEL_BLENDSHAPE] += _mouth3; } applyEyelidOffset(getFinalOrientationInWorldFrame()); } - } + } else { + computeFaceMovement(deltaTime); + } + auto eyeTracker = DependencyManager::get(); - _isEyeTrackerConnected = eyeTracker->isTracking(); + _isEyeTrackerConnected = eyeTracker && eyeTracker->isTracking(); + if (_isEyeTrackerConnected) { + // TODO? figure out where EyeTracker data harvested. Move it here? + _saccade = glm::vec3(); + } else { + computeEyeMovement(deltaTime); + } + } - Parent::simulate(deltaTime); -} \ No newline at end of file + computeEyePosition(); +} diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 1b9aa4dc18..e60481fc62 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -37,7 +37,14 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Head* head = _owningAvatar->getHead(); // make sure lookAt is not too close to face (avoid crosseyes) - glm::vec3 lookAt = _owningAvatar->isMyAvatar() ? head->getLookAtPosition() : head->getCorrectedLookAtPosition(); + glm::vec3 lookAt = head->getLookAtPosition(); + glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); + float focusDistance = glm::length(focusOffset); + const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; + if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) { + lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; + } + MyAvatar* myAvatar = static_cast(_owningAvatar); Rig::HeadParameters headParams; @@ -140,6 +147,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { auto orientation = myAvatar->getLocalOrientation(); _rig->computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState); + // evaluate AnimGraph animation and update jointStates. + Model::updateRig(deltaTime, parentTransform); + Rig::EyeParameters eyeParams; eyeParams.eyeLookAt = lookAt; eyeParams.eyeSaccade = head->getSaccade(); @@ -149,8 +159,5 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex; _rig->updateFromEyeParameters(eyeParams); - - // evaluate AnimGraph animation and update jointStates. - Parent::updateRig(deltaTime, parentTransform); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 99dbb1a28e..3430ffbd15 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include +#include #include @@ -28,7 +30,6 @@ #include "InterfaceLogging.h" #include "UserActivityLogger.h" #include "MainWindow.h" -#include #ifdef HAS_BUGSPLAT #include @@ -50,50 +51,49 @@ int main(int argc, const char* argv[]) { disableQtBearerPoll(); // Fixes wifi ping spikes + QElapsedTimer startupTime; + startupTime.start(); + // Set application infos QCoreApplication::setApplicationName(BuildInfo::INTERFACE_NAME); QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - const QString& applicationName = getInterfaceSharedMemoryName(); - - 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 - // for crashed instances to leave behind shared memory instances on unix. - QSharedMemory sharedMemory { applicationName }; - instanceMightBeRunning = !sharedMemory.create(1, QSharedMemory::ReadOnly); -#endif - - // allow multiple interfaces to run if this environment variable is set. - if (QProcessEnvironment::systemEnvironment().contains("HIFI_ALLOW_MULTIPLE_INSTANCES")) { - instanceMightBeRunning = false; - } - QCommandLineParser parser; + QCommandLineOption urlOption("url", "", "value"); + QCommandLineOption noUpdaterOption("no-updater", "Do not show auto-updater"); QCommandLineOption checkMinSpecOption("checkMinSpec", "Check if machine meets minimum specifications"); QCommandLineOption runServerOption("runServer", "Whether to run the server"); QCommandLineOption serverContentPathOption("serverContentPath", "Where to find server content", "serverContentPath"); QCommandLineOption allowMultipleInstancesOption("allowMultipleInstances", "Allow multiple instances to run"); + parser.addOption(urlOption); + parser.addOption(noUpdaterOption); parser.addOption(checkMinSpecOption); parser.addOption(runServerOption); parser.addOption(serverContentPathOption); parser.addOption(allowMultipleInstancesOption); parser.parse(arguments); - bool runServer = parser.isSet(runServerOption); - bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption); - QString serverContentPathOptionValue = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString(); - bool allowMultipleInstances = parser.isSet(allowMultipleInstancesOption); + + const QString& applicationName = getInterfaceSharedMemoryName(); + bool instanceMightBeRunning = true; +#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 + // for crashed instances to leave behind shared memory instances on unix. + QSharedMemory sharedMemory{ applicationName }; + instanceMightBeRunning = !sharedMemory.create(1, QSharedMemory::ReadOnly); +#endif + + // allow multiple interfaces to run if this environment variable is set. + bool allowMultipleInstances = parser.isSet(allowMultipleInstancesOption) || + QProcessEnvironment::systemEnvironment().contains("HIFI_ALLOW_MULTIPLE_INSTANCES"); if (allowMultipleInstances) { instanceMightBeRunning = false; } @@ -108,11 +108,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)) { - QCommandLineParser parser; - QCommandLineOption urlOption("url", "", "value"); - parser.addOption(urlOption); - parser.process(arguments); - if (parser.isSet(urlOption)) { QUrl url = QUrl(parser.value(urlOption)); if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) { @@ -156,9 +151,6 @@ int main(int argc, const char* argv[]) { } } - QElapsedTimer startupTime; - startupTime.start(); - // Debug option to demonstrate that the client's local time does not // need to be in sync with any other network node. This forces clock // skew for the individual client @@ -199,7 +191,21 @@ int main(int argc, const char* argv[]) { int exitCode; { - Application app(argc, const_cast(argv), startupTime, runServer, serverContentPathOptionValue); + RunningMarker runningMarker(nullptr, RUNNING_MARKER_FILENAME); + runningMarker.writeRunningMarkerFile(); + + bool noUpdater = parser.isSet(noUpdaterOption); + bool runServer = parser.isSet(runServerOption); + bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption); + QString serverContentPath = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString(); + if (runServer) { + SandboxUtils::runLocalSandbox(serverContentPath, true, RUNNING_MARKER_FILENAME, noUpdater); + } + + Application app(argc, const_cast(argv), startupTime); + + // Now that the main event loop is setup, launch running marker thread + runningMarker.startRunningMarker(); // If we failed the OpenGLVersion check, log it. if (override) { diff --git a/interface/src/scripting/AudioDeviceScriptingInterface.cpp b/interface/src/scripting/AudioDeviceScriptingInterface.cpp index cbb08c0af0..05168b0d4c 100644 --- a/interface/src/scripting/AudioDeviceScriptingInterface.cpp +++ b/interface/src/scripting/AudioDeviceScriptingInterface.cpp @@ -11,21 +11,19 @@ #include "AudioClient.h" #include "AudioDeviceScriptingInterface.h" - +#include "SettingsScriptingInterface.h" AudioDeviceScriptingInterface* AudioDeviceScriptingInterface::getInstance() { static AudioDeviceScriptingInterface sharedInstance; return &sharedInstance; } -QStringList AudioDeviceScriptingInterface::inputAudioDevices() const -{ - return DependencyManager::get()->getDeviceNames(QAudio::AudioInput).toList();; +QStringList AudioDeviceScriptingInterface::inputAudioDevices() const { + return _inputAudioDevices; } -QStringList AudioDeviceScriptingInterface::outputAudioDevices() const -{ - return DependencyManager::get()->getDeviceNames(QAudio::AudioOutput).toList();; +QStringList AudioDeviceScriptingInterface::outputAudioDevices() const { + return _outputAudioDevices; } bool AudioDeviceScriptingInterface::muted() @@ -33,11 +31,27 @@ bool AudioDeviceScriptingInterface::muted() return getMuted(); } -AudioDeviceScriptingInterface::AudioDeviceScriptingInterface() { +AudioDeviceScriptingInterface::AudioDeviceScriptingInterface(): QAbstractListModel(nullptr) { connect(DependencyManager::get().data(), &AudioClient::muteToggled, this, &AudioDeviceScriptingInterface::muteToggled); connect(DependencyManager::get().data(), &AudioClient::deviceChanged, - this, &AudioDeviceScriptingInterface::deviceChanged); + this, &AudioDeviceScriptingInterface::onDeviceChanged, Qt::QueuedConnection); + connect(DependencyManager::get().data(), &AudioClient::currentInputDeviceChanged, + this, &AudioDeviceScriptingInterface::onCurrentInputDeviceChanged, Qt::QueuedConnection); + connect(DependencyManager::get().data(), &AudioClient::currentOutputDeviceChanged, + this, &AudioDeviceScriptingInterface::onCurrentOutputDeviceChanged, Qt::QueuedConnection); + //fill up model + onDeviceChanged(); + //set up previously saved device + SettingsScriptingInterface* settings = SettingsScriptingInterface::getInstance(); + const QString inDevice = settings->getValue("audio_input_device").toString(); + if (inDevice != _currentInputDevice) { + setInputDeviceAsync(inDevice); + } + const QString outDevice = settings->getValue("audio_output_device").toString(); + if (outDevice != _currentOutputDevice) { + setOutputDeviceAsync(outDevice); + } } bool AudioDeviceScriptingInterface::setInputDevice(const QString& deviceName) { @@ -58,6 +72,43 @@ bool AudioDeviceScriptingInterface::setOutputDevice(const QString& deviceName) { return result; } +bool AudioDeviceScriptingInterface::setDeviceFromMenu(const QString& deviceMenuName) { + QAudio::Mode mode; + + if (deviceMenuName.indexOf("for Output") != -1) { + mode = QAudio::AudioOutput; + } else if (deviceMenuName.indexOf("for Input") != -1) { + mode = QAudio::AudioInput; + } else { + return false; + } + + for (ScriptingAudioDeviceInfo di: _devices) { + if (mode == di.mode && deviceMenuName.contains(di.name)) { + if (mode == QAudio::AudioOutput) { + setOutputDeviceAsync(di.name); + } else { + setInputDeviceAsync(di.name); + } + return true; + } + } + + return false; +} + +void AudioDeviceScriptingInterface::setInputDeviceAsync(const QString& deviceName) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "switchInputToAudioDevice", + Qt::QueuedConnection, + Q_ARG(const QString&, deviceName)); +} + +void AudioDeviceScriptingInterface::setOutputDeviceAsync(const QString& deviceName) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "switchOutputToAudioDevice", + Qt::QueuedConnection, + Q_ARG(const QString&, deviceName)); +} + QString AudioDeviceScriptingInterface::getInputDevice() { return DependencyManager::get()->getDeviceName(QAudio::AudioInput); } @@ -116,3 +167,105 @@ void AudioDeviceScriptingInterface::setMuted(bool muted) bool AudioDeviceScriptingInterface::getMuted() { return DependencyManager::get()->isMuted(); } + +QVariant AudioDeviceScriptingInterface::data(const QModelIndex& index, int role) const { + //sanity + if (!index.isValid() || index.row() >= _devices.size()) + return QVariant(); + + + if (role == Qt::DisplayRole || role == DisplayNameRole) { + return _devices.at(index.row()).name; + } else if (role == SelectedRole) { + return _devices.at(index.row()).selected; + } else if (role == AudioModeRole) { + return (int)_devices.at(index.row()).mode; + } + return QVariant(); +} + +int AudioDeviceScriptingInterface::rowCount(const QModelIndex& parent) const { + Q_UNUSED(parent) + return _devices.size(); +} + +QHash AudioDeviceScriptingInterface::roleNames() const { + QHash roles; + roles.insert(DisplayNameRole, "devicename"); + roles.insert(SelectedRole, "devicechecked"); + roles.insert(AudioModeRole, "devicemode"); + return roles; +} + +void AudioDeviceScriptingInterface::onDeviceChanged() +{ + beginResetModel(); + _outputAudioDevices.clear(); + _devices.clear(); + _currentOutputDevice = getOutputDevice(); + for (QString name: getOutputDevices()) { + ScriptingAudioDeviceInfo di; + di.name = name; + di.selected = (name == _currentOutputDevice); + di.mode = QAudio::AudioOutput; + _devices.append(di); + _outputAudioDevices.append(name); + } + emit outputAudioDevicesChanged(_outputAudioDevices); + + _inputAudioDevices.clear(); + _currentInputDevice = getInputDevice(); + for (QString name: getInputDevices()) { + ScriptingAudioDeviceInfo di; + di.name = name; + di.selected = (name == _currentInputDevice); + di.mode = QAudio::AudioInput; + _devices.append(di); + _inputAudioDevices.append(name); + } + emit inputAudioDevicesChanged(_inputAudioDevices); + + endResetModel(); + emit deviceChanged(); +} + +void AudioDeviceScriptingInterface::onCurrentInputDeviceChanged(const QString& name) +{ + currentDeviceUpdate(name, QAudio::AudioInput); + //we got a signal that device changed. Save it now + SettingsScriptingInterface* settings = SettingsScriptingInterface::getInstance(); + settings->setValue("audio_input_device", name); + emit currentInputDeviceChanged(name); +} + +void AudioDeviceScriptingInterface::onCurrentOutputDeviceChanged(const QString& name) +{ + currentDeviceUpdate(name, QAudio::AudioOutput); + //we got a signal that device changed. Save it now + SettingsScriptingInterface* settings = SettingsScriptingInterface::getInstance(); + settings->setValue("audio_output_device", name); + emit currentOutputDeviceChanged(name); +} + +void AudioDeviceScriptingInterface::currentDeviceUpdate(const QString& name, QAudio::Mode mode) +{ + QVector role; + role.append(SelectedRole); + + for (int i = 0; i < _devices.size(); i++) { + ScriptingAudioDeviceInfo di = _devices.at(i); + if (di.mode != mode) { + continue; + } + if (di.selected && di.name != name ) { + di.selected = false; + _devices[i] = di; + emit dataChanged(index(i, 0), index(i, 0), role); + } + if (di.name == name) { + di.selected = true; + _devices[i] = di; + emit dataChanged(index(i, 0), index(i, 0), role); + } + } +} diff --git a/interface/src/scripting/AudioDeviceScriptingInterface.h b/interface/src/scripting/AudioDeviceScriptingInterface.h index 4d1d47dcba..f912c35288 100644 --- a/interface/src/scripting/AudioDeviceScriptingInterface.h +++ b/interface/src/scripting/AudioDeviceScriptingInterface.h @@ -15,10 +15,18 @@ #include #include #include +#include +#include class AudioEffectOptions; -class AudioDeviceScriptingInterface : public QObject { +struct ScriptingAudioDeviceInfo { + QString name; + bool selected; + QAudio::Mode mode; +}; + +class AudioDeviceScriptingInterface : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QStringList inputAudioDevices READ inputAudioDevices NOTIFY inputAudioDevicesChanged) @@ -32,9 +40,26 @@ public: QStringList outputAudioDevices() const; bool muted(); + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QHash roleNames() const override; + + enum Roles { + DisplayNameRole = Qt::UserRole, + SelectedRole, + AudioModeRole + }; + +private slots: + void onDeviceChanged(); + void onCurrentInputDeviceChanged(const QString& name); + void onCurrentOutputDeviceChanged(const QString& name); + void currentDeviceUpdate(const QString& name, QAudio::Mode mode); + public slots: bool setInputDevice(const QString& deviceName); bool setOutputDevice(const QString& deviceName); + bool setDeviceFromMenu(const QString& deviceMenuName); QString getInputDevice(); QString getOutputDevice(); @@ -55,15 +80,28 @@ public slots: void setMuted(bool muted); + void setInputDeviceAsync(const QString& deviceName); + void setOutputDeviceAsync(const QString& deviceName); private: AudioDeviceScriptingInterface(); signals: void muteToggled(); void deviceChanged(); + void currentInputDeviceChanged(const QString& name); + void currentOutputDeviceChanged(const QString& name); void mutedChanged(bool muted); void inputAudioDevicesChanged(QStringList inputAudioDevices); void outputAudioDevicesChanged(QStringList outputAudioDevices); + +private: + QVector _devices; + + QStringList _inputAudioDevices; + QStringList _outputAudioDevices; + + QString _currentInputDevice; + QString _currentOutputDevice; }; #endif // hifi_AudioDeviceScriptingInterface_h diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index 7700874d9a..79314ce49a 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -28,11 +28,15 @@ const int MAX_HISTORY_SIZE = 64; const QString COMMAND_STYLE = "color: #266a9b;"; const QString RESULT_SUCCESS_STYLE = "color: #677373;"; +const QString RESULT_INFO_STYLE = "color: #223bd1;"; +const QString RESULT_WARNING_STYLE = "color: #d13b22;"; const QString RESULT_ERROR_STYLE = "color: #d13b22;"; const QString GUTTER_PREVIOUS_COMMAND = "<"; const QString GUTTER_ERROR = "X"; +const QString JSConsole::_consoleFileName { "about:console" }; + JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) : QWidget(parent), _ui(new Ui::Console), @@ -77,6 +81,8 @@ void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) { } if (_scriptEngine != NULL) { disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + disconnect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); + disconnect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); if (_ownScriptEngine) { _scriptEngine->deleteLater(); @@ -84,10 +90,12 @@ void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) { } // if scriptEngine is NULL then create one and keep track of it using _ownScriptEngine - _ownScriptEngine = scriptEngine == NULL; - _scriptEngine = _ownScriptEngine ? DependencyManager::get()->loadScript(QString(), false) : scriptEngine; + _ownScriptEngine = (scriptEngine == NULL); + _scriptEngine = _ownScriptEngine ? DependencyManager::get()->loadScript(_consoleFileName, false) : scriptEngine; connect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + connect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); + connect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); connect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); } @@ -107,11 +115,10 @@ void JSConsole::executeCommand(const QString& command) { QScriptValue JSConsole::executeCommandInWatcher(const QString& command) { QScriptValue result; - static const QString filename = "JSConcole"; QMetaObject::invokeMethod(_scriptEngine, "evaluate", Qt::ConnectionType::BlockingQueuedConnection, Q_RETURN_ARG(QScriptValue, result), Q_ARG(const QString&, command), - Q_ARG(const QString&, filename)); + Q_ARG(const QString&, _consoleFileName)); return result; } @@ -134,16 +141,26 @@ void JSConsole::commandFinished() { resetCurrentCommandHistory(); } -void JSConsole::handleError(const QString& scriptName, const QString& message) { +void JSConsole::handleError(const QString& message, const QString& scriptName) { Q_UNUSED(scriptName); appendMessage(GUTTER_ERROR, "" + message.toHtmlEscaped() + ""); } -void JSConsole::handlePrint(const QString& scriptName, const QString& message) { +void JSConsole::handlePrint(const QString& message, const QString& scriptName) { Q_UNUSED(scriptName); appendMessage("", message); } +void JSConsole::handleInfo(const QString& message, const QString& scriptName) { + Q_UNUSED(scriptName); + appendMessage("", "" + message.toHtmlEscaped() + ""); +} + +void JSConsole::handleWarning(const QString& message, const QString& scriptName) { + Q_UNUSED(scriptName); + appendMessage("", "" + message.toHtmlEscaped() + ""); +} + void JSConsole::mouseReleaseEvent(QMouseEvent* event) { _ui->promptTextEdit->setFocus(); } diff --git a/interface/src/ui/JSConsole.h b/interface/src/ui/JSConsole.h index d5f5aff301..864f847071 100644 --- a/interface/src/ui/JSConsole.h +++ b/interface/src/ui/JSConsole.h @@ -47,8 +47,10 @@ protected: protected slots: void scrollToBottom(); void resizeTextInput(); - void handlePrint(const QString& scriptName, const QString& message); - void handleError(const QString& scriptName, const QString& message); + void handlePrint(const QString& message, const QString& scriptName); + void handleInfo(const QString& message, const QString& scriptName); + void handleWarning(const QString& message, const QString& scriptName); + void handleError(const QString& message, const QString& scriptName); void commandFinished(); private: @@ -66,6 +68,7 @@ private: bool _ownScriptEngine; QString _rootCommand; ScriptEngine* _scriptEngine; + static const QString _consoleFileName; }; diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 1cf21edfb8..dd32e4893d 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -51,6 +51,7 @@ private: static void processFrames(); public: static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static bool isAlreadyTakingSnapshotAnimated() { return snapshotAnimatedFirstFrameTimestamp != 0; }; static Setting::Handle alsoTakeAnimatedSnapshot; static Setting::Handle snapshotAnimatedDuration; }; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 61a283b88c..4970112405 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -408,6 +408,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR const QVector& overlaysToInclude, const QVector& overlaysToDiscard, bool visibleOnly, bool collidableOnly) { + QReadLocker lock(&_lock); float bestDistance = std::numeric_limits::max(); bool bestIsFront = false; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ecc63801fc..d9eab9a78d 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -225,10 +225,6 @@ void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { } void Web3DOverlay::render(RenderArgs* args) { - if (!_visible || !getParentVisible()) { - return; - } - QOpenGLContext * currentContext = QOpenGLContext::currentContext(); QSurface * currentSurface = currentContext->surface(); if (!_webSurface) { @@ -282,6 +278,10 @@ void Web3DOverlay::render(RenderArgs* args) { _webSurface->resize(QSize(_resolution.x, _resolution.y)); } + if (!_visible || !getParentVisible()) { + return; + } + vec2 halfSize = getSize() / 2.0f; vec4 color(toGlm(getColor()), getAlpha()); diff --git a/libraries/animation/src/AnimContext.cpp b/libraries/animation/src/AnimContext.cpp index c8d3e7bcda..70ca3764b0 100644 --- a/libraries/animation/src/AnimContext.cpp +++ b/libraries/animation/src/AnimContext.cpp @@ -10,7 +10,11 @@ #include "AnimContext.h" -AnimContext::AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix) : +AnimContext::AnimContext(bool enableDebugDrawIKTargets, bool enableDebugDrawIKConstraints, + const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix) : _enableDebugDrawIKTargets(enableDebugDrawIKTargets), - _geometryToRigMatrix(geometryToRigMatrix) { + _enableDebugDrawIKConstraints(enableDebugDrawIKConstraints), + _geometryToRigMatrix(geometryToRigMatrix), + _rigToWorldMatrix(rigToWorldMatrix) +{ } diff --git a/libraries/animation/src/AnimContext.h b/libraries/animation/src/AnimContext.h index 3170911e14..f68535005c 100644 --- a/libraries/animation/src/AnimContext.h +++ b/libraries/animation/src/AnimContext.h @@ -16,15 +16,20 @@ class AnimContext { public: - AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix); + AnimContext(bool enableDebugDrawIKTargets, bool enableDebugDrawIKConstraints, + const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix); bool getEnableDebugDrawIKTargets() const { return _enableDebugDrawIKTargets; } + bool getEnableDebugDrawIKConstraints() const { return _enableDebugDrawIKConstraints; } const glm::mat4& getGeometryToRigMatrix() const { return _geometryToRigMatrix; } + const glm::mat4& getRigToWorldMatrix() const { return _rigToWorldMatrix; } protected: bool _enableDebugDrawIKTargets { false }; + bool _enableDebugDrawIKConstraints{ false }; glm::mat4 _geometryToRigMatrix; + glm::mat4 _rigToWorldMatrix; }; #endif // hifi_AnimContext_h diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 6edd969568..4471f11857 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -21,6 +21,39 @@ #include "SwingTwistConstraint.h" #include "AnimationLogging.h" +AnimInverseKinematics::IKTargetVar::IKTargetVar(const QString& jointNameIn, const QString& positionVarIn, const QString& rotationVarIn, + const QString& typeVarIn, const QString& weightVarIn, float weightIn, const std::vector& flexCoefficientsIn) : + jointName(jointNameIn), + positionVar(positionVarIn), + rotationVar(rotationVarIn), + typeVar(typeVarIn), + weightVar(weightVarIn), + weight(weightIn), + numFlexCoefficients(flexCoefficientsIn.size()), + jointIndex(-1) +{ + numFlexCoefficients = std::min(numFlexCoefficients, (size_t)MAX_FLEX_COEFFICIENTS); + for (size_t i = 0; i < numFlexCoefficients; i++) { + flexCoefficients[i] = flexCoefficientsIn[i]; + } +} + +AnimInverseKinematics::IKTargetVar::IKTargetVar(const IKTargetVar& orig) : + jointName(orig.jointName), + positionVar(orig.positionVar), + rotationVar(orig.rotationVar), + typeVar(orig.typeVar), + weightVar(orig.weightVar), + weight(orig.weight), + numFlexCoefficients(orig.numFlexCoefficients), + jointIndex(orig.jointIndex) +{ + numFlexCoefficients = std::min(numFlexCoefficients, (size_t)MAX_FLEX_COEFFICIENTS); + for (size_t i = 0; i < numFlexCoefficients; i++) { + flexCoefficients[i] = orig.flexCoefficients[i]; + } +} + AnimInverseKinematics::AnimInverseKinematics(const QString& id) : AnimNode(AnimNode::Type::InverseKinematics, id) { } @@ -60,26 +93,22 @@ void AnimInverseKinematics::computeAbsolutePoses(AnimPoseVec& absolutePoses) con } } -void AnimInverseKinematics::setTargetVars( - const QString& jointName, - const QString& positionVar, - const QString& rotationVar, - const QString& typeVar) { +void AnimInverseKinematics::setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar, + const QString& typeVar, const QString& weightVar, float weight, const std::vector& flexCoefficients) { + IKTargetVar targetVar(jointName, positionVar, rotationVar, typeVar, weightVar, weight, flexCoefficients); + // if there are dups, last one wins. bool found = false; - for (auto& targetVar: _targetVarVec) { - if (targetVar.jointName == jointName) { - // update existing targetVar - targetVar.positionVar = positionVar; - targetVar.rotationVar = rotationVar; - targetVar.typeVar = typeVar; + for (auto& targetVarIter: _targetVarVec) { + if (targetVarIter.jointName == jointName) { + targetVarIter = targetVar; found = true; break; } } if (!found) { // create a new entry - _targetVarVec.push_back(IKTargetVar(jointName, positionVar, rotationVar, typeVar)); + _targetVarVec.push_back(targetVar); } } @@ -107,10 +136,15 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses); glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, defaultPose.rot()); glm::vec3 translation = animVars.lookupRigToGeometry(targetVar.positionVar, defaultPose.trans()); + float weight = animVars.lookup(targetVar.weightVar, targetVar.weight); target.setPose(rotation, translation); target.setIndex(targetVar.jointIndex); + target.setWeight(weight); + target.setFlexCoefficients(targetVar.numFlexCoefficients, targetVar.flexCoefficients); + targets.push_back(target); + if (targetVar.jointIndex > _maxTargetIndex) { _maxTargetIndex = targetVar.jointIndex; } @@ -271,6 +305,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe // cache tip absolute position glm::vec3 tipPosition = absolutePoses[tipIndex].trans(); + size_t chainDepth = 1; + // descend toward root, pivoting each joint to get tip closer to target position while (pivotIndex != _hipsIndex && pivotsParentIndex != -1) { // compute the two lines that should be aligned @@ -312,9 +348,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe float angle = acosf(cosAngle); const float MIN_ADJUSTMENT_ANGLE = 1.0e-4f; if (angle > MIN_ADJUSTMENT_ANGLE) { - // reduce angle by a fraction (for stability) - const float STABILITY_FRACTION = 0.5f; - angle *= STABILITY_FRACTION; + // reduce angle by a flexCoefficient + angle *= target.getFlexCoefficient(chainDepth); deltaRotation = glm::angleAxis(angle, axis); // The swing will re-orient the tip but there will tend to be be a non-zero delta between the tip's @@ -385,6 +420,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe pivotIndex = pivotsParentIndex; pivotsParentIndex = _skeleton->getParentIndex(pivotIndex); + + chainDepth++; } return lowestMovedIndex; } @@ -399,6 +436,13 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + // allows solutionSource to be overridden by an animVar + auto solutionSource = animVars.lookup(_solutionSourceVar, (int)_solutionSource); + + if (context.getEnableDebugDrawIKConstraints()) { + debugDrawConstraints(context); + } + const float MAX_OVERLAY_DT = 1.0f / 30.0f; // what to clamp delta-time to in AnimInverseKinematics::overlay if (dt > MAX_OVERLAY_DT) { dt = MAX_OVERLAY_DT; @@ -410,25 +454,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars PROFILE_RANGE_EX(simulation_animation, "ik/relax", 0xffff00ff, 0); - // relax toward underPoses - // HACK: this relaxation needs to be constant per-frame rather than per-realtime - // in order to prevent IK "flutter" for bad FPS. The bad news is that the good parts - // of this relaxation will be FPS dependent (low FPS will make the limbs align slower - // in real-time), however most people will not notice this and this problem is less - // annoying than the flutter. - const float blend = (1.0f / 60.0f) / (0.25f); // effectively: dt / RELAXATION_TIMESCALE - int numJoints = (int)_relativePoses.size(); - for (int i = 0; i < numJoints; ++i) { - float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), underPoses[i].rot())); - if (_accumulators[i].isDirty()) { - // this joint is affected by IK --> blend toward underPose rotation - _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * underPoses[i].rot(), blend)); - } else { - // this joint is NOT affected by IK --> slam to underPose rotation - _relativePoses[i].rot() = underPoses[i].rot(); - } - _relativePoses[i].trans() = underPoses[i].trans(); - } + initRelativePosesFromSolutionSource((SolutionSource)solutionSource, underPoses); if (!underPoses.empty()) { // Sometimes the underpose itself can violate the constraints. Rather than @@ -604,9 +630,9 @@ void AnimInverseKinematics::clearIKJointLimitHistory() { } } -RotationConstraint* AnimInverseKinematics::getConstraint(int index) { +RotationConstraint* AnimInverseKinematics::getConstraint(int index) const { RotationConstraint* constraint = nullptr; - std::map::iterator constraintItr = _constraints.find(index); + std::map::const_iterator constraintItr = _constraints.find(index); if (constraintItr != _constraints.end()) { constraint = constraintItr->second; } @@ -622,17 +648,19 @@ void AnimInverseKinematics::clearConstraints() { _constraints.clear(); } -// set up swing limits around a swingTwistConstraint in an ellipse, where lateralSwingTheta is the swing limit for lateral swings (side to side) -// anteriorSwingTheta is swing limit for forward and backward swings. (where x-axis of reference rotation is sideways and -z-axis is forward) -static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float lateralSwingTheta, float anteriorSwingTheta) { +// set up swing limits around a swingTwistConstraint in an ellipse, where lateralSwingPhi is the swing limit for lateral swings (side to side) +// anteriorSwingPhi is swing limit for forward and backward swings. (where x-axis of reference rotation is sideways and -z-axis is forward) +static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float lateralSwingPhi, float anteriorSwingPhi) { assert(stConstraint); - const int NUM_SUBDIVISIONS = 8; + const int NUM_SUBDIVISIONS = 16; std::vector minDots; minDots.reserve(NUM_SUBDIVISIONS); float dTheta = TWO_PI / NUM_SUBDIVISIONS; float theta = 0.0f; for (int i = 0; i < NUM_SUBDIVISIONS; i++) { - minDots.push_back(cosf(glm::length(glm::vec2(anteriorSwingTheta * cosf(theta), lateralSwingTheta * sinf(theta))))); + float theta_prime = atanf((lateralSwingPhi / anteriorSwingPhi) * tanf(theta)); + float phi = (cosf(2.0f * theta_prime) * ((lateralSwingPhi - anteriorSwingPhi) / 2.0f)) + ((lateralSwingPhi + anteriorSwingPhi) / 2.0f); + minDots.push_back(cosf(phi)); theta += dTheta; } stConstraint->setSwingLimits(minDots); @@ -640,7 +668,6 @@ static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float l void AnimInverseKinematics::initConstraints() { if (!_skeleton) { - return; } // We create constraints for the joints shown here // (and their Left counterparts if applicable). @@ -744,30 +771,27 @@ void AnimInverseKinematics::initConstraints() { std::vector swungDirections; float deltaTheta = PI / 4.0f; float theta = 0.0f; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.25f, sinf(theta))); + swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.25f, sinf(theta))); theta += deltaTheta; swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); theta += deltaTheta; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.25f, sinf(theta))); // posterior + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.25f, sinf(theta))); // posterior theta += deltaTheta; swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); theta += deltaTheta; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.25f, sinf(theta))); + swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.25f, sinf(theta))); theta += deltaTheta; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta))); + swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.5f, sinf(theta))); theta += deltaTheta; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta))); // anterior + swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.5f, sinf(theta))); // anterior theta += deltaTheta; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta))); + swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.5f, sinf(theta))); - // rotate directions into joint-frame - glm::quat invAbsoluteRotation = glm::inverse(absolutePoses[i].rot()); - int numDirections = (int)swungDirections.size(); - for (int j = 0; j < numDirections; ++j) { - swungDirections[j] = invAbsoluteRotation * swungDirections[j]; + std::vector minDots; + for (size_t i = 0; i < swungDirections.size(); i++) { + minDots.push_back(glm::dot(glm::normalize(swungDirections[i]), Vectors::UNIT_Y)); } - stConstraint->setSwingLimits(swungDirections); - + stConstraint->setSwingLimits(minDots); constraint = static_cast(stConstraint); } else if (0 == baseName.compare("Hand", Qt::CaseSensitive)) { SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); @@ -819,7 +843,7 @@ void AnimInverseKinematics::initConstraints() { stConstraint->setTwistLimits(-MAX_SHOULDER_TWIST, MAX_SHOULDER_TWIST); std::vector minDots; - const float MAX_SHOULDER_SWING = PI / 20.0f; + const float MAX_SHOULDER_SWING = PI / 16.0f; minDots.push_back(cosf(MAX_SHOULDER_SWING)); stConstraint->setSwingLimits(minDots); @@ -957,6 +981,32 @@ void AnimInverseKinematics::initConstraints() { } } +void AnimInverseKinematics::initLimitCenterPoses() { + assert(_skeleton); + _limitCenterPoses.reserve(_skeleton->getNumJoints()); + for (int i = 0; i < _skeleton->getNumJoints(); i++) { + AnimPose pose = _skeleton->getRelativeDefaultPose(i); + RotationConstraint* constraint = getConstraint(i); + if (constraint) { + pose.rot() = constraint->computeCenterRotation(); + } + _limitCenterPoses.push_back(pose); + } + + // The limit center rotations for the LeftArm and RightArm form a t-pose. + // In order for the elbows to look more natural, we rotate them down by the avatar's sides + const float UPPER_ARM_THETA = PI / 3.0f; // 60 deg + int leftArmIndex = _skeleton->nameToJointIndex("LeftArm"); + const glm::quat armRot = glm::angleAxis(UPPER_ARM_THETA, Vectors::UNIT_X); + if (leftArmIndex >= 0 && leftArmIndex < (int)_limitCenterPoses.size()) { + _limitCenterPoses[leftArmIndex].rot() = _limitCenterPoses[leftArmIndex].rot() * armRot; + } + int rightArmIndex = _skeleton->nameToJointIndex("RightArm"); + if (rightArmIndex >= 0 && rightArmIndex < (int)_limitCenterPoses.size()) { + _limitCenterPoses[rightArmIndex].rot() = _limitCenterPoses[rightArmIndex].rot() * armRot; + } +} + void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { AnimNode::setSkeletonInternal(skeleton); @@ -973,6 +1023,7 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele if (skeleton) { initConstraints(); + initLimitCenterPoses(); _headIndex = _skeleton->nameToJointIndex("Head"); _hipsIndex = _skeleton->nameToJointIndex("Hips"); @@ -989,3 +1040,170 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele _hipsParentIndex = -1; } } + +static glm::vec3 sphericalToCartesian(float phi, float theta) { + float cos_phi = cosf(phi); + float sin_phi = sinf(phi); + return glm::vec3(sin_phi * cosf(theta), cos_phi, -sin_phi * sinf(theta)); +} + +void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) const { + if (_skeleton) { + const vec4 RED(1.0f, 0.0f, 0.0f, 1.0f); + const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f); + const vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); + const vec4 PURPLE(0.5f, 0.0f, 1.0f, 1.0f); + const vec4 CYAN(0.0f, 1.0f, 1.0f, 1.0f); + const vec4 GRAY(0.2f, 0.2f, 0.2f, 1.0f); + const vec4 MAGENTA(1.0f, 0.0f, 1.0f, 1.0f); + const float AXIS_LENGTH = 2.0f; // cm + const float TWIST_LENGTH = 4.0f; // cm + const float HINGE_LENGTH = 6.0f; // cm + const float SWING_LENGTH = 5.0f; // cm + + AnimPoseVec poses = _skeleton->getRelativeDefaultPoses(); + + // copy reference rotations into the relative poses + for (int i = 0; i < (int)poses.size(); i++) { + const RotationConstraint* constraint = getConstraint(i); + if (constraint) { + poses[i].rot() = constraint->getReferenceRotation(); + } + } + + // convert relative poses to absolute + _skeleton->convertRelativePosesToAbsolute(poses); + + mat4 geomToWorldMatrix = context.getRigToWorldMatrix() * context.getGeometryToRigMatrix(); + + // draw each pose and constraint + for (int i = 0; i < (int)poses.size(); i++) { + // transform local axes into world space. + auto pose = poses[i]; + glm::vec3 xAxis = transformVectorFast(geomToWorldMatrix, pose.rot() * Vectors::UNIT_X); + glm::vec3 yAxis = transformVectorFast(geomToWorldMatrix, pose.rot() * Vectors::UNIT_Y); + glm::vec3 zAxis = transformVectorFast(geomToWorldMatrix, pose.rot() * Vectors::UNIT_Z); + glm::vec3 pos = transformPoint(geomToWorldMatrix, pose.trans()); + DebugDraw::getInstance().drawRay(pos, pos + AXIS_LENGTH * xAxis, RED); + DebugDraw::getInstance().drawRay(pos, pos + AXIS_LENGTH * yAxis, GREEN); + DebugDraw::getInstance().drawRay(pos, pos + AXIS_LENGTH * zAxis, BLUE); + + // draw line to parent + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex != -1) { + glm::vec3 parentPos = transformPoint(geomToWorldMatrix, poses[parentIndex].trans()); + DebugDraw::getInstance().drawRay(pos, parentPos, GRAY); + } + + glm::quat parentAbsRot; + if (parentIndex != -1) { + parentAbsRot = poses[parentIndex].rot(); + } + + const RotationConstraint* constraint = getConstraint(i); + if (constraint) { + glm::quat refRot = constraint->getReferenceRotation(); + const ElbowConstraint* elbowConstraint = dynamic_cast(constraint); + if (elbowConstraint) { + glm::vec3 hingeAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * refRot * elbowConstraint->getHingeAxis()); + DebugDraw::getInstance().drawRay(pos, pos + HINGE_LENGTH * hingeAxis, MAGENTA); + + // draw elbow constraints + glm::quat minRot = glm::angleAxis(elbowConstraint->getMinAngle(), elbowConstraint->getHingeAxis()); + glm::quat maxRot = glm::angleAxis(elbowConstraint->getMaxAngle(), elbowConstraint->getHingeAxis()); + + const int NUM_SWING_STEPS = 10; + for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { + glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_Y); + DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); + } + + } else { + const SwingTwistConstraint* swingTwistConstraint = dynamic_cast(constraint); + if (swingTwistConstraint) { + // twist constraints + + glm::vec3 hingeAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * refRot * Vectors::UNIT_Y); + DebugDraw::getInstance().drawRay(pos, pos + HINGE_LENGTH * hingeAxis, MAGENTA); + + glm::quat minRot = glm::angleAxis(swingTwistConstraint->getMinTwist(), Vectors::UNIT_Y); + glm::quat maxRot = glm::angleAxis(swingTwistConstraint->getMaxTwist(), Vectors::UNIT_Y); + + const int NUM_SWING_STEPS = 10; + for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { + glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_X); + DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); + } + + // draw swing constraints. + const size_t NUM_MIN_DOTS = swingTwistConstraint->getMinDots().size(); + const float D_THETA = TWO_PI / (NUM_MIN_DOTS - 1); + float theta = 0.0f; + for (size_t i = 0, j = NUM_MIN_DOTS - 2; i < NUM_MIN_DOTS - 1; j = i, i++, theta += D_THETA) { + // compute swing rotation from theta and phi angles. + float phi = acosf(swingTwistConstraint->getMinDots()[i]); + glm::vec3 swungAxis = sphericalToCartesian(phi, theta); + glm::vec3 worldSwungAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * refRot * swungAxis); + glm::vec3 swingTip = pos + SWING_LENGTH * worldSwungAxis; + + float prevPhi = acos(swingTwistConstraint->getMinDots()[j]); + float prevTheta = theta - D_THETA; + glm::vec3 prevSwungAxis = sphericalToCartesian(prevPhi, prevTheta); + glm::vec3 prevWorldSwungAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * refRot * prevSwungAxis); + glm::vec3 prevSwingTip = pos + SWING_LENGTH * prevWorldSwungAxis; + + DebugDraw::getInstance().drawRay(pos, swingTip, PURPLE); + DebugDraw::getInstance().drawRay(prevSwingTip, swingTip, PURPLE); + } + } + } + pose.rot() = constraint->computeCenterRotation(); + } + } + } +} + +// for bones under IK, blend between previous solution (_relativePoses) to targetPoses +// for bones NOT under IK, copy directly from underPoses. +// mutates _relativePoses. +void AnimInverseKinematics::blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPoses, float blendFactor) { + // relax toward poses + int numJoints = (int)_relativePoses.size(); + for (int i = 0; i < numJoints; ++i) { + float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), targetPoses[i].rot())); + if (_accumulators[i].isDirty()) { + // this joint is affected by IK --> blend toward the targetPoses rotation + _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * targetPoses[i].rot(), blendFactor)); + } else { + // this joint is NOT affected by IK --> slam to underPoses rotation + _relativePoses[i].rot() = underPoses[i].rot(); + } + _relativePoses[i].trans() = underPoses[i].trans(); + } +} + +void AnimInverseKinematics::initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPoses) { + const float RELAX_BLEND_FACTOR = (1.0f / 16.0f); + const float COPY_BLEND_FACTOR = 1.0f; + switch (solutionSource) { + default: + case SolutionSource::RelaxToUnderPoses: + blendToPoses(underPoses, underPoses, RELAX_BLEND_FACTOR); + break; + case SolutionSource::RelaxToLimitCenterPoses: + blendToPoses(_limitCenterPoses, underPoses, RELAX_BLEND_FACTOR); + break; + case SolutionSource::PreviousSolution: + // do nothing... _relativePoses is already the previous solution + break; + case SolutionSource::UnderPoses: + _relativePoses = underPoses; + break; + case SolutionSource::LimitCenterPoses: + // essentially copy limitCenterPoses over to _relativePoses. + blendToPoses(_limitCenterPoses, underPoses, COPY_BLEND_FACTOR); + break; + } +} diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index c91b7aa9c4..74face6d0b 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -32,7 +32,8 @@ public: void loadPoses(const AnimPoseVec& poses); void computeAbsolutePoses(AnimPoseVec& absolutePoses) const; - void setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar, const QString& typeVar); + void setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar, + const QString& typeVar, const QString& weightVar, float weight, const std::vector& flexCoefficients); virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimNode::Triggers& triggersOut) override; virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; @@ -43,40 +44,54 @@ public: float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + enum class SolutionSource { + RelaxToUnderPoses = 0, + RelaxToLimitCenterPoses, + PreviousSolution, + UnderPoses, + LimitCenterPoses, + NumSolutionSources, + }; + + void setSolutionSource(SolutionSource solutionSource) { _solutionSource = solutionSource; } + void setSolutionSourceVar(const QString& solutionSourceVar) { _solutionSourceVar = solutionSourceVar; } + protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); void solveWithCyclicCoordinateDescent(const std::vector& targets); int solveTargetWithCCD(const IKTarget& target, AnimPoseVec& absolutePoses); virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; + void debugDrawConstraints(const AnimContext& context) const; + void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose); + void blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPose, float blendFactor); // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } - RotationConstraint* getConstraint(int index); + RotationConstraint* getConstraint(int index) const; void clearConstraints(); void initConstraints(); + void initLimitCenterPoses(); void computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt); // no copies AnimInverseKinematics(const AnimInverseKinematics&) = delete; AnimInverseKinematics& operator=(const AnimInverseKinematics&) = delete; + enum FlexCoefficients { MAX_FLEX_COEFFICIENTS = 10 }; struct IKTargetVar { - IKTargetVar(const QString& jointNameIn, - const QString& positionVarIn, - const QString& rotationVarIn, - const QString& typeVarIn) : - positionVar(positionVarIn), - rotationVar(rotationVarIn), - typeVar(typeVarIn), - jointName(jointNameIn), - jointIndex(-1) - {} + IKTargetVar(const QString& jointNameIn, const QString& positionVarIn, const QString& rotationVarIn, + const QString& typeVarIn, const QString& weightVarIn, float weightIn, const std::vector& flexCoefficientsIn); + IKTargetVar(const IKTargetVar& orig); + QString jointName; QString positionVar; QString rotationVar; QString typeVar; - QString jointName; + QString weightVar; + float weight; + float flexCoefficients[MAX_FLEX_COEFFICIENTS]; + size_t numFlexCoefficients; int jointIndex; // cached joint index }; @@ -85,6 +100,7 @@ protected: std::vector _targetVarVec; AnimPoseVec _defaultRelativePoses; // poses of the relaxed state AnimPoseVec _relativePoses; // current relative poses + AnimPoseVec _limitCenterPoses; // relative // experimental data for moving hips during IK glm::vec3 _hipsOffset { Vectors::ZERO }; @@ -100,6 +116,8 @@ protected: float _maxErrorOnLastSolve { FLT_MAX }; bool _previousEnableDebugIKTargets { false }; + SolutionSource _solutionSource { SolutionSource::RelaxToUnderPoses }; + QString _solutionSourceVar; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index bda4541f36..44ed8c6053 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -173,6 +173,13 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) { } \ float NAME = (float)NAME##_VAL.toDouble() +#define READ_OPTIONAL_FLOAT(NAME, JSON_OBJ, DEFAULT) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + float NAME = (float)DEFAULT; \ + if (NAME##_VAL.isDouble()) { \ + NAME = (float)NAME##_VAL.toDouble(); \ + } \ + do {} while (0) static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUrl) { auto idVal = jsonObj.value("id"); @@ -352,6 +359,23 @@ static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { return AnimOverlay::NumBoneSets; } +static const char* solutionSourceStrings[(int)AnimInverseKinematics::SolutionSource::NumSolutionSources] = { + "relaxToUnderPoses", + "relaxToLimitCenterPoses", + "previousSolution", + "underPoses", + "limitCenterPoses" +}; + +static AnimInverseKinematics::SolutionSource stringToSolutionSourceEnum(const QString& str) { + for (int i = 0; i < (int)AnimInverseKinematics::SolutionSource::NumSolutionSources; i++) { + if (str == solutionSourceStrings[i]) { + return (AnimInverseKinematics::SolutionSource)i; + } + } + return AnimInverseKinematics::SolutionSource::NumSolutionSources; +} + static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { READ_STRING(boneSet, jsonObj, id, jsonUrl, nullptr); @@ -453,10 +477,40 @@ AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QS READ_STRING(positionVar, targetObj, id, jsonUrl, nullptr); READ_STRING(rotationVar, targetObj, id, jsonUrl, nullptr); READ_OPTIONAL_STRING(typeVar, targetObj); + READ_OPTIONAL_STRING(weightVar, targetObj); + READ_OPTIONAL_FLOAT(weight, targetObj, 1.0f); - node->setTargetVars(jointName, positionVar, rotationVar, typeVar); + auto flexCoefficientsValue = targetObj.value("flexCoefficients"); + if (!flexCoefficientsValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad or missing flexCoefficients array in \"targets\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto flexCoefficientsArray = flexCoefficientsValue.toArray(); + std::vector flexCoefficients; + for (const auto& value : flexCoefficientsArray) { + flexCoefficients.push_back((float)value.toDouble()); + } + + node->setTargetVars(jointName, positionVar, rotationVar, typeVar, weightVar, weight, flexCoefficients); }; + READ_OPTIONAL_STRING(solutionSource, jsonObj); + + if (!solutionSource.isEmpty()) { + AnimInverseKinematics::SolutionSource solutionSourceType = stringToSolutionSourceEnum(solutionSource); + if (solutionSourceType != AnimInverseKinematics::SolutionSource::NumSolutionSources) { + node->setSolutionSource(solutionSourceType); + } else { + qCWarning(animation) << "AnimNodeLoader, bad solutionSourceType in \"solutionSource\", id = " << id << ", url = " << jsonUrl.toDisplayString(); + } + } + + READ_OPTIONAL_STRING(solutionSourceVar, jsonObj); + + if (!solutionSourceVar.isEmpty()) { + node->setSolutionSourceVar(solutionSourceVar); + } + return node; } diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index e1c8528e0b..470bbab8b6 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -39,7 +39,7 @@ glm::vec3 AnimPose::xformPoint(const glm::vec3& rhs) const { return *this * rhs; } -// really slow +// really slow, but accurate for transforms with non-uniform scale glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { glm::vec3 xAxis = _rot * glm::vec3(_scale.x, 0.0f, 0.0f); glm::vec3 yAxis = _rot * glm::vec3(0.0f, _scale.y, 0.0f); @@ -49,6 +49,11 @@ glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { return transInvMat * rhs; } +// faster, but does not handle non-uniform scale correctly. +glm::vec3 AnimPose::xformVectorFast(const glm::vec3& rhs) const { + return _rot * (_scale * rhs); +} + AnimPose AnimPose::operator*(const AnimPose& rhs) const { glm::mat4 result; glm_mat4u_mul(*this, rhs, result); diff --git a/libraries/animation/src/AnimPose.h b/libraries/animation/src/AnimPose.h index 893a5c1382..a2e22a24be 100644 --- a/libraries/animation/src/AnimPose.h +++ b/libraries/animation/src/AnimPose.h @@ -25,7 +25,8 @@ public: static const AnimPose identity; glm::vec3 xformPoint(const glm::vec3& rhs) const; - glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow + glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow, but accurate for transforms with non-uniform scale + glm::vec3 xformVectorFast(const glm::vec3& rhs) const; // faster, but does not handle non-uniform scale correctly. glm::vec3 operator*(const glm::vec3& rhs) const; // same as xformPoint AnimPose operator*(const AnimPose& rhs) const; diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index c5643034e5..a4659f1e76 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -33,6 +33,23 @@ void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, A } } +glm::quat averageQuats(size_t numQuats, const glm::quat* quats) { + if (numQuats == 0) { + return glm::quat(); + } + glm::quat accum = quats[0]; + glm::quat firstRot = quats[0]; + for (size_t i = 1; i < numQuats; i++) { + glm::quat rot = quats[i]; + float dot = glm::dot(firstRot, rot); + if (dot < 0.0f) { + rot = -rot; + } + accum += rot; + } + return glm::normalize(accum); +} + float accumulateTime(float startFrame, float endFrame, float timeScale, float currentFrame, float dt, bool loopFlag, const QString& id, AnimNode::Triggers& triggersOut) { diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index 6d394be882..055fd630eb 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -16,9 +16,9 @@ // this is where the magic happens void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result); +glm::quat averageQuats(size_t numQuats, const glm::quat* quats); + float accumulateTime(float startFrame, float endFrame, float timeScale, float currentFrame, float dt, bool loopFlag, const QString& id, AnimNode::Triggers& triggersOut); #endif - - diff --git a/libraries/animation/src/ElbowConstraint.cpp b/libraries/animation/src/ElbowConstraint.cpp index 6833c1762e..17c6bb2da6 100644 --- a/libraries/animation/src/ElbowConstraint.cpp +++ b/libraries/animation/src/ElbowConstraint.cpp @@ -13,6 +13,7 @@ #include #include +#include "AnimUtil.h" ElbowConstraint::ElbowConstraint() : _minAngle(-PI), @@ -77,3 +78,10 @@ bool ElbowConstraint::apply(glm::quat& rotation) const { return false; } +glm::quat ElbowConstraint::computeCenterRotation() const { + const size_t NUM_LIMITS = 2; + glm::quat limits[NUM_LIMITS]; + limits[0] = glm::angleAxis(_minAngle, _axis) * _referenceRotation; + limits[1] = glm::angleAxis(_maxAngle, _axis) * _referenceRotation; + return averageQuats(NUM_LIMITS, limits); +} diff --git a/libraries/animation/src/ElbowConstraint.h b/libraries/animation/src/ElbowConstraint.h index 21288715b5..d3f080374a 100644 --- a/libraries/animation/src/ElbowConstraint.h +++ b/libraries/animation/src/ElbowConstraint.h @@ -18,6 +18,12 @@ public: void setHingeAxis(const glm::vec3& axis); void setAngleLimits(float minAngle, float maxAngle); virtual bool apply(glm::quat& rotation) const override; + virtual glm::quat computeCenterRotation() const override; + + glm::vec3 getHingeAxis() const { return _axis; } + float getMinAngle() const { return _minAngle; } + float getMaxAngle() const { return _maxAngle; } + protected: glm::vec3 _axis; glm::vec3 _perpAxis; diff --git a/libraries/animation/src/IKTarget.cpp b/libraries/animation/src/IKTarget.cpp index fa4030ca6d..c67c0621c3 100644 --- a/libraries/animation/src/IKTarget.cpp +++ b/libraries/animation/src/IKTarget.cpp @@ -14,6 +14,23 @@ void IKTarget::setPose(const glm::quat& rotation, const glm::vec3& translation) _pose.trans() = translation; } +void IKTarget::setFlexCoefficients(size_t numFlexCoefficientsIn, const float* flexCoefficientsIn) { + _numFlexCoefficients = std::min(numFlexCoefficientsIn, (size_t)MAX_FLEX_COEFFICIENTS); + for (size_t i = 0; i < _numFlexCoefficients; i++) { + _flexCoefficients[i] = flexCoefficientsIn[i]; + } +} + +float IKTarget::getFlexCoefficient(size_t chainDepth) const { + const float DEFAULT_FLEX_COEFFICIENT = 0.5f; + + if (chainDepth < _numFlexCoefficients) { + return _flexCoefficients[chainDepth]; + } else { + return DEFAULT_FLEX_COEFFICIENT; + } +} + void IKTarget::setType(int type) { switch (type) { case (int)Type::RotationAndPosition: diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index acb01d9861..4f464c103c 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -35,15 +35,21 @@ public: void setPose(const glm::quat& rotation, const glm::vec3& translation); void setIndex(int index) { _index = index; } void setType(int); + void setFlexCoefficients(size_t numFlexCoefficientsIn, const float* flexCoefficientsIn); + float getFlexCoefficient(size_t chainDepth) const; - // HACK: give HmdHead targets more "weight" during IK algorithm - float getWeight() const { return _type == Type::HmdHead ? HACK_HMD_TARGET_WEIGHT : 1.0f; } + void setWeight(float weight) { _weight = weight; } + float getWeight() const { return _weight; } + + enum FlexCoefficients { MAX_FLEX_COEFFICIENTS = 10 }; private: AnimPose _pose; int _index{-1}; Type _type{Type::RotationAndPosition}; - + float _weight; + float _flexCoefficients[MAX_FLEX_COEFFICIENTS]; + size_t _numFlexCoefficients; }; #endif // hifi_IKTarget_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 700761b248..23db05eb73 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -305,30 +305,35 @@ void Rig::clearJointAnimationPriority(int index) { } } -void Rig::clearIKJointLimitHistory() { +std::shared_ptr Rig::getAnimInverseKinematicsNode() const { + std::shared_ptr result; if (_animNode) { _animNode->traverse([&](AnimNode::Pointer node) { // only report clip nodes as valid roles. auto ikNode = std::dynamic_pointer_cast(node); if (ikNode) { - ikNode->clearIKJointLimitHistory(); + result = ikNode; + return false; + } else { + return true; } - return true; }); } + return result; +} + +void Rig::clearIKJointLimitHistory() { + auto ikNode = getAnimInverseKinematicsNode(); + if (ikNode) { + ikNode->clearIKJointLimitHistory(); + } } void Rig::setMaxHipsOffsetLength(float maxLength) { _maxHipsOffsetLength = maxLength; - - if (_animNode) { - _animNode->traverse([&](AnimNode::Pointer node) { - auto ikNode = std::dynamic_pointer_cast(node); - if (ikNode) { - ikNode->setMaxHipsOffsetLength(_maxHipsOffsetLength); - } - return true; - }); + auto ikNode = getAnimInverseKinematicsNode(); + if (ikNode) { + ikNode->setMaxHipsOffsetLength(_maxHipsOffsetLength); } } @@ -936,7 +941,7 @@ void Rig::updateAnimationStateHandlers() { // called on avatar update thread (wh } } -void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { +void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, const glm::mat4& rigToWorldTransform) { PROFILE_RANGE_EX(simulation_animation_detail, __FUNCTION__, 0xffff00ff, 0); PerformanceTimer perfTimer("updateAnimations"); @@ -949,7 +954,8 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); - AnimContext context(_enableDebugDrawIKTargets, getGeometryToRigTransform()); + AnimContext context(_enableDebugDrawIKTargets, _enableDebugDrawIKConstraints, + getGeometryToRigTransform(), rigToWorldTransform); // evaluate the animation AnimNode::Triggers triggersOut; @@ -1025,10 +1031,12 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { _animVars.set("notIsTalking", !params.isTalking); if (params.hipsEnabled) { + _animVars.set("solutionSource", (int)AnimInverseKinematics::SolutionSource::RelaxToLimitCenterPoses); _animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition); _animVars.set("hipsPosition", extractTranslation(params.hipsMatrix)); _animVars.set("hipsRotation", glmExtractRotation(params.hipsMatrix)); } else { + _animVars.set("solutionSource", (int)AnimInverseKinematics::SolutionSource::RelaxToUnderPoses); _animVars.set("hipsType", (int)IKTarget::Type::Unknown); } @@ -1080,10 +1088,12 @@ void Rig::updateHeadAnimVars(const HeadParameters& params) { // Since there is an explicit hips ik target, switch the head to use the more generic RotationAndPosition IK chain type. // this will allow the spine to bend more, ensuring that it can reach the head target position. _animVars.set("headType", (int)IKTarget::Type::RotationAndPosition); + _animVars.unset("headWeight"); // use the default weight for this target. } else { // When there is no hips IK target, use the HmdHead IK chain type. This will make the spine very stiff, // but because the IK _hipsOffset is enabled, the hips will naturally follow underneath the head. _animVars.set("headType", (int)IKTarget::Type::HmdHead); + _animVars.set("headWeight", 8.0f); } } else { _animVars.unset("headPosition"); @@ -1392,22 +1402,24 @@ void Rig::computeAvatarBoundingCapsule( AnimInverseKinematics ikNode("boundingShape"); ikNode.setSkeleton(_animSkeleton); + + // AJT: FIX ME!!!!! ensure that empty weights vector does something reasonable.... ikNode.setTargetVars("LeftHand", "leftHandPosition", "leftHandRotation", - "leftHandType"); + "leftHandType", "leftHandWeight", 1.0f, {}); ikNode.setTargetVars("RightHand", "rightHandPosition", "rightHandRotation", - "rightHandType"); + "rightHandType", "rightHandWeight", 1.0f, {}); ikNode.setTargetVars("LeftFoot", "leftFootPosition", "leftFootRotation", - "leftFootType"); + "leftFootType", "leftFootWeight", 1.0f, {}); ikNode.setTargetVars("RightFoot", "rightFootPosition", "rightFootRotation", - "rightFootType"); + "rightFootType", "rightFootWeight", 1.0f, {}); AnimPose geometryToRig = _modelOffset * _geometryOffset; @@ -1440,7 +1452,7 @@ void Rig::computeAvatarBoundingCapsule( // call overlay twice: once to verify AnimPoseVec joints and again to do the IK AnimNode::Triggers triggersOut; - AnimContext context(false, glm::mat4()); + AnimContext context(false, false, glm::mat4(), glm::mat4()); float dt = 1.0f; // the value of this does not matter ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); AnimPoseVec finalPoses = ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2d024628f5..33b66f91ea 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -26,6 +26,7 @@ #include "SimpleMovingAverage.h" class Rig; +class AnimInverseKinematics; typedef std::shared_ptr RigPointer; // Rig instances are reentrant. @@ -111,6 +112,8 @@ public: void clearJointStates(); void clearJointAnimationPriority(int index); + std::shared_ptr getAnimInverseKinematicsNode() const; + void clearIKJointLimitHistory(); void setMaxHipsOffsetLength(float maxLength); float getMaxHipsOffsetLength() const; @@ -159,7 +162,7 @@ public: void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState); // Regardless of who started the animations or how many, update the joints. - void updateAnimations(float deltaTime, glm::mat4 rootTransform); + void updateAnimations(float deltaTime, const glm::mat4& rootTransform, const glm::mat4& rigToWorldTransform); // legacy void inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, @@ -228,6 +231,7 @@ public: const glm::mat4& getGeometryToRigTransform() const { return _geometryToRigTransform; } void setEnableDebugDrawIKTargets(bool enableDebugDrawIKTargets) { _enableDebugDrawIKTargets = enableDebugDrawIKTargets; } + void setEnableDebugDrawIKConstraints(bool enableDebugDrawIKConstraints) { _enableDebugDrawIKConstraints = enableDebugDrawIKConstraints; } // input assumed to be in rig space void computeHeadFromHMD(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut) const; @@ -338,6 +342,7 @@ protected: float _maxHipsOffsetLength { 1.0f }; bool _enableDebugDrawIKTargets { false }; + bool _enableDebugDrawIKConstraints { false }; private: QMap _stateHandlers; diff --git a/libraries/animation/src/RotationConstraint.h b/libraries/animation/src/RotationConstraint.h index 277e5293c6..e4a5334d41 100644 --- a/libraries/animation/src/RotationConstraint.h +++ b/libraries/animation/src/RotationConstraint.h @@ -38,6 +38,9 @@ public: /// \brief reset any remembered joint limit history virtual void clearHistory() {}; + /// \brief return the rotation that lies at the "center" of all the joint limits. + virtual glm::quat computeCenterRotation() const = 0; + protected: glm::quat _referenceRotation = glm::quat(); }; diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index 12d7e618e5..212343d4eb 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -15,6 +15,7 @@ #include #include #include +#include "AnimUtil.h" const float MIN_MINDOT = -0.999f; @@ -430,3 +431,33 @@ void SwingTwistConstraint::dynamicallyAdjustLimits(const glm::quat& rotation) { void SwingTwistConstraint::clearHistory() { _lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY; } + +glm::quat SwingTwistConstraint::computeCenterRotation() const { + const size_t NUM_TWIST_LIMITS = 2; + const size_t NUM_MIN_DOTS = getMinDots().size(); + std::vector swingLimits; + swingLimits.reserve(NUM_MIN_DOTS); + + glm::quat twistLimits[NUM_TWIST_LIMITS]; + if (_minTwist != _maxTwist) { + // to ensure that twists do not flip the center rotation, we devide twist angle by 2. + twistLimits[0] = glm::angleAxis(_minTwist / 2.0f, _referenceRotation * Vectors::UNIT_Y); + twistLimits[1] = glm::angleAxis(_maxTwist / 2.0f, _referenceRotation * Vectors::UNIT_Y); + } + const float D_THETA = TWO_PI / (NUM_MIN_DOTS - 1); + float theta = 0.0f; + for (size_t i = 0; i < NUM_MIN_DOTS - 1; i++, theta += D_THETA) { + // compute swing rotation from theta and phi angles. + float phi = acos(getMinDots()[i]); + float cos_phi = getMinDots()[i]; + float sin_phi = sinf(phi); + glm::vec3 swungAxis(sin_phi * cosf(theta), cos_phi, -sin_phi * sinf(theta)); + + // to ensure that swings > 90 degrees do not flip the center rotation, we devide phi / 2 + glm::quat swing = glm::angleAxis(phi / 2, glm::normalize(glm::cross(Vectors::UNIT_Y, swungAxis))); + swingLimits.push_back(swing); + } + glm::quat averageSwing = averageQuats(swingLimits.size(), &swingLimits[0]); + glm::quat averageTwist = averageQuats(2, twistLimits); + return averageSwing * averageTwist * _referenceRotation; +} diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index 06afd64232..a41664d353 100644 --- a/libraries/animation/src/SwingTwistConstraint.h +++ b/libraries/animation/src/SwingTwistConstraint.h @@ -58,7 +58,7 @@ public: virtual void dynamicallyAdjustLimits(const glm::quat& rotation) override; // for testing purposes - const std::vector& getMinDots() { return _swingLimitFunction.getMinDots(); } + const std::vector& getMinDots() const { return _swingLimitFunction.getMinDots(); } // SwingLimitFunction is an implementation of the constraint check described in the paper: // "The Parameterization of Joint Rotation with the Unit Quaternion" by Quang Liu and Edmond C. Prakash @@ -81,7 +81,7 @@ public: float getMinDot(float theta) const; // for testing purposes - const std::vector& getMinDots() { return _minDots; } + const std::vector& getMinDots() const { return _minDots; } private: // the limits are stored in a lookup table with cyclic boundary conditions @@ -99,6 +99,11 @@ public: void clearHistory() override; + virtual glm::quat computeCenterRotation() const override; + + float getMinTwist() const { return _minTwist; } + float getMaxTwist() const { return _maxTwist; } + private: float handleTwistBoundaryConditions(float twistAngle) const; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 680e9129aa..a4ac3feddc 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -76,42 +76,58 @@ using Mutex = std::mutex; using Lock = std::unique_lock; static Mutex _deviceMutex; -// background thread that continuously polls for device changes -class CheckDevicesThread : public QThread { +class BackgroundThread : public QThread { public: - const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; + BackgroundThread(AudioClient* client) : QThread((QObject*)client), _client(client) {} + virtual void join() = 0; +protected: + AudioClient* _client; +}; - CheckDevicesThread(AudioClient* audioClient) - : _audioClient(audioClient) { - } - - void beforeAboutToQuit() { - Lock lock(_checkDevicesMutex); - _quit = true; +// background thread continuously polling device changes +class CheckDevicesThread : public BackgroundThread { +public: + CheckDevicesThread(AudioClient* client) : BackgroundThread(client) {} + + void join() override { + _shouldQuit = true; + std::unique_lock lock(_joinMutex); + _joinCondition.wait(lock, [&]{ return !_isRunning; }); } +protected: void run() override { - while (true) { - { - Lock lock(_checkDevicesMutex); - if (_quit) { - break; - } - _audioClient->checkDevices(); - } + while (!_shouldQuit) { + _client->checkDevices(); + + const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS); } + std::lock_guard lock(_joinMutex); + _isRunning = false; + _joinCondition.notify_one(); } private: - AudioClient* _audioClient { nullptr }; - Mutex _checkDevicesMutex; - bool _quit { false }; + std::atomic _shouldQuit { false }; + bool _isRunning { true }; + std::mutex _joinMutex; + std::condition_variable _joinCondition; }; -void AudioInjectorsThread::prepare() { - _audio->prepareLocalAudioInjectors(); -} +// background thread buffering local injectors +class LocalInjectorsThread : public BackgroundThread { + Q_OBJECT +public: + LocalInjectorsThread(AudioClient* client) : BackgroundThread(client) {} + + void join() override { return; } + +private slots: + void prepare() { _client->prepareLocalAudioInjectors(); } +}; + +#include "AudioClient.moc" static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { for (int i = 0; i < numSamples/2; i++) { @@ -179,7 +195,6 @@ AudioClient::AudioClient() : _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), _localToOutputResampler(NULL), - _localAudioThread(this), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), @@ -210,13 +225,14 @@ AudioClient::AudioClient() : // start a thread to detect any device changes _checkDevicesThread = new CheckDevicesThread(this); - _checkDevicesThread->setObjectName("CheckDevices Thread"); + _checkDevicesThread->setObjectName("AudioClient CheckDevices Thread"); _checkDevicesThread->setPriority(QThread::LowPriority); _checkDevicesThread->start(); // start a thread to process local injectors - _localAudioThread.setObjectName("LocalAudio Thread"); - _localAudioThread.start(); + _localInjectorsThread = new LocalInjectorsThread(this); + _localInjectorsThread->setObjectName("AudioClient LocalInjectors Thread"); + _localInjectorsThread->start(); configureReverb(); @@ -231,18 +247,32 @@ AudioClient::AudioClient() : } AudioClient::~AudioClient() { - delete _checkDevicesThread; - stop(); if (_codec && _encoder) { _codec->releaseEncoder(_encoder); _encoder = nullptr; } } -void AudioClient::beforeAboutToQuit() { - static_cast(_checkDevicesThread)->beforeAboutToQuit(); +void AudioClient::customDeleter() { + deleteLater(); } +void AudioClient::cleanupBeforeQuit() { + // FIXME: this should be put in customDeleter, but there is still a reference to this when it is called, + // so this must be explicitly, synchronously stopped + + stop(); + + if (_checkDevicesThread) { + static_cast(_checkDevicesThread)->join(); + delete _checkDevicesThread; + } + + if (_localInjectorsThread) { + static_cast(_localInjectorsThread)->join(); + delete _localInjectorsThread; + } +} void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; @@ -769,7 +799,8 @@ QString AudioClient::getDefaultDeviceName(QAudio::Mode mode) { QVector AudioClient::getDeviceNames(QAudio::Mode mode) { QVector deviceNames; - foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { + const QList &availableDevice = getAvailableDevices(mode); + foreach(const QAudioDeviceInfo &audioDevice, availableDevice) { deviceNames << audioDevice.deviceName().trimmed(); } return deviceNames; @@ -1096,11 +1127,19 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { handleAudioInput(audioBuffer); } -void AudioClient::prepareLocalAudioInjectors() { +void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLock) { + bool doSynchronously = localAudioLock.operator bool(); + if (!localAudioLock) { + localAudioLock.reset(new Lock(_localAudioMutex)); + } + int samplesNeeded = std::numeric_limits::max(); while (samplesNeeded > 0) { - // unlock between every write to allow device switching - Lock lock(_localAudioMutex); + if (!doSynchronously) { + // unlock between every write to allow device switching + localAudioLock->unlock(); + localAudioLock->lock(); + } // in case of a device switch, consider bufferCapacity volatile across iterations if (_outputPeriod == 0) { @@ -1154,16 +1193,16 @@ void AudioClient::prepareLocalAudioInjectors() { } bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { - - QVector injectorsToRemove; - - // lock the injector vector - Lock lock(_injectorsMutex); - - if (_activeLocalAudioInjectors.size() == 0) { + // check the flag for injectors before attempting to lock + if (!_localInjectorsAvailable.load(std::memory_order_acquire)) { return false; } + // lock the injectors + Lock lock(_injectorsMutex); + + QVector injectorsToRemove; + memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); for (AudioInjector* injector : _activeLocalAudioInjectors) { @@ -1242,6 +1281,9 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { _activeLocalAudioInjectors.removeOne(injector); } + // update the flag + _localInjectorsAvailable.exchange(!_activeLocalAudioInjectors.empty(), std::memory_order_release); + return true; } @@ -1328,11 +1370,14 @@ bool AudioClient::outputLocalInjector(AudioInjector* injector) { // move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop()) injectorBuffer->setParent(nullptr); - injectorBuffer->moveToThread(&_localAudioThread); + injectorBuffer->moveToThread(_localInjectorsThread); + + // update the flag + _localInjectorsAvailable.exchange(true, std::memory_order_release); } else { qCDebug(audioclient) << "injector exists in active list already"; } - + return true; } else { @@ -1358,7 +1403,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn _audioInput->stop(); _inputDevice = NULL; - delete _audioInput; + _audioInput->deleteLater(); _audioInput = NULL; _numInputCallbackBytes = 0; @@ -1374,6 +1419,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn if (!inputDeviceInfo.isNull()) { qCDebug(audioclient) << "The audio input device " << inputDeviceInfo.deviceName() << "is available."; _inputAudioDeviceName = inputDeviceInfo.deviceName().trimmed(); + emit currentInputDeviceChanged(_inputAudioDeviceName); if (adjustedFormatForAudioDevice(inputDeviceInfo, _desiredInputFormat, _inputFormat)) { qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; @@ -1455,18 +1501,21 @@ void AudioClient::outputNotify() { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { bool supportedFormat = false; - Lock lock(_localAudioMutex); + Lock localAudioLock(_localAudioMutex); _localSamplesAvailable.exchange(0, std::memory_order_release); // cleanup any previously initialized device if (_audioOutput) { + _audioOutputIODevice.close(); _audioOutput->stop(); - delete _audioOutput; + //must be deleted in next eventloop cycle when its called from notify() + _audioOutput->deleteLater(); _audioOutput = NULL; _loopbackOutputDevice = NULL; - delete _loopbackAudioOutput; + //must be deleted in next eventloop cycle when its called from notify() + _loopbackAudioOutput->deleteLater(); _loopbackAudioOutput = NULL; delete[] _outputMixBuffer; @@ -1491,6 +1540,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice if (!outputDeviceInfo.isNull()) { qCDebug(audioclient) << "The audio output device " << outputDeviceInfo.deviceName() << "is available."; _outputAudioDeviceName = outputDeviceInfo.deviceName().trimmed(); + emit currentOutputDeviceChanged(_outputAudioDeviceName); if (adjustedFormatForAudioDevice(outputDeviceInfo, _desiredOutputFormat, _outputFormat)) { qCDebug(audioclient) << "The format to be used for audio output is" << _outputFormat; @@ -1525,14 +1575,23 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { if (state == QAudio::ActiveState) { // restrict device callback to _outputPeriod samples - _outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2; + _outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback may exceed reported period, so double it to avoid stutter + _outputPeriod *= 2; + _outputMixBuffer = new float[_outputPeriod]; _outputScratchBuffer = new int16_t[_outputPeriod]; // size local output mix buffer based on resampled network frame size - _networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - _localOutputMixBuffer = new float[_networkPeriod]; + int networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + _localOutputMixBuffer = new float[networkPeriod]; + + // local period should be at least twice the output period, + // in case two device reads happen before more data can be read (worst case) int localPeriod = _outputPeriod * 2; + // round up to an exact multiple of networkPeriod + localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod; + // this ensures lowest latency without stutter from underrun _localInjectorsStream.resizeForFrameSize(localPeriod); int bufferSize = _audioOutput->bufferSize(); @@ -1547,6 +1606,9 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice qCDebug(audioclient) << "local buffer (samples):" << localPeriod; disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); + + // unlock to avoid a deadlock with the device callback (which always succeeds this initialization) + localAudioLock.unlock(); } }); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); @@ -1685,12 +1747,24 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { int injectorSamplesPopped = 0; { bool append = networkSamplesPopped > 0; - // this does not require a lock as of the only two functions adding to _localSamplesAvailable (samples count): + // check the samples we have available locklessly; this is possible because only two functions add to the count: // - prepareLocalAudioInjectors will only increase samples count - // - switchOutputToAudioDevice will zero samples count - // stop the device, so that readData will exhaust the existing buffer or see a zeroed samples count - // and start the device, which can only see a zeroed samples count - samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire)); + // - switchOutputToAudioDevice will zero samples count, + // stop the device - so that readData will exhaust the existing buffer or see a zeroed samples count, + // and start the device - which can then only see a zeroed samples count + int samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); + + // if we do not have enough samples buffered despite having injectors, buffer them synchronously + if (samplesAvailable < samplesRequested && _audio->_localInjectorsAvailable.load(std::memory_order_acquire)) { + // try_to_lock, in case the device is being shut down already + std::unique_ptr localAudioLock(new Lock(_audio->_localAudioMutex, std::try_to_lock)); + if (localAudioLock->owns_lock()) { + _audio->prepareLocalAudioInjectors(std::move(localAudioLock)); + samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); + } + } + + samplesRequested = std::min(samplesRequested, samplesAvailable); if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); @@ -1698,7 +1772,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } // prepare injectors for the next callback - QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection); + QMetaObject::invokeMethod(_audio->_localInjectorsThread, "prepare", Qt::QueuedConnection); int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); int framesPopped = samplesPopped / AudioConstants::STEREO; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index aaedee7456..a1e56df33b 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -71,19 +71,6 @@ class QIODevice; class Transform; class NLPacket; -class AudioInjectorsThread : public QThread { - Q_OBJECT - -public: - AudioInjectorsThread(AudioClient* audio) : _audio(audio) {} - -public slots : - void prepare(); - -private: - AudioClient* _audio; -}; - class AudioClient : public AbstractAudioInterface, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY @@ -107,7 +94,6 @@ public: _audio(audio), _unfulfilledReads(0) {} void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); } - void stop() { close(); } qint64 readData(char * data, qint64 maxSize) override; qint64 writeData(const char * data, qint64 maxSize) override { return 0; } int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } @@ -158,7 +144,7 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - void checkDevices(); + bool outputLocalInjector(AudioInjector* injector) override; static const float CALLBACK_ACCELERATOR_RATIO; @@ -169,6 +155,7 @@ public: public slots: void start(); void stop(); + void cleanupBeforeQuit(); void handleAudioEnvironmentDataPacket(QSharedPointer message); void handleAudioDataPacket(QSharedPointer message); @@ -184,8 +171,6 @@ public slots: void audioMixerKilled(); void toggleMute(); - void beforeAboutToQuit(); - virtual void setIsStereoInput(bool stereo) override; void toggleAudioNoiseReduction() { _isNoiseGateEnabled = !_isNoiseGateEnabled; } @@ -198,8 +183,6 @@ public slots: int setOutputBufferSize(int numFrames, bool persist = true); - void prepareLocalAudioInjectors(); - bool outputLocalInjector(AudioInjector* injector) override; bool shouldLoopbackInjectors() override { return _shouldEchoToServer; } bool switchInputToAudioDevice(const QString& inputDeviceName); @@ -238,17 +221,23 @@ signals: void muteEnvironmentRequested(glm::vec3 position, float radius); + void currentOutputDeviceChanged(const QString& name); + void currentInputDeviceChanged(const QString& name); + protected: AudioClient(); ~AudioClient(); - virtual void customDeleter() override { - deleteLater(); - } + virtual void customDeleter() override; private: + friend class CheckDevicesThread; + friend class LocalInjectorsThread; + void outputFormatChanged(); void handleAudioInput(QByteArray& audioBuffer); + void checkDevices(); + void prepareLocalAudioInjectors(std::unique_ptr localAudioLock = nullptr); bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -295,8 +284,9 @@ private: AudioRingBuffer _inputRingBuffer; LocalInjectorsStream _localInjectorsStream; // In order to use _localInjectorsStream as a lock-free pipe, - // use it with a single producer/consumer, and track available samples + // use it with a single producer/consumer, and track available samples and injectors std::atomic _localSamplesAvailable { 0 }; + std::atomic _localInjectorsAvailable { false }; MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; @@ -337,19 +327,17 @@ private: // for network audio (used by network audio thread) int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; - // for local audio (used by audio injectors thread) - int _networkPeriod { 0 }; - float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; - int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; - float* _localOutputMixBuffer { NULL }; - AudioInjectorsThread _localAudioThread; - Mutex _localAudioMutex; - // for output audio (used by this thread) int _outputPeriod { 0 }; float* _outputMixBuffer { NULL }; int16_t* _outputScratchBuffer { NULL }; + // for local audio (used by audio injectors thread) + float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + float* _localOutputMixBuffer { NULL }; + Mutex _localAudioMutex; + AudioLimiter _audioLimiter; // Adds Reverb @@ -392,12 +380,13 @@ private: QString _selectedCodecName; Encoder* _encoder { nullptr }; // for outbound mic stream - QThread* _checkDevicesThread { nullptr }; - RateCounter<> _silentOutbound; RateCounter<> _audioOutbound; RateCounter<> _silentInbound; RateCounter<> _audioInbound; + + QThread* _checkDevicesThread { nullptr }; + QThread* _localInjectorsThread { nullptr }; }; diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index 2e4611cd4e..2e14b9956b 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -32,12 +32,12 @@ public: const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale, PacketType packetType, QString codecName = QString("")); -public slots: // threadsafe // moves injector->getLocalBuffer() to another thread (so removes its parent) // take care to delete it when ~AudioInjector, as parenting Qt semantics will not work virtual bool outputLocalInjector(AudioInjector* injector) = 0; +public slots: virtual bool shouldLoopbackInjectors() { return false; } virtual void setIsStereoInput(bool stereo) = 0; diff --git a/libraries/avatars-renderer/CMakeLists.txt b/libraries/avatars-renderer/CMakeLists.txt index b13bc0a4a6..2ac5e6766d 100644 --- a/libraries/avatars-renderer/CMakeLists.txt +++ b/libraries/avatars-renderer/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME avatars-renderer) AUTOSCRIBE_SHADER_LIB(gpu model render render-utils) setup_hifi_library(Widgets Network Script) -link_hifi_libraries(shared gpu model animation physics model-networking script-engine render image render-utils) +link_hifi_libraries(shared gpu model animation model-networking script-engine render image render-utils) target_bullet() diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index be55653f64..1968a731a4 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -338,54 +338,28 @@ void Avatar::simulate(float deltaTime, bool inView) { _simulationInViewRate.increment(); } - if (!isMyAvatar()) { - if (_smoothPositionTimer < _smoothPositionTime) { - // Smooth the remote avatar movement. - _smoothPositionTimer += deltaTime; - if (_smoothPositionTimer < _smoothPositionTime) { - AvatarData::setPosition( - lerp(_smoothPositionInitial, - _smoothPositionTarget, - easeInOutQuad(glm::clamp(_smoothPositionTimer / _smoothPositionTime, 0.0f, 1.0f))) - ); - updateAttitude(); - } - } - - if (_smoothOrientationTimer < _smoothOrientationTime) { - // Smooth the remote avatar movement. - _smoothOrientationTimer += deltaTime; - if (_smoothOrientationTimer < _smoothOrientationTime) { - AvatarData::setOrientation( - slerp(_smoothOrientationInitial, - _smoothOrientationTarget, - easeInOutQuad(glm::clamp(_smoothOrientationTimer / _smoothOrientationTime, 0.0f, 1.0f))) - ); - updateAttitude(); - } - } - } - PerformanceTimer perfTimer("simulate"); { PROFILE_RANGE(simulation, "updateJoints"); - if (inView && _hasNewJointData) { - _skeletonModel->getRig()->copyJointsFromJointData(_jointData); - glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset()); - _skeletonModel->getRig()->computeExternalPoses(rootTransform); - _jointDataSimulationRate.increment(); - - _skeletonModel->simulate(deltaTime, true); - - locationChanged(); // joints changed, so if there are any children, update them. - _hasNewJointData = false; - - glm::vec3 headPosition = getPosition(); - if (!_skeletonModel->getHeadPosition(headPosition)) { - headPosition = getPosition(); - } + if (inView) { Head* head = getHead(); - head->setPosition(headPosition); + if (_hasNewJointData) { + _skeletonModel->getRig()->copyJointsFromJointData(_jointData); + glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset()); + _skeletonModel->getRig()->computeExternalPoses(rootTransform); + _jointDataSimulationRate.increment(); + + _skeletonModel->simulate(deltaTime, true); + + locationChanged(); // joints changed, so if there are any children, update them. + _hasNewJointData = false; + + glm::vec3 headPosition = getPosition(); + if (!_skeletonModel->getHeadPosition(headPosition)) { + headPosition = getPosition(); + } + head->setPosition(headPosition); + } head->setScale(getUniformScale()); head->simulate(deltaTime); } else { @@ -1291,6 +1265,17 @@ void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { radius = halfExtents.x; } +float Avatar::computeMass() { + float radius; + glm::vec3 start, end; + getCapsule(start, end, radius); + // NOTE: + // volumeOfCapsule = volumeOfCylinder + volumeOfSphere + // volumeOfCapsule = (2PI * R^2 * H) + (4PI * R^3 / 3) + // volumeOfCapsule = 2PI * R^2 * (H + 2R/3) + return _density * TWO_PI * radius * radius * (glm::length(end - start) + 2.0f * radius / 3.0f); +} + // virtual void Avatar::rebuildCollisionShape() { addPhysicsFlags(Simulation::DIRTY_SHAPE); @@ -1369,31 +1354,13 @@ glm::quat Avatar::getUncachedRightPalmRotation() const { } void Avatar::setPosition(const glm::vec3& position) { - if (isMyAvatar()) { - // This is the local avatar, no need to handle any position smoothing. - AvatarData::setPosition(position); - updateAttitude(); - return; - } - - // Whether or not there is an existing smoothing going on, just reset the smoothing timer and set the starting position as the avatar's current position, then smooth to the new position. - _smoothPositionInitial = getPosition(); - _smoothPositionTarget = position; - _smoothPositionTimer = 0.0f; + AvatarData::setPosition(position); + updateAttitude(); } void Avatar::setOrientation(const glm::quat& orientation) { - if (isMyAvatar()) { - // This is the local avatar, no need to handle any position smoothing. - AvatarData::setOrientation(orientation); - updateAttitude(); - return; - } - - // Whether or not there is an existing smoothing going on, just reset the smoothing timer and set the starting position as the avatar's current position, then smooth to the new position. - _smoothOrientationInitial = getOrientation(); - _smoothOrientationTarget = orientation; - _smoothOrientationTimer = 0.0f; + AvatarData::setOrientation(orientation); + updateAttitude(); } void Avatar::updatePalms() { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 20704a08b2..ae24caca29 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -36,7 +36,6 @@ namespace render { } static const float SCALING_RATIO = .05f; -static const float SMOOTHING_RATIO = .05f; // 0 < ratio < 1 extern const float CHAT_MESSAGE_SCALE; extern const float CHAT_MESSAGE_HEIGHT; @@ -196,6 +195,7 @@ public: virtual void computeShapeInfo(ShapeInfo& shapeInfo); void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); + float computeMass(); using SpatiallyNestable::setPosition; virtual void setPosition(const glm::vec3& position) override; @@ -239,17 +239,8 @@ public: bool hasNewJointData() const { return _hasNewJointData; } - inline float easeInOutQuad(float lerpValue) { - assert(!((lerpValue < 0.0f) || (lerpValue > 1.0f))); - - if (lerpValue < 0.5f) { - return (2.0f * lerpValue * lerpValue); - } - - return (lerpValue*(4.0f - 2.0f * lerpValue) - 1.0f); - } float getBoundingRadius() const; - + void addToScene(AvatarSharedPointer self, const render::ScenePointer& scene); void ensureInScene(AvatarSharedPointer self, const render::ScenePointer& scene); bool isInScene() const { return render::Item::isValidID(_renderItemID); } @@ -271,9 +262,6 @@ public slots: void setModelURLFinished(bool success); protected: - const float SMOOTH_TIME_POSITION = 0.125f; - const float SMOOTH_TIME_ORIENTATION = 0.075f; - virtual const QString& getSessionDisplayNameForTransport() const override { return _empty; } // Save a tiny bit of bandwidth. Mixer won't look at what we send. QString _empty{}; virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) override { _sessionDisplayName = sessionDisplayName; } // don't use no-op setter! @@ -336,16 +324,6 @@ protected: RateCounter<> _skeletonModelSimulationRate; RateCounter<> _jointDataSimulationRate; - // Smoothing data for blending from one position/orientation to another on remote agents. - float _smoothPositionTime { SMOOTH_TIME_POSITION }; - float _smoothPositionTimer { std::numeric_limits::max() }; - float _smoothOrientationTime { SMOOTH_TIME_ORIENTATION }; - float _smoothOrientationTimer { std::numeric_limits::max() }; - glm::vec3 _smoothPositionInitial; - glm::vec3 _smoothPositionTarget; - glm::quat _smoothOrientationInitial; - glm::quat _smoothOrientationTarget; - private: class AvatarEntityDataHash { public: diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp index a90c9cd5f7..b4b0929c0c 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp @@ -23,9 +23,10 @@ #include "Avatar.h" +const float NORMAL_HZ = 60.0f; // the update rate the constant values were tuned for + using namespace std; -static bool fixGaze { false }; static bool disableEyelidAdjustment { false }; Head::Head(Avatar* owningAvatar) : @@ -42,17 +43,11 @@ void Head::reset() { _baseYaw = _basePitch = _baseRoll = 0.0f; } -void Head::simulate(float deltaTime) { - const float NORMAL_HZ = 60.0f; // the update rate the constant values were tuned for - +void Head::computeAudioLoudness(float deltaTime) { // grab the audio loudness from the owning avatar, if we have one - float audioLoudness = 0.0f; + float audioLoudness = _owningAvatar ? _owningAvatar->getAudioLoudness() : 0.0f; - if (_owningAvatar) { - audioLoudness = _owningAvatar->getAudioLoudness(); - } - - // Update audio trailing average for rendering facial animations + // Update audio trailing average for rendering facial animations const float AUDIO_AVERAGING_SECS = 0.05f; const float AUDIO_LONG_TERM_AVERAGING_SECS = 30.0f; _averageLoudness = glm::mix(_averageLoudness, audioLoudness, glm::min(deltaTime / AUDIO_AVERAGING_SECS, 1.0f)); @@ -63,117 +58,114 @@ void Head::simulate(float deltaTime) { _longTermAverageLoudness = glm::mix(_longTermAverageLoudness, _averageLoudness, glm::min(deltaTime / AUDIO_LONG_TERM_AVERAGING_SECS, 1.0f)); } - if (!_isFaceTrackerConnected) { - if (!_isEyeTrackerConnected) { - // Update eye saccades - const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; - const float AVERAGE_SACCADE_INTERVAL = 6.0f; - const float MICROSACCADE_MAGNITUDE = 0.002f; - const float SACCADE_MAGNITUDE = 0.04f; - const float NOMINAL_FRAME_RATE = 60.0f; + float audioAttackAveragingRate = (10.0f - deltaTime * NORMAL_HZ) / 10.0f; // --> 0.9 at 60 Hz + _audioAttack = audioAttackAveragingRate * _audioAttack + + (1.0f - audioAttackAveragingRate) * fabs((audioLoudness - _longTermAverageLoudness) - _lastLoudness); + _lastLoudness = (audioLoudness - _longTermAverageLoudness); +} - if (randFloat() < deltaTime / AVERAGE_MICROSACCADE_INTERVAL) { - _saccadeTarget = MICROSACCADE_MAGNITUDE * randVector(); - } else if (randFloat() < deltaTime / AVERAGE_SACCADE_INTERVAL) { - _saccadeTarget = SACCADE_MAGNITUDE * randVector(); - } - _saccade += (_saccadeTarget - _saccade) * pow(0.5f, NOMINAL_FRAME_RATE * deltaTime); - } else { - _saccade = glm::vec3(); - } +void Head::computeEyeMovement(float deltaTime) { + // Update eye saccades + const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; + const float AVERAGE_SACCADE_INTERVAL = 6.0f; + const float MICROSACCADE_MAGNITUDE = 0.002f; + const float SACCADE_MAGNITUDE = 0.04f; + const float NOMINAL_FRAME_RATE = 60.0f; - // Detect transition from talking to not; force blink after that and a delay - bool forceBlink = false; - const float TALKING_LOUDNESS = 100.0f; - const float BLINK_AFTER_TALKING = 0.25f; - _timeWithoutTalking += deltaTime; - if ((_averageLoudness - _longTermAverageLoudness) > TALKING_LOUDNESS) { - _timeWithoutTalking = 0.0f; + if (randFloat() < deltaTime / AVERAGE_MICROSACCADE_INTERVAL) { + _saccadeTarget = MICROSACCADE_MAGNITUDE * randVector(); + } else if (randFloat() < deltaTime / AVERAGE_SACCADE_INTERVAL) { + _saccadeTarget = SACCADE_MAGNITUDE * randVector(); + } + _saccade += (_saccadeTarget - _saccade) * pow(0.5f, NOMINAL_FRAME_RATE * deltaTime); - } else if (_timeWithoutTalking < BLINK_AFTER_TALKING && _timeWithoutTalking >= BLINK_AFTER_TALKING) { - forceBlink = true; - } + // Detect transition from talking to not; force blink after that and a delay + bool forceBlink = false; + const float TALKING_LOUDNESS = 100.0f; + const float BLINK_AFTER_TALKING = 0.25f; + _timeWithoutTalking += deltaTime; + if ((_averageLoudness - _longTermAverageLoudness) > TALKING_LOUDNESS) { + _timeWithoutTalking = 0.0f; + } else if (_timeWithoutTalking - deltaTime < BLINK_AFTER_TALKING && _timeWithoutTalking >= BLINK_AFTER_TALKING) { + forceBlink = true; + } - // Update audio attack data for facial animation (eyebrows and mouth) - float audioAttackAveragingRate = (10.0f - deltaTime * NORMAL_HZ) / 10.0f; // --> 0.9 at 60 Hz - _audioAttack = audioAttackAveragingRate * _audioAttack + - (1.0f - audioAttackAveragingRate) * fabs((audioLoudness - _longTermAverageLoudness) - _lastLoudness); - _lastLoudness = (audioLoudness - _longTermAverageLoudness); - - const float BROW_LIFT_THRESHOLD = 100.0f; - if (_audioAttack > BROW_LIFT_THRESHOLD) { - _browAudioLift += sqrtf(_audioAttack) * 0.01f; - } - _browAudioLift = glm::clamp(_browAudioLift *= 0.7f, 0.0f, 1.0f); - - const float BLINK_SPEED = 10.0f; - const float BLINK_SPEED_VARIABILITY = 1.0f; - const float BLINK_START_VARIABILITY = 0.25f; - const float FULLY_OPEN = 0.0f; - const float FULLY_CLOSED = 1.0f; - if (_leftEyeBlinkVelocity == 0.0f && _rightEyeBlinkVelocity == 0.0f) { - // no blinking when brows are raised; blink less with increasing loudness - const float BASE_BLINK_RATE = 15.0f / 60.0f; - const float ROOT_LOUDNESS_TO_BLINK_INTERVAL = 0.25f; - if (forceBlink || (_browAudioLift < EPSILON && shouldDo(glm::max(1.0f, sqrt(fabs(_averageLoudness - _longTermAverageLoudness)) * - ROOT_LOUDNESS_TO_BLINK_INTERVAL) / BASE_BLINK_RATE, deltaTime))) { - _leftEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; - _rightEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; - if (randFloat() < 0.5f) { - _leftEyeBlink = BLINK_START_VARIABILITY; - } else { - _rightEyeBlink = BLINK_START_VARIABILITY; - } - } - } else { - _leftEyeBlink = glm::clamp(_leftEyeBlink + _leftEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); - _rightEyeBlink = glm::clamp(_rightEyeBlink + _rightEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); - - if (_leftEyeBlink == FULLY_CLOSED) { - _leftEyeBlinkVelocity = -BLINK_SPEED; - - } else if (_leftEyeBlink == FULLY_OPEN) { - _leftEyeBlinkVelocity = 0.0f; - } - if (_rightEyeBlink == FULLY_CLOSED) { - _rightEyeBlinkVelocity = -BLINK_SPEED; - - } else if (_rightEyeBlink == FULLY_OPEN) { - _rightEyeBlinkVelocity = 0.0f; + const float BLINK_SPEED = 10.0f; + const float BLINK_SPEED_VARIABILITY = 1.0f; + const float BLINK_START_VARIABILITY = 0.25f; + const float FULLY_OPEN = 0.0f; + const float FULLY_CLOSED = 1.0f; + if (_leftEyeBlinkVelocity == 0.0f && _rightEyeBlinkVelocity == 0.0f) { + // no blinking when brows are raised; blink less with increasing loudness + const float BASE_BLINK_RATE = 15.0f / 60.0f; + const float ROOT_LOUDNESS_TO_BLINK_INTERVAL = 0.25f; + if (forceBlink || (_browAudioLift < EPSILON && shouldDo(glm::max(1.0f, sqrt(fabs(_averageLoudness - _longTermAverageLoudness)) * + ROOT_LOUDNESS_TO_BLINK_INTERVAL) / BASE_BLINK_RATE, deltaTime))) { + _leftEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; + _rightEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; + if (randFloat() < 0.5f) { + _leftEyeBlink = BLINK_START_VARIABILITY; + } else { + _rightEyeBlink = BLINK_START_VARIABILITY; } } - - // use data to update fake Faceshift blendshape coefficients - calculateMouthShapes(deltaTime); - FaceTracker::updateFakeCoefficients(_leftEyeBlink, - _rightEyeBlink, - _browAudioLift, - _audioJawOpen, - _mouth2, - _mouth3, - _mouth4, - _blendshapeCoefficients); - - applyEyelidOffset(getOrientation()); - } else { - _saccade = glm::vec3(); - } - if (fixGaze) { // if debug menu turns off, use no saccade - _saccade = glm::vec3(); + _leftEyeBlink = glm::clamp(_leftEyeBlink + _leftEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); + _rightEyeBlink = glm::clamp(_rightEyeBlink + _rightEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); + + if (_leftEyeBlink == FULLY_CLOSED) { + _leftEyeBlinkVelocity = -BLINK_SPEED; + + } else if (_leftEyeBlink == FULLY_OPEN) { + _leftEyeBlinkVelocity = 0.0f; + } + if (_rightEyeBlink == FULLY_CLOSED) { + _rightEyeBlinkVelocity = -BLINK_SPEED; + + } else if (_rightEyeBlink == FULLY_OPEN) { + _rightEyeBlinkVelocity = 0.0f; + } } + applyEyelidOffset(getOrientation()); +} + +void Head::computeFaceMovement(float deltaTime) { + // Update audio attack data for facial animation (eyebrows and mouth) + const float BROW_LIFT_THRESHOLD = 100.0f; + if (_audioAttack > BROW_LIFT_THRESHOLD) { + _browAudioLift += sqrtf(_audioAttack) * 0.01f; + } + _browAudioLift = glm::clamp(_browAudioLift *= 0.7f, 0.0f, 1.0f); + + // use data to update fake Faceshift blendshape coefficients + calculateMouthShapes(deltaTime); + FaceTracker::updateFakeCoefficients(_leftEyeBlink, + _rightEyeBlink, + _browAudioLift, + _audioJawOpen, + _mouth2, + _mouth3, + _mouth4, + _transientBlendshapeCoefficients); +} + +void Head::computeEyePosition() { _leftEyePosition = _rightEyePosition = getPosition(); - _eyePosition = getPosition(); - if (_owningAvatar) { auto skeletonModel = static_cast(_owningAvatar)->getSkeletonModel(); if (skeletonModel) { skeletonModel->getEyePositions(_leftEyePosition, _rightEyePosition); } } + _eyePosition = 0.5f * (_leftEyePosition + _rightEyePosition); +} - _eyePosition = calculateAverageEyePosition(); +void Head::simulate(float deltaTime) { + computeAudioLoudness(deltaTime); + computeFaceMovement(deltaTime); + computeEyeMovement(deltaTime); + computeEyePosition(); } void Head::calculateMouthShapes(float deltaTime) { @@ -203,6 +195,13 @@ void Head::calculateMouthShapes(float deltaTime) { float trailingAudioJawOpenRatio = (100.0f - deltaTime * NORMAL_HZ) / 100.0f; // --> 0.99 at 60 Hz _trailingAudioJawOpen = glm::mix(_trailingAudioJawOpen, _audioJawOpen, trailingAudioJawOpenRatio); + // truncate _mouthTime when mouth goes quiet to prevent floating point error on increment + const float SILENT_TRAILING_JAW_OPEN = 0.0002f; + const float MAX_SILENT_MOUTH_TIME = 10.0f; + if (_trailingAudioJawOpen < SILENT_TRAILING_JAW_OPEN && _mouthTime > MAX_SILENT_MOUTH_TIME) { + _mouthTime = 0.0f; + } + // Advance time at a rate proportional to loudness, and move the mouth shapes through // a cycle at differing speeds to create a continuous random blend of shapes. _mouthTime += sqrtf(_averageLoudness) * TIMESTEP_CONSTANT * deltaTimeRatio; @@ -228,15 +227,15 @@ void Head::applyEyelidOffset(glm::quat headOrientation) { for (int i = 0; i < 2; i++) { const int LEFT_EYE = 8; - float eyeCoefficient = _blendshapeCoefficients[i] - _blendshapeCoefficients[LEFT_EYE + i]; // Raw value + float eyeCoefficient = _transientBlendshapeCoefficients[i] - _transientBlendshapeCoefficients[LEFT_EYE + i]; eyeCoefficient = glm::clamp(eyelidOffset + eyeCoefficient * (1.0f - eyelidOffset), -1.0f, 1.0f); if (eyeCoefficient > 0.0f) { - _blendshapeCoefficients[i] = eyeCoefficient; - _blendshapeCoefficients[LEFT_EYE + i] = 0.0f; + _transientBlendshapeCoefficients[i] = eyeCoefficient; + _transientBlendshapeCoefficients[LEFT_EYE + i] = 0.0f; } else { - _blendshapeCoefficients[i] = 0.0f; - _blendshapeCoefficients[LEFT_EYE + i] = -eyeCoefficient; + _transientBlendshapeCoefficients[i] = 0.0f; + _transientBlendshapeCoefficients[LEFT_EYE + i] = -eyeCoefficient; } } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.h b/libraries/avatars-renderer/src/avatars-renderer/Head.h index aea6a41528..39331500b5 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.h @@ -83,7 +83,10 @@ public: float getTimeWithoutTalking() const { return _timeWithoutTalking; } protected: - glm::vec3 calculateAverageEyePosition() const { return _leftEyePosition + (_rightEyePosition - _leftEyePosition ) * 0.5f; } + void computeAudioLoudness(float deltaTime); + void computeEyeMovement(float deltaTime); + void computeFaceMovement(float deltaTime); + void computeEyePosition(); // disallow copies of the Head, copy of owning Avatar is disallowed too Head(const Head&); diff --git a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.h b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.h index 2f6c9a38aa..22a7e1863a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.h @@ -14,7 +14,7 @@ class OtherAvatar : public Avatar { public: explicit OtherAvatar(QThread* thread, RigPointer rig = nullptr); - void instantiableAvatar() {}; + virtual void instantiableAvatar() override {}; }; #endif // hifi_OtherAvatar_h diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index e1e5dc4282..d3453280ac 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -73,12 +73,13 @@ void SkeletonModel::initJointStates() { // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { + assert(!_owningAvatar->isMyAvatar()); const FBXGeometry& geometry = getFBXGeometry(); Head* head = _owningAvatar->getHead(); // make sure lookAt is not too close to face (avoid crosseyes) - glm::vec3 lookAt = _owningAvatar->isMyAvatar() ? head->getLookAtPosition() : head->getCorrectedLookAtPosition(); + glm::vec3 lookAt = head->getCorrectedLookAtPosition(); glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); float focusDistance = glm::length(focusOffset); const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; @@ -86,41 +87,36 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; } - if (!_owningAvatar->isMyAvatar()) { - // no need to call Model::updateRig() because otherAvatars get their joint state - // copied directly from AvtarData::_jointData (there are no Rig animations to blend) - _needsUpdateClusterMatrices = true; + // no need to call Model::updateRig() because otherAvatars get their joint state + // copied directly from AvtarData::_jointData (there are no Rig animations to blend) + _needsUpdateClusterMatrices = true; - // This is a little more work than we really want. - // - // Other avatars joint, including their eyes, should already be set just like any other joints - // from the wire data. But when looking at me, we want the eyes to use the corrected lookAt. - // - // Thus this should really only be ... else if (_owningAvatar->getHead()->isLookingAtMe()) {... - // However, in the !isLookingAtMe case, the eyes aren't rotating the way they should right now. - // We will revisit that as priorities allow, and particularly after the new rig/animation/joints. + // This is a little more work than we really want. + // + // Other avatars joint, including their eyes, should already be set just like any other joints + // from the wire data. But when looking at me, we want the eyes to use the corrected lookAt. + // + // Thus this should really only be ... else if (_owningAvatar->getHead()->isLookingAtMe()) {... + // However, in the !isLookingAtMe case, the eyes aren't rotating the way they should right now. + // We will revisit that as priorities allow, and particularly after the new rig/animation/joints. - // If the head is not positioned, updateEyeJoints won't get the math right - glm::quat headOrientation; - _rig->getJointRotation(geometry.headJointIndex, headOrientation); - glm::vec3 eulers = safeEulerAngles(headOrientation); - head->setBasePitch(glm::degrees(-eulers.x)); - head->setBaseYaw(glm::degrees(eulers.y)); - head->setBaseRoll(glm::degrees(-eulers.z)); + // If the head is not positioned, updateEyeJoints won't get the math right + glm::quat headOrientation; + _rig->getJointRotation(geometry.headJointIndex, headOrientation); + glm::vec3 eulers = safeEulerAngles(headOrientation); + head->setBasePitch(glm::degrees(-eulers.x)); + head->setBaseYaw(glm::degrees(eulers.y)); + head->setBaseRoll(glm::degrees(-eulers.z)); - Rig::EyeParameters eyeParams; - eyeParams.eyeLookAt = lookAt; - eyeParams.eyeSaccade = glm::vec3(0.0f); - eyeParams.modelRotation = getRotation(); - eyeParams.modelTranslation = getTranslation(); - eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex; - eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex; + Rig::EyeParameters eyeParams; + eyeParams.eyeLookAt = lookAt; + eyeParams.eyeSaccade = glm::vec3(0.0f); + eyeParams.modelRotation = getRotation(); + eyeParams.modelTranslation = getTranslation(); + eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex; + eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex; - _rig->updateFromEyeParameters(eyeParams); - } - - // evaluate AnimGraph animation and update jointStates. - Parent::updateRig(deltaTime, parentTransform); + _rig->updateFromEyeParameters(eyeParams); } void SkeletonModel::updateAttitude() { diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index cb819c6b20..3aa5ab69fa 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -52,6 +52,7 @@ const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData"; static const int TRANSLATION_COMPRESSION_RADIX = 12; static const int SENSOR_TO_WORLD_SCALE_RADIX = 10; static const float AUDIO_LOUDNESS_SCALE = 1024.0f; +static const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water #define ASSERT(COND) do { if (!(COND)) { abort(); } } while(0) @@ -65,7 +66,8 @@ AvatarData::AvatarData() : _headData(NULL), _errorLogExpiry(0), _owningAvatarMixer(), - _targetVelocity(0.0f) + _targetVelocity(0.0f), + _density(DEFAULT_AVATAR_DENSITY) { setBodyPitch(0.0f); setBodyYaw(-90.0f); @@ -170,13 +172,13 @@ QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail) { AvatarDataPacket::HasFlags hasFlagsOut; auto lastSentTime = _lastToByteArray; _lastToByteArray = usecTimestampNow(); - return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), + return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), hasFlagsOut, false, false, glm::vec3(0), nullptr, &_outboundDataRate); } QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, + AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, QVector* sentJointDataOut, AvatarDataRate* outboundDataRateOut) const { bool cullSmallChanges = (dataDetail == CullSmallData); @@ -199,7 +201,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // FIXME - // - // BUG -- if you enter a space bubble, and then back away, the avatar has wrong orientation until "send all" happens... + // BUG -- if you enter a space bubble, and then back away, the avatar has wrong orientation until "send all" happens... // this is an iFrame issue... what to do about that? // // BUG -- Resizing avatar seems to "take too long"... the avatar doesn't redraw at smaller size right away @@ -310,7 +312,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (hasAvatarOrientation) { auto startSection = destinationBuffer; - auto localOrientation = getLocalOrientation(); + auto localOrientation = getOrientationOutbound(); destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, localOrientation); int numBytes = destinationBuffer - startSection; @@ -445,17 +447,17 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (hasFaceTrackerInfo) { auto startSection = destinationBuffer; auto faceTrackerInfo = reinterpret_cast(destinationBuffer); + const auto& blendshapeCoefficients = _headData->getSummedBlendshapeCoefficients(); faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink; faceTrackerInfo->rightEyeBlink = _headData->_rightEyeBlink; faceTrackerInfo->averageLoudness = _headData->_averageLoudness; faceTrackerInfo->browAudioLift = _headData->_browAudioLift; - faceTrackerInfo->numBlendshapeCoefficients = _headData->_blendshapeCoefficients.size(); + faceTrackerInfo->numBlendshapeCoefficients = blendshapeCoefficients.size(); destinationBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo); - // followed by a variable number of float coefficients - memcpy(destinationBuffer, _headData->_blendshapeCoefficients.data(), _headData->_blendshapeCoefficients.size() * sizeof(float)); - destinationBuffer += _headData->_blendshapeCoefficients.size() * sizeof(float); + memcpy(destinationBuffer, blendshapeCoefficients.data(), blendshapeCoefficients.size() * sizeof(float)); + destinationBuffer += blendshapeCoefficients.size() * sizeof(float); int numBytes = destinationBuffer - startSection; if (outboundDataRateOut) { @@ -965,7 +967,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { const int coefficientsSize = sizeof(float) * numCoefficients; PACKET_READ_CHECK(FaceTrackerCoefficients, coefficientsSize); _headData->_blendshapeCoefficients.resize(numCoefficients); // make sure there's room for the copy! - _headData->_baseBlendshapeCoefficients.resize(numCoefficients); + _headData->_transientBlendshapeCoefficients.resize(numCoefficients); memcpy(_headData->_blendshapeCoefficients.data(), sourceBuffer, coefficientsSize); sourceBuffer += coefficientsSize; int numBytesRead = sourceBuffer - startSection; @@ -1471,13 +1473,13 @@ QStringList AvatarData::getJointNames() const { void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut) { QDataStream packetStream(data); - packetStream >> identityOut.uuid - >> identityOut.skeletonModelURL + packetStream >> identityOut.uuid + >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName >> identityOut.sessionDisplayName >> identityOut.avatarEntityData - >> identityOut.updatedAt; + >> identityOut.sequenceId; #ifdef WANT_DEBUG qCDebug(avatars) << __FUNCTION__ @@ -1489,6 +1491,10 @@ void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& ide } +glm::quat AvatarData::getOrientationOutbound() const { + return (getLocalOrientation()); +} + static const QUrl emptyURL(""); QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const { // We don't put file urls on the wire, but instead convert to empty. @@ -1497,11 +1503,13 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const { void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged) { - if (identity.updatedAt < _identityUpdatedAt) { - qCDebug(avatars) << "Ignoring late identity packet for avatar " << getSessionUUID() - << "identity.updatedAt:" << identity.updatedAt << "_identityUpdatedAt:" << _identityUpdatedAt; + if (identity.sequenceId < _identitySequenceId) { + qCDebug(avatars) << "Ignoring older identity packet for avatar" << getSessionUUID() + << "_identitySequenceId (" << _identitySequenceId << ") is greater than" << identity.sequenceId; return; } + // otherwise, set the identitySequenceId to match the incoming identity + _identitySequenceId = identity.sequenceId; if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) { setSkeletonModelURL(identity.skeletonModelURL); @@ -1533,9 +1541,6 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC identityChanged = true; } - // use the timestamp from this identity, since we want to honor the updated times in "server clock" - // this will overwrite any changes we made locally to this AvatarData's _identityUpdatedAt - _identityUpdatedAt = identity.updatedAt; } QByteArray AvatarData::identityByteArray() const { @@ -1544,13 +1549,13 @@ QByteArray AvatarData::identityByteArray() const { const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); // depends on _skeletonModelURL _avatarEntitiesLock.withReadLock([&] { - identityStream << getSessionUUID() - << urlToSend + identityStream << getSessionUUID() + << urlToSend << _attachmentData << _displayName << getSessionDisplayNameForTransport() // depends on _sessionDisplayName << _avatarEntityData - << _identityUpdatedAt; + << _identitySequenceId; }); return identityData; @@ -1576,8 +1581,6 @@ void AvatarData::setDisplayName(const QString& displayName) { _displayName = displayName; _sessionDisplayName = ""; - sendIdentityPacket(); - qCDebug(avatars) << "Changing display name for avatar to" << displayName; markIdentityDataChanged(); } @@ -2044,11 +2047,13 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { setSkeletonModelURL(bodyModelURL); } } + + QString newDisplayName = ""; if (json.contains(JSON_AVATAR_DISPLAY_NAME)) { - auto newDisplayName = json[JSON_AVATAR_DISPLAY_NAME].toString(); - if (newDisplayName != getDisplayName()) { - setDisplayName(newDisplayName); - } + newDisplayName = json[JSON_AVATAR_DISPLAY_NAME].toString(); + } + if (newDisplayName != getDisplayName()) { + setDisplayName(newDisplayName); } auto currentBasis = getRecordingBasis(); @@ -2078,14 +2083,16 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { setTargetScale((float)json[JSON_AVATAR_SCALE].toDouble()); } + QVector attachments; if (json.contains(JSON_AVATAR_ATTACHMENTS) && json[JSON_AVATAR_ATTACHMENTS].isArray()) { QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHMENTS].toArray(); - QVector attachments; for (auto attachmentJson : attachmentsJson) { AttachmentData attachment; attachment.fromJson(attachmentJson.toObject()); attachments.push_back(attachment); } + } + if (attachments != getAttachmentData()) { setAttachmentData(attachments); } @@ -2461,11 +2468,11 @@ QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEnt if (!jsonEntityProperties.isObject()) { qCDebug(avatars) << "bad AvatarEntityData in AvatarEntityMap" << QString(entityProperties.toHex()); } - + QVariant variantEntityProperties = jsonEntityProperties.toVariant(); QVariantMap entityPropertiesMap = variantEntityProperties.toMap(); QScriptValue scriptEntityProperties = variantMapToScriptValue(entityPropertiesMap, *engine); - + QString key = entityID.toString(); obj.setProperty(key, scriptEntityProperties); } @@ -2477,12 +2484,12 @@ void AvatarEntityMapFromScriptValue(const QScriptValue& object, AvatarEntityMap& while (itr.hasNext()) { itr.next(); QUuid EntityID = QUuid(itr.name()); - + QScriptValue scriptEntityProperties = itr.value(); QVariant variantEntityProperties = scriptEntityProperties.toVariant(); QJsonDocument jsonEntityProperties = QJsonDocument::fromVariant(variantEntityProperties); QByteArray binaryEntityProperties = jsonEntityProperties.toBinaryData(); - + value[EntityID] = binaryEntityProperties; } } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 6d801793b7..4104615cfe 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -165,15 +165,15 @@ namespace AvatarDataPacket { const size_t AVATAR_ORIENTATION_SIZE = 6; PACKED_BEGIN struct AvatarScale { - SmallFloat scale; // avatar's scale, compressed by packFloatRatioToTwoByte() + SmallFloat scale; // avatar's scale, compressed by packFloatRatioToTwoByte() } PACKED_END; const size_t AVATAR_SCALE_SIZE = 2; PACKED_BEGIN struct LookAtPosition { float lookAtPosition[3]; // world space position that eyes are focusing on. - // FIXME - unless the person has an eye tracker, this is simulated... + // FIXME - unless the person has an eye tracker, this is simulated... // a) maybe we can just have the client calculate this - // b) at distance this will be hard to discern and can likely be + // b) at distance this will be hard to discern and can likely be // descimated or dropped completely // // POTENTIAL SAVINGS - 12 bytes @@ -376,10 +376,10 @@ public: glm::vec3 getHandPosition() const; void setHandPosition(const glm::vec3& handPosition); - typedef enum { + typedef enum { NoData, PALMinimum, - MinimumData, + MinimumData, CullSmallData, IncludeSmallData, SendAllData @@ -388,7 +388,7 @@ public: virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail); virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, + AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, QVector* sentJointDataOut, AvatarDataRate* outboundDataRateOut = nullptr) const; virtual void doneEncoding(bool cullSmallChanges); @@ -417,23 +417,23 @@ public: void nextAttitude(glm::vec3 position, glm::quat orientation); // Can be safely called at any time. virtual void updateAttitude() {} // Tell skeleton mesh about changes - glm::quat getHeadOrientation() const { + glm::quat getHeadOrientation() const { lazyInitHeadData(); - return _headData->getOrientation(); + return _headData->getOrientation(); } - void setHeadOrientation(const glm::quat& orientation) { + void setHeadOrientation(const glm::quat& orientation) { if (_headData) { _headData->setOrientation(orientation); } } - void setLookAtPosition(const glm::vec3& lookAtPosition) { + void setLookAtPosition(const glm::vec3& lookAtPosition) { if (_headData) { - _headData->setLookAtPosition(lookAtPosition); + _headData->setLookAtPosition(lookAtPosition); } } - void setBlendshapeCoefficients(const QVector& blendshapeCoefficients) { + void setBlendshapeCoefficients(const QVector& blendshapeCoefficients) { if (_headData) { _headData->setBlendshapeCoefficients(blendshapeCoefficients); } @@ -470,7 +470,7 @@ public: void setDomainMinimumScale(float domainMinimumScale) { _domainMinimumScale = glm::clamp(domainMinimumScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); _scaleChanged = usecTimestampNow(); } - void setDomainMaximumScale(float domainMaximumScale) + void setDomainMaximumScale(float domainMaximumScale) { _domainMaximumScale = glm::clamp(domainMaximumScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); _scaleChanged = usecTimestampNow(); } // Hand State @@ -531,7 +531,7 @@ public: QString displayName; QString sessionDisplayName; AvatarEntityMap avatarEntityData; - quint64 updatedAt; + quint64 sequenceId; }; static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); @@ -548,8 +548,8 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setDisplayName(const QString& displayName); - virtual void setSessionDisplayName(const QString& sessionDisplayName) { - _sessionDisplayName = sessionDisplayName; + virtual void setSessionDisplayName(const QString& sessionDisplayName) { + _sessionDisplayName = sessionDisplayName; markIdentityDataChanged(); } @@ -604,6 +604,9 @@ public: return _lastSentJointData; } + // A method intended to be overriden by MyAvatar for polling orientation for network transmission. + virtual glm::quat getOrientationOutbound() const; + static const float OUT_OF_VIEW_PENALTY; static void sortAvatars( @@ -623,9 +626,11 @@ public: bool getIdentityDataChanged() const { return _identityDataChanged; } // has the identity data changed since the last time sendIdentityPacket() was called void markIdentityDataChanged() { _identityDataChanged = true; - _identityUpdatedAt = usecTimestampNow(); + _identitySequenceId++; } + float getDensity() const { return _density; } + signals: void displayNameChanged(); @@ -781,7 +786,8 @@ protected: float _audioAverageLoudness { 0.0f }; bool _identityDataChanged { false }; - quint64 _identityUpdatedAt { 0 }; + quint64 _identitySequenceId { 0 }; + float _density; private: friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 0d341c684e..2ccc64fee2 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -148,6 +148,7 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); bool identityChanged = false; bool displayNameChanged = false; + // In this case, the "sendingNode" is the Avatar Mixer. avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged); } } diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index b55be7c156..2704b6539c 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -28,14 +28,9 @@ HeadData::HeadData(AvatarData* owningAvatar) : _basePitch(0.0f), _baseRoll(0.0f), _lookAtPosition(0.0f, 0.0f, 0.0f), - _isFaceTrackerConnected(false), - _isEyeTrackerConnected(false), - _leftEyeBlink(0.0f), - _rightEyeBlink(0.0f), - _averageLoudness(0.0f), - _browAudioLift(0.0f), - _baseBlendshapeCoefficients(QVector(0, 0.0f)), - _currBlendShapeCoefficients(QVector(0, 0.0f)), + _blendshapeCoefficients(QVector(0, 0.0f)), + _transientBlendshapeCoefficients(QVector(0, 0.0f)), + _summedBlendshapeCoefficients(QVector(0, 0.0f)), _owningAvatar(owningAvatar) { @@ -85,22 +80,22 @@ static const QMap& getBlendshapesLookupMap() { } const QVector& HeadData::getSummedBlendshapeCoefficients() { - int maxSize = std::max(_baseBlendshapeCoefficients.size(), _blendshapeCoefficients.size()); - if (_currBlendShapeCoefficients.size() != maxSize) { - _currBlendShapeCoefficients.resize(maxSize); + int maxSize = std::max(_blendshapeCoefficients.size(), _transientBlendshapeCoefficients.size()); + if (_summedBlendshapeCoefficients.size() != maxSize) { + _summedBlendshapeCoefficients.resize(maxSize); } for (int i = 0; i < maxSize; i++) { - if (i >= _baseBlendshapeCoefficients.size()) { - _currBlendShapeCoefficients[i] = _blendshapeCoefficients[i]; - } else if (i >= _blendshapeCoefficients.size()) { - _currBlendShapeCoefficients[i] = _baseBlendshapeCoefficients[i]; + if (i >= _blendshapeCoefficients.size()) { + _summedBlendshapeCoefficients[i] = _transientBlendshapeCoefficients[i]; + } else if (i >= _transientBlendshapeCoefficients.size()) { + _summedBlendshapeCoefficients[i] = _blendshapeCoefficients[i]; } else { - _currBlendShapeCoefficients[i] = _baseBlendshapeCoefficients[i] + _blendshapeCoefficients[i]; + _summedBlendshapeCoefficients[i] = _blendshapeCoefficients[i] + _transientBlendshapeCoefficients[i]; } } - return _currBlendShapeCoefficients; + return _summedBlendshapeCoefficients; } void HeadData::setBlendshape(QString name, float val) { @@ -112,10 +107,10 @@ void HeadData::setBlendshape(QString name, float val) { if (_blendshapeCoefficients.size() <= it.value()) { _blendshapeCoefficients.resize(it.value() + 1); } - if (_baseBlendshapeCoefficients.size() <= it.value()) { - _baseBlendshapeCoefficients.resize(it.value() + 1); + if (_transientBlendshapeCoefficients.size() <= it.value()) { + _transientBlendshapeCoefficients.resize(it.value() + 1); } - _baseBlendshapeCoefficients[it.value()] = val; + _blendshapeCoefficients[it.value()] = val; } } @@ -131,14 +126,16 @@ QJsonObject HeadData::toJson() const { QJsonObject blendshapesJson; for (auto name : blendshapeLookupMap.keys()) { auto index = blendshapeLookupMap[name]; - if (index >= _blendshapeCoefficients.size()) { - continue; + float value = 0.0f; + if (index < _blendshapeCoefficients.size()) { + value += _blendshapeCoefficients[index]; } - auto value = _blendshapeCoefficients[index]; - if (value == 0.0f) { - continue; + if (index < _transientBlendshapeCoefficients.size()) { + value += _transientBlendshapeCoefficients[index]; + } + if (value != 0.0f) { + blendshapesJson[name] = value; } - blendshapesJson[name] = value; } if (!blendshapesJson.isEmpty()) { headJson[JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS] = blendshapesJson; @@ -163,8 +160,8 @@ void HeadData::fromJson(const QJsonObject& json) { QJsonArray blendshapeCoefficientsJson = jsonValue.toArray(); for (const auto& blendshapeCoefficient : blendshapeCoefficientsJson) { blendshapeCoefficients.push_back((float)blendshapeCoefficient.toDouble()); - setBlendshapeCoefficients(blendshapeCoefficients); } + setBlendshapeCoefficients(blendshapeCoefficients); } else if (jsonValue.isObject()) { QJsonObject blendshapeCoefficientsJson = jsonValue.toObject(); for (const QString& name : blendshapeCoefficientsJson.keys()) { diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index dbed0a6a65..be9d54e93e 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -63,7 +63,7 @@ public: void setBlendshapeCoefficients(const QVector& blendshapeCoefficients) { _blendshapeCoefficients = blendshapeCoefficients; } const glm::vec3& getLookAtPosition() const { return _lookAtPosition; } - void setLookAtPosition(const glm::vec3& lookAtPosition) { + void setLookAtPosition(const glm::vec3& lookAtPosition) { if (_lookAtPosition != lookAtPosition) { _lookAtPositionChanged = usecTimestampNow(); } @@ -85,16 +85,16 @@ protected: glm::vec3 _lookAtPosition; quint64 _lookAtPositionChanged { 0 }; - bool _isFaceTrackerConnected; - bool _isEyeTrackerConnected; - float _leftEyeBlink; - float _rightEyeBlink; - float _averageLoudness; - float _browAudioLift; + bool _isFaceTrackerConnected { false }; + bool _isEyeTrackerConnected { false }; + float _leftEyeBlink { 0.0f }; + float _rightEyeBlink { 0.0f }; + float _averageLoudness { 0.0f }; + float _browAudioLift { 0.0f }; QVector _blendshapeCoefficients; - QVector _baseBlendshapeCoefficients; - QVector _currBlendShapeCoefficients; + QVector _transientBlendshapeCoefficients; + QVector _summedBlendshapeCoefficients; AvatarData* _owningAvatar; private: diff --git a/libraries/controllers/src/controllers/InputRecorder.cpp b/libraries/controllers/src/controllers/InputRecorder.cpp index 2d2cd40739..60ff592144 100644 --- a/libraries/controllers/src/controllers/InputRecorder.cpp +++ b/libraries/controllers/src/controllers/InputRecorder.cpp @@ -22,20 +22,20 @@ #include #include - + QString SAVE_DIRECTORY = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + BuildInfo::MODIFIED_ORGANIZATION + "/" + BuildInfo::INTERFACE_NAME + "/hifi-input-recordings/"; QString FILE_PREFIX_NAME = "input-recording-"; -QString COMPRESS_EXTENSION = ".tar.gz"; +QString COMPRESS_EXTENSION = "json.gz"; namespace controller { - + QJsonObject poseToJsonObject(const Pose pose) { QJsonObject newPose; - + QJsonArray translation; translation.append(pose.translation.x); translation.append(pose.translation.y); translation.append(pose.translation.z); - + QJsonArray rotation; rotation.append(pose.rotation.x); rotation.append(pose.rotation.y); @@ -69,7 +69,7 @@ namespace controller { QJsonArray angularVelocity = object["angularVelocity"].toArray(); pose.valid = object["valid"].toBool(); - + pose.translation.x = translation[0].toDouble(); pose.translation.y = translation[1].toDouble(); pose.translation.z = translation[2].toDouble(); @@ -89,13 +89,13 @@ namespace controller { return pose; } - + void exportToFile(QJsonObject& object) { if (!QDir(SAVE_DIRECTORY).exists()) { QDir().mkdir(SAVE_DIRECTORY); } - + QString timeStamp = QDateTime::currentDateTime().toString(Qt::ISODate); timeStamp.replace(":", "-"); QString fileName = SAVE_DIRECTORY + FILE_PREFIX_NAME + timeStamp + COMPRESS_EXTENSION; @@ -124,7 +124,7 @@ namespace controller { status = true; return object; } - + InputRecorder::InputRecorder() {} InputRecorder::~InputRecorder() {} @@ -185,41 +185,42 @@ namespace controller { filePath.remove(0,8); QFileInfo info(filePath); QString extension = info.suffix(); - if (extension != "gz") { - qWarning() << "can not load file with exentsion of " << extension; - return; - } - bool success = false; - QJsonObject data = openFile(info.absoluteFilePath(), success); - if (success) { - _framesRecorded = data["frameCount"].toInt(); - QJsonArray actionArrayList = data["actionList"].toArray(); - QJsonArray poseArrayList = data["poseList"].toArray(); - - for (int actionIndex = 0; actionIndex < actionArrayList.size(); actionIndex++) { - QJsonArray actionState = actionArrayList[actionIndex].toArray(); - for (int index = 0; index < actionState.size(); index++) { - _currentFrameActions[index] = actionState[index].toInt(); - } - _actionStateList.push_back(_currentFrameActions); - _currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS)); - } - - for (int poseIndex = 0; poseIndex < poseArrayList.size(); poseIndex++) { - QJsonArray poseState = poseArrayList[poseIndex].toArray(); - for (int index = 0; index < poseState.size(); index++) { - _currentFramePoses[index] = jsonObjectToPose(poseState[index].toObject()); - } - _poseStateList.push_back(_currentFramePoses); - _currentFramePoses = PoseStates(toInt(Action::NUM_ACTIONS)); - } - } + if (extension != "gz") { + qWarning() << "can not load file with exentsion of " << extension; + return; + } + bool success = false; + QJsonObject data = openFile(info.absoluteFilePath(), success); + if (success) { + _framesRecorded = data["frameCount"].toInt(); + QJsonArray actionArrayList = data["actionList"].toArray(); + QJsonArray poseArrayList = data["poseList"].toArray(); - _loading = false; + for (int actionIndex = 0; actionIndex < actionArrayList.size(); actionIndex++) { + QJsonArray actionState = actionArrayList[actionIndex].toArray(); + for (int index = 0; index < actionState.size(); index++) { + _currentFrameActions[index] = actionState[index].toDouble(); + } + _actionStateList.push_back(_currentFrameActions); + _currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS)); + } + + for (int poseIndex = 0; poseIndex < poseArrayList.size(); poseIndex++) { + QJsonArray poseState = poseArrayList[poseIndex].toArray(); + for (int index = 0; index < poseState.size(); index++) { + _currentFramePoses[index] = jsonObjectToPose(poseState[index].toObject()); + } + _poseStateList.push_back(_currentFramePoses); + _currentFramePoses = PoseStates(toInt(Action::NUM_ACTIONS)); + } + } + + _loading = false; } void InputRecorder::stopRecording() { _recording = false; + _framesRecorded = (int)_actionStateList.size(); } void InputRecorder::startPlayback() { @@ -250,13 +251,13 @@ namespace controller { for(auto& channel : _currentFramePoses) { channel = Pose(); } - + for(auto& channel : _currentFrameActions) { channel = 0.0f; } } } - + float InputRecorder::getActionState(controller::Action action) { if (_actionStateList.size() > 0 ) { return _actionStateList[_playCount][toInt(action)]; @@ -282,7 +283,7 @@ namespace controller { if (_playback) { _playCount++; - if (_playCount == _framesRecorded) { + if (_playCount == (_framesRecorded - 1)) { _playCount = 0; } } diff --git a/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp index 57f86105b2..f424c02e6e 100644 --- a/libraries/entities/src/EntityDynamicInterface.cpp +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -43,14 +43,14 @@ | | +--------------------+ +-----------------------+ | | | | - | ObjectActionSpring | | ObjectConstraintHinge | + | ObjectActionTractor| | ObjectConstraintHinge | | (physics) | | (physics) | +--------------------+ +-----------------------+ A dynamic is a callback which is registered with bullet. A dynamic is called-back every physics -simulation step and can do whatever it wants with the various datastructures it has available. An +simulation step and can do whatever it wants with the various datastructures it has available. A dynamic, for example, can pull an EntityItem toward a point as if that EntityItem were connected to that point by a spring. @@ -60,7 +60,7 @@ script or when receiving information via an EntityTree data-stream (either over svo file). In the interface, if an EntityItem has dynamics, this EntityItem will have pointers to ObjectDynamic -subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects a dynamic-object +subclass (like ObjectDynamicTractor) instantiations. Code in the entities library affects a dynamic-object via the EntityDynamicInterface (which knows nothing about bullet). When the ObjectDynamic subclass instance is created, it is registered as a dynamic with bullet. Bullet will call into code in this instance with the btDynamicInterface every physics-simulation step. @@ -75,11 +75,11 @@ right now the AssignmentDynamic class is a place-holder. The dynamic-objects are instantiated by singleton (dependecy) subclasses of EntityDynamicFactoryInterface. In the interface, the subclass is an InterfaceDynamicFactory and it will produce things like -ObjectDynamicSpring. In an entity-server the subclass is an AssignmentDynamicFactory and it always +ObjectDynamicTractor. In an entity-server the subclass is an AssignmentDynamicFactory and it always produces AssignmentDynamics. Depending on the dynamic's type, it will have various arguments. When a script changes an argument of an -dynamic, the argument-holding member-variables of ObjectDynamicSpring (in this example) are updated and +dynamic, the argument-holding member-variables of ObjectDynamicTractor (in this example) are updated and also serialized into _dynamicData in the EntityItem. Each subclass of ObjectDynamic knows how to serialize and deserialize its own arguments. _dynamicData is what gets sent over the wire or saved in an svo file. When a packet-reader receives data for _dynamicData, it will save it in the EntityItem; this causes the @@ -103,7 +103,10 @@ EntityDynamicType EntityDynamicInterface::dynamicTypeFromString(QString dynamicT return DYNAMIC_TYPE_OFFSET; } if (normalizedDynamicTypeString == "spring") { - return DYNAMIC_TYPE_SPRING; + return DYNAMIC_TYPE_TRACTOR; + } + if (normalizedDynamicTypeString == "tractor") { + return DYNAMIC_TYPE_TRACTOR; } if (normalizedDynamicTypeString == "hold") { return DYNAMIC_TYPE_HOLD; @@ -139,7 +142,8 @@ QString EntityDynamicInterface::dynamicTypeToString(EntityDynamicType dynamicTyp case DYNAMIC_TYPE_OFFSET: return "offset"; case DYNAMIC_TYPE_SPRING: - return "spring"; + case DYNAMIC_TYPE_TRACTOR: + return "tractor"; case DYNAMIC_TYPE_HOLD: return "hold"; case DYNAMIC_TYPE_TRAVEL_ORIENTED: diff --git a/libraries/entities/src/EntityDynamicInterface.h b/libraries/entities/src/EntityDynamicInterface.h index c04aaf67b2..40e39eecf1 100644 --- a/libraries/entities/src/EntityDynamicInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -17,6 +17,7 @@ #include class EntityItem; +class EntityItemID; class EntitySimulation; using EntityItemPointer = std::shared_ptr; using EntityItemWeakPointer = std::weak_ptr; @@ -28,6 +29,7 @@ enum EntityDynamicType { DYNAMIC_TYPE_NONE = 0, DYNAMIC_TYPE_OFFSET = 1000, DYNAMIC_TYPE_SPRING = 2000, + DYNAMIC_TYPE_TRACTOR = 2100, DYNAMIC_TYPE_HOLD = 3000, DYNAMIC_TYPE_TRAVEL_ORIENTED = 4000, DYNAMIC_TYPE_HINGE = 5000, @@ -44,6 +46,9 @@ public: virtual ~EntityDynamicInterface() { } const QUuid& getID() const { return _id; } EntityDynamicType getType() const { return _type; } + + virtual void remapIDs(QHash& map) = 0; + virtual bool isAction() const { return false; } virtual bool isConstraint() const { return false; } virtual bool isReadyForAdd() const { return true; } @@ -89,15 +94,6 @@ public: QString argumentName, bool& ok, bool required = true); protected: - virtual glm::vec3 getPosition() = 0; - // virtual void setPosition(glm::vec3 position) = 0; - virtual glm::quat getRotation() = 0; - // virtual void setRotation(glm::quat rotation) = 0; - virtual glm::vec3 getLinearVelocity() = 0; - virtual void setLinearVelocity(glm::vec3 linearVelocity) = 0; - virtual glm::vec3 getAngularVelocity() = 0; - virtual void setAngularVelocity(glm::vec3 angularVelocity) = 0; - QUuid _id; EntityDynamicType _type; bool _active { false }; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index a6de541958..14122594fe 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1691,14 +1691,20 @@ void EntityItem::updateVelocity(const glm::vec3& value) { setLocalVelocity(Vectors::ZERO); } } else { - const float MIN_LINEAR_SPEED = 0.001f; - if (glm::length(value) < MIN_LINEAR_SPEED) { - velocity = ENTITY_ITEM_ZERO_VEC3; - } else { - velocity = value; + float speed = glm::length(value); + if (!glm::isnan(speed)) { + const float MIN_LINEAR_SPEED = 0.001f; + const float MAX_LINEAR_SPEED = 270.0f; // 3m per step at 90Hz + if (speed < MIN_LINEAR_SPEED) { + velocity = ENTITY_ITEM_ZERO_VEC3; + } else if (speed > MAX_LINEAR_SPEED) { + velocity = (MAX_LINEAR_SPEED / speed) * value; + } else { + velocity = value; + } + setLocalVelocity(velocity); + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } - setLocalVelocity(velocity); - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } } } @@ -1723,8 +1729,16 @@ void EntityItem::updateGravity(const glm::vec3& value) { if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { _gravity = Vectors::ZERO; } else { - _gravity = value; - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + float magnitude = glm::length(value); + if (!glm::isnan(magnitude)) { + const float MAX_ACCELERATION_OF_GRAVITY = 10.0f * 9.8f; // 10g + if (magnitude > MAX_ACCELERATION_OF_GRAVITY) { + _gravity = (MAX_ACCELERATION_OF_GRAVITY / magnitude) * value; + } else { + _gravity = value; + } + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + } } } } @@ -1735,14 +1749,20 @@ void EntityItem::updateAngularVelocity(const glm::vec3& value) { if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { setLocalAngularVelocity(Vectors::ZERO); } else { - const float MIN_ANGULAR_SPEED = 0.0002f; - if (glm::length(value) < MIN_ANGULAR_SPEED) { - angularVelocity = ENTITY_ITEM_ZERO_VEC3; - } else { - angularVelocity = value; + float speed = glm::length(value); + if (!glm::isnan(speed)) { + const float MIN_ANGULAR_SPEED = 0.0002f; + const float MAX_ANGULAR_SPEED = 9.0f * TWO_PI; // 1/10 rotation per step at 90Hz + if (speed < MIN_ANGULAR_SPEED) { + angularVelocity = ENTITY_ITEM_ZERO_VEC3; + } else if (speed > MAX_ANGULAR_SPEED) { + angularVelocity = (MAX_ANGULAR_SPEED / speed) * value; + } else { + angularVelocity = value; + } + setLocalAngularVelocity(angularVelocity); + _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } - setLocalAngularVelocity(angularVelocity); - _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } } } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index ffb65a2dba..b184d648da 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -662,6 +662,25 @@ QVector EntityScriptingInterface::findEntitiesInFrustum(QVariantMap frust return result; } +QVector EntityScriptingInterface::findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const { + EntityTypes::EntityType type = EntityTypes::getEntityTypeFromName(entityType); + + QVector result; + if (_entityTree) { + QVector entities; + _entityTree->withReadLock([&] { + _entityTree->findEntities(center, radius, entities); + }); + + foreach(EntityItemPointer entity, entities) { + if (entity->getType() == type) { + result << entity->getEntityItemID(); + } + } + } + return result; +} + RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersection(const PickRay& ray, bool precisionPicking, const QScriptValue& entityIdsToInclude, const QScriptValue& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) { PROFILE_RANGE(script_entities, __FUNCTION__); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index f5656860e3..575528fa78 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -212,9 +212,16 @@ public slots: /// - orientation /// - projection /// - centerRadius - /// this function will not find any models in script engine contexts which don't have access to models + /// this function will not find any models in script engine contexts which don't have access to entities Q_INVOKABLE QVector findEntitiesInFrustum(QVariantMap frustum) const; + /// finds entities of the indicated type within a sphere given by the center point and radius + /// @param {QString} string representation of entity type + /// @param {vec3} center point + /// @param {float} radius to search + /// this function will not find any entities in script engine contexts which don't have access to entities + Q_INVOKABLE QVector findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const; + /// If the scripting context has visible entities, this will determine a ray intersection, the results /// may be inaccurate if the engine is unable to access the visible entities, in which case result.accurate /// will be false. diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 76483d0786..6975d017b0 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -25,6 +25,8 @@ #include "RecurseOctreeToMapOperator.h" #include "LogHandler.h" #include "EntityEditFilters.h" +#include "EntityDynamicFactoryInterface.h" + static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour @@ -450,6 +452,13 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer(), entityID); + + existingEntity->forEachDescendant([&](SpatiallyNestablePointer descendant) { + auto descendantID = descendant->getID(); + theOperator.addEntityIDToDeleteList(descendantID); + emit deletingEntity(descendantID); + }); + recurseTreeWithOperator(&theOperator); processRemovedEntities(theOperator); _isDirty = true; @@ -1527,6 +1536,48 @@ void EntityTree::pruneTree() { recurseTreeWithOperator(&theOperator); } + +QByteArray EntityTree::remapActionDataIDs(QByteArray actionData, QHash& map) { + if (actionData.isEmpty()) { + return actionData; + } + + QDataStream serializedActionsStream(actionData); + QVector serializedActions; + serializedActionsStream >> serializedActions; + + auto actionFactory = DependencyManager::get(); + + QHash remappedActions; + foreach(QByteArray serializedAction, serializedActions) { + QDataStream serializedActionStream(serializedAction); + EntityDynamicType actionType; + QUuid oldActionID; + serializedActionStream >> actionType; + serializedActionStream >> oldActionID; + EntityDynamicPointer action = actionFactory->factoryBA(nullptr, serializedAction); + if (action) { + action->remapIDs(map); + remappedActions[action->getID()] = action; + } + } + + QVector remappedSerializedActions; + + QHash::const_iterator i = remappedActions.begin(); + while (i != remappedActions.end()) { + EntityDynamicPointer action = i.value(); + QByteArray bytesForAction = action->serialize(); + remappedSerializedActions << bytesForAction; + i++; + } + + QByteArray result; + QDataStream remappedSerializedActionsStream(&result, QIODevice::WriteOnly); + remappedSerializedActionsStream << remappedSerializedActions; + return result; +} + QVector EntityTree::sendEntities(EntityEditPacketSender* packetSender, EntityTreePointer localTree, float x, float y, float z) { SendEntitiesOperationArgs args; @@ -1543,71 +1594,67 @@ QVector EntityTree::sendEntities(EntityEditPacketSender* packetSen }); packetSender->releaseQueuedMessages(); + // the values from map are used as the list of successfully "sent" entities. If some didn't actually make it, + // pull them out. Bogus entries could happen if part of the imported data makes some reference to an entity + // that isn't in the data being imported. + QHash::iterator i = map.begin(); + while (i != map.end()) { + EntityItemID newID = i.value(); + if (localTree->findEntityByEntityItemID(newID)) { + i++; + } else { + i = map.erase(i); + } + } + return map.values().toVector(); } bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extraData) { SendEntitiesOperationArgs* args = static_cast(extraData); EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); - std::function getMapped = [&](EntityItemPointer& item) -> const EntityItemID { - EntityItemID oldID = item->getEntityItemID(); - if (args->map->contains(oldID)) { // Already been handled (e.g., as a parent of somebody that we've processed). - return args->map->value(oldID); - } - EntityItemID newID = QUuid::createUuid(); - args->map->insert(oldID, newID); + auto getMapped = [&args](EntityItemID oldID) { + if (oldID.isNull()) { + return EntityItemID(); + } + + QHash::iterator iter = args->map->find(oldID); + if (iter == args->map->end()) { + EntityItemID newID = QUuid::createUuid(); + args->map->insert(oldID, newID); + return newID; + } + return iter.value(); + }; + + entityTreeElement->forEachEntity([&args, &getMapped, &element](EntityItemPointer item) { + EntityItemID oldID = item->getEntityItemID(); + EntityItemID newID = getMapped(oldID); EntityItemProperties properties = item->getProperties(); + EntityItemID oldParentID = properties.getParentID(); if (oldParentID.isInvalidID()) { // no parent properties.setPosition(properties.getPosition() + args->root); } else { EntityItemPointer parentEntity = args->ourTree->findEntityByEntityItemID(oldParentID); if (parentEntity) { // map the parent - // Warning: (non-tail) recursion of getMapped could blow the call stack if the parent hierarchy is VERY deep. - properties.setParentID(getMapped(parentEntity)); + properties.setParentID(getMapped(parentEntity->getID())); // But do not add root offset in this case. } else { // Should not happen, but let's try to be helpful... item->globalizeProperties(properties, "Cannot find %3 parent of %2 %1", args->root); } } - if (!properties.getXNNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID()); - if (neighborEntity) { - properties.setXNNeighborID(getMapped(neighborEntity)); - } - } - if (!properties.getXPNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID()); - if (neighborEntity) { - properties.setXPNeighborID(getMapped(neighborEntity)); - } - } - if (!properties.getYNNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID()); - if (neighborEntity) { - properties.setYNNeighborID(getMapped(neighborEntity)); - } - } - if (!properties.getYPNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID()); - if (neighborEntity) { - properties.setYPNeighborID(getMapped(neighborEntity)); - } - } - if (!properties.getZNNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID()); - if (neighborEntity) { - properties.setZNNeighborID(getMapped(neighborEntity)); - } - } - if (!properties.getZPNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID()); - if (neighborEntity) { - properties.setZPNeighborID(getMapped(neighborEntity)); - } - } + properties.setXNNeighborID(getMapped(properties.getXNNeighborID())); + properties.setXPNeighborID(getMapped(properties.getXPNeighborID())); + properties.setYNNeighborID(getMapped(properties.getYNNeighborID())); + properties.setYPNeighborID(getMapped(properties.getYPNeighborID())); + properties.setZNNeighborID(getMapped(properties.getZNNeighborID())); + properties.setZPNeighborID(getMapped(properties.getZPNeighborID())); + + QByteArray actionData = properties.getActionData(); + properties.setActionData(remapActionDataIDs(actionData, *args->map)); // set creation time to "now" for imported entities properties.setCreated(usecTimestampNow()); @@ -1623,13 +1670,13 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra // also update the local tree instantly (note: this is not our tree, but an alternate tree) if (args->otherTree) { args->otherTree->withWriteLock([&] { - args->otherTree->addEntity(newID, properties); + EntityItemPointer entity = args->otherTree->addEntity(newID, properties); + entity->deserializeActions(); }); } return newID; - }; + }); - entityTreeElement->forEachEntity(getMapped); return true; } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 63f7bbfd66..d7e069b005 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -205,6 +205,8 @@ public: virtual void dumpTree() override; virtual void pruneTree() override; + static QByteArray remapActionDataIDs(QByteArray actionData, QHash& map); + QVector sendEntities(EntityEditPacketSender* packetSender, EntityTreePointer localTree, float x, float y, float z); diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 167cb8caac..1445d14d84 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -273,10 +273,9 @@ std::tuple requestData(QUrl& url) { return std::make_tuple(false, QByteArray()); } - request->send(); - QEventLoop loop; QObject::connect(request, &ResourceRequest::finished, &loop, &QEventLoop::quit); + request->send(); loop.exec(); if (request->getResult() == ResourceRequest::Success) { diff --git a/libraries/gpu-gl/CMakeLists.txt b/libraries/gpu-gl/CMakeLists.txt index 3e3853532a..65130d6d07 100644 --- a/libraries/gpu-gl/CMakeLists.txt +++ b/libraries/gpu-gl/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME gpu-gl) -setup_hifi_library() +setup_hifi_library(Concurrent) link_hifi_libraries(shared gl gpu) if (UNIX) target_link_libraries(${TARGET_NAME} pthread) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp index 1d1f92b297..2d71e8ed78 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp @@ -149,6 +149,10 @@ void GLBackend::resetUniformStage() { void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 3]._uint; + if (slot >(GLuint)MAX_NUM_UNIFORM_BUFFERS) { + qCDebug(gpugllogging) << "GLBackend::do_setUniformBuffer: Trying to set a uniform Buffer at slot #" << slot << " which doesn't exist. MaxNumUniformBuffers = " << getMaxNumUniformBuffers(); + return; + } BufferPointer uniformBuffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); GLintptr rangeStart = batch._params[paramOffset + 1]._uint; GLsizeiptr rangeSize = batch._params[paramOffset + 0]._uint; @@ -203,7 +207,7 @@ void GLBackend::resetResourceStage() { void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; if (slot >= (GLuint)MAX_NUM_RESOURCE_BUFFERS) { - // "GLBackend::do_setResourceBuffer: Trying to set a resource Buffer at slot #" + slot + " which doesn't exist. MaxNumResourceBuffers = " + getMaxNumResourceBuffers()); + qCDebug(gpugllogging) << "GLBackend::do_setResourceBuffer: Trying to set a resource Buffer at slot #" << slot << " which doesn't exist. MaxNumResourceBuffers = " << getMaxNumResourceBuffers(); return; } @@ -233,7 +237,7 @@ void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) { void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; if (slot >= (GLuint) MAX_NUM_RESOURCE_TEXTURES) { - // "GLBackend::do_setResourceTexture: Trying to set a resource Texture at slot #" + slot + " which doesn't exist. MaxNumResourceTextures = " + getMaxNumResourceTextures()); + qCDebug(gpugllogging) << "GLBackend::do_setResourceTexture: Trying to set a resource Texture at slot #" << slot << " which doesn't exist. MaxNumResourceTextures = " << getMaxNumResourceTextures(); return; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 5534419eaa..84dc49deba 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -160,8 +160,6 @@ const uvec3 GLVariableAllocationSupport::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 6 WorkQueue GLVariableAllocationSupport::_transferQueue; WorkQueue GLVariableAllocationSupport::_promoteQueue; WorkQueue GLVariableAllocationSupport::_demoteQueue; -TexturePointer GLVariableAllocationSupport::_currentTransferTexture; -TransferJobPointer GLVariableAllocationSupport::_currentTransferJob; size_t GLVariableAllocationSupport::_frameTexturesCreated { 0 }; #define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f @@ -176,30 +174,19 @@ const uvec3 GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1 const size_t GLVariableAllocationSupport::MAX_TRANSFER_SIZE = GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS.x * GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS.y * 4; #if THREADED_TEXTURE_BUFFERING -std::shared_ptr TransferJob::_bufferThread { nullptr }; -std::atomic TransferJob::_shutdownBufferingThread { false }; -Mutex TransferJob::_mutex; -TransferJob::VoidLambdaQueue TransferJob::_bufferLambdaQueue; -void TransferJob::startTransferLoop() { - if (_bufferThread) { - return; - } - _shutdownBufferingThread = false; - _bufferThread = std::make_shared([] { - TransferJob::bufferLoop(); +TexturePointer GLVariableAllocationSupport::_currentTransferTexture; +TransferJobPointer GLVariableAllocationSupport::_currentTransferJob; +QThreadPool* TransferJob::_bufferThreadPool { nullptr }; + +void TransferJob::startBufferingThread() { + static std::once_flag once; + std::call_once(once, [&] { + _bufferThreadPool = new QThreadPool(qApp); + _bufferThreadPool->setMaxThreadCount(1); }); } -void TransferJob::stopTransferLoop() { - if (!_bufferThread) { - return; - } - _shutdownBufferingThread = true; - _bufferThread->join(); - _bufferThread.reset(); - _shutdownBufferingThread = false; -} #endif TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset) @@ -233,7 +220,6 @@ TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t t // Buffering can invoke disk IO, so it should be off of the main and render threads _bufferingLambda = [=] { _mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face)->createView(_transferSize, _transferOffset); - _bufferingCompleted = true; }; _transferLambda = [=] { @@ -243,65 +229,66 @@ TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t t } TransferJob::TransferJob(const GLTexture& parent, std::function transferLambda) - : _parent(parent), _bufferingCompleted(true), _transferLambda(transferLambda) { + : _parent(parent), _bufferingRequired(false), _transferLambda(transferLambda) { } TransferJob::~TransferJob() { Backend::updateTextureTransferPendingSize(_transferSize, 0); } - bool TransferJob::tryTransfer() { - // Disable threaded texture transfer for now #if THREADED_TEXTURE_BUFFERING // Are we ready to transfer - if (_bufferingCompleted) { - _transferLambda(); + if (!bufferingCompleted()) { + startBuffering(); + return false; + } +#else + if (_bufferingRequired) { + _bufferingLambda(); + } +#endif + _transferLambda(); + return true; +} + +#if THREADED_TEXTURE_BUFFERING +bool TransferJob::bufferingRequired() const { + if (!_bufferingRequired) { + return false; + } + + // The default state of a QFuture is with status Canceled | Started | Finished, + // so we have to check isCancelled before we check the actual state + if (_bufferingStatus.isCanceled()) { return true; } - startBuffering(); - return false; -#else - if (!_bufferingCompleted) { - _bufferingLambda(); - _bufferingCompleted = true; - } - _transferLambda(); - return true; -#endif + return !_bufferingStatus.isStarted(); } -#if THREADED_TEXTURE_BUFFERING +bool TransferJob::bufferingCompleted() const { + if (!_bufferingRequired) { + return true; + } + + // The default state of a QFuture is with status Canceled | Started | Finished, + // so we have to check isCancelled before we check the actual state + if (_bufferingStatus.isCanceled()) { + return false; + } + + return _bufferingStatus.isFinished(); +} void TransferJob::startBuffering() { - if (_bufferingStarted) { - return; - } - _bufferingStarted = true; - { - Lock lock(_mutex); - _bufferLambdaQueue.push(_bufferingLambda); - } -} - -void TransferJob::bufferLoop() { - while (!_shutdownBufferingThread) { - VoidLambdaQueue workingQueue; - { - Lock lock(_mutex); - _bufferLambdaQueue.swap(workingQueue); - } - - if (workingQueue.empty()) { - QThread::msleep(5); - continue; - } - - while (!workingQueue.empty()) { - workingQueue.front()(); - workingQueue.pop(); - } + if (bufferingRequired()) { + assert(_bufferingStatus.isCanceled()); + _bufferingStatus = QtConcurrent::run(_bufferThreadPool, [=] { + _bufferingLambda(); + }); + assert(!_bufferingStatus.isCanceled()); + assert(_bufferingStatus.isStarted()); } } #endif @@ -316,7 +303,9 @@ GLVariableAllocationSupport::~GLVariableAllocationSupport() { void GLVariableAllocationSupport::addMemoryManagedTexture(const TexturePointer& texturePointer) { _memoryManagedTextures.push_back(texturePointer); - addToWorkQueue(texturePointer); + if (MemoryPressureState::Idle != _memoryPressureState) { + addToWorkQueue(texturePointer); + } } void GLVariableAllocationSupport::addToWorkQueue(const TexturePointer& texturePointer) { @@ -345,10 +334,8 @@ void GLVariableAllocationSupport::addToWorkQueue(const TexturePointer& texturePo break; case MemoryPressureState::Idle: - break; - - default: Q_UNREACHABLE(); + break; } } @@ -364,10 +351,10 @@ WorkQueue& GLVariableAllocationSupport::getActiveWorkQueue() { case MemoryPressureState::Transfer: return _transferQueue; - default: + case MemoryPressureState::Idle: + Q_UNREACHABLE(); break; } - Q_UNREACHABLE(); return empty; } @@ -460,16 +447,11 @@ void GLVariableAllocationSupport::updateMemoryPressure() { } if (newState != _memoryPressureState) { + _memoryPressureState = newState; #if THREADED_TEXTURE_BUFFERING if (MemoryPressureState::Transfer == _memoryPressureState) { - TransferJob::stopTransferLoop(); + TransferJob::startBufferingThread(); } - _memoryPressureState = newState; - if (MemoryPressureState::Transfer == _memoryPressureState) { - TransferJob::startTransferLoop(); - } -#else - _memoryPressureState = newState; #endif // Clear the existing queue _transferQueue = WorkQueue(); @@ -487,49 +469,111 @@ void GLVariableAllocationSupport::updateMemoryPressure() { } } +TexturePointer GLVariableAllocationSupport::getNextWorkQueueItem(WorkQueue& workQueue) { + while (!workQueue.empty()) { + auto workTarget = workQueue.top(); + + auto texture = workTarget.first.lock(); + if (!texture) { + workQueue.pop(); + continue; + } + + // Check whether the resulting texture can actually have work performed + GLTexture* gltexture = Backend::getGPUObject(*texture); + GLVariableAllocationSupport* vartexture = dynamic_cast(gltexture); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + if (vartexture->canDemote()) { + return texture; + } + break; + + case MemoryPressureState::Undersubscribed: + if (vartexture->canPromote()) { + return texture; + } + break; + + case MemoryPressureState::Transfer: + if (vartexture->hasPendingTransfers()) { + return texture; + } + break; + + case MemoryPressureState::Idle: + Q_UNREACHABLE(); + break; + } + + // If we got here, then the texture has no work to do in the current state, + // so pop it off the queue and continue + workQueue.pop(); + } + + return TexturePointer(); +} + +void GLVariableAllocationSupport::processWorkQueue(WorkQueue& workQueue) { + if (workQueue.empty()) { + return; + } + + // Get the front of the work queue to perform work + auto texture = getNextWorkQueueItem(workQueue); + if (!texture) { + return; + } + + // Grab the first item off the demote queue + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + + GLTexture* gltexture = Backend::getGPUObject(*texture); + GLVariableAllocationSupport* vartexture = dynamic_cast(gltexture); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + vartexture->demote(); + workQueue.pop(); + addToWorkQueue(texture); + break; + + case MemoryPressureState::Undersubscribed: + vartexture->promote(); + workQueue.pop(); + addToWorkQueue(texture); + break; + + case MemoryPressureState::Transfer: + if (vartexture->executeNextTransfer(texture)) { + workQueue.pop(); + addToWorkQueue(texture); + +#if THREADED_TEXTURE_BUFFERING + // Eagerly start the next buffering job if possible + texture = getNextWorkQueueItem(workQueue); + if (texture) { + gltexture = Backend::getGPUObject(*texture); + vartexture = dynamic_cast(gltexture); + vartexture->executeNextBuffer(texture); + } +#endif + } + break; + + case MemoryPressureState::Idle: + Q_UNREACHABLE(); + break; + } +} + void GLVariableAllocationSupport::processWorkQueues() { if (MemoryPressureState::Idle == _memoryPressureState) { return; } auto& workQueue = getActiveWorkQueue(); - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - while (!workQueue.empty()) { - auto workTarget = workQueue.top(); - workQueue.pop(); - auto texture = workTarget.first.lock(); - if (!texture) { - continue; - } - - // Grab the first item off the demote queue - GLTexture* gltexture = Backend::getGPUObject(*texture); - GLVariableAllocationSupport* vartexture = dynamic_cast(gltexture); - if (MemoryPressureState::Oversubscribed == _memoryPressureState) { - if (!vartexture->canDemote()) { - continue; - } - vartexture->demote(); - _memoryPressureStateStale = true; - } else if (MemoryPressureState::Undersubscribed == _memoryPressureState) { - if (!vartexture->canPromote()) { - continue; - } - vartexture->promote(); - _memoryPressureStateStale = true; - } else if (MemoryPressureState::Transfer == _memoryPressureState) { - if (!vartexture->hasPendingTransfers()) { - continue; - } - vartexture->executeNextTransfer(texture); - } else { - Q_UNREACHABLE(); - } - - // Reinject into the queue if more work to be done - addToWorkQueue(texture); - break; - } + // Do work on the front of the queue + processWorkQueue(workQueue); if (workQueue.empty()) { _memoryPressureState = MemoryPressureState::Idle; @@ -543,28 +587,83 @@ void GLVariableAllocationSupport::manageMemory() { processWorkQueues(); } +bool GLVariableAllocationSupport::executeNextTransfer(const TexturePointer& currentTexture) { +#if THREADED_TEXTURE_BUFFERING + // If a transfer job is active on the buffering thread, but has not completed it's buffering lambda, + // then we need to exit early, since we don't want to have the transfer job leave scope while it's + // being used in another thread -- See https://highfidelity.fogbugz.com/f/cases/4626 + if (_currentTransferJob && !_currentTransferJob->bufferingCompleted()) { + return false; + } +#endif -void GLVariableAllocationSupport::executeNextTransfer(const TexturePointer& currentTexture) { if (_populatedMip <= _allocatedMip) { +#if THREADED_TEXTURE_BUFFERING + _currentTransferJob.reset(); + _currentTransferTexture.reset(); +#endif + return true; + } + + // If the transfer queue is empty, rebuild it + if (_pendingTransfers.empty()) { + populateTransferQueue(); + } + + bool result = false; + if (!_pendingTransfers.empty()) { +#if THREADED_TEXTURE_BUFFERING + // If there is a current transfer, but it's not the top of the pending transfer queue, then it's an orphan, so we want to abandon it. + if (_currentTransferJob && _currentTransferJob != _pendingTransfers.front()) { + _currentTransferJob.reset(); + } + + if (!_currentTransferJob) { + // Keeping hold of a strong pointer to the transfer job ensures that if the pending transfer queue is rebuilt, the transfer job + // doesn't leave scope, causing a crash in the buffering thread + _currentTransferJob = _pendingTransfers.front(); + + // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture + _currentTransferTexture = currentTexture; + } + + // transfer jobs use asynchronous buffering of the texture data because it may involve disk IO, so we execute a try here to determine if the buffering + // is complete + if (_currentTransferJob->tryTransfer()) { + _pendingTransfers.pop(); + // Once a given job is finished, release the shared pointers keeping them alive + _currentTransferTexture.reset(); + _currentTransferJob.reset(); + result = true; + } +#else + if (_pendingTransfers.front()->tryTransfer()) { + _pendingTransfers.pop(); + result = true; + } +#endif + } + return result; +} + +#if THREADED_TEXTURE_BUFFERING +void GLVariableAllocationSupport::executeNextBuffer(const TexturePointer& currentTexture) { + if (_currentTransferJob && !_currentTransferJob->bufferingCompleted()) { return; } + // If the transfer queue is empty, rebuild it if (_pendingTransfers.empty()) { populateTransferQueue(); } if (!_pendingTransfers.empty()) { - // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture - _currentTransferTexture = currentTexture; - // Keeping hold of a strong pointer to the transfer job ensures that if the pending transfer queue is rebuilt, the transfer job - // doesn't leave scope, causing a crash in the buffering thread - _currentTransferJob = _pendingTransfers.front(); - // transfer jobs use asynchronous buffering of the texture data because it may involve disk IO, so we execute a try here to determine if the buffering - // is complete - if (_currentTransferJob->tryTransfer()) { - _pendingTransfers.pop(); - _currentTransferTexture.reset(); - _currentTransferJob.reset(); + if (!_currentTransferJob) { + _currentTransferJob = _pendingTransfers.front(); + _currentTransferTexture = currentTexture; } + + _currentTransferJob->startBuffering(); } } +#endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 877966f2d9..c6ce2a2495 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -8,6 +8,9 @@ #ifndef hifi_gpu_gl_GLTexture_h #define hifi_gpu_gl_GLTexture_h +#include +#include + #include "GLShared.h" #include "GLBackend.h" #include "GLTexelFormat.h" @@ -47,24 +50,19 @@ public: class TransferJob { using VoidLambda = std::function; using VoidLambdaQueue = std::queue; - using ThreadPointer = std::shared_ptr; const GLTexture& _parent; Texture::PixelsPointer _mipData; size_t _transferOffset { 0 }; size_t _transferSize { 0 }; - // Indicates if a transfer from backing storage to interal storage has started - bool _bufferingStarted { false }; - bool _bufferingCompleted { false }; + bool _bufferingRequired { true }; VoidLambda _transferLambda; VoidLambda _bufferingLambda; #if THREADED_TEXTURE_BUFFERING - static Mutex _mutex; - static VoidLambdaQueue _bufferLambdaQueue; - static ThreadPointer _bufferThread; - static std::atomic _shutdownBufferingThread; - static void bufferLoop(); + // Indicates if a transfer from backing storage to interal storage has started + QFuture _bufferingStatus; + static QThreadPool* _bufferThreadPool; #endif public: @@ -75,14 +73,13 @@ public: bool tryTransfer(); #if THREADED_TEXTURE_BUFFERING - static void startTransferLoop(); - static void stopTransferLoop(); + void startBuffering(); + bool bufferingRequired() const; + bool bufferingCompleted() const; + static void startBufferingThread(); #endif private: -#if THREADED_TEXTURE_BUFFERING - void startBuffering(); -#endif void transfer(); }; @@ -100,8 +97,10 @@ protected: static WorkQueue _transferQueue; static WorkQueue _promoteQueue; static WorkQueue _demoteQueue; +#if THREADED_TEXTURE_BUFFERING static TexturePointer _currentTransferTexture; static TransferJobPointer _currentTransferJob; +#endif static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS; static const uvec3 MAX_TRANSFER_DIMENSIONS; static const size_t MAX_TRANSFER_SIZE; @@ -109,6 +108,8 @@ protected: static void updateMemoryPressure(); static void processWorkQueues(); + static void processWorkQueue(WorkQueue& workQueue); + static TexturePointer getNextWorkQueueItem(WorkQueue& workQueue); static void addToWorkQueue(const TexturePointer& texture); static WorkQueue& getActiveWorkQueue(); @@ -118,7 +119,10 @@ protected: bool canPromote() const { return _allocatedMip > _minAllocatedMip; } bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } - void executeNextTransfer(const TexturePointer& currentTexture); +#if THREADED_TEXTURE_BUFFERING + void executeNextBuffer(const TexturePointer& currentTexture); +#endif + bool executeNextTransfer(const TexturePointer& currentTexture); virtual void populateTransferQueue() = 0; virtual void promote() = 0; virtual void demote() = 0; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 8319e61382..fe2761b37d 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -17,7 +17,6 @@ #include #define INCREMENTAL_TRANSFER 0 -#define THREADED_TEXTURE_BUFFERING 1 #define GPU_SSBO_TRANSFORM_OBJECT 1 namespace gpu { namespace gl45 { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 0f84d2a3c9..a545be9088 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -441,7 +441,11 @@ void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { // THen check that the mem texture passed make sense with its format Size expectedSize = evalStoredMipSize(level, getStoredMipFormat()); auto size = storage->size(); - if (storage->size() <= expectedSize) { + // NOTE: doing the same thing in all the next block but beeing able to breakpoint with more accuracy + if (storage->size() < expectedSize) { + _storage->assignMipData(level, storage); + _stamp++; + } else if (size == expectedSize) { _storage->assignMipData(level, storage); _stamp++; } else if (size > expectedSize) { @@ -468,7 +472,11 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin // THen check that the mem texture passed make sense with its format Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); auto size = storage->size(); - if (size <= expectedSize) { + // NOTE: doing the same thing in all the next block but beeing able to breakpoint with more accuracy + if (size < expectedSize) { + _storage->assignMipFaceData(level, face, storage); + _stamp++; + } else if (size == expectedSize) { _storage->assignMipFaceData(level, face, storage); _stamp++; } else if (size > expectedSize) { diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 7f91d8bb2e..3777f2bb50 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -22,6 +22,8 @@ #include "Forward.h" #include "Resource.h" +const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; + namespace ktx { class KTX; using KTXUniquePointer = std::unique_ptr; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 3fc4e0d432..92ead5f616 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -210,7 +210,16 @@ PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { auto faceSize = _ktxDescriptor->getMipFaceTexelsSize(level, face); if (faceSize != 0 && faceOffset != 0) { auto file = maybeOpenFile(); - result = file->createView(faceSize, faceOffset)->toMemoryStorage(); + if (file) { + auto storageView = file->createView(faceSize, faceOffset); + if (storageView) { + return storageView->toMemoryStorage(); + } else { + qWarning() << "Failed to get a valid storageView for faceSize=" << faceSize << " faceOffset=" << faceOffset << "out of valid file " << QString::fromStdString(_filename); + } + } else { + qWarning() << "Failed to get a valid file out of maybeOpenFile " << QString::fromStdString(_filename); + } } return result; } @@ -238,8 +247,9 @@ void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& stor throw std::runtime_error("Invalid level"); } - if (storage->size() != _ktxDescriptor->images[level]._imageSize) { - qWarning() << "Invalid image size: " << storage->size() << ", expected: " << _ktxDescriptor->images[level]._imageSize + auto& imageDesc = _ktxDescriptor->images[level]; + if (storage->size() != imageDesc._imageSize) { + qWarning() << "Invalid image size: " << storage->size() << ", expected: " << imageDesc._imageSize << ", level: " << level << ", filename: " << QString::fromStdString(_filename); return; } @@ -258,7 +268,7 @@ void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& stor return; } - memcpy(imageData, storage->data(), _ktxDescriptor->images[level]._imageSize); + memcpy(imageData, storage->data(), storage->size()); _minMipLevelAvailable = level; if (_offsetToMinMipKV > 0) { auto minMipKeyData = file->mutableData() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV; @@ -542,6 +552,13 @@ bool Texture::evalTextureFormat(const ktx::Header& header, Element& mipFormat, E } else { return false; } + } else if (header.getGLFormat() == ktx::GLFormat::RG && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + mipFormat = Format::VEC2NU8_XY; + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RG8) { + texelFormat = Format::VEC2NU8_XY; + } else { + return false; + } } else if (header.getGLFormat() == ktx::GLFormat::COMPRESSED_FORMAT && header.getGLType() == ktx::GLType::COMPRESSED_TYPE) { if (header.getGLInternaFormat_Compressed() == ktx::GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT) { mipFormat = Format::COLOR_COMPRESSED_SRGB; diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 68add428c1..32184dfe79 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -383,6 +383,7 @@ void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { } else if (mipFormat == gpu::Element::COLOR_RGBA_32) { compressionOptions.setFormat(nvtt::Format_RGBA); compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPitchAlignment(4); compressionOptions.setPixelFormat(32, 0x000000FF, 0x0000FF00, @@ -393,6 +394,7 @@ void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { } else if (mipFormat == gpu::Element::COLOR_BGRA_32) { compressionOptions.setFormat(nvtt::Format_RGBA); compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPitchAlignment(4); compressionOptions.setPixelFormat(32, 0x00FF0000, 0x0000FF00, @@ -403,6 +405,7 @@ void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { } else if (mipFormat == gpu::Element::COLOR_SRGBA_32) { compressionOptions.setFormat(nvtt::Format_RGBA); compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPitchAlignment(4); compressionOptions.setPixelFormat(32, 0x000000FF, 0x0000FF00, @@ -411,6 +414,7 @@ void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { } else if (mipFormat == gpu::Element::COLOR_SBGRA_32) { compressionOptions.setFormat(nvtt::Format_RGBA); compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPitchAlignment(4); compressionOptions.setPixelFormat(32, 0x00FF0000, 0x0000FF00, @@ -419,11 +423,13 @@ void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { } else if (mipFormat == gpu::Element::COLOR_R_8) { compressionOptions.setFormat(nvtt::Format_RGB); compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPitchAlignment(4); compressionOptions.setPixelFormat(8, 0, 0, 0); } else if (mipFormat == gpu::Element::VEC2NU8_XY) { inputOptions.setNormalMap(true); compressionOptions.setFormat(nvtt::Format_RGBA); compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPitchAlignment(4); compressionOptions.setPixelFormat(8, 8, 0, 0); } else { qCWarning(imagelogging) << "Unknown mip format"; diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index d9dd1105cd..3bf45ace98 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -37,7 +37,8 @@ enum Type { CUBE_TEXTURE, OCCLUSION_TEXTURE, SCATTERING_TEXTURE = OCCLUSION_TEXTURE, - LIGHTMAP_TEXTURE + LIGHTMAP_TEXTURE, + UNUSED_TEXTURE }; using TextureLoader = std::function; diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp index 38bb91e5c2..b43d015d65 100644 --- a/libraries/ktx/src/ktx/KTX.cpp +++ b/libraries/ktx/src/ktx/KTX.cpp @@ -22,6 +22,9 @@ uint32_t Header::evalPadding(size_t byteSize) { return (uint32_t) (3 - (byteSize + 3) % PACKING_SIZE);// padding ? PACKING_SIZE - padding : 0); } +bool Header::checkAlignment(size_t byteSize) { + return ((byteSize & 0x3) == 0); +} const Header::Identifier ktx::Header::IDENTIFIER {{ 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A @@ -114,6 +117,9 @@ size_t Header::evalFaceSize(uint32_t level) const { } size_t Header::evalImageSize(uint32_t level) const { auto faceSize = evalFaceSize(level); + if (!checkAlignment(faceSize)) { + return 0; + } if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { return faceSize; } else { @@ -139,6 +145,9 @@ ImageDescriptors Header::generateImageDescriptors() const { size_t imageOffset = 0; for (uint32_t level = 0; level < numberOfMipmapLevels; ++level) { auto imageSize = static_cast(evalImageSize(level)); + if (!checkAlignment(imageSize)) { + return ImageDescriptors(); + } if (imageSize == 0) { return ImageDescriptors(); } diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h index e8fa019a07..3f220abac3 100644 --- a/libraries/ktx/src/ktx/KTX.h +++ b/libraries/ktx/src/ktx/KTX.h @@ -38,15 +38,15 @@ UInt32 numberOfArrayElements UInt32 numberOfFaces UInt32 numberOfMipmapLevels UInt32 bytesOfKeyValueData - + for each keyValuePair that fits in bytesOfKeyValueData UInt32 keyAndValueByteSize Byte keyAndValue[keyAndValueByteSize] Byte valuePadding[3 - ((keyAndValueByteSize + 3) % 4)] end - + for each mipmap_level in numberOfMipmapLevels* - UInt32 imageSize; + UInt32 imageSize; for each array_element in numberOfArrayElements* for each face in numberOfFaces for each z_slice in pixelDepth* @@ -269,7 +269,7 @@ namespace ktx { COMPRESSED_RG11_EAC = 0x9272, COMPRESSED_SIGNED_RG11_EAC = 0x9273, }; - + enum class GLBaseInternalFormat : uint32_t { // GL 4.4 Table 8.11 DEPTH_COMPONENT = 0x1902, @@ -309,6 +309,7 @@ namespace ktx { static const uint32_t REVERSE_ENDIAN_TEST = 0x01020304; static uint32_t evalPadding(size_t byteSize); + static bool checkAlignment(size_t byteSize); Header(); @@ -418,9 +419,9 @@ namespace ktx { using FaceOffsets = std::vector; using FaceBytes = std::vector; + const uint32_t _numFaces; // This is the byte offset from the _start_ of the image region. For example, level 0 // will have a byte offset of 0. - const uint32_t _numFaces; const size_t _imageOffset; const uint32_t _imageSize; const uint32_t _faceSize; @@ -465,7 +466,7 @@ namespace ktx { class KTX; - // A KTX descriptor is a lightweight container for all the information about a serialized KTX file, but without the + // A KTX descriptor is a lightweight container for all the information about a serialized KTX file, but without the // actual image / face data available. struct KTXDescriptor { KTXDescriptor(const Header& header, const KeyValues& keyValues, const ImageDescriptors& imageDescriptors) : header(header), keyValues(keyValues), images(imageDescriptors) {} @@ -494,7 +495,7 @@ namespace ktx { // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the // following two functions // size_t sizeNeeded = KTX::evalStorageSize(header, images); - // + // // //allocate a buffer of size "sizeNeeded" or map a file with enough capacity // Byte* destBytes = new Byte[sizeNeeded]; // diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp index 440e2f048c..1b63af5262 100644 --- a/libraries/ktx/src/ktx/Reader.cpp +++ b/libraries/ktx/src/ktx/Reader.cpp @@ -148,12 +148,24 @@ namespace ktx { size_t imageSize = *reinterpret_cast(currentPtr); currentPtr += sizeof(uint32_t); + auto expectedImageSize = header.evalImageSize((uint32_t) images.size()); + if (imageSize != expectedImageSize) { + break; + } else if (!Header::checkAlignment(imageSize)) { + break; + } + + // The image size is the face size, beware! + size_t faceSize = imageSize; + if (numFaces == NUM_CUBEMAPFACES) { + imageSize = NUM_CUBEMAPFACES * faceSize; + } + // If enough data ahead then capture the pointer if ((currentPtr - srcBytes) + imageSize <= (srcSize)) { auto padding = Header::evalPadding(imageSize); if (numFaces == NUM_CUBEMAPFACES) { - size_t faceSize = imageSize / NUM_CUBEMAPFACES; Image::FaceBytes faces(NUM_CUBEMAPFACES); for (uint32_t face = 0; face < NUM_CUBEMAPFACES; face++) { faces[face] = currentPtr; @@ -166,6 +178,7 @@ namespace ktx { currentPtr += imageSize + padding; } } else { + // Stop here break; } } @@ -190,6 +203,10 @@ namespace ktx { // populate image table result->_images = parseImages(result->getHeader(), result->getTexelsDataSize(), result->getTexelsData()); + if (result->_images.size() != result->getHeader().getNumberOfLevels()) { + // Fail if the number of images produced doesn't match the header number of levels + return nullptr; + } return result; } diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp index 4226b8fa84..23f9d05596 100644 --- a/libraries/ktx/src/ktx/Writer.cpp +++ b/libraries/ktx/src/ktx/Writer.cpp @@ -127,6 +127,7 @@ namespace ktx { size_t KTX::writeWithoutImages(Byte* destBytes, size_t destByteSize, const Header& header, const ImageDescriptors& descriptors, const KeyValues& keyValues) { // Check again that we have enough destination capacity if (!destBytes || (destByteSize < evalStorageSize(header, descriptors, keyValues))) { + qWarning() << "Destination capacity is insufficient to write KTX without images"; return 0; } @@ -148,14 +149,17 @@ namespace ktx { for (size_t i = 0; i < descriptors.size(); ++i) { auto ptr = reinterpret_cast(currentDestPtr); - *ptr = descriptors[i]._imageSize; - ptr++; + uint32_t imageFaceSize = descriptors[i]._faceSize; + *ptr = imageFaceSize; // the imageSize written in the ktx is the FACE size + #ifdef DEBUG + ptr++; for (size_t k = 0; k < descriptors[i]._imageSize/4; k++) { *(ptr + k) = 0xFFFFFFFF; } #endif - currentDestPtr += descriptors[i]._imageSize + sizeof(uint32_t); + currentDestPtr += sizeof(uint32_t); + currentDestPtr += descriptors[i]._imageSize; } return destByteSize; @@ -210,7 +214,8 @@ namespace ktx { if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) { uint32_t imageOffset = currentPtr - destBytes; size_t imageSize = srcImages[l]._imageSize; - *(reinterpret_cast (currentPtr)) = (uint32_t) imageSize; + size_t imageFaceSize = srcImages[l]._faceSize; + *(reinterpret_cast (currentPtr)) = (uint32_t)imageFaceSize; // the imageSize written in the ktx is the FACE size currentPtr += sizeof(uint32_t); currentDataSize += sizeof(uint32_t); diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp index 8ec1c4e41c..e0447af8e6 100644 --- a/libraries/model-networking/src/model-networking/KTXCache.cpp +++ b/libraries/model-networking/src/model-networking/KTXCache.cpp @@ -22,7 +22,7 @@ KTXCache::KTXCache(const std::string& dir, const std::string& ext) : } KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { - FilePointer file = FileCache::writeFile(data, std::move(metadata)); + FilePointer file = FileCache::writeFile(data, std::move(metadata), true); return std::static_pointer_cast(file); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 9653cde7d8..8683d56b6b 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -501,12 +501,19 @@ void NetworkTexture::ktxMipRequestFinished() { if (texture) { texture->assignStoredMip(_ktxMipLevelRangeInFlight.first, _ktxMipRequest->getData().size(), reinterpret_cast(_ktxMipRequest->getData().data())); - _lowestKnownPopulatedMip = _textureSource->getGPUTexture()->minAvailableMipLevel(); + + if (texture->minAvailableMipLevel() <= _ktxMipLevelRangeInFlight.first) { + _lowestKnownPopulatedMip = texture->minAvailableMipLevel(); + _ktxResourceState = WAITING_FOR_MIP_REQUEST; + } else { + qWarning(networking) << "Failed to load mip: " << _url << ":" << _ktxMipLevelRangeInFlight.first; + _ktxResourceState = FAILED_TO_LOAD; + } } else { + _ktxResourceState = WAITING_FOR_MIP_REQUEST; qWarning(networking) << "Trying to update mips but texture is null"; } finishedLoading(true); - _ktxResourceState = WAITING_FOR_MIP_REQUEST; } else { finishedLoading(false); if (handleFailedRequest(_ktxMipRequest->getResult())) { @@ -792,6 +799,8 @@ void ImageReader::read() { texture = gpu::Texture::unserialize(ktxFile->getFilepath()); if (texture) { texture = textureCache->cacheTextureByHash(hash, texture); + } else { + qCWarning(modelnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating..."; } } } @@ -835,7 +844,7 @@ void ImageReader::read() { const char* data = reinterpret_cast(memKtx->_storage->data()); size_t length = memKtx->_storage->size(); auto& ktxCache = textureCache->_ktxCache; - networkTexture->_file = ktxCache.writeFile(data, KTXCache::Metadata(hash, length)); + networkTexture->_file = ktxCache.writeFile(data, KTXCache::Metadata(hash, length)); // if (!networkTexture->_file) { qCWarning(modelnetworking) << _url << "file cache failed"; } else { diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index aabc7fcb85..7dab18d457 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -27,8 +27,6 @@ #include "KTXCache.h" -const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; - namespace gpu { class Batch; } diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 8583b59c89..c66fe8daf0 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -313,6 +313,9 @@ void AddressManager::handleAPIResponse(QNetworkReply& requestReply) { QJsonObject responseObject = QJsonDocument::fromJson(requestReply.readAll()).object(); QJsonObject dataObject = responseObject["data"].toObject(); + // Lookup succeeded, don't keep re-trying it (especially on server restarts) + _previousLookup.clear(); + if (!dataObject.isEmpty()) { goToAddressFromObject(dataObject.toVariantMap(), requestReply); } else if (responseObject.contains(DATA_OBJECT_DOMAIN_KEY)) { @@ -739,6 +742,8 @@ 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); + } else { + handleUrl(currentAddress(), LookupTrigger::AttemptedRefresh); } } diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp index 0a859d511b..8f3509d8f3 100644 --- a/libraries/networking/src/FileCache.cpp +++ b/libraries/networking/src/FileCache.cpp @@ -17,6 +17,7 @@ #include #include +#include #include @@ -96,7 +97,7 @@ FilePointer FileCache::addFile(Metadata&& metadata, const std::string& filepath) return file; } -FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) { +FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata, bool overwrite) { assert(_initialized); std::string filepath = getFilepath(metadata.key); @@ -106,17 +107,23 @@ FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) { // if file already exists, return it FilePointer file = getFile(metadata.key); if (file) { - qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), metadata.key.c_str()); - return file; + if (!overwrite) { + qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), metadata.key.c_str()); + return file; + } else { + qCWarning(file_cache, "[%s] Overwriting %s", _dirname.c_str(), metadata.key.c_str()); + file.reset(); + } } - // write the new file - FILE* saveFile = fopen(filepath.c_str(), "wb"); - if (saveFile != nullptr && fwrite(data, metadata.length, 1, saveFile) && fclose(saveFile) == 0) { + QSaveFile saveFile(QString::fromStdString(filepath)); + if (saveFile.open(QIODevice::WriteOnly) + && saveFile.write(data, metadata.length) == static_cast(metadata.length) + && saveFile.commit()) { + file = addFile(std::move(metadata), filepath); } else { - qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), metadata.key.c_str(), strerror(errno)); - errno = 0; + qCWarning(file_cache, "[%s] Failed to write %s", _dirname.c_str(), metadata.key.c_str()); } return file; diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h index f77db555bc..908ddcd285 100644 --- a/libraries/networking/src/FileCache.h +++ b/libraries/networking/src/FileCache.h @@ -80,7 +80,7 @@ protected: /// must be called after construction to create the cache on the fs and restore persisted files void initialize(); - FilePointer writeFile(const char* data, Metadata&& metadata); + FilePointer writeFile(const char* data, Metadata&& metadata, bool overwrite = false); FilePointer getFile(const Key& key); /// create a file diff --git a/libraries/networking/src/FingerprintUtils.cpp b/libraries/networking/src/FingerprintUtils.cpp index 1990d356b6..216e0f28dd 100644 --- a/libraries/networking/src/FingerprintUtils.cpp +++ b/libraries/networking/src/FingerprintUtils.cpp @@ -19,8 +19,8 @@ #include #ifdef Q_OS_WIN -#include -#include +#include +#include #endif //Q_OS_WIN #ifdef Q_OS_MAC @@ -30,6 +30,9 @@ #endif //Q_OS_MAC static const QString FALLBACK_FINGERPRINT_KEY = "fallbackFingerprint"; + +QUuid FingerprintUtils::_machineFingerprint { QUuid() }; + QString FingerprintUtils::getMachineFingerprintString() { QString uuidString; #ifdef Q_OS_LINUX @@ -47,122 +50,32 @@ QString FingerprintUtils::getMachineFingerprintString() { #endif //Q_OS_MAC #ifdef Q_OS_WIN - HRESULT hres; - IWbemLocator *pLoc = NULL; - - // initialize com. Interface already does, but other - // users of this lib don't necessarily do so. - hres = CoInitializeEx(0, COINIT_MULTITHREADED); - if (FAILED(hres)) { - qCDebug(networking) << "Failed to initialize COM library!"; - return uuidString; - } + HKEY cryptoKey; - // initialize WbemLocator - hres = CoCreateInstance( - CLSID_WbemLocator, - 0, - CLSCTX_INPROC_SERVER, - IID_IWbemLocator, (LPVOID *) &pLoc); + // try and open the key that contains the machine GUID + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &cryptoKey) == ERROR_SUCCESS) { + DWORD type; + DWORD guidSize; - if (FAILED(hres)) { - qCDebug(networking) << "Failed to initialize WbemLocator"; - return uuidString; - } - - // Connect to WMI through the IWbemLocator::ConnectServer method - IWbemServices *pSvc = NULL; + const char* MACHINE_GUID_KEY = "MachineGuid"; - // Connect to the root\cimv2 namespace with - // the current user and obtain pointer pSvc - // to make IWbemServices calls. - hres = pLoc->ConnectServer( - _bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace - NULL, // User name. NULL = current user - NULL, // User password. NULL = current - 0, // Locale. NULL indicates current - NULL, // Security flags. - 0, // Authority (for example, Kerberos) - 0, // Context object - &pSvc // pointer to IWbemServices proxy - ); + // try and retrieve the size of the GUID value + if (RegQueryValueEx(cryptoKey, MACHINE_GUID_KEY, NULL, &type, NULL, &guidSize) == ERROR_SUCCESS) { + // make sure that the value is a string + if (type == REG_SZ) { + // retrieve the machine GUID and return that as our UUID string + std::string machineGUID(guidSize / sizeof(char), '\0'); - if (FAILED(hres)) { - pLoc->Release(); - qCDebug(networking) << "Failed to connect to WMI"; - return uuidString; - } - - // Set security levels on the proxy - hres = CoSetProxyBlanket( - pSvc, // Indicates the proxy to set - RPC_C_AUTHN_WINNT, // RPC_C_AUTHN_xxx - RPC_C_AUTHZ_NONE, // RPC_C_AUTHZ_xxx - NULL, // Server principal name - RPC_C_AUTHN_LEVEL_CALL, // RPC_C_AUTHN_LEVEL_xxx - RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx - NULL, // client identity - EOAC_NONE // proxy capabilities - ); - - if (FAILED(hres)) { - pSvc->Release(); - pLoc->Release(); - qCDebug(networking) << "Failed to set security on proxy blanket"; - return uuidString; - } - - // Use the IWbemServices pointer to grab the Win32_BIOS stuff - IEnumWbemClassObject* pEnumerator = NULL; - hres = pSvc->ExecQuery( - bstr_t("WQL"), - bstr_t("SELECT * FROM Win32_ComputerSystemProduct"), - WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, - NULL, - &pEnumerator); - - if (FAILED(hres)) { - pSvc->Release(); - pLoc->Release(); - qCDebug(networking) << "query to get Win32_ComputerSystemProduct info"; - return uuidString; - } - - // Get the SerialNumber from the Win32_BIOS data - IWbemClassObject *pclsObj; - ULONG uReturn = 0; - - SHORT sRetStatus = -100; - - while (pEnumerator) { - HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn); - - if(0 == uReturn){ - break; - } - - VARIANT vtProp; - - // Get the value of the Name property - hr = pclsObj->Get(L"UUID", 0, &vtProp, 0, 0); - if (!FAILED(hres)) { - switch (vtProp.vt) { - case VT_BSTR: - uuidString = QString::fromWCharArray(vtProp.bstrVal); - break; + if (RegQueryValueEx(cryptoKey, MACHINE_GUID_KEY, NULL, NULL, + reinterpret_cast(&machineGUID[0]), &guidSize) == ERROR_SUCCESS) { + uuidString = QString::fromStdString(machineGUID); + } } } - VariantClear(&vtProp); - pclsObj->Release(); + RegCloseKey(cryptoKey); } - pEnumerator->Release(); - // Cleanup - pSvc->Release(); - pLoc->Release(); - - qCDebug(networking) << "Windows BIOS UUID: " << uuidString; #endif //Q_OS_WIN return uuidString; @@ -171,29 +84,36 @@ QString FingerprintUtils::getMachineFingerprintString() { QUuid FingerprintUtils::getMachineFingerprint() { - QString uuidString = getMachineFingerprintString(); + if (_machineFingerprint.isNull()) { + QString uuidString = getMachineFingerprintString(); + + // now, turn into uuid. A malformed string will + // return QUuid() ("{00000...}"), which handles + // any errors in getting the string + QUuid uuid(uuidString); - // now, turn into uuid. A malformed string will - // return QUuid() ("{00000...}"), which handles - // any errors in getting the string - QUuid uuid(uuidString); - if (uuid == QUuid()) { - // if you cannot read a fallback key cuz we aren't saving them, just generate one for - // this session and move on - if (DependencyManager::get().isNull()) { - return QUuid::createUuid(); - } - // read fallback key (if any) - Settings settings; - uuid = QUuid(settings.value(FALLBACK_FINGERPRINT_KEY).toString()); - qCDebug(networking) << "read fallback maching fingerprint: " << uuid.toString(); if (uuid == QUuid()) { - // no fallback yet, set one - uuid = QUuid::createUuid(); - settings.setValue(FALLBACK_FINGERPRINT_KEY, uuid.toString()); - qCDebug(networking) << "no fallback machine fingerprint, setting it to: " << uuid.toString(); + // if you cannot read a fallback key cuz we aren't saving them, just generate one for + // this session and move on + if (DependencyManager::get().isNull()) { + return QUuid::createUuid(); + } + // read fallback key (if any) + Settings settings; + uuid = QUuid(settings.value(FALLBACK_FINGERPRINT_KEY).toString()); + qCDebug(networking) << "read fallback maching fingerprint: " << uuid.toString(); + + if (uuid == QUuid()) { + // no fallback yet, set one + uuid = QUuid::createUuid(); + settings.setValue(FALLBACK_FINGERPRINT_KEY, uuid.toString()); + qCDebug(networking) << "no fallback machine fingerprint, setting it to: " << uuid.toString(); + } } + + _machineFingerprint = uuid; } - return uuid; + + return _machineFingerprint; } diff --git a/libraries/networking/src/FingerprintUtils.h b/libraries/networking/src/FingerprintUtils.h index 572b150ec4..c4cb900a48 100644 --- a/libraries/networking/src/FingerprintUtils.h +++ b/libraries/networking/src/FingerprintUtils.h @@ -21,6 +21,7 @@ public: private: static QString getMachineFingerprintString(); + static QUuid _machineFingerprint; }; #endif // hifi_FingerprintUtils_h diff --git a/libraries/networking/src/SandboxUtils.cpp b/libraries/networking/src/SandboxUtils.cpp index bf17a0b1e4..d816f7ebee 100644 --- a/libraries/networking/src/SandboxUtils.cpp +++ b/libraries/networking/src/SandboxUtils.cpp @@ -9,63 +9,52 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include -#include -#include -#include +#include "SandboxUtils.h" + #include #include #include #include -#include -#include -#include #include #include #include -#include "SandboxUtils.h" #include "NetworkAccessManager.h" #include "NetworkLogging.h" +namespace SandboxUtils { -void SandboxUtils::ifLocalSandboxRunningElse(std::function localSandboxRunningDoThis, - std::function localSandboxNotRunningDoThat) { - - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); +QNetworkReply* getStatus() { + auto& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest sandboxStatus(SANDBOX_STATUS_URL); sandboxStatus.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); sandboxStatus.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(sandboxStatus); - - connect(reply, &QNetworkReply::finished, this, [reply, localSandboxRunningDoThis, localSandboxNotRunningDoThat]() { - if (reply->error() == QNetworkReply::NoError) { - auto statusData = reply->readAll(); - auto statusJson = QJsonDocument::fromJson(statusData); - if (!statusJson.isEmpty()) { - auto statusObject = statusJson.object(); - auto serversValue = statusObject.value("servers"); - if (!serversValue.isUndefined() && serversValue.isObject()) { - auto serversObject = serversValue.toObject(); - auto serversCount = serversObject.size(); - const int MINIMUM_EXPECTED_SERVER_COUNT = 5; - if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) { - localSandboxRunningDoThis(); - return; - } - } - } - } - localSandboxNotRunningDoThat(); - }); + return networkAccessManager.get(sandboxStatus); } +bool readStatus(QByteArray statusData) { + auto statusJson = QJsonDocument::fromJson(statusData); -void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater) { - QString applicationDirPath = QFileInfo(QCoreApplication::applicationFilePath()).path(); - QString serverPath = applicationDirPath + "/server-console/server-console.exe"; - qCDebug(networking) << "Application dir path is: " << applicationDirPath; + if (!statusJson.isEmpty()) { + auto statusObject = statusJson.object(); + auto serversValue = statusObject.value("servers"); + if (!serversValue.isUndefined() && serversValue.isObject()) { + auto serversObject = serversValue.toObject(); + auto serversCount = serversObject.size(); + const int MINIMUM_EXPECTED_SERVER_COUNT = 5; + if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) { + return true; + } + } + } + + return false; +} + +void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater) { + QString serverPath = "./server-console/server-console.exe"; + qCDebug(networking) << "Running marker path is: " << runningMarkerName; qCDebug(networking) << "Server path is: " << serverPath; qCDebug(networking) << "autoShutdown: " << autoShutdown; qCDebug(networking) << "noUpdater: " << noUpdater; @@ -80,7 +69,7 @@ void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QStri } if (hasContentPath) { - QString serverContentPath = applicationDirPath + "/" + contentPath; + QString serverContentPath = "./" + contentPath; args << "--contentPath" << serverContentPath; } @@ -93,10 +82,8 @@ void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QStri args << "--noUpdater"; } - qCDebug(networking) << applicationDirPath; qCDebug(networking) << "Launching sandbox with:" << args; qCDebug(networking) << QProcess::startDetached(serverPath, args); - - // Sleep a short amount of time to give the server a chance to start - usleep(2000000); /// do we really need this?? +} + } diff --git a/libraries/networking/src/SandboxUtils.h b/libraries/networking/src/SandboxUtils.h index aaceafb9ef..42484b8edf 100644 --- a/libraries/networking/src/SandboxUtils.h +++ b/libraries/networking/src/SandboxUtils.h @@ -12,21 +12,16 @@ #ifndef hifi_SandboxUtils_h #define hifi_SandboxUtils_h -#include -#include +#include +class QNetworkReply; -const QString SANDBOX_STATUS_URL = "http://localhost:60332/status"; +namespace SandboxUtils { + const QString SANDBOX_STATUS_URL = "http://localhost:60332/status"; -class SandboxUtils : public QObject { - Q_OBJECT -public: - /// determines if the local sandbox is likely running. It does not account for custom setups, and is only - /// intended to detect the standard local sandbox install. - void ifLocalSandboxRunningElse(std::function localSandboxRunningDoThis, - std::function localSandboxNotRunningDoThat); - - static void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater); + QNetworkReply* getStatus(); + bool readStatus(QByteArray statusData); + void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater); }; #endif // hifi_SandboxUtils_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 82b4bf703d..9d970fa318 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -56,7 +56,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::IdentityPacketsIncludeUpdateTime); + return static_cast(AvatarMixerPacketVersion::AvatarIdentitySequenceId); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); case PacketType::ICEServerHeartbeat: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 746ae80361..f88015a4e4 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -234,7 +234,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { VariableAvatarData, AvatarAsChildFixes, StickAndBallDefaultAvatar, - IdentityPacketsIncludeUpdateTime + IdentityPacketsIncludeUpdateTime, + AvatarIdentitySequenceId }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/physics/src/BulletUtil.h b/libraries/physics/src/BulletUtil.h index b6fac74617..c456ed8af8 100644 --- a/libraries/physics/src/BulletUtil.h +++ b/libraries/physics/src/BulletUtil.h @@ -1,6 +1,6 @@ // // BulletUtil.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.11.02 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp old mode 100644 new mode 100755 index 5c85f8fc50..ee240a6aac --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -1,6 +1,6 @@ // // CharacterControllerInterface.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2015.10.21 // Copyright 2015 High Fidelity, Inc. @@ -13,8 +13,8 @@ #include -#include "PhysicsCollisionGroups.h" #include "ObjectMotionState.h" +#include "PhysicsHelpers.h" #include "PhysicsLogging.h" const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f); @@ -62,10 +62,6 @@ CharacterController::CharacterMotor::CharacterMotor(const glm::vec3& vel, const } CharacterController::CharacterController() { - _halfHeight = 1.0f; - - _enabled = false; - _floorDistance = MAX_FALL_HEIGHT; _targetVelocity.setValue(0.0f, 0.0f, 0.0f); @@ -107,6 +103,7 @@ bool CharacterController::needsAddition() const { void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { if (_dynamicsWorld != world) { + // remove from old world if (_dynamicsWorld) { if (_rigidBody) { _dynamicsWorld->removeRigidBody(_rigidBody); @@ -114,16 +111,27 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { } _dynamicsWorld = nullptr; } + int16_t collisionGroup = computeCollisionGroup(); + if (_rigidBody) { + updateMassProperties(); + } if (world && _rigidBody) { + // add to new world _dynamicsWorld = world; _pendingFlags &= ~PENDING_FLAG_JUMP; - // Before adding the RigidBody to the world we must save its oldGravity to the side - // because adding an object to the world will overwrite it with the default gravity. - btVector3 oldGravity = _rigidBody->getGravity(); - _dynamicsWorld->addRigidBody(_rigidBody, BULLET_COLLISION_GROUP_MY_AVATAR, BULLET_COLLISION_MASK_MY_AVATAR); + _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR); _dynamicsWorld->addAction(this); - // restore gravity settings - _rigidBody->setGravity(oldGravity); + // restore gravity settings because adding an object to the world overwrites its gravity setting + _rigidBody->setGravity(_gravity * _currentUp); + btCollisionShape* shape = _rigidBody->getCollisionShape(); + assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE); + _ghost.setCharacterShape(static_cast(shape)); + } + _ghost.setCollisionGroupAndMask(collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR & (~ collisionGroup)); + _ghost.setCollisionWorld(_dynamicsWorld); + _ghost.setRadiusAndHalfHeight(_radius, _halfHeight); + if (_rigidBody) { + _ghost.setWorldTransform(_rigidBody->getWorldTransform()); } } if (_dynamicsWorld) { @@ -138,38 +146,78 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { } } -static const float COS_PI_OVER_THREE = cosf(PI / 3.0f); +bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) { + bool pushing = _targetVelocity.length2() > FLT_EPSILON; + + btDispatcher* dispatcher = collisionWorld->getDispatcher(); + int numManifolds = dispatcher->getNumManifolds(); + bool hasFloor = false; + + btTransform rotation = _rigidBody->getWorldTransform(); + rotation.setOrigin(btVector3(0.0f, 0.0f, 0.0f)); // clear translation part -bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) const { - int numManifolds = collisionWorld->getDispatcher()->getNumManifolds(); for (int i = 0; i < numManifolds; i++) { - btPersistentManifold* contactManifold = collisionWorld->getDispatcher()->getManifoldByIndexInternal(i); - const btCollisionObject* obA = static_cast(contactManifold->getBody0()); - const btCollisionObject* obB = static_cast(contactManifold->getBody1()); - if (obA == _rigidBody || obB == _rigidBody) { + btPersistentManifold* contactManifold = dispatcher->getManifoldByIndexInternal(i); + if (_rigidBody == contactManifold->getBody1() || _rigidBody == contactManifold->getBody0()) { + bool characterIsFirst = _rigidBody == contactManifold->getBody0(); int numContacts = contactManifold->getNumContacts(); + int stepContactIndex = -1; + float highestStep = _minStepHeight; for (int j = 0; j < numContacts; j++) { - btManifoldPoint& pt = contactManifold->getContactPoint(j); - - // check to see if contact point is touching the bottom sphere of the capsule. - // and the contact normal is not slanted too much. - float contactPointY = (obA == _rigidBody) ? pt.m_localPointA.getY() : pt.m_localPointB.getY(); - btVector3 normal = (obA == _rigidBody) ? pt.m_normalWorldOnB : -pt.m_normalWorldOnB; - if (contactPointY < -_halfHeight && normal.dot(_currentUp) > COS_PI_OVER_THREE) { - return true; + // check for "floor" + btManifoldPoint& contact = contactManifold->getContactPoint(j); + btVector3 pointOnCharacter = characterIsFirst ? contact.m_localPointA : contact.m_localPointB; // object-local-frame + btVector3 normal = characterIsFirst ? contact.m_normalWorldOnB : -contact.m_normalWorldOnB; // points toward character + btScalar hitHeight = _halfHeight + _radius + pointOnCharacter.dot(_currentUp); + if (hitHeight < _maxStepHeight && normal.dot(_currentUp) > _minFloorNormalDotUp) { + hasFloor = true; + if (!pushing) { + // we're not pushing against anything so we can early exit + // (all we need to know is that there is a floor) + break; + } } + if (pushing && _targetVelocity.dot(normal) < 0.0f) { + // remember highest step obstacle + if (!_stepUpEnabled || hitHeight > _maxStepHeight) { + // this manifold is invalidated by point that is too high + stepContactIndex = -1; + break; + } else if (hitHeight > highestStep && normal.dot(_targetVelocity) < 0.0f ) { + highestStep = hitHeight; + stepContactIndex = j; + hasFloor = true; + } + } + } + if (stepContactIndex > -1 && highestStep > _stepHeight) { + // remember step info for later + btManifoldPoint& contact = contactManifold->getContactPoint(stepContactIndex); + btVector3 pointOnCharacter = characterIsFirst ? contact.m_localPointA : contact.m_localPointB; // object-local-frame + _stepNormal = characterIsFirst ? contact.m_normalWorldOnB : -contact.m_normalWorldOnB; // points toward character + _stepHeight = highestStep; + _stepPoint = rotation * pointOnCharacter; // rotate into world-frame + } + if (hasFloor && !(pushing && _stepUpEnabled)) { + // early exit since all we need to know is that we're on a floor + break; } } } - return false; + return hasFloor; +} + +void CharacterController::updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) { + preStep(collisionWorld); + playerStep(collisionWorld, deltaTime); } void CharacterController::preStep(btCollisionWorld* collisionWorld) { // trace a ray straight down to see if we're standing on the ground - const btTransform& xform = _rigidBody->getWorldTransform(); + const btTransform& transform = _rigidBody->getWorldTransform(); // rayStart is at center of bottom sphere - btVector3 rayStart = xform.getOrigin() - _halfHeight * _currentUp; + btVector3 rayStart = transform.getOrigin() - _halfHeight * _currentUp; // rayEnd is some short distance outside bottom sphere const btScalar FLOOR_PROXIMITY_THRESHOLD = 0.3f * _radius; @@ -183,21 +231,16 @@ void CharacterController::preStep(btCollisionWorld* collisionWorld) { if (rayCallback.hasHit()) { _floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius; } - - _hasSupport = checkForSupport(collisionWorld); } const btScalar MIN_TARGET_SPEED = 0.001f; const btScalar MIN_TARGET_SPEED_SQUARED = MIN_TARGET_SPEED * MIN_TARGET_SPEED; -void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) { +void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar dt) { + _stepHeight = _minStepHeight; // clears memory of last step obstacle + _hasSupport = checkForSupport(collisionWorld); btVector3 velocity = _rigidBody->getLinearVelocity() - _parentVelocity; computeNewVelocity(dt, velocity); - _rigidBody->setLinearVelocity(velocity + _parentVelocity); - - // Dynamicaly compute a follow velocity to move this body toward the _followDesiredBodyTransform. - // Rather than add this velocity to velocity the RigidBody, we explicitly teleport the RigidBody towards its goal. - // This mirrors the computation done in MyAvatar::FollowHelper::postPhysicsUpdate(). const float MINIMUM_TIME_REMAINING = 0.005f; const float MAX_DISPLACEMENT = 0.5f * _radius; @@ -231,6 +274,47 @@ void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) { _rigidBody->setWorldTransform(btTransform(endRot, endPos)); } _followTime += dt; + + if (_steppingUp) { + float horizontalTargetSpeed = (_targetVelocity - _targetVelocity.dot(_currentUp) * _currentUp).length(); + if (horizontalTargetSpeed > FLT_EPSILON) { + // compute a stepUpSpeed that will reach the top of the step in the time it would take + // to move over the _stepPoint at target speed + float horizontalDistance = (_stepPoint - _stepPoint.dot(_currentUp) * _currentUp).length(); + float timeToStep = horizontalDistance / horizontalTargetSpeed; + float stepUpSpeed = _stepHeight / timeToStep; + + // magically clamp stepUpSpeed to a fraction of horizontalTargetSpeed + // to prevent the avatar from moving unreasonably fast according to human eye + const float MAX_STEP_UP_SPEED = 0.65f * horizontalTargetSpeed; + if (stepUpSpeed > MAX_STEP_UP_SPEED) { + stepUpSpeed = MAX_STEP_UP_SPEED; + } + + // add minimum velocity to counteract gravity's displacement during one step + // Note: the 0.5 factor comes from the fact that we really want the + // average velocity contribution from gravity during the step + stepUpSpeed -= 0.5f * _gravity * timeToStep; // remember: _gravity is negative scalar + + btScalar vDotUp = velocity.dot(_currentUp); + if (vDotUp < stepUpSpeed) { + // character doesn't have enough upward velocity to cover the step so we help using a "sky hook" + // which uses micro-teleports rather than applying real velocity + // to prevent the avatar from popping up after the step is done + btTransform transform = _rigidBody->getWorldTransform(); + transform.setOrigin(transform.getOrigin() + (dt * stepUpSpeed) * _currentUp); + _rigidBody->setWorldTransform(transform); + } + + // don't allow the avatar to fall downward when stepping up + // since otherwise this would tend to defeat the step-up behavior + if (vDotUp < 0.0f) { + velocity -= vDotUp * _currentUp; + } + } + } + _rigidBody->setLinearVelocity(velocity + _parentVelocity); + _ghost.setWorldTransform(_rigidBody->getWorldTransform()); } void CharacterController::jump() { @@ -272,95 +356,100 @@ void CharacterController::setState(State desiredState) { #ifdef DEBUG_STATE_CHANGE qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason; #endif - if (desiredState == State::Hover && _state != State::Hover) { - // hover enter - if (_rigidBody) { - _rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f)); - } - } else if (_state == State::Hover && desiredState != State::Hover) { - // hover exit - if (_rigidBody) { - _rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp); - } - } _state = desiredState; + updateGravity(); } } -void CharacterController::setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale) { - _boxScale = scale; +void CharacterController::updateGravity() { + int16_t collisionGroup = computeCollisionGroup(); + if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _gravity = 0.0f; + } else { + const float DEFAULT_CHARACTER_GRAVITY = -5.0f; + _gravity = DEFAULT_CHARACTER_GRAVITY; + } + if (_rigidBody) { + _rigidBody->setGravity(_gravity * _currentUp); + } +} - float x = _boxScale.x; - float z = _boxScale.z; +void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale) { + float x = scale.x; + float z = scale.z; float radius = 0.5f * sqrtf(0.5f * (x * x + z * z)); - float halfHeight = 0.5f * _boxScale.y - radius; + float halfHeight = 0.5f * scale.y - radius; float MIN_HALF_HEIGHT = 0.1f; if (halfHeight < MIN_HALF_HEIGHT) { halfHeight = MIN_HALF_HEIGHT; } // compare dimensions - float radiusDelta = glm::abs(radius - _radius); - float heightDelta = glm::abs(halfHeight - _halfHeight); - if (radiusDelta < FLT_EPSILON && heightDelta < FLT_EPSILON) { - // shape hasn't changed --> nothing to do - } else { + if (glm::abs(radius - _radius) > FLT_EPSILON || glm::abs(halfHeight - _halfHeight) > FLT_EPSILON) { + _radius = radius; + _halfHeight = halfHeight; + const btScalar DEFAULT_MIN_STEP_HEIGHT_FACTOR = 0.005f; + const btScalar DEFAULT_MAX_STEP_HEIGHT_FACTOR = 0.65f; + _minStepHeight = DEFAULT_MIN_STEP_HEIGHT_FACTOR * (_halfHeight + _radius); + _maxStepHeight = DEFAULT_MAX_STEP_HEIGHT_FACTOR * (_halfHeight + _radius); + if (_dynamicsWorld) { // must REMOVE from world prior to shape update _pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION; } _pendingFlags |= PENDING_FLAG_UPDATE_SHAPE; - // only need to ADD back when we happen to be enabled - if (_enabled) { - _pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION; - } + _pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION; } // it's ok to change offset immediately -- there are no thread safety issues here - _shapeLocalOffset = corner + 0.5f * _boxScale; + _shapeLocalOffset = minCorner + 0.5f * scale; } -void CharacterController::setEnabled(bool enabled) { - if (enabled != _enabled) { - if (enabled) { - // Don't bother clearing REMOVE bit since it might be paired with an UPDATE_SHAPE bit. - // Setting the ADD bit here works for all cases so we don't even bother checking other bits. - _pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION; - } else { - if (_dynamicsWorld) { - _pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION; - } - _pendingFlags &= ~ PENDING_FLAG_ADD_TO_SIMULATION; +void CharacterController::setCollisionless(bool collisionless) { + if (collisionless != _collisionless) { + _collisionless = collisionless; + _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP; + } +} + +int16_t CharacterController::computeCollisionGroup() const { + if (_collisionless) { + return _collisionlessAllowed ? BULLET_COLLISION_GROUP_COLLISIONLESS : BULLET_COLLISION_GROUP_MY_AVATAR; + } else { + return BULLET_COLLISION_GROUP_MY_AVATAR; + } +} + +void CharacterController::handleChangedCollisionGroup() { + if (_pendingFlags & PENDING_FLAG_UPDATE_COLLISION_GROUP) { + // ATM the easiest way to update collision groups is to remove/re-add the RigidBody + if (_dynamicsWorld) { + _dynamicsWorld->removeRigidBody(_rigidBody); + int16_t collisionGroup = computeCollisionGroup(); + _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR); } - SET_STATE(State::Hover, "setEnabled"); - _enabled = enabled; + _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP; + updateGravity(); } } void CharacterController::updateUpAxis(const glm::quat& rotation) { - btVector3 oldUp = _currentUp; _currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS); - if (_state != State::Hover) { - const btScalar MIN_UP_ERROR = 0.01f; - if (oldUp.distance(_currentUp) > MIN_UP_ERROR) { - _rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp); - } + if (_state != State::Hover && _rigidBody) { + _rigidBody->setGravity(_gravity * _currentUp); } } void CharacterController::setPositionAndOrientation( const glm::vec3& position, const glm::quat& orientation) { - // TODO: update gravity if up has changed updateUpAxis(orientation); - - btQuaternion bodyOrientation = glmToBullet(orientation); - btVector3 bodyPosition = glmToBullet(position + orientation * _shapeLocalOffset); - _characterBodyTransform = btTransform(bodyOrientation, bodyPosition); + _rotation = glmToBullet(orientation); + _position = glmToBullet(position + orientation * _shapeLocalOffset); } void CharacterController::getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const { - if (_enabled && _rigidBody) { + if (_rigidBody) { const btTransform& avatarTransform = _rigidBody->getWorldTransform(); rotation = bulletToGLM(avatarTransform.getRotation()); position = bulletToGLM(avatarTransform.getOrigin()) - rotation * _shapeLocalOffset; @@ -428,16 +517,19 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel btScalar angle = motor.rotation.getAngle(); btVector3 velocity = worldVelocity.rotate(axis, -angle); - if (_state == State::Hover || motor.hTimescale == motor.vTimescale) { + int16_t collisionGroup = computeCollisionGroup(); + if (collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS || + _state == State::Hover || motor.hTimescale == motor.vTimescale) { // modify velocity btScalar tau = dt / motor.hTimescale; if (tau > 1.0f) { tau = 1.0f; } - velocity += (motor.velocity - velocity) * tau; + velocity += tau * (motor.velocity - velocity); // rotate back into world-frame velocity = velocity.rotate(axis, angle); + _targetVelocity += (tau * motor.velocity).rotate(axis, angle); // store the velocity and weight velocities.push_back(velocity); @@ -445,12 +537,26 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel } else { // compute local UP btVector3 up = _currentUp.rotate(axis, -angle); + btVector3 motorVelocity = motor.velocity; + + // save these non-adjusted components for later + btVector3 vTargetVelocity = motorVelocity.dot(up) * up; + btVector3 hTargetVelocity = motorVelocity - vTargetVelocity; + + if (_stepHeight > _minStepHeight && !_steppingUp) { + // there is a step --> compute velocity direction to go over step + btVector3 motorVelocityWF = motorVelocity.rotate(axis, angle); + if (motorVelocityWF.dot(_stepNormal) < 0.0f) { + // the motor pushes against step + _steppingUp = true; + } + } // split velocity into horizontal and vertical components btVector3 vVelocity = velocity.dot(up) * up; btVector3 hVelocity = velocity - vVelocity; - btVector3 vTargetVelocity = motor.velocity.dot(up) * up; - btVector3 hTargetVelocity = motor.velocity - vTargetVelocity; + btVector3 vMotorVelocity = motorVelocity.dot(up) * up; + btVector3 hMotorVelocity = motorVelocity - vMotorVelocity; // modify each component separately btScalar maxTau = 0.0f; @@ -460,7 +566,7 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel tau = 1.0f; } maxTau = tau; - hVelocity += (hTargetVelocity - hVelocity) * tau; + hVelocity += (hMotorVelocity - hVelocity) * tau; } if (motor.vTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) { btScalar tau = dt / motor.vTimescale; @@ -470,11 +576,12 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel if (tau > maxTau) { maxTau = tau; } - vVelocity += (vTargetVelocity - vVelocity) * tau; + vVelocity += (vMotorVelocity - vVelocity) * tau; } // add components back together and rotate into world-frame velocity = (hVelocity + vVelocity).rotate(axis, angle); + _targetVelocity += maxTau * (hTargetVelocity + vTargetVelocity).rotate(axis, angle); // store velocity and weights velocities.push_back(velocity); @@ -492,6 +599,8 @@ void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) { velocities.reserve(_motors.size()); std::vector weights; weights.reserve(_motors.size()); + _targetVelocity = btVector3(0.0f, 0.0f, 0.0f); + _steppingUp = false; for (int i = 0; i < (int)_motors.size(); ++i) { applyMotor(i, dt, velocity, velocities, weights); } @@ -507,14 +616,18 @@ void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) { for (size_t i = 0; i < velocities.size(); ++i) { velocity += (weights[i] / totalWeight) * velocities[i]; } + _targetVelocity /= totalWeight; } if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) { velocity = btVector3(0.0f, 0.0f, 0.0f); } // 'thrust' is applied at the very end + _targetVelocity += dt * _linearAcceleration; velocity += dt * _linearAcceleration; - _targetVelocity = velocity; + // Note the differences between these two variables: + // _targetVelocity = ideal final velocity according to input + // velocity = real final velocity after motors are applied to current velocity } void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) { @@ -523,57 +636,60 @@ void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) { velocity = bulletToGLM(btVelocity); } -void CharacterController::preSimulation() { - if (_enabled && _dynamicsWorld && _rigidBody) { - quint64 now = usecTimestampNow(); +void CharacterController::updateState() { + if (!_dynamicsWorld) { + return; + } + const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius; + const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight; + const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND; + const btScalar MIN_HOVER_HEIGHT = 2.5f; + const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND; - // slam body to where it is supposed to be - _rigidBody->setWorldTransform(_characterBodyTransform); - btVector3 velocity = _rigidBody->getLinearVelocity(); - _preSimulationVelocity = velocity; + // scan for distant floor + // rayStart is at center of bottom sphere + btVector3 rayStart = _position; - // scan for distant floor - // rayStart is at center of bottom sphere - btVector3 rayStart = _characterBodyTransform.getOrigin(); + btScalar rayLength = _radius; + int16_t collisionGroup = computeCollisionGroup(); + if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) { + rayLength += MAX_FALL_HEIGHT; + } else { + rayLength += MIN_HOVER_HEIGHT; + } + btVector3 rayEnd = rayStart - rayLength * _currentUp; - // rayEnd is straight down MAX_FALL_HEIGHT - btScalar rayLength = _radius + MAX_FALL_HEIGHT; - btVector3 rayEnd = rayStart - rayLength * _currentUp; - - const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius; - const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight; - const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND; - const btScalar MIN_HOVER_HEIGHT = 2.5f; - const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND; - const btScalar MAX_WALKING_SPEED = 2.5f; + ClosestNotMe rayCallback(_rigidBody); + rayCallback.m_closestHitFraction = 1.0f; + _dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback); + bool rayHasHit = rayCallback.hasHit(); + quint64 now = usecTimestampNow(); + if (rayHasHit) { + _rayHitStartTime = now; + _floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight); + } else { const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND; - - ClosestNotMe rayCallback(_rigidBody); - rayCallback.m_closestHitFraction = 1.0f; - _dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback); - bool rayHasHit = rayCallback.hasHit(); - if (rayHasHit) { - _rayHitStartTime = now; - _floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight); - } else if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) { + if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) { rayHasHit = true; } else { _floorDistance = FLT_MAX; } + } - // record a time stamp when the jump button was first pressed. - if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) { - if (_pendingFlags & PENDING_FLAG_JUMP) { - _jumpButtonDownStartTime = now; - _jumpButtonDownCount++; - } + // record a time stamp when the jump button was first pressed. + bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP; + if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) { + if (_pendingFlags & PENDING_FLAG_JUMP) { + _jumpButtonDownStartTime = now; + _jumpButtonDownCount++; } + } - bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP; - - btVector3 actualHorizVelocity = velocity - velocity.dot(_currentUp) * _currentUp; - bool flyingFast = _state == State::Hover && actualHorizVelocity.length() > (MAX_WALKING_SPEED * 0.75f); + btVector3 velocity = _preSimulationVelocity; + // disable normal state transitions while collisionless + const btScalar MAX_WALKING_SPEED = 2.65f; + if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) { switch (_state) { case State::Ground: if (!rayHasHit && !_hasSupport) { @@ -613,6 +729,9 @@ void CharacterController::preSimulation() { break; } case State::Hover: + btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length(); + bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f); + if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { SET_STATE(State::InAir, "near ground"); } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) { @@ -620,6 +739,28 @@ void CharacterController::preSimulation() { } break; } + } else { + // when collisionless: only switch between State::Ground and State::Hover + // and bypass state debugging + if (rayHasHit) { + if (velocity.length() > (MAX_WALKING_SPEED)) { + _state = State::Hover; + } else { + _state = State::Ground; + } + } else { + _state = State::Hover; + } + } +} + +void CharacterController::preSimulation() { + if (_rigidBody) { + // slam body transform and remember velocity + _rigidBody->setWorldTransform(btTransform(btTransform(_rotation, _position))); + _preSimulationVelocity = _rigidBody->getLinearVelocity(); + + updateState(); } _previousFlags = _pendingFlags; @@ -631,14 +772,11 @@ void CharacterController::preSimulation() { } void CharacterController::postSimulation() { - // postSimulation() exists for symmetry and just in case we need to do something here later - if (_enabled && _dynamicsWorld && _rigidBody) { - btVector3 velocity = _rigidBody->getLinearVelocity(); - _velocityChange = velocity - _preSimulationVelocity; + if (_rigidBody) { + _velocityChange = _rigidBody->getLinearVelocity() - _preSimulationVelocity; } } - bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) { if (!_rigidBody) { return false; @@ -651,11 +789,17 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio } void CharacterController::setFlyingAllowed(bool value) { - if (_flyingAllowed != value) { + if (value != _flyingAllowed) { _flyingAllowed = value; - if (!_flyingAllowed && _state == State::Hover) { SET_STATE(State::InAir, "flying not allowed"); } } } + +void CharacterController::setCollisionlessAllowed(bool value) { + if (value != _collisionlessAllowed) { + _collisionlessAllowed = value; + _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP; + } +} diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index 586ea175e6..6790495ff8 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -1,6 +1,6 @@ // // CharacterControllerInterface.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2015.10.21 // Copyright 2015 High Fidelity, Inc. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_CharacterControllerInterface_h -#define hifi_CharacterControllerInterface_h +#ifndef hifi_CharacterController_h +#define hifi_CharacterController_h #include #include @@ -19,14 +19,18 @@ #include #include +#include +#include + #include "BulletUtil.h" +#include "CharacterGhostObject.h" const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0; const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1; const uint32_t PENDING_FLAG_UPDATE_SHAPE = 1U << 2; const uint32_t PENDING_FLAG_JUMP = 1U << 3; - -const float DEFAULT_CHARACTER_GRAVITY = -5.0f; +const uint32_t PENDING_FLAG_UPDATE_COLLISION_GROUP = 1U << 4; +const float DEFAULT_MIN_FLOOR_NORMAL_DOT_UP = cosf(PI / 3.0f); class btRigidBody; class btCollisionWorld; @@ -44,7 +48,7 @@ public: bool needsRemoval() const; bool needsAddition() const; - void setDynamicsWorld(btDynamicsWorld* world); + virtual void setDynamicsWorld(btDynamicsWorld* world); btCollisionObject* getCollisionObject() { return _rigidBody; } virtual void updateShapeIfNecessary() = 0; @@ -56,10 +60,7 @@ public: virtual void warp(const btVector3& origin) override { } virtual void debugDraw(btIDebugDraw* debugDrawer) override { } virtual void setUpInterpolate(bool value) override { } - virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override { - preStep(collisionWorld); - playerStep(collisionWorld, deltaTime); - } + virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override; virtual void preStep(btCollisionWorld *collisionWorld) override; virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override; virtual bool canJump() const override { assert(false); return false; } // never call this @@ -69,6 +70,7 @@ public: void clearMotors(); void addMotor(const glm::vec3& velocity, const glm::quat& rotation, float horizTimescale, float vertTimescale = -1.0f); void applyMotor(int index, btScalar dt, btVector3& worldVelocity, std::vector& velocities, std::vector& weights); + void setStepUpEnabled(bool enabled) { _stepUpEnabled = enabled; } void computeNewVelocity(btScalar dt, btVector3& velocity); void computeNewVelocity(btScalar dt, glm::vec3& velocity); @@ -103,16 +105,20 @@ public: }; State getState() const { return _state; } + void updateState(); - void setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale); + void setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale); - bool isEnabled() const { return _enabled; } // thread-safe - void setEnabled(bool enabled); - bool isEnabledAndReady() const { return _enabled && _dynamicsWorld; } + bool isEnabledAndReady() const { return _dynamicsWorld; } + + void setCollisionless(bool collisionless); + int16_t computeCollisionGroup() const; + void handleChangedCollisionGroup(); bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); void setFlyingAllowed(bool value); + void setCollisionlessAllowed(bool value); protected: @@ -122,8 +128,10 @@ protected: void setState(State state); #endif + virtual void updateMassProperties() = 0; + void updateGravity(); void updateUpAxis(const glm::quat& rotation); - bool checkForSupport(btCollisionWorld* collisionWorld) const; + bool checkForSupport(btCollisionWorld* collisionWorld); protected: struct CharacterMotor { @@ -136,6 +144,7 @@ protected: }; std::vector _motors; + CharacterGhostObject _ghost; btVector3 _currentUp; btVector3 _targetVelocity; btVector3 _parentVelocity; @@ -144,6 +153,8 @@ protected: btTransform _followDesiredBodyTransform; btScalar _followTimeRemaining; btTransform _characterBodyTransform; + btVector3 _position; + btQuaternion _rotation; glm::vec3 _shapeLocalOffset; @@ -155,13 +166,23 @@ protected: quint32 _jumpButtonDownCount; quint32 _takeoffJumpButtonID; - btScalar _halfHeight; - btScalar _radius; + // data for walking up steps + btVector3 _stepPoint { 0.0f, 0.0f, 0.0f }; + btVector3 _stepNormal { 0.0f, 0.0f, 0.0f }; + bool _steppingUp { false }; + btScalar _stepHeight { 0.0f }; + btScalar _minStepHeight { 0.0f }; + btScalar _maxStepHeight { 0.0f }; + btScalar _minFloorNormalDotUp { DEFAULT_MIN_FLOOR_NORMAL_DOT_UP }; + + btScalar _halfHeight { 0.0f }; + btScalar _radius { 0.0f }; btScalar _floorDistance; + bool _stepUpEnabled { true }; bool _hasSupport; - btScalar _gravity; + btScalar _gravity { 0.0f }; btScalar _jumpSpeed; btScalar _followTime; @@ -169,7 +190,6 @@ protected: btQuaternion _followAngularDisplacement; btVector3 _linearAcceleration; - std::atomic_bool _enabled; State _state; bool _isPushingUp; @@ -179,6 +199,8 @@ protected: uint32_t _previousFlags { 0 }; bool _flyingAllowed { true }; + bool _collisionlessAllowed { true }; + bool _collisionless { false }; }; -#endif // hifi_CharacterControllerInterface_h +#endif // hifi_CharacterController_h diff --git a/libraries/physics/src/CharacterGhostObject.cpp b/libraries/physics/src/CharacterGhostObject.cpp new file mode 100755 index 0000000000..331485dd01 --- /dev/null +++ b/libraries/physics/src/CharacterGhostObject.cpp @@ -0,0 +1,99 @@ +// +// CharacterGhostObject.cpp +// libraries/physics/src +// +// Created by Andrew Meadows 2016.08.26 +// 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 "CharacterGhostObject.h" + +#include +#include + +#include + +#include "CharacterRayResult.h" +#include "CharacterGhostShape.h" + + +CharacterGhostObject::~CharacterGhostObject() { + removeFromWorld(); + if (_ghostShape) { + delete _ghostShape; + _ghostShape = nullptr; + setCollisionShape(nullptr); + } +} + +void CharacterGhostObject::setCollisionGroupAndMask(int16_t group, int16_t mask) { + _collisionFilterGroup = group; + _collisionFilterMask = mask; + // TODO: if this probe is in the world reset ghostObject overlap cache +} + +void CharacterGhostObject::getCollisionGroupAndMask(int16_t& group, int16_t& mask) const { + group = _collisionFilterGroup; + mask = _collisionFilterMask; +} + +void CharacterGhostObject::setRadiusAndHalfHeight(btScalar radius, btScalar halfHeight) { + _radius = radius; + _halfHeight = halfHeight; +} + +// override of btCollisionObject::setCollisionShape() +void CharacterGhostObject::setCharacterShape(btConvexHullShape* shape) { + assert(shape); + // we create our own shape with an expanded Aabb for more reliable sweep tests + if (_ghostShape) { + delete _ghostShape; + } + + _ghostShape = new CharacterGhostShape(static_cast(shape)); + setCollisionShape(_ghostShape); +} + +void CharacterGhostObject::setCollisionWorld(btCollisionWorld* world) { + if (world != _world) { + removeFromWorld(); + _world = world; + addToWorld(); + } +} + +bool CharacterGhostObject::rayTest(const btVector3& start, + const btVector3& end, + CharacterRayResult& result) const { + if (_world && _inWorld) { + _world->rayTest(start, end, result); + } + return result.hasHit(); +} + +void CharacterGhostObject::refreshOverlappingPairCache() { + assert(_world && _inWorld); + btVector3 minAabb, maxAabb; + getCollisionShape()->getAabb(getWorldTransform(), minAabb, maxAabb); + // this updates both pairCaches: world broadphase and ghostobject + _world->getBroadphase()->setAabb(getBroadphaseHandle(), minAabb, maxAabb, _world->getDispatcher()); +} + +void CharacterGhostObject::removeFromWorld() { + if (_world && _inWorld) { + _world->removeCollisionObject(this); + _inWorld = false; + } +} + +void CharacterGhostObject::addToWorld() { + if (_world && !_inWorld) { + assert(getCollisionShape()); + setCollisionFlags(getCollisionFlags() | btCollisionObject::CF_NO_CONTACT_RESPONSE); + _world->addCollisionObject(this, _collisionFilterGroup, _collisionFilterMask); + _inWorld = true; + } +} diff --git a/libraries/physics/src/CharacterGhostObject.h b/libraries/physics/src/CharacterGhostObject.h new file mode 100755 index 0000000000..1e4625c6f6 --- /dev/null +++ b/libraries/physics/src/CharacterGhostObject.h @@ -0,0 +1,62 @@ +// +// CharacterGhostObject.h +// libraries/physics/src +// +// Created by Andrew Meadows 2016.08.26 +// 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_CharacterGhostObject_h +#define hifi_CharacterGhostObject_h + +#include + +#include +#include +#include + +#include "CharacterSweepResult.h" +#include "CharacterRayResult.h" + +class CharacterGhostShape; + +class CharacterGhostObject : public btPairCachingGhostObject { +public: + CharacterGhostObject() { } + ~CharacterGhostObject(); + + void setCollisionGroupAndMask(int16_t group, int16_t mask); + void getCollisionGroupAndMask(int16_t& group, int16_t& mask) const; + + void setRadiusAndHalfHeight(btScalar radius, btScalar halfHeight); + void setUpDirection(const btVector3& up); + + void setCharacterShape(btConvexHullShape* shape); + + void setCollisionWorld(btCollisionWorld* world); + + bool rayTest(const btVector3& start, + const btVector3& end, + CharacterRayResult& result) const; + + void refreshOverlappingPairCache(); + +protected: + void removeFromWorld(); + void addToWorld(); + +protected: + btCollisionWorld* _world { nullptr }; // input, pointer to world + btScalar _halfHeight { 0.0f }; + btScalar _radius { 0.0f }; + btConvexHullShape* _characterShape { nullptr }; // input, shape of character + CharacterGhostShape* _ghostShape { nullptr }; // internal, shape whose Aabb is used for overlap cache + int16_t _collisionFilterGroup { 0 }; + int16_t _collisionFilterMask { 0 }; + bool _inWorld { false }; // internal, was added to world +}; + +#endif // hifi_CharacterGhostObject_h diff --git a/libraries/physics/src/CharacterGhostShape.cpp b/libraries/physics/src/CharacterGhostShape.cpp new file mode 100644 index 0000000000..09f4f0b80f --- /dev/null +++ b/libraries/physics/src/CharacterGhostShape.cpp @@ -0,0 +1,31 @@ +// +// CharacterGhostShape.cpp +// libraries/physics/src +// +// Created by Andrew Meadows 2016.09.14 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "CharacterGhostShape.h" + +#include + + +CharacterGhostShape::CharacterGhostShape(const btConvexHullShape* shape) : + btConvexHullShape(reinterpret_cast(shape->getUnscaledPoints()), shape->getNumPoints(), sizeof(btVector3)) { + assert(shape); + assert(shape->getUnscaledPoints()); + assert(shape->getNumPoints() > 0); + setMargin(shape->getMargin()); +} + +void CharacterGhostShape::getAabb (const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const { + btConvexHullShape::getAabb(t, aabbMin, aabbMax); + // double the size of the Aabb by expanding both corners by half the extent + btVector3 expansion = 0.5f * (aabbMax - aabbMin); + aabbMin -= expansion; + aabbMax += expansion; +} diff --git a/libraries/physics/src/CharacterGhostShape.h b/libraries/physics/src/CharacterGhostShape.h new file mode 100644 index 0000000000..dc75c148d5 --- /dev/null +++ b/libraries/physics/src/CharacterGhostShape.h @@ -0,0 +1,25 @@ +// +// CharacterGhostShape.h +// libraries/physics/src +// +// Created by Andrew Meadows 2016.09.14 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_CharacterGhostShape_h +#define hifi_CharacterGhostShape_h + +#include + +class CharacterGhostShape : public btConvexHullShape { + // Same as btConvexHullShape but reports an expanded Aabb for larger ghost overlap cache +public: + CharacterGhostShape(const btConvexHullShape* shape); + + virtual void getAabb (const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const override; +}; + +#endif // hifi_CharacterGhostShape_h diff --git a/libraries/physics/src/CharacterRayResult.cpp b/libraries/physics/src/CharacterRayResult.cpp new file mode 100755 index 0000000000..7a81e9cca6 --- /dev/null +++ b/libraries/physics/src/CharacterRayResult.cpp @@ -0,0 +1,31 @@ +// +// CharaterRayResult.cpp +// libraries/physics/src +// +// Created by Andrew Meadows 2016.09.05 +// 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 "CharacterRayResult.h" + +#include + +#include "CharacterGhostObject.h" + +CharacterRayResult::CharacterRayResult (const CharacterGhostObject* character) : + btCollisionWorld::ClosestRayResultCallback(btVector3(0.0f, 0.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)), + _character(character) +{ + assert(_character); + _character->getCollisionGroupAndMask(m_collisionFilterGroup, m_collisionFilterMask); +} + +btScalar CharacterRayResult::addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) { + if (rayResult.m_collisionObject == _character) { + return 1.0f; + } + return ClosestRayResultCallback::addSingleResult (rayResult, normalInWorldSpace); +} diff --git a/libraries/physics/src/CharacterRayResult.h b/libraries/physics/src/CharacterRayResult.h new file mode 100644 index 0000000000..e8b0bb7f99 --- /dev/null +++ b/libraries/physics/src/CharacterRayResult.h @@ -0,0 +1,44 @@ +// +// CharaterRayResult.h +// libraries/physics/src +// +// Created by Andrew Meadows 2016.09.05 +// 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_CharacterRayResult_h +#define hifi_CharacterRayResult_h + +#include +#include + +class CharacterGhostObject; + +class CharacterRayResult : public btCollisionWorld::ClosestRayResultCallback { +public: + CharacterRayResult (const CharacterGhostObject* character); + + virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override; + +protected: + const CharacterGhostObject* _character; + + // Note: Public data members inherited from ClosestRayResultCallback + // + // btVector3 m_rayFromWorld;//used to calculate hitPointWorld from hitFraction + // btVector3 m_rayToWorld; + // btVector3 m_hitNormalWorld; + // btVector3 m_hitPointWorld; + // + // Note: Public data members inherited from RayResultCallback + // + // btScalar m_closestHitFraction; + // const btCollisionObject* m_collisionObject; + // short int m_collisionFilterGroup; + // short int m_collisionFilterMask; +}; + +#endif // hifi_CharacterRayResult_h diff --git a/libraries/physics/src/CharacterSweepResult.cpp b/libraries/physics/src/CharacterSweepResult.cpp new file mode 100755 index 0000000000..a5c4092b1d --- /dev/null +++ b/libraries/physics/src/CharacterSweepResult.cpp @@ -0,0 +1,42 @@ +// +// CharaterSweepResult.cpp +// libraries/physics/src +// +// Created by Andrew Meadows 2016.09.01 +// 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 "CharacterSweepResult.h" + +#include + +#include "CharacterGhostObject.h" + +CharacterSweepResult::CharacterSweepResult(const CharacterGhostObject* character) + : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0f, 0.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)), + _character(character) +{ + // set collision group and mask to match _character + assert(_character); + _character->getCollisionGroupAndMask(m_collisionFilterGroup, m_collisionFilterMask); +} + +btScalar CharacterSweepResult::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool useWorldFrame) { + // skip objects that we shouldn't collide with + if (!convexResult.m_hitCollisionObject->hasContactResponse()) { + return btScalar(1.0); + } + if (convexResult.m_hitCollisionObject == _character) { + return btScalar(1.0); + } + + return ClosestConvexResultCallback::addSingleResult(convexResult, useWorldFrame); +} + +void CharacterSweepResult::resetHitHistory() { + m_hitCollisionObject = nullptr; + m_closestHitFraction = btScalar(1.0f); +} diff --git a/libraries/physics/src/CharacterSweepResult.h b/libraries/physics/src/CharacterSweepResult.h new file mode 100644 index 0000000000..1e2898a3cf --- /dev/null +++ b/libraries/physics/src/CharacterSweepResult.h @@ -0,0 +1,45 @@ +// +// CharaterSweepResult.h +// libraries/physics/src +// +// Created by Andrew Meadows 2016.09.01 +// 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_CharacterSweepResult_h +#define hifi_CharacterSweepResult_h + +#include +#include + + +class CharacterGhostObject; + +class CharacterSweepResult : public btCollisionWorld::ClosestConvexResultCallback { +public: + CharacterSweepResult(const CharacterGhostObject* character); + virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool useWorldFrame) override; + void resetHitHistory(); +protected: + const CharacterGhostObject* _character; + + // NOTE: Public data members inherited from ClosestConvexResultCallback: + // + // btVector3 m_convexFromWorld; // unused except by btClosestNotMeConvexResultCallback + // btVector3 m_convexToWorld; // unused except by btClosestNotMeConvexResultCallback + // btVector3 m_hitNormalWorld; + // btVector3 m_hitPointWorld; + // const btCollisionObject* m_hitCollisionObject; + // + // NOTE: Public data members inherited from ConvexResultCallback: + // + // btScalar m_closestHitFraction; + // short int m_collisionFilterGroup; + // short int m_collisionFilterMask; + +}; + +#endif // hifi_CharacterSweepResult_h diff --git a/libraries/physics/src/CollisionRenderMeshCache.cpp b/libraries/physics/src/CollisionRenderMeshCache.cpp index 3a1c4d0ea4..40a8a4aff9 100644 --- a/libraries/physics/src/CollisionRenderMeshCache.cpp +++ b/libraries/physics/src/CollisionRenderMeshCache.cpp @@ -1,6 +1,6 @@ // // CollisionRenderMeshCache.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2016.07.13 // Copyright 2016 High Fidelity, Inc. diff --git a/libraries/physics/src/CollisionRenderMeshCache.h b/libraries/physics/src/CollisionRenderMeshCache.h index 910b43996e..6a6857a5ae 100644 --- a/libraries/physics/src/CollisionRenderMeshCache.h +++ b/libraries/physics/src/CollisionRenderMeshCache.h @@ -1,6 +1,6 @@ // // CollisionRenderMeshCache.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2016.07.13 // Copyright 2016 High Fidelity, Inc. diff --git a/libraries/physics/src/ContactInfo.cpp b/libraries/physics/src/ContactInfo.cpp index 085f746a73..7fdf6c854b 100644 --- a/libraries/physics/src/ContactInfo.cpp +++ b/libraries/physics/src/ContactInfo.cpp @@ -1,6 +1,6 @@ // // ContactEvent.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2015.01.20 // Copyright 2015 High Fidelity, Inc. diff --git a/libraries/physics/src/ContactInfo.h b/libraries/physics/src/ContactInfo.h index 8d05f73b61..39fc011420 100644 --- a/libraries/physics/src/ContactInfo.h +++ b/libraries/physics/src/ContactInfo.h @@ -1,6 +1,6 @@ // // ContactEvent.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2015.01.20 // Copyright 2015 High Fidelity, Inc. diff --git a/libraries/physics/src/ObjectAction.cpp b/libraries/physics/src/ObjectAction.cpp index 5f5f763ca6..de14a46be4 100644 --- a/libraries/physics/src/ObjectAction.cpp +++ b/libraries/physics/src/ObjectAction.cpp @@ -1,6 +1,6 @@ // // ObjectAction.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Seth Alves 2015-6-2 // Copyright 2015 High Fidelity, Inc. diff --git a/libraries/physics/src/ObjectAction.h b/libraries/physics/src/ObjectAction.h index fb141a4620..f71159ad88 100644 --- a/libraries/physics/src/ObjectAction.h +++ b/libraries/physics/src/ObjectAction.h @@ -1,6 +1,6 @@ // // ObjectAction.h -// libraries/physcis/src +// libraries/physics/src // // Created by Seth Alves 2015-6-2 // Copyright 2015 High Fidelity, Inc. diff --git a/libraries/physics/src/ObjectActionSpring.cpp b/libraries/physics/src/ObjectActionTractor.cpp similarity index 69% rename from libraries/physics/src/ObjectActionSpring.cpp rename to libraries/physics/src/ObjectActionTractor.cpp index 8c73f43d42..4bb5d850a9 100644 --- a/libraries/physics/src/ObjectActionSpring.cpp +++ b/libraries/physics/src/ObjectActionTractor.cpp @@ -1,9 +1,9 @@ // -// ObjectActionSpring.cpp +// ObjectActionTractor.cpp // libraries/physics/src // -// Created by Seth Alves 2015-6-5 -// Copyright 2015 High Fidelity, Inc. +// Created by Seth Alves 2015-5-8 +// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -11,18 +11,18 @@ #include "QVariantGLM.h" -#include "ObjectActionSpring.h" +#include "ObjectActionTractor.h" #include "PhysicsLogging.h" -const float SPRING_MAX_SPEED = 10.0f; -const float MAX_SPRING_TIMESCALE = 600.0f; // 10 min is a long time +const float TRACTOR_MAX_SPEED = 10.0f; +const float MAX_TRACTOR_TIMESCALE = 600.0f; // 10 min is a long time -const uint16_t ObjectActionSpring::springVersion = 1; +const uint16_t ObjectActionTractor::tractorVersion = 1; -ObjectActionSpring::ObjectActionSpring(const QUuid& id, EntityItemPointer ownerEntity) : - ObjectAction(DYNAMIC_TYPE_SPRING, id, ownerEntity), +ObjectActionTractor::ObjectActionTractor(const QUuid& id, EntityItemPointer ownerEntity) : + ObjectAction(DYNAMIC_TYPE_TRACTOR, id, ownerEntity), _positionalTarget(glm::vec3(0.0f)), _desiredPositionalTarget(glm::vec3(0.0f)), _linearTimeScale(FLT_MAX), @@ -32,52 +32,17 @@ ObjectActionSpring::ObjectActionSpring(const QUuid& id, EntityItemPointer ownerE _angularTimeScale(FLT_MAX), _rotationalTargetSet(true) { #if WANT_DEBUG - qCDebug(physics) << "ObjectActionSpring::ObjectActionSpring"; + qCDebug(physics) << "ObjectActionTractor::ObjectActionTractor"; #endif } -ObjectActionSpring::~ObjectActionSpring() { +ObjectActionTractor::~ObjectActionTractor() { #if WANT_DEBUG - qCDebug(physics) << "ObjectActionSpring::~ObjectActionSpring"; + qCDebug(physics) << "ObjectActionTractor::~ObjectActionTractor"; #endif } -SpatiallyNestablePointer ObjectActionSpring::getOther() { - SpatiallyNestablePointer other; - withWriteLock([&]{ - if (_otherID == QUuid()) { - // no other - return; - } - other = _other.lock(); - if (other && other->getID() == _otherID) { - // other is already up-to-date - return; - } - if (other) { - // we have a pointer to other, but it's wrong - other.reset(); - _other.reset(); - } - // we have an other-id but no pointer to other cached - QSharedPointer parentFinder = DependencyManager::get(); - if (!parentFinder) { - return; - } - EntityItemPointer ownerEntity = _ownerEntity.lock(); - if (!ownerEntity) { - return; - } - bool success; - _other = parentFinder->find(_otherID, success, ownerEntity->getParentTree()); - if (success) { - other = _other.lock(); - } - }); - return other; -} - -bool ObjectActionSpring::getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, +bool ObjectActionTractor::getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, glm::vec3& linearVelocity, glm::vec3& angularVelocity, float& linearTimeScale, float& angularTimeScale) { SpatiallyNestablePointer other = getOther(); @@ -90,7 +55,7 @@ bool ObjectActionSpring::getTarget(float deltaTimeStep, glm::quat& rotation, glm rotation = _desiredRotationalTarget * other->getRotation(); position = other->getRotation() * _desiredPositionalTarget + other->getPosition(); } else { - // we should have an "other" but can't find it, so disable the spring. + // we should have an "other" but can't find it, so disable the tractor. linearTimeScale = FLT_MAX; angularTimeScale = FLT_MAX; } @@ -104,7 +69,7 @@ bool ObjectActionSpring::getTarget(float deltaTimeStep, glm::quat& rotation, glm return true; } -bool ObjectActionSpring::prepareForSpringUpdate(btScalar deltaTimeStep) { +bool ObjectActionTractor::prepareForTractorUpdate(btScalar deltaTimeStep) { auto ownerEntity = _ownerEntity.lock(); if (!ownerEntity) { return false; @@ -116,59 +81,59 @@ bool ObjectActionSpring::prepareForSpringUpdate(btScalar deltaTimeStep) { glm::vec3 angularVelocity; bool linearValid = false; - int linearSpringCount = 0; + int linearTractorCount = 0; bool angularValid = false; - int angularSpringCount = 0; + int angularTractorCount = 0; - QList springDerivedActions; - springDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_SPRING)); - springDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_FAR_GRAB)); - springDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_HOLD)); + QList tractorDerivedActions; + tractorDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_TRACTOR)); + tractorDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_FAR_GRAB)); + tractorDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_HOLD)); - foreach (EntityDynamicPointer action, springDerivedActions) { - std::shared_ptr springAction = std::static_pointer_cast(action); + foreach (EntityDynamicPointer action, tractorDerivedActions) { + std::shared_ptr tractorAction = std::static_pointer_cast(action); glm::quat rotationForAction; glm::vec3 positionForAction; glm::vec3 linearVelocityForAction; glm::vec3 angularVelocityForAction; float linearTimeScale; float angularTimeScale; - bool success = springAction->getTarget(deltaTimeStep, + bool success = tractorAction->getTarget(deltaTimeStep, rotationForAction, positionForAction, linearVelocityForAction, angularVelocityForAction, linearTimeScale, angularTimeScale); if (success) { - if (angularTimeScale < MAX_SPRING_TIMESCALE) { + if (angularTimeScale < MAX_TRACTOR_TIMESCALE) { angularValid = true; - angularSpringCount++; + angularTractorCount++; angularVelocity += angularVelocityForAction; - if (springAction.get() == this) { + if (tractorAction.get() == this) { // only use the rotation for this action rotation = rotationForAction; } } - if (linearTimeScale < MAX_SPRING_TIMESCALE) { + if (linearTimeScale < MAX_TRACTOR_TIMESCALE) { linearValid = true; - linearSpringCount++; + linearTractorCount++; position += positionForAction; linearVelocity += linearVelocityForAction; } } } - if ((angularValid && angularSpringCount > 0) || (linearValid && linearSpringCount > 0)) { + if ((angularValid && angularTractorCount > 0) || (linearValid && linearTractorCount > 0)) { withWriteLock([&]{ - if (linearValid && linearSpringCount > 0) { - position /= linearSpringCount; - linearVelocity /= linearSpringCount; + if (linearValid && linearTractorCount > 0) { + position /= linearTractorCount; + linearVelocity /= linearTractorCount; _positionalTarget = position; _linearVelocityTarget = linearVelocity; _positionalTargetSet = true; _active = true; } - if (angularValid && angularSpringCount > 0) { - angularVelocity /= angularSpringCount; + if (angularValid && angularTractorCount > 0) { + angularVelocity /= angularTractorCount; _rotationalTarget = rotation; _angularVelocityTarget = angularVelocity; _rotationalTargetSet = true; @@ -181,8 +146,8 @@ bool ObjectActionSpring::prepareForSpringUpdate(btScalar deltaTimeStep) { } -void ObjectActionSpring::updateActionWorker(btScalar deltaTimeStep) { - if (!prepareForSpringUpdate(deltaTimeStep)) { +void ObjectActionTractor::updateActionWorker(btScalar deltaTimeStep) { + if (!prepareForTractorUpdate(deltaTimeStep)) { return; } @@ -199,16 +164,16 @@ void ObjectActionSpring::updateActionWorker(btScalar deltaTimeStep) { ObjectMotionState* motionState = static_cast(physicsInfo); btRigidBody* rigidBody = motionState->getRigidBody(); if (!rigidBody) { - qCDebug(physics) << "ObjectActionSpring::updateActionWorker no rigidBody"; + qCDebug(physics) << "ObjectActionTractor::updateActionWorker no rigidBody"; return; } - if (_linearTimeScale < MAX_SPRING_TIMESCALE) { + if (_linearTimeScale < MAX_TRACTOR_TIMESCALE) { btVector3 targetVelocity(0.0f, 0.0f, 0.0f); btVector3 offset = rigidBody->getCenterOfMassPosition() - glmToBullet(_positionalTarget); float offsetLength = offset.length(); if (offsetLength > FLT_EPSILON) { - float speed = glm::min(offsetLength / _linearTimeScale, SPRING_MAX_SPEED); + float speed = glm::min(offsetLength / _linearTimeScale, TRACTOR_MAX_SPEED); targetVelocity = (-speed / offsetLength) * offset; if (speed > rigidBody->getLinearSleepingThreshold()) { forceBodyNonStatic(); @@ -219,7 +184,7 @@ void ObjectActionSpring::updateActionWorker(btScalar deltaTimeStep) { rigidBody->setLinearVelocity(targetVelocity); } - if (_angularTimeScale < MAX_SPRING_TIMESCALE) { + if (_angularTimeScale < MAX_TRACTOR_TIMESCALE) { btVector3 targetVelocity(0.0f, 0.0f, 0.0f); btQuaternion bodyRotation = rigidBody->getOrientation(); @@ -253,7 +218,7 @@ void ObjectActionSpring::updateActionWorker(btScalar deltaTimeStep) { const float MIN_TIMESCALE = 0.1f; -bool ObjectActionSpring::updateArguments(QVariantMap arguments) { +bool ObjectActionTractor::updateArguments(QVariantMap arguments) { glm::vec3 positionalTarget; float linearTimeScale; glm::quat rotationalTarget; @@ -264,33 +229,33 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { bool needUpdate = false; bool somethingChanged = ObjectDynamic::updateArguments(arguments); withReadLock([&]{ - // targets are required, spring-constants are optional + // targets are required, tractor-constants are optional bool ok = true; - positionalTarget = EntityDynamicInterface::extractVec3Argument("spring action", arguments, "targetPosition", ok, false); + positionalTarget = EntityDynamicInterface::extractVec3Argument("tractor action", arguments, "targetPosition", ok, false); if (!ok) { positionalTarget = _desiredPositionalTarget; } ok = true; - linearTimeScale = EntityDynamicInterface::extractFloatArgument("spring action", arguments, "linearTimeScale", ok, false); + linearTimeScale = EntityDynamicInterface::extractFloatArgument("tractor action", arguments, "linearTimeScale", ok, false); if (!ok || linearTimeScale <= 0.0f) { linearTimeScale = _linearTimeScale; } ok = true; - rotationalTarget = EntityDynamicInterface::extractQuatArgument("spring action", arguments, "targetRotation", ok, false); + rotationalTarget = EntityDynamicInterface::extractQuatArgument("tractor action", arguments, "targetRotation", ok, false); if (!ok) { rotationalTarget = _desiredRotationalTarget; } ok = true; angularTimeScale = - EntityDynamicInterface::extractFloatArgument("spring action", arguments, "angularTimeScale", ok, false); + EntityDynamicInterface::extractFloatArgument("tractor action", arguments, "angularTimeScale", ok, false); if (!ok) { angularTimeScale = _angularTimeScale; } ok = true; - otherID = QUuid(EntityDynamicInterface::extractStringArgument("spring action", + otherID = QUuid(EntityDynamicInterface::extractStringArgument("tractor action", arguments, "otherID", ok, false)); if (!ok) { otherID = _otherID; @@ -328,7 +293,7 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { return true; } -QVariantMap ObjectActionSpring::getArguments() { +QVariantMap ObjectActionTractor::getArguments() { QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { arguments["linearTimeScale"] = _linearTimeScale; @@ -342,7 +307,7 @@ QVariantMap ObjectActionSpring::getArguments() { return arguments; } -void ObjectActionSpring::serializeParameters(QDataStream& dataStream) const { +void ObjectActionTractor::serializeParameters(QDataStream& dataStream) const { withReadLock([&] { dataStream << _desiredPositionalTarget; dataStream << _linearTimeScale; @@ -356,20 +321,20 @@ void ObjectActionSpring::serializeParameters(QDataStream& dataStream) const { }); } -QByteArray ObjectActionSpring::serialize() const { +QByteArray ObjectActionTractor::serialize() const { QByteArray serializedActionArguments; QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); - dataStream << DYNAMIC_TYPE_SPRING; + dataStream << DYNAMIC_TYPE_TRACTOR; dataStream << getID(); - dataStream << ObjectActionSpring::springVersion; + dataStream << ObjectActionTractor::tractorVersion; serializeParameters(dataStream); return serializedActionArguments; } -void ObjectActionSpring::deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream) { +void ObjectActionTractor::deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream) { withWriteLock([&] { dataStream >> _desiredPositionalTarget; dataStream >> _linearTimeScale; @@ -391,7 +356,7 @@ void ObjectActionSpring::deserializeParameters(QByteArray serializedArguments, Q }); } -void ObjectActionSpring::deserialize(QByteArray serializedArguments) { +void ObjectActionTractor::deserialize(QByteArray serializedArguments) { QDataStream dataStream(serializedArguments); EntityDynamicType type; @@ -404,7 +369,7 @@ void ObjectActionSpring::deserialize(QByteArray serializedArguments) { uint16_t serializationVersion; dataStream >> serializationVersion; - if (serializationVersion != ObjectActionSpring::springVersion) { + if (serializationVersion != ObjectActionTractor::tractorVersion) { assert(false); return; } diff --git a/libraries/physics/src/ObjectActionSpring.h b/libraries/physics/src/ObjectActionTractor.h similarity index 69% rename from libraries/physics/src/ObjectActionSpring.h rename to libraries/physics/src/ObjectActionTractor.h index 8f810d7956..c629d84998 100644 --- a/libraries/physics/src/ObjectActionSpring.h +++ b/libraries/physics/src/ObjectActionTractor.h @@ -1,23 +1,23 @@ // -// ObjectActionSpring.h +// ObjectActionTractor.h // libraries/physics/src // -// Created by Seth Alves 2015-6-5 -// Copyright 2015 High Fidelity, Inc. +// Created by Seth Alves 2017-5-8 +// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_ObjectActionSpring_h -#define hifi_ObjectActionSpring_h +#ifndef hifi_ObjectActionTractor_h +#define hifi_ObjectActionTractor_h #include "ObjectAction.h" -class ObjectActionSpring : public ObjectAction { +class ObjectActionTractor : public ObjectAction { public: - ObjectActionSpring(const QUuid& id, EntityItemPointer ownerEntity); - virtual ~ObjectActionSpring(); + ObjectActionTractor(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectActionTractor(); virtual bool updateArguments(QVariantMap arguments) override; virtual QVariantMap getArguments() override; @@ -32,7 +32,7 @@ public: float& linearTimeScale, float& angularTimeScale); protected: - static const uint16_t springVersion; + static const uint16_t tractorVersion; glm::vec3 _positionalTarget; glm::vec3 _desiredPositionalTarget; @@ -47,14 +47,10 @@ protected: glm::vec3 _linearVelocityTarget; glm::vec3 _angularVelocityTarget; - EntityItemID _otherID; - SpatiallyNestableWeakPointer _other; - SpatiallyNestablePointer getOther(); - - virtual bool prepareForSpringUpdate(btScalar deltaTimeStep); + virtual bool prepareForTractorUpdate(btScalar deltaTimeStep); void serializeParameters(QDataStream& dataStream) const; void deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream); }; -#endif // hifi_ObjectActionSpring_h +#endif // hifi_ObjectActionTractor_h diff --git a/libraries/physics/src/ObjectConstraintBallSocket.cpp b/libraries/physics/src/ObjectConstraintBallSocket.cpp index 35f138e840..9dd85954a3 100644 --- a/libraries/physics/src/ObjectConstraintBallSocket.cpp +++ b/libraries/physics/src/ObjectConstraintBallSocket.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "QVariantGLM.h" #include "EntityTree.h" @@ -40,7 +42,7 @@ QList ObjectConstraintBallSocket::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -76,13 +78,16 @@ btTypedConstraint* ObjectConstraintBallSocket::getConstraint() { withReadLock([&]{ constraint = static_cast(_constraint); pivotInA = _pivotInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pivotInB = _pivotInB; }); if (constraint) { return constraint; } + static QString repeatedBallSocketNoRigidBody = LogHandler::getInstance().addRepeatedMessageRegex( + "ObjectConstraintBallSocket::getConstraint -- no rigidBody.*"); + btRigidBody* rigidBodyA = getRigidBody(); if (!rigidBodyA) { qCDebug(physics) << "ObjectConstraintBallSocket::getConstraint -- no rigidBodyA"; @@ -94,6 +99,7 @@ btTypedConstraint* ObjectConstraintBallSocket::getConstraint() { btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); if (!rigidBodyB) { + qCDebug(physics) << "ObjectConstraintBallSocket::getConstraint -- no rigidBodyB"; return nullptr; } @@ -136,7 +142,7 @@ bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("ball-socket constraint", arguments, "otherEntityID", ok, false)); if (!ok) { - otherEntityID = _otherEntityID; + otherEntityID = _otherID; } ok = true; @@ -147,7 +153,7 @@ bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { if (somethingChanged || pivotInA != _pivotInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pivotInB != _pivotInB) { // something changed needUpdate = true; @@ -157,7 +163,7 @@ bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { if (needUpdate) { withWriteLock([&] { _pivotInA = pivotInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pivotInB = pivotInB; _active = true; @@ -178,11 +184,9 @@ bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { QVariantMap ObjectConstraintBallSocket::getArguments() { QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { - if (_constraint) { - arguments["pivot"] = glmToQMap(_pivotInA); - arguments["otherEntityID"] = _otherEntityID; - arguments["otherPivot"] = glmToQMap(_pivotInB); - } + arguments["pivot"] = glmToQMap(_pivotInA); + arguments["otherEntityID"] = _otherID; + arguments["otherPivot"] = glmToQMap(_pivotInB); }); return arguments; } @@ -200,7 +204,7 @@ QByteArray ObjectConstraintBallSocket::serialize() const { dataStream << _tag; dataStream << _pivotInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pivotInB; }); @@ -232,7 +236,7 @@ void ObjectConstraintBallSocket::deserialize(QByteArray serializedArguments) { dataStream >> _tag; dataStream >> _pivotInA; - dataStream >> _otherEntityID; + dataStream >> _otherID; dataStream >> _pivotInB; _active = true; diff --git a/libraries/physics/src/ObjectConstraintBallSocket.h b/libraries/physics/src/ObjectConstraintBallSocket.h index 9e0b942a6f..1c02fa736a 100644 --- a/libraries/physics/src/ObjectConstraintBallSocket.h +++ b/libraries/physics/src/ObjectConstraintBallSocket.h @@ -38,8 +38,6 @@ protected: void updateBallSocket(); glm::vec3 _pivotInA; - - EntityItemID _otherEntityID; glm::vec3 _pivotInB; }; diff --git a/libraries/physics/src/ObjectConstraintConeTwist.cpp b/libraries/physics/src/ObjectConstraintConeTwist.cpp index a0a9a5fe0c..49f926af81 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.cpp +++ b/libraries/physics/src/ObjectConstraintConeTwist.cpp @@ -9,20 +9,22 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "QVariantGLM.h" #include "EntityTree.h" #include "ObjectConstraintConeTwist.h" #include "PhysicsLogging.h" - -const uint16_t ObjectConstraintConeTwist::constraintVersion = 1; - +const uint16_t CONE_TWIST_VERSION_WITH_UNUSED_PAREMETERS = 1; +const uint16_t ObjectConstraintConeTwist::constraintVersion = 2; +const glm::vec3 DEFAULT_CONE_TWIST_AXIS(1.0f, 0.0f, 0.0f); ObjectConstraintConeTwist::ObjectConstraintConeTwist(const QUuid& id, EntityItemPointer ownerEntity) : ObjectConstraint(DYNAMIC_TYPE_CONE_TWIST, id, ownerEntity), - _pivotInA(glm::vec3(0.0f)), - _axisInA(glm::vec3(0.0f)) + _axisInA(DEFAULT_CONE_TWIST_AXIS), + _axisInB(DEFAULT_CONE_TWIST_AXIS) { #if WANT_DEBUG qCDebug(physics) << "ObjectConstraintConeTwist::ObjectConstraintConeTwist"; @@ -40,7 +42,7 @@ QList ObjectConstraintConeTwist::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -56,18 +58,12 @@ void ObjectConstraintConeTwist::updateConeTwist() { float swingSpan1; float swingSpan2; float twistSpan; - float softness; - float biasFactor; - float relaxationFactor; withReadLock([&]{ constraint = static_cast(_constraint); swingSpan1 = _swingSpan1; swingSpan2 = _swingSpan2; twistSpan = _twistSpan; - softness = _softness; - biasFactor = _biasFactor; - relaxationFactor = _relaxationFactor; }); if (!constraint) { @@ -76,10 +72,7 @@ void ObjectConstraintConeTwist::updateConeTwist() { constraint->setLimit(swingSpan1, swingSpan2, - twistSpan, - softness, - biasFactor, - relaxationFactor); + twistSpan); } @@ -95,7 +88,7 @@ btTypedConstraint* ObjectConstraintConeTwist::getConstraint() { constraint = static_cast(_constraint); pivotInA = _pivotInA; axisInA = _axisInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pivotInB = _pivotInB; axisInB = _axisInB; }); @@ -103,23 +96,41 @@ btTypedConstraint* ObjectConstraintConeTwist::getConstraint() { return constraint; } + static QString repeatedConeTwistNoRigidBody = LogHandler::getInstance().addRepeatedMessageRegex( + "ObjectConstraintConeTwist::getConstraint -- no rigidBody.*"); + btRigidBody* rigidBodyA = getRigidBody(); if (!rigidBodyA) { qCDebug(physics) << "ObjectConstraintConeTwist::getConstraint -- no rigidBodyA"; return nullptr; } + if (glm::length(axisInA) < FLT_EPSILON) { + qCWarning(physics) << "cone-twist axis cannot be a zero vector"; + axisInA = DEFAULT_CONE_TWIST_AXIS; + } else { + axisInA = glm::normalize(axisInA); + } + if (!otherEntityID.isNull()) { // This coneTwist is between two entities... find the other rigid body. - glm::quat rotA = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); - glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInB)); + if (glm::length(axisInB) < FLT_EPSILON) { + qCWarning(physics) << "cone-twist axis cannot be a zero vector"; + axisInB = DEFAULT_CONE_TWIST_AXIS; + } else { + axisInB = glm::normalize(axisInB); + } + + glm::quat rotA = glm::rotation(DEFAULT_CONE_TWIST_AXIS, axisInA); + glm::quat rotB = glm::rotation(DEFAULT_CONE_TWIST_AXIS, axisInB); btTransform frameInA(glmToBullet(rotA), glmToBullet(pivotInA)); btTransform frameInB(glmToBullet(rotB), glmToBullet(pivotInB)); btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); if (!rigidBodyB) { + qCDebug(physics) << "ObjectConstraintConeTwist::getConstraint -- no rigidBodyB"; return nullptr; } @@ -127,7 +138,7 @@ btTypedConstraint* ObjectConstraintConeTwist::getConstraint() { } else { // This coneTwist is between an entity and the world-frame. - glm::quat rot = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); + glm::quat rot = glm::rotation(DEFAULT_CONE_TWIST_AXIS, axisInA); btTransform frameInA(glmToBullet(rot), glmToBullet(pivotInA)); @@ -157,9 +168,6 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { float swingSpan1; float swingSpan2; float twistSpan; - float softness; - float biasFactor; - float relaxationFactor; bool needUpdate = false; bool somethingChanged = ObjectDynamic::updateArguments(arguments); @@ -180,7 +188,7 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("coneTwist constraint", arguments, "otherEntityID", ok, false)); if (!ok) { - otherEntityID = _otherEntityID; + otherEntityID = _otherID; } ok = true; @@ -213,37 +221,15 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { twistSpan = _twistSpan; } - ok = true; - softness = EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "softness", ok, false); - if (!ok) { - softness = _softness; - } - - ok = true; - biasFactor = EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "biasFactor", ok, false); - if (!ok) { - biasFactor = _biasFactor; - } - - ok = true; - relaxationFactor = - EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "relaxationFactor", ok, false); - if (!ok) { - relaxationFactor = _relaxationFactor; - } - if (somethingChanged || pivotInA != _pivotInA || axisInA != _axisInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pivotInB != _pivotInB || axisInB != _axisInB || swingSpan1 != _swingSpan1 || swingSpan2 != _swingSpan2 || - twistSpan != _twistSpan || - softness != _softness || - biasFactor != _biasFactor || - relaxationFactor != _relaxationFactor) { + twistSpan != _twistSpan) { // something changed needUpdate = true; } @@ -253,15 +239,12 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { withWriteLock([&] { _pivotInA = pivotInA; _axisInA = axisInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pivotInB = pivotInB; _axisInB = axisInB; _swingSpan1 = swingSpan1; _swingSpan2 = swingSpan2; _twistSpan = twistSpan; - _softness = softness; - _biasFactor = biasFactor; - _relaxationFactor = relaxationFactor; _active = true; @@ -281,19 +264,14 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { QVariantMap ObjectConstraintConeTwist::getArguments() { QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { - if (_constraint) { - arguments["pivot"] = glmToQMap(_pivotInA); - arguments["axis"] = glmToQMap(_axisInA); - arguments["otherEntityID"] = _otherEntityID; - arguments["otherPivot"] = glmToQMap(_pivotInB); - arguments["otherAxis"] = glmToQMap(_axisInB); - arguments["swingSpan1"] = _swingSpan1; - arguments["swingSpan2"] = _swingSpan2; - arguments["twistSpan"] = _twistSpan; - arguments["softness"] = _softness; - arguments["biasFactor"] = _biasFactor; - arguments["relaxationFactor"] = _relaxationFactor; - } + arguments["pivot"] = glmToQMap(_pivotInA); + arguments["axis"] = glmToQMap(_axisInA); + arguments["otherEntityID"] = _otherID; + arguments["otherPivot"] = glmToQMap(_pivotInB); + arguments["otherAxis"] = glmToQMap(_axisInB); + arguments["swingSpan1"] = _swingSpan1; + arguments["swingSpan2"] = _swingSpan2; + arguments["twistSpan"] = _twistSpan; }); return arguments; } @@ -312,15 +290,12 @@ QByteArray ObjectConstraintConeTwist::serialize() const { dataStream << _pivotInA; dataStream << _axisInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pivotInB; dataStream << _axisInB; dataStream << _swingSpan1; dataStream << _swingSpan2; dataStream << _twistSpan; - dataStream << _softness; - dataStream << _biasFactor; - dataStream << _relaxationFactor; }); return serializedConstraintArguments; @@ -339,7 +314,7 @@ void ObjectConstraintConeTwist::deserialize(QByteArray serializedArguments) { uint16_t serializationVersion; dataStream >> serializationVersion; - if (serializationVersion != ObjectConstraintConeTwist::constraintVersion) { + if (serializationVersion > ObjectConstraintConeTwist::constraintVersion) { assert(false); return; } @@ -352,15 +327,18 @@ void ObjectConstraintConeTwist::deserialize(QByteArray serializedArguments) { dataStream >> _pivotInA; dataStream >> _axisInA; - dataStream >> _otherEntityID; + dataStream >> _otherID; dataStream >> _pivotInB; dataStream >> _axisInB; dataStream >> _swingSpan1; dataStream >> _swingSpan2; dataStream >> _twistSpan; - dataStream >> _softness; - dataStream >> _biasFactor; - dataStream >> _relaxationFactor; + if (serializationVersion == CONE_TWIST_VERSION_WITH_UNUSED_PAREMETERS) { + float softness, biasFactor, relaxationFactor; + dataStream >> softness; + dataStream >> biasFactor; + dataStream >> relaxationFactor; + } _active = true; }); diff --git a/libraries/physics/src/ObjectConstraintConeTwist.h b/libraries/physics/src/ObjectConstraintConeTwist.h index 02297e2b91..ea8b2aadb6 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.h +++ b/libraries/physics/src/ObjectConstraintConeTwist.h @@ -40,16 +40,12 @@ protected: glm::vec3 _pivotInA; glm::vec3 _axisInA; - EntityItemID _otherEntityID; glm::vec3 _pivotInB; glm::vec3 _axisInB; float _swingSpan1 { TWO_PI }; float _swingSpan2 { TWO_PI };; float _twistSpan { TWO_PI };; - float _softness { 1.0f }; - float _biasFactor {0.3f }; - float _relaxationFactor { 1.0f }; }; #endif // hifi_ObjectConstraintConeTwist_h diff --git a/libraries/physics/src/ObjectConstraintHinge.cpp b/libraries/physics/src/ObjectConstraintHinge.cpp index cf91ca904b..52be64796a 100644 --- a/libraries/physics/src/ObjectConstraintHinge.cpp +++ b/libraries/physics/src/ObjectConstraintHinge.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "QVariantGLM.h" #include "EntityTree.h" @@ -16,7 +18,8 @@ #include "PhysicsLogging.h" -const uint16_t ObjectConstraintHinge::constraintVersion = 1; +const uint16_t HINGE_VERSION_WITH_UNUSED_PAREMETERS = 1; +const uint16_t ObjectConstraintHinge::constraintVersion = 2; const glm::vec3 DEFAULT_HINGE_AXIS(1.0f, 0.0f, 0.0f); ObjectConstraintHinge::ObjectConstraintHinge(const QUuid& id, EntityItemPointer ownerEntity) : @@ -40,7 +43,7 @@ QList ObjectConstraintHinge::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -56,25 +59,19 @@ void ObjectConstraintHinge::updateHinge() { glm::vec3 axisInA; float low; float high; - float softness; - float biasFactor; - float relaxationFactor; withReadLock([&]{ axisInA = _axisInA; constraint = static_cast(_constraint); low = _low; high = _high; - biasFactor = _biasFactor; - relaxationFactor = _relaxationFactor; - softness = _softness; }); if (!constraint) { return; } - constraint->setLimit(low, high, softness, biasFactor, relaxationFactor); + constraint->setLimit(low, high); } @@ -90,7 +87,7 @@ btTypedConstraint* ObjectConstraintHinge::getConstraint() { constraint = static_cast(_constraint); pivotInA = _pivotInA; axisInA = _axisInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pivotInB = _pivotInB; axisInB = _axisInB; }); @@ -98,6 +95,9 @@ btTypedConstraint* ObjectConstraintHinge::getConstraint() { return constraint; } + static QString repeatedHingeNoRigidBody = LogHandler::getInstance().addRepeatedMessageRegex( + "ObjectConstraintHinge::getConstraint -- no rigidBody.*"); + btRigidBody* rigidBodyA = getRigidBody(); if (!rigidBodyA) { qCDebug(physics) << "ObjectConstraintHinge::getConstraint -- no rigidBodyA"; @@ -115,6 +115,7 @@ btTypedConstraint* ObjectConstraintHinge::getConstraint() { // This hinge is between two entities... find the other rigid body. btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); if (!rigidBodyB) { + qCDebug(physics) << "ObjectConstraintHinge::getConstraint -- no rigidBodyB"; return nullptr; } @@ -159,9 +160,6 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { glm::vec3 axisInB; float low; float high; - float softness; - float biasFactor; - float relaxationFactor; bool needUpdate = false; bool somethingChanged = ObjectDynamic::updateArguments(arguments); @@ -182,7 +180,7 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("hinge constraint", arguments, "otherEntityID", ok, false)); if (!ok) { - otherEntityID = _otherEntityID; + otherEntityID = _otherID; } ok = true; @@ -209,36 +207,14 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { high = _high; } - ok = true; - softness = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, "softness", ok, false); - if (!ok) { - softness = _softness; - } - - ok = true; - biasFactor = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, "biasFactor", ok, false); - if (!ok) { - biasFactor = _biasFactor; - } - - ok = true; - relaxationFactor = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, - "relaxationFactor", ok, false); - if (!ok) { - relaxationFactor = _relaxationFactor; - } - if (somethingChanged || pivotInA != _pivotInA || axisInA != _axisInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pivotInB != _pivotInB || axisInB != _axisInB || low != _low || - high != _high || - softness != _softness || - biasFactor != _biasFactor || - relaxationFactor != _relaxationFactor) { + high != _high) { // something changed needUpdate = true; } @@ -248,14 +224,11 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { withWriteLock([&] { _pivotInA = pivotInA; _axisInA = axisInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pivotInB = pivotInB; _axisInB = axisInB; _low = low; _high = high; - _softness = softness; - _biasFactor = biasFactor; - _relaxationFactor = relaxationFactor; _active = true; @@ -275,18 +248,17 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { QVariantMap ObjectConstraintHinge::getArguments() { QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { + arguments["pivot"] = glmToQMap(_pivotInA); + arguments["axis"] = glmToQMap(_axisInA); + arguments["otherEntityID"] = _otherID; + arguments["otherPivot"] = glmToQMap(_pivotInB); + arguments["otherAxis"] = glmToQMap(_axisInB); + arguments["low"] = _low; + arguments["high"] = _high; if (_constraint) { - arguments["pivot"] = glmToQMap(_pivotInA); - arguments["axis"] = glmToQMap(_axisInA); - arguments["otherEntityID"] = _otherEntityID; - arguments["otherPivot"] = glmToQMap(_pivotInB); - arguments["otherAxis"] = glmToQMap(_axisInB); - arguments["low"] = _low; - arguments["high"] = _high; - arguments["softness"] = _softness; - arguments["biasFactor"] = _biasFactor; - arguments["relaxationFactor"] = _relaxationFactor; arguments["angle"] = static_cast(_constraint)->getHingeAngle(); // [-PI,PI] + } else { + arguments["angle"] = 0.0f; } }); return arguments; @@ -303,14 +275,11 @@ QByteArray ObjectConstraintHinge::serialize() const { withReadLock([&] { dataStream << _pivotInA; dataStream << _axisInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pivotInB; dataStream << _axisInB; dataStream << _low; dataStream << _high; - dataStream << _softness; - dataStream << _biasFactor; - dataStream << _relaxationFactor; dataStream << localTimeToServerTime(_expires); dataStream << _tag; @@ -332,7 +301,7 @@ void ObjectConstraintHinge::deserialize(QByteArray serializedArguments) { uint16_t serializationVersion; dataStream >> serializationVersion; - if (serializationVersion != ObjectConstraintHinge::constraintVersion) { + if (serializationVersion > ObjectConstraintHinge::constraintVersion) { assert(false); return; } @@ -340,14 +309,17 @@ void ObjectConstraintHinge::deserialize(QByteArray serializedArguments) { withWriteLock([&] { dataStream >> _pivotInA; dataStream >> _axisInA; - dataStream >> _otherEntityID; + dataStream >> _otherID; dataStream >> _pivotInB; dataStream >> _axisInB; dataStream >> _low; dataStream >> _high; - dataStream >> _softness; - dataStream >> _biasFactor; - dataStream >> _relaxationFactor; + if (serializationVersion == HINGE_VERSION_WITH_UNUSED_PAREMETERS) { + float softness, biasFactor, relaxationFactor; + dataStream >> softness; + dataStream >> biasFactor; + dataStream >> relaxationFactor; + } quint64 serverExpires; dataStream >> serverExpires; diff --git a/libraries/physics/src/ObjectConstraintHinge.h b/libraries/physics/src/ObjectConstraintHinge.h index 07ce8eb8a3..bb9505fbae 100644 --- a/libraries/physics/src/ObjectConstraintHinge.h +++ b/libraries/physics/src/ObjectConstraintHinge.h @@ -40,7 +40,6 @@ protected: glm::vec3 _pivotInA; glm::vec3 _axisInA; - EntityItemID _otherEntityID; glm::vec3 _pivotInB; glm::vec3 _axisInB; @@ -49,27 +48,9 @@ protected: // https://gamedev.stackexchange.com/questions/71436/what-are-the-parameters-for-bthingeconstraintsetlimit // - // softness: a negative measure of the friction that determines how much the hinge rotates for a given force. A high - // softness would make the hinge rotate easily like it's oiled then. - // biasFactor: an offset for the relaxed rotation of the hinge. It won't be right in the middle of the low and high angles - // anymore. 1.0f is the neural value. - // relaxationFactor: a measure of how much force is applied internally to bring the hinge in its central rotation. - // This is right in the middle of the low and high angles. For example, consider a western swing door. After - // walking through it will swing in both directions but at the end it stays right in the middle. - - // http://javadoc.jmonkeyengine.org/com/jme3/bullet/joints/HingeJoint.html - // - // _softness - the factor at which the velocity error correction starts operating, i.e. a softness of 0.9 means that - // the vel. corr starts at 90% of the limit range. - // _biasFactor - the magnitude of the position correction. It tells you how strictly the position error (drift) is - // corrected. - // _relaxationFactor - the rate at which velocity errors are corrected. This can be seen as the strength of the - // limits. A low value will make the the limits more spongy. - - - float _softness { 0.9f }; - float _biasFactor { 0.3f }; - float _relaxationFactor { 1.0f }; + // softness: unused + // biasFactor: unused + // relaxationFactor: unused }; #endif // hifi_ObjectConstraintHinge_h diff --git a/libraries/physics/src/ObjectConstraintSlider.cpp b/libraries/physics/src/ObjectConstraintSlider.cpp index d7d4df78af..ded9ad47e6 100644 --- a/libraries/physics/src/ObjectConstraintSlider.cpp +++ b/libraries/physics/src/ObjectConstraintSlider.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "QVariantGLM.h" #include "EntityTree.h" @@ -17,12 +19,12 @@ const uint16_t ObjectConstraintSlider::constraintVersion = 1; - +const glm::vec3 DEFAULT_SLIDER_AXIS(1.0f, 0.0f, 0.0f); ObjectConstraintSlider::ObjectConstraintSlider(const QUuid& id, EntityItemPointer ownerEntity) : ObjectConstraint(DYNAMIC_TYPE_SLIDER, id, ownerEntity), - _pointInA(glm::vec3(0.0f)), - _axisInA(glm::vec3(0.0f)) + _axisInA(DEFAULT_SLIDER_AXIS), + _axisInB(DEFAULT_SLIDER_AXIS) { } @@ -34,7 +36,7 @@ QList ObjectConstraintSlider::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -77,7 +79,7 @@ btTypedConstraint* ObjectConstraintSlider::getConstraint() { constraint = static_cast(_constraint); pointInA = _pointInA; axisInA = _axisInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pointInB = _pointInB; axisInB = _axisInB; }); @@ -85,23 +87,41 @@ btTypedConstraint* ObjectConstraintSlider::getConstraint() { return constraint; } + static QString repeatedSliderNoRigidBody = LogHandler::getInstance().addRepeatedMessageRegex( + "ObjectConstraintSlider::getConstraint -- no rigidBody.*"); + btRigidBody* rigidBodyA = getRigidBody(); if (!rigidBodyA) { qCDebug(physics) << "ObjectConstraintSlider::getConstraint -- no rigidBodyA"; return nullptr; } + if (glm::length(axisInA) < FLT_EPSILON) { + qCWarning(physics) << "slider axis cannot be a zero vector"; + axisInA = DEFAULT_SLIDER_AXIS; + } else { + axisInA = glm::normalize(axisInA); + } + if (!otherEntityID.isNull()) { // This slider is between two entities... find the other rigid body. - glm::quat rotA = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); - glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInB)); + if (glm::length(axisInB) < FLT_EPSILON) { + qCWarning(physics) << "slider axis cannot be a zero vector"; + axisInB = DEFAULT_SLIDER_AXIS; + } else { + axisInB = glm::normalize(axisInB); + } + + glm::quat rotA = glm::rotation(DEFAULT_SLIDER_AXIS, axisInA); + glm::quat rotB = glm::rotation(DEFAULT_SLIDER_AXIS, axisInB); btTransform frameInA(glmToBullet(rotA), glmToBullet(pointInA)); btTransform frameInB(glmToBullet(rotB), glmToBullet(pointInB)); btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); if (!rigidBodyB) { + qCDebug(physics) << "ObjectConstraintSlider::getConstraint -- no rigidBodyB"; return nullptr; } @@ -109,7 +129,7 @@ btTypedConstraint* ObjectConstraintSlider::getConstraint() { } else { // This slider is between an entity and the world-frame. - glm::quat rot = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); + glm::quat rot = glm::rotation(DEFAULT_SLIDER_AXIS, axisInA); btTransform frameInA(glmToBullet(rot), glmToBullet(pointInA)); @@ -160,7 +180,7 @@ bool ObjectConstraintSlider::updateArguments(QVariantMap arguments) { otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("slider constraint", arguments, "otherEntityID", ok, false)); if (!ok) { - otherEntityID = _otherEntityID; + otherEntityID = _otherID; } ok = true; @@ -202,7 +222,7 @@ bool ObjectConstraintSlider::updateArguments(QVariantMap arguments) { if (somethingChanged || pointInA != _pointInA || axisInA != _axisInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pointInB != _pointInB || axisInB != _axisInB || linearLow != _linearLow || @@ -218,7 +238,7 @@ bool ObjectConstraintSlider::updateArguments(QVariantMap arguments) { withWriteLock([&] { _pointInA = pointInA; _axisInA = axisInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pointInB = pointInB; _axisInB = axisInB; _linearLow = linearLow; @@ -244,18 +264,21 @@ bool ObjectConstraintSlider::updateArguments(QVariantMap arguments) { QVariantMap ObjectConstraintSlider::getArguments() { QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { + arguments["point"] = glmToQMap(_pointInA); + arguments["axis"] = glmToQMap(_axisInA); + arguments["otherEntityID"] = _otherID; + arguments["otherPoint"] = glmToQMap(_pointInB); + arguments["otherAxis"] = glmToQMap(_axisInB); + arguments["linearLow"] = _linearLow; + arguments["linearHigh"] = _linearHigh; + arguments["angularLow"] = _angularLow; + arguments["angularHigh"] = _angularHigh; if (_constraint) { - arguments["point"] = glmToQMap(_pointInA); - arguments["axis"] = glmToQMap(_axisInA); - arguments["otherEntityID"] = _otherEntityID; - arguments["otherPoint"] = glmToQMap(_pointInB); - arguments["otherAxis"] = glmToQMap(_axisInB); - arguments["linearLow"] = _linearLow; - arguments["linearHigh"] = _linearHigh; - arguments["angularLow"] = _angularLow; - arguments["angularHigh"] = _angularHigh; arguments["linearPosition"] = static_cast(_constraint)->getLinearPos(); arguments["angularPosition"] = static_cast(_constraint)->getAngularPos(); + } else { + arguments["linearPosition"] = 0.0f; + arguments["angularPosition"] = 0.0f; } }); return arguments; @@ -275,7 +298,7 @@ QByteArray ObjectConstraintSlider::serialize() const { dataStream << _pointInA; dataStream << _axisInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pointInB; dataStream << _axisInB; dataStream << _linearLow; @@ -313,7 +336,7 @@ void ObjectConstraintSlider::deserialize(QByteArray serializedArguments) { dataStream >> _pointInA; dataStream >> _axisInA; - dataStream >> _otherEntityID; + dataStream >> _otherID; dataStream >> _pointInB; dataStream >> _axisInB; dataStream >> _linearLow; diff --git a/libraries/physics/src/ObjectConstraintSlider.h b/libraries/physics/src/ObjectConstraintSlider.h index d616b9954c..36ecca0a2c 100644 --- a/libraries/physics/src/ObjectConstraintSlider.h +++ b/libraries/physics/src/ObjectConstraintSlider.h @@ -40,7 +40,6 @@ protected: glm::vec3 _pointInA; glm::vec3 _axisInA; - EntityItemID _otherEntityID; glm::vec3 _pointInB; glm::vec3 _axisInB; diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp index 3cb9f5b405..3deadd6468 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -24,6 +24,27 @@ ObjectDynamic::ObjectDynamic(EntityDynamicType type, const QUuid& id, EntityItem ObjectDynamic::~ObjectDynamic() { } +void ObjectDynamic::remapIDs(QHash& map) { + withWriteLock([&]{ + if (!_id.isNull()) { + // just force our ID to something new -- action IDs don't go into the map + _id = QUuid::createUuid(); + } + + if (!_otherID.isNull()) { + QHash::iterator iter = map.find(_otherID); + if (iter == map.end()) { + // not found, add it + QUuid oldOtherID = _otherID; + _otherID = QUuid::createUuid(); + map.insert(oldOtherID, _otherID); + } else { + _otherID = iter.value(); + } + } + }); +} + qint64 ObjectDynamic::getEntityServerClockSkew() const { auto nodeList = DependencyManager::get(); @@ -139,56 +160,6 @@ btRigidBody* ObjectDynamic::getRigidBody() { return nullptr; } -glm::vec3 ObjectDynamic::getPosition() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::vec3(0.0f); - } - return bulletToGLM(rigidBody->getCenterOfMassPosition()); -} - -glm::quat ObjectDynamic::getRotation() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::quat(0.0f, 0.0f, 0.0f, 1.0f); - } - return bulletToGLM(rigidBody->getOrientation()); -} - -glm::vec3 ObjectDynamic::getLinearVelocity() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::vec3(0.0f); - } - return bulletToGLM(rigidBody->getLinearVelocity()); -} - -void ObjectDynamic::setLinearVelocity(glm::vec3 linearVelocity) { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return; - } - rigidBody->setLinearVelocity(glmToBullet(glm::vec3(0.0f))); - rigidBody->activate(); -} - -glm::vec3 ObjectDynamic::getAngularVelocity() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::vec3(0.0f); - } - return bulletToGLM(rigidBody->getAngularVelocity()); -} - -void ObjectDynamic::setAngularVelocity(glm::vec3 angularVelocity) { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return; - } - rigidBody->setAngularVelocity(glmToBullet(angularVelocity)); - rigidBody->activate(); -} - void ObjectDynamic::activateBody(bool forceActivation) { auto rigidBody = getRigidBody(); if (rigidBody) { @@ -274,3 +245,38 @@ QList ObjectDynamic::getRigidBodies() { result += getRigidBody(); return result; } + +SpatiallyNestablePointer ObjectDynamic::getOther() { + SpatiallyNestablePointer other; + withWriteLock([&]{ + if (_otherID == QUuid()) { + // no other + return; + } + other = _other.lock(); + if (other && other->getID() == _otherID) { + // other is already up-to-date + return; + } + if (other) { + // we have a pointer to other, but it's wrong + other.reset(); + _other.reset(); + } + // we have an other-id but no pointer to other cached + QSharedPointer parentFinder = DependencyManager::get(); + if (!parentFinder) { + return; + } + EntityItemPointer ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + bool success; + _other = parentFinder->find(_otherID, success, ownerEntity->getParentTree()); + if (success) { + other = _other.lock(); + } + }); + return other; +} diff --git a/libraries/physics/src/ObjectDynamic.h b/libraries/physics/src/ObjectDynamic.h index dcd0103a55..7fdf2e323a 100644 --- a/libraries/physics/src/ObjectDynamic.h +++ b/libraries/physics/src/ObjectDynamic.h @@ -29,6 +29,8 @@ public: ObjectDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); virtual ~ObjectDynamic(); + virtual void remapIDs(QHash& map) override; + virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } virtual void setOwnerEntity(const EntityItemPointer ownerEntity) override { _ownerEntity = ownerEntity; } @@ -54,12 +56,6 @@ protected: btRigidBody* getOtherRigidBody(EntityItemID otherEntityID); EntityItemPointer getEntityByID(EntityItemID entityID) const; virtual btRigidBody* getRigidBody(); - virtual glm::vec3 getPosition() override; - virtual glm::quat getRotation() override; - virtual glm::vec3 getLinearVelocity() override; - virtual void setLinearVelocity(glm::vec3 linearVelocity) override; - virtual glm::vec3 getAngularVelocity() override; - virtual void setAngularVelocity(glm::vec3 angularVelocity) override; virtual void activateBody(bool forceActivation = false); virtual void forceBodyNonStatic(); @@ -67,6 +63,10 @@ protected: QString _tag; quint64 _expires { 0 }; // in seconds since epoch + EntityItemID _otherID; + SpatiallyNestableWeakPointer _other; + SpatiallyNestablePointer getOther(); + private: qint64 getEntityServerClockSkew() const; }; diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index 38f079c1d4..5a36d69035 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -1,6 +1,6 @@ // // ObjectMotionState.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.11.05 // Copyright 2014 High Fidelity, Inc. @@ -63,10 +63,7 @@ ShapeManager* ObjectMotionState::getShapeManager() { } ObjectMotionState::ObjectMotionState(const btCollisionShape* shape) : - _motionType(MOTION_TYPE_STATIC), _shape(shape), - _body(nullptr), - _mass(0.0f), _lastKinematicStep(worldSimulationStep) { } @@ -74,7 +71,43 @@ ObjectMotionState::ObjectMotionState(const btCollisionShape* shape) : ObjectMotionState::~ObjectMotionState() { assert(!_body); setShape(nullptr); - _type = MOTIONSTATE_TYPE_INVALID; +} + +void ObjectMotionState::setMass(float mass) { + _density = 1.0f; + if (_shape) { + // we compute the density for the current shape's Aabb volume + // and save that instead of the total mass + btTransform transform; + transform.setIdentity(); + btVector3 minCorner, maxCorner; + _shape->getAabb(transform, minCorner, maxCorner); + btVector3 diagonal = maxCorner - minCorner; + float volume = diagonal.getX() * diagonal.getY() * diagonal.getZ(); + if (volume > EPSILON) { + _density = fabsf(mass) / volume; + } + } +} + +float ObjectMotionState::getMass() const { + if (_shape) { + // scale the density by the current Aabb volume to get mass + btTransform transform; + transform.setIdentity(); + btVector3 minCorner, maxCorner; + _shape->getAabb(transform, minCorner, maxCorner); + btVector3 diagonal = maxCorner - minCorner; + float volume = diagonal.getX() * diagonal.getY() * diagonal.getZ(); + + // cap the max mass for numerical stability + const float MIN_OBJECT_MASS = 0.0f; + const float MAX_OBJECT_DENSITY = 20000.0f; // kg/m^3 density of Tungsten + const float MAX_OBJECT_VOLUME = 1.0e6f; + const float MAX_OBJECT_MASS = MAX_OBJECT_DENSITY * MAX_OBJECT_VOLUME; + return glm::clamp(_density * volume, MIN_OBJECT_MASS, MAX_OBJECT_MASS); + } + return 0.0f; } void ObjectMotionState::setBodyLinearVelocity(const glm::vec3& velocity) const { diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index 4230f636b3..1e582ea854 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -1,6 +1,6 @@ // // ObjectMotionState.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.11.05 // Copyright 2014 High Fidelity, Inc. @@ -93,8 +93,8 @@ public: MotionStateType getType() const { return _type; } virtual PhysicsMotionType getMotionType() const { return _motionType; } - void setMass(float mass) { _mass = fabsf(mass); } - float getMass() { return _mass; } + void setMass(float mass); + float getMass() const; void setBodyLinearVelocity(const glm::vec3& velocity) const; void setBodyAngularVelocity(const glm::vec3& velocity) const; @@ -159,12 +159,12 @@ protected: void setRigidBody(btRigidBody* body); virtual void setShape(const btCollisionShape* shape); - MotionStateType _type = MOTIONSTATE_TYPE_INVALID; // type of MotionState - PhysicsMotionType _motionType; // type of motion: KINEMATIC, DYNAMIC, or STATIC + MotionStateType _type { MOTIONSTATE_TYPE_INVALID }; // type of MotionState + PhysicsMotionType _motionType { MOTION_TYPE_STATIC }; // type of motion: KINEMATIC, DYNAMIC, or STATIC const btCollisionShape* _shape; - btRigidBody* _body; - float _mass; + btRigidBody* _body { nullptr }; + float _density { 1.0f }; uint32_t _lastKinematicStep; bool _hasInternalKinematicChanges { false }; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 5081f981d4..2e69ff987c 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -1,6 +1,6 @@ // // PhysicalEntitySimulation.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2015.04.27 // Copyright 2015 High Fidelity, Inc. diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index e0b15440bb..b9acf4cace 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -1,6 +1,6 @@ // // PhysicalEntitySimulation.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2015.04.27 // Copyright 2015 High Fidelity, Inc. diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 87a15eb264..3a02e95e7c 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -1,6 +1,6 @@ // // PhysicsEngine.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.10.29 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index 07de0e7b5c..e9b29a43a4 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -1,6 +1,6 @@ // // PhysicsEngine.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.10.29 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 35e050024a..d209667966 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -1,6 +1,6 @@ // // ShapeFactory.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.12.01 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/physics/src/ShapeFactory.h b/libraries/physics/src/ShapeFactory.h index a1022104dd..2bf79f390c 100644 --- a/libraries/physics/src/ShapeFactory.h +++ b/libraries/physics/src/ShapeFactory.h @@ -1,6 +1,6 @@ // // ShapeFactory.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.12.01 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/physics/src/ShapeManager.cpp b/libraries/physics/src/ShapeManager.cpp index b61fb0037b..fd3e35d28a 100644 --- a/libraries/physics/src/ShapeManager.cpp +++ b/libraries/physics/src/ShapeManager.cpp @@ -1,6 +1,6 @@ // // ShapeManager.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.10.29 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/physics/src/ShapeManager.h b/libraries/physics/src/ShapeManager.h index 261c06ddb9..ed81b5e8f8 100644 --- a/libraries/physics/src/ShapeManager.h +++ b/libraries/physics/src/ShapeManager.h @@ -1,6 +1,6 @@ // // ShapeManager.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.10.29 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index eddda41d5e..e09afa3f31 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -376,7 +376,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f)); glm::vec3 meshFrameDirection = glm::vec3(worldToMeshMatrix * glm::vec4(direction, 0.0f)); - for (const auto& triangleSet : _modelSpaceMeshTriangleSets) { + for (auto& triangleSet : _modelSpaceMeshTriangleSets) { float triangleSetDistance = 0.0f; BoxFace triangleSetFace; glm::vec3 triangleSetNormal; @@ -573,7 +573,7 @@ bool Model::addToScene(const render::ScenePointer& scene, bool somethingAdded = false; if (_collisionGeometry) { - if (_collisionRenderItems.empty()) { + if (_collisionRenderItemsMap.empty()) { foreach (auto renderItem, _collisionRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); @@ -583,7 +583,7 @@ bool Model::addToScene(const render::ScenePointer& scene, transaction.resetItem(item, renderPayload); _collisionRenderItemsMap.insert(item, renderPayload); } - somethingAdded = !_collisionRenderItems.empty(); + somethingAdded = !_collisionRenderItemsMap.empty(); } } else { if (_modelMeshRenderItemsMap.empty()) { @@ -632,7 +632,7 @@ void Model::removeFromScene(const render::ScenePointer& scene, render::Transacti transaction.removeItem(item); } _collisionRenderItems.clear(); - _collisionRenderItems.clear(); + _collisionRenderItemsMap.clear(); _addedToScene = false; _renderInfoVertexCount = 0; @@ -1046,12 +1046,12 @@ void Model::simulate(float deltaTime, bool fullUpdate) { //virtual void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { _needsUpdateClusterMatrices = true; - _rig->updateAnimations(deltaTime, parentTransform); + glm::mat4 rigToWorldTransform = createMatFromQuatAndPos(getRotation(), getTranslation()); + _rig->updateAnimations(deltaTime, parentTransform, rigToWorldTransform); } void Model::computeMeshPartLocalBounds() { for (auto& part : _modelMeshRenderItems) { - assert(part->_meshIndex < _modelMeshRenderItems.size()); const Model::MeshState& state = _meshStates.at(part->_meshIndex); part->computeAdjustedLocalBound(state.clusterMatrices); } diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index e97bc329c6..5ec8ce4b12 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -24,6 +24,7 @@ class AudioScriptingInterface : public QObject, public Dependency { SINGLETON_DEPENDENCY public: + virtual ~AudioScriptingInterface() {} void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } protected: diff --git a/libraries/script-engine/src/Mat4.cpp b/libraries/script-engine/src/Mat4.cpp index 6676d0cde1..6965f43b32 100644 --- a/libraries/script-engine/src/Mat4.cpp +++ b/libraries/script-engine/src/Mat4.cpp @@ -11,7 +11,9 @@ #include #include +#include #include "ScriptEngineLogging.h" +#include "ScriptEngine.h" #include "Mat4.h" glm::mat4 Mat4::multiply(const glm::mat4& m1, const glm::mat4& m2) const { @@ -66,10 +68,12 @@ glm::vec3 Mat4::getUp(const glm::mat4& m) const { return glm::vec3(m[0][1], m[1][1], m[2][1]); } -void Mat4::print(const QString& label, const glm::mat4& m) const { - qCDebug(scriptengine) << qPrintable(label) << - "row0 =" << m[0][0] << "," << m[1][0] << "," << m[2][0] << "," << m[3][0] << - "row1 =" << m[0][1] << "," << m[1][1] << "," << m[2][1] << "," << m[3][1] << - "row2 =" << m[0][2] << "," << m[1][2] << "," << m[2][2] << "," << m[3][2] << - "row3 =" << m[0][3] << "," << m[1][3] << "," << m[2][3] << "," << m[3][3]; +void Mat4::print(const QString& label, const glm::mat4& m, bool transpose) const { + glm::dmat4 out = transpose ? glm::transpose(m) : m; + QString message = QString("%1 %2").arg(qPrintable(label)); + message = message.arg(glm::to_string(out).c_str()); + qCDebug(scriptengine) << message; + if (ScriptEngine* scriptEngine = qobject_cast(engine())) { + scriptEngine->print(message); + } } diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 19bbbe178a..8b942874ee 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -16,9 +16,10 @@ #include #include +#include /// Scriptable Mat4 object. Used exclusively in the JavaScript API -class Mat4 : public QObject { +class Mat4 : public QObject, protected QScriptable { Q_OBJECT public slots: @@ -43,7 +44,7 @@ public slots: glm::vec3 getRight(const glm::mat4& m) const; glm::vec3 getUp(const glm::mat4& m) const; - void print(const QString& label, const glm::mat4& m) const; + void print(const QString& label, const glm::mat4& m, bool transpose = false) const; }; #endif // hifi_Mat4_h diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 05002dcf5d..a6f7acffc8 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -15,7 +15,9 @@ #include #include +#include #include "ScriptEngineLogging.h" +#include "ScriptEngine.h" #include "Quat.h" quat Quat::normalize(const glm::quat& q) { @@ -114,8 +116,17 @@ float Quat::dot(const glm::quat& q1, const glm::quat& q2) { return glm::dot(q1, q2); } -void Quat::print(const QString& label, const glm::quat& q) { - qCDebug(scriptengine) << qPrintable(label) << q.x << "," << q.y << "," << q.z << "," << q.w; +void Quat::print(const QString& label, const glm::quat& q, bool asDegrees) { + QString message = QString("%1 %2").arg(qPrintable(label)); + if (asDegrees) { + message = message.arg(glm::to_string(glm::dvec3(safeEulerAngles(q))).c_str()); + } else { + message = message.arg(glm::to_string(glm::dquat(q)).c_str()); + } + qCDebug(scriptengine) << message; + if (ScriptEngine* scriptEngine = qobject_cast(engine())) { + scriptEngine->print(message); + } } bool Quat::equal(const glm::quat& q1, const glm::quat& q2) { diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index ee3ab9aa7c..3b3a6fde7c 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -18,6 +18,7 @@ #include #include +#include /**jsdoc * A Quaternion @@ -30,7 +31,7 @@ */ /// Scriptable interface a Quaternion helper class object. Used exclusively in the JavaScript API -class Quat : public QObject { +class Quat : public QObject, protected QScriptable { Q_OBJECT public slots: @@ -58,7 +59,7 @@ public slots: glm::quat slerp(const glm::quat& q1, const glm::quat& q2, float alpha); glm::quat squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h); float dot(const glm::quat& q1, const glm::quat& q2); - void print(const QString& label, const glm::quat& q); + void print(const QString& label, const glm::quat& q, bool asDegrees = false); bool equal(const glm::quat& q1, const glm::quat& q2); glm::quat cancelOutRollAndPitch(const glm::quat& q); glm::quat cancelOutRoll(const glm::quat& q); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index c904062507..8bbb9a3e2c 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -105,11 +105,11 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { } message += context->argument(i).toString(); } - qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline + qCDebug(scriptengineScript).noquote() << message; // noquote() so that \n is treated as newline - // FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print? - engine->globalObject().property("Script").property("print") - .call(engine->nullValue(), QScriptValueList({ message })); + if (ScriptEngine *scriptEngine = qobject_cast(engine)) { + scriptEngine->print(message); + } return QScriptValue(); } @@ -472,6 +472,11 @@ void ScriptEngine::scriptInfoMessage(const QString& message) { emit infoMessage(message, getFilename()); } +void ScriptEngine::scriptPrintedMessage(const QString& message) { + qCDebug(scriptengine) << message; + emit printedMessage(message, getFilename()); +} + // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of // callAnimationStateHandler requires that the type be registered. // These two are meaningful, if we ever do want to use them... diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 5ea8d052e9..bdb14dfe8c 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -48,6 +48,8 @@ class QScriptEngineDebugger; static const QString NO_SCRIPT(""); static const int SCRIPT_FPS = 60; +static const int DEFAULT_MAX_ENTITY_PPS = 9000; +static const int DEFAULT_ENTITY_PPS_PER_SCRIPT = 900; class CallbackData { public: @@ -221,6 +223,7 @@ public: void scriptErrorMessage(const QString& message); void scriptWarningMessage(const QString& message); void scriptInfoMessage(const QString& message); + void scriptPrintedMessage(const QString& message); int getNumRunningEntityScripts() const; bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 88b0e0b7b5..2076657288 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -453,7 +453,8 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL (scriptFilename.scheme() != "http" && scriptFilename.scheme() != "https" && scriptFilename.scheme() != "atp" && - scriptFilename.scheme() != "file")) { + scriptFilename.scheme() != "file" && + scriptFilename.scheme() != "about")) { // deal with a "url" like c:/something scriptUrl = normalizeScriptURL(QUrl::fromLocalFile(scriptFilename.toString())); } else { @@ -472,7 +473,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL }, Qt::QueuedConnection); - if (scriptFilename.isEmpty()) { + if (scriptFilename.isEmpty() || !scriptUrl.isValid()) { launchScriptEngine(scriptEngine); } else { // connect to the appropriate signals of this script engine diff --git a/libraries/script-engine/src/ScriptUUID.cpp b/libraries/script-engine/src/ScriptUUID.cpp index 6a52f4f6ca..ee15f1a760 100644 --- a/libraries/script-engine/src/ScriptUUID.cpp +++ b/libraries/script-engine/src/ScriptUUID.cpp @@ -14,6 +14,7 @@ #include #include "ScriptEngineLogging.h" +#include "ScriptEngine.h" #include "ScriptUUID.h" QUuid ScriptUUID::fromString(const QString& s) { @@ -36,6 +37,11 @@ bool ScriptUUID::isNull(const QUuid& id) { return id.isNull(); } -void ScriptUUID::print(const QString& lable, const QUuid& id) { - qCDebug(scriptengine) << qPrintable(lable) << id.toString(); +void ScriptUUID::print(const QString& label, const QUuid& id) { + QString message = QString("%1 %2").arg(qPrintable(label)); + message = message.arg(id.toString()); + qCDebug(scriptengine) << message; + if (ScriptEngine* scriptEngine = qobject_cast(engine())) { + scriptEngine->print(message); + } } diff --git a/libraries/script-engine/src/ScriptUUID.h b/libraries/script-engine/src/ScriptUUID.h index db94b5082b..221f9c46f0 100644 --- a/libraries/script-engine/src/ScriptUUID.h +++ b/libraries/script-engine/src/ScriptUUID.h @@ -15,9 +15,10 @@ #define hifi_ScriptUUID_h #include +#include /// Scriptable interface for a UUID helper class object. Used exclusively in the JavaScript API -class ScriptUUID : public QObject { +class ScriptUUID : public QObject, protected QScriptable { Q_OBJECT public slots: @@ -26,7 +27,7 @@ public slots: QUuid generate(); bool isEqual(const QUuid& idA, const QUuid& idB); bool isNull(const QUuid& id); - void print(const QString& lable, const QUuid& id); + void print(const QString& label, const QUuid& id); }; #endif // hifi_ScriptUUID_h diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index 139fe0552d..644f1e6f0c 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -250,6 +250,10 @@ static void addButtonProxyToQmlTablet(QQuickItem* qmlTablet, TabletButtonProxy* if (QThread::currentThread() != qmlTablet->thread()) { connectionType = Qt::BlockingQueuedConnection; } + if (buttonProxy == NULL){ + qCCritical(scriptengine) << "TabletScriptingInterface addButtonProxyToQmlTablet buttonProxy is NULL"; + return; + } bool hasResult = QMetaObject::invokeMethod(qmlTablet, "addButtonProxy", connectionType, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, buttonProxy->getProperties())); if (!hasResult) { diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp index 6c8f618500..a156f56d96 100644 --- a/libraries/script-engine/src/Vec3.cpp +++ b/libraries/script-engine/src/Vec3.cpp @@ -14,20 +14,26 @@ #include #include +#include #include "ScriptEngineLogging.h" #include "NumericalConstants.h" #include "Vec3.h" +#include "ScriptEngine.h" float Vec3::orientedAngle(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3) { float radians = glm::orientedAngle(glm::normalize(v1), glm::normalize(v2), glm::normalize(v3)); return glm::degrees(radians); } - -void Vec3::print(const QString& lable, const glm::vec3& v) { - qCDebug(scriptengine) << qPrintable(lable) << v.x << "," << v.y << "," << v.z; +void Vec3::print(const QString& label, const glm::vec3& v) { + QString message = QString("%1 %2").arg(qPrintable(label)); + message = message.arg(glm::to_string(glm::dvec3(v)).c_str()); + qCDebug(scriptengine) << message; + if (ScriptEngine* scriptEngine = qobject_cast(engine())) { + scriptEngine->print(message); + } } bool Vec3::withinEpsilon(const glm::vec3& v1, const glm::vec3& v2, float epsilon) { diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index b3a3dc3035..c7179a80c0 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -17,6 +17,7 @@ #include #include +#include #include "GLMHelpers.h" @@ -48,7 +49,7 @@ */ /// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API -class Vec3 : public QObject { +class Vec3 : public QObject, protected QScriptable { Q_OBJECT Q_PROPERTY(glm::vec3 UNIT_X READ UNIT_X CONSTANT) Q_PROPERTY(glm::vec3 UNIT_Y READ UNIT_Y CONSTANT) diff --git a/libraries/shared/src/AABox.cpp b/libraries/shared/src/AABox.cpp index 3f3146cc04..cea0a83d52 100644 --- a/libraries/shared/src/AABox.cpp +++ b/libraries/shared/src/AABox.cpp @@ -114,6 +114,10 @@ static bool isWithin(float value, float corner, float size) { return value >= corner && value <= corner + size; } +bool AABox::contains(const Triangle& triangle) const { + return contains(triangle.v0) && contains(triangle.v1) && contains(triangle.v2); +} + bool AABox::contains(const glm::vec3& point) const { return isWithin(point.x, _corner.x, _scale.x) && isWithin(point.y, _corner.y, _scale.y) && @@ -622,3 +626,40 @@ void AABox::transform(const glm::mat4& matrix) { _corner = newCenter - newDir; _scale = newDir * 2.0f; } + +AABox AABox::getOctreeChild(OctreeChild child) const { + AABox result(*this); // self + switch (child) { + case topLeftNear: + result._corner.y += _scale.y / 2.0f; + break; + case topLeftFar: + result._corner.y += _scale.y / 2.0f; + result._corner.z += _scale.z / 2.0f; + break; + case topRightNear: + result._corner.y += _scale.y / 2.0f; + result._corner.x += _scale.x / 2.0f; + break; + case topRightFar: + result._corner.y += _scale.y / 2.0f; + result._corner.x += _scale.x / 2.0f; + result._corner.z += _scale.z / 2.0f; + break; + case bottomLeftNear: + // _corner = same as parent + break; + case bottomLeftFar: + result._corner.z += _scale.z / 2.0f; + break; + case bottomRightNear: + result._corner.x += _scale.x / 2.0f; + break; + case bottomRightFar: + result._corner.x += _scale.x / 2.0f; + result._corner.z += _scale.z / 2.0f; + break; + } + result._scale /= 2.0f; // everything is half the scale + return result; +} diff --git a/libraries/shared/src/AABox.h b/libraries/shared/src/AABox.h index a53cc26163..eef83974ea 100644 --- a/libraries/shared/src/AABox.h +++ b/libraries/shared/src/AABox.h @@ -20,6 +20,7 @@ #include #include "BoxBase.h" +#include "GeometryUtil.h" #include "StreamUtils.h" class AACube; @@ -58,6 +59,7 @@ public: const glm::vec3& getMinimumPoint() const { return _corner; } glm::vec3 getMaximumPoint() const { return calcTopFarLeft(); } + bool contains(const Triangle& triangle) const; bool contains(const glm::vec3& point) const; bool contains(const AABox& otherBox) const; bool touches(const AABox& otherBox) const; @@ -112,6 +114,19 @@ public: void clear() { _corner = INFINITY_VECTOR; _scale = glm::vec3(0.0f); } + typedef enum { + topLeftNear, + topLeftFar, + topRightNear, + topRightFar, + bottomLeftNear, + bottomLeftFar, + bottomRightNear, + bottomRightFar + } OctreeChild; + + AABox getOctreeChild(OctreeChild child) const; // returns the AABox of the would be octree child of this AABox + private: glm::vec3 getClosestPointOnFace(const glm::vec3& point, BoxFace face) const; glm::vec3 getClosestPointOnFace(const glm::vec4& origin, const glm::vec4& direction, BoxFace face) const; diff --git a/libraries/shared/src/BackgroundMode.h b/libraries/shared/src/BackgroundMode.h index e6e585d9d8..0e0d684e62 100644 --- a/libraries/shared/src/BackgroundMode.h +++ b/libraries/shared/src/BackgroundMode.h @@ -1,6 +1,6 @@ // // BackgroundMode.h -// libraries/physcis/src +// libraries/physics/src // // Copyright 2015 High Fidelity, Inc. // diff --git a/libraries/shared/src/Interpolate.cpp b/libraries/shared/src/Interpolate.cpp index ba93a21a8e..35c164f0f2 100644 --- a/libraries/shared/src/Interpolate.cpp +++ b/libraries/shared/src/Interpolate.cpp @@ -77,3 +77,13 @@ float Interpolate::calculateFadeRatio(quint64 start) { const float EASING_SCALE = 1.001f; return std::min(EASING_SCALE * fadeRatio, 1.0f); } + +float Interpolate::easeInOutQuad(float lerpValue) { + assert(!((lerpValue < 0.0f) || (lerpValue > 1.0f))); + + if (lerpValue < 0.5f) { + return (2.0f * lerpValue * lerpValue); + } + + return (lerpValue*(4.0f - 2.0f * lerpValue) - 1.0f); +} \ No newline at end of file diff --git a/libraries/shared/src/Interpolate.h b/libraries/shared/src/Interpolate.h index 79ebd2f7fc..fca6a40b61 100644 --- a/libraries/shared/src/Interpolate.h +++ b/libraries/shared/src/Interpolate.h @@ -30,6 +30,9 @@ public: static float simpleNonLinearBlend(float fraction); static float calculateFadeRatio(quint64 start); + + // Basic ease-in-ease-out function for smoothing values. + static float easeInOutQuad(float lerpValue); }; #endif // hifi_Interpolate_h diff --git a/libraries/shared/src/Profile.cpp b/libraries/shared/src/Profile.cpp index 7a8a8f0570..eb7440f4b3 100644 --- a/libraries/shared/src/Profile.cpp +++ b/libraries/shared/src/Profile.cpp @@ -34,7 +34,7 @@ Q_LOGGING_CATEGORY(trace_simulation_physics_detail, "trace.simulation.physics.de #endif static bool tracingEnabled() { - return DependencyManager::get()->isEnabled(); + return DependencyManager::isSet() && DependencyManager::get()->isEnabled(); } Duration::Duration(const QLoggingCategory& category, const QString& name, uint32_t argbColor, uint64_t payload, const QVariantMap& baseArgs) : _name(name), _category(category) { diff --git a/libraries/shared/src/RunningMarker.cpp b/libraries/shared/src/RunningMarker.cpp index 89fb3ada23..f8aaee42df 100644 --- a/libraries/shared/src/RunningMarker.cpp +++ b/libraries/shared/src/RunningMarker.cpp @@ -33,11 +33,11 @@ void RunningMarker::startRunningMarker() { _runningMarkerThread->setObjectName("Running Marker Thread"); _runningMarkerThread->start(); - writeRunningMarkerFiler(); // write the first file, even before timer + writeRunningMarkerFile(); // write the first file, even before timer _runningMarkerTimer = new QTimer(); QObject::connect(_runningMarkerTimer, &QTimer::timeout, [=](){ - writeRunningMarkerFiler(); + writeRunningMarkerFile(); }); _runningMarkerTimer->start(RUNNING_STATE_CHECK_IN_MSECS); @@ -53,7 +53,7 @@ RunningMarker::~RunningMarker() { _runningMarkerThread->deleteLater(); } -void RunningMarker::writeRunningMarkerFiler() { +void RunningMarker::writeRunningMarkerFile() { QFile runningMarkerFile(getFilePath()); // always write, even it it exists, so that it touches the files diff --git a/libraries/shared/src/RunningMarker.h b/libraries/shared/src/RunningMarker.h index 16763d27e6..1137dbf5fa 100644 --- a/libraries/shared/src/RunningMarker.h +++ b/libraries/shared/src/RunningMarker.h @@ -27,10 +27,11 @@ public: QString getFilePath(); static QString getMarkerFilePath(QString name); -protected: - void writeRunningMarkerFiler(); + + void writeRunningMarkerFile(); void deleteRunningMarkerFile(); +private: QObject* _parent { nullptr }; QString _name; QThread* _runningMarkerThread { nullptr }; diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index 54694dfd0a..258d1f8491 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -106,6 +106,10 @@ namespace Setting { return (_isSet) ? _value : other; } + bool isSet() const { + return _isSet; + } + const T& getDefault() const { return _defaultValue; } diff --git a/libraries/shared/src/SettingHelpers.cpp b/libraries/shared/src/SettingHelpers.cpp index 9e2d15fcd0..dd301aa5aa 100644 --- a/libraries/shared/src/SettingHelpers.cpp +++ b/libraries/shared/src/SettingHelpers.cpp @@ -126,10 +126,8 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { } switch (variantType) { - case QVariant::Map: - case QVariant::List: case QVariant::Hash: { - qCritical() << "Unsupported variant type" << variant.typeName(); + qCritical() << "Unsupported variant type" << variant.typeName() << ";" << key << variant; Q_ASSERT(false); break; } @@ -143,6 +141,8 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { case QVariant::UInt: case QVariant::Bool: case QVariant::Double: + case QVariant::Map: + case QVariant::List: object.insert(key, QJsonValue::fromVariant(variant)); break; diff --git a/libraries/shared/src/SettingInterface.h b/libraries/shared/src/SettingInterface.h index 082adf3e54..575641c0e7 100644 --- a/libraries/shared/src/SettingInterface.h +++ b/libraries/shared/src/SettingInterface.h @@ -21,7 +21,6 @@ namespace Setting { class Manager; void init(); - void cleanupSettings(); class Interface { public: diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index b8ea3a4272..583bceeaf2 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -1,6 +1,6 @@ // // ShapeInfo.cpp -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.10.29 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 98b397ee16..17e4703fc2 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -1,6 +1,6 @@ // // ShapeInfo.h -// libraries/physcis/src +// libraries/physics/src // // Created by Andrew Meadows 2014.10.29 // Copyright 2014 High Fidelity, Inc. diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 81a0eed9f3..a68d27e620 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -768,9 +768,10 @@ bool similarStrings(const QString& stringA, const QString& stringB) { } void disableQtBearerPoll() { - // to work around the Qt constant wireless scanning, set the env for polling interval very high - const QByteArray EXTREME_BEARER_POLL_TIMEOUT = QString::number(INT16_MAX).toLocal8Bit(); - qputenv("QT_BEARER_POLL_TIMEOUT", EXTREME_BEARER_POLL_TIMEOUT); + // to disable the Qt constant wireless scanning, set the env for polling interval + qDebug() << "Disabling Qt wireless polling by using a negative value for QTimer::setInterval"; + const QByteArray DISABLE_BEARER_POLL_TIMEOUT = QString::number(-1).toLocal8Bit(); + qputenv("QT_BEARER_POLL_TIMEOUT", DISABLE_BEARER_POLL_TIMEOUT); } void printSystemInformation() { diff --git a/libraries/shared/src/SimpleMovingAverage.h b/libraries/shared/src/SimpleMovingAverage.h index 0404ab9646..3855375f4c 100644 --- a/libraries/shared/src/SimpleMovingAverage.h +++ b/libraries/shared/src/SimpleMovingAverage.h @@ -71,7 +71,7 @@ public: void addSample(T sample) { if (numSamples > 0) { - average = (sample * WEIGHTING) + (average * ONE_MINUS_WEIGHTING); + average = (sample * (T)WEIGHTING) + (average * (T)ONE_MINUS_WEIGHTING); } else { average = sample; } diff --git a/libraries/shared/src/TriangleSet.cpp b/libraries/shared/src/TriangleSet.cpp index cdb3fd6b2c..aa21aa5cc0 100644 --- a/libraries/shared/src/TriangleSet.cpp +++ b/libraries/shared/src/TriangleSet.cpp @@ -12,9 +12,11 @@ #include "GLMHelpers.h" #include "TriangleSet.h" -void TriangleSet::insert(const Triangle& t) { - _triangles.push_back(t); +void TriangleSet::insert(const Triangle& t) { + _isBalanced = false; + + _triangles.push_back(t); _bounds += t.v0; _bounds += t.v1; _bounds += t.v2; @@ -23,39 +25,31 @@ void TriangleSet::insert(const Triangle& t) { void TriangleSet::clear() { _triangles.clear(); _bounds.clear(); + _isBalanced = false; + + _triangleOctree.clear(); } -// Determine of the given ray (origin/direction) in model space intersects with any triangles -// in the set. If an intersection occurs, the distance and surface normal will be provided. bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) { - bool intersectedSomething = false; - float boxDistance = std::numeric_limits::max(); - float bestDistance = std::numeric_limits::max(); + // reset our distance to be the max possible, lower level tests will store best distance here + distance = std::numeric_limits::max(); - if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) { - if (precision) { - for (const auto& triangle : _triangles) { - float thisTriangleDistance; - if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { - if (thisTriangleDistance < bestDistance) { - bestDistance = thisTriangleDistance; - intersectedSomething = true; - surfaceNormal = triangle.getNormal(); - distance = bestDistance; - } - } - } - } else { - intersectedSomething = true; - distance = boxDistance; - } + if (!_isBalanced) { + balanceOctree(); } - return intersectedSomething; -} + int trianglesTouched = 0; + auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched); + #if WANT_DEBUGGING + if (precision) { + qDebug() << "trianglesTouched :" << trianglesTouched << "out of:" << _triangleOctree._population << "_triangles.size:" << _triangles.size(); + } + #endif + return result; +} bool TriangleSet::convexHullContains(const glm::vec3& point) const { if (!_bounds.contains(point)) { @@ -74,3 +68,198 @@ bool TriangleSet::convexHullContains(const glm::vec3& point) const { return insideMesh; } +void TriangleSet::debugDump() { + qDebug() << __FUNCTION__; + qDebug() << "bounds:" << getBounds(); + qDebug() << "triangles:" << size() << "at top level...."; + qDebug() << "----- _triangleOctree -----"; + _triangleOctree.debugDump(); +} + +void TriangleSet::balanceOctree() { + _triangleOctree.reset(_bounds, 0); + + // insert all the triangles + for (size_t i = 0; i < _triangles.size(); i++) { + _triangleOctree.insert(i); + } + + _isBalanced = true; + + #if WANT_DEBUGGING + debugDump(); + #endif +} + + +// Determine of the given ray (origin/direction) in model space intersects with any triangles +// in the set. If an intersection occurs, the distance and surface normal will be provided. +bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) { + + bool intersectedSomething = false; + float boxDistance = distance; + float bestDistance = distance; + + if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) { + + // if our bounding box intersects at a distance greater than the current known + // best distance, than we can safely not check any of our triangles + if (boxDistance > bestDistance) { + return false; + } + + if (precision) { + for (const auto& triangleIndex : _triangleIndices) { + const auto& triangle = _allTriangles[triangleIndex]; + float thisTriangleDistance; + trianglesTouched++; + if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { + if (thisTriangleDistance < bestDistance) { + bestDistance = thisTriangleDistance; + intersectedSomething = true; + surfaceNormal = triangle.getNormal(); + distance = bestDistance; + } + } + } + } else { + intersectedSomething = true; + distance = boxDistance; + } + } + + return intersectedSomething; +} + +static const int MAX_DEPTH = 4; // for now +static const int MAX_CHILDREN = 8; + +TriangleSet::TriangleOctreeCell::TriangleOctreeCell(std::vector& allTriangles, const AABox& bounds, int depth) : + _allTriangles(allTriangles) +{ + reset(bounds, depth); +} + +void TriangleSet::TriangleOctreeCell::clear() { + _population = 0; + _triangleIndices.clear(); + _bounds.clear(); + _children.clear(); +} + +void TriangleSet::TriangleOctreeCell::reset(const AABox& bounds, int depth) { + clear(); + _bounds = bounds; + _depth = depth; +} + +void TriangleSet::TriangleOctreeCell::debugDump() { + qDebug() << __FUNCTION__; + qDebug() << "bounds:" << getBounds(); + qDebug() << "depth:" << _depth; + qDebug() << "population:" << _population << "this level or below" + << " ---- triangleIndices:" << _triangleIndices.size() << "in this cell"; + + qDebug() << "child cells:" << _children.size(); + if (_depth < MAX_DEPTH) { + int childNum = 0; + for (auto& child : _children) { + qDebug() << "child:" << childNum; + child.second.debugDump(); + childNum++; + } + } +} + +void TriangleSet::TriangleOctreeCell::insert(size_t triangleIndex) { + const Triangle& triangle = _allTriangles[triangleIndex]; + _population++; + // if we're not yet at the max depth, then check which child the triangle fits in + if (_depth < MAX_DEPTH) { + + for (int child = 0; child < MAX_CHILDREN; child++) { + AABox childBounds = getBounds().getOctreeChild((AABox::OctreeChild)child); + + + // if the child AABox would contain the triangle... + if (childBounds.contains(triangle)) { + // if the child cell doesn't yet exist, create it... + if (_children.find((AABox::OctreeChild)child) == _children.end()) { + _children.insert( + std::pair + ((AABox::OctreeChild)child, TriangleOctreeCell(_allTriangles, childBounds, _depth + 1))); + } + + // insert the triangleIndex in the child cell + _children.at((AABox::OctreeChild)child).insert(triangleIndex); + return; + } + } + } + // either we're at max depth, or the triangle doesn't fit in one of our + // children and so we want to just record it here + _triangleIndices.push_back(triangleIndex); +} + +bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) { + + if (_population < 1) { + return false; // no triangles below here, so we can't intersect + } + + float bestLocalDistance = distance; + BoxFace bestLocalFace; + glm::vec3 bestLocalNormal; + bool intersects = false; + + // if the ray intersects our bounding box, then continue + if (getBounds().findRayIntersection(origin, direction, bestLocalDistance, bestLocalFace, bestLocalNormal)) { + + // if the intersection with our bounding box, is greater than the current best distance (the distance passed in) + // then we know that none of our triangles can represent a better intersection and we can return + + if (bestLocalDistance > distance) { + return false; + } + + bestLocalDistance = distance; + + float childDistance = distance; + BoxFace childFace; + glm::vec3 childNormal; + + // if we're not yet at the max depth, then check which child the triangle fits in + if (_depth < MAX_DEPTH) { + for (auto& child : _children) { + // check each child, if there's an intersection, it will return some distance that we need + // to compare against the other results, because there might be multiple intersections and + // we will always choose the best (shortest) intersection + if (child.second.findRayIntersection(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) { + if (childDistance < bestLocalDistance) { + bestLocalDistance = childDistance; + bestLocalFace = childFace; + bestLocalNormal = childNormal; + intersects = true; + } + } + } + } + // also check our local triangle set + if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) { + if (childDistance < bestLocalDistance) { + bestLocalDistance = childDistance; + bestLocalFace = childFace; + bestLocalNormal = childNormal; + intersects = true; + } + } + } + if (intersects) { + distance = bestLocalDistance; + face = bestLocalFace; + surfaceNormal = bestLocalNormal; + } + return intersects; +} diff --git a/libraries/shared/src/TriangleSet.h b/libraries/shared/src/TriangleSet.h index b54f1a642a..6cedc4da7e 100644 --- a/libraries/shared/src/TriangleSet.h +++ b/libraries/shared/src/TriangleSet.h @@ -15,19 +15,64 @@ #include "GeometryUtil.h" class TriangleSet { -public: - void reserve(size_t size) { _triangles.reserve(size); } // reserve space in the datastructure for size number of triangles - size_t size() const { return _triangles.size(); } - const Triangle& getTriangle(size_t t) const { return _triangles[t]; } + class TriangleOctreeCell { + public: + TriangleOctreeCell(std::vector& allTriangles) : + _allTriangles(allTriangles) + { } + + void insert(size_t triangleIndex); + void reset(const AABox& bounds, int depth = 0); + void clear(); + + bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched); + + const AABox& getBounds() const { return _bounds; } + + void debugDump(); + + protected: + TriangleOctreeCell(std::vector& allTriangles, const AABox& bounds, int depth); + + // checks our internal list of triangles + bool findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched); + + std::vector& _allTriangles; + std::map _children; + int _depth{ 0 }; + int _population{ 0 }; + AABox _bounds; + std::vector _triangleIndices; + + friend class TriangleSet; + }; + +public: + TriangleSet() : + _triangleOctree(_triangles) + {} + + void debugDump(); void insert(const Triangle& t); + + bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision); + + void balanceOctree(); + + void reserve(size_t size) { _triangles.reserve(size); } // reserve space in the datastructure for size number of triangles + size_t size() const { return _triangles.size(); } void clear(); // Determine if the given ray (origin/direction) in model space intersects with any triangles in the set. If an // intersection occurs, the distance and surface normal will be provided. - bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const; + // note: this might side-effect internal structures + bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched); // Determine if a point is "inside" all the triangles of a convex hull. It is the responsibility of the caller to // determine that the triangle set is indeed a convex hull. If the triangles added to this set are not in fact a @@ -35,7 +80,10 @@ public: bool convexHullContains(const glm::vec3& point) const; const AABox& getBounds() const { return _bounds; } -private: +protected: + + bool _isBalanced{ false }; + TriangleOctreeCell _triangleOctree; std::vector _triangles; AABox _bounds; }; diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index aae1f8455f..e479559e6a 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -25,7 +25,9 @@ StoragePointer Storage::createView(size_t viewSize, size_t offset) const { viewSize = selfSize; } if ((viewSize + offset) > selfSize) { - throw std::runtime_error("Invalid mapping range"); + return StoragePointer(); + //TODO: Disable te exception for now and return an empty storage instead. + //throw std::runtime_error("Invalid mapping range"); } return std::make_shared(shared_from_this(), viewSize, data() + offset); } @@ -68,7 +70,7 @@ StoragePointer FileStorage::create(const QString& filename, size_t size, const u } FileStorage::FileStorage(const QString& filename) : _file(filename) { - if (_file.open(QFile::ReadWrite)) { + if (_file.open(QFile::ReadOnly)) { _mapped = _file.map(0, _file.size()); if (_mapped) { _valid = true; @@ -90,3 +92,34 @@ FileStorage::~FileStorage() { _file.close(); } } + +void FileStorage::ensureWriteAccess() { + if (_hasWriteAccess) { + return; + } + + if (_mapped) { + if (!_file.unmap(_mapped)) { + throw std::runtime_error("Unable to unmap file"); + } + } + if (_file.isOpen()) { + _file.close(); + } + _valid = false; + _mapped = nullptr; + + if (_file.open(QFile::ReadWrite)) { + _mapped = _file.map(0, _file.size()); + if (_mapped) { + _valid = true; + _hasWriteAccess = true; + } else { + qCWarning(storagelogging) << "Failed to map file " << _file.fileName(); + throw std::runtime_error("Failed to map file"); + } + } else { + qCWarning(storagelogging) << "Failed to open file " << _file.fileName(); + throw std::runtime_error("Failed to open file"); + } +} \ No newline at end of file diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index da5b773d52..4cad9fa083 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -60,11 +60,14 @@ namespace storage { FileStorage& operator=(const FileStorage& other) = delete; const uint8_t* data() const override { return _mapped; } - uint8_t* mutableData() override { return _mapped; } + uint8_t* mutableData() override { ensureWriteAccess(); return _mapped; } size_t size() const override { return _file.size(); } operator bool() const override { return _valid; } private: + void ensureWriteAccess(); + bool _valid { false }; + bool _hasWriteAccess { false }; QFile _file; uint8_t* _mapped { nullptr }; }; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 86b37135d2..3bda481243 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -10,6 +10,7 @@ // #include "ViveControllerManager.h" +#include #include #include @@ -20,15 +21,18 @@ #include #include #include +#include +#include +#include #include - +#include +#include +#include #include #include -#include "OpenVrHelpers.h" - extern PoseData _nextSimPoseData; vr::IVRSystem* acquireOpenVrSystem(); @@ -36,14 +40,50 @@ void releaseOpenVrSystem(); static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b"; +const quint64 CALIBRATION_TIMELAPSE = 1 * USECS_PER_SECOND; static const char* MENU_PARENT = "Avatar"; static const char* MENU_NAME = "Vive Controllers"; static const char* MENU_PATH = "Avatar" ">" "Vive Controllers"; static const char* RENDER_CONTROLLERS = "Render Hand Controllers"; +static const int MIN_PUCK_COUNT = 2; +static const int MIN_FEET_AND_HIPS = 3; +static const int MIN_FEET_HIPS_CHEST = 4; +static const int FIRST_FOOT = 0; +static const int SECOND_FOOT = 1; +static const int HIP = 2; +static const int CHEST = 3; const char* ViveControllerManager::NAME { "OpenVR" }; +const std::map TRACKING_RESULT_TO_STRING = { + {vr::TrackingResult_Uninitialized, QString("vr::TrackingResult_Uninitialized")}, + {vr::TrackingResult_Calibrating_InProgress, QString("vr::TrackingResult_Calibrating_InProgess")}, + {vr::TrackingResult_Calibrating_OutOfRange, QString("TrackingResult_Calibrating_OutOfRange")}, + {vr::TrackingResult_Running_OK, QString("TrackingResult_Running_Ok")}, + {vr::TrackingResult_Running_OutOfRange, QString("TrackingResult_Running_OutOfRange")} +}; + +static glm::mat4 computeOffset(glm::mat4 defaultToReferenceMat, glm::mat4 defaultJointMat, controller::Pose puckPose) { + glm::mat4 poseMat = createMatFromQuatAndPos(puckPose.rotation, puckPose.translation); + glm::mat4 referenceJointMat = defaultToReferenceMat * defaultJointMat; + return glm::inverse(poseMat) * referenceJointMat; +} + +static bool sortPucksYPosition(std::pair firstPuck, std::pair secondPuck) { + return (firstPuck.second.translation.y < secondPuck.second.translation.y); +} + +static QString deviceTrackingResultToString(vr::ETrackingResult trackingResult) { + QString result; + auto iterator = TRACKING_RESULT_TO_STRING.find(trackingResult); + + if (iterator != TRACKING_RESULT_TO_STRING.end()) { + return iterator->second; + } + return result; +} + bool ViveControllerManager::isSupported() const { return openVrSupported(); } @@ -122,9 +162,19 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu } } +ViveControllerManager::InputDevice::InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) { + createPreferences(); + + _configStringMap[Config::Auto] = QString("Auto"); + _configStringMap[Config::Feet] = QString("Feet"); + _configStringMap[Config::FeetAndHips] = QString("FeetAndHips"); + _configStringMap[Config::FeetHipsAndChest] = QString("FeetHipsAndChest"); +} + void ViveControllerManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { _poseStateMap.clear(); _buttonPressedMap.clear(); + _validTrackedObjects.clear(); // While the keyboard is open, we defer strictly to the keyboard values if (isOpenVrKeyboardShown()) { @@ -143,6 +193,7 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle // collect poses for all generic trackers for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { handleTrackedObject(i, inputCalibrationData); + handleHmd(i, inputCalibrationData); } // handle haptics @@ -164,33 +215,201 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle numTrackedControllers++; } _trackedControllers = numTrackedControllers; + + if (checkForCalibrationEvent()) { + quint64 currentTime = usecTimestampNow(); + if (!_timeTilCalibrationSet) { + _timeTilCalibrationSet = true; + _timeTilCalibration = currentTime + CALIBRATION_TIMELAPSE; + } + + if (currentTime > _timeTilCalibration && !_triggersPressedHandled) { + _triggersPressedHandled = true; + calibrateOrUncalibrate(inputCalibrationData); + } + } else { + _triggersPressedHandled = false; + _timeTilCalibrationSet = false; + } + + updateCalibratedLimbs(); + _lastSimPoseData = _nextSimPoseData; } void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) { - uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex; - + printDeviceTrackingResultChange(deviceIndex); if (_system->IsTrackedDeviceConnected(deviceIndex) && _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_GenericTracker && _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid && poseIndex <= controller::TRACKED_OBJECT_15) { - // process pose - const mat4& mat = _nextSimPoseData.poses[deviceIndex]; - const vec3 linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; - const vec3 angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; + mat4& mat = mat4(); + vec3 linearVelocity = vec3(); + vec3 angularVelocity = vec3(); + // check if the device is tracking out of range, then process the correct pose depending on the result. + if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult != vr::TrackingResult_Running_OutOfRange) { + mat = _nextSimPoseData.poses[deviceIndex]; + linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; + angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; + } else { + mat = _lastSimPoseData.poses[deviceIndex]; + linearVelocity = _lastSimPoseData.linearVelocities[deviceIndex]; + angularVelocity = _lastSimPoseData.angularVelocities[deviceIndex]; + + // make sure that we do not overwrite the pose in the _lastSimPose with incorrect data. + _nextSimPoseData.poses[deviceIndex] = _lastSimPoseData.poses[deviceIndex]; + _nextSimPoseData.linearVelocities[deviceIndex] = _lastSimPoseData.linearVelocities[deviceIndex]; + _nextSimPoseData.angularVelocities[deviceIndex] = _lastSimPoseData.angularVelocities[deviceIndex]; + + } controller::Pose pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; _poseStateMap[poseIndex] = pose.transform(controllerToAvatar); + _validTrackedObjects.push_back(std::make_pair(poseIndex, _poseStateMap[poseIndex])); } else { controller::Pose invalidPose; _poseStateMap[poseIndex] = invalidPose; } } +void ViveControllerManager::InputDevice::calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration) { + if (!_calibrated) { + calibrate(inputCalibration); + } else { + uncalibrate(); + } +} + +void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibrationData& inputCalibration) { + qDebug() << "Puck Calibration: Starting..."; + // convert the hmd head from sensor space to avatar space + glm::mat4 hmdSensorFlippedMat = inputCalibration.hmdSensorMat * Matrices::Y_180; + glm::mat4 sensorToAvatarMat = glm::inverse(inputCalibration.avatarMat) * inputCalibration.sensorToWorldMat; + glm::mat4 hmdAvatarMat = sensorToAvatarMat * hmdSensorFlippedMat; + + // cancel the roll and pitch for the hmd head + glm::quat hmdRotation = cancelOutRollAndPitch(glmExtractRotation(hmdAvatarMat)); + glm::vec3 hmdTranslation = extractTranslation(hmdAvatarMat); + glm::mat4 currentHmd = createMatFromQuatAndPos(hmdRotation, hmdTranslation); + + // calculate the offset from the centerOfEye to defaultHeadMat + glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat; + + glm::mat4 currentHead = currentHmd * defaultHeadOffset; + + // calculate the defaultToRefrenceXform + glm::mat4 defaultToReferenceMat = currentHead * glm::inverse(inputCalibration.defaultHeadMat); + + int puckCount = (int)_validTrackedObjects.size(); + qDebug() << "Puck Calibration: " << puckCount << " pucks found for calibration"; + _config = _preferedConfig; + if (_config != Config::Auto && puckCount < MIN_PUCK_COUNT) { + qDebug() << "Puck Calibration: Failed: Could not meet the minimal # of pucks"; + uncalibrate(); + return; + } else if (_config == Config::Auto){ + if (puckCount == MIN_PUCK_COUNT) { + _config = Config::Feet; + qDebug() << "Puck Calibration: Auto Config: " << configToString(_config) << " configuration"; + } else if (puckCount == MIN_FEET_AND_HIPS) { + _config = Config::FeetAndHips; + qDebug() << "Puck Calibration: Auto Config: " << configToString(_config) << " configuration"; + } else if (puckCount >= MIN_FEET_HIPS_CHEST) { + _config = Config::FeetHipsAndChest; + qDebug() << "Puck Calibration: Auto Config: " << configToString(_config) << " configuration"; + } else { + qDebug() << "Puck Calibration: Auto Config Failed: Could not meet the minimal # of pucks"; + uncalibrate(); + return; + } + } + + std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksYPosition); + + auto& firstFoot = _validTrackedObjects[FIRST_FOOT]; + auto& secondFoot = _validTrackedObjects[SECOND_FOOT]; + controller::Pose& firstFootPose = firstFoot.second; + controller::Pose& secondFootPose = secondFoot.second; + + if (firstFootPose.translation.x < secondFootPose.translation.x) { + _jointToPuckMap[controller::LEFT_FOOT] = firstFoot.first; + _pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, firstFootPose); + _jointToPuckMap[controller::RIGHT_FOOT] = secondFoot.first; + _pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, secondFootPose); + + } else { + _jointToPuckMap[controller::LEFT_FOOT] = secondFoot.first; + _pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, secondFootPose); + _jointToPuckMap[controller::RIGHT_FOOT] = firstFoot.first; + _pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, firstFootPose); + } + + if (_config == Config::Feet) { + // done + } else if (_config == Config::FeetAndHips && puckCount >= MIN_FEET_AND_HIPS) { + _jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first; + _pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second); + } else if (_config == Config::FeetHipsAndChest && puckCount >= MIN_FEET_HIPS_CHEST) { + _jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first; + _pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second); + _jointToPuckMap[controller::SPINE2] = _validTrackedObjects[CHEST].first; + _pucksOffset[_validTrackedObjects[CHEST].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultSpine2, _validTrackedObjects[CHEST].second); + } else { + qDebug() << "Puck Calibration: " << configToString(_config) << " Config Failed: Could not meet the minimal # of pucks"; + uncalibrate(); + return; + } + _calibrated = true; + qDebug() << "PuckCalibration: " << configToString(_config) << " Configuration Successful"; +} + +void ViveControllerManager::InputDevice::uncalibrate() { + _config = Config::Auto; + _pucksOffset.clear(); + _jointToPuckMap.clear(); + _calibrated = false; +} + +void ViveControllerManager::InputDevice::updateCalibratedLimbs() { + _poseStateMap[controller::LEFT_FOOT] = addOffsetToPuckPose(controller::LEFT_FOOT); + _poseStateMap[controller::RIGHT_FOOT] = addOffsetToPuckPose(controller::RIGHT_FOOT); + _poseStateMap[controller::HIPS] = addOffsetToPuckPose(controller::HIPS); + _poseStateMap[controller::SPINE2] = addOffsetToPuckPose(controller::SPINE2); +} + +controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(int joint) const { + auto puck = _jointToPuckMap.find(joint); + if (puck != _jointToPuckMap.end()) { + uint32_t puckIndex = puck->second; + auto puckPose = _poseStateMap.find(puckIndex); + auto puckOffset = _pucksOffset.find(puckIndex); + + if ((puckPose != _poseStateMap.end()) && (puckOffset != _pucksOffset.end())) { + return puckPose->second.postTransform(puckOffset->second); + } + } + return controller::Pose(); +} + +void ViveControllerManager::InputDevice::handleHmd(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) { + uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex; + + if (_system->IsTrackedDeviceConnected(deviceIndex) && + _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_HMD && + _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) { + + const mat4& mat = _nextSimPoseData.poses[deviceIndex]; + const vec3 linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; + const vec3 angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; + + handleHeadPoseEvent(inputCalibrationData, mat, linearVelocity, angularVelocity); + } +} + void ViveControllerManager::InputDevice::handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand) { if (_system->IsTrackedDeviceConnected(deviceIndex) && @@ -262,7 +481,7 @@ 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, + // 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); @@ -276,6 +495,22 @@ enum ViveButtonChannel { RIGHT_APP_MENU }; +void ViveControllerManager::InputDevice::printDeviceTrackingResultChange(uint32_t deviceIndex) { + if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult != _lastSimPoseData.vrPoses[deviceIndex].eTrackingResult) { + qDebug() << "OpenVR: Device" << deviceIndex << "Tracking Result changed from" << + deviceTrackingResultToString(_lastSimPoseData.vrPoses[deviceIndex].eTrackingResult) + << "to" << deviceTrackingResultToString(_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult); + } +} + +bool ViveControllerManager::InputDevice::checkForCalibrationEvent() { + auto& endOfMap = _buttonPressedMap.end(); + auto& leftTrigger = _buttonPressedMap.find(controller::LT); + auto& rightTrigger = _buttonPressedMap.find(controller::RT); + auto& leftAppButton = _buttonPressedMap.find(LEFT_APP_MENU); + auto& rightAppButton = _buttonPressedMap.find(RIGHT_APP_MENU); + return ((leftTrigger != endOfMap && leftAppButton != endOfMap) && (rightTrigger != endOfMap && rightAppButton != endOfMap)); +} // These functions do translation from the Steam IDs to the standard controller IDs void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand) { @@ -305,6 +540,19 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint } } +void ViveControllerManager::InputDevice::handleHeadPoseEvent(const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, + const vec3& linearVelocity, const vec3& angularVelocity) { + + //perform a 180 flip to make the HMD face the +z instead of -z, beacuse the head faces +z + glm::mat4 matYFlip = mat * Matrices::Y_180; + controller::Pose pose(extractTranslation(matYFlip), glmExtractRotation(matYFlip), linearVelocity, angularVelocity); + + glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; + controller::Pose hmdHeadPose = pose.transform(sensorToAvatar); + _poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset); +} + void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand) { @@ -353,7 +601,7 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef float hapticTime = strength * MAX_HAPTIC_TIME; if (hapticTime < duration * 1000.0f) { _system->TriggerHapticPulse(deviceIndex, 0, hapticTime); - } + } float remainingHapticTime = duration - (hapticTime / 1000.0f + deltaTime * 1000.0f); // in milliseconds if (leftHand) { @@ -364,6 +612,56 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef } } +void ViveControllerManager::InputDevice::loadSettings() { + Settings settings; + settings.beginGroup("PUCK_CONFIG"); + { + _preferedConfig = (Config)settings.value("configuration", QVariant((int)Config::Auto)).toInt(); + } + settings.endGroup(); +} + +void ViveControllerManager::InputDevice::saveSettings() const { + Settings settings; + settings.beginGroup("PUCK_CONFIG"); + { + settings.setValue(QString("configuration"), (int)_preferedConfig); + } + settings.endGroup(); +} + +QString ViveControllerManager::InputDevice::configToString(Config config) { + return _configStringMap[config]; +} + +void ViveControllerManager::InputDevice::setConfigFromString(const QString& value) { + if (value == "Auto") { + _preferedConfig = Config::Auto; + } else if (value == "Feet") { + _preferedConfig = Config::Feet; + } else if (value == "FeetAndHips") { + _preferedConfig = Config::FeetAndHips; + } else if (value == "FeetHipsAndChest") { + _preferedConfig = Config::FeetHipsAndChest; + } +} + +void ViveControllerManager::InputDevice::createPreferences() { + loadSettings(); + auto preferences = DependencyManager::get(); + static const QString VIVE_PUCKS_CONFIG = "Vive Pucks Configuration"; + + { + auto getter = [this]()->QString { return _configStringMap[_preferedConfig]; }; + auto setter = [this](const QString& value) { setConfigFromString(value); saveSettings(); }; + auto preference = new ComboBoxPreference(VIVE_PUCKS_CONFIG, "Configuration", getter, setter); + QStringList list = (QStringList() << "Auto" << "Feet" << "FeetAndHips" << "FeetHipsAndChest"); + preference->setItems(list); + preferences->addPreference(preference); + + } +} + controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableInputs() const { using namespace controller; QVector availableInputs{ @@ -404,6 +702,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI // 3d location of controller makePair(LEFT_HAND, "LeftHand"), makePair(RIGHT_HAND, "RightHand"), + makePair(LEFT_FOOT, "LeftFoot"), + makePair(RIGHT_FOOT, "RightFoot"), + makePair(HIPS, "Hips"), + makePair(SPINE2, "Spine2"), + makePair(HEAD, "Head"), // 16 tracked poses makePair(TRACKED_OBJECT_00, "TrackedObject00"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index dc1883d5e4..fa2566da45 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -14,15 +14,18 @@ #include #include +#include +#include +#include #include - #include #include #include #include #include #include +#include "OpenVrHelpers.h" namespace vr { class IVRSystem; @@ -48,24 +51,33 @@ public: private: class InputDevice : public controller::InputDevice { public: - InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) {} + InputDevice(vr::IVRSystem*& system); private: // Device functions controller::Input::NamedVector getAvailableInputs() const override; QString getDefaultMappingConfig() const override; void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; void focusOutEvent() override; - + void createPreferences(); bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override; void hapticsHelper(float deltaTime, bool leftHand); - + void calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration); + void calibrate(const controller::InputCalibrationData& inputCalibration); + void uncalibrate(); + controller::Pose addOffsetToPuckPose(int joint) const; + void updateCalibratedLimbs(); + bool checkForCalibrationEvent(); void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); + void handleHmd(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData); void handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData); 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); void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand); + void handleHeadPoseEvent(const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, + const vec3& angularVelocity); void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton); + void printDeviceTrackingResultChange(uint32_t deviceIndex); class FilteredStick { public: @@ -90,10 +102,17 @@ private: float _timer { 0.0f }; glm::vec2 _stick { 0.0f, 0.0f }; }; - + enum class Config { Feet, FeetAndHips, FeetHipsAndChest, Auto }; + Config _config { Config::Auto }; + Config _preferedConfig { Config::Auto }; FilteredStick _filteredLeftStick; FilteredStick _filteredRightStick; + std::vector> _validTrackedObjects; + std::map _pucksOffset; + std::map _jointToPuckMap; + std::map _configStringMap; + PoseData _lastSimPoseData; // perform an action when the InputDevice mutex is acquired. using Locker = std::unique_lock; template @@ -101,12 +120,20 @@ private: int _trackedControllers { 0 }; vr::IVRSystem*& _system; + quint64 _timeTilCalibration { 0.0f }; float _leftHapticStrength { 0.0f }; float _leftHapticDuration { 0.0f }; float _rightHapticStrength { 0.0f }; float _rightHapticDuration { 0.0f }; + bool _triggersPressedHandled { false }; + bool _calibrated { false }; + bool _timeTilCalibrationSet { false }; mutable std::recursive_mutex _lock; + QString configToString(Config config); + void setConfigFromString(const QString& value); + void loadSettings(); + void saveSettings() const; friend class ViveControllerManager; }; diff --git a/script-archive/vrShop/item/shopItemGrab.js b/script-archive/vrShop/item/shopItemGrab.js index 2b63ca1b53..f0488cb497 100644 --- a/script-archive/vrShop/item/shopItemGrab.js +++ b/script-archive/vrShop/item/shopItemGrab.js @@ -1,6 +1,6 @@ // shopItemGrab.js // -// Semplified and coarse version of handControllerGrab.js with the addition of the ownerID concept. +// Simplified and coarse version of handControllerGrab.js with the addition of the ownerID concept. // This grab is the only one which should run in the vrShop. It allows only near grab and add the feature of checking the ownerID. (See shopGrapSwapperEntityScript.js) // @@ -63,8 +63,8 @@ var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-g // equip // -var EQUIP_SPRING_SHUTOFF_DISTANCE = 0.05; -var EQUIP_SPRING_TIMEFRAME = 0.4; // how quickly objects move to their new position +var EQUIP_TRACTOR_SHUTOFF_DISTANCE = 0.05; +var EQUIP_TRACTOR_TIMEFRAME = 0.4; // how quickly objects move to their new position // // other constants @@ -121,7 +121,7 @@ var STATE_EQUIP_SEARCHING = 11; var STATE_EQUIP = 12 var STATE_CONTINUE_EQUIP_BD = 13; // equip while bumper is still held down var STATE_CONTINUE_EQUIP = 14; -var STATE_EQUIP_SPRING = 16; +var STATE_EQUIP_TRACTOR = 16; function stateToName(state) { @@ -152,8 +152,8 @@ function stateToName(state) { return "continue_equip_bd"; case STATE_CONTINUE_EQUIP: return "continue_equip"; - case STATE_EQUIP_SPRING: - return "state_equip_spring"; + case STATE_EQUIP_TRACTOR: + return "state_equip_tractor"; } return "unknown"; @@ -216,7 +216,7 @@ function MyController(hand) { case STATE_EQUIP: this.nearGrabbing(); break; - case STATE_EQUIP_SPRING: + case STATE_EQUIP_TRACTOR: this.pullTowardEquipPosition() break; case STATE_CONTINUE_NEAR_GRABBING: @@ -404,14 +404,14 @@ function MyController(hand) { return; } else if (!properties.locked) { var ownerObj = getEntityCustomData('ownerKey', intersection.entityID, null); - + if (ownerObj == null || ownerObj.ownerID === MyAvatar.sessionUUID) { //I can only grab new or already mine items this.grabbedEntity = intersection.entityID; if (this.state == STATE_SEARCHING) { this.setState(STATE_NEAR_GRABBING); } else { // equipping if (typeof grabbableData.spatialKey !== 'undefined') { - this.setState(STATE_EQUIP_SPRING); + this.setState(STATE_EQUIP_TRACTOR); } else { this.setState(STATE_EQUIP); } @@ -558,7 +558,7 @@ function MyController(hand) { var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); - // use a spring to pull the object to where it will be when equipped + // use a tractor to pull the object to where it will be when equipped var relativeRotation = { x: 0.0, y: 0.0, @@ -582,34 +582,34 @@ function MyController(hand) { var offset = Vec3.multiplyQbyV(targetRotation, relativePosition); var targetPosition = Vec3.sum(handPosition, offset); - if (typeof this.equipSpringID === 'undefined' || - this.equipSpringID === null || - this.equipSpringID === NULL_ACTION_ID) { - this.equipSpringID = Entities.addAction("spring", this.grabbedEntity, { + if (typeof this.equipTractorID === 'undefined' || + this.equipTractorID === null || + this.equipTractorID === NULL_ACTION_ID) { + this.equipTractorID = Entities.addAction("tractor", this.grabbedEntity, { targetPosition: targetPosition, - linearTimeScale: EQUIP_SPRING_TIMEFRAME, + linearTimeScale: EQUIP_TRACTOR_TIMEFRAME, targetRotation: targetRotation, - angularTimeScale: EQUIP_SPRING_TIMEFRAME, + angularTimeScale: EQUIP_TRACTOR_TIMEFRAME, ttl: ACTION_TTL }); - if (this.equipSpringID === NULL_ACTION_ID) { - this.equipSpringID = null; + if (this.equipTractorID === NULL_ACTION_ID) { + this.equipTractorID = null; this.setState(STATE_OFF); return; } } else { - Entities.updateAction(this.grabbedEntity, this.equipSpringID, { + Entities.updateAction(this.grabbedEntity, this.equipTractorID, { targetPosition: targetPosition, - linearTimeScale: EQUIP_SPRING_TIMEFRAME, + linearTimeScale: EQUIP_TRACTOR_TIMEFRAME, targetRotation: targetRotation, - angularTimeScale: EQUIP_SPRING_TIMEFRAME, + angularTimeScale: EQUIP_TRACTOR_TIMEFRAME, ttl: ACTION_TTL }); } - if (Vec3.distance(grabbedProperties.position, targetPosition) < EQUIP_SPRING_SHUTOFF_DISTANCE) { - Entities.deleteAction(this.grabbedEntity, this.equipSpringID); - this.equipSpringID = null; + if (Vec3.distance(grabbedProperties.position, targetPosition) < EQUIP_TRACTOR_SHUTOFF_DISTANCE) { + Entities.deleteAction(this.grabbedEntity, this.equipTractorID); + this.equipTractorID = null; this.setState(STATE_EQUIP); } }; @@ -862,4 +862,4 @@ function cleanup() { } Script.scriptEnding.connect(cleanup); -Script.update.connect(update); \ No newline at end of file +Script.update.connect(update); diff --git a/scripts/developer/tests/dynamics/dynamics-tests.html b/scripts/developer/tests/dynamics/dynamics-tests.html index 0f324e121c..6ef82c4b70 100644 --- a/scripts/developer/tests/dynamics/dynamics-tests.html +++ b/scripts/developer/tests/dynamics/dynamics-tests.html @@ -9,8 +9,8 @@ lifetime:
-
- A platform with a lever. The lever can be moved in a cone and rotated. A spring brings it back to its neutral position. +
+ A platform with a lever. The lever can be moved in a cone and rotated. A tractor brings it back to its neutral position.

A grabbable door with a hinge between it and world-space. @@ -31,7 +31,7 @@ A chain of spheres connected by ball-and-socket joints coincident-with the spheres.

- A self-righting ragdoll. The head is on a weak spring vs the body. + A self-righting ragdoll. The head is on a weak tractor vs the body. diff --git a/scripts/developer/tests/dynamics/dynamicsTests.js b/scripts/developer/tests/dynamics/dynamicsTests.js index 376eff182b..c0b001eab3 100644 --- a/scripts/developer/tests/dynamics/dynamicsTests.js +++ b/scripts/developer/tests/dynamics/dynamicsTests.js @@ -28,7 +28,7 @@ - function coneTwistAndSpringLeverTest(params) { + function coneTwistAndTractorLeverTest(params) { var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: -0.5, z: -2})); var lifetime = params.lifetime; @@ -84,10 +84,10 @@ tag: "cone-twist test" }); - Entities.addAction("spring", leverID, { + Entities.addAction("tractor", leverID, { targetRotation: { x: 0, y: 0, z: 0, w: 1 }, angularTimeScale: 0.2, - tag: "cone-twist test spring" + tag: "cone-twist test tractor" }); @@ -349,11 +349,11 @@ userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" }); - Entities.addAction("spring", headID, { + Entities.addAction("tractor", headID, { targetRotation: { x: 0, y: 0, z: 0, w: 1 }, angularTimeScale: 2.0, otherID: bodyID, - tag: "cone-twist test spring" + tag: "cone-twist test tractor" }); @@ -705,7 +705,7 @@ if (event["dynamics-tests-command"]) { var commandToFunctionMap = { - "cone-twist-and-spring-lever-test": coneTwistAndSpringLeverTest, + "cone-twist-and-tractor-lever-test": coneTwistAndTractorLeverTest, "door-vs-world-test": doorVSWorldTest, "hinge-chain-test": hingeChainTest, "slider-vs-world-test": sliderVSWorldTest, diff --git a/scripts/developer/tests/performance/rayPickPerformance.js b/scripts/developer/tests/performance/rayPickPerformance.js new file mode 100644 index 0000000000..b4faf4c1be --- /dev/null +++ b/scripts/developer/tests/performance/rayPickPerformance.js @@ -0,0 +1,131 @@ +// +// rayPickingPerformance.js +// examples +// +// Created by Brad Hefta-Gaub on 5/13/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + + +var MIN_RANGE = -3; +var MAX_RANGE = 3; +var RANGE_DELTA = 0.5; +var OUTER_LOOPS = 10; + +// NOTE: These expected results depend completely on the model, and the range settings above +var EXPECTED_TESTS = 1385 * OUTER_LOOPS; +var EXPECTED_INTERSECTIONS = 1286 * OUTER_LOOPS; + + +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); +var model_url = "http://hifi-content.s3.amazonaws.com/caitlyn/production/Scansite/buddhaReduced.fbx"; + +var rayPickOverlays = Array(); + +var modelEntity = Entities.addEntity({ + type: "Model", + modelURL: model_url, + dimensions: { + x: 0.671, + y: 1.21, + z: 0.938 + }, + position: center +}); + +function rayCastTest() { + var tests = 0; + var intersections = 0; + + var testStart = Date.now(); + for (var t = 0; t < OUTER_LOOPS; t++) { + print("beginning loop:" + t); + for (var x = MIN_RANGE; x < MAX_RANGE; x += RANGE_DELTA) { + for (var y = MIN_RANGE; y < MAX_RANGE; y += RANGE_DELTA) { + for (var z = MIN_RANGE; z < MAX_RANGE; z += RANGE_DELTA) { + if ((x <= -2 || x >= 2) || + (y <= -2 || y >= 2) || + (z <= -2 || z >= 2)) { + + tests++; + + var origin = { x: center.x + x, + y: center.y + y, + z: center.z + z }; + var direction = Vec3.subtract(center, origin); + + var pickRay = { + origin: origin, + direction: direction + }; + + var pickResults = Entities.findRayIntersection(pickRay, true); + + var color; + var visible; + + if (pickResults.intersects && pickResults.entityID == modelEntity) { + intersections++; + color = { + red: 0, + green: 255, + blue: 0 + }; + visible = false; + + } else { + /* + print("NO INTERSECTION?"); + Vec3.print("origin:", origin); + Vec3.print("direction:", direction); + */ + + color = { + red: 255, + green: 0, + blue: 0 + }; + visible = true; + } + + var overlayID = Overlays.addOverlay("line3d", { + color: color, + alpha: 1, + visible: visible, + lineWidth: 2, + start: origin, + end: Vec3.sum(origin,Vec3.multiply(5,direction)) + }); + + rayPickOverlays.push(overlayID); + + } + } + } + } + print("ending loop:" + t); + } + var testEnd = Date.now(); + var testElapsed = testEnd - testStart; + + + print("EXPECTED tests:" + EXPECTED_TESTS + " intersections:" + EXPECTED_INTERSECTIONS); + print("ACTUAL tests:" + tests + " intersections:" + intersections); + print("ELAPSED TIME:" + testElapsed + " ms"); + +} + +function cleanup() { + Entities.deleteEntity(modelEntity); + rayPickOverlays.forEach(function(item){ + Overlays.deleteOverlay(item); + }); +} + +Script.scriptEnding.connect(cleanup); + +rayCastTest(); // run ray cast test immediately \ No newline at end of file diff --git a/scripts/developer/tests/printTest.js b/scripts/developer/tests/printTest.js new file mode 100644 index 0000000000..c1fe6ec745 --- /dev/null +++ b/scripts/developer/tests/printTest.js @@ -0,0 +1,39 @@ +/* eslint-env jasmine */ + +// this test generates sample print, Script.print, etc. output + +main(); + +function main() { + // to match with historical behavior, Script.print(message) output only triggers + // the printedMessage signal (and therefore doesn't show up in the application log) + Script.print('[Script.print] hello world'); + + // the rest of these should show up in both the application log and signaled print handlers + print('[print]', 'hello', 'world'); + + // note: these trigger the equivalent of an emit + Script.printedMessage('[Script.printedMessage] hello world', '{filename}'); + Script.infoMessage('[Script.infoMessage] hello world', '{filename}'); + Script.warningMessage('[Script.warningMessage] hello world', '{filename}'); + Script.errorMessage('[Script.errorMessage] hello world', '{filename}'); + + { + Vec3.print('[Vec3.print]', Vec3.HALF); + + var q = Quat.fromPitchYawRollDegrees(45, 45, 45); + Quat.print('[Quat.print]', q); + Quat.print('[Quat.print (euler)]', q, true); + + function vec4(x,y,z,w) { + return { x: x, y: y, z: z, w: w }; + } + var m = Mat4.createFromColumns( + vec4(1,2,3,4), vec4(5,6,7,8), vec4(9,10,11,12), vec4(13,14,15,16) + ); + Mat4.print('[Mat4.print (col major)]', m); + Mat4.print('[Mat4.print (row major)]', m, true); + + Uuid.print('[Uuid.print]', Uuid.fromString(Uuid.toString(0))); + } +} diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index b97e1ff049..f5c3e6eafa 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1376,7 +1376,9 @@ function MyController(hand) { visible: true, alpha: 1, parentID: AVATAR_SELF_ID, - parentJointIndex: this.controllerJointIndex, + parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"), endParentID: farParentID }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index a6d2d165f7..f39165f3df 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -309,7 +309,7 @@ var toolBar = (function () { gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } }); } - } + } } function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. @@ -482,22 +482,52 @@ var toolBar = (function () { createNewEntity({ type: "ParticleEffect", isEmitting: true, + emitterShouldTrail: true, + color: { + red: 200, + green: 200, + blue: 200 + }, + colorSpread: { + red: 0, + green: 0, + blue: 0 + }, + colorStart: { + red: 200, + green: 200, + blue: 200 + }, + colorFinish: { + red: 0, + green: 0, + blue: 0 + }, emitAcceleration: { - x: 0, - y: -1, - z: 0 + x: -0.5, + y: 2.5, + z: -0.5 }, accelerationSpread: { - x: 5, - y: 0, - z: 5 + x: 0.5, + y: 1, + z: 0.5 }, - emitSpeed: 1, - lifespan: 1, - particleRadius: 0.025, + emitRate: 5.5, + emitSpeed: 0, + speedSpread: 0, + lifespan: 1.5, + maxParticles: 10, + particleRadius: 0.25, + radiusStart: 0, + radiusFinish: 0.1, + radiusSpread: 0, + alpha: 0, + alphaStart: 1, alphaFinish: 0, - emitRate: 100, - textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png" + polarStart: 0, + polarFinish: 0, + textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png" }); }); @@ -656,7 +686,7 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) { return; var data = JSON.parse(message); - + if (data.method === "selectOverlay") { print("setting selection to overlay " + data.overlayID); var entity = entityIconOverlayManager.findEntity(data.overlayID); @@ -664,7 +694,7 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) { if (entity !== null) { selectionManager.setSelections([entity]); } - } + } } Messages.subscribe("entityToolUpdates"); @@ -774,7 +804,7 @@ function wasTabletClicked(event) { var result = Overlays.findRayIntersection(rayPick, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); return result.intersects; } - + function mouseClickEvent(event) { var wantDebug = false; var result, properties, tabletClicked; @@ -784,7 +814,7 @@ function mouseClickEvent(event) { if (tabletClicked) { return; } - + if (result === null || result === undefined) { if (!event.isShifted) { selectionManager.clearSelections(); @@ -2062,7 +2092,7 @@ function selectParticleEntity(entityID) { selectedParticleEntity = entityID; particleExplorerTool.setActiveParticleEntity(entityID); - particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); // Switch to particle explorer var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 32a0956615..d52ff3d4a6 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -44,6 +44,7 @@ function showSetupComplete() { '

Snapshot location set.

' + '

Press the big red button to take a snap!

' + ''; + document.getElementById("snap-button").disabled = false; } function showSnapshotInstructions() { var snapshotImagesDiv = document.getElementById("snapshot-images"); @@ -69,7 +70,6 @@ function login() { })); } function clearImages() { - document.getElementById("snap-button").disabled = false; var snapshotImagesDiv = document.getElementById("snapshot-images"); snapshotImagesDiv.classList.remove("snapshotInstructions"); while (snapshotImagesDiv.hasChildNodes()) { @@ -359,7 +359,7 @@ function showUploadingMessage(selectedID, destination) { shareBarHelp.classList.add("uploading"); shareBarHelp.setAttribute("data-destination", destination); } -function hideUploadingMessageAndShare(selectedID, storyID) { +function hideUploadingMessageAndMaybeShare(selectedID, storyID) { if (selectedID.id) { selectedID = selectedID.id; // sometimes (?), `containerID` is passed as an HTML object to these functions; we just want the ID } @@ -382,21 +382,28 @@ function hideUploadingMessageAndShare(selectedID, storyID) { var facebookButton = document.getElementById(selectedID + "facebookButton"); window.open(facebookButton.getAttribute("href"), "_blank"); shareBarHelp.innerHTML = facebookShareText; + // This emitWebEvent() call isn't necessary in the "hifi" and "blast" cases + // because the "removeFromStoryIDsToMaybeDelete()" call happens + // in snapshot.js when sharing with that method. + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "removeFromStoryIDsToMaybeDelete", + story_id: storyID + })); break; case 'twitter': var twitterButton = document.getElementById(selectedID + "twitterButton"); window.open(twitterButton.getAttribute("href"), "_blank"); shareBarHelp.innerHTML = twitterShareText; + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "removeFromStoryIDsToMaybeDelete", + story_id: storyID + })); break; } shareBarHelp.setAttribute("data-destination", ""); - - EventBridge.emitWebEvent(JSON.stringify({ - type: "snapshot", - action: "removeFromStoryIDsToMaybeDelete", - story_id: storyID - })); } } function updateShareInfo(containerID, storyID) { @@ -415,9 +422,9 @@ function updateShareInfo(containerID, storyID) { facebookButton.setAttribute("href", 'https://www.facebook.com/dialog/feed?app_id=1585088821786423&link=' + shareURL); twitterButton.setAttribute("target", "_blank"); - twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelity&hashtags=VR,HiFi'); + twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelityinc&hashtags=VR,HiFi'); - hideUploadingMessageAndShare(containerID, storyID); + hideUploadingMessageAndMaybeShare(containerID, storyID); } function blastToConnections(selectedID, isGif) { if (selectedID.id) { @@ -552,6 +559,12 @@ function shareButtonClicked(destination, selectedID) { if (!storyID) { showUploadingMessage(selectedID, destination); + } else { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "removeFromStoryIDsToMaybeDelete", + story_id: storyID + })); } } @@ -637,6 +650,7 @@ window.onload = function () { shareForUrl("p1"); appendShareBar("p1", messageOptions.isLoggedIn, messageOptions.canShare, true, false, false, messageOptions.canBlast); document.getElementById("p1").classList.remove("processingGif"); + document.getElementById("snap-button").disabled = false; } } else { imageCount = message.image_data.length; @@ -675,6 +689,9 @@ function takeSnapshot() { type: "snapshot", action: "takeSnapshot" })); + if (document.getElementById('stillAndGif').checked === true) { + document.getElementById("snap-button").disabled = true; + } } function testInBrowser(test) { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 2f109597d7..e000e14aec 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1020,7 +1020,7 @@ function loaded() { elTextText.value = properties.text; elTextLineHeight.value = properties.lineHeight.toFixed(4); - elTextFaceCamera = properties.faceCamera; + elTextFaceCamera.checked = 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; diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 1493ce7953..757743accc 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -96,7 +96,7 @@ function calcSpawnInfo(hand, height) { * @param hand [number] -1 indicates no hand, Controller.Standard.RightHand or Controller.Standard.LeftHand * @param clientOnly [bool] true indicates tablet model is only visible to client. */ -WebTablet = function (url, width, dpi, hand, clientOnly, location) { +WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { var _this = this; @@ -107,6 +107,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) { this.depth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor; this.landscape = false; + visible = visible === true; + if (dpi) { this.dpi = dpi; } else { @@ -125,7 +127,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) { "grabbableKey": {"grabbable": true} }), dimensions: this.getDimensions(), - parentID: AVATAR_SELF_ID + parentID: AVATAR_SELF_ID, + visible: visible }; // compute position, rotation & parentJointIndex of the tablet @@ -158,7 +161,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) { parentID: this.tabletEntityID, parentJointIndex: -1, showKeyboardFocusHighlight: false, - isAA: HMD.active + isAA: HMD.active, + visible: visible }); var HOME_BUTTON_Y_OFFSET = (this.height / 2) - (this.height / 20); @@ -168,7 +172,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) { localRotation: {x: 0, y: 1, z: 0, w: 0}, dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor}, alpha: 0.0, - visible: true, + visible: visible, drawInFront: false, parentID: this.tabletEntityID, parentJointIndex: -1 diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 79d45d5cd2..725803f824 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -3,6 +3,7 @@ // examples // // Created by Brad hefta-Gaub on 10/1/14. +// Modified by Daniela Fontes @DanielaFifo and Tiago Andrade @TagoWill on 4/7/2017 // Copyright 2014 High Fidelity, Inc. // // This script implements a class useful for building tools for editing entities. @@ -340,6 +341,11 @@ SelectionDisplay = (function() { green: 120, blue: 120 }; + var grabberColorCloner = { + red: 0, + green: 155, + blue: 0 + }; var grabberLineWidth = 0.5; var grabberSolid = true; var grabberMoveUpPosition = { @@ -405,6 +411,23 @@ SelectionDisplay = (function() { borderSize: 1.4, }; + var grabberPropertiesCloner = { + position: { + x: 0, + y: 0, + z: 0 + }, + size: grabberSizeCorner, + color: grabberColorCloner, + alpha: 1, + solid: grabberSolid, + visible: false, + dashed: false, + lineWidth: grabberLineWidth, + drawInFront: true, + borderSize: 1.4, + }; + var spotLightLineProperties = { color: lightOverlayColor, lineWidth: 1.5, @@ -582,6 +605,8 @@ SelectionDisplay = (function() { var grabberPointLightF = Overlays.addOverlay("cube", grabberPropertiesEdge); var grabberPointLightN = Overlays.addOverlay("cube", grabberPropertiesEdge); + var grabberCloner = Overlays.addOverlay("cube", grabberPropertiesCloner); + var stretchHandles = [ grabberLBN, grabberRBN, @@ -628,6 +653,8 @@ SelectionDisplay = (function() { grabberPointLightR, grabberPointLightF, grabberPointLightN, + + grabberCloner ]; @@ -969,6 +996,7 @@ SelectionDisplay = (function() { grabberPointLightCircleX, grabberPointLightCircleY, grabberPointLightCircleZ, + ].concat(stretchHandles); overlayNames[highlightBox] = "highlightBox"; @@ -1015,7 +1043,7 @@ SelectionDisplay = (function() { overlayNames[rotateZeroOverlay] = "rotateZeroOverlay"; overlayNames[rotateCurrentOverlay] = "rotateCurrentOverlay"; - + overlayNames[grabberCloner] = "grabberCloner"; var activeTool = null; var grabberTools = {}; @@ -2135,6 +2163,12 @@ SelectionDisplay = (function() { position: FAR }); + Overlays.editOverlay(grabberCloner, { + visible: true, + rotation: rotation, + position: EdgeTR + }); + var boxPosition = Vec3.multiplyQbyV(rotation, center); boxPosition = Vec3.sum(position, boxPosition); Overlays.editOverlay(selectionBox, { @@ -2292,7 +2326,6 @@ SelectionDisplay = (function() { rotation: Quat.fromPitchYawRollDegrees(90, 0, 0), }); - }; that.setOverlaysVisible = function(isVisible) { @@ -2324,7 +2357,7 @@ SelectionDisplay = (function() { greatestDimension: 0.0, startingDistance: 0.0, startingElevation: 0.0, - onBegin: function(event) { + onBegin: function(event,isAltFromGrab) { SelectionManager.saveProperties(); startPosition = SelectionManager.worldPosition; var dimensions = SelectionManager.worldDimensions; @@ -2339,7 +2372,7 @@ SelectionDisplay = (function() { // Duplicate entities if alt is pressed. This will make a // copy of the selected entities and move the _original_ entities, not // the new ones. - if (event.isAlt) { + if (event.isAlt || isAltFromGrab) { duplicatedEntityIDs = []; for (var otherEntityID in SelectionManager.savedProperties) { var properties = SelectionManager.savedProperties[otherEntityID]; @@ -2580,6 +2613,34 @@ SelectionDisplay = (function() { }, }); + addGrabberTool(grabberCloner, { + mode: "CLONE", + onBegin: function(event) { + + var pickRay = generalComputePickRay(event.x, event.y); + var result = Overlays.findRayIntersection(pickRay); + translateXZTool.pickPlanePosition = result.intersection; + translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, SelectionManager.worldDimensions.y), + SelectionManager.worldDimensions.z); + + translateXZTool.onBegin(event,true); + }, + elevation: function (event) { + translateXZTool.elevation(event); + }, + + onEnd: function (event) { + translateXZTool.onEnd(event); + }, + + onMove: function (event) { + translateXZTool.onMove(event); + } + }); + + + + var vec3Mult = function(v1, v2) { return { x: v1.x * v2.x, @@ -2592,6 +2653,16 @@ SelectionDisplay = (function() { // pivot - point to use as a pivot // offset - the position of the overlay tool relative to the selections center position var makeStretchTool = function(stretchMode, direction, pivot, offset, customOnMove) { + // directionFor3DStretch - direction and pivot for 3D stretch + // distanceFor3DStretch - distance from the intersection point and the handController + // used to increase the scale taking into account the distance to the object + // DISTANCE_INFLUENCE_THRESHOLD - constant that holds the minimum distance where the + // distance to the object will influence the stretch/resize/scale + var directionFor3DStretch = getDirectionsFor3DStretch(stretchMode); + var distanceFor3DStretch = 0; + var DISTANCE_INFLUENCE_THRESHOLD = 1.2; + + var signs = { x: direction.x < 0 ? -1 : (direction.x > 0 ? 1 : 0), y: direction.y < 0 ? -1 : (direction.y > 0 ? 1 : 0), @@ -2603,18 +2674,23 @@ SelectionDisplay = (function() { y: Math.abs(direction.y) > 0 ? 1 : 0, z: Math.abs(direction.z) > 0 ? 1 : 0, }; + + var numDimensions = mask.x + mask.y + mask.z; var planeNormal = null; var lastPick = null; + var lastPick3D = null; var initialPosition = null; var initialDimensions = null; var initialIntersection = null; var initialProperties = null; var registrationPoint = null; var deltaPivot = null; + var deltaPivot3D = null; var pickRayPosition = null; + var pickRayPosition3D = null; var rotation = null; var onBegin = function(event) { @@ -2652,8 +2728,20 @@ SelectionDisplay = (function() { // Scaled offset in world coordinates var scaledOffsetWorld = vec3Mult(initialDimensions, offsetRP); + pickRayPosition = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld)); - + + if (directionFor3DStretch) { + // pivot, offset and pickPlanePosition for 3D manipulation + var scaledPivot3D = Vec3.multiply(0.5, Vec3.multiply(1.0, directionFor3DStretch)); + deltaPivot3D = Vec3.subtract(centeredRP, scaledPivot3D); + + var scaledOffsetWorld3D = vec3Mult(initialDimensions, + Vec3.subtract(Vec3.multiply(0.5, Vec3.multiply(-1.0, directionFor3DStretch)), + centeredRP)); + + pickRayPosition3D = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld)); + } var start = null; var end = null; if (numDimensions == 1 && mask.x) { @@ -2754,12 +2842,25 @@ SelectionDisplay = (function() { }; } } + planeNormal = Vec3.multiplyQbyV(rotation, planeNormal); var pickRay = generalComputePickRay(event.x, event.y); lastPick = rayPlaneIntersection(pickRay, pickRayPosition, planeNormal); - + + var planeNormal3D = { + x: 0, + y: 0, + z: 0 + }; + if (directionFor3DStretch) { + lastPick3D = rayPlaneIntersection(pickRay, + pickRayPosition3D, + planeNormal3D); + distanceFor3DStretch = Vec3.length(Vec3.subtract(pickRayPosition3D, pickRay.origin)); + } + SelectionManager.saveProperties(); }; @@ -2790,24 +2891,50 @@ SelectionDisplay = (function() { dimensions = SelectionManager.worldDimensions; rotation = SelectionManager.worldRotation; } + + var localDeltaPivot = deltaPivot; + var localSigns = signs; var pickRay = generalComputePickRay(event.x, event.y); - newPick = rayPlaneIntersection(pickRay, + + // Are we using handControllers or Mouse - only relevant for 3D tools + var controllerPose = getControllerWorldLocation(activeHand, true); + if (HMD.isHMDAvailable() + && HMD.isHandControllerAvailable() && controllerPose.valid && that.triggered && directionFor3DStretch) { + localDeltaPivot = deltaPivot3D; + + newPick = pickRay.origin; + + var vector = Vec3.subtract(newPick, lastPick3D); + + vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector); + + if (distanceFor3DStretch > DISTANCE_INFLUENCE_THRESHOLD) { + // Range of Motion + vector = Vec3.multiply(distanceFor3DStretch , vector); + } + + localSigns = directionFor3DStretch; + + } else { + newPick = rayPlaneIntersection(pickRay, pickRayPosition, planeNormal); - var vector = Vec3.subtract(newPick, lastPick); + var vector = Vec3.subtract(newPick, lastPick); - vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector); - - vector = vec3Mult(mask, vector); + vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector); + vector = vec3Mult(mask, vector); + + } + if (customOnMove) { - var change = Vec3.multiply(-1, vec3Mult(signs, vector)); + var change = Vec3.multiply(-1, vec3Mult(localSigns, vector)); customOnMove(vector, change); } else { vector = grid.snapToSpacing(vector); - var changeInDimensions = Vec3.multiply(-1, vec3Mult(signs, vector)); + var changeInDimensions = Vec3.multiply(-1, vec3Mult(localSigns, vector)); var newDimensions; if (proportional) { var absX = Math.abs(changeInDimensions.x); @@ -2829,37 +2956,39 @@ SelectionDisplay = (function() { } else { newDimensions = Vec3.sum(initialDimensions, changeInDimensions); } - - newDimensions.x = Math.max(newDimensions.x, MINIMUM_DIMENSION); - newDimensions.y = Math.max(newDimensions.y, MINIMUM_DIMENSION); - newDimensions.z = Math.max(newDimensions.z, MINIMUM_DIMENSION); - - var changeInPosition = Vec3.multiplyQbyV(rotation, vec3Mult(deltaPivot, changeInDimensions)); - var newPosition = Vec3.sum(initialPosition, changeInPosition); - - for (var i = 0; i < SelectionManager.selections.length; i++) { - Entities.editEntity(SelectionManager.selections[i], { - position: newPosition, - dimensions: newDimensions, - }); - } - - var wantDebug = false; - if (wantDebug) { - print(stretchMode); - //Vec3.print(" newIntersection:", newIntersection); - Vec3.print(" vector:", vector); - //Vec3.print(" oldPOS:", oldPOS); - //Vec3.print(" newPOS:", newPOS); - Vec3.print(" changeInDimensions:", changeInDimensions); - Vec3.print(" newDimensions:", newDimensions); - - Vec3.print(" changeInPosition:", changeInPosition); - Vec3.print(" newPosition:", newPosition); - } - - SelectionManager._update(); } + + + newDimensions.x = Math.max(newDimensions.x, MINIMUM_DIMENSION); + newDimensions.y = Math.max(newDimensions.y, MINIMUM_DIMENSION); + newDimensions.z = Math.max(newDimensions.z, MINIMUM_DIMENSION); + + var changeInPosition = Vec3.multiplyQbyV(rotation, vec3Mult(localDeltaPivot, changeInDimensions)); + var newPosition = Vec3.sum(initialPosition, changeInPosition); + + for (var i = 0; i < SelectionManager.selections.length; i++) { + Entities.editEntity(SelectionManager.selections[i], { + position: newPosition, + dimensions: newDimensions, + }); + } + + + var wantDebug = false; + if (wantDebug) { + print(stretchMode); + //Vec3.print(" newIntersection:", newIntersection); + Vec3.print(" vector:", vector); + //Vec3.print(" oldPOS:", oldPOS); + //Vec3.print(" newPOS:", newPOS); + Vec3.print(" changeInDimensions:", changeInDimensions); + Vec3.print(" newDimensions:", newDimensions); + + Vec3.print(" changeInPosition:", changeInPosition); + Vec3.print(" newPosition:", newPosition); + } + + SelectionManager._update(); }; @@ -2870,6 +2999,75 @@ SelectionDisplay = (function() { onEnd: onEnd }; }; + + // Direction for the stretch tool when using hand controller + var directionsFor3DGrab = { + LBN: { + x: 1, + y: 1, + z: 1 + }, + RBN: { + x: -1, + y: 1, + z: 1 + }, + LBF: { + x: 1, + y: 1, + z: -1 + }, + RBF: { + x: -1, + y: 1, + z: -1 + }, + LTN: { + x: 1, + y: -1, + z: 1 + }, + RTN: { + x: -1, + y: -1, + z: 1 + }, + LTF: { + x: 1, + y: -1, + z: -1 + }, + RTF: { + x: -1, + y: -1, + z: -1 + } + }; + + // Returns a vector with directions for the stretch tool in 3D using hand controllers + function getDirectionsFor3DStretch(mode) { + if (mode === "STRETCH_LBN") { + return directionsFor3DGrab.LBN; + } else if (mode === "STRETCH_RBN") { + return directionsFor3DGrab.RBN; + } else if (mode === "STRETCH_LBF") { + return directionsFor3DGrab.LBF; + } else if (mode === "STRETCH_RBF") { + return directionsFor3DGrab.RBF; + } else if (mode === "STRETCH_LTN") { + return directionsFor3DGrab.LTN; + } else if (mode === "STRETCH_RTN") { + return directionsFor3DGrab.RTN; + } else if (mode === "STRETCH_LTF") { + return directionsFor3DGrab.LTF; + } else if (mode === "STRETCH_RTF") { + return directionsFor3DGrab.RTF; + } else { + return null; + } + } + + function addStretchTool(overlay, mode, pivot, direction, offset, handleMove) { if (!pivot) { @@ -4344,6 +4542,12 @@ SelectionDisplay = (function() { highlightNeeded = true; break; + case grabberCloner: + pickedColor = grabberColorCloner; + pickedAlpha = grabberAlpha; + highlightNeeded = true; + break; + default: if (previousHandle) { Overlays.editOverlay(previousHandle, { diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 78be54f774..37a334bd70 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -122,7 +122,8 @@ function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]"; - print.apply(null, [].concat.apply([LABEL, stateString, JSON.stringify(waitingList), connecting], + var current = "[" + currentHand + "/" + currentHandJointIndex + "]" + print.apply(null, [].concat.apply([LABEL, stateString, current, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); } @@ -197,7 +198,7 @@ } var animationData = {}; - function updateAnimationData() { + function updateAnimationData(verticalOffset) { // all we are doing here is moving the right hand to a spot // that is in front of and a bit above the hips. Basing how // far in front as scaling with the avatar's height (say hips @@ -208,6 +209,9 @@ offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; } animationData.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); + if (verticalOffset) { + animationData.rightHandPosition.y += verticalOffset; + } animationData.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); } function shakeHandsAnimation() { @@ -346,7 +350,32 @@ } return false; } - + function findNearestAvatar() { + // We only look some max distance away (much larger than the handshake distance, but still...) + var minDistance = MAX_AVATAR_DISTANCE * 20; + var closestAvatar; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + var avatar = AvatarList.getAvatar(id); + if (avatar && avatar.sessionUUID != MyAvatar.sessionUUID) { + var currentDistance = Vec3.distance(avatar.position, MyAvatar.position); + if (minDistance > currentDistance) { + minDistance = currentDistance; + closestAvatar = avatar; + } + } + }); + return closestAvatar; + } + function adjustAnimationHeight() { + var avatar = findNearestAvatar(); + if (avatar) { + var myHeadIndex = MyAvatar.getJointIndex("Head"); + var otherHeadIndex = avatar.getJointIndex("Head"); + var diff = (avatar.getJointPosition(otherHeadIndex).y - MyAvatar.getJointPosition(myHeadIndex).y) / 2; + print("head height difference: " + diff); + updateAnimationData(diff); + } + } function findNearestWaitingAvatar() { var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); var minDistance = MAX_AVATAR_DISTANCE; @@ -365,6 +394,8 @@ return nearestAvatar; } function messageSend(message) { + // we always append whether or not we are logged in... + message.isLoggedIn = Account.isLoggedIn(); Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); } function handStringMessageSend(message) { @@ -433,6 +464,10 @@ handStringMessageSend({ key: "waiting", }); + // potentially adjust height of handshake + if (fromKeyboard) { + adjustAnimationHeight(); + } lookForWaitingAvatar(); } } @@ -462,7 +497,9 @@ endHandshakeAnimation(); // No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us // in a weird state. - request({ uri: requestUrl, method: 'DELETE' }, debug); + if (Account.isLoggedIn()) { + request({ uri: requestUrl, method: 'DELETE' }, debug); + } } function updateTriggers(value, fromKeyboard, hand) { @@ -589,7 +626,7 @@ } } - function makeConnection(id) { + function makeConnection(id, isLoggedIn) { // send done to let the connection know you have made connection. messageSend({ key: "done", @@ -605,7 +642,10 @@ // It would be "simpler" to skip this and just look at the response, but: // 1. We don't want to bother the metaverse with request that we know will fail. // 2. We don't want our code here to be dependent on precisely how the metaverse responds (400, 401, etc.) - if (!Account.isLoggedIn()) { + // 3. We also don't want to connect to someone who is anonymous _now_, but was not earlier and still has + // the same node id. Since the messaging doesn't say _who_ isn't logged in, fail the same as if we are + // not logged in. + if (!Account.isLoggedIn() || isLoggedIn === false) { handleConnectionResponseAndMaybeRepeat("401:Unauthorized", {statusCode: 401}); return; } @@ -627,8 +667,12 @@ // we change states, start the connectionInterval where we check // to be sure the hand is still close enough. If not, we terminate // the interval, go back to the waiting state. If we make it - // the entire CONNECTING_TIME, we make the connection. - function startConnecting(id, jointIndex) { + // the entire CONNECTING_TIME, we make the connection. We pass in + // whether or not the connecting id is actually logged in, as now we + // will allow to start the connection process but have it stop with a + // fail message before trying to call the backend if the other guy isn't + // logged in. + function startConnecting(id, jointIndex, isLoggedIn) { var count = 0; debug("connecting", id, "hand", jointIndex); // do we need to do this? @@ -670,7 +714,7 @@ startHandshake(); } else if (count > CONNECTING_TIME / CONNECTING_INTERVAL) { debug("made connection with " + id); - makeConnection(id); + makeConnection(id, isLoggedIn); stopConnecting(); } }, CONNECTING_INTERVAL); @@ -735,7 +779,7 @@ if (state === STATES.WAITING && (!connectingId || connectingId === senderID)) { if (message.id === MyAvatar.sessionUUID) { stopWaiting(); - startConnecting(senderID, exisitingOrSearchedJointIndex()); + startConnecting(senderID, exisitingOrSearchedJointIndex(), message.isLoggedIn); } else if (connectingId) { // this is for someone else (we lost race in connectionRequest), // so lets start over @@ -754,12 +798,15 @@ startHandshake(); break; } - startConnecting(senderID, connectingHandJointIndex); + startConnecting(senderID, connectingHandJointIndex, message.isLoggedIn); } break; case "done": delete waitingList[senderID]; - if (state === STATES.CONNECTING && connectingId === senderID) { + if (connectingId !== senderID) { + break; + } + if (state === STATES.CONNECTING) { // if they are done, and didn't connect us, terminate our // connecting if (message.connectionId !== MyAvatar.sessionUUID) { @@ -768,11 +815,20 @@ // value for isKeyboard, as we should not change the animation // state anyways (if any) startHandshake(); + } else { + // they just created a connection request to us, and we are connecting to + // them, so lets just stop connecting and make connection.. + makeConnection(connectingId, message.isLoggedIn); + stopConnecting(); } } else { - // if waiting or inactive, lets clear the connecting id. If in makingConnection, - // do nothing - if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) { + if (state == STATES.MAKING_CONNECTION) { + // we are making connection, they just started, so lets reset the + // poll count just in case + pollCount = 0; + } else { + // if waiting or inactive, lets clear the connecting id. If in makingConnection, + // do nothing clearConnecting(); if (state !== STATES.INACTIVE) { startHandshake(); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 9229ec772a..0500c13f9b 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -268,7 +268,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See break; case 'refreshConnections': print('Refreshing Connections...'); - getConnectionData(); + getConnectionData(false); UserActivityLogger.palAction("refresh_connections", ""); break; case 'removeConnection': @@ -281,25 +281,27 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See print("Error: unable to remove connection", connectionUserName, error || response.status); return; } - getConnectionData(); + getConnectionData(false); }); break case 'removeFriend': friendUserName = message.params; + print("Removing " + friendUserName + " from friends."); request({ uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName, method: 'DELETE' }, function (error, response) { if (error || (response.status !== 'success')) { - print("Error: unable to unfriend", friendUserName, error || response.status); + print("Error: unable to unfriend " + friendUserName, error || response.status); return; } - getConnectionData(); + getConnectionData(friendUserName); }); break case 'addFriend': friendUserName = message.params; + print("Adding " + friendUserName + " to friends."); request({ uri: METAVERSE_BASE + '/api/v1/user/friends', method: 'POST', @@ -312,7 +314,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See print("Error: unable to friend " + friendUserName, error || response.status); return; } - getConnectionData(); // For now, just refresh all connection data. Later, just refresh the one friended row. + getConnectionData(friendUserName); } ); break; @@ -360,8 +362,6 @@ function getProfilePicture(username, callback) { // callback(url) if successfull }); } function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) - // The back end doesn't do user connections yet. Fake it by getting all users that have made themselves accessible to us, - // and pretending that they are all connections. url = METAVERSE_BASE + '/api/v1/users?' if (domain) { url += 'status=' + domain.slice(1, -1); // without curly braces @@ -369,25 +369,22 @@ function getAvailableConnections(domain, callback) { // callback([{usename, loca url += 'filter=connections'; // regardless of whether online } requestJSON(url, function (connectionsData) { - // The back end doesn't include the profile picture data, but we can add that here. - // For our current purposes, there's no need to be fancy and try to reduce latency by doing some number of requests in parallel, - // so these requests are all sequential. - var users = connectionsData.users; - function addPicture(index) { - if (index >= users.length) { - return callback(users); - } - var user = users[index]; - getProfilePicture(user.username, function (url) { - user.profileUrl = url; - addPicture(index + 1); - }); - } - addPicture(0); + callback(connectionsData.users); }); } - -function getConnectionData(domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. +function getInfoAboutUser(specificUsername, callback) { + url = METAVERSE_BASE + '/api/v1/users?filter=connections' + requestJSON(url, function (connectionsData) { + for (user in connectionsData.users) { + if (connectionsData.users[user].username === specificUsername) { + callback(connectionsData.users[user]); + return; + } + } + callback(false); + }); +} +function getConnectionData(specificUsername, domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. function frob(user) { // get into the right format var formattedSessionId = user.location.node_id || ''; if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) { @@ -397,19 +394,29 @@ function getConnectionData(domain) { // Update all the usernames that I am entit sessionId: formattedSessionId, userName: user.username, connection: user.connection, - profileUrl: user.profileUrl, + profileUrl: user.images.thumbnail, placeName: (user.location.root || user.location.domain || {}).name || '' }; } - getAvailableConnections(domain, function (users) { - if (domain) { - users.forEach(function (user) { + if (specificUsername) { + getInfoAboutUser(specificUsername, function (user) { + if (user) { updateUser(frob(user)); - }); - } else { - sendToQml({ method: 'connections', params: users.map(frob) }); - } - }); + } else { + print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!'); + } + }); + } else { + getAvailableConnections(domain, function (users) { + if (domain) { + users.forEach(function (user) { + updateUser(frob(user)); + }); + } else { + sendToQml({ method: 'connections', params: users.map(frob) }); + } + }); + } } // @@ -486,7 +493,7 @@ function populateNearbyUserList(selectData, oldAudioData) { data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); }); - getConnectionData(location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). + getConnectionData(false, location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). conserveResources = Object.keys(avatarsOfInterest).length > 20; sendToQml({ method: 'nearbyUsers', params: data }); if (selectData) { diff --git a/scripts/system/playRecordingAC.js b/scripts/system/playRecordingAC.js index b4fae9a2e3..98e5220c31 100644 --- a/scripts/system/playRecordingAC.js +++ b/scripts/system/playRecordingAC.js @@ -14,6 +14,7 @@ var APP_NAME = "PLAYBACK", HIFI_RECORDER_CHANNEL = "HiFi-Recorder-Channel", + RECORDER_COMMAND_ERROR = "error", HIFI_PLAYER_CHANNEL = "HiFi-Player-Channel", PLAYER_COMMAND_PLAY = "play", PLAYER_COMMAND_STOP = "stop", @@ -47,9 +48,17 @@ searchState = SEARCH_IDLE, otherPlayersPlaying, otherPlayersPlayingCounts, - pauseCount; + pauseCount, + isDestroyLater = false, + + destroy; function onUpdateTimestamp() { + if (isDestroyLater) { + destroy(); + return; + } + userData.timestamp = Date.now(); Entities.editEntity(entityID, { userData: JSON.stringify(userData) }); EntityViewer.queryOctree(); // Keep up to date ready for find(). @@ -69,12 +78,14 @@ if (sender !== scriptUUID) { message = JSON.parse(message); - index = otherPlayersPlaying.indexOf(message.entity); - if (index !== -1) { - otherPlayersPlayingCounts[index] += 1; - } else { - otherPlayersPlaying.push(message.entity); - otherPlayersPlayingCounts.push(1); + if (message.playing !== undefined) { + index = otherPlayersPlaying.indexOf(message.entity); + if (index !== -1) { + otherPlayersPlayingCounts[index] += 1; + } else { + otherPlayersPlaying.push(message.entity); + otherPlayersPlayingCounts.push(1); + } } } } @@ -83,10 +94,11 @@ // Create a new persistence entity (even if already have one but that should never occur). var properties; - log("Create recording " + filename); + log("Create recording entity for " + filename); - if (updateTimestampTimer !== null) { - Script.clearInterval(updateTimestampTimer); // Just in case. + if (updateTimestampTimer !== null) { // Just in case. + Script.clearInterval(updateTimestampTimer); + updateTimestampTimer = null; } searchState = SEARCH_IDLE; @@ -114,6 +126,7 @@ return true; } + log("Could not create recording entity for " + filename); return false; } @@ -224,7 +237,7 @@ return result; } - function destroy() { + destroy = function () { // Delete current persistence entity. if (entityID !== null) { // Just in case. Entities.deleteEntity(entityID); @@ -233,7 +246,13 @@ } if (updateTimestampTimer !== null) { // Just in case. Script.clearInterval(updateTimestampTimer); + updateTimestampTimer = null; } + }; + + function destroyLater() { + // Schedules a call to destroy() when timer threading suits. + isDestroyLater = true; } function setUp() { @@ -254,6 +273,7 @@ create: create, find: find, destroy: destroy, + destroyLater: destroyLater, setUp: setUp, tearDown: tearDown }; @@ -261,41 +281,78 @@ Player = (function () { // Recording playback functions. - var isPlayingRecording = false, + var userID = null, + isPlayingRecording = false, recordingFilename = "", autoPlayTimer = null, + autoPlay, playRecording; - function play(recording, position, orientation) { + function error(message) { + // Send error message to user. + Messages.sendMessage(HIFI_RECORDER_CHANNEL, JSON.stringify({ + command: RECORDER_COMMAND_ERROR, + user: userID, + message: message + })); + } + + function play(user, recording, position, orientation) { + var errorMessage; + + if (autoPlayTimer) { // Cancel auto-play. + // FIXME: Once in a while Script.clearTimeout() fails. + // [DEBUG] [hifi.scriptengine] [3748] [agent] stopTimer -- not in _timerFunctionMap QObject(0x0) + Script.clearTimeout(autoPlayTimer); + autoPlayTimer = null; + } + + userID = user; + if (Entity.create(recording, position, orientation)) { - log("Play new recording " + recordingFilename); - isPlayingRecording = true; + log("Play recording " + recording); + isPlayingRecording = true; // Immediate feedback. recordingFilename = recording; - playRecording(recordingFilename, position, orientation); + playRecording(recordingFilename, position, orientation, true); } else { - log("Could not create entity to play new recording " + recordingFilename); + errorMessage = "Could not persist recording " + recording.slice(4); // Remove leading "atp:". + log(errorMessage); + error(errorMessage); + + autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_ERROR_INTERVAL); // Resume auto-play later. } } - function autoPlay() { + autoPlay = function () { var recording, AUTOPLAY_SEARCH_DELTA = 1000; // Random delay to help reduce collisions between AC scripts. Script.setTimeout(function () { + // Guard against Script.clearTimeout() in play() not always working. + if (isPlayingRecording) { + return; + } + recording = Entity.find(); if (recording) { - log("Play persisted recording " + recordingFilename); - playRecording(recording.recording, recording.position, recording.orientation); + log("Play persisted recording " + recording.recording); + userID = null; + autoPlayTimer = null; + isPlayingRecording = true; // Immediate feedback. + recordingFilename = recording.recording; + playRecording(recording.recording, recording.position, recording.orientation, false); } else { autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_SEARCH_INTERVAL); // Try again soon. } }, Math.random() * AUTOPLAY_SEARCH_DELTA); - } + }; - playRecording = function (recording, position, orientation) { + playRecording = function (recording, position, orientation, isManual) { Recording.loadRecording(recording, function (success) { + var errorMessage; + if (success) { Users.disableIgnoreRadius(); @@ -310,15 +367,22 @@ Recording.setPlayerLoop(true); Recording.setPlayerUseSkeletonModel(true); - isPlayingRecording = true; - recordingFilename = recording; - Recording.setPlayerTime(0.0); Recording.startPlaying(); UserActivityLogger.logAction("playRecordingAC_play_recording"); } else { - log("Failed to load recording " + recording); + if (isManual) { + // Delete persistence entity if manual play request. + Entity.destroyLater(); // Schedule for deletion; works around timer threading issues. + } + + errorMessage = "Could not load recording " + recording.slice(4); // Remove leading "atp:". + log(errorMessage); + error(errorMessage); + + isPlayingRecording = false; + recordingFilename = ""; autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_ERROR_INTERVAL); // Try again later. } }); @@ -374,7 +438,15 @@ recording: Player.recording(), entity: Entity.id() })); - heartbeatTimer = Script.setTimeout(sendHeartbeat, HEARTBEAT_INTERVAL); + } + + function onHeartbeatTimer() { + sendHeartbeat(); + heartbeatTimer = Script.setTimeout(onHeartbeatTimer, HEARTBEAT_INTERVAL); + } + + function startHeartbeat() { + onHeartbeatTimer(); } function stopHeartbeat() { @@ -394,7 +466,7 @@ switch (message.command) { case PLAYER_COMMAND_PLAY: if (!Player.isPlaying()) { - Player.play(message.recording, message.position, message.orientation); + Player.play(sender, message.recording, message.position, message.orientation); } else { log("Didn't start playing " + message.recording + " because already playing " + Player.recording()); } @@ -418,7 +490,7 @@ Messages.subscribe(HIFI_PLAYER_CHANNEL); Player.autoPlay(); - sendHeartbeat(); + startHeartbeat(); UserActivityLogger.logAction("playRecordingAC_script_load"); } diff --git a/scripts/system/record.js b/scripts/system/record.js index 3db82696ef..5439d68c9a 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -37,6 +37,12 @@ Window.alert(message); } + function logDetails() { + return { + current_domain: location.placename + }; + } + RecordingIndicator = (function () { // Displays "recording" overlay. @@ -181,7 +187,7 @@ recordingState = IDLE; log("Finish recording"); - UserActivityLogger.logAction("record_finish_recording"); + UserActivityLogger.logAction("record_finish_recording", logDetails()); playSound(finishRecordingSound); Recording.stopRecording(); RecordingIndicator.hide(); @@ -269,6 +275,7 @@ Player = (function () { var HIFI_RECORDER_CHANNEL = "HiFi-Recorder-Channel", + RECORDER_COMMAND_ERROR = "error", HIFI_PLAYER_CHANNEL = "HiFi-Player-Channel", PLAYER_COMMAND_PLAY = "play", PLAYER_COMMAND_STOP = "stop", @@ -277,7 +284,6 @@ playerIsPlayings = [], // True if AC player script is playing a recording. playerRecordings = [], // Assignment client mappings of recordings being played. playerTimestamps = [], // Timestamps of last heartbeat update from player script. - playerStartupTimeouts = [], // Timers that check that recording has started playing. updateTimer, UPDATE_INTERVAL = 5000; // Must be > player's HEARTBEAT_INTERVAL. @@ -298,7 +304,6 @@ playerIsPlayings.splice(i, 1); playerRecordings.splice(i, 1); playerTimestamps.splice(i, 1); - playerStartupTimeouts.splice(i, 1); } } @@ -309,8 +314,7 @@ } function playRecording(recording, position, orientation) { - var index, - CHECK_PLAYING_TIMEOUT = 10000; + var index; // Optional function parameters. if (position === undefined) { @@ -334,26 +338,9 @@ position: position, orientation: orientation })); - - playerStartupTimeouts[index] = Script.setTimeout(function () { - if ((!playerIsPlayings[index] || playerRecordings[index] !== recording) && playerStartupTimeouts[index]) { - error("Didn't start playing recording " - + recording.slice(4) + "!"); // Remove leading "atp:" from recording. - } - playerStartupTimeouts[index] = null; - }, CHECK_PLAYING_TIMEOUT); } function stopPlayingRecording(playerID) { - var index; - - // Cancel check that recording started playing. - index = playerIDs.indexOf(playerID); - if (index !== -1 && playerStartupTimeouts[index] !== null) { - // Cannot clearTimeout() without program log error, so just set null. - playerStartupTimeouts[index] = null; - } - Messages.sendMessage(HIFI_PLAYER_CHANNEL, JSON.stringify({ player: playerID, command: PLAYER_COMMAND_STOP @@ -370,15 +357,21 @@ message = JSON.parse(message); - index = playerIDs.indexOf(sender); - if (index === -1) { - index = playerIDs.length; - playerIDs[index] = sender; + if (message.command === RECORDER_COMMAND_ERROR) { + if (message.user === MyAvatar.sessionUUID) { + error(message.message); + } + } else { + index = playerIDs.indexOf(sender); + if (index === -1) { + index = playerIDs.length; + playerIDs[index] = sender; + } + playerIsPlayings[index] = message.playing; + playerRecordings[index] = message.recording; + playerTimestamps[index] = Date.now(); + Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs); } - playerIsPlayings[index] = message.playing; - playerRecordings[index] = message.recording; - playerTimestamps[index] = Date.now(); - Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs); } function reset() { @@ -386,7 +379,6 @@ playerIsPlayings = []; playerRecordings = []; playerTimestamps = []; - playerStartupTimeouts = []; Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs); } @@ -519,10 +511,11 @@ value: Player.numberOfPlayers() })); updateRecordingStatus(!Recorder.isIdle()); - UserActivityLogger.logAction("record_open_dialog"); + UserActivityLogger.logAction("record_open_dialog", logDetails()); break; case STOP_PLAYING_RECORDING_ACTION: // Stop the specified player. + log("Unload recording " + message.value); Player.stopPlayingRecording(message.value); break; case LOAD_RECORDING_ACTION: @@ -530,7 +523,7 @@ recording = Window.browseAssets("Select Recording to Play", "recordings", "*.hfr"); if (recording) { log("Load recording " + recording); - UserActivityLogger.logAction("record_load_recording"); + UserActivityLogger.logAction("record_load_recording", logDetails()); Player.playRecording("atp:" + recording, MyAvatar.position, MyAvatar.orientation); } break; @@ -660,7 +653,7 @@ isConnected = Window.location.isConnected; Script.update.connect(onUpdate); - UserActivityLogger.logAction("record_run_script"); + UserActivityLogger.logAction("record_run_script", logDetails()); } function tearDown() { diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index 2dd426932f..2d40795692 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -17,32 +17,22 @@ const INPUT = "Input"; const OUTPUT = "Output"; -function parseMenuItem(item) { - const USE = "Use "; - const FOR_INPUT = " for " + INPUT; - const FOR_OUTPUT = " for " + OUTPUT; - if (item.slice(0, USE.length) == USE) { - if (item.slice(-FOR_INPUT.length) == FOR_INPUT) { - return { device: item.slice(USE.length, -FOR_INPUT.length), mode: INPUT }; - } else if (item.slice(-FOR_OUTPUT.length) == FOR_OUTPUT) { - return { device: item.slice(USE.length, -FOR_OUTPUT.length), mode: OUTPUT }; - } - } -} - +const SELECT_AUDIO_SCRIPT_STARTUP_TIMEOUT = 300; // // VAR DEFINITIONS // var debugPrintStatements = true; const INPUT_DEVICE_SETTING = "audio_input_device"; const OUTPUT_DEVICE_SETTING = "audio_output_device"; -var audioDevicesList = []; +var audioDevicesList = []; // placeholder for menu items var wasHmdActive = false; // assume it's not active to start var switchedAudioInputToHMD = false; var switchedAudioOutputToHMD = false; var previousSelectedInputAudioDevice = ""; var previousSelectedOutputAudioDevice = ""; -var skipMenuEvents = true; + +var interfaceInputDevice = ""; +var interfaceOutputDevice = ""; // // BEGIN FUNCTION DEFINITIONS @@ -56,56 +46,37 @@ function debug() { function setupAudioMenus() { // menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches - skipMenuEvents = true; - Script.setTimeout(function() { skipMenuEvents = false; }, 200); - removeAudioMenus(); // Setup audio input devices Menu.addSeparator("Audio", "Input Audio Device"); - var inputDevices = AudioDevice.getInputDevices(); - for (var i = 0; i < inputDevices.length; i++) { - var audioDeviceMenuString = "Use " + inputDevices[i] + " for Input"; + var currentInputDevice = AudioDevice.getInputDevice() + for (var i = 0; i < AudioDevice.inputAudioDevices.length; i++) { + var audioDeviceMenuString = "Use " + AudioDevice.inputAudioDevices[i] + " for Input"; Menu.addMenuItem({ menuName: "Audio", menuItemName: audioDeviceMenuString, isCheckable: true, - isChecked: inputDevices[i] == AudioDevice.getInputDevice() + isChecked: AudioDevice.inputAudioDevices[i] == currentInputDevice }); audioDevicesList.push(audioDeviceMenuString); } // Setup audio output devices Menu.addSeparator("Audio", "Output Audio Device"); - var outputDevices = AudioDevice.getOutputDevices(); - for (var i = 0; i < outputDevices.length; i++) { - var audioDeviceMenuString = "Use " + outputDevices[i] + " for Output"; + var currentOutputDevice = AudioDevice.getOutputDevice() + for (var i = 0; i < AudioDevice.outputAudioDevices.length; i++) { + var audioDeviceMenuString = "Use " + AudioDevice.outputAudioDevices[i] + " for Output"; Menu.addMenuItem({ menuName: "Audio", menuItemName: audioDeviceMenuString, isCheckable: true, - isChecked: outputDevices[i] == AudioDevice.getOutputDevice() + isChecked: AudioDevice.outputAudioDevices[i] == currentOutputDevice }); audioDevicesList.push(audioDeviceMenuString); } } -function checkDeviceMismatch() { - var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); - var interfaceInputDevice = AudioDevice.getInputDevice(); - if (interfaceInputDevice != inputDeviceSetting) { - debug("Input Setting & Device mismatch! Input SETTING: " + inputDeviceSetting + "Input DEVICE IN USE: " + interfaceInputDevice); - switchAudioDevice("Use " + inputDeviceSetting + " for Input"); - } - - var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); - var interfaceOutputDevice = AudioDevice.getOutputDevice(); - if (interfaceOutputDevice != outputDeviceSetting) { - debug("Output Setting & Device mismatch! Output SETTING: " + outputDeviceSetting + "Output DEVICE IN USE: " + interfaceOutputDevice); - switchAudioDevice("Use " + outputDeviceSetting + " for Output"); - } -} - function removeAudioMenus() { Menu.removeSeparator("Audio", "Input Audio Device"); Menu.removeSeparator("Audio", "Output Audio Device"); @@ -124,67 +95,28 @@ function removeAudioMenus() { function onDevicechanged() { debug("System audio devices changed. Removing and replacing Audio Menus..."); setupAudioMenus(); - checkDeviceMismatch(); } function onMenuEvent(audioDeviceMenuString) { - if (!skipMenuEvents) { - switchAudioDevice(audioDeviceMenuString); + if (Menu.isOptionChecked(audioDeviceMenuString) && + (audioDeviceMenuString !== interfaceInputDevice && + audioDeviceMenuString !== interfaceOutputDevice)) { + AudioDevice.setDeviceFromMenu(audioDeviceMenuString) } } -function switchAudioDevice(audioDeviceMenuString) { - // if the device is not plugged in, short-circuit - if (!~audioDevicesList.indexOf(audioDeviceMenuString)) { - return; - } - - var selection = parseMenuItem(audioDeviceMenuString); - if (!selection) { - debug("Invalid Audio audioDeviceMenuString! Doesn't end with 'for Input' or 'for Output'"); - return; - } - - // menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches - skipMenuEvents = true; - Script.setTimeout(function() { skipMenuEvents = false; }, 200); - - var selectedDevice = selection.device; - if (selection.mode == INPUT) { - var currentInputDevice = AudioDevice.getInputDevice(); - if (selectedDevice != currentInputDevice) { - debug("Switching audio INPUT device from " + currentInputDevice + " to " + selectedDevice); - Menu.setIsOptionChecked("Use " + currentInputDevice + " for Input", false); - if (AudioDevice.setInputDevice(selectedDevice)) { - Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); - Menu.setIsOptionChecked(audioDeviceMenuString, true); - } else { - debug("Error setting audio input device!") - Menu.setIsOptionChecked(audioDeviceMenuString, false); - } +function onCurrentDeviceChanged() { + debug("System audio device switched. "); + interfaceInputDevice = "Use " + AudioDevice.getInputDevice() + " for Input"; + interfaceOutputDevice = "Use " + AudioDevice.getOutputDevice() + " for Output"; + for (var index = 0; index < audioDevicesList.length; index++) { + if (audioDevicesList[index] === interfaceInputDevice || + audioDevicesList[index] === interfaceOutputDevice) { + if (Menu.isOptionChecked(audioDevicesList[index]) === false) + Menu.setIsOptionChecked(audioDevicesList[index], true); } else { - debug("Selected input device is the same as the current input device!") - Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); - Menu.setIsOptionChecked(audioDeviceMenuString, true); - AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) - } - } else if (selection.mode == OUTPUT) { - var currentOutputDevice = AudioDevice.getOutputDevice(); - if (selectedDevice != currentOutputDevice) { - debug("Switching audio OUTPUT device from " + currentOutputDevice + " to " + selectedDevice); - Menu.setIsOptionChecked("Use " + currentOutputDevice + " for Output", false); - if (AudioDevice.setOutputDevice(selectedDevice)) { - Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); - Menu.setIsOptionChecked(audioDeviceMenuString, true); - } else { - debug("Error setting audio output device!") - Menu.setIsOptionChecked(audioDeviceMenuString, false); - } - } else { - debug("Selected output device is the same as the current output device!") - Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); - Menu.setIsOptionChecked(audioDeviceMenuString, true); - AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + if (Menu.isOptionChecked(audioDevicesList[index]) === true) + Menu.setIsOptionChecked(audioDevicesList[index], false); } } } @@ -192,12 +124,12 @@ function switchAudioDevice(audioDeviceMenuString) { function restoreAudio() { if (switchedAudioInputToHMD) { debug("Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); - switchAudioDevice("Use " + previousSelectedInputAudioDevice + " for Input"); + AudioDevice.setInputDeviceAsync(previousSelectedInputAudioDevice) switchedAudioInputToHMD = false; } if (switchedAudioOutputToHMD) { debug("Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); - switchAudioDevice("Use " + previousSelectedOutputAudioDevice + " for Output"); + AudioDevice.setOutputDeviceAsync(previousSelectedOutputAudioDevice) switchedAudioOutputToHMD = false; } } @@ -224,7 +156,7 @@ function checkHMDAudio() { debug("previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); if (hmdPreferredAudioInput != previousSelectedInputAudioDevice) { switchedAudioInputToHMD = true; - switchAudioDevice("Use " + hmdPreferredAudioInput + " for Input"); + AudioDevice.setInputDeviceAsync(hmdPreferredAudioInput) } } if (hmdPreferredAudioOutput !== "") { @@ -233,7 +165,7 @@ function checkHMDAudio() { debug("previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice) { switchedAudioOutputToHMD = true; - switchAudioDevice("Use " + hmdPreferredAudioOutput + " for Output"); + AudioDevice.setOutputDeviceAsync(hmdPreferredAudioOutput) } } } else { @@ -255,14 +187,15 @@ function checkHMDAudio() { Script.setTimeout(function () { debug("Connecting deviceChanged(), displayModeChanged(), and switchAudioDevice()..."); AudioDevice.deviceChanged.connect(onDevicechanged); + AudioDevice.currentInputDeviceChanged.connect(onCurrentDeviceChanged); + AudioDevice.currentOutputDeviceChanged.connect(onCurrentDeviceChanged); HMD.displayModeChanged.connect(checkHMDAudio); Menu.menuItemEvent.connect(onMenuEvent); debug("Setting up Audio I/O menu for the first time..."); setupAudioMenus(); - checkDeviceMismatch(); debug("Checking HMD audio status...") checkHMDAudio(); -}, 3000); +}, SELECT_AUDIO_SCRIPT_STARTUP_TIMEOUT); debug("Connecting scriptEnding()"); Script.scriptEnding.connect(function () { @@ -270,6 +203,8 @@ Script.scriptEnding.connect(function () { removeAudioMenus(); Menu.menuItemEvent.disconnect(onMenuEvent); HMD.displayModeChanged.disconnect(checkHMDAudio); + AudioDevice.currentInputDeviceChanged.disconnect(onCurrentDeviceChanged); + AudioDevice.currentOutputDeviceChanged.disconnect(onCurrentDeviceChanged); AudioDevice.deviceChanged.disconnect(onDevicechanged); }); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 77278caadd..8b5ae3c9a7 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -51,6 +51,11 @@ function openLoginWindow() { } } +function removeFromStoryIDsToMaybeDelete(story_id) { + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(story_id), 1); + print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); +} + function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) @@ -111,7 +116,7 @@ function onMessage(message) { case 'openSettings': if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { - Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "General Preferences"); + Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); } else { tablet.loadQMLOnTop("TabletGeneralPreferences.qml"); } @@ -191,6 +196,7 @@ function onMessage(message) { return; } else { print("SUCCESS uploading announcement story! Story ID:", response.user_story.id); + removeFromStoryIDsToMaybeDelete(message.story_id); // Don't delete original "for_url" story } }); } @@ -230,13 +236,13 @@ function onMessage(message) { return; } else { print("SUCCESS changing audience" + (message.isAnnouncement ? " and posting announcement!" : "!")); + removeFromStoryIDsToMaybeDelete(message.story_id); } }); } break; case 'removeFromStoryIDsToMaybeDelete': - storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1); - print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); + removeFromStoryIDsToMaybeDelete(message.story_id); break; default: print('Unknown message action received by snapshot.js!'); diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index bd5be142a0..f83e8d9550 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -92,7 +92,7 @@ tabletScalePercentage = getTabletScalePercentageFromSettings(); UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (tabletScalePercentage / 100), - null, activeHand, true); + null, activeHand, true, null, false); UIWebTablet.register(); HMD.tabletID = UIWebTablet.tabletEntityID; HMD.homeButtonID = UIWebTablet.homeButtonID; diff --git a/scripts/tutorials/createFloatingLanternBox.js b/scripts/tutorials/createFloatingLanternBox.js new file mode 100644 index 0000000000..c84214e295 --- /dev/null +++ b/scripts/tutorials/createFloatingLanternBox.js @@ -0,0 +1,43 @@ +"use strict"; +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// createFloatinLanternBox.js +// +// Created by MrRoboman on 17/05/04 +// Copyright 2017 High Fidelity, Inc. +// +// Creates a crate that spawn floating lanterns +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var COMPOUND_SHAPE_URL = "http://hifi-content.s3.amazonaws.com/Examples%20Content/production/maracas/woodenCrate_phys.obj"; +var MODEL_URL = "http://hifi-content.s3.amazonaws.com/Examples%20Content/production/maracas/woodenCrate_VR.fbx"; +var SCRIPT_URL = Script.resolvePath("./entity_scripts/floatingLanternBox.js?v=" + Date.now()); +var START_POSITION = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), 2)); +START_POSITION.y -= .6; +var LIFETIME = 3600; +var SCALE_FACTOR = 1; + +var lanternBox = { + type: "Model", + name: "Floating Lantern Box", + description: "Spawns Lanterns that float away when grabbed and released!", + script: SCRIPT_URL, + modelURL: MODEL_URL, + shapeType: "Compound", + compoundShapeURL: COMPOUND_SHAPE_URL, + position: START_POSITION, + lifetime: LIFETIME, + dimensions: { + x: 0.8696 * SCALE_FACTOR, + y: 0.58531 * SCALE_FACTOR, + z: 0.9264 * SCALE_FACTOR + }, + owningAvatarID: MyAvatar.sessionUUID +}; + +Entities.addEntity(lanternBox); +Script.stop(); diff --git a/scripts/tutorials/entity_scripts/floatingLantern.js b/scripts/tutorials/entity_scripts/floatingLantern.js new file mode 100644 index 0000000000..aa25dc0003 --- /dev/null +++ b/scripts/tutorials/entity_scripts/floatingLantern.js @@ -0,0 +1,106 @@ +"use strict"; +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// floatinLantern.js +// +// Created by MrRoboman on 17/05/04 +// Copyright 2017 High Fidelity, Inc. +// +// Makes floating lanterns rise upon being released and corrects their rotation as the fly. +// +// 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; + + var SLOW_SPIN_THRESHOLD = 0.1; + var ROTATION_COMPLETE_THRESHOLD = 0.01; + var ROTATION_SPEED = 0.2; + var HOME_ROTATION = {x: 0, y: 0, z: 0, w: 0}; + + + floatingLantern = function() { + _this = this; + this.updateConnected = false; + }; + + floatingLantern.prototype = { + + preload: function(entityID) { + this.entityID = entityID; + }, + + unload: function(entityID) { + this.disconnectUpdate(); + }, + + startNearGrab: function() { + this.disconnectUpdate(); + }, + + startDistantGrab: function() { + this.disconnectUpdate(); + }, + + releaseGrab: function() { + Entities.editEntity(this.entityID, { + gravity: { + x: 0, + y: 0.5, + z: 0 + } + }); + }, + + update: function(dt) { + var lanternProps = Entities.getEntityProperties(_this.entityID); + + if (lanternProps && lanternProps.rotation && lanternProps.owningAvatarID === MyAvatar.sessionUUID) { + + var spinningSlowly = ( + Math.abs(lanternProps.angularVelocity.x) < SLOW_SPIN_THRESHOLD && + Math.abs(lanternProps.angularVelocity.y) < SLOW_SPIN_THRESHOLD && + Math.abs(lanternProps.angularVelocity.z) < SLOW_SPIN_THRESHOLD + ); + + var rotationComplete = ( + Math.abs(lanternProps.rotation.x - HOME_ROTATION.x) < ROTATION_COMPLETE_THRESHOLD && + Math.abs(lanternProps.rotation.y - HOME_ROTATION.y) < ROTATION_COMPLETE_THRESHOLD && + Math.abs(lanternProps.rotation.z - HOME_ROTATION.z) < ROTATION_COMPLETE_THRESHOLD + ); + + if (spinningSlowly && !rotationComplete) { + var newRotation = Quat.slerp(lanternProps.rotation, HOME_ROTATION, ROTATION_SPEED * dt); + + Entities.editEntity(_this.entityID, { + rotation: newRotation, + angularVelocity: { + x: 0, + y: 0, + z: 0 + } + }); + } + } + }, + + connectUpdate: function() { + if (!this.updateConnected) { + this.updateConnected = true; + Script.update.connect(this.update); + } + }, + + disconnectUpdate: function() { + if (this.updateConnected) { + this.updateConnected = false; + Script.update.disconnect(this.update); + } + } + }; + + return new floatingLantern(); +}); diff --git a/scripts/tutorials/entity_scripts/floatingLanternBox.js b/scripts/tutorials/entity_scripts/floatingLanternBox.js new file mode 100644 index 0000000000..b5fb0c27d9 --- /dev/null +++ b/scripts/tutorials/entity_scripts/floatingLanternBox.js @@ -0,0 +1,103 @@ +"use strict"; +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// floatingLanternBox.js +// +// Created by MrRoboman on 17/05/04 +// Copyright 2017 High Fidelity, Inc. +// +// Spawns new floating lanterns every couple seconds if the old ones have been removed. +// +// 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; + var LANTERN_MODEL_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Models/chinaLantern_capsule.fbx"; + var LANTERN_SCRIPT_URL = Script.resolvePath("floatingLantern.js?v=" + Date.now()); + var LIFETIME = 120; + var RESPAWN_INTERVAL = 1000; + var MAX_LANTERNS = 4; + var SCALE_FACTOR = 1; + + var LANTERN = { + type: "Model", + name: "Floating Lantern", + description: "Spawns Lanterns that float away when grabbed and released!", + modelURL: LANTERN_MODEL_URL, + script: LANTERN_SCRIPT_URL, + dimensions: { + x: 0.2049 * SCALE_FACTOR, + y: 0.4 * SCALE_FACTOR, + z: 0.2049 * SCALE_FACTOR + }, + gravity: { + x: 0, + y: -1, + z: 0 + }, + velocity: { + x: 0, y: .01, z: 0 + }, + linearDampening: 0, + shapeType: 'Box', + lifetime: LIFETIME, + dynamic: true + }; + + lanternBox = function() { + _this = this; + }; + + lanternBox.prototype = { + + preload: function(entityID) { + this.entityID = entityID; + var props = Entities.getEntityProperties(this.entityID); + + if (props.owningAvatarID === MyAvatar.sessionUUID) { + this.respawnTimer = Script.setInterval(this.spawnAllLanterns.bind(this), RESPAWN_INTERVAL); + } + }, + + unload: function(entityID) { + if (this.respawnTimer) { + Script.clearInterval(this.respawnTimer); + } + }, + + spawnAllLanterns: function() { + var props = Entities.getEntityProperties(this.entityID); + var lanternCount = 0; + var nearbyEntities = Entities.findEntities(props.position, props.dimensions.x * 0.75); + + for (var i = 0; i < nearbyEntities.length; i++) { + var name = Entities.getEntityProperties(nearbyEntities[i], ["name"]).name; + if (name === "Floating Lantern") { + lanternCount++; + } + } + + while (lanternCount++ < MAX_LANTERNS) { + this.spawnLantern(); + } + }, + + spawnLantern: function() { + var boxProps = Entities.getEntityProperties(this.entityID); + + LANTERN.position = boxProps.position; + LANTERN.position.x += Math.random() * .2 - .1; + LANTERN.position.y += Math.random() * .2 + .1; + LANTERN.position.z += Math.random() * .2 - .1; + LANTERN.owningAvatarID = boxProps.owningAvatarID; + + return Entities.addEntity(LANTERN); + } + }; + + return new lanternBox(); +}); diff --git a/scripts/tutorials/entity_scripts/springHold.js b/scripts/tutorials/entity_scripts/springHold.js index 12b34381a6..059ea2cc6f 100644 --- a/scripts/tutorials/entity_scripts/springHold.js +++ b/scripts/tutorials/entity_scripts/springHold.js @@ -113,4 +113,4 @@ }; return new SpringHold(); -}); \ No newline at end of file +}); diff --git a/scripts/tutorials/entity_scripts/touch.js b/scripts/tutorials/entity_scripts/touch.js index d6a59aa167..1d0586b350 100644 --- a/scripts/tutorials/entity_scripts/touch.js +++ b/scripts/tutorials/entity_scripts/touch.js @@ -2,7 +2,7 @@ // touch.js // - // Sample file using spring action, haptic vibration, and color change to demonstrate two spheres + // Sample file using tractor action, haptic vibration, and color change to demonstrate two spheres // That can give a sense of touch to the holders. // Create two standard spheres, make them grabbable, and attach this entity script. Grab them and touch them together. // @@ -53,7 +53,7 @@ _this = this; } - function updateSpringAction(timescale) { + function updateTractorAction(timescale) { var targetProps = Entities.getEntityProperties(_this.entityID); // // Look for nearby entities to touch @@ -113,7 +113,7 @@ var success = Entities.updateAction(_this.copy, _this.actionID, props); } - function createSpringAction(timescale) { + function createTractorAction(timescale) { var targetProps = Entities.getEntityProperties(_this.entityID); var props = { @@ -123,7 +123,7 @@ angularTimeScale: timescale, ttl: ACTION_TTL }; - _this.actionID = Entities.addAction("spring", _this.copy, props); + _this.actionID = Entities.addAction("tractor", _this.copy, props); return; } @@ -170,7 +170,7 @@ }); } - function deleteSpringAction() { + function deleteTractorAction() { Entities.deleteAction(_this.copy, _this.actionID); } @@ -188,19 +188,19 @@ }, startNearGrab: function(entityID, data) { createCopy(); - createSpringAction(TIMESCALE); + createTractorAction(TIMESCALE); makeOriginalInvisible(); setHand(Entities.getEntityProperties(_this.entityID).position); }, continueNearGrab: function() { - updateSpringAction(TIMESCALE); + updateTractorAction(TIMESCALE); }, releaseGrab: function() { - deleteSpringAction(); + deleteTractorAction(); deleteCopy(); makeOriginalVisible(); } }; return new TouchExample(); -}); \ No newline at end of file +}); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 8dc993e6fe..0561956709 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -19,3 +19,6 @@ set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") add_subdirectory(atp-get) set_target_properties(atp-get PROPERTIES FOLDER "Tools") + +add_subdirectory(oven) +set_target_properties(oven PROPERTIES FOLDER "Tools") diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt new file mode 100644 index 0000000000..24c8a9a0e2 --- /dev/null +++ b/tools/oven/CMakeLists.txt @@ -0,0 +1,19 @@ +set(TARGET_NAME oven) + +setup_hifi_project(Widgets Gui Concurrent) + +link_hifi_libraries(networking shared image gpu ktx) + +if (WIN32) + package_libraries_for_deployment() +endif () + +# try to find the FBX SDK but fail silently if we don't +# because this tool is not built by default +find_package(FBX) +if (FBX_FOUND) + target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) + target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR}) +endif () + +set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) diff --git a/tools/oven/src/Baker.cpp b/tools/oven/src/Baker.cpp new file mode 100644 index 0000000000..c0cbd8d124 --- /dev/null +++ b/tools/oven/src/Baker.cpp @@ -0,0 +1,32 @@ +// +// Baker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ModelBakingLoggingCategory.h" + +#include "Baker.h" + +void Baker::handleError(const QString& error) { + qCCritical(model_baking).noquote() << error; + _errorList.append(error); + emit finished(); +} + +void Baker::handleErrors(const QStringList& errors) { + // we're appending errors, presumably from a baking operation we called + // add those to our list and emit that we are finished + _errorList.append(errors); + emit finished(); +} + +void Baker::handleWarning(const QString& warning) { + qCWarning(model_baking).noquote() << warning; + _warningList.append(warning); +} diff --git a/tools/oven/src/Baker.h b/tools/oven/src/Baker.h new file mode 100644 index 0000000000..d7107428bf --- /dev/null +++ b/tools/oven/src/Baker.h @@ -0,0 +1,43 @@ +// +// Baker.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Baker_h +#define hifi_Baker_h + +#include + +class Baker : public QObject { + Q_OBJECT + +public: + bool hasErrors() const { return !_errorList.isEmpty(); } + QStringList getErrors() const { return _errorList; } + + bool hasWarnings() const { return !_warningList.isEmpty(); } + QStringList getWarnings() const { return _warningList; } + +public slots: + virtual void bake() = 0; + +signals: + void finished(); + +protected: + void handleError(const QString& error); + void handleWarning(const QString& warning); + + void handleErrors(const QStringList& errors); + + QStringList _errorList; + QStringList _warningList; +}; + +#endif // hifi_Baker_h diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp new file mode 100644 index 0000000000..cb2a6bca29 --- /dev/null +++ b/tools/oven/src/DomainBaker.cpp @@ -0,0 +1,475 @@ +// +// DomainBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include "Gzip.h" + +#include "Oven.h" + +#include "DomainBaker.h" + +DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, + const QString& baseOutputPath, const QUrl& destinationPath) : + _localEntitiesFileURL(localModelFileURL), + _domainName(domainName), + _baseOutputPath(baseOutputPath) +{ + // make sure the destination path has a trailing slash + if (!destinationPath.toString().endsWith('/')) { + _destinationPath = destinationPath.toString() + '/'; + } else { + _destinationPath = destinationPath; + } +} + +void DomainBaker::bake() { + setupOutputFolder(); + + if (hasErrors()) { + return; + } + + loadLocalFile(); + + if (hasErrors()) { + return; + } + + enumerateEntities(); + + if (hasErrors()) { + return; + } + + // in case we've baked and re-written all of our entities already, check if we're done + checkIfRewritingComplete(); +} + +void DomainBaker::setupOutputFolder() { + // in order to avoid overwriting previous bakes, we create a special output folder with the domain name and timestamp + + // first, construct the directory name + auto domainPrefix = !_domainName.isEmpty() ? _domainName + "-" : ""; + auto timeNow = QDateTime::currentDateTime(); + + static const QString FOLDER_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; + QString outputDirectoryName = domainPrefix + timeNow.toString(FOLDER_TIMESTAMP_FORMAT); + + // make sure we can create that directory + QDir outputDir { _baseOutputPath }; + + if (!outputDir.mkpath(outputDirectoryName)) { + // add an error to specify that the output directory could not be created + handleError("Could not create output folder"); + + return; + } + + // store the unique output path so we can re-use it when saving baked models + outputDir.cd(outputDirectoryName); + _uniqueOutputPath = outputDir.absolutePath(); + + // add a content folder inside the unique output folder + static const QString CONTENT_OUTPUT_FOLDER_NAME = "content"; + if (!outputDir.mkpath(CONTENT_OUTPUT_FOLDER_NAME)) { + // add an error to specify that the content output directory could not be created + handleError("Could not create content folder"); + return; + } + + _contentOutputPath = outputDir.absoluteFilePath(CONTENT_OUTPUT_FOLDER_NAME); +} + +const QString ENTITIES_OBJECT_KEY = "Entities"; + +void DomainBaker::loadLocalFile() { + // load up the local entities file + QFile entitiesFile { _localEntitiesFileURL.toLocalFile() }; + + if (!entitiesFile.open(QIODevice::ReadOnly)) { + // add an error to our list to specify that the file could not be read + handleError("Could not open entities file"); + + // return to stop processing + return; + } + + // grab a byte array from the file + auto fileContents = entitiesFile.readAll(); + + // check if we need to inflate a gzipped models file or if this was already decompressed + static const QString GZIPPED_ENTITIES_FILE_SUFFIX = "gz"; + if (QFileInfo(_localEntitiesFileURL.toLocalFile()).suffix() == "gz") { + // this was a gzipped models file that we need to decompress + QByteArray uncompressedContents; + gunzip(fileContents, uncompressedContents); + fileContents = uncompressedContents; + } + + // read the file contents to a JSON document + auto jsonDocument = QJsonDocument::fromJson(fileContents); + + // grab the entities object from the root JSON object + _entities = jsonDocument.object()[ENTITIES_OBJECT_KEY].toArray(); + + if (_entities.isEmpty()) { + // add an error to our list stating that the models file was empty + + // return to stop processing + return; + } +} + +const QString ENTITY_MODEL_URL_KEY = "modelURL"; +const QString ENTITY_SKYBOX_KEY = "skybox"; +const QString ENTITY_SKYBOX_URL_KEY = "url"; +const QString ENTITY_KEYLIGHT_KEY = "keyLight"; +const QString ENTITY_KEYLIGHT_AMBIENT_URL_KEY = "ambientURL"; + +void DomainBaker::enumerateEntities() { + qDebug() << "Enumerating" << _entities.size() << "entities from domain"; + + for (auto it = _entities.begin(); it != _entities.end(); ++it) { + // make sure this is a JSON object + if (it->isObject()) { + auto entity = it->toObject(); + + // check if this is an entity with a model URL or is a skybox texture + if (entity.contains(ENTITY_MODEL_URL_KEY)) { + // grab a QUrl for the model URL + QUrl modelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + + // check if the file pointed to by this URL is a bakeable model, by comparing extensions + auto modelFileName = modelURL.fileName(); + + static const QStringList BAKEABLE_MODEL_EXTENSIONS { ".fbx" }; + auto completeLowerExtension = modelFileName.mid(modelFileName.indexOf('.')).toLower(); + + if (BAKEABLE_MODEL_EXTENSIONS.contains(completeLowerExtension)) { + // grab a clean version of the URL without a query or fragment + modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup an FBXBaker for this URL, as long as we don't already have one + if (!_modelBakers.contains(modelURL)) { + QSharedPointer baker { + new FBXBaker(modelURL, _contentOutputPath, []() -> QThread* { + return qApp->getNextWorkerThread(); + }), &FBXBaker::deleteLater + }; + + // make sure our handler is called when the baker is done + connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _modelBakers.insert(modelURL, baker); + + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(qApp->getFBXBakerThread()); + QMetaObject::invokeMethod(baker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that we can easily re-write + // the model URL to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(modelURL, *it); + } + } else { +// // We check now to see if we have either a texture for a skybox or a keylight, or both. +// if (entity.contains(ENTITY_SKYBOX_KEY)) { +// auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); +// if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { +// // we have a URL to a skybox, grab it +// QUrl skyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() }; +// +// // setup a bake of the skybox +// bakeSkybox(skyboxURL, *it); +// } +// } +// +// if (entity.contains(ENTITY_KEYLIGHT_KEY)) { +// auto keyLightObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); +// if (keyLightObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { +// // we have a URL to a skybox, grab it +// QUrl skyboxURL { keyLightObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY].toString() }; +// +// // setup a bake of the skybox +// bakeSkybox(skyboxURL, *it); +// } +// } + } + } + } + + // emit progress now to say we're just starting + emit bakeProgress(0, _totalNumberOfSubBakes); +} + +void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { + + auto skyboxFileName = skyboxURL.fileName(); + + static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { + ".jpg", ".png", ".gif", ".bmp", ".pbm", ".pgm", ".ppm", ".xbm", ".xpm", ".svg" + }; + auto completeLowerExtension = skyboxFileName.mid(skyboxFileName.indexOf('.')).toLower(); + + if (BAKEABLE_SKYBOX_EXTENSIONS.contains(completeLowerExtension)) { + // grab a clean version of the URL without a query or fragment + skyboxURL = skyboxURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a texture baker for this URL, as long as we aren't baking a skybox already + if (!_skyboxBakers.contains(skyboxURL)) { + // setup a baker for this skybox + + QSharedPointer skyboxBaker { + new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath), + &TextureBaker::deleteLater + }; + + // make sure our handler is called when the skybox baker is done + connect(skyboxBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedSkyboxBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _skyboxBakers.insert(skyboxURL, skyboxBaker); + + // move the baker to a worker thread and kickoff the bake + skyboxBaker->moveToThread(qApp->getNextWorkerThread()); + QMetaObject::invokeMethod(skyboxBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the skybox URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(skyboxURL, entity); + } +} + +void DomainBaker::handleFinishedModelBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this FBXBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getFBXUrl(); + + // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // entity objects needing a URL re-write + for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getFBXUrl())) { + + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = entityValue.toObject(); + + // grab the old URL + QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + + // setup a new URL using the prefix we were passed + QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); + + // copy the fragment and query, and user info from the old model URL + newModelURL.setQuery(oldModelURL.query()); + newModelURL.setFragment(oldModelURL.fragment()); + newModelURL.setUserInfo(oldModelURL.userInfo()); + + // set the new model URL as the value in our temp QJsonObject + entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString(); + + // check if the entity also had an animation at the same URL + // in which case it should be replaced with our baked model URL too + const QString ENTITY_ANIMATION_KEY = "animation"; + const QString ENTITIY_ANIMATION_URL_KEY = "url"; + + if (entity.contains(ENTITY_ANIMATION_KEY)) { + auto animationObject = entity[ENTITY_ANIMATION_KEY].toObject(); + + if (animationObject.contains(ENTITIY_ANIMATION_URL_KEY)) { + // grab the old animation URL + QUrl oldAnimationURL { animationObject[ENTITIY_ANIMATION_URL_KEY].toString() }; + + // check if its stripped down version matches our stripped down model URL + if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveQuery | QUrl::RemoveFragment)) { + // the animation URL matched the old model URL, so make the animation URL point to the baked FBX + // with its original query and fragment + auto newAnimationURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); + newAnimationURL.setQuery(oldAnimationURL.query()); + newAnimationURL.setFragment(oldAnimationURL.fragment()); + newAnimationURL.setUserInfo(oldAnimationURL.userInfo()); + + animationObject[ENTITIY_ANIMATION_URL_KEY] = newAnimationURL.toString(); + + // replace the animation object in the entity object + entity[ENTITY_ANIMATION_KEY] = animationObject; + } + } + } + + // replace our temp object with the value referenced by our QJsonValueRef + entityValue = entity; + } + } else { + // this model failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the model to our warnings + _warningList << baker->getErrors(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getFBXUrl()); + + // drop our shared pointer to this baker so that it gets cleaned up + _modelBakers.remove(baker->getFBXUrl()); + + // emit progress to tell listeners how many models we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last model we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + +void DomainBaker::handleFinishedSkyboxBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this FBXBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getTextureURL(); + + // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // entity objects needing a URL re-write + for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = entityValue.toObject(); + + if (entity.contains(ENTITY_SKYBOX_KEY)) { + auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); + + if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { + if (rewriteSkyboxURL(skyboxObject[ENTITY_SKYBOX_URL_KEY], baker)) { + // we re-wrote the URL, replace the skybox object referenced by the entity object + entity[ENTITY_SKYBOX_KEY] = skyboxObject; + } + } + } + + if (entity.contains(ENTITY_KEYLIGHT_KEY)) { + auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); + + if (ambientObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { + if (rewriteSkyboxURL(ambientObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY], baker)) { + // we re-wrote the URL, replace the ambient object referenced by the entity object + entity[ENTITY_KEYLIGHT_KEY] = ambientObject; + } + } + } + + // replace our temp object with the value referenced by our QJsonValueRef + entityValue = entity; + } + } else { + // this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the model to our warnings + _warningList << baker->getWarnings(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getTextureURL()); + + // drop our shared pointer to this baker so that it gets cleaned up + _skyboxBakers.remove(baker->getTextureURL()); + + // emit progress to tell listeners how many models we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last model we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + +bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) { + // grab the old skybox URL + QUrl oldSkyboxURL { urlValue.toString() }; + + if (oldSkyboxURL.matches(baker->getTextureURL(), QUrl::RemoveQuery | QUrl::RemoveFragment)) { + // change the URL to point to the baked texture with its original query and fragment + + auto newSkyboxURL = _destinationPath.resolved(baker->getBakedTextureFileName()); + newSkyboxURL.setQuery(oldSkyboxURL.query()); + newSkyboxURL.setFragment(oldSkyboxURL.fragment()); + newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); + + urlValue = newSkyboxURL.toString(); + + return true; + } else { + return false; + } +} + +void DomainBaker::checkIfRewritingComplete() { + if (_entitiesNeedingRewrite.isEmpty()) { + writeNewEntitiesFile(); + + if (hasErrors()) { + return; + } + + // we've now written out our new models file - time to say that we are finished up + emit finished(); + } +} + +void DomainBaker::writeNewEntitiesFile() { + // we've enumerated all of our entities and re-written all the URLs we'll be able to re-write + // time to write out a main models.json.gz file + + // first setup a document with the entities array below the entities key + QJsonDocument entitiesDocument; + + QJsonObject rootObject; + rootObject[ENTITIES_OBJECT_KEY] = _entities; + + entitiesDocument.setObject(rootObject); + + // turn that QJsonDocument into a byte array ready for compression + QByteArray jsonByteArray = entitiesDocument.toJson(); + + // compress the json byte array using gzip + QByteArray compressedJson; + gzip(jsonByteArray, compressedJson); + + // write the gzipped json to a new models file + static const QString MODELS_FILE_NAME = "models.json.gz"; + + auto bakedEntitiesFilePath = QDir(_uniqueOutputPath).filePath(MODELS_FILE_NAME); + QFile compressedEntitiesFile { bakedEntitiesFilePath }; + + if (!compressedEntitiesFile.open(QIODevice::WriteOnly) + || (compressedEntitiesFile.write(compressedJson) == -1)) { + + // add an error to our list to state that the output models file could not be created or could not be written to + handleError("Failed to export baked entities file"); + + return; + } + + qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; +} + diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h new file mode 100644 index 0000000000..5244408115 --- /dev/null +++ b/tools/oven/src/DomainBaker.h @@ -0,0 +1,70 @@ +// +// DomainBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DomainBaker_h +#define hifi_DomainBaker_h + +#include +#include +#include +#include + +#include "Baker.h" +#include "FBXBaker.h" +#include "TextureBaker.h" + +class DomainBaker : public Baker { + Q_OBJECT +public: + // This is a real bummer, but the FBX SDK is not thread safe - even with separate FBXManager objects. + // This means that we need to put all of the FBX importing/exporting from the same process on the same thread. + // That means you must pass a usable running QThread when constructing a domain baker. + DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, + const QString& baseOutputPath, const QUrl& destinationPath); + +signals: + void allModelsFinished(); + void bakeProgress(int baked, int total); + +private slots: + virtual void bake() override; + void handleFinishedModelBaker(); + void handleFinishedSkyboxBaker(); + +private: + void setupOutputFolder(); + void loadLocalFile(); + void enumerateEntities(); + void checkIfRewritingComplete(); + void writeNewEntitiesFile(); + + void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity); + bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker); + + QUrl _localEntitiesFileURL; + QString _domainName; + QString _baseOutputPath; + QString _uniqueOutputPath; + QString _contentOutputPath; + QUrl _destinationPath; + + QJsonArray _entities; + + QHash> _modelBakers; + QHash> _skyboxBakers; + + QMultiHash _entitiesNeedingRewrite; + + int _totalNumberOfSubBakes { 0 }; + int _completedSubBakes { 0 }; +}; + +#endif // hifi_DomainBaker_h diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp new file mode 100644 index 0000000000..8a72784d7c --- /dev/null +++ b/tools/oven/src/FBXBaker.cpp @@ -0,0 +1,554 @@ +// +// FBXBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 3/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include // need this include so we don't get an error looking for std::isnan + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "ModelBakingLoggingCategory.h" +#include "TextureBaker.h" + +#include "FBXBaker.h" + +std::once_flag onceFlag; +FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr }; + +FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, + TextureBakerThreadGetter textureThreadGetter, bool copyOriginals) : + _fbxURL(fbxURL), + _baseOutputPath(baseOutputPath), + _textureThreadGetter(textureThreadGetter), + _copyOriginals(copyOriginals) +{ + std::call_once(onceFlag, [](){ + // create the static FBX SDK manager + _sdkManager = FBXSDKManagerUniquePointer(FbxManager::Create(), [](FbxManager* manager){ + manager->Destroy(); + }); + }); + + // grab the name of the FBX from the URL, this is used for folder output names + auto fileName = fbxURL.fileName(); + _fbxName = fileName.left(fileName.lastIndexOf('.')); +} + +static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; +static const QString ORIGINAL_OUTPUT_SUBFOLDER = "original/"; + +QString FBXBaker::pathToCopyOfOriginal() const { + return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); +} + +void FBXBaker::bake() { + qCDebug(model_baking) << "Baking" << _fbxURL; + + // setup the output folder for the results of this bake + setupOutputFolder(); + + if (hasErrors()) { + return; + } + + connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy); + + // make a local copy of the FBX file + loadSourceFBX(); +} + +void FBXBaker::bakeSourceCopy() { + // load the scene from the FBX file + importScene(); + + if (hasErrors()) { + return; + } + + // enumerate the textures found in the scene and start a bake for them + rewriteAndBakeSceneTextures(); + + if (hasErrors()) { + return; + } + + // export the FBX with re-written texture references + exportScene(); + + if (hasErrors()) { + return; + } + + // check if we're already done with textures (in case we had none to re-write) + checkIfTexturesFinished(); +} + +void FBXBaker::setupOutputFolder() { + // construct the output path using the name of the fbx and the base output path + _uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "/"; + + // make sure there isn't already an output directory using the same name + int iteration = 0; + + while (QDir(_uniqueOutputPath).exists()) { + _uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "-" + QString::number(++iteration) + "/"; + } + + qCDebug(model_baking) << "Creating FBX output folder" << _uniqueOutputPath; + + // attempt to make the output folder + if (!QDir().mkdir(_uniqueOutputPath)) { + handleError("Failed to create FBX output folder " + _uniqueOutputPath); + return; + } + + // make the baked and original sub-folders used during export + QDir uniqueOutputDir = _uniqueOutputPath; + if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) { + handleError("Failed to create baked/original subfolders in " + _uniqueOutputPath); + return; + } +} + +void FBXBaker::loadSourceFBX() { + // check if the FBX is local or first needs to be downloaded + if (_fbxURL.isLocalFile()) { + // load up the local file + QFile localFBX { _fbxURL.toLocalFile() }; + + // make a copy in the output folder + localFBX.copy(pathToCopyOfOriginal()); + + // emit our signal to start the import of the FBX source copy + emit sourceCopyReadyToLoad(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + + networkRequest.setUrl(_fbxURL); + + qCDebug(model_baking) << "Downloading" << _fbxURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); + } +} + +void FBXBaker::handleFBXNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _fbxURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(pathToCopyOfOriginal()); + + qDebug(model_baking) << "Writing copy of original FBX to" << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly) || (copyOfOriginal.write(requestReply->readAll()) == -1)) { + // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made + handleError("Could not create copy of " + _fbxURL.toString()); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + // emit our signal to start the import of the FBX source copy + emit sourceCopyReadyToLoad(); + } else { + // add an error to our list stating that the FBX could not be downloaded + handleError("Failed to download " + _fbxURL.toString()); + } +} + +void FBXBaker::importScene() { + // create an FBX SDK importer + FbxImporter* importer = FbxImporter::Create(_sdkManager.get(), ""); + + // import the copy of the original FBX file + QString originalCopyPath = pathToCopyOfOriginal(); + bool importStatus = importer->Initialize(originalCopyPath.toLocal8Bit().data()); + + if (!importStatus) { + // failed to initialize importer, print an error and return + handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString()); + return; + } else { + qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene"; + } + + // setup a new scene to hold the imported file + _scene = FbxScene::Create(_sdkManager.get(), "bakeScene"); + + // import the file to the created scene + importer->Import(_scene); + + // destroy the importer that is no longer needed + importer->Destroy(); +} + +QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { + auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (texturePath.startsWith(fbxPath)) { + // texture path is a child of the FBX path, return the texture path without the fbx path + return texturePath.mid(fbxPath.length()); + } else { + // the texture path was not a child of the FBX path, return the empty string + return ""; + } +} + +QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { + // first make sure we have a unique base name for this texture + // in case another texture referenced by this model has the same base name + auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; + + QString bakedTextureFileName { textureFileInfo.completeBaseName() }; + + if (nameMatches > 0) { + // there are already nameMatches texture with this name + // append - and that number to our baked texture file name so that it is unique + bakedTextureFileName += "-" + QString::number(nameMatches); + } + + bakedTextureFileName += BAKED_TEXTURE_EXT; + + // increment the number of name matches + ++nameMatches; + + return bakedTextureFileName; +} + +QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) { + QUrl urlToTexture; + + if (textureFileInfo.exists() && textureFileInfo.isFile()) { + // set the texture URL to the local texture that we have confirmed exists + urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); + } else { + // external texture that we'll need to download or find + + // first check if it the RelativePath to the texture in the FBX was relative + QString relativeFileName = fileTexture->GetRelativeFileName(); + auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); + + // this is a relative file path which will require different handling + // depending on the location of the original FBX + if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + // the absolute path we ran into for the texture in the FBX exists on this machine + // so use that file + urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); + } else { + // we didn't find the texture on this machine at the absolute path + // so assume that it is right beside the FBX to match the behaviour of interface + urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); + } + } + + return urlToTexture; +} + +image::TextureUsage::Type textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { + using namespace image::TextureUsage; + + // this is a property we know has a texture, we need to match it to a High Fidelity known texture type + // since that information is passed to the baking process + + // grab the hierarchical name for this property and lowercase it for case-insensitive compare + auto propertyName = QString(property.GetHierarchicalName()).toLower(); + + // figure out the type of the property based on what known value string it matches + if ((propertyName.contains("diffuse") && !propertyName.contains("tex_global_diffuse")) + || propertyName.contains("tex_color_map")) { + return ALBEDO_TEXTURE; + } else if (propertyName.contains("transparentcolor") || propertyName.contains("transparencyfactor")) { + return ALBEDO_TEXTURE; + } else if (propertyName.contains("bump")) { + return BUMP_TEXTURE; + } else if (propertyName.contains("normal")) { + return NORMAL_TEXTURE; + } else if ((propertyName.contains("specular") && !propertyName.contains("tex_global_specular")) + || propertyName.contains("reflection")) { + return SPECULAR_TEXTURE; + } else if (propertyName.contains("tex_metallic_map")) { + return METALLIC_TEXTURE; + } else if (propertyName.contains("shininess")) { + return GLOSS_TEXTURE; + } else if (propertyName.contains("tex_roughness_map")) { + return ROUGHNESS_TEXTURE; + } else if (propertyName.contains("emissive")) { + return EMISSIVE_TEXTURE; + } else if (propertyName.contains("ambientcolor")) { + return LIGHTMAP_TEXTURE; + } else if (propertyName.contains("ambientfactor")) { + // we need to check what the ambient factor is, since that tells Interface to process this texture + // either as an occlusion texture or a light map + auto lambertMaterial = FbxCast(material); + + if (lambertMaterial->AmbientFactor == 0) { + return LIGHTMAP_TEXTURE; + } else if (lambertMaterial->AmbientFactor > 0) { + return OCCLUSION_TEXTURE; + } else { + return UNUSED_TEXTURE; + } + + } else if (propertyName.contains("tex_ao_map")) { + return OCCLUSION_TEXTURE; + } + + return UNUSED_TEXTURE; +} + +void FBXBaker::rewriteAndBakeSceneTextures() { + + // enumerate the surface materials to find the textures used in the scene + int numMaterials = _scene->GetMaterialCount(); + for (int i = 0; i < numMaterials; i++) { + FbxSurfaceMaterial* material = _scene->GetMaterial(i); + + if (material) { + // enumerate the properties of this material to see what texture channels it might have + FbxProperty property = material->GetFirstProperty(); + + while (property.IsValid()) { + // first check if this property has connected textures, if not we don't need to bother with it here + if (property.GetSrcObjectCount() > 0) { + + // figure out the type of texture from the material property + auto textureType = textureTypeForMaterialProperty(property, material); + + if (textureType != image::TextureUsage::UNUSED_TEXTURE) { + int numTextures = property.GetSrcObjectCount(); + + for (int j = 0; j < numTextures; j++) { + FbxFileTexture* fileTexture = property.GetSrcObject(j); + + // use QFileInfo to easily split up the existing texture filename into its components + QString fbxFileName { fileTexture->GetFileName() }; + QFileInfo textureFileInfo { fbxFileName.replace("\\", "/") }; + + // make sure this texture points to something and isn't one we've already re-mapped + if (!textureFileInfo.filePath().isEmpty() + && textureFileInfo.suffix() != BAKED_TEXTURE_EXT.mid(1)) { + + // construct the new baked texture file name and file path + // ensuring that the baked texture will have a unique name + // even if there was another texture with the same name at a different path + auto bakedTextureFileName = createBakedTextureFileName(textureFileInfo); + QString bakedTextureFilePath { + _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + bakedTextureFileName + }; + + qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() + << "to" << bakedTextureFilePath; + + // write the new filename into the FBX scene + fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); + + // write the relative filename to be the baked texture file name since it will + // be right beside the FBX + fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData()); + + // figure out the URL to this texture, embedded or external + auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); + + if (!_bakingTextures.contains(urlToTexture)) { + // bake this texture asynchronously + bakeTexture(urlToTexture, textureType, _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER); + } + } + } + } + } + + property = material->GetNextProperty(property); + } + } + } +} + +void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir) { + // start a bake for this texture and add it to our list to keep track of + QSharedPointer bakingTexture { + new TextureBaker(textureURL, textureType, outputDir), + &TextureBaker::deleteLater + }; + + // make sure we hear when the baking texture is done + connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); + + // keep a shared pointer to the baking texture + _bakingTextures.insert(textureURL, bakingTexture); + + // start baking the texture on one of our available worker threads + bakingTexture->moveToThread(_textureThreadGetter()); + QMetaObject::invokeMethod(bakingTexture.data(), "bake"); +} + +void FBXBaker::handleBakedTexture() { + TextureBaker* bakedTexture = qobject_cast(sender()); + + // make sure we haven't already run into errors, and that this is a valid texture + if (bakedTexture) { + if (!hasErrors()) { + if (!bakedTexture->hasErrors()) { + if (_copyOriginals) { + // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture + + // use the path to the texture being baked to determine if this was an embedded or a linked texture + + // it is embeddded if the texure being baked was inside the original output folder + // since that is where the FBX SDK places the .fbm folder it generates when importing the FBX + + auto originalOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER); + + if (!originalOutputFolder.isParentOf(bakedTexture->getTextureURL())) { + // for linked textures we want to save a copy of original texture beside the original FBX + + qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); + + // check if we have a relative path to use for the texture + auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL()); + + QFile originalTextureFile { + _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName() + }; + + if (relativeTexturePath.length() > 0) { + // make the folders needed by the relative path + } + + if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { + qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() + << "for" << _fbxURL; + } else { + handleError("Could not save original external texture " + originalTextureFile.fileName() + + " for " + _fbxURL.toString()); + return; + } + } + } + + + // now that this texture has been baked and handled, we can remove that TextureBaker from our hash + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } else { + // there was an error baking this texture - add it to our list of errors + _errorList.append(bakedTexture->getErrors()); + + // we don't emit finished yet so that the other textures can finish baking first + _pendingErrorEmission = true; + + // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } + } else { + // we have errors to attend to, so we don't do extra processing for this texture + // but we do need to remove that TextureBaker from our list + // and then check if we're done with all textures + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } + } +} + +void FBXBaker::exportScene() { + // setup the exporter + FbxExporter* exporter = FbxExporter::Create(_sdkManager.get(), ""); + + auto rewrittenFBXPath = _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + _fbxName + BAKED_FBX_EXTENSION; + + // save the relative path to this FBX inside our passed output folder + _bakedFBXRelativePath = rewrittenFBXPath; + _bakedFBXRelativePath.remove(_baseOutputPath + "/"); + + bool exportStatus = exporter->Initialize(rewrittenFBXPath.toLocal8Bit().data()); + + if (!exportStatus) { + // failed to initialize exporter, print an error and return + handleError("Failed to export FBX file at " + _fbxURL.toString() + " to " + rewrittenFBXPath + + "- error: " + exporter->GetStatus().GetErrorString()); + } + + // export the scene + exporter->Export(_scene); + + qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << rewrittenFBXPath; +} + + +void FBXBaker::removeEmbeddedMediaFolder() { + // now that the bake is complete, remove the embedded media folder produced by the FBX SDK when it imports an FBX + auto embeddedMediaFolderName = _fbxURL.fileName().replace(".fbx", ".fbm"); + QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively(); +} + +void FBXBaker::possiblyCleanupOriginals() { + if (!_copyOriginals) { + // caller did not ask us to keep the original around, so delete the original output folder now + QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER).removeRecursively(); + } +} + +void FBXBaker::checkIfTexturesFinished() { + // check if we're done everything we need to do for this FBX + // and emit our finished signal if we're done + + if (_bakingTextures.isEmpty()) { + // remove the embedded media folder that the FBX SDK produces when reading the original + removeEmbeddedMediaFolder(); + + // cleanup the originals if we weren't asked to keep them around + possiblyCleanupOriginals(); + + if (hasErrors()) { + // if we're checking for completion but we have errors + // that means one or more of our texture baking operations failed + + if (_pendingErrorEmission) { + emit finished(); + } + + return; + } else { + qCDebug(model_baking) << "Finished baking" << _fbxURL; + + emit finished(); + } + } +} diff --git a/tools/oven/src/FBXBaker.h b/tools/oven/src/FBXBaker.h new file mode 100644 index 0000000000..bcfebbe2a8 --- /dev/null +++ b/tools/oven/src/FBXBaker.h @@ -0,0 +1,101 @@ +// +// FBXBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 3/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_FBXBaker_h +#define hifi_FBXBaker_h + +#include +#include +#include +#include + +#include "Baker.h" +#include "TextureBaker.h" + +#include + +namespace fbxsdk { + class FbxManager; + class FbxProperty; + class FbxScene; + class FbxFileTexture; +} + +static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; +using FBXSDKManagerUniquePointer = std::unique_ptr>; + +using TextureBakerThreadGetter = std::function; + +class FBXBaker : public Baker { + Q_OBJECT +public: + FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, + TextureBakerThreadGetter textureThreadGetter, bool copyOriginals = true); + + QUrl getFBXUrl() const { return _fbxURL; } + QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } + +public slots: + // all calls to FBXBaker::bake for FBXBaker instances must be from the same thread + // because the Autodesk SDK will cause a crash if it is called from multiple threads + virtual void bake() override; + +signals: + void sourceCopyReadyToLoad(); + +private slots: + void bakeSourceCopy(); + void handleFBXNetworkReply(); + void handleBakedTexture(); + +private: + void setupOutputFolder(); + + void loadSourceFBX(); + + void bakeCopiedFBX(); + + void importScene(); + void rewriteAndBakeSceneTextures(); + void exportScene(); + void removeEmbeddedMediaFolder(); + void possiblyCleanupOriginals(); + + void checkIfTexturesFinished(); + + QString createBakedTextureFileName(const QFileInfo& textureFileInfo); + QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); + + void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir); + + QString pathToCopyOfOriginal() const; + + QUrl _fbxURL; + QString _fbxName; + + QString _baseOutputPath; + QString _uniqueOutputPath; + QString _bakedFBXRelativePath; + + static FBXSDKManagerUniquePointer _sdkManager; + fbxsdk::FbxScene* _scene { nullptr }; + + QMultiHash> _bakingTextures; + QHash _textureNameMatchCount; + + TextureBakerThreadGetter _textureThreadGetter; + + bool _copyOriginals { true }; + + bool _pendingErrorEmission { false }; +}; + +#endif // hifi_FBXBaker_h diff --git a/tools/oven/src/ModelBakingLoggingCategory.cpp b/tools/oven/src/ModelBakingLoggingCategory.cpp new file mode 100644 index 0000000000..f897ddf5ca --- /dev/null +++ b/tools/oven/src/ModelBakingLoggingCategory.cpp @@ -0,0 +1,14 @@ +// +// ModelBakingLoggingCategory.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ModelBakingLoggingCategory.h" + +Q_LOGGING_CATEGORY(model_baking, "hifi.model-baking"); diff --git a/tools/oven/src/ModelBakingLoggingCategory.h b/tools/oven/src/ModelBakingLoggingCategory.h new file mode 100644 index 0000000000..6c7d9d5db6 --- /dev/null +++ b/tools/oven/src/ModelBakingLoggingCategory.h @@ -0,0 +1,19 @@ +// +// ModelBakingLoggingCategory.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ModelBakingLoggingCategory_h +#define hifi_ModelBakingLoggingCategory_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(model_baking) + +#endif // hifi_ModelBakingLoggingCategory_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp new file mode 100644 index 0000000000..ac8ef505ba --- /dev/null +++ b/tools/oven/src/Oven.cpp @@ -0,0 +1,100 @@ +// +// Oven.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include + +#include + +#include "ui/OvenMainWindow.h" + +#include "Oven.h" + +static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/export"; + +Oven::Oven(int argc, char* argv[]) : + QApplication(argc, argv) +{ + QCoreApplication::setOrganizationName("High Fidelity"); + QCoreApplication::setApplicationName("Oven"); + + // init the settings interface so we can save and load settings + Setting::init(); + + // check if we were passed any command line arguments that would tell us just to run without the GUI + + // setup the GUI + _mainWindow = new OvenMainWindow; + _mainWindow->show(); + + // setup our worker threads + setupWorkerThreads(QThread::idealThreadCount() - 1); + + // Autodesk's SDK means that we need a single thread for all FBX importing/exporting in the same process + // setup the FBX Baker thread + setupFBXBakerThread(); +} + +Oven::~Oven() { + // cleanup the worker threads + for (auto i = 0; i < _workerThreads.size(); ++i) { + _workerThreads[i]->quit(); + _workerThreads[i]->wait(); + } + + // cleanup the FBX Baker thread + _fbxBakerThread->quit(); + _fbxBakerThread->wait(); +} + +void Oven::setupWorkerThreads(int numWorkerThreads) { + for (auto i = 0; i < numWorkerThreads; ++i) { + // setup a worker thread yet and add it to our concurrent vector + auto newThread = new QThread(this); + newThread->setObjectName("Oven Worker Thread " + QString::number(i + 1)); + + _workerThreads.push_back(newThread); + } +} + +void Oven::setupFBXBakerThread() { + // we're being asked for the FBX baker thread, but we don't have one yet + // so set that up now + _fbxBakerThread = new QThread(this); + _fbxBakerThread->setObjectName("Oven FBX Baker Thread"); +} + +QThread* Oven::getFBXBakerThread() { + if (!_fbxBakerThread->isRunning()) { + // start the FBX baker thread if it isn't running yet + _fbxBakerThread->start(); + } + + return _fbxBakerThread; +} + +QThread* Oven::getNextWorkerThread() { + // Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use. + // We can't use QThreadPool because we want to put QObjects with signals/slots on these threads. + // So instead we setup our own list of threads, up to one less than the ideal thread count + // (for the FBX Baker Thread to have room), and cycle through them to hand a usable running thread back to our callers. + + auto nextIndex = ++_nextWorkerThreadIndex; + auto nextThread = _workerThreads[nextIndex % _workerThreads.size()]; + + // start the thread if it isn't running yet + if (!nextThread->isRunning()) { + nextThread->start(); + } + + return nextThread; +} + diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h new file mode 100644 index 0000000000..350c615ce0 --- /dev/null +++ b/tools/oven/src/Oven.h @@ -0,0 +1,53 @@ +// +// Oven.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Oven_h +#define hifi_Oven_h + +#include + +#include + +#include + +#if defined(qApp) +#undef qApp +#endif +#define qApp (static_cast(QCoreApplication::instance())) + +class OvenMainWindow; + +class Oven : public QApplication { + Q_OBJECT + +public: + Oven(int argc, char* argv[]); + ~Oven(); + + OvenMainWindow* getMainWindow() const { return _mainWindow; } + + QThread* getFBXBakerThread(); + QThread* getNextWorkerThread(); + +private: + void setupWorkerThreads(int numWorkerThreads); + void setupFBXBakerThread(); + + OvenMainWindow* _mainWindow; + QThread* _fbxBakerThread; + QList _workerThreads; + + std::atomic _nextWorkerThreadIndex; + int _numWorkerThreads; +}; + + +#endif // hifi_Oven_h diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp new file mode 100644 index 0000000000..70df511d2c --- /dev/null +++ b/tools/oven/src/TextureBaker.cpp @@ -0,0 +1,131 @@ +// +// TextureBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ModelBakingLoggingCategory.h" + +#include "TextureBaker.h" + +const QString BAKED_TEXTURE_EXT = ".ktx"; + +TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) : + _textureURL(textureURL), + _textureType(textureType), + _outputDirectory(outputDirectory) +{ + // figure out the baked texture filename + auto originalFilename = textureURL.fileName(); + _bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT; +} + +void TextureBaker::bake() { + // once our texture is loaded, kick off a the processing + connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); + + // first load the texture (either locally or remotely) + loadTexture(); +} + +void TextureBaker::loadTexture() { + // check if the texture is local or first needs to be downloaded + if (_textureURL.isLocalFile()) { + // load up the local file + QFile localTexture { _textureURL.toLocalFile() }; + + if (!localTexture.open(QIODevice::ReadOnly)) { + handleError("Unable to open texture " + _textureURL.toString()); + return; + } + + _originalTexture = localTexture.readAll(); + + emit originalTextureLoaded(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_textureURL); + + qCDebug(model_baking) << "Downloading" << _textureURL; + + // kickoff the download, wait for slot to tell us it is done + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply); + } +} + +void TextureBaker::handleTextureNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded texture" << _textureURL; + + // store the original texture so it can be passed along for the bake + _originalTexture = requestReply->readAll(); + + emit originalTextureLoaded(); + } else { + // add an error to our list stating that this texture could not be downloaded + handleError("Error downloading " + _textureURL.toString() + " - " + requestReply->errorString()); + } +} + +void TextureBaker::processTexture() { + auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(), + ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType); + + if (!processedTexture) { + handleError("Could not process texture " + _textureURL.toString()); + return; + } + + // the baked textures need to have the source hash added for cache checks in Interface + // so we add that to the processed texture before handling it off to be serialized + auto hashData = QCryptographicHash::hash(_originalTexture, QCryptographicHash::Md5); + std::string hash = hashData.toHex().toStdString(); + processedTexture->setSourceHash(hash); + + auto memKTX = gpu::Texture::serialize(*processedTexture); + + if (!memKTX) { + handleError("Could not serialize " + _textureURL.toString() + " to KTX"); + return; + } + + const char* data = reinterpret_cast(memKTX->_storage->data()); + const size_t length = memKTX->_storage->size(); + + // attempt to write the baked texture to the destination file path + QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) }; + + if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { + handleError("Could not write baked texture for " + _textureURL.toString()); + } + + qCDebug(model_baking) << "Baked texture" << _textureURL; + emit finished(); +} diff --git a/tools/oven/src/TextureBaker.h b/tools/oven/src/TextureBaker.h new file mode 100644 index 0000000000..ee1e968f20 --- /dev/null +++ b/tools/oven/src/TextureBaker.h @@ -0,0 +1,59 @@ +// +// TextureBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TextureBaker_h +#define hifi_TextureBaker_h + +#include +#include +#include + +#include + +#include "Baker.h" + +extern const QString BAKED_TEXTURE_EXT; + +class TextureBaker : public Baker { + Q_OBJECT + +public: + TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory); + + const QByteArray& getOriginalTexture() const { return _originalTexture; } + + QUrl getTextureURL() const { return _textureURL; } + + QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); } + QString getBakedTextureFileName() const { return _bakedTextureFileName; } + +public slots: + virtual void bake() override; + +signals: + void originalTextureLoaded(); + +private slots: + void processTexture(); + +private: + void loadTexture(); + void handleTextureNetworkReply(); + + QUrl _textureURL; + QByteArray _originalTexture; + image::TextureUsage::Type _textureType; + + QDir _outputDirectory; + QString _bakedTextureFileName; +}; + +#endif // hifi_TextureBaker_h diff --git a/tools/oven/src/main.cpp b/tools/oven/src/main.cpp new file mode 100644 index 0000000000..9c778245b5 --- /dev/null +++ b/tools/oven/src/main.cpp @@ -0,0 +1,16 @@ +// +// main.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 3/28/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include "Oven.h" + +int main (int argc, char** argv) { + Oven app(argc, argv); + return app.exec(); +} diff --git a/tools/oven/src/ui/BakeWidget.cpp b/tools/oven/src/ui/BakeWidget.cpp new file mode 100644 index 0000000000..23a4822d82 --- /dev/null +++ b/tools/oven/src/ui/BakeWidget.cpp @@ -0,0 +1,46 @@ +// +// BakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "BakeWidget.h" + +BakeWidget::BakeWidget(QWidget* parent, Qt::WindowFlags flags) : + QWidget(parent, flags) +{ + +} + +BakeWidget::~BakeWidget() { + // if we're going down, our bakers are about to too + // enumerate them, send a cancelled status to the results table, and remove them + auto it = _bakers.begin(); + while (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + resultsWindow->changeStatusForRow(resultRow, "Cancelled"); + + it = _bakers.erase(it); + } +} + +void BakeWidget::cancelButtonClicked() { + // the user wants to go back to the mode selection screen + // remove ourselves from the stacked widget and call delete later so we'll be cleaned up + auto stackedWidget = qobject_cast(parentWidget()); + stackedWidget->removeWidget(this); + + this->deleteLater(); +} diff --git a/tools/oven/src/ui/BakeWidget.h b/tools/oven/src/ui/BakeWidget.h new file mode 100644 index 0000000000..e7ab8d1840 --- /dev/null +++ b/tools/oven/src/ui/BakeWidget.h @@ -0,0 +1,33 @@ +// +// BakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BakeWidget_h +#define hifi_BakeWidget_h + +#include + +#include "../Baker.h" + +class BakeWidget : public QWidget { + Q_OBJECT +public: + BakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + ~BakeWidget(); + + void cancelButtonClicked(); + +protected: + using BakerRowPair = std::pair, int>; + using BakerRowPairList = std::list; + BakerRowPairList _bakers; +}; + +#endif // hifi_BakeWidget_h diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp new file mode 100644 index 0000000000..7d667305bb --- /dev/null +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -0,0 +1,286 @@ +// +// DomainBakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "DomainBakeWidget.h" + +static const QString DOMAIN_NAME_SETTING_KEY = "domain_name"; +static const QString EXPORT_DIR_SETTING_KEY = "domain_export_directory"; +static const QString BROWSE_START_DIR_SETTING_KEY = "domain_search_directory"; +static const QString DESTINATION_PATH_SETTING_KEY = "destination_path"; + +DomainBakeWidget::DomainBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + BakeWidget(parent, flags), + _domainNameSetting(DOMAIN_NAME_SETTING_KEY), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _browseStartDirectory(BROWSE_START_DIR_SETTING_KEY), + _destinationPathSetting(DESTINATION_PATH_SETTING_KEY) +{ + setupUI(); +} + +void DomainBakeWidget::setupUI() { + // setup a grid layout to hold everything + QGridLayout* gridLayout = new QGridLayout; + + int rowIndex = 0; + + // setup a section to enter the name of the domain being baked + QLabel* domainNameLabel = new QLabel("Domain Name"); + + _domainNameLineEdit = new QLineEdit; + _domainNameLineEdit->setPlaceholderText("welcome"); + + // set the text of the domain name from whatever was used during last bake + if (!_domainNameSetting.get().isEmpty()) { + _domainNameLineEdit->setText(_domainNameSetting.get()); + } + + gridLayout->addWidget(domainNameLabel); + gridLayout->addWidget(_domainNameLineEdit, rowIndex, 1, 1, -1); + + ++rowIndex; + + // setup a section to choose the file being baked + QLabel* entitiesFileLabel = new QLabel("Entities File"); + + _entitiesFileLineEdit = new QLineEdit; + _entitiesFileLineEdit->setPlaceholderText("File"); + + QPushButton* chooseFileButton = new QPushButton("Browse..."); + connect(chooseFileButton, &QPushButton::clicked, this, &DomainBakeWidget::chooseFileButtonClicked); + + // add the components for the entities file picker to the layout + gridLayout->addWidget(entitiesFileLabel, rowIndex, 0); + gridLayout->addWidget(_entitiesFileLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseFileButton, rowIndex, 4); + + // start a new row for next component + ++rowIndex; + + // setup a section to choose the output directory + QLabel* outputDirectoryLabel = new QLabel("Output Directory"); + + _outputDirLineEdit = new QLineEdit; + + // set the current export directory to whatever was last used + _outputDirLineEdit->setText(_exportDirectory.get()); + + // whenever the output directory line edit changes, update the value in settings + connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &DomainBakeWidget::outputDirectoryChanged); + + QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse..."); + connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &DomainBakeWidget::chooseOutputDirButtonClicked); + + // add the components for the output directory picker to the layout + gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0); + gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4); + + // start a new row for the next component + ++rowIndex; + + // setup a section to choose the upload prefix - the URL where baked models will be made available + QLabel* uploadPrefixLabel = new QLabel("Destination URL Path"); + + _destinationPathLineEdit = new QLineEdit; + _destinationPathLineEdit->setPlaceholderText("http://cdn.example.com/baked-domain/"); + + if (!_destinationPathSetting.get().isEmpty()) { + _destinationPathLineEdit->setText(_destinationPathSetting.get()); + } + + gridLayout->addWidget(uploadPrefixLabel, rowIndex, 0); + gridLayout->addWidget(_destinationPathLineEdit, rowIndex, 1, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a horizontal line to split the bake/cancel buttons off + QFrame* lineFrame = new QFrame; + lineFrame->setFrameShape(QFrame::HLine); + lineFrame->setFrameShadow(QFrame::Sunken); + gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a button that will kickoff the bake + QPushButton* bakeButton = new QPushButton("Bake"); + connect(bakeButton, &QPushButton::clicked, this, &DomainBakeWidget::bakeButtonClicked); + gridLayout->addWidget(bakeButton, rowIndex, 3); + + // add a cancel button to go back to the modes page + QPushButton* cancelButton = new QPushButton("Cancel"); + connect(cancelButton, &QPushButton::clicked, this, &DomainBakeWidget::cancelButtonClicked); + gridLayout->addWidget(cancelButton, rowIndex, 4); + + setLayout(gridLayout); +} + +void DomainBakeWidget::chooseFileButtonClicked() { + // pop a file dialog so the user can select the entities file + + // if we have picked an FBX before, start in the folder that matches the last path + // otherwise start in the home directory + auto startDir = _browseStartDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedFile = QFileDialog::getOpenFileName(this, "Choose Entities File", startDir, + "Entities File (*.json *.gz)"); + + if (!selectedFile.isEmpty()) { + // set the contents of the entities file text box to be the path to the selected file + _entitiesFileLineEdit->setText(selectedFile); + + auto directoryOfEntitiesFile = QFileInfo(selectedFile).absolutePath(); + + // save the directory containing this entities file so we can default to it next time we show the file dialog + _browseStartDirectory.set(directoryOfEntitiesFile); + } +} + +void DomainBakeWidget::chooseOutputDirButtonClicked() { + // pop a file dialog so the user can select the output directory + + // if we have a previously selected output directory, use that as the initial path in the choose dialog + // otherwise use the user's home directory + auto startDir = _exportDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir); + + if (!selectedDir.isEmpty()) { + // set the contents of the output directory text box to be the path to the directory + _outputDirLineEdit->setText(selectedDir); + } +} + +void DomainBakeWidget::outputDirectoryChanged(const QString& newDirectory) { + // update the export directory setting so we can re-use it next time + _exportDirectory.set(newDirectory); +} + +void DomainBakeWidget::bakeButtonClicked() { + + // save whatever the current domain name is in settings, we'll re-use it next time the widget is shown + _domainNameSetting.set(_domainNameLineEdit->text()); + + // save whatever the current destination path is in settings, we'll re-use it next time the widget is shown + _destinationPathSetting.set(_destinationPathLineEdit->text()); + + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + + if (!outputDirectory.exists()) { + return; + } + + // make sure we have a non empty URL to an entities file to bake + if (!_entitiesFileLineEdit->text().isEmpty()) { + // everything seems to be in place, kick off a bake for this entities file now + auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text()); + auto domainBaker = std::unique_ptr { + new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), + outputDirectory.absolutePath(), _destinationPathLineEdit->text()) + }; + + // make sure we hear from the baker when it is done + connect(domainBaker.get(), &DomainBaker::finished, this, &DomainBakeWidget::handleFinishedBaker); + + // watch the baker's progress so that we can put its progress in the results table + connect(domainBaker.get(), &DomainBaker::bakeProgress, this, &DomainBakeWidget::handleBakerProgress); + + // move the baker to the next available Oven worker thread + auto nextThread = qApp->getNextWorkerThread(); + domainBaker->moveToThread(nextThread); + + // kickoff the domain baker on its thread + QMetaObject::invokeMethod(domainBaker.get(), "bake"); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + auto resultsRowName = _domainNameLineEdit->text().isEmpty() ? fileToBakeURL.fileName() : _domainNameLineEdit->text(); + auto resultsRow = resultsWindow->addPendingResultRow(resultsRowName, outputDirectory); + + // keep the unique ptr to the domain baker and the index to the row representing it in the results table + _bakers.emplace_back(std::move(domainBaker), resultsRow); + } +} + +void DomainBakeWidget::handleBakerProgress(int baked, int total) { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + + // grab the results window, don't force it to be brought to the top + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(false); + + int percentage = roundf(float(baked) / float(total) * 100.0f); + + auto statusString = QString("Baking - %1 of %2 - %3%").arg(baked).arg(total).arg(percentage); + resultsWindow->changeStatusForRow(resultRow, statusString); + } + } +} + +void DomainBakeWidget::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + if (baker->hasErrors()) { + auto errors = baker->getErrors(); + errors.removeDuplicates(); + + resultsWindow->changeStatusForRow(resultRow, errors.join("\n")); + } else if (baker->hasWarnings()) { + auto warnings = baker->getWarnings(); + warnings.removeDuplicates(); + + resultsWindow->changeStatusForRow(resultRow, warnings.join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } + + // remove the DomainBaker now that it has completed + _bakers.erase(it); + } + } +} diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h new file mode 100644 index 0000000000..cd8c4a012e --- /dev/null +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -0,0 +1,54 @@ +// +// DomainBakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DomainBakeWidget_h +#define hifi_DomainBakeWidget_h + +#include + +#include + +#include "../DomainBaker.h" +#include "BakeWidget.h" + +class QLineEdit; + +class DomainBakeWidget : public BakeWidget { + Q_OBJECT + +public: + DomainBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + + void handleBakerProgress(int baked, int total); + void handleFinishedBaker(); + +private: + void setupUI(); + + QLineEdit* _domainNameLineEdit; + QLineEdit* _entitiesFileLineEdit; + QLineEdit* _outputDirLineEdit; + QLineEdit* _destinationPathLineEdit; + + Setting::Handle _domainNameSetting; + Setting::Handle _exportDirectory; + Setting::Handle _browseStartDirectory; + Setting::Handle _destinationPathSetting; +}; + +#endif // hifi_ModelBakeWidget_h diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp new file mode 100644 index 0000000000..c696fbad26 --- /dev/null +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -0,0 +1,227 @@ +// +// ModelBakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "ModelBakeWidget.h" + +static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory"; +static const auto MODEL_START_DIR_SETTING_KEY = "model_search_directory"; + +ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + BakeWidget(parent, flags), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) +{ + setupUI(); +} + +void ModelBakeWidget::setupUI() { + // setup a grid layout to hold everything + QGridLayout* gridLayout = new QGridLayout; + + int rowIndex = 0; + + // setup a section to choose the file being baked + QLabel* modelFileLabel = new QLabel("Model File(s)"); + + _modelLineEdit = new QLineEdit; + _modelLineEdit->setPlaceholderText("File or URL"); + + QPushButton* chooseFileButton = new QPushButton("Browse..."); + connect(chooseFileButton, &QPushButton::clicked, this, &ModelBakeWidget::chooseFileButtonClicked); + + // add the components for the model file picker to the layout + gridLayout->addWidget(modelFileLabel, rowIndex, 0); + gridLayout->addWidget(_modelLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseFileButton, rowIndex, 4); + + // start a new row for next component + ++rowIndex; + + // setup a section to choose the output directory + QLabel* outputDirectoryLabel = new QLabel("Output Directory"); + + _outputDirLineEdit = new QLineEdit; + + // set the current export directory to whatever was last used + _outputDirLineEdit->setText(_exportDirectory.get()); + + // whenever the output directory line edit changes, update the value in settings + connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &ModelBakeWidget::outputDirectoryChanged); + + QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse..."); + connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &ModelBakeWidget::chooseOutputDirButtonClicked); + + // add the components for the output directory picker to the layout + gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0); + gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4); + + // start a new row for the next component + ++rowIndex; + + // add a horizontal line to split the bake/cancel buttons off + QFrame* lineFrame = new QFrame; + lineFrame->setFrameShape(QFrame::HLine); + lineFrame->setFrameShadow(QFrame::Sunken); + gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a button that will kickoff the bake + QPushButton* bakeButton = new QPushButton("Bake"); + connect(bakeButton, &QPushButton::clicked, this, &ModelBakeWidget::bakeButtonClicked); + gridLayout->addWidget(bakeButton, rowIndex, 3); + + // add a cancel button to go back to the modes page + QPushButton* cancelButton = new QPushButton("Cancel"); + connect(cancelButton, &QPushButton::clicked, this, &ModelBakeWidget::cancelButtonClicked); + gridLayout->addWidget(cancelButton, rowIndex, 4); + + setLayout(gridLayout); +} + +void ModelBakeWidget::chooseFileButtonClicked() { + // pop a file dialog so the user can select the model file + + // if we have picked an FBX before, start in the folder that matches the last path + // otherwise start in the home directory + auto startDir = _modelStartDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx)"); + + if (!selectedFiles.isEmpty()) { + // set the contents of the model file text box to be the path to the selected file + _modelLineEdit->setText(selectedFiles.join(',')); + + auto directoryOfModel = QFileInfo(selectedFiles[0]).absolutePath(); + + if (_outputDirLineEdit->text().isEmpty()) { + // if our output directory is not yet set, set it to the directory of this model + _outputDirLineEdit->setText(directoryOfModel); + } + + // save the directory containing the file(s) so we can default to it next time we show the file dialog + _modelStartDirectory.set(directoryOfModel); + } +} + +void ModelBakeWidget::chooseOutputDirButtonClicked() { + // pop a file dialog so the user can select the output directory + + // if we have a previously selected output directory, use that as the initial path in the choose dialog + // otherwise use the user's home directory + auto startDir = _exportDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir); + + if (!selectedDir.isEmpty()) { + // set the contents of the output directory text box to be the path to the directory + _outputDirLineEdit->setText(selectedDir); + } +} + +void ModelBakeWidget::outputDirectoryChanged(const QString& newDirectory) { + // update the export directory setting so we can re-use it next time + _exportDirectory.set(newDirectory); +} + +void ModelBakeWidget::bakeButtonClicked() { + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + + if (!outputDirectory.exists()) { + return; + } + + // make sure we have a non empty URL to a model to bake + if (_modelLineEdit->text().isEmpty()) { + return; + } + + // split the list from the model line edit to see how many models we need to bake + auto fileURLStrings = _modelLineEdit->text().split(','); + foreach (QString fileURLString, fileURLStrings) { + // construct a URL from the path in the model file text box + QUrl modelToBakeURL(fileURLString); + + // if the URL doesn't have a scheme, assume it is a local file + if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") { + modelToBakeURL.setScheme("file"); + } + + // everything seems to be in place, kick off a bake for this model now + auto baker = std::unique_ptr { + new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), []() -> QThread* { + return qApp->getNextWorkerThread(); + }, false) + }; + + // move the baker to the FBX baker thread + baker->moveToThread(qApp->getFBXBakerThread()); + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(baker.get(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(baker.get(), &FBXBaker::finished, this, &ModelBakeWidget::handleFinishedBaker); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory); + + // keep a unique_ptr to this baker + // and remember the row that represents it in the results table + _bakers.emplace_back(std::move(baker), resultsRow); + } +} + +void ModelBakeWidget::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + if (baker->hasErrors()) { + resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } + + _bakers.erase(it); + } + } +} diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h new file mode 100644 index 0000000000..ed08990ba5 --- /dev/null +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -0,0 +1,51 @@ +// +// ModelBakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ModelBakeWidget_h +#define hifi_ModelBakeWidget_h + +#include + +#include + +#include "../FBXBaker.h" + +#include "BakeWidget.h" + +class QLineEdit; +class QThread; + +class ModelBakeWidget : public BakeWidget { + Q_OBJECT + +public: + ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + + void handleFinishedBaker(); + +private: + void setupUI(); + + QLineEdit* _modelLineEdit; + QLineEdit* _outputDirLineEdit; + + Setting::Handle _exportDirectory; + Setting::Handle _modelStartDirectory; +}; + +#endif // hifi_ModelBakeWidget_h diff --git a/tools/oven/src/ui/ModesWidget.cpp b/tools/oven/src/ui/ModesWidget.cpp new file mode 100644 index 0000000000..624aa949cc --- /dev/null +++ b/tools/oven/src/ui/ModesWidget.cpp @@ -0,0 +1,69 @@ +// +// ModesWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/7/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include + +#include "DomainBakeWidget.h" +#include "ModelBakeWidget.h" +#include "SkyboxBakeWidget.h" + +#include "ModesWidget.h" + +ModesWidget::ModesWidget(QWidget* parent, Qt::WindowFlags flags) : + QWidget(parent, flags) +{ + setupUI(); +} + +void ModesWidget::setupUI() { + // setup a horizontal box layout to hold our mode buttons + QHBoxLayout* horizontalLayout = new QHBoxLayout; + + // add a button for domain baking + QPushButton* domainButton = new QPushButton("Bake Domain"); + connect(domainButton, &QPushButton::clicked, this, &ModesWidget::showDomainBakingWidget); + horizontalLayout->addWidget(domainButton); + + // add a button for model baking + QPushButton* modelsButton = new QPushButton("Bake Models"); + connect(modelsButton, &QPushButton::clicked, this, &ModesWidget::showModelBakingWidget); + horizontalLayout->addWidget(modelsButton); + + // add a button for skybox baking + QPushButton* skyboxButton = new QPushButton("Bake Skyboxes"); + connect(skyboxButton, &QPushButton::clicked, this, &ModesWidget::showSkyboxBakingWidget); + horizontalLayout->addWidget(skyboxButton); + + setLayout(horizontalLayout); +} + +void ModesWidget::showModelBakingWidget() { + auto stackedWidget = qobject_cast(parentWidget()); + + // add a new widget for model baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new ModelBakeWidget)); +} + +void ModesWidget::showDomainBakingWidget() { + auto stackedWidget = qobject_cast(parentWidget()); + + // add a new widget for domain baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new DomainBakeWidget)); +} + +void ModesWidget::showSkyboxBakingWidget() { + auto stackedWidget = qobject_cast(parentWidget()); + + // add a new widget for skybox baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new SkyboxBakeWidget)); +} diff --git a/tools/oven/src/ui/ModesWidget.h b/tools/oven/src/ui/ModesWidget.h new file mode 100644 index 0000000000..fd660923f2 --- /dev/null +++ b/tools/oven/src/ui/ModesWidget.h @@ -0,0 +1,31 @@ +// +// ModesWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/7/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ModesWidget_h +#define hifi_ModesWidget_h + +#include + +class ModesWidget : public QWidget { + Q_OBJECT +public: + ModesWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void showModelBakingWidget(); + void showDomainBakingWidget(); + void showSkyboxBakingWidget(); + +private: + void setupUI(); +}; + +#endif // hifi_ModesWidget_h diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp new file mode 100644 index 0000000000..dd40fb1f8f --- /dev/null +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -0,0 +1,61 @@ +// +// OvenMainWindow.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "ModesWidget.h" + +#include "OvenMainWindow.h" + +OvenMainWindow::OvenMainWindow(QWidget *parent, Qt::WindowFlags flags) : + QMainWindow(parent, flags) +{ + setWindowTitle("High Fidelity Oven"); + + // give the window a fixed width that will never change + setFixedWidth(FIXED_WINDOW_WIDTH); + + // setup a stacked layout for the main "modes" menu and subseq + QStackedWidget* stackedWidget = new QStackedWidget(this); + stackedWidget->addWidget(new ModesWidget); + + setCentralWidget(stackedWidget); +} + +OvenMainWindow::~OvenMainWindow() { + if (_resultsWindow) { + _resultsWindow->close(); + _resultsWindow->deleteLater(); + } +} + +ResultsWindow* OvenMainWindow::showResultsWindow(bool shouldRaise) { + if (!_resultsWindow) { + // we don't have a results window right now, so make a new one + _resultsWindow = new ResultsWindow; + + // even though we're about to show the results window, we do it here so that the move below works + _resultsWindow->show(); + + // place the results window initially below our window + _resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom()); + } + + // show the results window and make sure it is in front + _resultsWindow->show(); + + if (shouldRaise) { + _resultsWindow->raise(); + } + + // return a pointer to the results window the caller can use + return _resultsWindow; +} diff --git a/tools/oven/src/ui/OvenMainWindow.h b/tools/oven/src/ui/OvenMainWindow.h new file mode 100644 index 0000000000..a557d5e8dd --- /dev/null +++ b/tools/oven/src/ui/OvenMainWindow.h @@ -0,0 +1,34 @@ +// +// OvenMainWindow.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_OvenMainWindow_h +#define hifi_OvenMainWindow_h + +#include +#include + +#include "ResultsWindow.h" + +const int FIXED_WINDOW_WIDTH = 640; + +class OvenMainWindow : public QMainWindow { + Q_OBJECT +public: + OvenMainWindow(QWidget *parent = Q_NULLPTR, Qt::WindowFlags flags = Qt::WindowFlags()); + ~OvenMainWindow(); + + ResultsWindow* showResultsWindow(bool shouldRaise = true); + +private: + QPointer _resultsWindow; +}; + +#endif // hifi_OvenMainWindow_h diff --git a/tools/oven/src/ui/ResultsWindow.cpp b/tools/oven/src/ui/ResultsWindow.cpp new file mode 100644 index 0000000000..35b5160f9b --- /dev/null +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -0,0 +1,100 @@ +// +// ResultsWindow.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include + +#include "OvenMainWindow.h" + +#include "ResultsWindow.h" + +ResultsWindow::ResultsWindow(QWidget* parent) : + QWidget(parent) +{ + // add a title to this window to identify it + setWindowTitle("High Fidelity Oven - Bake Results"); + + // give this dialog the same starting width as the main application window + resize(FIXED_WINDOW_WIDTH, size().height()); + + // have the window delete itself when closed + setAttribute(Qt::WA_DeleteOnClose); + + setupUI(); +} + +void ResultsWindow::setupUI() { + QVBoxLayout* resultsLayout = new QVBoxLayout(this); + + // add a results table to the widget + _resultsTable = new QTableWidget(0, 2, this); + + // add the header to the table widget + _resultsTable->setHorizontalHeaderLabels({"File", "Status"}); + + // add that table widget to the vertical box layout, so we can make it stretch to the size of the parent + resultsLayout->insertWidget(0, _resultsTable); + + // make the filename column hold 25% of the total width + // strech the last column of the table (that holds the results) to fill up the remaining available size + _resultsTable->horizontalHeader()->resizeSection(0, 0.25 * FIXED_WINDOW_WIDTH); + _resultsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + // make sure we hear about cell clicks so that we can show the output directory for the given row + connect(_resultsTable, &QTableWidget::cellClicked, this, &ResultsWindow::handleCellClicked); + + // set the layout of this widget to the created layout + setLayout(resultsLayout); +} + +void ResultsWindow::handleCellClicked(int rowIndex, int columnIndex) { + // make sure this click was on the file/domain being baked + if (columnIndex == 0) { + // use QDesktopServices to show the output directory for this row + auto directory = _outputDirectories[rowIndex]; + QDesktopServices::openUrl(QUrl::fromLocalFile(directory.absolutePath())); + } +} + +int ResultsWindow::addPendingResultRow(const QString& fileName, const QDir& outputDirectory) { + int rowIndex = _resultsTable->rowCount(); + + _resultsTable->insertRow(rowIndex); + + // add a new item for the filename, make it non-editable + auto fileNameItem = new QTableWidgetItem(fileName); + fileNameItem->setFlags(fileNameItem->flags() & ~Qt::ItemIsEditable); + _resultsTable->setItem(rowIndex, 0, fileNameItem); + + auto statusItem = new QTableWidgetItem("Baking..."); + statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable); + _resultsTable->setItem(rowIndex, 1, statusItem); + + // push an output directory to our list so we can show it if the user clicks on this bake in the results table + _outputDirectories.push_back(outputDirectory); + + return rowIndex; +} + +void ResultsWindow::changeStatusForRow(int rowIndex, const QString& result) { + const int STATUS_COLUMN = 1; + auto statusItem = new QTableWidgetItem(result); + statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable); + _resultsTable->setItem(rowIndex, STATUS_COLUMN, statusItem); + + // resize the row for the new contents + _resultsTable->resizeRowToContents(rowIndex); + // reszie the column for the new contents + _resultsTable->resizeColumnToContents(STATUS_COLUMN); +} diff --git a/tools/oven/src/ui/ResultsWindow.h b/tools/oven/src/ui/ResultsWindow.h new file mode 100644 index 0000000000..ae7bb0e327 --- /dev/null +++ b/tools/oven/src/ui/ResultsWindow.h @@ -0,0 +1,39 @@ +// +// ResultsWindow.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ResultsWindow_h +#define hifi_ResultsWindow_h + +#include +#include + +class QTableWidget; + +class ResultsWindow : public QWidget { + Q_OBJECT + +public: + ResultsWindow(QWidget* parent = nullptr); + + void setupUI(); + + int addPendingResultRow(const QString& fileName, const QDir& outputDirectory); + void changeStatusForRow(int rowIndex, const QString& result); + +private slots: + void handleCellClicked(int rowIndex, int columnIndex); + +private: + QTableWidget* _resultsTable { nullptr }; + QList _outputDirectories; +}; + +#endif // hifi_ResultsWindow_h diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp new file mode 100644 index 0000000000..d5c280aebd --- /dev/null +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -0,0 +1,223 @@ +// +// SkyboxBakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "SkyboxBakeWidget.h" + +static const auto EXPORT_DIR_SETTING_KEY = "skybox_export_directory"; +static const auto SELECTION_START_DIR_SETTING_KEY = "skybox_search_directory"; + +SkyboxBakeWidget::SkyboxBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + BakeWidget(parent, flags), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _selectionStartDirectory(SELECTION_START_DIR_SETTING_KEY) +{ + setupUI(); +} + +void SkyboxBakeWidget::setupUI() { + // setup a grid layout to hold everything + QGridLayout* gridLayout = new QGridLayout; + + int rowIndex = 0; + + // setup a section to choose the file being baked + QLabel* skyboxFileLabel = new QLabel("Skybox File(s)"); + + _selectionLineEdit = new QLineEdit; + _selectionLineEdit->setPlaceholderText("File or URL"); + + QPushButton* chooseFileButton = new QPushButton("Browse..."); + connect(chooseFileButton, &QPushButton::clicked, this, &SkyboxBakeWidget::chooseFileButtonClicked); + + // add the components for the skybox file picker to the layout + gridLayout->addWidget(skyboxFileLabel, rowIndex, 0); + gridLayout->addWidget(_selectionLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseFileButton, rowIndex, 4); + + // start a new row for next component + ++rowIndex; + + // setup a section to choose the output directory + QLabel* outputDirectoryLabel = new QLabel("Output Directory"); + + _outputDirLineEdit = new QLineEdit; + + // set the current export directory to whatever was last used + _outputDirLineEdit->setText(_exportDirectory.get()); + + // whenever the output directory line edit changes, update the value in settings + connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &SkyboxBakeWidget::outputDirectoryChanged); + + QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse..."); + connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &SkyboxBakeWidget::chooseOutputDirButtonClicked); + + // add the components for the output directory picker to the layout + gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0); + gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4); + + // start a new row for the next component + ++rowIndex; + + // add a horizontal line to split the bake/cancel buttons off + QFrame* lineFrame = new QFrame; + lineFrame->setFrameShape(QFrame::HLine); + lineFrame->setFrameShadow(QFrame::Sunken); + gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a button that will kickoff the bake + QPushButton* bakeButton = new QPushButton("Bake"); + connect(bakeButton, &QPushButton::clicked, this, &SkyboxBakeWidget::bakeButtonClicked); + gridLayout->addWidget(bakeButton, rowIndex, 3); + + // add a cancel button to go back to the modes page + QPushButton* cancelButton = new QPushButton("Cancel"); + connect(cancelButton, &QPushButton::clicked, this, &SkyboxBakeWidget::cancelButtonClicked); + gridLayout->addWidget(cancelButton, rowIndex, 4); + + setLayout(gridLayout); +} + +void SkyboxBakeWidget::chooseFileButtonClicked() { + // pop a file dialog so the user can select the skybox file(s) + + // if we have picked a skybox before, start in the folder that matches the last path + // otherwise start in the home directory + auto startDir = _selectionStartDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Skybox", startDir); + + if (!selectedFiles.isEmpty()) { + // set the contents of the file select text box to be the path to the selected file + _selectionLineEdit->setText(selectedFiles.join(',')); + + if (_outputDirLineEdit->text().isEmpty()) { + auto directoryOfSkybox = QFileInfo(selectedFiles[0]).absolutePath(); + + // if our output directory is not yet set, set it to the directory of this skybox + _outputDirLineEdit->setText(directoryOfSkybox); + } + } +} + +void SkyboxBakeWidget::chooseOutputDirButtonClicked() { + // pop a file dialog so the user can select the output directory + + // if we have a previously selected output directory, use that as the initial path in the choose dialog + // otherwise use the user's home directory + auto startDir = _exportDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir); + + if (!selectedDir.isEmpty()) { + // set the contents of the output directory text box to be the path to the directory + _outputDirLineEdit->setText(selectedDir); + } +} + +void SkyboxBakeWidget::outputDirectoryChanged(const QString& newDirectory) { + // update the export directory setting so we can re-use it next time + _exportDirectory.set(newDirectory); +} + +void SkyboxBakeWidget::bakeButtonClicked() { + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + + if (!outputDirectory.exists()) { + return; + } + + // make sure we have a non empty URL to a skybox to bake + if (_selectionLineEdit->text().isEmpty()) { + return; + } + + // split the list from the selection line edit to see how many skyboxes we need to bake + auto fileURLStrings = _selectionLineEdit->text().split(','); + foreach (QString fileURLString, fileURLStrings) { + // construct a URL from the path in the skybox file text box + QUrl skyboxToBakeURL(fileURLString); + + // if the URL doesn't have a scheme, assume it is a local file + if (skyboxToBakeURL.scheme() != "http" && skyboxToBakeURL.scheme() != "https" && skyboxToBakeURL.scheme() != "ftp") { + skyboxToBakeURL.setScheme("file"); + } + + // everything seems to be in place, kick off a bake for this skybox now + auto baker = std::unique_ptr { + new TextureBaker(skyboxToBakeURL, image::TextureUsage::CUBE_TEXTURE, outputDirectory.absolutePath()) + }; + + // move the baker to a worker thread + baker->moveToThread(qApp->getNextWorkerThread()); + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(baker.get(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(baker.get(), &TextureBaker::finished, this, &SkyboxBakeWidget::handleFinishedBaker); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + auto resultsRow = resultsWindow->addPendingResultRow(skyboxToBakeURL.fileName(), outputDirectory); + + // keep a unique_ptr to this baker + // and remember the row that represents it in the results table + _bakers.emplace_back(std::move(baker), resultsRow); + } +} + +void SkyboxBakeWidget::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + if (baker->hasErrors()) { + resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } + + // drop our strong pointer to the baker now that we are done with it + _bakers.erase(it); + } + } +} diff --git a/tools/oven/src/ui/SkyboxBakeWidget.h b/tools/oven/src/ui/SkyboxBakeWidget.h new file mode 100644 index 0000000000..4063a5459b --- /dev/null +++ b/tools/oven/src/ui/SkyboxBakeWidget.h @@ -0,0 +1,50 @@ +// +// SkyboxBakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SkyboxBakeWidget_h +#define hifi_SkyboxBakeWidget_h + +#include + +#include + +#include "../TextureBaker.h" + +#include "BakeWidget.h" + +class QLineEdit; + +class SkyboxBakeWidget : public BakeWidget { + Q_OBJECT + +public: + SkyboxBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + + void handleFinishedBaker(); + +private: + void setupUI(); + + QLineEdit* _selectionLineEdit; + QLineEdit* _outputDirLineEdit; + + Setting::Handle _exportDirectory; + Setting::Handle _selectionStartDirectory; +}; + +#endif // hifi_SkyboxBakeWidget_h diff --git a/tutorial/ACAudioSearchAndInject_tutorial.js b/tutorial/ACAudioSearchAndInject_tutorial.js index 70e936bb1c..5e2998ff1e 100644 --- a/tutorial/ACAudioSearchAndInject_tutorial.js +++ b/tutorial/ACAudioSearchAndInject_tutorial.js @@ -1,4 +1,5 @@ "use strict"; + /*jslint nomen: true, plusplus: true, vars: true*/ /*global AvatarList, Entities, EntityViewer, Script, SoundCache, Audio, print, randFloat*/ // @@ -38,19 +39,27 @@ var DEFAULT_SOUND_DATA = { playbackGapRange: 0 // in ms }; +//var AGENT_AVATAR_POSITION = { x: -1.5327, y: 0.672515, z: 5.91573 }; +var AGENT_AVATAR_POSITION = { x: -2.83785, y: 1.45243, z: -13.6042 }; + //var isACScript = this.EntityViewer !== undefined; var isACScript = true; -Script.include("http://hifi-content.s3.amazonaws.com/ryan/development/utils_ryan.js"); if (isACScript) { Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList. Avatar.skeletonModelURL = "http://hifi-content.s3.amazonaws.com/ozan/dev/avatars/invisible_avatar/invisible_avatar.fst"; + Avatar.position = AGENT_AVATAR_POSITION; + Agent.isListeningToAudioStream = true; } function ignore() {} function debug() { // Display the arguments not just [Object object]. //print.apply(null, [].map.call(arguments, JSON.stringify)); } +function randFloat(low, high) { + return low + Math.random() * (high - low); +} + if (isACScript) { EntityViewer.setCenterRadius(QUERY_RADIUS); } @@ -93,6 +102,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n return; } var properties, soundData; // Latest data, pulled from local octree. + // getEntityProperties locks the tree, which competes with the asynchronous processing of queryOctree results. // Most entity updates are fast and only a very few do getEntityProperties. function ensureSoundData() { // We only getEntityProperities when we need to. @@ -115,43 +125,54 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n } } } + // Stumbling on big new pile of entities will do a lot of getEntityProperties. Once. if (that.lastUserDataUpdate < userDataCutoff) { // NO DATA => SOUND DATA ensureSoundData(); } + if (!that.url) { // NO DATA => NO DATA return that.stop(); } + if (!that.sound) { // SOUND DATA => DOWNLOADING that.sound = SoundCache.getSound(soundData.url); // SoundCache can manage duplicates better than we can. } + if (!that.sound.downloaded) { // DOWNLOADING => DOWNLOADING return; } + if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING return; } + ensureSoundData(); // We'll try to play/setOptions and will need position, so we might as well get soundData, too. if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around) return that.stop(); } + var options = { position: properties.position, loop: soundData.loop || DEFAULT_SOUND_DATA.loop, volume: soundData.volume || DEFAULT_SOUND_DATA.volume }; + function repeat() { return !options.loop && (soundData.playbackGap >= 0); } + function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME; return now + base + randFloat(-Math.min(base, range), range); } + if (that.injector && soundData.playing === false) { that.injector.stop(); that.injector = null; } + if (!that.injector) { if (soundData.playing === false) { // WAITING => PLAYING | WAITING return; @@ -165,6 +186,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.playing) { // Subtle: a looping sound will not check playbackGap. if (repeat()) { // WAITING => PLAYING @@ -178,6 +200,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n } }; } + function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) { ignore(avatarPosition, avatar); // We could use avatars and/or avatarPositions to prioritize which ones to play. var entitySound = entityCache[entityIdentifier]; @@ -186,7 +209,9 @@ function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) } entitySound.timestamp = timestamp; // Might be updated for multiple avatars. That's fine. } + var nUpdates = UPDATES_PER_STATS_LOG, lastStats = Date.now(); + function updateAllEntityData() { // A fast update of all entities we know about. A few make sounds. var now = Date.now(), expirationCutoff = now - EXPIRATION_TIME, diff --git a/tutorial/Changelog.md b/tutorial/Changelog.md new file mode 100644 index 0000000000..bd923b6841 --- /dev/null +++ b/tutorial/Changelog.md @@ -0,0 +1,3 @@ + * home-tutorial-34 + * Update tutorial to only start if `HMD.active` + * Update builder's grid to use "Good - Sub-meshes" for collision shape type diff --git a/unpublishedScripts/interaction/Interaction.js b/unpublishedScripts/interaction/Interaction.js new file mode 100644 index 0000000000..bb763c01e7 --- /dev/null +++ b/unpublishedScripts/interaction/Interaction.js @@ -0,0 +1,179 @@ +// +// Interaction.js +// scripts/interaction +// +// Created by Trevor Berninger on 3/20/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function(){ + print("loading interaction script"); + + var Avatar = false; + var NPC = false; + var previousNPC = false; + var hasCenteredOnNPC = false; + var distance = 10; + var r = 8; + var player = false; + + var baselineX = 0; + var baselineY = 0; + var nodRange = 20; + var shakeRange = 20; + + var ticker = false; + var heartbeatTimer = false; + + function callOnNPC(message) { + if(NPC) + Messages.sendMessage("interactionComs", NPC + ":" + message); + else + Messages.sendMessage("interactionComs", previousNPC + ":" + message); + } + + LimitlessSpeechRecognition.onFinishedSpeaking.connect(function(speech) { + print("Got: " + speech); + callOnNPC("voiceData:" + speech); + }); + + LimitlessSpeechRecognition.onReceivedTranscription.connect(function(speech) { + callOnNPC("speaking"); + }); + + function setBaselineRotations(rot) { + baselineX = rot.x; + baselineY = rot.y; + } + + function findLookedAtNPC() { + var intersection = AvatarList.findRayIntersection({origin: MyAvatar.position, direction: Quat.getFront(Camera.getOrientation())}, true); + if (intersection.intersects && intersection.distance <= distance){ + var npcAvatar = AvatarList.getAvatar(intersection.avatarID); + if (npcAvatar.displayName.search("NPC") != -1) { + setBaselineRotations(Quat.safeEulerAngles(Camera.getOrientation())); + return intersection.avatarID; + } + } + return false; + } + + function isStillFocusedNPC() { + var avatar = AvatarList.getAvatar(NPC); + if (avatar) { + var avatarPosition = avatar.position; + return Vec3.distance(MyAvatar.position, avatarPosition) <= distance && Math.abs(Quat.dot(Camera.getOrientation(), Quat.lookAtSimple(MyAvatar.position, avatarPosition))) > 0.6; + } + return false; // NPC reference died. Maybe it crashed or we teleported to a new world? + } + + function onWeLostFocus() { + print("lost NPC: " + NPC); + callOnNPC("onLostFocused"); + var baselineX = 0; + var baselineY = 0; + } + + function onWeGainedFocus() { + print("found NPC: " + NPC); + callOnNPC("onFocused"); + var rotation = Quat.safeEulerAngles(Camera.getOrientation()); + baselineX = rotation.x; + baselineY = rotation.y; + LimitlessSpeechRecognition.setListeningToVoice(true); + } + + function checkFocus() { + var newNPC = findLookedAtNPC(); + + if (NPC && newNPC != NPC && !isStillFocusedNPC()) { + onWeLostFocus(); + previousNPC = NPC; + NPC = false; + } + if (!NPC && newNPC != false) { + NPC = newNPC; + onWeGainedFocus(); + } + } + + function checkGesture() { + var rotation = Quat.safeEulerAngles(Camera.getOrientation()); + + var deltaX = Math.abs(rotation.x - baselineX); + if (deltaX > 180) { + deltaX -= 180; + } + var deltaY = Math.abs(rotation.y - baselineY); + if (deltaY > 180) { + deltaY -= 180; + } + + if (deltaX >= nodRange && deltaY <= shakeRange) { + callOnNPC("onNodReceived"); + } else if (deltaY >= shakeRange && deltaX <= nodRange) { + callOnNPC("onShakeReceived"); + } + } + + function tick() { + checkFocus(); + if (NPC) { + checkGesture(); + } + } + + function heartbeat() { + callOnNPC("beat"); + } + + Messages.subscribe("interactionComs"); + + Messages.messageReceived.connect(function (channel, message, sender) { + if(channel === "interactionComs" && player) { + var codeIndex = message.search('clientexec'); + if(codeIndex != -1) { + var code = message.substr(codeIndex+11); + Script.evaluate(code, ''); + } + } + }); + + this.enterEntity = function(id) { + player = true; + print("Something entered me: " + id); + LimitlessSpeechRecognition.setAuthKey("testKey"); + if (!ticker) { + ticker = Script.setInterval(tick, 333); + } + if(!heartbeatTimer) { + heartbeatTimer = Script.setInterval(heartbeat, 1000); + } + }; + this.leaveEntity = function(id) { + LimitlessSpeechRecognition.setListeningToVoice(false); + player = false; + print("Something left me: " + id); + if (previousNPC) + Messages.sendMessage("interactionComs", previousNPC + ":leftArea"); + if (ticker) { + ticker.stop(); + ticker = false; + } + if (heartbeatTimer) { + heartbeatTimer.stop(); + heartbeatTimer = false; + } + }; + this.unload = function() { + print("Okay. I'm Unloading!"); + if (ticker) { + ticker.stop(); + ticker = false; + } + }; + print("finished loading interaction script"); +}); diff --git a/unpublishedScripts/interaction/NPCHelpers.js b/unpublishedScripts/interaction/NPCHelpers.js new file mode 100644 index 0000000000..188178b281 --- /dev/null +++ b/unpublishedScripts/interaction/NPCHelpers.js @@ -0,0 +1,179 @@ +// +// NPCHelpers.js +// scripts/interaction +// +// Created by Trevor Berninger on 3/20/17. +// Copyright 2017 High Fidelity Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var audioInjector = false; +var blocked = false; +var playingResponseAnim = false; +var storyURL = ""; +var _qid = "start"; + +print("TESTTEST"); + +function strContains(str, sub) { + return str.search(sub) != -1; +} + +function callbackOnCondition(conditionFunc, ms, callback, count) { + var thisCount = 0; + if (typeof count !== 'undefined') { + thisCount = count; + } + if (conditionFunc()) { + callback(); + } else if (thisCount < 10) { + Script.setTimeout(function() { + callbackOnCondition(conditionFunc, ms, callback, thisCount + 1); + }, ms); + } else { + print("callbackOnCondition timeout"); + } +} + +function playAnim(animURL, looping, onFinished) { + print("got anim: " + animURL); + print("looping: " + looping); + // Start caching the animation if not already cached. + AnimationCache.getAnimation(animURL); + + // Tell the avatar to animate so that we can tell if the animation is ready without crashing + Avatar.startAnimation(animURL, 30, 1, false, false, 0, 1); + + // Continually check if the animation is ready + callbackOnCondition(function(){ + var details = Avatar.getAnimationDetails(); + // if we are running the request animation and are past the first frame, the anim is loaded properly + print("running: " + details.running); + print("url and animURL: " + details.url.trim().replace(/ /g, "%20") + " | " + animURL.trim().replace(/ /g, "%20")); + print("currentFrame: " + details.currentFrame); + return details.running && details.url.trim().replace(/ /g, "%20") == animURL.trim().replace(/ /g, "%20") && details.currentFrame > 0; + }, 250, function(){ + var timeOfAnim = ((AnimationCache.getAnimation(animURL).frames.length / 30) * 1000) + 100; // frames to miliseconds plus a small buffer + print("animation loaded. length: " + timeOfAnim); + // Start the animation again but this time with frame information + Avatar.startAnimation(animURL, 30, 1, looping, true, 0, AnimationCache.getAnimation(animURL).frames.length); + if (typeof onFinished !== 'undefined') { + print("onFinished defined. setting the timeout with timeOfAnim"); + timers.push(Script.setTimeout(onFinished, timeOfAnim)); + } + }); +} + +function playSound(soundURL, onFinished) { + callbackOnCondition(function() { + return SoundCache.getSound(soundURL).downloaded; + }, 250, function() { + if (audioInjector) { + audioInjector.stop(); + } + audioInjector = Audio.playSound(SoundCache.getSound(soundURL), {position: Avatar.position, volume: 1.0}); + if (typeof onFinished !== 'undefined') { + audioInjector.finished.connect(onFinished); + } + }); +} + +function npcRespond(soundURL, animURL, onFinished) { + if (typeof soundURL !== 'undefined' && soundURL != '') { + print("npcRespond got soundURL!"); + playSound(soundURL, function(){ + print("sound finished"); + var animDetails = Avatar.getAnimationDetails(); + print("animDetails.lastFrame: " + animDetails.lastFrame); + print("animDetails.currentFrame: " + animDetails.currentFrame); + if (animDetails.lastFrame < animDetails.currentFrame + 1 || !playingResponseAnim) { + onFinished(); + } + audioInjector = false; + }); + } + if (typeof animURL !== 'undefined' && animURL != '') { + print("npcRespond got animURL!"); + playingResponseAnim = true; + playAnim(animURL, false, function() { + print("anim finished"); + playingResponseAnim = false; + print("injector: " + audioInjector); + if (!audioInjector || !audioInjector.isPlaying()) { + print("resetting Timer"); + print("about to call onFinished"); + onFinished(); + } + }); + } +} + +function npcRespondBlocking(soundURL, animURL, onFinished) { + print("blocking response requested"); + if (!blocked) { + print("not already blocked"); + blocked = true; + npcRespond(soundURL, animURL, function(){ + if (onFinished){ + onFinished(); + }blocked = false; + }); + } +} + +function npcContinueStory(soundURL, animURL, nextID, onFinished) { + if (!nextID) { + nextID = _qid; + } + npcRespondBlocking(soundURL, animURL, function(){ + if (onFinished){ + onFinished(); + }setQid(nextID); + }); +} + +function setQid(newQid) { + print("setting quid"); + print("_qid: " + _qid); + _qid = newQid; + print("_qid: " + _qid); + doActionFromServer("init"); +} + +function runOnClient(code) { + Messages.sendMessage("interactionComs", "clientexec:" + code); +} + +function doActionFromServer(action, data, useServerCache) { + if (action == "start") { + ignoreCount = 0; + _qid = "start"; + } + var xhr = new XMLHttpRequest(); + xhr.open("POST", "http://gserv_devel.studiolimitless.com/story", true); + xhr.onreadystatechange = function(){ + if (xhr.readyState == 4){ + if (xhr.status == 200) { + print("200!"); + print("evaluating: " + xhr.responseText); + Script.evaluate(xhr.responseText, ""); + } else if (xhr.status == 444) { + print("Limitless Serv 444: API error: " + xhr.responseText); + } else { + print("HTTP Code: " + xhr.status + ": " + xhr.responseText); + } + } + }; + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + var postData = "url=" + storyURL + "&action=" + action + "&qid=" + _qid; + if (typeof data !== 'undefined' && data != '') { + postData += "&data=" + data; + } + if (typeof useServerCache !== 'undefined' && !useServerCache) { + postData += "&nocache=true"; + } + print("Sending: " + postData); + xhr.send(postData); +} diff --git a/unpublishedScripts/interaction/NPC_AC.js b/unpublishedScripts/interaction/NPC_AC.js new file mode 100644 index 0000000000..eb2d9f4caf --- /dev/null +++ b/unpublishedScripts/interaction/NPC_AC.js @@ -0,0 +1,102 @@ +// +// NPC_AC.js +// scripts/interaction +// +// Created by Trevor Berninger on 3/20/17. +// Copyright 2017 High Fidelity Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var currentlyUsedIndices = []; +var timers = []; +var currentlyEngaged = false; +var questionNumber = 0; +var heartbeatTimeout = false; +function getRandomRiddle() { + var randIndex = null; + do { + randIndex = Math.floor(Math.random() * 15) + 1; + } while (randIndex in currentlyUsedIndices); + + currentlyUsedIndices.push(randIndex); + return randIndex.toString(); +} + +Script.include("https://raw.githubusercontent.com/Delamare2112/hifi/Interaction/unpublishedScripts/interaction/NPCHelpers.js", function(){ + print("NPCHelpers included.");main(); +}); + +var idleAnim = "https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/idle.fbx"; +var FST = "https://s3.amazonaws.com/hifi-public/tony/fixed-sphinx/sphinx.fst"; + +Agent.isAvatar = true; +Avatar.skeletonModelURL = FST; +Avatar.displayName = "NPC"; +Avatar.position = {x: 0.3, y: -23.4, z: 8.0}; +Avatar.orientation = {x: 0, y: 1, z: 0, w: 0}; +// Avatar.position = {x: 1340.3555, y: 4.078, z: -420.1562}; +// Avatar.orientation = {x: 0, y: -0.707, z: 0, w: 0.707}; +Avatar.scale = 2; + +Messages.subscribe("interactionComs"); + +function endInteraction() { + print("ending interaction"); + blocked = false; + currentlyEngaged = false; + if(audioInjector) + audioInjector.stop(); + for (var t in timers) { + Script.clearTimeout(timers[t]); + } + if(_qid != "Restarting") { + npcRespondBlocking( + 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/EarlyExit_0' + (Math.floor(Math.random() * 2) + 1).toString() + '.wav', + 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/reversedSphinx.fbx', + function(){ + Avatar.startAnimation('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx', 0); + } + ); + } +} + +function main() { + storyURL = "https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Sphinx.json"; + Messages.messageReceived.connect(function (channel, message, sender) { + if(!strContains(message, 'beat')) + print(sender + " -> NPC @" + Agent.sessionUUID + ": " + message); + if (channel === "interactionComs" && strContains(message, Agent.sessionUUID)) { + if (strContains(message, 'beat')) { + if(heartbeatTimeout) { + Script.clearTimeout(heartbeatTimeout); + heartbeatTimeout = false; + } + heartbeatTimeout = Script.setTimeout(endInteraction, 1500); + } + else if (strContains(message, "onFocused") && !currentlyEngaged) { + blocked = false; + currentlyEngaged = true; + currentlyUsedIndices = []; + doActionFromServer("start"); + } else if (strContains(message, "leftArea")) { + + } else if (strContains(message, "speaking")) { + + } else { + var voiceDataIndex = message.search("voiceData"); + if (voiceDataIndex != -1) { + var words = message.substr(voiceDataIndex+10); + if (!isNaN(_qid) && (strContains(words, "repeat") || (strContains(words, "say") && strContains(words, "again")))) { + doActionFromServer("init"); + } else { + doActionFromServer("words", words); + } + } + } + } + }); + // Script.update.connect(updateGem); + Avatar.startAnimation("https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx", 0); +} diff --git a/unpublishedScripts/interaction/Sphinx.json b/unpublishedScripts/interaction/Sphinx.json new file mode 100644 index 0000000000..2a76417fd7 --- /dev/null +++ b/unpublishedScripts/interaction/Sphinx.json @@ -0,0 +1,159 @@ +{ + "Name": "10 Questions", + "Defaults": + { + "Actions": + { + "positive": "var x=function(){if(questionNumber>=2){setQid('Finished');return;}var suffix=['A', 'B'][questionNumber++] + '_0' + (Math.floor(Math.random() * 2) + 2).toString() + '.wav';npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/RightAnswer'+suffix, 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/RightAnswerB_02.fbx', getRandomRiddle());};x();", + "unknown": "var suffix=(Math.floor(Math.random() * 3) + 1).toString();npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/WrongAnswer_0' + suffix + '.wav','https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/WrongAnswer_0' + suffix + '.fbx', getRandomRiddle());", + "hint": "var suffix=(Math.floor(Math.random() * 2) + 1).toString();npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Hint_0' + suffix + '.wav','https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hint_0' + suffix + '.fbx')" + }, + "Responses": + { + "positive": ["yes","yup","yeah","yahoo","sure","affirmative","okay","aye","right","exactly","course","naturally","unquestionably","positively","yep","definitely","certainly","fine","absolutely","positive","love","fantastic"], + "thinking": ["oh", "think about", "i know", "what was", "well", "not sure", "one before", "hold", "one moment", "one second", "1 second", "1 sec", "one sec"], + "hint": ["hint", "heads"] + } + }, + "Story": + [ + { + "QID": "start", + "init": "questionNumber=0;npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/HiFi_Sphinx_Anim_Combined_Entrance_Audio.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx', getRandomRiddle());" + }, + { + "QID": "1", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Blackboard.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Blackboard.fbx');", + "responses": + { + "positive": ["blackboard", "chalkboard", "chalk board", "slate"] + } + }, + { + "QID": "2", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Breath.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Breath.fbx');", + "responses": + { + "positive": ["breath", "death"] + } + }, + { + "QID": "3", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Clock.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Clock.fbx');", + "responses": + { + "positive": ["clock", "cock"] + } + }, + { + "QID": "4", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Coffin.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Coffin.fbx');", + "responses": + { + "positive": ["coffin", "casket", "possum"] + } + }, + { + "QID": "5", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Coin.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Coin.fbx');", + "responses": + { + "positive": ["coin", "boing", "coinage", "coin piece", "change", "join"] + } + }, + { + "QID": "6", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Corn.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Corn.fbx');", + "responses": + { + "positive": ["corn", "born", "maize", "maze", "means", "torn", "horn", "worn", "porn"] + } + }, + { + "QID": "7", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Darkness.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Darkness.fbx');", + "responses": + { + "positive": ["darkness", "dark", "blackness"] + } + }, + { + "QID": "8", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Gloves.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Gloves.fbx');", + "responses": + { + "positive": ["gloves", "love"] + } + }, + { + "QID": "9", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Gold.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Gold.fbx');", + "responses": + { + "positive": ["gold", "old", "bold", "cold", "told"] + } + }, + { + "QID": "10", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_River.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_River.fbx');", + "responses": + { + "positive": ["river", "bigger", "stream", "creek", "brook"] + } + }, + { + "QID": "11", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Secret.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Secret.fbx');", + "responses": + { + "positive": ["secret"] + } + }, + { + "QID": "12", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Shadow.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Shadow.fbx');", + "responses": + { + "positive": ["shadow"] + } + }, + { + "QID": "13", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Silence.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Silence.fbx');", + "responses": + { + "positive": ["silence", "lance", "quiet"] + } + }, + { + "QID": "14", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Stairs.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Stairs.fbx');", + "responses": + { + "positive": ["stairs", "steps", "stair", "stairwell", "there's", "stairway"] + } + }, + { + "QID": "15", + "init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Umbrella.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Umbrella.fbx');", + "responses": + { + "positive": ["umbrella"] + } + }, + { + "QID": "Finished", + "init": "Script.clearTimeout(heartbeatTimeout);heartbeatTimeout = false;npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/ConclusionRight_02.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/ConclusionRight_02.fbx', function(){runOnClient('MyAvatar.goToLocation({x: 5, y: -29, z: -63}, true, true);');setQid('Restarting');});", + "positive": "", + "negative": "", + "unknown": "" + }, + { + "QID": "Restarting", + "init": "npcRespondBlocking('', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/reversedSphinx.fbx', function(){Avatar.startAnimation('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx', 0);_qid='';});", + "positive": "", + "negative": "", + "unknown": "" + } + ] +} diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/applauseOmeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/applauseOmeter.js new file mode 100644 index 0000000000..f6225d1a13 --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/applauseOmeter.js @@ -0,0 +1,70 @@ +// +// Created by Alan-Michael Moody on 5/2/2017 +// + +(function () { + var thisEntityID; + + this.preload = function (entityID) { + thisEntityID = entityID; + }; + + var SCAN_RATE = 100; //ms + var REFERENCE_FRAME_COUNT = 30; + var MAX_AUDIO_THRESHOLD = 16000; + + var framePool = []; + + function scanEngine() { + var avatarLoudnessPool = []; + + function average(a) { + var sum = 0; + var total = a.length; + for (var i = 0; i < total; i++) { + sum += a[i]; + } + return Math.round(sum / total); + } + + function audioClamp(input) { + if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD; + return input; + } + + + var avatars = AvatarList.getAvatarIdentifiers(); + avatars.forEach(function (id) { + var avatar = AvatarList.getAvatar(id); + avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness))); + + }); + + + framePool.push(average(avatarLoudnessPool)); + if (framePool.length >= REFERENCE_FRAME_COUNT) { + framePool.shift(); + } + + function normalizedAverage(a) { + a = a.map(function (v) { + return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v); + }); + return average(a); + } + + var norm = normalizedAverage(framePool); + + // we have a range of 55 to -53 degrees for the needle + + var scaledDegrees = (norm / -.94) + 54.5; // shifting scale from 100 to 55 to -53 ish its more like -51 ; + + Entities.setAbsoluteJointRotationInObjectFrame(thisEntityID, 0, Quat.fromPitchYawRollDegrees(0, 0, scaledDegrees)); + + } + + Script.setInterval(function () { + scanEngine(); + }, SCAN_RATE); + +}); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/bakedTextMeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/bakedTextMeter.js new file mode 100644 index 0000000000..021429618e --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/bakedTextMeter.js @@ -0,0 +1,79 @@ +// +// Created by Alan-Michael Moody on 4/17/2017 +// + +(function () { + var barID; + + this.preload = function (entityID) { + var children = Entities.getChildrenIDs(entityID); + var childZero = Entities.getEntityProperties(children[0]); + barID = childZero.id; + }; + + var SCAN_RATE = 100; //ms + var REFERENCE_FRAME_COUNT = 30; + var MAX_AUDIO_THRESHOLD = 16000; + + var framePool = []; + + function scanEngine() { + var avatarLoudnessPool = []; + + function average(a) { + var sum = 0; + var total = a.length; + for (var i = 0; i < total; i++) { + sum += a[i]; + } + return Math.round(sum / total); + } + + function audioClamp(input) { + if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD; + return input; + } + + + var avatars = AvatarList.getAvatarIdentifiers(); + avatars.forEach(function (id) { + var avatar = AvatarList.getAvatar(id); + avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness))); + }); + + + framePool.push(average(avatarLoudnessPool)); + if (framePool.length >= REFERENCE_FRAME_COUNT) { + framePool.shift(); + } + + function normalizedAverage(a) { + a = a.map(function (v) { + return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v); + }); + return average(a); + } + + var norm = normalizedAverage(framePool); + + + var barProperties = Entities.getEntityProperties(barID); + + var colorShift = 2.55 * norm; //shifting the scale to 0 - 255 + var xShift = norm / 52; // changing scale from 0-100 to 0-1.9 ish + var normShift = xShift - 0.88; //shifting local displacement (-0.88) + var halfShift = xShift / 2; + Entities.editEntity(barID, { + dimensions: {x: xShift, y: barProperties.dimensions.y, z: barProperties.dimensions.z}, + localPosition: {x: normShift - (halfShift), y: -0.0625, z: -0.015}, + color: {red: colorShift, green: barProperties.color.green, blue: barProperties.color.blue} + }); + + + } + + Script.setInterval(function () { + scanEngine(); + }, SCAN_RATE); + +}); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/meter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/meter.js new file mode 100644 index 0000000000..e753633c0b --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/meter.js @@ -0,0 +1,92 @@ +// +// Created by Alan-Michael Moody on 4/17/2017 +// + +(function () { + var barID, textID; + + this.preload = function (entityID) { + + var children = Entities.getChildrenIDs(entityID); + var childZero = Entities.getEntityProperties(children[0]); + var childOne = Entities.getEntityProperties(children[1]); + var childZeroUserData = JSON.parse(Entities.getEntityProperties(children[0]).userData); + + if (childZeroUserData.name === "bar") { + barID = childZero.id; + textID = childOne.id; + } else { + barID = childOne.id; + textID = childZero.id; + } + }; + + var SCAN_RATE = 100; //ms + var REFERENCE_FRAME_COUNT = 30; + var MAX_AUDIO_THRESHOLD = 16000; + + var framePool = []; + + function scanEngine() { + var avatarLoudnessPool = []; + + function average(a) { + var sum = 0; + var total = a.length; + for (var i = 0; i < total; i++) { + sum += a[i]; + } + return Math.round(sum / total); + } + + function audioClamp(input) { + if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD; + return input; + } + + + var avatars = AvatarList.getAvatarIdentifiers(); + avatars.forEach(function (id) { + var avatar = AvatarList.getAvatar(id); + avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness))); + + }); + + + framePool.push(average(avatarLoudnessPool)); + if (framePool.length >= REFERENCE_FRAME_COUNT) { + framePool.shift(); + } + + function normalizedAverage(a) { + a = a.map(function (v) { + return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v); + }); + return average(a); + } + + var norm = normalizedAverage(framePool); + + Entities.editEntity(textID, {text: "Loudness: % " + norm}); + + var barProperties = Entities.getEntityProperties(barID); + + + var colorShift = 2.55 * norm; //shifting the scale to 0 - 255 + var xShift = norm / 100; // changing scale from 0-100 to 0-1 + var normShift = xShift - .5; //shifting scale form 0-1 to -.5 to .5 + var halfShift = xShift / 2 ; + Entities.editEntity(barID, { + dimensions: {x: xShift, y: barProperties.dimensions.y, z: barProperties.dimensions.z}, + localPosition: {x: normShift - (halfShift), y: 0, z: 0.1}, + color: {red: colorShift, green: barProperties.color.green, blue: barProperties.color.blue} + }); + + + } + + Script.setInterval(function () { + scanEngine(); + }, SCAN_RATE); + +}); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/models/applauseOmeter.fbx b/unpublishedScripts/marketplace/audienceApplauseMeter/models/applauseOmeter.fbx new file mode 100644 index 0000000000..4f9ae22b32 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/models/applauseOmeter.fbx differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-plastic.fbx b/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-plastic.fbx new file mode 100644 index 0000000000..940ae0d867 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-plastic.fbx differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-text-entity.fbx b/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-text-entity.fbx new file mode 100644 index 0000000000..fd930d3072 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-text-entity.fbx differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-wood.fbx b/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-wood.fbx new file mode 100644 index 0000000000..86b87832c4 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/models/meter-wood.fbx differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/rezApplauseOmeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/rezApplauseOmeter.js new file mode 100644 index 0000000000..1d89861512 --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/rezApplauseOmeter.js @@ -0,0 +1,24 @@ +// +// Created by Alan-Michael Moody on 5/2/2017 +// + +'use strict'; + +(function () { + var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation)); + + var meter = { + stand: { + type: 'Model', + modelURL: 'https://binaryrelay.com/files/public-docs/hifi/meter/applauseOmeter.fbx', + lifetime: '3600', + script: 'https://binaryrelay.com/files/public-docs/hifi/meter/applauseOmeter.js', + position: Vec3.sum(pos, {x: 0, y: 2.0, z: 0}) + } + + }; + + + Entities.addEntity(meter.stand); + +})(); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/rezMeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/rezMeter.js new file mode 100644 index 0000000000..d3fba9ea56 --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/rezMeter.js @@ -0,0 +1,67 @@ +// +// Created by Alan-Michael Moody on 4/17/2017 +// + +"use strict"; + +(function () { // BEGIN LOCAL_SCOPE + var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation)); + + var graph = { + background: { + type: "Box", + dimensions: {x: 1, y: 1, z: .1}, + color: { + red: 128, + green: 128, + blue: 128 + }, + lifetime: "3600", + script: "https://binaryrelay.com/files/public-docs/hifi/meter/basic/meter.js", + position: pos + }, + bar: { + type: "Box", + parentID: "", + userData: '{"name":"bar"}', + dimensions: {x: .05, y: .25, z: .1}, + color: { + red: 0, + green: 0, + blue: 0 + }, + lifetime: "3600", + position: Vec3.sum(pos, {x: -0.495, y: 0, z: 0.1}) + }, + displayText: { + type: "Text", + parentID: "", + userData: '{"name":"displayText"}', + text: "Loudness: % ", + textColor: { + red: 0, + green: 0, + blue: 0 + }, + backgroundColor: { + red: 128, + green: 128, + blue: 128 + }, + visible: 0.5, + dimensions: {x: 0.70, y: 0.15, z: 0.1}, + lifetime: "3600", + position: Vec3.sum(pos, {x: 0, y: 0.4, z: 0.06}) + } + }; + + var background = Entities.addEntity(graph.background); + + graph.bar.parentID = background; + graph.displayText.parentID = background; + + var bar = Entities.addEntity(graph.bar); + var displayText = Entities.addEntity(graph.displayText); + + +})(); // END LOCAL_SCOPE diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/rezPlasticMeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/rezPlasticMeter.js new file mode 100644 index 0000000000..781585ebf6 --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/rezPlasticMeter.js @@ -0,0 +1,43 @@ +// +// Created by Alan-Michael Moody on 4/17/2017 +// + +"use strict"; + +(function () { + var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation)); + + var graph = { + background: { + type: "Model", + modelURL: "https://binaryrelay.com/files/public-docs/hifi/meter/plastic/meter-plastic.fbx", + color: { + red: 128, + green: 128, + blue: 128 + }, + lifetime: "3600", + script: "https://binaryrelay.com/files/public-docs/hifi/meter/plastic/meter.js", + position: pos + }, + bar: { + type: "Box", + parentID: "", + userData: '{"name":"bar"}', + dimensions: {x: .05, y: .245, z: .07}, + color: { + red: 0, + green: 0, + blue: 0 + }, + lifetime: "3600", + position: Vec3.sum(pos, {x: -0.90, y: 0, z: -0.15}) + } + }; + + + graph.bar.parentID = Entities.addEntity(graph.background); + Entities.addEntity(graph.bar); + + +})(); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/rezTextEntityMeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/rezTextEntityMeter.js new file mode 100644 index 0000000000..c74d595683 --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/rezTextEntityMeter.js @@ -0,0 +1,67 @@ +// +// Created by Alan-Michael Moody on 4/17/2017 +// + +"use strict"; + +(function () { + var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation)); + + var graph = { + background: { + type: "Model", + modelURL: "https://binaryrelay.com/files/public-docs/hifi/meter/text-entity/meter-text-entity.fbx", + color: { + red: 128, + green: 128, + blue: 128 + }, + lifetime: "3600", + script: "https://binaryrelay.com/files/public-docs/hifi/meter/text-entity/meter.js", + position: pos + }, + bar: { + type: "Box", + parentID: "", + userData: '{"name":"bar"}', + dimensions: {x: .05, y: .245, z: .07}, + color: { + red: 0, + green: 0, + blue: 0 + }, + lifetime: "3600", + position: Vec3.sum(pos, {x: -0.88, y: 0, z: -0.15}) + }, + displayText: { + type: "Text", + parentID: "", + userData: '{"name":"displayText"}', + text: "Make Some Noise:", + textColor: { + red: 0, + green: 0, + blue: 0 + }, + backgroundColor: { + red: 255, + green: 255, + blue: 255 + }, + dimensions: {x: .82, y: 0.115, z: 0.15}, + lifetime: "3600", + lineHeight: .08, + position: Vec3.sum(pos, {x: -0.2, y: 0.175, z: -0.035}) + } + }; + + var background = Entities.addEntity(graph.background); + + graph.bar.parentID = background; + graph.displayText.parentID = background; + + var bar = Entities.addEntity(graph.bar); + var displayText = Entities.addEntity(graph.displayText); + + +})(); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/rezWoodMeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/rezWoodMeter.js new file mode 100644 index 0000000000..b40c60275b --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/rezWoodMeter.js @@ -0,0 +1,42 @@ +// +// Created by Alan-Michael Moody on 4/17/2017 +// + +"use strict"; + +(function () { + var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation)); + + var graph = { + background: { + type: "Model", + modelURL: "https://binaryrelay.com/files/public-docs/hifi/meter/wood/meter-wood.fbx", + color: { + red: 128, + green: 128, + blue: 128 + }, + lifetime: "3600", + script: "https://binaryrelay.com/files/public-docs/hifi/meter/wood/meter.js", + position: pos + }, + bar: { + type: "Box", + parentID: "", + userData: '{"name":"bar"}', + dimensions: {x: .05, y: .245, z: .07}, + color: { + red: 0, + green: 0, + blue: 0 + }, + lifetime: "3600", + position: Vec3.sum(pos, {x: -0.88, y: 0, z: -0.15}) + } + }; + + graph.bar.parentID = Entities.addEntity(graph.background); + Entities.addEntity(graph.bar); + + +})(); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textEntityMeter.js b/unpublishedScripts/marketplace/audienceApplauseMeter/textEntityMeter.js new file mode 100644 index 0000000000..f290e9604d --- /dev/null +++ b/unpublishedScripts/marketplace/audienceApplauseMeter/textEntityMeter.js @@ -0,0 +1,89 @@ +// +// Created by Alan-Michael Moody on 4/17/2017 +// + +(function () { + var barID, textID, originalText; + + this.preload = function (entityID) { + + var children = Entities.getChildrenIDs(entityID); + var childZero = Entities.getEntityProperties(children[0]); + var childOne = Entities.getEntityProperties(children[1]); + var childZeroUserData = JSON.parse(Entities.getEntityProperties(children[0]).userData); + + if (childZeroUserData.name === "bar") { + barID = childZero.id; + textID = childOne.id; + originalText = childOne.text + } else { + barID = childOne.id; + textID = childZero.id; + originalText = childZero.text; + } + }; + + var SCAN_RATE = 100; //ms + var REFERENCE_FRAME_COUNT = 30; + var MAX_AUDIO_THRESHOLD = 16000; + + var framePool = []; + + function scanEngine() { + var avatarLoudnessPool = []; + + function average(a) { + var sum = 0; + var total = a.length; + for (var i = 0; i < total; i++) { + sum += a[i]; + } + return Math.round(sum / total); + } + + function audioClamp(input) { + if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD; + return input; + } + + + var avatars = AvatarList.getAvatarIdentifiers(); + avatars.forEach(function (id) { + var avatar = AvatarList.getAvatar(id); + avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness))); + }); + + + framePool.push(average(avatarLoudnessPool)); + if (framePool.length >= REFERENCE_FRAME_COUNT) { + framePool.shift(); + } + + function normalizedAverage(a) { + a = a.map(function (v) { + return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v); + }); + return average(a); + } + + var norm = normalizedAverage(framePool); + Entities.editEntity(textID, {text: originalText + " % " + norm}); + + var barProperties = Entities.getEntityProperties(barID); + + var colorShift = 2.55 * norm; //shifting the scale to 0 - 255 + var xShift = norm / 52; // changing scale from 0-100 to 0-1.9 ish + var normShift = xShift - 0.88; //shifting local displacement (-0.88) + var halfShift = xShift / 2; + Entities.editEntity(barID, { + dimensions: {x: xShift, y: barProperties.dimensions.y, z: barProperties.dimensions.z}, + localPosition: {x: normShift - ( halfShift ), y: -0.0625, z: -0.015}, + color: {red: colorShift, green: barProperties.color.green, blue: barProperties.color.blue} + }); + } + + Script.setInterval(function () { + scanEngine(); + }, SCAN_RATE); + +}); diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/meter.diffuse.psd b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/meter.diffuse.psd new file mode 100644 index 0000000000..07fed10d31 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/meter.diffuse.psd differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_diffuse.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_diffuse.png new file mode 100644 index 0000000000..4e7a3110ab Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_diffuse.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_diffuse_text.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_diffuse_text.png new file mode 100644 index 0000000000..402f2ecf3f Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_diffuse_text.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_emissive.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_emissive.png new file mode 100644 index 0000000000..cb5ee722f2 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_emissive.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_normal_map.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_normal_map.png new file mode 100644 index 0000000000..c96e377a59 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_normal_map.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_reflection.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_reflection.png new file mode 100644 index 0000000000..a3023ced35 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_reflection.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_roughness.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_roughness.png new file mode 100644 index 0000000000..f9fcc2040f Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/plastic/meter.done.plastic_roughness.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_diffuse.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_diffuse.png new file mode 100644 index 0000000000..8243b4e250 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_diffuse.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_diffuse_text.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_diffuse_text.png new file mode 100644 index 0000000000..b9d32a5bb8 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_diffuse_text.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_emissive.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_emissive.png new file mode 100644 index 0000000000..cb5ee722f2 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_emissive.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_normal_map.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_normal_map.png new file mode 100644 index 0000000000..3029bf60c4 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_normal_map.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_reflection.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_reflection.png new file mode 100644 index 0000000000..a3023ced35 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_reflection.png differ diff --git a/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_roughness.png b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_roughness.png new file mode 100644 index 0000000000..89d439c404 Binary files /dev/null and b/unpublishedScripts/marketplace/audienceApplauseMeter/textures/wood/meter.done_roughness.png differ diff --git a/unpublishedScripts/marketplace/boppo/lookAtEntity.js b/unpublishedScripts/marketplace/boppo/lookAtEntity.js index ba072814f2..a0e9274db3 100644 --- a/unpublishedScripts/marketplace/boppo/lookAtEntity.js +++ b/unpublishedScripts/marketplace/boppo/lookAtEntity.js @@ -89,7 +89,7 @@ LookAtTarget = function(sourceEntityID) { } }); if (!actionFound) { - Entities.addAction('spring', _sourceEntityID, getNewActionProperties()); + Entities.addAction('tractor', _sourceEntityID, getNewActionProperties()); } } }; diff --git a/unpublishedScripts/marketplace/chat/Chat.js b/unpublishedScripts/marketplace/chat/Chat.js new file mode 100644 index 0000000000..33bfcfeb4d --- /dev/null +++ b/unpublishedScripts/marketplace/chat/Chat.js @@ -0,0 +1,987 @@ +"use strict"; + +// Chat.js +// By Don Hopkins (dhopkins@donhopkins.com) +// +// 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 webPageURL = "ChatPage.html"; // URL of tablet web page. + var randomizeWebPageURL = true; // Set to true for debugging. + var lastWebPageURL = ""; // Last random URL of tablet web page. + var onChatPage = false; // True when chat web page is opened. + var webHandlerConnected = false; // True when the web handler has been connected. + var channelName = "Chat"; // Unique name for channel that we listen to. + var tabletButtonName = "CHAT"; // Tablet button label. + var tabletButtonIcon = "icons/tablet-icons/menu-i.svg"; // Icon for chat button. + var tabletButtonActiveIcon = "icons/tablet-icons/menu-a.svg"; // Active icon for chat button. + var tabletButton = null; // The button we create in the tablet. + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); // The awesome tablet. + var chatLog = []; // Array of chat messages in the form of [avatarID, displayName, message, data]. + var avatarIdentifiers = {}; // Map of avatar ids to dict of identifierParams. + var speechBubbleShowing = false; // Is the speech bubble visible? + var speechBubbleMessage = null; // The message shown in the speech bubble. + var speechBubbleData = null; // The data of the speech bubble message. + var speechBubbleTextID = null; // The id of the speech bubble local text entity. + var speechBubbleTimer = null; // The timer to pop down the speech bubble. + var speechBubbleParams = null; // The params used to create or edit the speech bubble. + + // Persistent variables saved in the Settings. + var chatName = ''; // The user's name shown in chat. + var chatLogMaxSize = 100; // The maximum number of chat messages we remember. + var sendTyping = true; // Send typing begin and end notification. + var identifyAvatarDuration = 10; // How long to leave the avatar identity line up, in seconds. + var identifyAvatarLineColor = { red: 0, green: 255, blue: 0 }; // The color of the avatar identity line. + var identifyAvatarMyJointName = 'Head'; // My bone from which to draw the avatar identity line. + var identifyAvatarYourJointName = 'Head'; // Your bone to which to draw the avatar identity line. + var speechBubbleDuration = 10; // How long to leave the speech bubble up, in seconds. + var speechBubbleTextColor = {red: 255, green: 255, blue: 255}; // The text color of the speech bubble. + var speechBubbleBackgroundColor = {red: 0, green: 0, blue: 0}; // The background color of the speech bubble. + var speechBubbleOffset = {x: 0, y: 0.3, z: 0.0}; // The offset from the joint to whic the speech bubble is attached. + var speechBubbleJointName = 'Head'; // The name of the joint to which the speech bubble is attached. + var speechBubbleLineHeight = 0.05; // The height of a line of text in the speech bubble. + + // Load the persistent variables from the Settings, with defaults. + function loadSettings() { + chatName = Settings.getValue('Chat_chatName', MyAvatar.displayName); + if (!chatName) { + chatName = randomAvatarName(); + } + chatLogMaxSize = Settings.getValue('Chat_chatLogMaxSize', 100); + sendTyping = Settings.getValue('Chat_sendTyping', true); + identifyAvatarDuration = Settings.getValue('Chat_identifyAvatarDuration', 10); + identifyAvatarLineColor = Settings.getValue('Chat_identifyAvatarLineColor', { red: 0, green: 255, blue: 0 }); + identifyAvatarMyJointName = Settings.getValue('Chat_identifyAvatarMyJointName', 'Head'); + identifyAvatarYourJointName = Settings.getValue('Chat_identifyAvatarYourJointName', 'Head'); + speechBubbleDuration = Settings.getValue('Chat_speechBubbleDuration', 10); + speechBubbleTextColor = Settings.getValue('Chat_speechBubbleTextColor', {red: 255, green: 255, blue: 255}); + speechBubbleBackgroundColor = Settings.getValue('Chat_speechBubbleBackgroundColor', {red: 0, green: 0, blue: 0}); + speechBubbleOffset = Settings.getValue('Chat_speechBubbleOffset', {x: 0.0, y: 0.3, z:0.0}); + speechBubbleJointName = Settings.getValue('Chat_speechBubbleJointName', 'Head'); + speechBubbleLineHeight = Settings.getValue('Chat_speechBubbleLineHeight', 0.05); + + saveSettings(); + } + + // Save the persistent variables to the Settings. + function saveSettings() { + Settings.setValue('Chat_chatName', chatName); + Settings.setValue('Chat_chatLogMaxSize', chatLogMaxSize); + Settings.setValue('Chat_sendTyping', sendTyping); + Settings.setValue('Chat_identifyAvatarDuration', identifyAvatarDuration); + Settings.setValue('Chat_identifyAvatarLineColor', identifyAvatarLineColor); + Settings.setValue('Chat_identifyAvatarMyJointName', identifyAvatarMyJointName); + Settings.setValue('Chat_identifyAvatarYourJointName', identifyAvatarYourJointName); + Settings.setValue('Chat_speechBubbleDuration', speechBubbleDuration); + Settings.setValue('Chat_speechBubbleTextColor', speechBubbleTextColor); + Settings.setValue('Chat_speechBubbleBackgroundColor', speechBubbleBackgroundColor); + Settings.setValue('Chat_speechBubbleOffset', speechBubbleOffset); + Settings.setValue('Chat_speechBubbleJointName', speechBubbleJointName); + Settings.setValue('Chat_speechBubbleLineHeight', speechBubbleLineHeight); + } + + // Reset the Settings and persistent variables to the defaults. + function resetSettings() { + Settings.setValue('Chat_chatName', null); + Settings.setValue('Chat_chatLogMaxSize', null); + Settings.setValue('Chat_sendTyping', null); + Settings.setValue('Chat_identifyAvatarDuration', null); + Settings.setValue('Chat_identifyAvatarLineColor', null); + Settings.setValue('Chat_identifyAvatarMyJointName', null); + Settings.setValue('Chat_identifyAvatarYourJointName', null); + Settings.setValue('Chat_speechBubbleDuration', null); + Settings.setValue('Chat_speechBubbleTextColor', null); + Settings.setValue('Chat_speechBubbleBackgroundColor', null); + Settings.setValue('Chat_speechBubbleOffset', null); + Settings.setValue('Chat_speechBubbleJointName', null); + Settings.setValue('Chat_speechBubbleLineHeight', null); + + loadSettings(); + } + + // Update anything that might depend on the settings. + function updateSettings() { + updateSpeechBubble(); + trimChatLog(); + updateChatPage(); + } + + // Trim the chat log so it is no longer than chatLogMaxSize lines. + function trimChatLog() { + if (chatLog.length > chatLogMaxSize) { + chatLog.splice(0, chatLogMaxSize - chatLog.length); + } + } + + // Clear the local chat log. + function clearChatLog() { + //print("clearChatLog"); + chatLog = []; + updateChatPage(); + } + + // We got a chat message from the channel. + // Trim the chat log, save the latest message in the chat log, + // and show the message on the tablet, if the chat page is showing. + function handleTransmitChatMessage(avatarID, displayName, message, data) { + //print("receiveChat", "avatarID", avatarID, "displayName", displayName, "message", message, "data", data); + + trimChatLog(); + chatLog.push([avatarID, displayName, message, data]); + + if (onChatPage) { + tablet.emitScriptEvent( + JSON.stringify({ + type: "ReceiveChatMessage", + avatarID: avatarID, + displayName: displayName, + message: message, + data: data + })); + } + } + + // Trim the chat log, save the latest log message in the chat log, + // and show the message on the tablet, if the chat page is showing. + function logMessage(message, data) { + //print("logMessage", message, data); + + trimChatLog(); + chatLog.push([null, null, message, data]); + + if (onChatPage) { + tablet.emitScriptEvent( + JSON.stringify({ + type: "LogMessage", + message: message, + data: data + })); + } + } + + // An empty chat message was entered. + // Hide our speech bubble. + function emptyChatMessage(data) { + popDownSpeechBubble(); + } + + // Notification that we typed a keystroke. + function type() { + //print("type"); + } + + // Notification that we began typing. + // Notify everyone that we started typing. + function beginTyping() { + //print("beginTyping"); + if (!sendTyping) { + return; + } + + Messages.sendMessage( + channelName, + JSON.stringify({ + type: 'AvatarBeginTyping', + avatarID: MyAvatar.sessionUUID, + displayName: chatName + })); + } + + // Notification that somebody started typing. + function handleAvatarBeginTyping(avatarID, displayName) { + //print("handleAvatarBeginTyping:", "avatarID", avatarID, displayName); + } + + // Notification that we stopped typing. + // Notify everyone that we stopped typing. + function endTyping() { + //print("endTyping"); + if (!sendTyping) { + return; + } + + Messages.sendMessage( + channelName, + JSON.stringify({ + type: 'AvatarEndTyping', + avatarID: MyAvatar.sessionUUID, + displayName: chatName + })); + } + + // Notification that somebody stopped typing. + function handleAvatarEndTyping(avatarID, displayName) { + //print("handleAvatarEndTyping:", "avatarID", avatarID, displayName); + } + + // Identify an avatar by drawing a line from our head to their head. + // If the avatar is our own, then just draw a line up into the sky. + function identifyAvatar(yourAvatarID) { + //print("identifyAvatar", yourAvatarID); + + unidentifyAvatars(); + + var myAvatarID = MyAvatar.sessionUUID; + var myJointIndex = MyAvatar.getJointIndex(identifyAvatarMyJointName); + var myJointRotation = + Quat.multiply( + MyAvatar.orientation, + MyAvatar.getAbsoluteJointRotationInObjectFrame(myJointIndex)); + var myJointPosition = + Vec3.sum( + MyAvatar.position, + Vec3.multiplyQbyV( + MyAvatar.orientation, + MyAvatar.getAbsoluteJointTranslationInObjectFrame(myJointIndex))); + + var yourJointIndex = -1; + var yourJointPosition; + + if (yourAvatarID == myAvatarID) { + + // You pointed at your own name, so draw a line up from your head. + + yourJointPosition = { + x: myJointPosition.x, + y: myJointPosition.y + 1000.0, + z: myJointPosition.z + }; + + } else { + + // You pointed at somebody else's name, so draw a line from your head to their head. + + var yourAvatar = AvatarList.getAvatar(yourAvatarID); + if (!yourAvatar) { + return; + } + + yourJointIndex = yourAvatar.getJointIndex(identifyAvatarMyJointName) + + var yourJointRotation = + Quat.multiply( + yourAvatar.orientation, + yourAvatar.getAbsoluteJointRotationInObjectFrame(yourJointIndex)); + yourJointPosition = + Vec3.sum( + yourAvatar.position, + Vec3.multiplyQbyV( + yourAvatar.orientation, + yourAvatar.getAbsoluteJointTranslationInObjectFrame(yourJointIndex))); + + } + + var identifierParams = { + parentID: myAvatarID, + parentJointIndex: myJointIndex, + lifetime: identifyAvatarDuration, + start: myJointPosition, + endParentID: yourAvatarID, + endParentJointIndex: yourJointIndex, + end: yourJointPosition, + color: identifyAvatarLineColor, + alpha: 1, + lineWidth: 1 + }; + + avatarIdentifiers[yourAvatarID] = identifierParams; + + identifierParams.lineID = Overlays.addOverlay("line3d", identifierParams); + + //print("ADDOVERLAY lineID", lineID, "myJointPosition", JSON.stringify(myJointPosition), "yourJointPosition", JSON.stringify(yourJointPosition), "lineData", JSON.stringify(lineData)); + + identifierParams.timer = + Script.setTimeout(function() { + //print("DELETEOVERLAY lineID"); + unidentifyAvatar(yourAvatarID); + }, identifyAvatarDuration * 1000); + + } + + // Stop identifying an avatar. + function unidentifyAvatar(yourAvatarID) { + //print("unidentifyAvatar", yourAvatarID); + + var identifierParams = avatarIdentifiers[yourAvatarID]; + if (!identifierParams) { + return; + } + + if (identifierParams.timer) { + Script.clearTimeout(identifierParams.timer); + } + + if (identifierParams.lineID) { + Overlays.deleteOverlay(identifierParams.lineID); + } + + delete avatarIdentifiers[yourAvatarID]; + } + + // Stop identifying all avatars. + function unidentifyAvatars() { + var ids = []; + + for (var avatarID in avatarIdentifiers) { + ids.push(avatarID); + } + + for (var i = 0, n = ids.length; i < n; i++) { + var avatarID = ids[i]; + unidentifyAvatar(avatarID); + } + + } + + // Turn to face another avatar. + function faceAvatar(yourAvatarID, displayName) { + //print("faceAvatar:", yourAvatarID, displayName); + + var myAvatarID = MyAvatar.sessionUUID; + if (yourAvatarID == myAvatarID) { + // You clicked on yourself. + return; + } + + var yourAvatar = AvatarList.getAvatar(yourAvatarID); + if (!yourAvatar) { + logMessage(displayName + ' is not here!', null); + return; + } + + // Project avatar positions to the floor and get the direction between those points, + // then face my avatar towards your avatar. + var yourPosition = yourAvatar.position; + yourPosition.y = 0; + var myPosition = MyAvatar.position; + myPosition.y = 0; + var myOrientation = Quat.lookAtSimple(myPosition, yourPosition); + MyAvatar.orientation = myOrientation; + } + + // Make a hopefully unique random anonymous avatar name. + function randomAvatarName() { + return 'Anon_' + Math.floor(Math.random() * 1000000); + } + + // Change the avatar size to bigger. + function biggerSize() { + //print("biggerSize"); + logMessage("Increasing avatar size bigger!", null); + MyAvatar.increaseSize(); + } + + // Change the avatar size to smaller. + function smallerSize() { + //print("smallerSize"); + logMessage("Decreasing avatar size smaler!", null); + MyAvatar.decreaseSize(); + } + + // Set the avatar size to normal. + function normalSize() { + //print("normalSize"); + logMessage("Resetting avatar size to normal!", null); + MyAvatar.resetSize(); + } + + // Send out a "Who" message, including our avatarID as myAvatarID, + // which will be sent in the response, so we can tell the reply + // is to our request. + function transmitWho() { + //print("transmitWho"); + logMessage("Who is here?", null); + Messages.sendMessage( + channelName, + JSON.stringify({ + type: 'Who', + myAvatarID: MyAvatar.sessionUUID + })); + } + + // Send a reply to a "Who" message, with a friendly message, + // our avatarID and our displayName. myAvatarID is the id + // of the avatar who send the Who message, to whom we're + // responding. + function handleWho(myAvatarID) { + var avatarID = MyAvatar.sessionUUID; + if (myAvatarID == avatarID) { + // Don't reply to myself. + return; + } + + var message = "I'm here!"; + var data = {}; + + Messages.sendMessage( + channelName, + JSON.stringify({ + type: 'ReplyWho', + myAvatarID: myAvatarID, + avatarID: avatarID, + displayName: chatName, + message: message, + data: data + })); + } + + // Receive the reply to a "Who" message. Ignore it unless we were the one + // who sent it out (if myAvatarIS is our avatar's id). + function handleReplyWho(myAvatarID, avatarID, displayName, message, data) { + if (myAvatarID != MyAvatar.sessionUUID) { + return; + } + + handleTransmitChatMessage(avatarID, displayName, message, data); + } + + // Handle input form the user, possibly multiple lines separated by newlines. + // Each line may be a chat command starting with "/", or a chat message. + function handleChatMessage(message, data) { + + var messageLines = message.trim().split('\n'); + + for (var i = 0, n = messageLines.length; i < n; i++) { + var messageLine = messageLines[i]; + + if (messageLine.substr(0, 1) == '/') { + handleChatCommand(messageLine, data); + } else { + transmitChatMessage(messageLine, data); + } + } + + } + + // Handle a chat command prefixed by "/". + function handleChatCommand(message, data) { + + var commandLine = message.substr(1); + var tokens = commandLine.trim().split(' '); + var command = tokens[0]; + var rest = commandLine.substr(command.length + 1).trim(); + + //print("commandLine", commandLine, "command", command, "tokens", tokens, "rest", rest); + + switch (command) { + + case '?': + case 'help': + logMessage('Type "/?" or "/help" for help, which is this!', null); + logMessage('Type "/name " to set your chat name, or "/name" to use your display name, or a random name if that is not defined.', null); + logMessage('Type "/shutup" to shut up your overhead chat message.', null); + logMessage('Type "/say " to say something.', null); + logMessage('Type "/clear" to clear your cha, nullt log.', null); + logMessage('Type "/who" to ask who is h, nullere to chat.', null); + logMessage('Type "/bigger", "/smaller" or "/normal" to change, null your avatar size.', null); + logMessage('(Sorry, that\'s all there is so far!)', null); + break; + + case 'name': + if (rest == '') { + if (MyAvatar.displayName) { + chatName = MyAvatar.displayName; + saveSettings(); + logMessage('Your chat name has been set to your display name "' + chatName + '".', null); + } else { + chatName = randomAvatarName(); + saveSettings(); + logMessage('Your avatar\'s display name is not defined, so your chat name has been set to "' + chatName + '".', null); + } + } else { + chatName = rest; + saveSettings(); + logMessage('Your chat name has been set to "' + chatName + '".', null); + } + break; + + case 'shutup': + popDownSpeechBubble(); + logMessage('Overhead chat message shut up.', null); + break; + + case 'say': + if (rest == '') { + emptyChatMessage(data); + } else { + transmitChatMessage(rest, data); + } + break; + + case 'who': + transmitWho(); + break; + + case 'clear': + clearChatLog(); + break; + + case 'bigger': + biggerSize(); + break; + + case 'smaller': + smallerSize(); + break; + + case 'normal': + normalSize(); + break; + + case 'resetsettings': + resetSettings(); + updateSettings(); + break; + + case 'speechbubbleheight': + var y = parseInt(rest); + if (!isNaN(y)) { + speechBubbleOffset.y = y; + } + saveSettings(); + updateSettings(); + break; + + case 'speechbubbleduration': + var duration = parseFloat(rest); + if (!isNaN(duration)) { + speechBubbleDuration = duration; + } + saveSettings(); + updateSettings(); + break; + + default: + logMessage('Unknown chat command. Type "/help" or "/?" for help.', null); + break; + + } + + } + + // Send out a chat message to everyone. + function transmitChatMessage(message, data) { + //print("transmitChatMessage", 'avatarID', avatarID, 'displayName', displayName, 'message', message, 'data', data); + + popUpSpeechBubble(message, data); + + Messages.sendMessage( + channelName, + JSON.stringify({ + type: 'TransmitChatMessage', + avatarID: MyAvatar.sessionUUID, + displayName: chatName, + message: message, + data: data + })); + + } + + // Show the speech bubble. + function popUpSpeechBubble(message, data) { + //print("popUpSpeechBubble", message, data); + + popDownSpeechBubble(); + + speechBubbleShowing = true; + speechBubbleMessage = message; + speechBubbleData = data; + + updateSpeechBubble(); + + if (speechBubbleDuration > 0) { + speechBubbleTimer = Script.setTimeout( + function () { + popDownSpeechBubble(); + }, + speechBubbleDuration * 1000); + } + } + + // Update the speech bubble. + // This is factored out so we can update an existing speech bubble if any settings change. + function updateSpeechBubble() { + if (!speechBubbleShowing) { + return; + } + + var jointIndex = MyAvatar.getJointIndex(speechBubbleJointName); + var dimensions = { + x: 100.0, + y: 100.0, + z: 0.1 + }; + + speechBubbleParams = { + type: "Text", + lifetime: speechBubbleDuration, + parentID: MyAvatar.sessionUUID, + jointIndex: jointIndex, + dimensions: dimensions, + lineHeight: speechBubbleLineHeight, + leftMargin: 0, + topMargin: 0, + rightMargin: 0, + bottomMargin: 0, + faceCamera: true, + drawInFront: true, + ignoreRayIntersection: true, + text: speechBubbleMessage, + textColor: speechBubbleTextColor, + color: speechBubbleTextColor, + backgroundColor: speechBubbleBackgroundColor + }; + + // Only overlay text3d has a way to measure the text, not entities. + // So we make a temporary one just for measuring text, then delete it. + var speechBubbleTextOverlayID = Overlays.addOverlay("text3d", speechBubbleParams); + var textSize = Overlays.textSize(speechBubbleTextOverlayID, speechBubbleMessage); + try { + Overlays.deleteOverlay(speechBubbleTextOverlayID); + } catch (e) {} + + //print("updateSpeechBubble:", "speechBubbleMessage", speechBubbleMessage, "textSize", textSize.width, textSize.height); + + var fudge = 0.02; + var width = textSize.width + fudge; + var height = textSize.height + fudge; + dimensions = { + x: width, + y: height, + z: 0.1 + }; + speechBubbleParams.dimensions = dimensions; + + var headRotation = + Quat.multiply( + MyAvatar.orientation, + MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex)); + var headPosition = + Vec3.sum( + MyAvatar.position, + Vec3.multiplyQbyV( + MyAvatar.orientation, + MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex))); + var rotatedOffset = + Vec3.multiplyQbyV( + headRotation, + speechBubbleOffset); + var position = + Vec3.sum( + headPosition, + rotatedOffset); + speechBubbleParams.position = position; + + if (!speechBubbleTextID) { + speechBubbleTextID = + Entities.addEntity(speechBubbleParams, true); + } else { + Entities.editEntity(speechBubbleTextID, speechBubbleParams); + } + + //print("speechBubbleTextID:", speechBubbleTextID, "speechBubbleParams", JSON.stringify(speechBubbleParams)); + } + + // Hide the speech bubble. + function popDownSpeechBubble() { + cancelSpeechBubbleTimer(); + + speechBubbleShowing = false; + + //print("popDownSpeechBubble speechBubbleTextID", speechBubbleTextID); + + if (speechBubbleTextID) { + try { + Entities.deleteEntity(speechBubbleTextID); + } catch (e) {} + speechBubbleTextID = null; + } + } + + // Cancel the speech bubble popup timer. + function cancelSpeechBubbleTimer() { + if (speechBubbleTimer) { + Script.clearTimeout(speechBubbleTimer); + speechBubbleTimer = null; + } + } + + // Show the tablet web page and connect the web handler. + function showTabletWebPage() { + var url = Script.resolvePath(webPageURL); + if (randomizeWebPageURL) { + url += '?rand=' + Math.random(); + } + lastWebPageURL = url; + onChatPage = true; + tablet.gotoWebScreen(lastWebPageURL); + // Connect immediately so we don't miss anything. + connectWebHandler(); + } + + // Update the tablet web page with the chat log. + function updateChatPage() { + if (!onChatPage) { + return; + } + + tablet.emitScriptEvent( + JSON.stringify({ + type: "Update", + chatLog: chatLog + })); + } + + function onChatMessageReceived(channel, message, senderID) { + + // Ignore messages to any other channel than mine. + if (channel != channelName) { + return; + } + + // Parse the message and pull out the message parameters. + var messageData = JSON.parse(message); + var messageType = messageData.type; + + //print("MESSAGE", message); + //print("MESSAGEDATA", messageData, JSON.stringify(messageData)); + + switch (messageType) { + + case 'TransmitChatMessage': + handleTransmitChatMessage(messageData.avatarID, messageData.displayName, messageData.message, messageData.data); + break; + + case 'AvatarBeginTyping': + handleAvatarBeginTyping(messageData.avatarID, messageData.displayName); + break; + + case 'AvatarEndTyping': + handleAvatarEndTyping(messageData.avatarID, messageData.displayName); + break; + + case 'Who': + handleWho(messageData.myAvatarID); + break; + + case 'ReplyWho': + handleReplyWho(messageData.myAvatarID, messageData.avatarID, messageData.displayName, messageData.message, messageData.data); + break; + + default: + print("onChatMessageReceived: unknown messageType", messageType, "message", message); + break; + + } + + } + + // Handle events from the tablet web page. + function onWebEventReceived(event) { + if (!onChatPage) { + return; + } + + //print("onWebEventReceived: event", event); + + var eventData = JSON.parse(event); + var eventType = eventData.type; + + switch (eventType) { + + case 'Ready': + updateChatPage(); + break; + + case 'Update': + updateChatPage(); + break; + + case 'HandleChatMessage': + var message = eventData.message; + var data = eventData.data; + //print("onWebEventReceived: HandleChatMessage:", 'message', message, 'data', data); + handleChatMessage(message, data); + break; + + case 'PopDownSpeechBubble': + popDownSpeechBubble(); + break; + + case 'EmptyChatMessage': + emptyChatMessage(); + break; + + case 'Type': + type(); + break; + + case 'BeginTyping': + beginTyping(); + break; + + case 'EndTyping': + endTyping(); + break; + + case 'IdentifyAvatar': + identifyAvatar(eventData.avatarID); + break; + + case 'UnidentifyAvatar': + unidentifyAvatar(eventData.avatarID); + break; + + case 'FaceAvatar': + faceAvatar(eventData.avatarID, eventData.displayName); + break; + + case 'ClearChatLog': + clearChatLog(); + break; + + case 'Who': + transmitWho(); + break; + + case 'Bigger': + biggerSize(); + break; + + case 'Smaller': + smallerSize(); + break; + + case 'Normal': + normalSize(); + break; + + default: + print("onWebEventReceived: unexpected eventType", eventType); + break; + + } + } + + function onScreenChanged(type, url) { + //print("onScreenChanged", "type", type, "url", url, "lastWebPageURL", lastWebPageURL); + + if ((type === "Web") && + (url === lastWebPageURL)) { + if (!onChatPage) { + onChatPage = true; + connectWebHandler(); + } + } else { + if (onChatPage) { + onChatPage = false; + disconnectWebHandler(); + } + } + + } + + function connectWebHandler() { + if (webHandlerConnected) { + return; + } + + try { + tablet.webEventReceived.connect(onWebEventReceived); + } catch (e) { + print("connectWebHandler: error connecting: " + e); + return; + } + + webHandlerConnected = true; + //print("connectWebHandler connected"); + + updateChatPage(); + } + + function disconnectWebHandler() { + if (!webHandlerConnected) { + return; + } + + try { + tablet.webEventReceived.disconnect(onWebEventReceived); + } catch (e) { + print("disconnectWebHandler: error disconnecting web handler: " + e); + return; + } + webHandlerConnected = false; + + //print("disconnectWebHandler: disconnected"); + } + + // Show the tablet web page when the chat button on the tablet is clicked. + function onTabletButtonClicked() { + showTabletWebPage(); + } + + // Shut down the chat application when the tablet button is destroyed. + function onTabletButtonDestroyed() { + shutDown(); + } + + // Start up the chat application. + function startUp() { + //print("startUp"); + + loadSettings(); + + tabletButton = tablet.addButton({ + icon: tabletButtonIcon, + activeIcon: tabletButtonActiveIcon, + text: tabletButtonName, + sortOrder: 0 + }); + + Messages.subscribe(channelName); + + tablet.screenChanged.connect(onScreenChanged); + + Messages.messageReceived.connect(onChatMessageReceived); + + tabletButton.clicked.connect(onTabletButtonClicked); + + Script.scriptEnding.connect(onTabletButtonDestroyed); + + logMessage('Type "/?" or "/help" for help with chat.', null); + + //print("Added chat button to tablet."); + } + + // Shut down the chat application. + function shutDown() { + //print("shutDown"); + + popDownSpeechBubble(); + unidentifyAvatars(); + disconnectWebHandler(); + + if (onChatPage) { + tablet.gotoHomeScreen(); + onChatPage = false; + } + + tablet.screenChanged.disconnect(onScreenChanged); + + Messages.messageReceived.disconnect(onChatMessageReceived); + + // Clean up the tablet button we made. + tabletButton.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(tabletButton); + tabletButton = null; + + //print("Removed chat button from tablet."); + } + + // Kick off the chat application! + startUp(); + +}()); diff --git a/unpublishedScripts/marketplace/chat/ChatPage.html b/unpublishedScripts/marketplace/chat/ChatPage.html new file mode 100644 index 0000000000..e1a3776dd5 --- /dev/null +++ b/unpublishedScripts/marketplace/chat/ChatPage.html @@ -0,0 +1,511 @@ + + + + Chat + + + + + + + + +
+ +
+ Chat +
+ +
+ +
+ +
+ +
+ + + + + + diff --git a/unpublishedScripts/marketplace/tablet-raiseHand/tablet-raiseHand.js b/unpublishedScripts/marketplace/tablet-raiseHand/tablet-raiseHand.js new file mode 100644 index 0000000000..f7702053a4 --- /dev/null +++ b/unpublishedScripts/marketplace/tablet-raiseHand/tablet-raiseHand.js @@ -0,0 +1,102 @@ +"use strict"; +// +// tablet-raiseHand.js +// +// client script that creates a tablet button to raise hand +// +// Created by Triplelexx on 17/04/22 +// Copyright 2017 High Fidelity, Inc. +// +// Hand icons adapted from https://linearicons.com, created by Perxis https://perxis.com CC BY-SA 4.0 license. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + var BUTTON_NAME = "RAISE\nHAND"; + var USERCONNECTION_MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; + var DEBUG_PREFIX = "TABLET RAISE HAND: "; + var isRaiseHandButtonActive = false; + var animHandlerId; + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + text: BUTTON_NAME, + icon: "icons/tablet-icons/raise-hand-i.svg", + activeIcon: "icons/tablet-icons/raise-hand-a.svg" + }); + + function onClicked() { + isRaiseHandButtonActive = !isRaiseHandButtonActive; + button.editProperties({ isActive: isRaiseHandButtonActive }); + if (isRaiseHandButtonActive) { + removeAnimation(); + animHandlerId = MyAvatar.addAnimationStateHandler(raiseHandAnimation, []); + Messages.subscribe(USERCONNECTION_MESSAGE_CHANNEL); + Messages.messageReceived.connect(messageHandler); + } else { + removeAnimation(); + Messages.unsubscribe(USERCONNECTION_MESSAGE_CHANNEL); + Messages.messageReceived.disconnect(messageHandler); + } + } + + function removeAnimation() { + if (animHandlerId) { + animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); + } + } + + function raiseHandAnimation(animationProperties) { + // all we are doing here is moving the right hand to a spot that is above the hips. + var headIndex = MyAvatar.getJointIndex("Head"); + var offset = 0.0; + var result = {}; + if (headIndex) { + offset = 0.85 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; + } + var handPos = Vec3.multiply(offset, { x: -0.7, y: 1.25, z: 0.25 }); + result.rightHandPosition = handPos; + result.rightHandRotation = Quat.fromPitchYawRollDegrees(0, 0, 0); + return result; + } + + function messageHandler(channel, messageString, senderID) { + if (channel !== USERCONNECTION_MESSAGE_CHANNEL && senderID !== MyAvatar.sessionUUID) { + return; + } + var message = {}; + try { + message = JSON.parse(messageString); + } catch (e) { + print(DEBUG_PREFIX + "messageHandler error: " + e); + } + switch (message.key) { + case "waiting": + case "connecting": + case "connectionAck": + case "connectionRequest": + case "done": + removeAnimation(); + if (isRaiseHandButtonActive) { + isRaiseHandButtonActive = false; + button.editProperties({ isActive: isRaiseHandButtonActive }); + } + break; + default: + print(DEBUG_PREFIX + "messageHandler unknown message: " + message); + break; + } + } + + button.clicked.connect(onClicked); + + Script.scriptEnding.connect(function() { + Messages.unsubscribe(USERCONNECTION_MESSAGE_CHANNEL); + Messages.messageReceived.disconnect(messageHandler); + button.clicked.disconnect(onClicked); + tablet.removeButton(button); + removeAnimation(); + }); +}()); // END LOCAL_SCOPE