From 9b5f19399c6512d92ee1e79fdb1913e482e02253 Mon Sep 17 00:00:00 2001 From: Triplelexx Date: Tue, 25 Apr 2017 00:45:06 +0100 Subject: [PATCH 001/146] create tablet-raiseHand.js --- .../icons/tablet-icons/raise-hand-a.svg | 70 ++++++++++++ .../icons/tablet-icons/raise-hand-i.svg | 60 +++++++++++ .../tablet-raiseHand/tablet-raiseHand.js | 102 ++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 interface/resources/icons/tablet-icons/raise-hand-a.svg create mode 100644 interface/resources/icons/tablet-icons/raise-hand-i.svg create mode 100644 unpublishedScripts/marketplace/tablet-raiseHand/tablet-raiseHand.js 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/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 From a586a31a93c61a4ba7fd5d40cc180e2aa6e3b262 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 29 Mar 2017 13:48:24 -0700 Subject: [PATCH 002/146] stub the oven tool and add a find module for FBX SDK --- cmake/modules/FindFBXSDK.cmake | 110 +++++++++++++++++++++++++++++++++ tools/CMakeLists.txt | 3 + tools/oven/CMakeLists.txt | 5 ++ tools/oven/src/main.cpp | 14 +++++ 4 files changed, 132 insertions(+) create mode 100644 cmake/modules/FindFBXSDK.cmake create mode 100644 tools/oven/CMakeLists.txt create mode 100644 tools/oven/src/main.cpp diff --git a/cmake/modules/FindFBXSDK.cmake b/cmake/modules/FindFBXSDK.cmake new file mode 100644 index 0000000000..477aff6bf2 --- /dev/null +++ b/cmake/modules/FindFBXSDK.cmake @@ -0,0 +1,110 @@ +# 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) + set(FBX_VERSION 2017.0.1) +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/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..5244b68217 --- /dev/null +++ b/tools/oven/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET_NAME oven) + +setup_hifi_project() + +find_package(FBXSDK REQUIRED) diff --git a/tools/oven/src/main.cpp b/tools/oven/src/main.cpp new file mode 100644 index 0000000000..831ba00328 --- /dev/null +++ b/tools/oven/src/main.cpp @@ -0,0 +1,14 @@ +// +// 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 + + +int main (int argc, char** argv) { + return 0; +} From b04d50709014ba725b5977ebb64df340c605ce67 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 29 Mar 2017 14:01:40 -0700 Subject: [PATCH 003/146] use correct FBX_VERSION for WIN32 --- cmake/modules/FindFBXSDK.cmake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmake/modules/FindFBXSDK.cmake b/cmake/modules/FindFBXSDK.cmake index 477aff6bf2..7f6a424aa1 100644 --- a/cmake/modules/FindFBXSDK.cmake +++ b/cmake/modules/FindFBXSDK.cmake @@ -17,7 +17,11 @@ # which uses the MIT license (https://github.com/ufz-vislab/VtkFbxConverter/blob/master/LICENSE.txt) if (NOT FBX_VERSION) - set(FBX_VERSION 2017.0.1) + 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}") From 26d4cc73e0c9ec3667aec35b5a0f196c0ea2b47a Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 30 Mar 2017 15:16:23 -0700 Subject: [PATCH 004/146] add stubbed FBXBaker leveraging FBX SDK for read/write --- libraries/model-baking/CMakeLists.txt | 7 ++ libraries/model-baking/src/FBXBaker.cpp | 156 ++++++++++++++++++++++++ libraries/model-baking/src/FBXBaker.h | 38 ++++++ tools/oven/CMakeLists.txt | 2 +- 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 libraries/model-baking/CMakeLists.txt create mode 100644 libraries/model-baking/src/FBXBaker.cpp create mode 100644 libraries/model-baking/src/FBXBaker.h diff --git a/libraries/model-baking/CMakeLists.txt b/libraries/model-baking/CMakeLists.txt new file mode 100644 index 0000000000..45c0350bbe --- /dev/null +++ b/libraries/model-baking/CMakeLists.txt @@ -0,0 +1,7 @@ +set(TARGET_NAME model-baking) + +setup_hifi_library() + +find_package(FBXSDK REQUIRED) +target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) +target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR}) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp new file mode 100644 index 0000000000..af981b471e --- /dev/null +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -0,0 +1,156 @@ +// +// FBXBaker.cpp +// libraries/model-baking/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 +#include + +#include + +#include "FBXBaker.h" + +Q_LOGGING_CATEGORY(model_baking, "hifi.model-baking"); + +FBXBaker::FBXBaker(std::string fbxPath) : + _fbxPath(fbxPath) +{ + // create an FBX SDK manager + _sdkManager = FbxManager::Create(); +} + +bool FBXBaker::bakeFBX() { + + // load the scene from the FBX file + if (importScene()) { + // enumerate the textures found in the scene and bake them + rewriteAndCollectSceneTextures(); + } else { + return false; + } + + return true; +} + +bool FBXBaker::importScene() { + // create an FBX SDK importer + FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); + + // import the FBX file at the given path + bool importStatus = importer->Initialize(_fbxPath.c_str()); + + if (!importStatus) { + // failed to import the FBX file, print an error and return + qCDebug(model_baking) << "Failed to import FBX file at" << _fbxPath.c_str() << "- error:" << importer->GetStatus().GetErrorString(); + + return false; + } + + // setup a new scene to hold the imported file + _scene = FbxScene::Create(_sdkManager, "bakeScene"); + + // import the file to the created scene + importer->Import(_scene); + + // destroy the importer that is no longer needed + importer->Destroy(); + + return true; +} + +bool FBXBaker::rewriteAndCollectSceneTextures() { + // grab the root node from the scene + FbxNode* rootNode = _scene->GetRootNode(); + + if (rootNode) { + // enumerate the children of the root node + for (int i = 0; i < rootNode->GetChildCount(); ++i) { + FbxNode* node = rootNode->GetChild(i); + + // check if this child is a mesh node + if (node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eMesh) { + FbxMesh* mesh = (FbxMesh*) node->GetNodeAttribute(); + + // make sure this mesh is valid + if (mesh->GetNode() != nullptr) { + // figure out the number of materials in this mesh + int numMaterials = mesh->GetNode()->GetSrcObjectCount(); + + // enumerate the materials in this mesh + for (int materialIndex = 0; materialIndex < numMaterials; materialIndex++) { + // grab this material + FbxSurfaceMaterial* material = mesh->GetNode()->GetSrcObject(materialIndex); + + if (material) { + // enumerate the textures in this valid material + int textureIndex; + FBXSDK_FOR_EACH_TEXTURE(textureIndex) { + // collect this texture so we know later to bake it + FbxProperty property = material->FindProperty(FbxLayerElement::sTextureChannelNames[textureIndex]); + if (property.IsValid()) { + rewriteAndCollectChannelTextures(property); + } + } + + } + } + + } + } + } + } + + return true; +} + +bool FBXBaker::rewriteAndCollectChannelTextures(FbxProperty& property) { + if (property.IsValid()) { + int textureCount = property.GetSrcObjectCount(); + + // enumerate the textures for this channel + for (int i = 0; i < textureCount; ++i) { + // check if this texture is layered + FbxLayeredTexture* layeredTexture = property.GetSrcObject(i); + if (layeredTexture) { + // enumerate the layers of the layered texture + int numberOfLayers = layeredTexture->GetSrcObjectCount(); + + for (int j = 0; j < numberOfLayers; ++j) { + FbxTexture* texture = layeredTexture->GetSrcObject(j); + rewriteAndCollectTexture(texture); + } + } else { + FbxTexture* texture = property.GetSrcObject(i); + rewriteAndCollectTexture(texture); + } + } + } + + return true; +} + +static const QString BAKED_TEXTURE_DIRECTORY = "textures"; + +bool FBXBaker::rewriteAndCollectTexture(fbxsdk::FbxTexture* texture) { + FbxFileTexture* fileTexture = FbxCast(texture); + if (fileTexture) { + qCDebug(model_baking) << "Flagging" << fileTexture->GetRelativeFileName() << "for bake and re-mapping to .xtk in FBX"; + + // use QFileInfo to easily split up the existing texture filename into its components + QFileInfo textureFileInfo { fileTexture->GetRelativeFileName() }; + + // construct the new baked texture file name + QString bakedTextureFileName { BAKED_TEXTURE_DIRECTORY + "/" + textureFileInfo.baseName() + ".xtk" }; + + // write the new filename into the FBX scene + fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit()); + } + + return true; +} diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h new file mode 100644 index 0000000000..4ebea6f2fa --- /dev/null +++ b/libraries/model-baking/src/FBXBaker.h @@ -0,0 +1,38 @@ +// +// FBXBaker.h +// libraries/model-baking/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 + +Q_DECLARE_LOGGING_CATEGORY(model_baking) + +class FBXBaker { + +public: + FBXBaker(std::string fbxPath); + bool bakeFBX(); + +private: + bool importScene(); + bool rewriteAndCollectSceneTextures(); + bool rewriteAndCollectChannelTextures(FbxProperty& property); + bool rewriteAndCollectTexture(FbxTexture* texture); + + std::string _fbxPath; + FbxManager* _sdkManager; + FbxScene* _scene { nullptr }; +}; + +#endif // hifi_FBXBaker_h diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 5244b68217..473fa707f1 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,4 +2,4 @@ set(TARGET_NAME oven) setup_hifi_project() -find_package(FBXSDK REQUIRED) +link_hifi_libraries(model-baking) From 86208f237b4c31d147f4780c4046d9c905b11388 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 30 Mar 2017 15:17:07 -0700 Subject: [PATCH 005/146] constantize the baked texture extension --- libraries/model-baking/src/FBXBaker.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index af981b471e..e4de73b991 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -136,6 +136,7 @@ bool FBXBaker::rewriteAndCollectChannelTextures(FbxProperty& property) { } static const QString BAKED_TEXTURE_DIRECTORY = "textures"; +static const QString BAKED_TEXTURE_EXT = ".xtk"; bool FBXBaker::rewriteAndCollectTexture(fbxsdk::FbxTexture* texture) { FbxFileTexture* fileTexture = FbxCast(texture); @@ -146,7 +147,7 @@ bool FBXBaker::rewriteAndCollectTexture(fbxsdk::FbxTexture* texture) { QFileInfo textureFileInfo { fileTexture->GetRelativeFileName() }; // construct the new baked texture file name - QString bakedTextureFileName { BAKED_TEXTURE_DIRECTORY + "/" + textureFileInfo.baseName() + ".xtk" }; + QString bakedTextureFileName { BAKED_TEXTURE_DIRECTORY + "/" + textureFileInfo.baseName() + BAKED_TEXTURE_EXT }; // write the new filename into the FBX scene fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit()); From 32c348eb3de6c04f1c464cce5d2706c3d7d522d0 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 11:19:51 -0700 Subject: [PATCH 006/146] use SDK 2017 with new texture discovery strategy --- cmake/modules/FindFBXSDK.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/modules/FindFBXSDK.cmake b/cmake/modules/FindFBXSDK.cmake index 7f6a424aa1..d6e9ed801d 100644 --- a/cmake/modules/FindFBXSDK.cmake +++ b/cmake/modules/FindFBXSDK.cmake @@ -18,9 +18,9 @@ if (NOT FBX_VERSION) if (WIN32) - set(FBX_VERSION 2017.1) + set(FBX_VERSION 2016.1.1) else() - set(FBX_VERSION 2017.0.1) + set(FBX_VERSION 2016.1.1) endif() endif() From 711938fb3d6702a95796be2e90867c07c5d9d9d3 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 11:20:05 -0700 Subject: [PATCH 007/146] lay async foundation for FBXBaker --- libraries/model-baking/src/FBXBaker.cpp | 197 +++++++++++++----------- libraries/model-baking/src/FBXBaker.h | 39 +++-- tools/oven/src/main.cpp | 6 + 3 files changed, 138 insertions(+), 104 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index e4de73b991..41fa0e1a12 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -13,29 +13,44 @@ #include #include +#include #include "FBXBaker.h" Q_LOGGING_CATEGORY(model_baking, "hifi.model-baking"); -FBXBaker::FBXBaker(std::string fbxPath) : +FBXBaker::FBXBaker(QUrl fbxPath) : _fbxPath(fbxPath) { // create an FBX SDK manager _sdkManager = FbxManager::Create(); } -bool FBXBaker::bakeFBX() { +FBXBaker::~FBXBaker() { + _sdkManager->Destroy(); +} - // load the scene from the FBX file - if (importScene()) { - // enumerate the textures found in the scene and bake them - rewriteAndCollectSceneTextures(); +void FBXBaker::start() { + // check if the FBX is local or first needs to be downloaded + if (_fbxPath.isLocalFile()) { + // local file, bake now + bake(); } else { - return false; + // remote file, kick off a download } +} - return true; +void FBXBaker::bake() { + // (1) load the scene from the FBX file + // (2) enumerate the textures found in the scene and bake them + // (3) export the FBX with re-written texture references + // (4) enumerate the collected texture paths and bake the textures + + // a failure at any step along the way stops the chain + importScene() && rewriteAndCollectSceneTextures() && exportScene() && bakeTextures(); + + // emit a signal saying that we are done, with whatever errors were produced + emit finished(_errorList); } bool FBXBaker::importScene() { @@ -43,11 +58,11 @@ bool FBXBaker::importScene() { FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); // import the FBX file at the given path - bool importStatus = importer->Initialize(_fbxPath.c_str()); + bool importStatus = importer->Initialize(_fbxPath.toLocalFile().toLocal8Bit().data()); if (!importStatus) { - // failed to import the FBX file, print an error and return - qCDebug(model_baking) << "Failed to import FBX file at" << _fbxPath.c_str() << "- error:" << importer->GetStatus().GetErrorString(); + // failed to initialize importer, print an error and return + qCDebug(model_baking) << "Failed to import FBX file at" << _fbxPath << "- error:" << importer->GetStatus().GetErrorString(); return false; } @@ -64,93 +79,89 @@ bool FBXBaker::importScene() { return true; } -bool FBXBaker::rewriteAndCollectSceneTextures() { - // grab the root node from the scene - FbxNode* rootNode = _scene->GetRootNode(); - - if (rootNode) { - // enumerate the children of the root node - for (int i = 0; i < rootNode->GetChildCount(); ++i) { - FbxNode* node = rootNode->GetChild(i); - - // check if this child is a mesh node - if (node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eMesh) { - FbxMesh* mesh = (FbxMesh*) node->GetNodeAttribute(); - - // make sure this mesh is valid - if (mesh->GetNode() != nullptr) { - // figure out the number of materials in this mesh - int numMaterials = mesh->GetNode()->GetSrcObjectCount(); - - // enumerate the materials in this mesh - for (int materialIndex = 0; materialIndex < numMaterials; materialIndex++) { - // grab this material - FbxSurfaceMaterial* material = mesh->GetNode()->GetSrcObject(materialIndex); - - if (material) { - // enumerate the textures in this valid material - int textureIndex; - FBXSDK_FOR_EACH_TEXTURE(textureIndex) { - // collect this texture so we know later to bake it - FbxProperty property = material->FindProperty(FbxLayerElement::sTextureChannelNames[textureIndex]); - if (property.IsValid()) { - rewriteAndCollectChannelTextures(property); - } - } - - } - } - - } - } - } - } - - return true; -} - -bool FBXBaker::rewriteAndCollectChannelTextures(FbxProperty& property) { - if (property.IsValid()) { - int textureCount = property.GetSrcObjectCount(); - - // enumerate the textures for this channel - for (int i = 0; i < textureCount; ++i) { - // check if this texture is layered - FbxLayeredTexture* layeredTexture = property.GetSrcObject(i); - if (layeredTexture) { - // enumerate the layers of the layered texture - int numberOfLayers = layeredTexture->GetSrcObjectCount(); - - for (int j = 0; j < numberOfLayers; ++j) { - FbxTexture* texture = layeredTexture->GetSrcObject(j); - rewriteAndCollectTexture(texture); - } - } else { - FbxTexture* texture = property.GetSrcObject(i); - rewriteAndCollectTexture(texture); - } - } - } - - return true; -} - static const QString BAKED_TEXTURE_DIRECTORY = "textures"; -static const QString BAKED_TEXTURE_EXT = ".xtk"; +static const QString BAKED_TEXTURE_EXT = ".ktx"; -bool FBXBaker::rewriteAndCollectTexture(fbxsdk::FbxTexture* texture) { - FbxFileTexture* fileTexture = FbxCast(texture); - if (fileTexture) { - qCDebug(model_baking) << "Flagging" << fileTexture->GetRelativeFileName() << "for bake and re-mapping to .xtk in FBX"; +static const QString EXPORT_PATH { "/Users/birarda/code/hifi/lod/test-oven/export/DiscGolfBasket.ktx.fbx" }; - // use QFileInfo to easily split up the existing texture filename into its components - QFileInfo textureFileInfo { fileTexture->GetRelativeFileName() }; +bool FBXBaker::rewriteAndCollectSceneTextures() { + // get a count of the textures used in the scene + int numTextures = _scene->GetTextureCount(); - // construct the new baked texture file name - QString bakedTextureFileName { BAKED_TEXTURE_DIRECTORY + "/" + textureFileInfo.baseName() + BAKED_TEXTURE_EXT }; + // enumerate the textures in the scene + for (int i = 0; i < numTextures; i++) { + // grab each file texture + FbxFileTexture* fileTexture = FbxCast(_scene->GetTexture(i)); - // write the new filename into the FBX scene - fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit()); + if (fileTexture) { + // use QFileInfo to easily split up the existing texture filename into its components + QFileInfo textureFileInfo { fileTexture->GetFileName() }; + + // make sure this texture points to something + if (!textureFileInfo.filePath().isEmpty()) { + + // construct the new baked texture file name and file path + QString bakedTextureFileName { textureFileInfo.baseName() + BAKED_TEXTURE_EXT }; + QString bakedTextureFilePath { QFileInfo(EXPORT_PATH).absolutePath() + "/textures/" + bakedTextureFileName }; + + qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; + + // write the new filename into the FBX scene + fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); + + // add the texture to the list of textures needing to be baked + if (textureFileInfo.exists() && textureFileInfo.isFile()) { + // append the URL to the local texture that we have confirmed exists + _unbakedTextures.append(QUrl::fromLocalFile(textureFileInfo.absoluteFilePath())); + } else { + + } + } + } + } + + return true; +} + +bool FBXBaker::exportScene() { + // setup the exporter + FbxExporter* exporter = FbxExporter::Create(_sdkManager, ""); + + bool exportStatus = exporter->Initialize(EXPORT_PATH.toLocal8Bit().data()); + + if (!exportStatus) { + // failed to initialize exporter, print an error and return + qCDebug(model_baking) << "Failed to export FBX file at" << _fbxPath + << "to" << EXPORT_PATH << "- error:" << exporter->GetStatus().GetErrorString(); + + return false; + } + + // export the scene + exporter->Export(_scene); + + return true; +} + +bool FBXBaker::bakeTextures() { + // enumerate the list of unbaked textures + foreach(const QUrl& textureUrl, _unbakedTextures) { + qCDebug(model_baking) << "Baking texture at" << textureUrl; + + if (textureUrl.isLocalFile()) { + // this is a local file that we've already determined is available on the filesystem + + // load the file + QFile localTexture { textureUrl.toLocalFile() }; + + if (!localTexture.open(QIODevice::ReadOnly)) { + // add an error to the list stating that this texture couldn't be baked because it could not be loaded + } + + // call the image library to produce a compressed KTX for this image + } else { + // this is a remote texture that we'll need to download first + } } return true; diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 4ebea6f2fa..0ae8311522 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -12,27 +12,44 @@ #ifndef hifi_FBXBaker_h #define hifi_FBXBaker_h -#include - #include +#include Q_DECLARE_LOGGING_CATEGORY(model_baking) -class FBXBaker { +namespace fbxsdk { + class FbxManager; + class FbxProperty; + class FbxScene; + class FbxTexture; +} +class FBXBaker : public QObject { + Q_OBJECT public: - FBXBaker(std::string fbxPath); - bool bakeFBX(); + FBXBaker(QUrl fbxPath); + ~FBXBaker(); + + void start(); + +signals: + void finished(QStringList errorList); private: + void bake(); bool importScene(); bool rewriteAndCollectSceneTextures(); - bool rewriteAndCollectChannelTextures(FbxProperty& property); - bool rewriteAndCollectTexture(FbxTexture* texture); - - std::string _fbxPath; - FbxManager* _sdkManager; - FbxScene* _scene { nullptr }; + bool exportScene(); + bool bakeTextures(); + bool bakeTexture(); + + QUrl _fbxPath; + fbxsdk::FbxManager* _sdkManager; + fbxsdk::FbxScene* _scene { nullptr }; + + QStringList _errorList; + + QList _unbakedTextures; }; #endif // hifi_FBXBaker_h diff --git a/tools/oven/src/main.cpp b/tools/oven/src/main.cpp index 831ba00328..e74df068dd 100644 --- a/tools/oven/src/main.cpp +++ b/tools/oven/src/main.cpp @@ -8,7 +8,13 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +#include + int main (int argc, char** argv) { + + FBXBaker baker(QUrl("file:///Users/birarda/code/hifi/lod/test-oven/DiscGolfBasket.fbx")); + baker.start(); + return 0; } From 1b30afa03ebd72c1b6d33bf821dc8a9acfffc80b Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 15:20:19 -0700 Subject: [PATCH 008/146] add basic Oven QCoreApplication, start to output results --- libraries/model-baking/CMakeLists.txt | 2 + libraries/model-baking/src/FBXBaker.cpp | 123 +++++++++++++++++++++--- libraries/model-baking/src/FBXBaker.h | 22 ++++- tools/oven/src/Oven.cpp | 23 +++++ tools/oven/src/Oven.h | 30 ++++++ tools/oven/src/main.cpp | 10 +- 6 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 tools/oven/src/Oven.cpp create mode 100644 tools/oven/src/Oven.h diff --git a/libraries/model-baking/CMakeLists.txt b/libraries/model-baking/CMakeLists.txt index 45c0350bbe..487b8536c9 100644 --- a/libraries/model-baking/CMakeLists.txt +++ b/libraries/model-baking/CMakeLists.txt @@ -2,6 +2,8 @@ set(TARGET_NAME model-baking) setup_hifi_library() +link_hifi_libraries(networking) + find_package(FBXSDK REQUIRED) target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR}) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 41fa0e1a12..0be4222f59 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -10,33 +10,117 @@ // #include -#include +#include #include -#include + +#include #include "FBXBaker.h" Q_LOGGING_CATEGORY(model_baking, "hifi.model-baking"); -FBXBaker::FBXBaker(QUrl fbxPath) : - _fbxPath(fbxPath) +FBXBaker::FBXBaker(QUrl fbxURL, QString baseOutputPath) : + _fbxURL(fbxURL), + _baseOutputPath(baseOutputPath) { // create an FBX SDK manager _sdkManager = FbxManager::Create(); + + // grab the name of the FBX from the URL, this is used for folder output names + auto fileName = fbxURL.fileName(); + _fbxName = fileName.left(fileName.indexOf('.')); } FBXBaker::~FBXBaker() { _sdkManager->Destroy(); } + void FBXBaker::start() { + // setup the output folder for the results of this bake + if (!setupOutputFolder()) { + return; + } + // check if the FBX is local or first needs to be downloaded - if (_fbxPath.isLocalFile()) { - // local file, bake now + if (_fbxURL.isLocalFile()) { + // load up the local file + QFile localFBX { _fbxURL.toLocalFile() }; + + // make a copy in the output folder + localFBX.copy(_uniqueOutputPath + _fbxURL.fileName()); + + // start the bake now that we have everything in place bake(); } 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.setUrl(_fbxURL); + + qCDebug(model_baking) << "Downloading" << _fbxURL; + + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); + } +} + +bool 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)) { + qCWarning(model_baking) << "Failed to created FBX output folder" << _uniqueOutputPath; + + emit finished(); + return false; + } + + return true; +} + +void FBXBaker::handleFBXNetworkReply() { + QNetworkReply* 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(_uniqueOutputPath + _fbxURL.fileName()); + + 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 could not be made + emit finished(); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + // kick off the bake process now that everything is ready to go + bake(); + } else { + qDebug() << "ERROR DOWNLOADING FBX" << requestReply->errorString(); } } @@ -46,25 +130,31 @@ void FBXBaker::bake() { // (3) export the FBX with re-written texture references // (4) enumerate the collected texture paths and bake the textures + qCDebug(model_baking) << "Baking" << _fbxURL; + // a failure at any step along the way stops the chain importScene() && rewriteAndCollectSceneTextures() && exportScene() && bakeTextures(); // emit a signal saying that we are done, with whatever errors were produced - emit finished(_errorList); + emit finished(); } bool FBXBaker::importScene() { // create an FBX SDK importer FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); - // import the FBX file at the given path - bool importStatus = importer->Initialize(_fbxPath.toLocalFile().toLocal8Bit().data()); + // import the copy of the original FBX file + QString originalCopyPath = _uniqueOutputPath + _fbxURL.fileName(); + bool importStatus = importer->Initialize(originalCopyPath.toLocal8Bit().data()); if (!importStatus) { // failed to initialize importer, print an error and return - qCDebug(model_baking) << "Failed to import FBX file at" << _fbxPath << "- error:" << importer->GetStatus().GetErrorString(); + qCCritical(model_baking) << "Failed to import FBX file at" << _fbxURL + << "- error:" << importer->GetStatus().GetErrorString(); return false; + } else { + qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene"; } // setup a new scene to hold the imported file @@ -82,8 +172,6 @@ bool FBXBaker::importScene() { static const QString BAKED_TEXTURE_DIRECTORY = "textures"; static const QString BAKED_TEXTURE_EXT = ".ktx"; -static const QString EXPORT_PATH { "/Users/birarda/code/hifi/lod/test-oven/export/DiscGolfBasket.ktx.fbx" }; - bool FBXBaker::rewriteAndCollectSceneTextures() { // get a count of the textures used in the scene int numTextures = _scene->GetTextureCount(); @@ -102,7 +190,7 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { // construct the new baked texture file name and file path QString bakedTextureFileName { textureFileInfo.baseName() + BAKED_TEXTURE_EXT }; - QString bakedTextureFilePath { QFileInfo(EXPORT_PATH).absolutePath() + "/textures/" + bakedTextureFileName }; + QString bakedTextureFilePath { _uniqueOutputPath + "ktx/" + bakedTextureFileName }; qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; @@ -127,12 +215,13 @@ bool FBXBaker::exportScene() { // setup the exporter FbxExporter* exporter = FbxExporter::Create(_sdkManager, ""); - bool exportStatus = exporter->Initialize(EXPORT_PATH.toLocal8Bit().data()); + auto rewrittenFBXPath = _uniqueOutputPath + _fbxName + ".ktx.fbx"; + bool exportStatus = exporter->Initialize(rewrittenFBXPath.toLocal8Bit().data()); if (!exportStatus) { // failed to initialize exporter, print an error and return - qCDebug(model_baking) << "Failed to export FBX file at" << _fbxPath - << "to" << EXPORT_PATH << "- error:" << exporter->GetStatus().GetErrorString(); + qCCritical(model_baking) << "Failed to export FBX file at" << _fbxURL + << "to" << rewrittenFBXPath << "- error:" << exporter->GetStatus().GetErrorString(); return false; } @@ -140,6 +229,8 @@ bool FBXBaker::exportScene() { // export the scene exporter->Export(_scene); + qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << rewrittenFBXPath; + return true; } diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 0ae8311522..3bd721253f 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -12,8 +12,10 @@ #ifndef hifi_FBXBaker_h #define hifi_FBXBaker_h -#include -#include +#include +#include +#include +#include Q_DECLARE_LOGGING_CATEGORY(model_baking) @@ -27,23 +29,33 @@ namespace fbxsdk { class FBXBaker : public QObject { Q_OBJECT public: - FBXBaker(QUrl fbxPath); + FBXBaker(QUrl fbxURL, QString baseOutputPath); ~FBXBaker(); void start(); signals: - void finished(QStringList errorList); + void finished(); +private slots: + void handleFBXNetworkReply(); + private: void bake(); + + bool setupOutputFolder(); bool importScene(); bool rewriteAndCollectSceneTextures(); bool exportScene(); bool bakeTextures(); bool bakeTexture(); - QUrl _fbxPath; + QUrl _fbxURL; + QString _fbxName; + + QString _baseOutputPath; + QString _uniqueOutputPath; + fbxsdk::FbxManager* _sdkManager; fbxsdk::FbxScene* _scene { nullptr }; diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp new file mode 100644 index 0000000000..b517da8151 --- /dev/null +++ b/tools/oven/src/Oven.cpp @@ -0,0 +1,23 @@ +// +// 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 "Oven.h" + +static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/export"; + +Oven::Oven(int argc, char* argv[]) : + QCoreApplication(argc, argv), + _testBake(QUrl("file:///Users/birarda/code/hifi/lod/test-oven/DiscGolfBasket.fbx"), OUTPUT_FOLDER) +{ + _testBake.start(); +} diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h new file mode 100644 index 0000000000..72de77b889 --- /dev/null +++ b/tools/oven/src/Oven.h @@ -0,0 +1,30 @@ +// +// 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 + +class Oven : public QCoreApplication { + Q_OBJECT + +public: + Oven(int argc, char* argv[]); + +private: + FBXBaker _testBake; +}; + + +#endif // hifi_Oven_h diff --git a/tools/oven/src/main.cpp b/tools/oven/src/main.cpp index e74df068dd..9c778245b5 100644 --- a/tools/oven/src/main.cpp +++ b/tools/oven/src/main.cpp @@ -8,13 +8,9 @@ // 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" int main (int argc, char** argv) { - - FBXBaker baker(QUrl("file:///Users/birarda/code/hifi/lod/test-oven/DiscGolfBasket.fbx")); - baker.start(); - - return 0; + Oven app(argc, argv); + return app.exec(); } From 6af7ecf47b7b3c553249456b14dbd517fe701fcd Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 17:03:17 -0700 Subject: [PATCH 009/146] add subfolders in output, add logic to find linked textures --- libraries/model-baking/src/FBXBaker.cpp | 79 +++++++++++++++++++++---- libraries/model-baking/src/FBXBaker.h | 4 +- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 0be4222f59..589e74ab77 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -36,8 +36,16 @@ FBXBaker::~FBXBaker() { _sdkManager->Destroy(); } +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::start() { + qCDebug(model_baking) << "Baking" << _fbxURL; + // setup the output folder for the results of this bake if (!setupOutputFolder()) { return; @@ -49,7 +57,7 @@ void FBXBaker::start() { QFile localFBX { _fbxURL.toLocalFile() }; // make a copy in the output folder - localFBX.copy(_uniqueOutputPath + _fbxURL.fileName()); + localFBX.copy(pathToCopyOfOriginal()); // start the bake now that we have everything in place bake(); @@ -87,7 +95,16 @@ bool FBXBaker::setupOutputFolder() { // attempt to make the output folder if (!QDir().mkdir(_uniqueOutputPath)) { - qCWarning(model_baking) << "Failed to created FBX output folder" << _uniqueOutputPath; + qCCritical(model_baking) << "Failed to create FBX output folder" << _uniqueOutputPath; + + emit finished(); + return false; + } + + // make the baked and original sub-folders used during export + QDir uniqueOutputDir = _uniqueOutputPath; + if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) { + qCCritical(model_baking) << "Failed to create baked/original subfolders in" << _uniqueOutputPath; emit finished(); return false; @@ -103,7 +120,7 @@ void FBXBaker::handleFBXNetworkReply() { qCDebug(model_baking) << "Downloaded" << _fbxURL; // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_uniqueOutputPath + _fbxURL.fileName()); + QFile copyOfOriginal(pathToCopyOfOriginal()); qDebug(model_baking) << "Writing copy of original FBX to" << copyOfOriginal.fileName(); @@ -130,8 +147,6 @@ void FBXBaker::bake() { // (3) export the FBX with re-written texture references // (4) enumerate the collected texture paths and bake the textures - qCDebug(model_baking) << "Baking" << _fbxURL; - // a failure at any step along the way stops the chain importScene() && rewriteAndCollectSceneTextures() && exportScene() && bakeTextures(); @@ -144,7 +159,7 @@ bool FBXBaker::importScene() { FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); // import the copy of the original FBX file - QString originalCopyPath = _uniqueOutputPath + _fbxURL.fileName(); + QString originalCopyPath = pathToCopyOfOriginal(); bool importStatus = importer->Initialize(originalCopyPath.toLocal8Bit().data()); if (!importStatus) { @@ -169,7 +184,7 @@ bool FBXBaker::importScene() { return true; } -static const QString BAKED_TEXTURE_DIRECTORY = "textures"; +static const QString BAKED_TEXTURE_DIRECTORY = "textures/"; static const QString BAKED_TEXTURE_EXT = ".ktx"; bool FBXBaker::rewriteAndCollectSceneTextures() { @@ -190,7 +205,7 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { // construct the new baked texture file name and file path QString bakedTextureFileName { textureFileInfo.baseName() + BAKED_TEXTURE_EXT }; - QString bakedTextureFilePath { _uniqueOutputPath + "ktx/" + bakedTextureFileName }; + QString bakedTextureFilePath { _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + BAKED_TEXTURE_DIRECTORY + bakedTextureFileName }; qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; @@ -200,9 +215,51 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { // add the texture to the list of textures needing to be baked if (textureFileInfo.exists() && textureFileInfo.isFile()) { // append the URL to the local texture that we have confirmed exists - _unbakedTextures.append(QUrl::fromLocalFile(textureFileInfo.absoluteFilePath())); + _unbakedTextures.insert(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("\\", "/")); + +#ifndef Q_OS_WIN + // it turns out that paths that start with a drive letter and a colon appear to QFileInfo + // as a relative path on UNIX systems - we perform a special check here to handle that case + bool isAbsolute = relativeFileName[1] == ':' || apparentRelativePath.isAbsolute(); +#else + bool isAbsolute = apparentRelativePath.isAbsolute(); +#endif + + if (isAbsolute) { + // this is a relative file path which will require different handling + // depending on the location of the original FBX + if (_fbxURL.isLocalFile()) { + // since the loaded FBX is loaded, first check if we actually have the texture locally + // at the absolute path + if (apparentRelativePath.exists() && apparentRelativePath.isFile()) { + // the absolute path we ran into for the texture in the FBX exists on this machine + // so use that file + _unbakedTextures.insert(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 + _unbakedTextures.insert(_fbxURL.resolved(apparentRelativePath.fileName())); + } + } else { + // the original FBX was remote and downloaded + + // since this "relative" texture path is actually absolute, we have to assume it is beside the FBX + // which matches the behaviour of Interface + + // append that path to our list of unbaked textures + _unbakedTextures.insert(_fbxURL.resolved(apparentRelativePath.fileName())); + } + } else { + // simply construct a URL with the relative path to the asset, locally or remotely + // and append that to the list of unbaked textures + _unbakedTextures.insert(_fbxURL.resolved(apparentRelativePath.filePath())); + } } } } @@ -211,11 +268,13 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { return true; } +static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; + bool FBXBaker::exportScene() { // setup the exporter FbxExporter* exporter = FbxExporter::Create(_sdkManager, ""); - auto rewrittenFBXPath = _uniqueOutputPath + _fbxName + ".ktx.fbx"; + auto rewrittenFBXPath = _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + _fbxName + BAKED_FBX_EXTENSION; bool exportStatus = exporter->Initialize(rewrittenFBXPath.toLocal8Bit().data()); if (!exportStatus) { diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 3bd721253f..da1bf10d34 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -50,6 +50,8 @@ private: bool bakeTextures(); bool bakeTexture(); + QString pathToCopyOfOriginal() const; + QUrl _fbxURL; QString _fbxName; @@ -61,7 +63,7 @@ private: QStringList _errorList; - QList _unbakedTextures; + QSet _unbakedTextures; }; #endif // hifi_FBXBaker_h From c9bc22334f3ba08578706ce32a24cf2b968da710 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 17:12:29 -0700 Subject: [PATCH 010/146] add embedded media folder removal to FBXBaker --- libraries/model-baking/src/FBXBaker.cpp | 11 ++++++++++- libraries/model-baking/src/FBXBaker.h | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 589e74ab77..0ff6b39704 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -148,7 +148,7 @@ void FBXBaker::bake() { // (4) enumerate the collected texture paths and bake the textures // a failure at any step along the way stops the chain - importScene() && rewriteAndCollectSceneTextures() && exportScene() && bakeTextures(); + importScene() && rewriteAndCollectSceneTextures() && exportScene() && bakeTextures() && removeEmbeddedMediaFolder(); // emit a signal saying that we are done, with whatever errors were produced emit finished(); @@ -316,3 +316,12 @@ bool FBXBaker::bakeTextures() { return true; } + +bool 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(); + + // we always return true because a failure to delete the embedded media folder is not a failure of the bake + return true; +} diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index da1bf10d34..a6cf64ebaf 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -49,6 +49,7 @@ private: bool exportScene(); bool bakeTextures(); bool bakeTexture(); + bool removeEmbeddedMediaFolder(); QString pathToCopyOfOriginal() const; From 95ce9d1b25d549a40b2727e316d93f5933ecbca7 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 17:32:15 -0700 Subject: [PATCH 011/146] add handling for multiple textures with same base name --- libraries/model-baking/src/FBXBaker.cpp | 40 ++++++++++++++++++++----- libraries/model-baking/src/FBXBaker.h | 3 +- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 0ff6b39704..6717cbf69e 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -204,7 +204,24 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { if (!textureFileInfo.filePath().isEmpty()) { // construct the new baked texture file name and file path - QString bakedTextureFileName { textureFileInfo.baseName() + BAKED_TEXTURE_EXT }; + + // 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.baseName() }; + + 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; + QString bakedTextureFilePath { _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + BAKED_TEXTURE_DIRECTORY + bakedTextureFileName }; qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; @@ -212,10 +229,12 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { // write the new filename into the FBX scene fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); + QUrl urlToTexture; + // add the texture to the list of textures needing to be baked if (textureFileInfo.exists() && textureFileInfo.isFile()) { - // append the URL to the local texture that we have confirmed exists - _unbakedTextures.insert(QUrl::fromLocalFile(textureFileInfo.absoluteFilePath())); + // 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 @@ -240,11 +259,11 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { if (apparentRelativePath.exists() && apparentRelativePath.isFile()) { // the absolute path we ran into for the texture in the FBX exists on this machine // so use that file - _unbakedTextures.insert(QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath())); + 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 - _unbakedTextures.insert(_fbxURL.resolved(apparentRelativePath.fileName())); + urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); } } else { // the original FBX was remote and downloaded @@ -253,14 +272,17 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { // which matches the behaviour of Interface // append that path to our list of unbaked textures - _unbakedTextures.insert(_fbxURL.resolved(apparentRelativePath.fileName())); + urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); } } else { // simply construct a URL with the relative path to the asset, locally or remotely // and append that to the list of unbaked textures - _unbakedTextures.insert(_fbxURL.resolved(apparentRelativePath.filePath())); + urlToTexture = _fbxURL.resolved(apparentRelativePath.filePath()); } } + + // add the deduced url to the texture, associated with the resulting baked texture file name, to our hash + _unbakedTextures.insert(urlToTexture, bakedTextureFileName); } } } @@ -295,7 +317,9 @@ bool FBXBaker::exportScene() { bool FBXBaker::bakeTextures() { // enumerate the list of unbaked textures - foreach(const QUrl& textureUrl, _unbakedTextures) { + for (auto it = _unbakedTextures.begin(); it != _unbakedTextures.end(); ++it) { + auto& textureUrl = it.key(); + qCDebug(model_baking) << "Baking texture at" << textureUrl; if (textureUrl.isLocalFile()) { diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index a6cf64ebaf..fe28922f66 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -64,7 +64,8 @@ private: QStringList _errorList; - QSet _unbakedTextures; + QHash _unbakedTextures; + QHash _textureNameMatchCount; }; #endif // hifi_FBXBaker_h From e75f17a7f9bedfdfde1ca8c102d0937b358c7b46 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 18:10:52 -0700 Subject: [PATCH 012/146] add a TextureBaker called from the FBXBaker --- libraries/model-baking/src/FBXBaker.cpp | 47 +++-------- libraries/model-baking/src/FBXBaker.h | 13 +-- .../src/ModelBakingLoggingCategory.cpp | 14 ++++ .../src/ModelBakingLoggingCategory.h | 19 +++++ libraries/model-baking/src/TextureBaker.cpp | 79 +++++++++++++++++++ libraries/model-baking/src/TextureBaker.h | 43 ++++++++++ 6 files changed, 175 insertions(+), 40 deletions(-) create mode 100644 libraries/model-baking/src/ModelBakingLoggingCategory.cpp create mode 100644 libraries/model-baking/src/ModelBakingLoggingCategory.h create mode 100644 libraries/model-baking/src/TextureBaker.cpp create mode 100644 libraries/model-baking/src/TextureBaker.h diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 6717cbf69e..3370698738 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -16,9 +16,11 @@ #include +#include "ModelBakingLoggingCategory.h" +#include "TextureBaker.h" + #include "FBXBaker.h" -Q_LOGGING_CATEGORY(model_baking, "hifi.model-baking"); FBXBaker::FBXBaker(QUrl fbxURL, QString baseOutputPath) : _fbxURL(fbxURL), @@ -143,15 +145,12 @@ void FBXBaker::handleFBXNetworkReply() { void FBXBaker::bake() { // (1) load the scene from the FBX file - // (2) enumerate the textures found in the scene and bake them + // (2) enumerate the textures found in the scene and start a bake for them // (3) export the FBX with re-written texture references - // (4) enumerate the collected texture paths and bake the textures - // a failure at any step along the way stops the chain - importScene() && rewriteAndCollectSceneTextures() && exportScene() && bakeTextures() && removeEmbeddedMediaFolder(); - - // emit a signal saying that we are done, with whatever errors were produced - emit finished(); + importScene(); + rewriteAndBakeSceneTextures(); + exportScene(); } bool FBXBaker::importScene() { @@ -187,7 +186,7 @@ bool FBXBaker::importScene() { static const QString BAKED_TEXTURE_DIRECTORY = "textures/"; static const QString BAKED_TEXTURE_EXT = ".ktx"; -bool FBXBaker::rewriteAndCollectSceneTextures() { +bool FBXBaker::rewriteAndBakeSceneTextures() { // get a count of the textures used in the scene int numTextures = _scene->GetTextureCount(); @@ -283,6 +282,11 @@ bool FBXBaker::rewriteAndCollectSceneTextures() { // add the deduced url to the texture, associated with the resulting baked texture file name, to our hash _unbakedTextures.insert(urlToTexture, bakedTextureFileName); + + // start a bake for this texture and add it to our list to keep track of + auto bakingTexture = new TextureBaker(urlToTexture); + bakingTexture->start(); + _bakingTextures.emplace_back(bakingTexture); } } } @@ -315,31 +319,6 @@ bool FBXBaker::exportScene() { return true; } -bool FBXBaker::bakeTextures() { - // enumerate the list of unbaked textures - for (auto it = _unbakedTextures.begin(); it != _unbakedTextures.end(); ++it) { - auto& textureUrl = it.key(); - - qCDebug(model_baking) << "Baking texture at" << textureUrl; - - if (textureUrl.isLocalFile()) { - // this is a local file that we've already determined is available on the filesystem - - // load the file - QFile localTexture { textureUrl.toLocalFile() }; - - if (!localTexture.open(QIODevice::ReadOnly)) { - // add an error to the list stating that this texture couldn't be baked because it could not be loaded - } - - // call the image library to produce a compressed KTX for this image - } else { - // this is a remote texture that we'll need to download first - } - } - - return true; -} bool FBXBaker::removeEmbeddedMediaFolder() { // now that the bake is complete, remove the embedded media folder produced by the FBX SDK when it imports an FBX diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index fe28922f66..78a0dde0bb 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -13,12 +13,9 @@ #define hifi_FBXBaker_h #include -#include #include #include -Q_DECLARE_LOGGING_CATEGORY(model_baking) - namespace fbxsdk { class FbxManager; class FbxProperty; @@ -26,6 +23,8 @@ namespace fbxsdk { class FbxTexture; } +class TextureBaker; + class FBXBaker : public QObject { Q_OBJECT public: @@ -45,10 +44,8 @@ private: bool setupOutputFolder(); bool importScene(); - bool rewriteAndCollectSceneTextures(); + bool rewriteAndBakeSceneTextures(); bool exportScene(); - bool bakeTextures(); - bool bakeTexture(); bool removeEmbeddedMediaFolder(); QString pathToCopyOfOriginal() const; @@ -66,6 +63,10 @@ private: QHash _unbakedTextures; QHash _textureNameMatchCount; + + QHash _downloadedTextures; + + std::list> _bakingTextures; }; #endif // hifi_FBXBaker_h diff --git a/libraries/model-baking/src/ModelBakingLoggingCategory.cpp b/libraries/model-baking/src/ModelBakingLoggingCategory.cpp new file mode 100644 index 0000000000..c2ad6360d2 --- /dev/null +++ b/libraries/model-baking/src/ModelBakingLoggingCategory.cpp @@ -0,0 +1,14 @@ +// +// ModelBakingLoggingCategory.cpp +// libraries/model-baking/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/libraries/model-baking/src/ModelBakingLoggingCategory.h b/libraries/model-baking/src/ModelBakingLoggingCategory.h new file mode 100644 index 0000000000..600618ed5e --- /dev/null +++ b/libraries/model-baking/src/ModelBakingLoggingCategory.h @@ -0,0 +1,19 @@ +// +// ModelBakingLoggingCategory.h +// libraries/model-baking/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/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp new file mode 100644 index 0000000000..ce86b18479 --- /dev/null +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -0,0 +1,79 @@ +// +// TextureBaker.cpp +// libraries/model-baker/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 "ModelBakingLoggingCategory.h" + +#include "TextureBaker.h" + +TextureBaker::TextureBaker(const QUrl& textureURL) : + _textureURL(textureURL) +{ + +} + +void TextureBaker::start() { + + // 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)) { + qCWarning(model_baking) << "Unable to open local texture at" << _textureURL << "for baking"; + + emit finished(); + return; + } + + _originalTexture = localTexture.readAll(); + + // start the bake now that we have everything in place + bake(); + } 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.setUrl(_textureURL); + + qCDebug(model_baking) << "Downloading" << _textureURL; + + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply); + } +} + +void TextureBaker::handleTextureNetworkReply() { + QNetworkReply* requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded texture at" << _textureURL; + _originalTexture = requestReply->readAll(); + } else { + qCDebug(model_baking) << "Error downloading texture" << requestReply->errorString(); + } +} + +void TextureBaker::bake() { + qCDebug(model_baking) << "Baking texture at" << _textureURL; + + // call image library to asynchronously bake this texture +} diff --git a/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h new file mode 100644 index 0000000000..5544352005 --- /dev/null +++ b/libraries/model-baking/src/TextureBaker.h @@ -0,0 +1,43 @@ +// +// TextureBaker.h +// libraries/model-baker/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 + +class TextureBaker : public QObject { + Q_OBJECT + +public: + TextureBaker(const QUrl& textureURL); + + void start(); + + const QByteArray& getOriginalTexture() const { return _originalTexture; } + + const QUrl& getTextureURL() const { return _textureURL; } + +signals: + void finished(); + +private slots: + void handleTextureNetworkReply(); + +private: + void bake(); + + QUrl _textureURL; + QByteArray _originalTexture; +}; + +#endif // hifi_TextureBaker_h From 1a17da6ecc447115dc88724fcff2e6371f0f04f2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 5 Apr 2017 18:21:53 -0700 Subject: [PATCH 013/146] add helper to deduce texture relative URL after download --- libraries/model-baking/src/FBXBaker.cpp | 13 +++++++++++++ libraries/model-baking/src/TextureBaker.cpp | 2 ++ 2 files changed, 15 insertions(+) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 3370698738..5f9037ddb9 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -186,6 +186,19 @@ bool FBXBaker::importScene() { static const QString BAKED_TEXTURE_DIRECTORY = "textures/"; static const QString BAKED_TEXTURE_EXT = ".ktx"; +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 ""; + } +} + bool FBXBaker::rewriteAndBakeSceneTextures() { // get a count of the textures used in the scene int numTextures = _scene->GetTextureCount(); diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index ce86b18479..4f21ccd624 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -66,6 +66,8 @@ void TextureBaker::handleTextureNetworkReply() { if (requestReply->error() == QNetworkReply::NoError) { qCDebug(model_baking) << "Downloaded texture at" << _textureURL; + + // store the original texture so it can be passed along for the bake _originalTexture = requestReply->readAll(); } else { qCDebug(model_baking) << "Error downloading texture" << requestReply->errorString(); From 679580a620b0fc51f546cc9bb30b8ca5ff5bb094 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 6 Apr 2017 10:39:23 -0700 Subject: [PATCH 014/146] don't save copy of embedded raw textures --- libraries/model-baking/src/FBXBaker.cpp | 85 ++++++++++++++++----- libraries/model-baking/src/FBXBaker.h | 5 +- libraries/model-baking/src/TextureBaker.cpp | 4 + 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 5f9037ddb9..aae48a51a5 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -39,10 +39,10 @@ FBXBaker::~FBXBaker() { } static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; -static const QString ORIGINAL_OUTPUT_SUBFOLDER = "original/"; +static const QString RAW_OUTPUT_SUBFOLDER = "raw/"; -QString FBXBaker::pathToCopyOfOriginal() const { - return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); +QString FBXBaker::pathToCopyOfRaw() const { + return _uniqueOutputPath + RAW_OUTPUT_SUBFOLDER + _fbxURL.fileName(); } void FBXBaker::start() { @@ -59,7 +59,7 @@ void FBXBaker::start() { QFile localFBX { _fbxURL.toLocalFile() }; // make a copy in the output folder - localFBX.copy(pathToCopyOfOriginal()); + localFBX.copy(pathToCopyOfRaw()); // start the bake now that we have everything in place bake(); @@ -103,10 +103,10 @@ bool FBXBaker::setupOutputFolder() { return false; } - // make the baked and original sub-folders used during export + // make the baked and raw sub-folders used during export QDir uniqueOutputDir = _uniqueOutputPath; - if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) { - qCCritical(model_baking) << "Failed to create baked/original subfolders in" << _uniqueOutputPath; + if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(RAW_OUTPUT_SUBFOLDER)) { + qCCritical(model_baking) << "Failed to create baked/raw subfolders in" << _uniqueOutputPath; emit finished(); return false; @@ -122,19 +122,19 @@ void FBXBaker::handleFBXNetworkReply() { qCDebug(model_baking) << "Downloaded" << _fbxURL; // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(pathToCopyOfOriginal()); + QFile copyOfRaw(pathToCopyOfRaw()); - qDebug(model_baking) << "Writing copy of original FBX to" << copyOfOriginal.fileName(); + qDebug(model_baking) << "Writing copy of raw FBX to" << copyOfRaw.fileName(); - if (!copyOfOriginal.open(QIODevice::WriteOnly) || (copyOfOriginal.write(requestReply->readAll()) == -1)) { + if (!copyOfRaw.open(QIODevice::WriteOnly) || (copyOfRaw.write(requestReply->readAll()) == -1)) { - // add an error to the error list for this FBX stating that a duplicate of the original could not be made + // add an error to the error list for this FBX stating that a duplicate of the raw FBX could not be made emit finished(); return; } // close that file now that we are done writing to it - copyOfOriginal.close(); + copyOfRaw.close(); // kick off the bake process now that everything is ready to go bake(); @@ -157,9 +157,9 @@ bool FBXBaker::importScene() { // create an FBX SDK importer FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); - // import the copy of the original FBX file - QString originalCopyPath = pathToCopyOfOriginal(); - bool importStatus = importer->Initialize(originalCopyPath.toLocal8Bit().data()); + // import the copy of the raw FBX file + QString rawCopyPath = pathToCopyOfRaw(); + bool importStatus = importer->Initialize(rawCopyPath.toLocal8Bit().data()); if (!importStatus) { // failed to initialize importer, print an error and return @@ -296,10 +296,8 @@ bool FBXBaker::rewriteAndBakeSceneTextures() { // add the deduced url to the texture, associated with the resulting baked texture file name, to our hash _unbakedTextures.insert(urlToTexture, bakedTextureFileName); - // start a bake for this texture and add it to our list to keep track of - auto bakingTexture = new TextureBaker(urlToTexture); - bakingTexture->start(); - _bakingTextures.emplace_back(bakingTexture); + // bake this texture asynchronously + bakeTexture(urlToTexture); } } } @@ -307,6 +305,53 @@ bool FBXBaker::rewriteAndBakeSceneTextures() { return true; } +void FBXBaker::bakeTexture(const QUrl& textureURL) { + // start a bake for this texture and add it to our list to keep track of + auto bakingTexture = new TextureBaker(textureURL); + + connect(bakingTexture, &TextureBaker::finished, this, &FBXBaker::handleBakedTexture); + + bakingTexture->start(); + + _bakingTextures.emplace_back(bakingTexture); +} + +void FBXBaker::handleBakedTexture() { + auto bakedTexture = qobject_cast(sender()); + + // 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 unbaked output folder + // + + auto rawOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + RAW_OUTPUT_SUBFOLDER); + + if (!rawOutputFolder.isParentOf(bakedTexture->getTextureURL())) { + // for linked textures we want to save a copy of original texture beside the original FBX + + qCDebug(model_baking) << "Saving raw 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 + RAW_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 { + qCWarning(model_baking) << "Could not save original external texture" << originalTextureFile.fileName() + << "for" << _fbxURL; + } + } +} + static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; bool FBXBaker::exportScene() { @@ -336,7 +381,7 @@ bool FBXBaker::exportScene() { bool 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(); + QDir(_uniqueOutputPath + RAW_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively(); // we always return true because a failure to delete the embedded media folder is not a failure of the bake return true; diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 78a0dde0bb..b8638372f2 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -38,6 +38,7 @@ signals: private slots: void handleFBXNetworkReply(); + void handleBakedTexture(); private: void bake(); @@ -48,7 +49,9 @@ private: bool exportScene(); bool removeEmbeddedMediaFolder(); - QString pathToCopyOfOriginal() const; + void bakeTexture(const QUrl& textureURL); + + QString pathToCopyOfRaw() const; QUrl _fbxURL; QString _fbxName; diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index 4f21ccd624..4cf1e09410 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -69,6 +69,9 @@ void TextureBaker::handleTextureNetworkReply() { // store the original texture so it can be passed along for the bake _originalTexture = requestReply->readAll(); + + // kickoff the texture bake now that everything is ready to go + bake(); } else { qCDebug(model_baking) << "Error downloading texture" << requestReply->errorString(); } @@ -78,4 +81,5 @@ void TextureBaker::bake() { qCDebug(model_baking) << "Baking texture at" << _textureURL; // call image library to asynchronously bake this texture + emit finished(); } From fcefe3ff817a4b1291ac036a37ccf8e6bea9a807 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 6 Apr 2017 10:47:13 -0700 Subject: [PATCH 015/146] rename the raw output folder back to original --- libraries/model-baking/src/FBXBaker.cpp | 47 +++++++++++++------------ libraries/model-baking/src/FBXBaker.h | 2 +- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index aae48a51a5..b57ab88315 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -39,10 +39,10 @@ FBXBaker::~FBXBaker() { } static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; -static const QString RAW_OUTPUT_SUBFOLDER = "raw/"; +static const QString ORIGINAL_OUTPUT_SUBFOLDER = "original/"; -QString FBXBaker::pathToCopyOfRaw() const { - return _uniqueOutputPath + RAW_OUTPUT_SUBFOLDER + _fbxURL.fileName(); +QString FBXBaker::pathToCopyOfOriginal() const { + return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); } void FBXBaker::start() { @@ -59,7 +59,7 @@ void FBXBaker::start() { QFile localFBX { _fbxURL.toLocalFile() }; // make a copy in the output folder - localFBX.copy(pathToCopyOfRaw()); + localFBX.copy(pathToCopyOfOriginal()); // start the bake now that we have everything in place bake(); @@ -103,10 +103,10 @@ bool FBXBaker::setupOutputFolder() { return false; } - // make the baked and raw sub-folders used during export + // make the baked and original sub-folders used during export QDir uniqueOutputDir = _uniqueOutputPath; - if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(RAW_OUTPUT_SUBFOLDER)) { - qCCritical(model_baking) << "Failed to create baked/raw subfolders in" << _uniqueOutputPath; + if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) { + qCCritical(model_baking) << "Failed to create baked/original subfolders in" << _uniqueOutputPath; emit finished(); return false; @@ -122,19 +122,19 @@ void FBXBaker::handleFBXNetworkReply() { qCDebug(model_baking) << "Downloaded" << _fbxURL; // grab the contents of the reply and make a copy in the output folder - QFile copyOfRaw(pathToCopyOfRaw()); + QFile copyOfOriginal(pathToCopyOfOriginal()); - qDebug(model_baking) << "Writing copy of raw FBX to" << copyOfRaw.fileName(); + qDebug(model_baking) << "Writing copy of original FBX to" << copyOfOriginal.fileName(); - if (!copyOfRaw.open(QIODevice::WriteOnly) || (copyOfRaw.write(requestReply->readAll()) == -1)) { + 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 raw FBX could not be made + // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made emit finished(); return; } // close that file now that we are done writing to it - copyOfRaw.close(); + copyOfOriginal.close(); // kick off the bake process now that everything is ready to go bake(); @@ -151,15 +151,18 @@ void FBXBaker::bake() { importScene(); rewriteAndBakeSceneTextures(); exportScene(); + + + removeEmbeddedMediaFolder(); } bool FBXBaker::importScene() { // create an FBX SDK importer FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); - // import the copy of the raw FBX file - QString rawCopyPath = pathToCopyOfRaw(); - bool importStatus = importer->Initialize(rawCopyPath.toLocal8Bit().data()); + // 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 @@ -321,21 +324,21 @@ void FBXBaker::handleBakedTexture() { // 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 unbaked output folder - // + // 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 rawOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + RAW_OUTPUT_SUBFOLDER); + auto originalOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER); - if (!rawOutputFolder.isParentOf(bakedTexture->getTextureURL())) { + 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 raw texture for" << bakedTexture->getTextureURL(); + 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 + RAW_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName() + _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName() }; if (relativeTexturePath.length() > 0) { @@ -381,7 +384,7 @@ bool FBXBaker::exportScene() { bool 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 + RAW_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively(); + QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively(); // we always return true because a failure to delete the embedded media folder is not a failure of the bake return true; diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index b8638372f2..44c3be2955 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -51,7 +51,7 @@ private: void bakeTexture(const QUrl& textureURL); - QString pathToCopyOfRaw() const; + QString pathToCopyOfOriginal() const; QUrl _fbxURL; QString _fbxName; From 647377d07ae8739f2e4c6d4f5ae74eb6604efe98 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 6 Apr 2017 16:08:11 -0700 Subject: [PATCH 016/146] enumerate materials to find textures with types --- libraries/model-baking/src/FBXBaker.cpp | 247 ++++++++++++++++-------- libraries/model-baking/src/FBXBaker.h | 27 ++- tools/oven/src/Oven.cpp | 2 +- 3 files changed, 191 insertions(+), 85 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index b57ab88315..625d0ae963 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -202,105 +202,190 @@ QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { } } -bool FBXBaker::rewriteAndBakeSceneTextures() { - // get a count of the textures used in the scene - int numTextures = _scene->GetTextureCount(); +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()]; - // enumerate the textures in the scene - for (int i = 0; i < numTextures; i++) { - // grab each file texture - FbxFileTexture* fileTexture = FbxCast(_scene->GetTexture(i)); + QString bakedTextureFileName { textureFileInfo.baseName() }; - if (fileTexture) { - // use QFileInfo to easily split up the existing texture filename into its components - QFileInfo textureFileInfo { fileTexture->GetFileName() }; + 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); + } - // make sure this texture points to something - if (!textureFileInfo.filePath().isEmpty()) { + bakedTextureFileName += BAKED_TEXTURE_EXT; - // construct the new baked texture file name and file path + // increment the number of name matches + ++nameMatches; - // 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()]; + return bakedTextureFileName; +} - QString bakedTextureFileName { textureFileInfo.baseName() }; +QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) { + QUrl urlToTexture; - 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); - } + 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 - bakedTextureFileName += BAKED_TEXTURE_EXT; - - // increment the number of name matches - ++nameMatches; - - QString bakedTextureFilePath { _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + BAKED_TEXTURE_DIRECTORY + bakedTextureFileName }; - - qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; - - // write the new filename into the FBX scene - fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); - - QUrl urlToTexture; - - // add the texture to the list of textures needing to be baked - 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("\\", "/")); + // first check if it the RelativePath to the texture in the FBX was relative + QString relativeFileName = fileTexture->GetRelativeFileName(); + auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); #ifndef Q_OS_WIN - // it turns out that paths that start with a drive letter and a colon appear to QFileInfo - // as a relative path on UNIX systems - we perform a special check here to handle that case - bool isAbsolute = relativeFileName[1] == ':' || apparentRelativePath.isAbsolute(); + // it turns out that paths that start with a drive letter and a colon appear to QFileInfo + // as a relative path on UNIX systems - we perform a special check here to handle that case + bool isAbsolute = relativeFileName[1] == ':' || apparentRelativePath.isAbsolute(); #else - bool isAbsolute = apparentRelativePath.isAbsolute(); + bool isAbsolute = apparentRelativePath.isAbsolute(); #endif - if (isAbsolute) { - // this is a relative file path which will require different handling - // depending on the location of the original FBX - if (_fbxURL.isLocalFile()) { - // since the loaded FBX is loaded, first check if we actually have the texture locally - // at the absolute path - if (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()); + if (isAbsolute) { + // this is a relative file path which will require different handling + // depending on the location of the original FBX + if (_fbxURL.isLocalFile()) { + // since the loaded FBX is loaded, first check if we actually have the texture locally + // at the absolute path + if (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()); + } + } else { + // the original FBX was remote and downloaded + + // since this "relative" texture path is actually absolute, we have to assume it is beside the FBX + // which matches the behaviour of Interface + + // append that path to our list of unbaked textures + urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); + } + } else { + // simply construct a URL with the relative path to the asset, locally or remotely + // and append that to the list of unbaked textures + urlToTexture = _fbxURL.resolved(apparentRelativePath.filePath()); + } + } + + return urlToTexture; +} + +TextureType textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { + // 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; +} + +bool 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 != 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 + QFileInfo textureFileInfo { fileTexture->GetFileName() }; + + // make sure this texture points to something + if (!textureFileInfo.filePath().isEmpty()) { + + // 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 + BAKED_TEXTURE_DIRECTORY + bakedTextureFileName + }; + + qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; + + // write the new filename into the FBX scene + fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); + + // figure out the URL to this texture, embedded or external + auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); + + // add the deduced url to the texture, associated with the resulting baked texture file name, + // to our hash of textures needing to be baked + _unbakedTextures.insert(urlToTexture, bakedTextureFileName); + + // bake this texture asynchronously + bakeTexture(urlToTexture); } - } else { - // the original FBX was remote and downloaded - - // since this "relative" texture path is actually absolute, we have to assume it is beside the FBX - // which matches the behaviour of Interface - - // append that path to our list of unbaked textures - urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); } - } else { - // simply construct a URL with the relative path to the asset, locally or remotely - // and append that to the list of unbaked textures - urlToTexture = _fbxURL.resolved(apparentRelativePath.filePath()); } } - // add the deduced url to the texture, associated with the resulting baked texture file name, to our hash - _unbakedTextures.insert(urlToTexture, bakedTextureFileName); - - // bake this texture asynchronously - bakeTexture(urlToTexture); + property = material->GetNextProperty(property); } } } diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 44c3be2955..2e15e30bce 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -20,9 +20,28 @@ namespace fbxsdk { class FbxManager; class FbxProperty; class FbxScene; - class FbxTexture; + class FbxFileTexture; } +enum TextureType { + DEFAULT_TEXTURE, + STRICT_TEXTURE, + ALBEDO_TEXTURE, + NORMAL_TEXTURE, + BUMP_TEXTURE, + SPECULAR_TEXTURE, + METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey + ROUGHNESS_TEXTURE, + GLOSS_TEXTURE, + EMISSIVE_TEXTURE, + CUBE_TEXTURE, + OCCLUSION_TEXTURE, + SCATTERING_TEXTURE = OCCLUSION_TEXTURE, + LIGHTMAP_TEXTURE, + CUSTOM_TEXTURE, + UNUSED_TEXTURE = -1 +}; + class TextureBaker; class FBXBaker : public QObject { @@ -49,6 +68,9 @@ private: bool exportScene(); bool removeEmbeddedMediaFolder(); + QString createBakedTextureFileName(const QFileInfo& textureFileInfo); + QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); + void bakeTexture(const QUrl& textureURL); QString pathToCopyOfOriginal() const; @@ -66,8 +88,7 @@ private: QHash _unbakedTextures; QHash _textureNameMatchCount; - - QHash _downloadedTextures; + QHash _textureTypes; std::list> _bakingTextures; }; diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index b517da8151..90025522a9 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -17,7 +17,7 @@ static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/exp Oven::Oven(int argc, char* argv[]) : QCoreApplication(argc, argv), - _testBake(QUrl("file:///Users/birarda/code/hifi/lod/test-oven/DiscGolfBasket.fbx"), OUTPUT_FOLDER) + _testBake(QUrl("file:///Users/birarda/code/hifi/lod/test-oven/Test-Object6.fbx"), OUTPUT_FOLDER) { _testBake.start(); } From 6519a9c45b4a6710de5fa10c9cd0ce1a2d7f24c4 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 6 Apr 2017 16:30:21 -0700 Subject: [PATCH 017/146] bump FBX SDK version back to 2017 --- cmake/modules/FindFBXSDK.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/modules/FindFBXSDK.cmake b/cmake/modules/FindFBXSDK.cmake index d6e9ed801d..7f6a424aa1 100644 --- a/cmake/modules/FindFBXSDK.cmake +++ b/cmake/modules/FindFBXSDK.cmake @@ -18,9 +18,9 @@ if (NOT FBX_VERSION) if (WIN32) - set(FBX_VERSION 2016.1.1) + set(FBX_VERSION 2017.1) else() - set(FBX_VERSION 2016.1.1) + set(FBX_VERSION 2017.0.1) endif() endif() From 8d3b854e69c317e57467d9d902bf3467d5d9f710 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 7 Apr 2017 14:13:22 -0700 Subject: [PATCH 018/146] add a simple UI to Oven to bake individual model --- tools/oven/CMakeLists.txt | 2 +- tools/oven/src/Oven.cpp | 24 ++++- tools/oven/src/Oven.h | 10 ++- tools/oven/src/ui/ModelBakeWidget.cpp | 121 ++++++++++++++++++++++++++ tools/oven/src/ui/ModelBakeWidget.h | 43 +++++++++ tools/oven/src/ui/OvenMainWindow.cpp | 12 +++ tools/oven/src/ui/OvenMainWindow.h | 21 +++++ 7 files changed, 224 insertions(+), 9 deletions(-) create mode 100644 tools/oven/src/ui/ModelBakeWidget.cpp create mode 100644 tools/oven/src/ui/ModelBakeWidget.h create mode 100644 tools/oven/src/ui/OvenMainWindow.cpp create mode 100644 tools/oven/src/ui/OvenMainWindow.h diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 473fa707f1..4d64126fb8 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME oven) -setup_hifi_project() +setup_hifi_project(Widgets Gui) link_hifi_libraries(model-baking) diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 90025522a9..e77dd9b988 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -9,15 +9,31 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include "ui/OvenMainWindow.h" +#include "ui/ModelBakeWidget.h" #include "Oven.h" static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/export"; Oven::Oven(int argc, char* argv[]) : - QCoreApplication(argc, argv), - _testBake(QUrl("file:///Users/birarda/code/hifi/lod/test-oven/Test-Object6.fbx"), OUTPUT_FOLDER) + QApplication(argc, argv) { - _testBake.start(); + QCoreApplication::setOrganizationName("High Fidelity"); + QCoreApplication::setApplicationName("Oven"); + + // check if we were passed any command line arguments that would tell us just to run without the GUI + + // setup the GUI + setupGUI(); } + +void Oven::setupGUI() { + _mainWindow = new OvenMainWindow; + + _mainWindow->setWindowTitle("High Fidelity Oven"); + + _mainWindow->setCentralWidget(new ModelBakeWidget); + _mainWindow->show(); +} + diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h index 72de77b889..2a628fa0c8 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -12,18 +12,20 @@ #ifndef hifi_Oven_h #define hifi_Oven_h -#include +#include -#include +class OvenMainWindow; -class Oven : public QCoreApplication { +class Oven : public QApplication { Q_OBJECT public: Oven(int argc, char* argv[]); private: - FBXBaker _testBake; + void setupGUI(); + + OvenMainWindow* _mainWindow; }; diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp new file mode 100644 index 0000000000..b32afa156d --- /dev/null +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -0,0 +1,121 @@ +// +// 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 "ModelBakeWidget.h" + +ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + QWidget(parent, flags) +{ + 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"); + + _modelLineEdit = new QLineEdit; + + 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; + + 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 button that will kickoff the bake + QPushButton* bakeButton = new QPushButton("Bake Model"); + connect(bakeButton, &QPushButton::clicked, this, &ModelBakeWidget::bakeButtonClicked); + + // add the bake button to the grid + gridLayout->addWidget(bakeButton, rowIndex, 0, -1, -1); + + setLayout(gridLayout); +} + +void ModelBakeWidget::chooseFileButtonClicked() { + // pop a file dialog so the user can select the model file + auto selectedFile = QFileDialog::getOpenFileName(this, "Choose Model", QDir::homePath()); + + if (!selectedFile.isEmpty()) { + // set the contents of the model file text box to be the path to the selected file + _modelLineEdit->setText(selectedFile); + } +} + +void ModelBakeWidget::chooseOutputDirButtonClicked() { + // pop a file dialog so the user can select the output directory + auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", QDir::homePath()); + + if (!selectedDir.isEmpty()) { + // set the contents of the output directory text box to be the path to the directory + _outputDirLineEdit->setText(selectedDir); + } +} + +void ModelBakeWidget::bakeButtonClicked() { + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + + if (!outputDirectory.exists()) { + + } + + // make sure we have a non empty URL to a model to bake + if (_modelLineEdit->text().isEmpty()) { + + } + + // construct a URL from the path in the model file text box + QUrl modelToBakeURL(_modelLineEdit->text()); + + // if the URL doesn't have a scheme, assume it is a local file + if (modelToBakeURL.scheme().isEmpty()) { + modelToBakeURL.setScheme("file"); + } + + // everything seems to be in place, kick off a bake now + _baker.reset(new FBXBaker(modelToBakeURL, outputDirectory.absolutePath())); + _baker->start(); +} diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h new file mode 100644 index 0000000000..adcaaf2a50 --- /dev/null +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -0,0 +1,43 @@ +// +// 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 + +class QLineEdit; + +class ModelBakeWidget : public QWidget { + Q_OBJECT + +public: + ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + +private: + void setupUI(); + + std::unique_ptr _baker; + + QLineEdit* _modelLineEdit; + QLineEdit* _outputDirLineEdit; + + QUrl modelToBakeURL; +}; + +#endif // hifi_ModelBakeWidget_h diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp new file mode 100644 index 0000000000..8f7829d765 --- /dev/null +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -0,0 +1,12 @@ +// +// 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 "OvenMainWindow.h" diff --git a/tools/oven/src/ui/OvenMainWindow.h b/tools/oven/src/ui/OvenMainWindow.h new file mode 100644 index 0000000000..b9813be34e --- /dev/null +++ b/tools/oven/src/ui/OvenMainWindow.h @@ -0,0 +1,21 @@ +// +// 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 + +class OvenMainWindow : public QMainWindow { + +}; + +#endif // hifi_OvenMainWindow_h From e1840eb4fedace904111cf0fe6ec9d336958d462 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 7 Apr 2017 14:19:23 -0700 Subject: [PATCH 019/146] give the Oven window a fixed width --- tools/oven/src/Oven.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index e77dd9b988..7f185a404e 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -33,7 +33,12 @@ void Oven::setupGUI() { _mainWindow->setWindowTitle("High Fidelity Oven"); + // give the window a fixed width that will never change + const int FIXED_WINDOW_WIDTH = 640; + _mainWindow->setFixedWidth(FIXED_WINDOW_WIDTH); + _mainWindow->setCentralWidget(new ModelBakeWidget); + _mainWindow->show(); } From 425385d9829f07d5791cfcc81ec5214e3bb1c653 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 7 Apr 2017 14:44:09 -0700 Subject: [PATCH 020/146] leverage settings to remember paths used before --- libraries/shared/src/SettingHandle.h | 4 +++ libraries/shared/src/SettingInterface.h | 1 - tools/oven/CMakeLists.txt | 2 +- tools/oven/src/Oven.cpp | 5 ++++ tools/oven/src/ui/ModelBakeWidget.cpp | 39 +++++++++++++++++++++++-- tools/oven/src/ui/ModelBakeWidget.h | 7 +++++ 6 files changed, 53 insertions(+), 5 deletions(-) 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/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/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 4d64126fb8..2e40cc9de4 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,4 +2,4 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui) -link_hifi_libraries(model-baking) +link_hifi_libraries(model-baking shared) diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 7f185a404e..1e7eb3906e 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "ui/OvenMainWindow.h" #include "ui/ModelBakeWidget.h" @@ -22,6 +24,9 @@ Oven::Oven(int argc, char* 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 diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index b32afa156d..a3fd874306 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -20,8 +20,13 @@ #include "ModelBakeWidget.h" +static const QString EXPORT_DIR_SETTING_KEY = "model_export_directory"; +static const QString MODEL_START_DIR_SETTING_KEY = "model_search_directory"; + ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) : - QWidget(parent, flags) + QWidget(parent, flags), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) { setupUI(); } @@ -53,6 +58,12 @@ void ModelBakeWidget::setupUI() { _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); @@ -76,17 +87,34 @@ void ModelBakeWidget::setupUI() { void ModelBakeWidget::chooseFileButtonClicked() { // pop a file dialog so the user can select the model file - auto selectedFile = QFileDialog::getOpenFileName(this, "Choose Model", QDir::homePath()); + + // 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 selectedFile = QFileDialog::getOpenFileName(this, "Choose Model", startDir); if (!selectedFile.isEmpty()) { // set the contents of the model file text box to be the path to the selected file _modelLineEdit->setText(selectedFile); + _modelStartDirectory.set(QDir(selectedFile).absolutePath()); } } void ModelBakeWidget::chooseOutputDirButtonClicked() { // pop a file dialog so the user can select the output directory - auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", QDir::homePath()); + + // 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 @@ -94,6 +122,11 @@ void ModelBakeWidget::chooseOutputDirButtonClicked() { } } +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()); diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index adcaaf2a50..0f7efa8aad 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -14,6 +14,8 @@ #include +#include + #include class QLineEdit; @@ -29,6 +31,8 @@ private slots: void chooseOutputDirButtonClicked(); void bakeButtonClicked(); + void outputDirectoryChanged(const QString& newDirectory); + private: void setupUI(); @@ -38,6 +42,9 @@ private: QLineEdit* _outputDirLineEdit; QUrl modelToBakeURL; + + Setting::Handle _exportDirectory; + Setting::Handle _modelStartDirectory; }; #endif // hifi_ModelBakeWidget_h From 1fc678a92900bd8e8b8119a3fa489c278abddc61 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 7 Apr 2017 15:29:09 -0700 Subject: [PATCH 021/146] add placeholder text, set export folder from FBX if not set --- tools/oven/src/ui/ModelBakeWidget.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index a3fd874306..eb4f792c33 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -41,6 +41,7 @@ void ModelBakeWidget::setupUI() { QLabel* modelFileLabel = new QLabel("Model File"); _modelLineEdit = new QLineEdit; + _modelLineEdit->setPlaceholderText("File or URL"); QPushButton* chooseFileButton = new QPushButton("Browse..."); connect(chooseFileButton, &QPushButton::clicked, this, &ModelBakeWidget::chooseFileButtonClicked); @@ -100,7 +101,14 @@ void ModelBakeWidget::chooseFileButtonClicked() { if (!selectedFile.isEmpty()) { // set the contents of the model file text box to be the path to the selected file _modelLineEdit->setText(selectedFile); - _modelStartDirectory.set(QDir(selectedFile).absolutePath()); + + auto directoryOfModel = QFileInfo(selectedFile).absolutePath(); + + // save the directory containing this model so we can default to it next time we show the file dialog + _modelStartDirectory.set(directoryOfModel); + + // if our output directory is not yet set, set it to the directory of this model + _outputDirLineEdit->setText(directoryOfModel); } } From d9efd4adef647ec0dc2329af374dad771cf13d56 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 7 Apr 2017 15:40:40 -0700 Subject: [PATCH 022/146] don't save copy of originals for one-off bake --- libraries/model-baking/src/FBXBaker.cpp | 19 ++++++++++++------- libraries/model-baking/src/FBXBaker.h | 7 +++++-- tools/oven/src/ui/ModelBakeWidget.cpp | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 625d0ae963..af1c861623 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -22,9 +22,10 @@ #include "FBXBaker.h" -FBXBaker::FBXBaker(QUrl fbxURL, QString baseOutputPath) : +FBXBaker::FBXBaker(QUrl fbxURL, QString baseOutputPath, bool copyOriginals) : _fbxURL(fbxURL), - _baseOutputPath(baseOutputPath) + _baseOutputPath(baseOutputPath), + _copyOriginals(copyOriginals) { // create an FBX SDK manager _sdkManager = FbxManager::Create(); @@ -152,8 +153,8 @@ void FBXBaker::bake() { rewriteAndBakeSceneTextures(); exportScene(); - removeEmbeddedMediaFolder(); + possiblyCleanupOriginals(); } bool FBXBaker::importScene() { @@ -466,11 +467,15 @@ bool FBXBaker::exportScene() { } -bool FBXBaker::removeEmbeddedMediaFolder() { +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(); - - // we always return true because a failure to delete the embedded media folder is not a failure of the bake - return true; +} + +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(); + } } diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 2e15e30bce..cc357cd251 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -47,7 +47,7 @@ class TextureBaker; class FBXBaker : public QObject { Q_OBJECT public: - FBXBaker(QUrl fbxURL, QString baseOutputPath); + FBXBaker(QUrl fbxURL, QString baseOutputPath, bool copyOriginals = true); ~FBXBaker(); void start(); @@ -66,7 +66,8 @@ private: bool importScene(); bool rewriteAndBakeSceneTextures(); bool exportScene(); - bool removeEmbeddedMediaFolder(); + void removeEmbeddedMediaFolder(); + void possiblyCleanupOriginals(); QString createBakedTextureFileName(const QFileInfo& textureFileInfo); QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); @@ -91,6 +92,8 @@ private: QHash _textureTypes; std::list> _bakingTextures; + + bool _copyOriginals { true }; }; #endif // hifi_FBXBaker_h diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index eb4f792c33..1fe214a7f6 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -157,6 +157,6 @@ void ModelBakeWidget::bakeButtonClicked() { } // everything seems to be in place, kick off a bake now - _baker.reset(new FBXBaker(modelToBakeURL, outputDirectory.absolutePath())); + _baker.reset(new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false)); _baker->start(); } From 31bf01250386f00e4b7116ade23d29a6df2eac07 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 7 Apr 2017 15:54:26 -0700 Subject: [PATCH 023/146] handle multi-file select for model bake UI --- tools/oven/src/ui/ModelBakeWidget.cpp | 35 ++++++++++++++++----------- tools/oven/src/ui/ModelBakeWidget.h | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 1fe214a7f6..1a8156eaf3 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -38,7 +38,7 @@ void ModelBakeWidget::setupUI() { int rowIndex = 0; // setup a section to choose the file being baked - QLabel* modelFileLabel = new QLabel("Model File"); + QLabel* modelFileLabel = new QLabel("Model File(s)"); _modelLineEdit = new QLineEdit; _modelLineEdit->setPlaceholderText("File or URL"); @@ -77,7 +77,7 @@ void ModelBakeWidget::setupUI() { ++rowIndex; // add a button that will kickoff the bake - QPushButton* bakeButton = new QPushButton("Bake Model"); + QPushButton* bakeButton = new QPushButton("Bake"); connect(bakeButton, &QPushButton::clicked, this, &ModelBakeWidget::bakeButtonClicked); // add the bake button to the grid @@ -96,13 +96,13 @@ void ModelBakeWidget::chooseFileButtonClicked() { startDir = QDir::homePath(); } - auto selectedFile = QFileDialog::getOpenFileName(this, "Choose Model", startDir); + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir); - if (!selectedFile.isEmpty()) { + if (!selectedFiles.isEmpty()) { // set the contents of the model file text box to be the path to the selected file - _modelLineEdit->setText(selectedFile); + _modelLineEdit->setText(selectedFiles.join(',')); - auto directoryOfModel = QFileInfo(selectedFile).absolutePath(); + auto directoryOfModel = QFileInfo(selectedFiles[0]).absolutePath(); // save the directory containing this model so we can default to it next time we show the file dialog _modelStartDirectory.set(directoryOfModel); @@ -148,15 +148,22 @@ void ModelBakeWidget::bakeButtonClicked() { } - // construct a URL from the path in the model file text box - QUrl modelToBakeURL(_modelLineEdit->text()); + // 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().isEmpty()) { - modelToBakeURL.setScheme("file"); + // if the URL doesn't have a scheme, assume it is a local file + if (modelToBakeURL.scheme().isEmpty()) { + modelToBakeURL.setScheme("file"); + } + + // everything seems to be in place, kick off a bake for this model now + auto baker = new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false); + baker->start(); + _bakers.emplace_back(baker); } - // everything seems to be in place, kick off a bake now - _baker.reset(new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false)); - _baker->start(); + } diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 0f7efa8aad..5fe3a37e0a 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -36,7 +36,7 @@ private slots: private: void setupUI(); - std::unique_ptr _baker; + std::list> _bakers; QLineEdit* _modelLineEdit; QLineEdit* _outputDirLineEdit; From 4e0aba10bcf055df8e6771cc5daa886fda4384c7 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 7 Apr 2017 16:57:31 -0700 Subject: [PATCH 024/146] add a modes menu and hook it up to the existing model widget --- tools/oven/src/Oven.cpp | 14 -------- tools/oven/src/Oven.h | 2 -- tools/oven/src/ui/ModelBakeWidget.cpp | 30 +++++++++++++--- tools/oven/src/ui/ModelBakeWidget.h | 1 + tools/oven/src/ui/ModesWidget.cpp | 51 +++++++++++++++++++++++++++ tools/oven/src/ui/ModesWidget.h | 29 +++++++++++++++ tools/oven/src/ui/OvenMainWindow.cpp | 20 +++++++++++ tools/oven/src/ui/OvenMainWindow.h | 4 ++- 8 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 tools/oven/src/ui/ModesWidget.cpp create mode 100644 tools/oven/src/ui/ModesWidget.h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 1e7eb3906e..ede1b7c1e4 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -12,7 +12,6 @@ #include #include "ui/OvenMainWindow.h" -#include "ui/ModelBakeWidget.h" #include "Oven.h" @@ -30,20 +29,7 @@ Oven::Oven(int argc, char* argv[]) : // check if we were passed any command line arguments that would tell us just to run without the GUI // setup the GUI - setupGUI(); -} - -void Oven::setupGUI() { _mainWindow = new OvenMainWindow; - - _mainWindow->setWindowTitle("High Fidelity Oven"); - - // give the window a fixed width that will never change - const int FIXED_WINDOW_WIDTH = 640; - _mainWindow->setFixedWidth(FIXED_WINDOW_WIDTH); - - _mainWindow->setCentralWidget(new ModelBakeWidget); - _mainWindow->show(); } diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h index 2a628fa0c8..7ec9bdbd3b 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -23,8 +23,6 @@ public: Oven(int argc, char* argv[]); private: - void setupGUI(); - OvenMainWindow* _mainWindow; }; diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 1a8156eaf3..ff95e521c5 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -76,12 +77,24 @@ void ModelBakeWidget::setupUI() { // 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 the bake button to the grid - gridLayout->addWidget(bakeButton, rowIndex, 0, -1, -1); + // 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); } @@ -141,11 +154,13 @@ void ModelBakeWidget::bakeButtonClicked() { 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 @@ -164,6 +179,13 @@ void ModelBakeWidget::bakeButtonClicked() { baker->start(); _bakers.emplace_back(baker); } - - +} + +void ModelBakeWidget::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/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 5fe3a37e0a..5d20b3fb53 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -30,6 +30,7 @@ private slots: void chooseFileButtonClicked(); void chooseOutputDirButtonClicked(); void bakeButtonClicked(); + void cancelButtonClicked(); void outputDirectoryChanged(const QString& newDirectory); diff --git a/tools/oven/src/ui/ModesWidget.cpp b/tools/oven/src/ui/ModesWidget.cpp new file mode 100644 index 0000000000..1df2c9fe4f --- /dev/null +++ b/tools/oven/src/ui/ModesWidget.cpp @@ -0,0 +1,51 @@ +// +// 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 "ModelBakeWidget.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 model baking + QPushButton* modelsButton = new QPushButton("Bake Models"); + connect(modelsButton, &QPushButton::clicked, this, &ModesWidget::showModelBakingWidget); + horizontalLayout->addWidget(modelsButton); + + // add a button for domain baking + QPushButton* domainButton = new QPushButton("Bake Domain"); + horizontalLayout->addWidget(domainButton); + + // add a button for texture baking + QPushButton* textureButton = new QPushButton("Bake Textures"); + horizontalLayout->addWidget(textureButton); + + setLayout(horizontalLayout); +} + +void ModesWidget::showModelBakingWidget() { + auto stackedWidget = qobject_cast(parentWidget()); + + // add a new widget for making baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new ModelBakeWidget)); +} diff --git a/tools/oven/src/ui/ModesWidget.h b/tools/oven/src/ui/ModesWidget.h new file mode 100644 index 0000000000..bde2898e0c --- /dev/null +++ b/tools/oven/src/ui/ModesWidget.h @@ -0,0 +1,29 @@ +// +// 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(); + +private: + void setupUI(); +}; + +#endif // hifi_ModesWidget_h diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp index 8f7829d765..9c25fb5c72 100644 --- a/tools/oven/src/ui/OvenMainWindow.cpp +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -9,4 +9,24 @@ // 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 + const int FIXED_WINDOW_WIDTH = 640; + 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); +} diff --git a/tools/oven/src/ui/OvenMainWindow.h b/tools/oven/src/ui/OvenMainWindow.h index b9813be34e..0941be543b 100644 --- a/tools/oven/src/ui/OvenMainWindow.h +++ b/tools/oven/src/ui/OvenMainWindow.h @@ -15,7 +15,9 @@ #include class OvenMainWindow : public QMainWindow { - + Q_OBJECT +public: + OvenMainWindow(QWidget *parent = Q_NULLPTR, Qt::WindowFlags flags = Qt::WindowFlags()); }; #endif // hifi_OvenMainWindow_h From 074fb083136fc8ac3a53c720bcc33ff89fe911b3 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 12 Apr 2017 14:56:07 -0700 Subject: [PATCH 025/146] place the baked textures beside the baked FBX --- libraries/model-baking/src/FBXBaker.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index af1c861623..437a2f197d 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -187,7 +187,6 @@ bool FBXBaker::importScene() { return true; } -static const QString BAKED_TEXTURE_DIRECTORY = "textures/"; static const QString BAKED_TEXTURE_EXT = ".ktx"; QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { @@ -364,7 +363,7 @@ bool FBXBaker::rewriteAndBakeSceneTextures() { // even if there was another texture with the same name at a different path auto bakedTextureFileName = createBakedTextureFileName(textureFileInfo); QString bakedTextureFilePath { - _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + BAKED_TEXTURE_DIRECTORY + bakedTextureFileName + _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + bakedTextureFileName }; qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; From 177d4d0e0752ad5f95c900f8e137f91680ceea78 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 12 Apr 2017 16:46:05 -0700 Subject: [PATCH 026/146] add a simple domain baker to enumerate models.json.gz --- tools/oven/src/DomainBaker.cpp | 111 ++++++++++++++++ tools/oven/src/DomainBaker.h | 44 ++++++ tools/oven/src/ui/DomainBakeWidget.cpp | 177 +++++++++++++++++++++++++ tools/oven/src/ui/DomainBakeWidget.h | 49 +++++++ tools/oven/src/ui/ModelBakeWidget.h | 2 - tools/oven/src/ui/ModesWidget.cpp | 9 ++ tools/oven/src/ui/ModesWidget.h | 1 + 7 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 tools/oven/src/DomainBaker.cpp create mode 100644 tools/oven/src/DomainBaker.h create mode 100644 tools/oven/src/ui/DomainBakeWidget.cpp create mode 100644 tools/oven/src/ui/DomainBakeWidget.h diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp new file mode 100644 index 0000000000..dbd080d5a7 --- /dev/null +++ b/tools/oven/src/DomainBaker.cpp @@ -0,0 +1,111 @@ +// +// 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 "Gzip.h" + +#include "DomainBaker.h" + +DomainBaker::DomainBaker(const QUrl& localModelFileURL, QString baseOutputPath) : + _localEntitiesFileURL(localModelFileURL), + _baseOutputPath(baseOutputPath) +{ + +} + +void DomainBaker::start() { + loadLocalFile(); + enumerateEntities(); +} + +void DomainBaker::loadLocalFile() { + // load up the local entities file + QFile modelsFile { _localEntitiesFileURL.toLocalFile() }; + + if (!modelsFile.open(QIODevice::ReadOnly)) { + // add an error to our list to specify that the file could not be read + + // return to stop processing + return; + } + + // grab a byte array from the file + auto fileContents = modelsFile.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"].toArray(); + + if (_entities.isEmpty()) { + // add an error to our list stating that the models file was empty + + // return to stop processing + return; + } +} + +void DomainBaker::enumerateEntities() { + qDebug() << "Enumerating" << _entities.size() << "entities from domain"; + + foreach(QJsonValue entityValue, _entities) { + // make sure this is a JSON object + if (entityValue.isObject()) { + auto entity = entityValue.toObject(); + + // check if this is an entity with a model URL + static const QString ENTITY_MODEL_URL_KEY = "modelURL"; + if (entity.contains(ENTITY_MODEL_URL_KEY)) { + // grab a QUrl for the model URL + auto modelURL = QUrl(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.setFragment(""); + modelURL.setQuery(""); + + // setup an FBXBaker for this URL, as long as we don't already have one + if (!_bakers.contains(modelURL)) { + QSharedPointer baker { new FBXBaker(modelURL, _baseOutputPath) }; + + // start the baker + baker->start(); + + // insert it into our bakers hash so we hold a strong pointer to it + _bakers.insert(modelURL, baker); + } + } + } + } + } +} diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h new file mode 100644 index 0000000000..50a2a12759 --- /dev/null +++ b/tools/oven/src/DomainBaker.h @@ -0,0 +1,44 @@ +// +// 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 + +class DomainBaker : public QObject { + Q_OBJECT +public: + DomainBaker(const QUrl& localEntitiesFileURL, QString baseOutputPath); + +public slots: + void start(); + +signals: + void finished(); + +private: + void loadLocalFile(); + void enumerateEntities(); + + QUrl _localEntitiesFileURL; + QString _baseOutputPath; + QJsonArray _entities; + + + QHash> _bakers; +}; + +#endif // hifi_DomainBaker_h diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp new file mode 100644 index 0000000000..d5d5901ffa --- /dev/null +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -0,0 +1,177 @@ +// +// 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 "DomainBakeWidget.h" + +static const QString EXPORT_DIR_SETTING_KEY = "domain_export_directory"; +static const QString BROWSE_START_DIR_SETTING_KEY = "domain_search_directory"; + +DomainBakeWidget::DomainBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + QWidget(parent, flags), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _browseStartDirectory(BROWSE_START_DIR_SETTING_KEY) +{ + setupUI(); +} + +void DomainBakeWidget::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* entitiesFileLabel = new QLabel("Entities File"); + + _entitiesFileLineEdit = new QLineEdit; + _entitiesFileLineEdit->setPlaceholderText("File or URL"); + + 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; + + // 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); + + 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); + + // if our output directory is not yet set, set it to the directory of this entities file + _outputDirLineEdit->setText(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() { + // 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()); + _baker = std::unique_ptr { new DomainBaker(fileToBakeURL, outputDirectory.absolutePath()) }; + _baker->start(); + + return; + } +} + +void DomainBakeWidget::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/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h new file mode 100644 index 0000000000..485f80683c --- /dev/null +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -0,0 +1,49 @@ +// +// 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" + +class QLineEdit; + +class DomainBakeWidget : public QWidget { + Q_OBJECT + +public: + DomainBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + void cancelButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + +private: + void setupUI(); + + std::unique_ptr _baker; + + QLineEdit* _entitiesFileLineEdit; + QLineEdit* _outputDirLineEdit; + + Setting::Handle _exportDirectory; + Setting::Handle _browseStartDirectory; +}; + +#endif // hifi_ModelBakeWidget_h diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 5d20b3fb53..354ad9f311 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -42,8 +42,6 @@ private: QLineEdit* _modelLineEdit; QLineEdit* _outputDirLineEdit; - QUrl modelToBakeURL; - Setting::Handle _exportDirectory; Setting::Handle _modelStartDirectory; }; diff --git a/tools/oven/src/ui/ModesWidget.cpp b/tools/oven/src/ui/ModesWidget.cpp index 1df2c9fe4f..867f89b4c4 100644 --- a/tools/oven/src/ui/ModesWidget.cpp +++ b/tools/oven/src/ui/ModesWidget.cpp @@ -13,6 +13,7 @@ #include #include +#include "DomainBakeWidget.h" #include "ModelBakeWidget.h" #include "ModesWidget.h" @@ -34,6 +35,7 @@ void ModesWidget::setupUI() { // 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 texture baking @@ -49,3 +51,10 @@ void ModesWidget::showModelBakingWidget() { // add a new widget for making 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 making baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new DomainBakeWidget)); +} diff --git a/tools/oven/src/ui/ModesWidget.h b/tools/oven/src/ui/ModesWidget.h index bde2898e0c..e7e239d63e 100644 --- a/tools/oven/src/ui/ModesWidget.h +++ b/tools/oven/src/ui/ModesWidget.h @@ -21,6 +21,7 @@ public: private slots: void showModelBakingWidget(); + void showDomainBakingWidget(); private: void setupUI(); From a773b0de042ef340932d8564d6af148b326dc692 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 12 Apr 2017 17:14:07 -0700 Subject: [PATCH 027/146] output domain bake to a unique folder with timestamp --- libraries/model-baking/src/FBXBaker.cpp | 2 +- libraries/model-baking/src/FBXBaker.h | 2 +- tools/oven/src/DomainBaker.cpp | 31 +++++++++++++++++++++++-- tools/oven/src/DomainBaker.h | 7 ++++-- tools/oven/src/ui/DomainBakeWidget.cpp | 15 +++++++++++- tools/oven/src/ui/DomainBakeWidget.h | 1 + 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 437a2f197d..561ce61994 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -22,7 +22,7 @@ #include "FBXBaker.h" -FBXBaker::FBXBaker(QUrl fbxURL, QString baseOutputPath, bool copyOriginals) : +FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals) : _fbxURL(fbxURL), _baseOutputPath(baseOutputPath), _copyOriginals(copyOriginals) diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index cc357cd251..9843d8f298 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -47,7 +47,7 @@ class TextureBaker; class FBXBaker : public QObject { Q_OBJECT public: - FBXBaker(QUrl fbxURL, QString baseOutputPath, bool copyOriginals = true); + FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals = true); ~FBXBaker(); void start(); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index dbd080d5a7..c3953aeda3 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -20,18 +20,45 @@ #include "DomainBaker.h" -DomainBaker::DomainBaker(const QUrl& localModelFileURL, QString baseOutputPath) : +DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, const QString& baseOutputPath) : _localEntitiesFileURL(localModelFileURL), + _domainName(domainName), _baseOutputPath(baseOutputPath) { } void DomainBaker::start() { + setupOutputFolder(); loadLocalFile(); enumerateEntities(); } +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 baseDir { _baseOutputPath }; + + if (!baseDir.mkpath(outputDirectoryName)) { + + // add an error to specify that the output directory could not be created + + return; + } + + // store the unique output path so we can re-use it when saving baked models + baseDir.cd(outputDirectoryName); + _uniqueOutputPath = baseDir.absolutePath(); +} + void DomainBaker::loadLocalFile() { // load up the local entities file QFile modelsFile { _localEntitiesFileURL.toLocalFile() }; @@ -96,7 +123,7 @@ void DomainBaker::enumerateEntities() { // setup an FBXBaker for this URL, as long as we don't already have one if (!_bakers.contains(modelURL)) { - QSharedPointer baker { new FBXBaker(modelURL, _baseOutputPath) }; + QSharedPointer baker { new FBXBaker(modelURL, _uniqueOutputPath) }; // start the baker baker->start(); diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 50a2a12759..c0fc40bb93 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -21,7 +21,7 @@ class DomainBaker : public QObject { Q_OBJECT public: - DomainBaker(const QUrl& localEntitiesFileURL, QString baseOutputPath); + DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, const QString& baseOutputPath); public slots: void start(); @@ -30,13 +30,16 @@ signals: void finished(); private: + void setupOutputFolder(); void loadLocalFile(); void enumerateEntities(); QUrl _localEntitiesFileURL; + QString _domainName; QString _baseOutputPath; - QJsonArray _entities; + QString _uniqueOutputPath; + QJsonArray _entities; QHash> _bakers; }; diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index d5d5901ffa..9d73036a96 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -38,6 +38,17 @@ void DomainBakeWidget::setupUI() { 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"); + + 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"); @@ -160,7 +171,9 @@ void DomainBakeWidget::bakeButtonClicked() { 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()); - _baker = std::unique_ptr { new DomainBaker(fileToBakeURL, outputDirectory.absolutePath()) }; + _baker = std::unique_ptr { + new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), outputDirectory.absolutePath()) + }; _baker->start(); return; diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index 485f80683c..38a2c6a577 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -39,6 +39,7 @@ private: std::unique_ptr _baker; + QLineEdit* _domainNameLineEdit; QLineEdit* _entitiesFileLineEdit; QLineEdit* _outputDirLineEdit; From e1dc1990e526350c0fe5eeaa743c605793b33715 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 13 Apr 2017 12:02:14 -0700 Subject: [PATCH 028/146] add writing of new entities file during domain bake --- libraries/model-baking/src/FBXBaker.cpp | 38 +++++- libraries/model-baking/src/FBXBaker.h | 9 ++ libraries/model-baking/src/TextureBaker.cpp | 3 + tools/oven/src/DomainBaker.cpp | 141 +++++++++++++++++--- tools/oven/src/DomainBaker.h | 12 +- tools/oven/src/ui/DomainBakeWidget.cpp | 37 ++++- tools/oven/src/ui/DomainBakeWidget.h | 3 + 7 files changed, 220 insertions(+), 23 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 561ce61994..f798a137e4 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -140,7 +140,10 @@ void FBXBaker::handleFBXNetworkReply() { // kick off the bake process now that everything is ready to go bake(); } else { - qDebug() << "ERROR DOWNLOADING FBX" << requestReply->errorString(); + // add an error to our list stating that the FBX could not be downloaded + + + emit finished(); } } @@ -155,6 +158,12 @@ void FBXBaker::bake() { removeEmbeddedMediaFolder(); possiblyCleanupOriginals(); + + // at this point we are sure that we've finished everything that does not relate to textures + // so set that flag now + _finishedNonTextureOperations = true; + + checkIfFinished(); } bool FBXBaker::importScene() { @@ -226,6 +235,8 @@ QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) { QUrl urlToTexture; + qDebug() << "Looking at" << textureFileInfo.absoluteFilePath(); + if (textureFileInfo.exists() && textureFileInfo.isFile()) { // set the texture URL to the local texture that we have confirmed exists urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); @@ -355,8 +366,9 @@ bool FBXBaker::rewriteAndBakeSceneTextures() { // use QFileInfo to easily split up the existing texture filename into its components QFileInfo textureFileInfo { fileTexture->GetFileName() }; - // make sure this texture points to something - if (!textureFileInfo.filePath().isEmpty()) { + // make sure this texture points to something and isn't one we've already re-mapped + if (!textureFileInfo.filePath().isEmpty() + && textureFileInfo.completeSuffix() != 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 @@ -438,15 +450,25 @@ void FBXBaker::handleBakedTexture() { << "for" << _fbxURL; } } -} -static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; + // now that this texture has been baked and handled, we can remove that TextureBaker from our list + _unbakedTextures.remove(bakedTexture->getTextureURL()); + + // since this could have been the last texture we were waiting for + // we should perform a quick check now to see if we are done baking this model + checkIfFinished(); +} bool FBXBaker::exportScene() { // setup the exporter FbxExporter* exporter = FbxExporter::Create(_sdkManager, ""); 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) { @@ -478,3 +500,9 @@ void FBXBaker::possiblyCleanupOriginals() { QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER).removeRecursively(); } } + +void FBXBaker::checkIfFinished() { + if (_unbakedTextures.isEmpty() && _finishedNonTextureOperations) { + emit finished(); + } +} diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 9843d8f298..71ded0c44a 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -44,6 +44,8 @@ enum TextureType { class TextureBaker; +static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; + class FBXBaker : public QObject { Q_OBJECT public: @@ -52,6 +54,9 @@ public: void start(); + QUrl getFBXUrl() const { return _fbxURL; } + QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } + signals: void finished(); @@ -68,6 +73,7 @@ private: bool exportScene(); void removeEmbeddedMediaFolder(); void possiblyCleanupOriginals(); + void checkIfFinished(); QString createBakedTextureFileName(const QFileInfo& textureFileInfo); QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); @@ -81,6 +87,7 @@ private: QString _baseOutputPath; QString _uniqueOutputPath; + QString _bakedFBXRelativePath; fbxsdk::FbxManager* _sdkManager; fbxsdk::FbxScene* _scene { nullptr }; @@ -94,6 +101,8 @@ private: std::list> _bakingTextures; bool _copyOriginals { true }; + + bool _finishedNonTextureOperations { false }; }; #endif // hifi_FBXBaker_h diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index 4cf1e09410..367ae5f463 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -73,7 +73,10 @@ void TextureBaker::handleTextureNetworkReply() { // kickoff the texture bake now that everything is ready to go bake(); } else { + // add an error to our list stating that this texture could not be downloaded qCDebug(model_baking) << "Error downloading texture" << requestReply->errorString(); + + emit finished(); } } diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index c3953aeda3..7cd675de3f 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -20,12 +20,18 @@ #include "DomainBaker.h" -DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, const QString& baseOutputPath) : +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::start() { @@ -45,9 +51,9 @@ void DomainBaker::setupOutputFolder() { QString outputDirectoryName = domainPrefix + timeNow.toString(FOLDER_TIMESTAMP_FORMAT); // make sure we can create that directory - QDir baseDir { _baseOutputPath }; + QDir outputDir { _baseOutputPath }; - if (!baseDir.mkpath(outputDirectoryName)) { + if (!outputDir.mkpath(outputDirectoryName)) { // add an error to specify that the output directory could not be created @@ -55,10 +61,22 @@ void DomainBaker::setupOutputFolder() { } // store the unique output path so we can re-use it when saving baked models - baseDir.cd(outputDirectoryName); - _uniqueOutputPath = baseDir.absolutePath(); + 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 + + return; + } + + _contentOutputPath = outputDir.absoluteFilePath(CONTENT_OUTPUT_FOLDER_NAME); } +const QString ENTITIES_OBJECT_KEY = "Entities"; + void DomainBaker::loadLocalFile() { // load up the local entities file QFile modelsFile { _localEntitiesFileURL.toLocalFile() }; @@ -86,7 +104,7 @@ void DomainBaker::loadLocalFile() { auto jsonDocument = QJsonDocument::fromJson(fileContents); // grab the entities object from the root JSON object - _entities = jsonDocument.object()["Entities"].toArray(); + _entities = jsonDocument.object()[ENTITIES_OBJECT_KEY].toArray(); if (_entities.isEmpty()) { // add an error to our list stating that the models file was empty @@ -96,19 +114,20 @@ void DomainBaker::loadLocalFile() { } } +static const QString ENTITY_MODEL_URL_KEY = "modelURL"; + void DomainBaker::enumerateEntities() { qDebug() << "Enumerating" << _entities.size() << "entities from domain"; - foreach(QJsonValue entityValue, _entities) { + for (auto it = _entities.begin(); it != _entities.end(); ++it) { // make sure this is a JSON object - if (entityValue.isObject()) { - auto entity = entityValue.toObject(); + if (it->isObject()) { + auto entity = it->toObject(); // check if this is an entity with a model URL - static const QString ENTITY_MODEL_URL_KEY = "modelURL"; if (entity.contains(ENTITY_MODEL_URL_KEY)) { // grab a QUrl for the model URL - auto modelURL = QUrl(entity[ENTITY_MODEL_URL_KEY].toString()); + 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(); @@ -118,12 +137,15 @@ void DomainBaker::enumerateEntities() { if (BAKEABLE_MODEL_EXTENSIONS.contains(completeLowerExtension)) { // grab a clean version of the URL without a query or fragment - modelURL.setFragment(""); - modelURL.setQuery(""); + modelURL.setFragment(QString()); + modelURL.setQuery(QString()); // setup an FBXBaker for this URL, as long as we don't already have one if (!_bakers.contains(modelURL)) { - QSharedPointer baker { new FBXBaker(modelURL, _uniqueOutputPath) }; + QSharedPointer baker { new FBXBaker(modelURL, _contentOutputPath) }; + + // make sure our handler is called when the baker is done + connect(baker.data(), &FBXBaker::finished, this, &DomainBaker::handleFinishedBaker); // start the baker baker->start(); @@ -131,8 +153,97 @@ void DomainBaker::enumerateEntities() { // insert it into our bakers hash so we hold a strong pointer to it _bakers.insert(modelURL, baker); } + + // 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); } } } } + + _enumeratedAllEntities = true; + + // check if it's time to write out the final entities file with re-written URLs + possiblyOutputEntitiesFile(); } + +void DomainBaker::handleFinishedBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + // this FBXBaker is done and everything went according to plan + + // 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().mid(1)); + + // copy the fragment and query from the old model URL + newModelURL.setQuery(oldModelURL.query()); + newModelURL.setFragment(oldModelURL.fragment()); + + // set the new model URL as the value in our temp QJsonObject + entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString(); + + // replace our temp object with the value referenced by our QJsonValueRef + entityValue = entity; + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getFBXUrl()); + + // check if it's time to write out the final entities file with re-written URLs + possiblyOutputEntitiesFile(); + } +} + +void DomainBaker::possiblyOutputEntitiesFile() { + if (_enumeratedAllEntities && _entitiesNeedingRewrite.isEmpty()) { + // 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)) { + qWarning() << "Failed to export baked entities file to" << bakedEntitiesFilePath; + // add an error to our list to state that the output models file could not be created or could not be written to + + return; + } + + qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; + + // we've now written out our new models file - time to say that we are finished up + emit finished(); + } +} + diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index c0fc40bb93..17419b736a 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -21,7 +21,8 @@ class DomainBaker : public QObject { Q_OBJECT public: - DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, const QString& baseOutputPath); + DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, + const QString& baseOutputPath, const QUrl& destinationPath); public slots: void start(); @@ -29,19 +30,28 @@ public slots: signals: void finished(); +private slots: + void handleFinishedBaker(); + private: void setupOutputFolder(); void loadLocalFile(); void enumerateEntities(); + void possiblyOutputEntitiesFile(); QUrl _localEntitiesFileURL; QString _domainName; QString _baseOutputPath; QString _uniqueOutputPath; + QString _contentOutputPath; + QUrl _destinationPath; QJsonArray _entities; QHash> _bakers; + QMultiHash _entitiesNeedingRewrite; + + bool _enumeratedAllEntities { false }; }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 9d73036a96..2194f17a39 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -21,13 +21,17 @@ #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) : QWidget(parent, flags), + _domainNameSetting(DOMAIN_NAME_SETTING_KEY), _exportDirectory(EXPORT_DIR_SETTING_KEY), - _browseStartDirectory(BROWSE_START_DIR_SETTING_KEY) + _browseStartDirectory(BROWSE_START_DIR_SETTING_KEY), + _destinationPathSetting(DESTINATION_PATH_SETTING_KEY) { setupUI(); } @@ -44,6 +48,11 @@ void DomainBakeWidget::setupUI() { _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); @@ -88,6 +97,22 @@ void DomainBakeWidget::setupUI() { // 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); @@ -160,6 +185,13 @@ void DomainBakeWidget::outputDirectoryChanged(const QString& 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()); @@ -172,7 +204,8 @@ void DomainBakeWidget::bakeButtonClicked() { // everything seems to be in place, kick off a bake for this entities file now auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text()); _baker = std::unique_ptr { - new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), outputDirectory.absolutePath()) + new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), + outputDirectory.absolutePath(), _destinationPathLineEdit->text()) }; _baker->start(); diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index 38a2c6a577..20b4eaa4b9 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -42,9 +42,12 @@ private: QLineEdit* _domainNameLineEdit; QLineEdit* _entitiesFileLineEdit; QLineEdit* _outputDirLineEdit; + QLineEdit* _destinationPathLineEdit; + Setting::Handle _domainNameSetting; Setting::Handle _exportDirectory; Setting::Handle _browseStartDirectory; + Setting::Handle _destinationPathSetting; }; #endif // hifi_ModelBakeWidget_h From 916cecb8ec147c3d9e2227743ea78f7e0b915059 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 13 Apr 2017 16:17:06 -0700 Subject: [PATCH 029/146] use QtConcurrent to cleanup threading of bakers --- libraries/model-baking/CMakeLists.txt | 2 +- libraries/model-baking/src/FBXBaker.cpp | 151 ++++++++++---------- libraries/model-baking/src/FBXBaker.h | 15 +- libraries/model-baking/src/TextureBaker.cpp | 35 ++--- libraries/model-baking/src/TextureBaker.h | 8 +- tools/oven/CMakeLists.txt | 2 +- tools/oven/src/DomainBaker.cpp | 109 +++++++------- tools/oven/src/DomainBaker.h | 9 +- tools/oven/src/Oven.cpp | 2 + tools/oven/src/ui/DomainBakeWidget.cpp | 6 +- tools/oven/src/ui/ModelBakeWidget.cpp | 2 +- 11 files changed, 174 insertions(+), 167 deletions(-) diff --git a/libraries/model-baking/CMakeLists.txt b/libraries/model-baking/CMakeLists.txt index 487b8536c9..a0774cdcc1 100644 --- a/libraries/model-baking/CMakeLists.txt +++ b/libraries/model-baking/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME model-baking) -setup_hifi_library() +setup_hifi_library(Concurrent) link_hifi_libraries(networking) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index f798a137e4..ac72fb152c 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -11,8 +11,12 @@ #include +#include +#include #include +#include #include +#include #include @@ -46,14 +50,71 @@ QString FBXBaker::pathToCopyOfOriginal() const { return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); } -void FBXBaker::start() { +void FBXBaker::bake() { qCDebug(model_baking) << "Baking" << _fbxURL; // setup the output folder for the results of this bake - if (!setupOutputFolder()) { + setupOutputFolder(); + + // make a local copy of the FBX file + loadSourceFBX(); + + // load the scene from the FBX file + importScene(); + + // enumerate the textures found in the scene and start a bake for them + rewriteAndBakeSceneTextures(); + + // export the FBX with re-written texture references + exportScene(); + + // 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 the texture baking operations are not complete + // use a QEventLoop to process events until texture bake operations are complete + if (!_unbakedTextures.isEmpty()) { + QEventLoop eventLoop; + connect(this, &FBXBaker::allTexturesBaked, &eventLoop, &QEventLoop::quit); + eventLoop.exec(); + } + + emit finished(); +} + +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)) { + qCCritical(model_baking) << "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)) { + qCCritical(model_baking) << "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 @@ -77,48 +138,18 @@ void FBXBaker::start() { networkRequest.setUrl(_fbxURL); qCDebug(model_baking) << "Downloading" << _fbxURL; - auto networkReply = networkAccessManager.get(networkRequest); - connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); + + // start a QEventLoop so we process events while waiting for the request to complete + QEventLoop eventLoop; + connect(networkReply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); + eventLoop.exec(); + + handleFBXNetworkReply(networkReply); } } -bool 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)) { - qCCritical(model_baking) << "Failed to create FBX output folder" << _uniqueOutputPath; - - emit finished(); - return false; - } - - // make the baked and original sub-folders used during export - QDir uniqueOutputDir = _uniqueOutputPath; - if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) { - qCCritical(model_baking) << "Failed to create baked/original subfolders in" << _uniqueOutputPath; - - emit finished(); - return false; - } - - return true; -} - -void FBXBaker::handleFBXNetworkReply() { - QNetworkReply* requestReply = qobject_cast(sender()); - +void FBXBaker::handleFBXNetworkReply(QNetworkReply* requestReply) { if (requestReply->error() == QNetworkReply::NoError) { qCDebug(model_baking) << "Downloaded" << _fbxURL; @@ -136,36 +167,12 @@ void FBXBaker::handleFBXNetworkReply() { // close that file now that we are done writing to it copyOfOriginal.close(); - - // kick off the bake process now that everything is ready to go - bake(); } else { // add an error to our list stating that the FBX could not be downloaded - - emit finished(); } } -void FBXBaker::bake() { - // (1) load the scene from the FBX file - // (2) enumerate the textures found in the scene and start a bake for them - // (3) export the FBX with re-written texture references - - importScene(); - rewriteAndBakeSceneTextures(); - exportScene(); - - removeEmbeddedMediaFolder(); - possiblyCleanupOriginals(); - - // at this point we are sure that we've finished everything that does not relate to textures - // so set that flag now - _finishedNonTextureOperations = true; - - checkIfFinished(); -} - bool FBXBaker::importScene() { // create an FBX SDK importer FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); @@ -235,8 +242,6 @@ QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) { QUrl urlToTexture; - qDebug() << "Looking at" << textureFileInfo.absoluteFilePath(); - if (textureFileInfo.exists() && textureFileInfo.isFile()) { // set the texture URL to the local texture that we have confirmed exists urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); @@ -411,7 +416,7 @@ void FBXBaker::bakeTexture(const QUrl& textureURL) { connect(bakingTexture, &TextureBaker::finished, this, &FBXBaker::handleBakedTexture); - bakingTexture->start(); + QtConcurrent::run(bakingTexture, &TextureBaker::bake); _bakingTextures.emplace_back(bakingTexture); } @@ -454,9 +459,9 @@ void FBXBaker::handleBakedTexture() { // now that this texture has been baked and handled, we can remove that TextureBaker from our list _unbakedTextures.remove(bakedTexture->getTextureURL()); - // since this could have been the last texture we were waiting for - // we should perform a quick check now to see if we are done baking this model - checkIfFinished(); + if (_unbakedTextures.isEmpty()) { + emit allTexturesBaked(); + } } bool FBXBaker::exportScene() { @@ -500,9 +505,3 @@ void FBXBaker::possiblyCleanupOriginals() { QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER).removeRecursively(); } } - -void FBXBaker::checkIfFinished() { - if (_unbakedTextures.isEmpty() && _finishedNonTextureOperations) { - emit finished(); - } -} diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 71ded0c44a..458723dab0 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -12,6 +12,7 @@ #ifndef hifi_FBXBaker_h #define hifi_FBXBaker_h +#include #include #include #include @@ -52,28 +53,29 @@ public: FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals = true); ~FBXBaker(); - void start(); + void bake(); QUrl getFBXUrl() const { return _fbxURL; } QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } signals: void finished(); + void allTexturesBaked(); private slots: - void handleFBXNetworkReply(); void handleBakedTexture(); private: - void bake(); + void setupOutputFolder(); + + void loadSourceFBX(); + void handleFBXNetworkReply(QNetworkReply* requestReply); - bool setupOutputFolder(); bool importScene(); bool rewriteAndBakeSceneTextures(); bool exportScene(); void removeEmbeddedMediaFolder(); void possiblyCleanupOriginals(); - void checkIfFinished(); QString createBakedTextureFileName(const QFileInfo& textureFileInfo); QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); @@ -99,10 +101,9 @@ private: QHash _textureTypes; std::list> _bakingTextures; + QFutureSynchronizer _textureBakeSynchronizer; bool _copyOriginals { true }; - - bool _finishedNonTextureOperations { false }; }; #endif // hifi_FBXBaker_h diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index 367ae5f463..b8c49c6d50 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include @@ -24,8 +25,16 @@ TextureBaker::TextureBaker(const QUrl& textureURL) : } -void TextureBaker::start() { +void TextureBaker::bake() { + // first load the texture (either locally or remotely) + loadTexture(); + qCDebug(model_baking) << "Baking texture at" << _textureURL; + + emit finished(); +} + +void TextureBaker::loadTexture() { // check if the texture is local or first needs to be downloaded if (_textureURL.isLocalFile()) { // load up the local file @@ -39,9 +48,6 @@ void TextureBaker::start() { } _originalTexture = localTexture.readAll(); - - // start the bake now that we have everything in place - bake(); } else { // remote file, kick off a download auto& networkAccessManager = NetworkAccessManager::getInstance(); @@ -57,13 +63,17 @@ void TextureBaker::start() { qCDebug(model_baking) << "Downloading" << _textureURL; auto networkReply = networkAccessManager.get(networkRequest); - connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply); + + // use an event loop to process events while we wait for the network reply + QEventLoop eventLoop; + connect(networkReply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); + eventLoop.exec(); + + handleTextureNetworkReply(networkReply); } } -void TextureBaker::handleTextureNetworkReply() { - QNetworkReply* requestReply = qobject_cast(sender()); - +void TextureBaker::handleTextureNetworkReply(QNetworkReply* requestReply) { if (requestReply->error() == QNetworkReply::NoError) { qCDebug(model_baking) << "Downloaded texture at" << _textureURL; @@ -75,14 +85,5 @@ void TextureBaker::handleTextureNetworkReply() { } else { // add an error to our list stating that this texture could not be downloaded qCDebug(model_baking) << "Error downloading texture" << requestReply->errorString(); - - emit finished(); } } - -void TextureBaker::bake() { - qCDebug(model_baking) << "Baking texture at" << _textureURL; - - // call image library to asynchronously bake this texture - emit finished(); -} diff --git a/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h index 5544352005..7394a0652e 100644 --- a/libraries/model-baking/src/TextureBaker.h +++ b/libraries/model-baking/src/TextureBaker.h @@ -21,7 +21,7 @@ class TextureBaker : public QObject { public: TextureBaker(const QUrl& textureURL); - void start(); + void bake(); const QByteArray& getOriginalTexture() const { return _originalTexture; } @@ -30,11 +30,9 @@ public: signals: void finished(); -private slots: - void handleTextureNetworkReply(); - private: - void bake(); + void loadTexture(); + void handleTextureNetworkReply(QNetworkReply* requestReply); QUrl _textureURL; QByteArray _originalTexture; diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 2e40cc9de4..2c5d0b98e5 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME oven) -setup_hifi_project(Widgets Gui) +setup_hifi_project(Widgets Gui Concurrent) link_hifi_libraries(model-baking shared) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 7cd675de3f..c8e03e1224 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -9,10 +9,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include +#include #include #include - #include #include @@ -34,10 +34,22 @@ DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainNam } } -void DomainBaker::start() { +void DomainBaker::bake() { setupOutputFolder(); loadLocalFile(); enumerateEntities(); + + if (!_entitiesNeedingRewrite.isEmpty()) { + // use a QEventLoop to wait for all entity rewrites to be completed before writing the final models file + QEventLoop eventLoop; + connect(this, &DomainBaker::allModelsFinished, &eventLoop, &QEventLoop::quit); + eventLoop.exec(); + } + + writeNewEntitiesFile(); + + // we've now written out our new models file - time to say that we are finished up + emit finished(); } void DomainBaker::setupOutputFolder() { @@ -147,11 +159,11 @@ void DomainBaker::enumerateEntities() { // make sure our handler is called when the baker is done connect(baker.data(), &FBXBaker::finished, this, &DomainBaker::handleFinishedBaker); - // start the baker - baker->start(); - // insert it into our bakers hash so we hold a strong pointer to it _bakers.insert(modelURL, baker); + + // send the FBXBaker to the thread pool + QtConcurrent::run(baker.data(), &FBXBaker::bake); } // add this QJsonValueRef to our multi hash so that we can easily re-write @@ -161,11 +173,6 @@ void DomainBaker::enumerateEntities() { } } } - - _enumeratedAllEntities = true; - - // check if it's time to write out the final entities file with re-written URLs - possiblyOutputEntitiesFile(); } void DomainBaker::handleFinishedBaker() { @@ -201,49 +208,45 @@ void DomainBaker::handleFinishedBaker() { // remove the baked URL from the multi hash of entities needing a re-write _entitiesNeedingRewrite.remove(baker->getFBXUrl()); - // check if it's time to write out the final entities file with re-written URLs - possiblyOutputEntitiesFile(); - } -} - -void DomainBaker::possiblyOutputEntitiesFile() { - if (_enumeratedAllEntities && _entitiesNeedingRewrite.isEmpty()) { - // 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)) { - qWarning() << "Failed to export baked entities file to" << bakedEntitiesFilePath; - // add an error to our list to state that the output models file could not be created or could not be written to - - return; + if (_entitiesNeedingRewrite.isEmpty()) { + emit allModelsFinished(); } - - qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; - - // 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)) { + qWarning() << "Failed to export baked entities file to" << bakedEntitiesFilePath; + // add an error to our list to state that the output models file could not be created or could not be written to + + 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 index 17419b736a..e3585ba64f 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -24,11 +24,12 @@ public: DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, const QString& baseOutputPath, const QUrl& destinationPath); -public slots: - void start(); +public: + void bake(); signals: void finished(); + void allModelsFinished(); private slots: void handleFinishedBaker(); @@ -37,7 +38,7 @@ private: void setupOutputFolder(); void loadLocalFile(); void enumerateEntities(); - void possiblyOutputEntitiesFile(); + void writeNewEntitiesFile(); QUrl _localEntitiesFileURL; QString _domainName; @@ -50,8 +51,6 @@ private: QHash> _bakers; QMultiHash _entitiesNeedingRewrite; - - bool _enumeratedAllEntities { false }; }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index ede1b7c1e4..60754759f4 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include "ui/OvenMainWindow.h" diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 2194f17a39..01bc6110cb 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include #include @@ -207,7 +209,9 @@ void DomainBakeWidget::bakeButtonClicked() { new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), outputDirectory.absolutePath(), _destinationPathLineEdit->text()) }; - _baker->start(); + + // run the baker in our thread pool + QtConcurrent::run(_baker.get(), &DomainBaker::bake); return; } diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index ff95e521c5..5f31ed2673 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -176,7 +176,7 @@ void ModelBakeWidget::bakeButtonClicked() { // everything seems to be in place, kick off a bake for this model now auto baker = new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false); - baker->start(); + baker->bake(); _bakers.emplace_back(baker); } } From c5fbd28ecf3425bb2f110aa59078dc9b0fee36c5 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 13 Apr 2017 18:25:27 -0700 Subject: [PATCH 030/146] put all FBXBaker on same thread for bad FBX SDK --- libraries/model-baking/src/FBXBaker.cpp | 103 +++++++++++--------- libraries/model-baking/src/FBXBaker.h | 23 +++-- libraries/model-baking/src/TextureBaker.cpp | 3 - tools/oven/src/DomainBaker.cpp | 19 +++- tools/oven/src/DomainBaker.h | 3 + 5 files changed, 93 insertions(+), 58 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index ac72fb152c..5b5a8b7c0a 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -50,39 +50,59 @@ QString FBXBaker::pathToCopyOfOriginal() const { return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); } +void FBXBaker::handleError(const QString& error) { + qCCritical(model_baking) << error; + _errorList << error; + emit finished(); +} + 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; + } + // 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 the texture baking operations are not complete - // use a QEventLoop to process events until texture bake operations are complete - if (!_unbakedTextures.isEmpty()) { - QEventLoop eventLoop; - connect(this, &FBXBaker::allTexturesBaked, &eventLoop, &QEventLoop::quit); - eventLoop.exec(); + if (hasErrors()) { + return; } - emit finished(); + // cleanup the originals if we weren't asked to keep them around + possiblyCleanupOriginals(); } void FBXBaker::setupOutputFolder() { @@ -100,16 +120,14 @@ void FBXBaker::setupOutputFolder() { // attempt to make the output folder if (!QDir().mkdir(_uniqueOutputPath)) { - qCCritical(model_baking) << "Failed to create FBX output folder" << _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)) { - qCCritical(model_baking) << "Failed to create baked/original subfolders in" << _uniqueOutputPath; - + handleError("Failed to create baked/original subfolders in " + _uniqueOutputPath); return; } } @@ -123,8 +141,8 @@ void FBXBaker::loadSourceFBX() { // make a copy in the output folder localFBX.copy(pathToCopyOfOriginal()); - // start the bake now that we have everything in place - bake(); + // 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(); @@ -140,16 +158,13 @@ void FBXBaker::loadSourceFBX() { qCDebug(model_baking) << "Downloading" << _fbxURL; auto networkReply = networkAccessManager.get(networkRequest); - // start a QEventLoop so we process events while waiting for the request to complete - QEventLoop eventLoop; - connect(networkReply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); - eventLoop.exec(); - - handleFBXNetworkReply(networkReply); + connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); } } -void FBXBaker::handleFBXNetworkReply(QNetworkReply* requestReply) { +void FBXBaker::handleFBXNetworkReply() { + auto requestReply = qobject_cast(sender()); + if (requestReply->error() == QNetworkReply::NoError) { qCDebug(model_baking) << "Downloaded" << _fbxURL; @@ -159,21 +174,23 @@ void FBXBaker::handleFBXNetworkReply(QNetworkReply* requestReply) { 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 - emit finished(); + 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()); } } -bool FBXBaker::importScene() { +void FBXBaker::importScene() { // create an FBX SDK importer FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); @@ -183,10 +200,8 @@ bool FBXBaker::importScene() { if (!importStatus) { // failed to initialize importer, print an error and return - qCCritical(model_baking) << "Failed to import FBX file at" << _fbxURL - << "- error:" << importer->GetStatus().GetErrorString(); - - return false; + handleError("Failed to import FBX file at" + _fbxURL.toString() + " - error:" + importer->GetStatus().GetErrorString()); + return; } else { qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene"; } @@ -199,8 +214,6 @@ bool FBXBaker::importScene() { // destroy the importer that is no longer needed importer->Destroy(); - - return true; } static const QString BAKED_TEXTURE_EXT = ".ktx"; @@ -344,7 +357,7 @@ TextureType textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMate return UNUSED_TEXTURE; } -bool FBXBaker::rewriteAndBakeSceneTextures() { +void FBXBaker::rewriteAndBakeSceneTextures() { // enumerate the surface materials to find the textures used in the scene int numMaterials = _scene->GetMaterialCount(); @@ -406,8 +419,6 @@ bool FBXBaker::rewriteAndBakeSceneTextures() { } } } - - return true; } void FBXBaker::bakeTexture(const QUrl& textureURL) { @@ -449,22 +460,24 @@ void FBXBaker::handleBakedTexture() { if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() - << "for" << _fbxURL; + << "for" << _fbxURL; } else { - qCWarning(model_baking) << "Could not save original external texture" << originalTextureFile.fileName() - << "for" << _fbxURL; + 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 list _unbakedTextures.remove(bakedTexture->getTextureURL()); + // check if we're done everything we need to do for this FBX if (_unbakedTextures.isEmpty()) { - emit allTexturesBaked(); + emit finished(); } } -bool FBXBaker::exportScene() { +void FBXBaker::exportScene() { // setup the exporter FbxExporter* exporter = FbxExporter::Create(_sdkManager, ""); @@ -478,18 +491,14 @@ bool FBXBaker::exportScene() { if (!exportStatus) { // failed to initialize exporter, print an error and return - qCCritical(model_baking) << "Failed to export FBX file at" << _fbxURL - << "to" << rewrittenFBXPath << "- error:" << exporter->GetStatus().GetErrorString(); - - return false; + 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; - - return true; } diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 458723dab0..10c8a5c359 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -53,7 +53,9 @@ public: FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals = true); ~FBXBaker(); - void bake(); + Q_INVOKABLE void bake(); + + bool hasErrors() const { return !_errorList.isEmpty(); } QUrl getFBXUrl() const { return _fbxURL; } QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } @@ -62,18 +64,23 @@ signals: void finished(); void allTexturesBaked(); + void sourceCopyReadyToLoad(); + private slots: + void bakeSourceCopy(); + void handleFBXNetworkReply(); void handleBakedTexture(); - + private: void setupOutputFolder(); void loadSourceFBX(); - void handleFBXNetworkReply(QNetworkReply* requestReply); - bool importScene(); - bool rewriteAndBakeSceneTextures(); - bool exportScene(); + void bakeCopiedFBX(); + + void importScene(); + void rewriteAndBakeSceneTextures(); + void exportScene(); void removeEmbeddedMediaFolder(); void possiblyCleanupOriginals(); @@ -84,6 +91,8 @@ private: QString pathToCopyOfOriginal() const; + void handleError(const QString& error); + QUrl _fbxURL; QString _fbxName; @@ -104,6 +113,8 @@ private: QFutureSynchronizer _textureBakeSynchronizer; bool _copyOriginals { true }; + + bool _finishedNonTextureOperations { false }; }; #endif // hifi_FBXBaker_h diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index b8c49c6d50..a717835ed7 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -79,9 +79,6 @@ void TextureBaker::handleTextureNetworkReply(QNetworkReply* requestReply) { // store the original texture so it can be passed along for the bake _originalTexture = requestReply->readAll(); - - // kickoff the texture bake now that everything is ready to go - bake(); } else { // add an error to our list stating that this texture could not be downloaded qCDebug(model_baking) << "Error downloading texture" << requestReply->errorString(); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index c8e03e1224..c779fd3a40 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -37,6 +37,7 @@ DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainNam void DomainBaker::bake() { setupOutputFolder(); loadLocalFile(); + setupBakerThread(); enumerateEntities(); if (!_entitiesNeedingRewrite.isEmpty()) { @@ -48,6 +49,9 @@ void DomainBaker::bake() { writeNewEntitiesFile(); + // stop the FBX baker thread now that all our bakes have completed + _fbxBakerThread->quit(); + // we've now written out our new models file - time to say that we are finished up emit finished(); } @@ -126,6 +130,15 @@ void DomainBaker::loadLocalFile() { } } +void DomainBaker::setupBakerThread() { + // 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 on the same thread. + // We'll setup that thread now and then move the FBXBaker objects to the thread later when enumerating entities. + _fbxBakerThread = std::unique_ptr(new QThread); + _fbxBakerThread->setObjectName("Domain FBX Baker Thread"); + _fbxBakerThread->start(); +} + static const QString ENTITY_MODEL_URL_KEY = "modelURL"; void DomainBaker::enumerateEntities() { @@ -162,8 +175,10 @@ void DomainBaker::enumerateEntities() { // insert it into our bakers hash so we hold a strong pointer to it _bakers.insert(modelURL, baker); - // send the FBXBaker to the thread pool - QtConcurrent::run(baker.data(), &FBXBaker::bake); + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(_fbxBakerThread.get()); + QMetaObject::invokeMethod(baker.data(), "bake"); } // add this QJsonValueRef to our multi hash so that we can easily re-write diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index e3585ba64f..f949ddba9c 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -37,6 +38,7 @@ private slots: private: void setupOutputFolder(); void loadLocalFile(); + void setupBakerThread(); void enumerateEntities(); void writeNewEntitiesFile(); @@ -49,6 +51,7 @@ private: QJsonArray _entities; + std::unique_ptr _fbxBakerThread; QHash> _bakers; QMultiHash _entitiesNeedingRewrite; }; From 7c5376bb1f6b8b9191ca1453ea93a0a87cde972f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 14 Apr 2017 13:06:12 -0700 Subject: [PATCH 031/146] put fbx bakers on their own thread from ModelBakeWidget --- tools/oven/src/ui/ModelBakeWidget.cpp | 37 +++++++++++++++++++-------- tools/oven/src/ui/ModelBakeWidget.h | 4 +++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 5f31ed2673..3b28bb77fa 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "ModelBakeWidget.h" @@ -27,11 +28,18 @@ static const QString MODEL_START_DIR_SETTING_KEY = "model_search_directory"; ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) : QWidget(parent, flags), _exportDirectory(EXPORT_DIR_SETTING_KEY), - _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) + _modelStartDirectory(MODEL_START_DIR_SETTING_KEY), + _bakerThread(new QThread(this)) { setupUI(); } +ModelBakeWidget::~ModelBakeWidget() { + // before we go down, stop the baker thread and make sure it's done + _bakerThread->quit(); + _bakerThread->wait(); +} + void ModelBakeWidget::setupUI() { // setup a grid layout to hold everything QGridLayout* gridLayout = new QGridLayout; @@ -115,13 +123,12 @@ void ModelBakeWidget::chooseFileButtonClicked() { // 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()) { + auto directoryOfModel = QFileInfo(selectedFiles[0]).absolutePath(); - // save the directory containing this model so we can default to it next time we show the file dialog - _modelStartDirectory.set(directoryOfModel); - - // if our output directory is not yet set, set it to the directory of this model - _outputDirLineEdit->setText(directoryOfModel); + // if our output directory is not yet set, set it to the directory of this model + _outputDirLineEdit->setText(directoryOfModel); + } } } @@ -153,13 +160,11 @@ void ModelBakeWidget::bakeButtonClicked() { 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; } @@ -176,7 +181,19 @@ void ModelBakeWidget::bakeButtonClicked() { // everything seems to be in place, kick off a bake for this model now auto baker = new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false); - baker->bake(); + + // move the baker to the baker thread + baker->moveToThread(_bakerThread); + + // make sure we start the baker thread if it isn't already running + if (!_bakerThread->isRunning()) { + _bakerThread->start(); + } + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(baker, "bake"); + + // keep a unique_ptr to this baker _bakers.emplace_back(baker); } } diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 354ad9f311..f277d91938 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -19,12 +19,14 @@ #include class QLineEdit; +class QThread; class ModelBakeWidget : public QWidget { Q_OBJECT public: ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + ~ModelBakeWidget(); private slots: void chooseFileButtonClicked(); @@ -44,6 +46,8 @@ private: Setting::Handle _exportDirectory; Setting::Handle _modelStartDirectory; + + QThread* _bakerThread; }; #endif // hifi_ModelBakeWidget_h From 392549353907a0622705a22431d6b4562c92d917 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 14 Apr 2017 18:08:02 -0700 Subject: [PATCH 032/146] add a simple results window to show bake results --- libraries/model-baking/src/FBXBaker.cpp | 6 +- libraries/model-baking/src/FBXBaker.h | 1 + tools/oven/src/Oven.h | 7 +++ tools/oven/src/ui/ModelBakeWidget.cpp | 35 ++++++++++- tools/oven/src/ui/ModelBakeWidget.h | 4 +- tools/oven/src/ui/OvenMainWindow.cpp | 22 ++++++- tools/oven/src/ui/OvenMainWindow.h | 11 ++++ tools/oven/src/ui/ResultsWindow.cpp | 79 +++++++++++++++++++++++++ tools/oven/src/ui/ResultsWindow.h | 34 +++++++++++ 9 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 tools/oven/src/ui/ResultsWindow.cpp create mode 100644 tools/oven/src/ui/ResultsWindow.h diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 5b5a8b7c0a..0cc484ce5c 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -51,8 +51,8 @@ QString FBXBaker::pathToCopyOfOriginal() const { } void FBXBaker::handleError(const QString& error) { - qCCritical(model_baking) << error; - _errorList << error; + qCCritical(model_baking).noquote() << error; + _errorList.append(error); emit finished(); } @@ -200,7 +200,7 @@ void FBXBaker::importScene() { if (!importStatus) { // failed to initialize importer, print an error and return - handleError("Failed to import FBX file at" + _fbxURL.toString() + " - error:" + importer->GetStatus().GetErrorString()); + handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString()); return; } else { qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene"; diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 10c8a5c359..a6dd6ad55a 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -56,6 +56,7 @@ public: Q_INVOKABLE void bake(); bool hasErrors() const { return !_errorList.isEmpty(); } + QStringList getErrors() const { return _errorList; } QUrl getFBXUrl() const { return _fbxURL; } QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h index 7ec9bdbd3b..3fc9a4c0f6 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -14,6 +14,11 @@ #include +#if defined(qApp) +#undef qApp +#endif +#define qApp (static_cast(QCoreApplication::instance())) + class OvenMainWindow; class Oven : public QApplication { @@ -22,6 +27,8 @@ class Oven : public QApplication { public: Oven(int argc, char* argv[]); + OvenMainWindow* getMainWindow() const { return _mainWindow; } + private: OvenMainWindow* _mainWindow; }; diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 3b28bb77fa..cf21483255 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -20,6 +20,9 @@ #include #include +#include "../Oven.h" +#include "OvenMainWindow.h" + #include "ModelBakeWidget.h" static const QString EXPORT_DIR_SETTING_KEY = "model_export_directory"; @@ -180,7 +183,7 @@ void ModelBakeWidget::bakeButtonClicked() { } // everything seems to be in place, kick off a bake for this model now - auto baker = new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false); + auto baker = QSharedPointer { new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false) }; // move the baker to the baker thread baker->moveToThread(_bakerThread); @@ -191,10 +194,36 @@ void ModelBakeWidget::bakeButtonClicked() { } // invoke the bake method on the baker thread - QMetaObject::invokeMethod(baker, "bake"); + QMetaObject::invokeMethod(baker.data(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(baker.data(), &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()); // keep a unique_ptr to this baker - _bakers.emplace_back(baker); + // and remember the row that represents it in the results table + _bakers.insert(baker, resultsRow); + } +} + +void ModelBakeWidget::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // turn this baker into a shared pointer + auto sharedBaker = QSharedPointer(baker); + + // add the results of this bake to the results window + auto resultRow = _bakers.value(sharedBaker); + + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + if (sharedBaker->hasErrors()) { + resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } } } diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index f277d91938..3d76ec0d4d 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -36,10 +36,12 @@ private slots: void outputDirectoryChanged(const QString& newDirectory); + void handleFinishedBaker(); + private: void setupUI(); - std::list> _bakers; + QHash, int> _bakers; QLineEdit* _modelLineEdit; QLineEdit* _outputDirLineEdit; diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp index 9c25fb5c72..1cf2986d17 100644 --- a/tools/oven/src/ui/OvenMainWindow.cpp +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -21,7 +21,6 @@ OvenMainWindow::OvenMainWindow(QWidget *parent, Qt::WindowFlags flags) : setWindowTitle("High Fidelity Oven"); // give the window a fixed width that will never change - const int FIXED_WINDOW_WIDTH = 640; setFixedWidth(FIXED_WINDOW_WIDTH); // setup a stacked layout for the main "modes" menu and subseq @@ -30,3 +29,24 @@ OvenMainWindow::OvenMainWindow(QWidget *parent, Qt::WindowFlags flags) : setCentralWidget(stackedWidget); } + +OvenMainWindow::~OvenMainWindow() { + if (_resultsWindow) { + _resultsWindow->close(); + _resultsWindow->deleteLater(); + } +} + +ResultsWindow* OvenMainWindow::showResultsWindow() { + if (!_resultsWindow) { + // we don't have a results window right now, so make a new one + _resultsWindow = new ResultsWindow; + } + + // show the results window, place it right below our window + _resultsWindow->show(); + _resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom()); + + // 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 index 0941be543b..2d5d2aec99 100644 --- a/tools/oven/src/ui/OvenMainWindow.h +++ b/tools/oven/src/ui/OvenMainWindow.h @@ -12,12 +12,23 @@ #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(); + +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..99389f363c --- /dev/null +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -0,0 +1,79 @@ +// +// 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 "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); + + // set the layout of this widget to the created layout + setLayout(resultsLayout); +} + + +int ResultsWindow::addPendingResultRow(const QString& fileName) { + 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); + + return rowIndex; +} + +void ResultsWindow::changeStatusForRow(int rowIndex, const QString& result) { + auto statusItem = new QTableWidgetItem(result); + statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable); + _resultsTable->setItem(rowIndex, 1, statusItem); +} diff --git a/tools/oven/src/ui/ResultsWindow.h b/tools/oven/src/ui/ResultsWindow.h new file mode 100644 index 0000000000..b7e380a631 --- /dev/null +++ b/tools/oven/src/ui/ResultsWindow.h @@ -0,0 +1,34 @@ +// +// 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 + +class QTableWidget; + +class ResultsWindow : public QWidget { + Q_OBJECT + +public: + ResultsWindow(QWidget* parent = nullptr); + + void setupUI(); + + int addPendingResultRow(const QString& fileName); + void changeStatusForRow(int rowIndex, const QString& result); + +private: + QTableWidget* _resultsTable { nullptr }; +}; + +#endif // hifi_ResultsWindow_h From 2b188427f1bb8cc543434653da4826fca1ab561d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 14 Apr 2017 18:20:55 -0700 Subject: [PATCH 033/146] cleanup memory management in memory bake widget --- tools/oven/src/ui/ModelBakeWidget.cpp | 28 ++++++++++++++------------- tools/oven/src/ui/ModelBakeWidget.h | 4 +++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index cf21483255..08b5402821 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -183,7 +183,7 @@ void ModelBakeWidget::bakeButtonClicked() { } // everything seems to be in place, kick off a bake for this model now - auto baker = QSharedPointer { new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false) }; + auto baker = std::unique_ptr { new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false) }; // move the baker to the baker thread baker->moveToThread(_bakerThread); @@ -194,10 +194,10 @@ void ModelBakeWidget::bakeButtonClicked() { } // invoke the bake method on the baker thread - QMetaObject::invokeMethod(baker.data(), "bake"); + QMetaObject::invokeMethod(baker.get(), "bake"); // make sure we hear about the results of this baker when it is done - connect(baker.data(), &FBXBaker::finished, this, &ModelBakeWidget::handleFinishedBaker); + 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(); @@ -205,24 +205,26 @@ void ModelBakeWidget::bakeButtonClicked() { // keep a unique_ptr to this baker // and remember the row that represents it in the results table - _bakers.insert(baker, resultsRow); + _bakers.emplace_back(std::move(baker), resultsRow); } } void ModelBakeWidget::handleFinishedBaker() { if (auto baker = qobject_cast(sender())) { - // turn this baker into a shared pointer - auto sharedBaker = QSharedPointer(baker); - // add the results of this bake to the results window - auto resultRow = _bakers.value(sharedBaker); + auto it = std::remove_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); - auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); - if (sharedBaker->hasErrors()) { - resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); - } else { - resultsWindow->changeStatusForRow(resultRow, "Success"); + if (baker->hasErrors()) { + resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } } } } diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 3d76ec0d4d..9b7a2fed20 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -41,7 +41,9 @@ private slots: private: void setupUI(); - QHash, int> _bakers; + using BakerRowPair = std::pair, int>; + using BakerRowPairList = std::list; + BakerRowPairList _bakers; QLineEdit* _modelLineEdit; QLineEdit* _outputDirLineEdit; From 83eb37b8141db997d2054a023732dcaf9b533f28 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 14 Apr 2017 18:33:36 -0700 Subject: [PATCH 034/146] add domain bake to results table --- libraries/model-baking/src/Baker.cpp | 20 ++++++++++++ libraries/model-baking/src/Baker.h | 35 ++++++++++++++++++++ libraries/model-baking/src/FBXBaker.cpp | 6 ---- libraries/model-baking/src/FBXBaker.h | 11 +++---- libraries/model-baking/src/TextureBaker.h | 4 ++- tools/oven/src/DomainBaker.h | 5 +-- tools/oven/src/ui/DomainBakeWidget.cpp | 40 ++++++++++++++++++++--- tools/oven/src/ui/DomainBakeWidget.h | 6 +++- 8 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 libraries/model-baking/src/Baker.cpp create mode 100644 libraries/model-baking/src/Baker.h diff --git a/libraries/model-baking/src/Baker.cpp b/libraries/model-baking/src/Baker.cpp new file mode 100644 index 0000000000..8e118790cc --- /dev/null +++ b/libraries/model-baking/src/Baker.cpp @@ -0,0 +1,20 @@ +// +// Baker.cpp +// libraries/model-baking/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(); +} diff --git a/libraries/model-baking/src/Baker.h b/libraries/model-baking/src/Baker.h new file mode 100644 index 0000000000..19b1486346 --- /dev/null +++ b/libraries/model-baking/src/Baker.h @@ -0,0 +1,35 @@ +// +// Baker.h +// libraries/model-baking/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: + virtual void bake() = 0; + + bool hasErrors() const { return !_errorList.isEmpty(); } + QStringList getErrors() const { return _errorList; } + +signals: + void finished(); + +protected: + void handleError(const QString& error); + + QStringList _errorList; +}; + +#endif // hifi_Baker_h diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 0cc484ce5c..8181932247 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -50,12 +50,6 @@ QString FBXBaker::pathToCopyOfOriginal() const { return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); } -void FBXBaker::handleError(const QString& error) { - qCCritical(model_baking).noquote() << error; - _errorList.append(error); - emit finished(); -} - void FBXBaker::bake() { qCDebug(model_baking) << "Baking" << _fbxURL; diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index a6dd6ad55a..a44ce4d0bf 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -17,6 +17,8 @@ #include #include +#include "Baker.h" + namespace fbxsdk { class FbxManager; class FbxProperty; @@ -47,16 +49,13 @@ class TextureBaker; static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; -class FBXBaker : public QObject { +class FBXBaker : public Baker { Q_OBJECT public: FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals = true); ~FBXBaker(); - Q_INVOKABLE void bake(); - - bool hasErrors() const { return !_errorList.isEmpty(); } - QStringList getErrors() const { return _errorList; } + Q_INVOKABLE virtual void bake() override; QUrl getFBXUrl() const { return _fbxURL; } QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } @@ -92,8 +91,6 @@ private: QString pathToCopyOfOriginal() const; - void handleError(const QString& error); - QUrl _fbxURL; QString _fbxName; diff --git a/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h index 7394a0652e..06bac0d066 100644 --- a/libraries/model-baking/src/TextureBaker.h +++ b/libraries/model-baking/src/TextureBaker.h @@ -15,7 +15,9 @@ #include #include -class TextureBaker : public QObject { +#include "Baker.h" + +class TextureBaker : public Baker { Q_OBJECT public: diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index f949ddba9c..3eae758445 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -17,16 +17,17 @@ #include #include +#include #include -class DomainBaker : public QObject { +class DomainBaker : public Baker { Q_OBJECT public: DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, const QString& baseOutputPath, const QUrl& destinationPath); public: - void bake(); + virtual void bake() override; signals: void finished(); diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 01bc6110cb..b1f549dd00 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -21,6 +21,9 @@ #include #include +#include "../Oven.h" +#include "OvenMainWindow.h" + #include "DomainBakeWidget.h" static const QString DOMAIN_NAME_SETTING_KEY = "domain_name"; @@ -205,15 +208,44 @@ void DomainBakeWidget::bakeButtonClicked() { 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()); - _baker = std::unique_ptr { + auto domainBaker = std::unique_ptr { new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), outputDirectory.absolutePath(), _destinationPathLineEdit->text()) }; - // run the baker in our thread pool - QtConcurrent::run(_baker.get(), &DomainBaker::bake); + // make sure we hear from the baker when it is done + connect(domainBaker.get(), &DomainBaker::finished, this, &DomainBakeWidget::handleFinishedBaker); - return; + // run the baker in our thread pool + QtConcurrent::run(domainBaker.get(), &DomainBaker::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); + + // 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::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::remove_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"); + } + } } } diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index 20b4eaa4b9..606f550203 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -34,10 +34,14 @@ private slots: void outputDirectoryChanged(const QString& newDirectory); + void handleFinishedBaker(); + private: void setupUI(); - std::unique_ptr _baker; + using BakerRowPair = std::pair, int>; + using BakerRowPairList = std::list; + BakerRowPairList _bakers; QLineEdit* _domainNameLineEdit; QLineEdit* _entitiesFileLineEdit; From 429e65888b38a3c86f866029b5bb28c4b4c1378c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Sun, 16 Apr 2017 15:52:17 -0700 Subject: [PATCH 035/146] cleanup threading and result handling for DomainBaker --- libraries/model-baking/src/Baker.cpp | 12 ++ libraries/model-baking/src/Baker.h | 8 ++ libraries/model-baking/src/FBXBaker.cpp | 150 ++++++++++++-------- libraries/model-baking/src/FBXBaker.h | 14 +- libraries/model-baking/src/TextureBaker.cpp | 8 +- libraries/model-baking/src/TextureBaker.h | 5 +- tools/oven/src/DomainBaker.cpp | 91 ++++++++---- tools/oven/src/DomainBaker.h | 1 - tools/oven/src/ui/DomainBakeWidget.cpp | 2 + 9 files changed, 190 insertions(+), 101 deletions(-) diff --git a/libraries/model-baking/src/Baker.cpp b/libraries/model-baking/src/Baker.cpp index 8e118790cc..b692c1d96b 100644 --- a/libraries/model-baking/src/Baker.cpp +++ b/libraries/model-baking/src/Baker.cpp @@ -18,3 +18,15 @@ void Baker::handleError(const QString& error) { _errorList.append(error); emit finished(); } + +void Baker::appendErrors(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/libraries/model-baking/src/Baker.h b/libraries/model-baking/src/Baker.h index 19b1486346..6853620361 100644 --- a/libraries/model-baking/src/Baker.h +++ b/libraries/model-baking/src/Baker.h @@ -23,13 +23,21 @@ public: bool hasErrors() const { return !_errorList.isEmpty(); } QStringList getErrors() const { return _errorList; } + bool hasWarnings() const { return !_warningList.isEmpty(); } + QStringList getWarnings() const { return _warningList; } + signals: void finished(); protected: void handleError(const QString& error); + void handleWarning(const QString& warning); + + void appendErrors(const QStringList& errors); + void appendWarnings(const QStringList& warnings) { _warningList << warnings; } QStringList _errorList; + QStringList _warningList; }; #endif // hifi_Baker_h diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 8181932247..ff1d6659aa 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -18,6 +18,8 @@ #include #include +#include + #include #include "ModelBakingLoggingCategory.h" @@ -25,24 +27,26 @@ #include "FBXBaker.h" +std::once_flag onceFlag; +FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr }; FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals) : _fbxURL(fbxURL), _baseOutputPath(baseOutputPath), _copyOriginals(copyOriginals) { - // create an FBX SDK manager - _sdkManager = FbxManager::Create(); + 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.indexOf('.')); } -FBXBaker::~FBXBaker() { - _sdkManager->Destroy(); -} - static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; static const QString ORIGINAL_OUTPUT_SUBFOLDER = "original/"; @@ -88,15 +92,8 @@ void FBXBaker::bakeSourceCopy() { return; } - // remove the embedded media folder that the FBX SDK produces when reading the original - removeEmbeddedMediaFolder(); - - if (hasErrors()) { - return; - } - - // cleanup the originals if we weren't asked to keep them around - possiblyCleanupOriginals(); + // check if we're already done with textures (in case we had none to re-write) + checkIfTexturesFinished(); } void FBXBaker::setupOutputFolder() { @@ -186,7 +183,7 @@ void FBXBaker::handleFBXNetworkReply() { void FBXBaker::importScene() { // create an FBX SDK importer - FbxImporter* importer = FbxImporter::Create(_sdkManager, ""); + FbxImporter* importer = FbxImporter::Create(_sdkManager.get(), ""); // import the copy of the original FBX file QString originalCopyPath = pathToCopyOfOriginal(); @@ -201,7 +198,7 @@ void FBXBaker::importScene() { } // setup a new scene to hold the imported file - _scene = FbxScene::Create(_sdkManager, "bakeScene"); + _scene = FbxScene::Create(_sdkManager.get(), "bakeScene"); // import the file to the created scene importer->Import(_scene); @@ -397,13 +394,15 @@ void FBXBaker::rewriteAndBakeSceneTextures() { // figure out the URL to this texture, embedded or external auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); - - // add the deduced url to the texture, associated with the resulting baked texture file name, - // to our hash of textures needing to be baked - _unbakedTextures.insert(urlToTexture, bakedTextureFileName); - - // bake this texture asynchronously - bakeTexture(urlToTexture); + + if (!_unbakedTextures.contains(urlToTexture)) { + // add the deduced url to the texture, associated with the resulting baked texture file name, + // to our hash of textures needing to be baked + _unbakedTextures.insert(urlToTexture, bakedTextureFileName); + + // bake this texture asynchronously + bakeTexture(urlToTexture); + } } } } @@ -417,63 +416,68 @@ void FBXBaker::rewriteAndBakeSceneTextures() { void FBXBaker::bakeTexture(const QUrl& textureURL) { // start a bake for this texture and add it to our list to keep track of - auto bakingTexture = new TextureBaker(textureURL); + QSharedPointer bakingTexture { new TextureBaker(textureURL), &TextureBaker::deleteLater }; - connect(bakingTexture, &TextureBaker::finished, this, &FBXBaker::handleBakedTexture); + connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); - QtConcurrent::run(bakingTexture, &TextureBaker::bake); + QtConcurrent::run(bakingTexture.data(), &TextureBaker::bake); - _bakingTextures.emplace_back(bakingTexture); + _bakingTextures.insert(bakingTexture); } void FBXBaker::handleBakedTexture() { - auto bakedTexture = qobject_cast(sender()); + TextureBaker* bakedTexture = qobject_cast(sender()); - // use the path to the texture being baked to determine if this was an embedded or a linked texture + // make sure we haven't already run into errors, and that this is a valid texture + if (!hasErrors() && bakedTexture) { + if (!bakedTexture->hasErrors()) { + // 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 + // 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); + 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 + 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(); + 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()); + // 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() - }; + QFile originalTextureFile { + _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName() + }; - if (relativeTexturePath.length() > 0) { - // make the folders needed by the relative path + 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 list + _unbakedTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } else { + // there was an error baking this texture - add it to our list of errors and stop processing this FBX + appendErrors(bakedTexture->getErrors()); } - - 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 list - _unbakedTextures.remove(bakedTexture->getTextureURL()); - - // check if we're done everything we need to do for this FBX - if (_unbakedTextures.isEmpty()) { - emit finished(); } } void FBXBaker::exportScene() { // setup the exporter - FbxExporter* exporter = FbxExporter::Create(_sdkManager, ""); + FbxExporter* exporter = FbxExporter::Create(_sdkManager.get(), ""); auto rewrittenFBXPath = _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + _fbxName + BAKED_FBX_EXTENSION; @@ -508,3 +512,27 @@ void FBXBaker::possiblyCleanupOriginals() { 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 (_unbakedTextures.isEmpty()) { + // remove the embedded media folder that the FBX SDK produces when reading the original + removeEmbeddedMediaFolder(); + + if (hasErrors()) { + return; + } + + // cleanup the originals if we weren't asked to keep them around + possiblyCleanupOriginals(); + + if (hasErrors()) { + return; + } + + qCDebug(model_baking) << "Finished baking" << _fbxURL; + + emit finished(); + } +} diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index a44ce4d0bf..ef94de7a2e 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -18,6 +18,7 @@ #include #include "Baker.h" +#include "TextureBaker.h" namespace fbxsdk { class FbxManager; @@ -45,23 +46,22 @@ enum TextureType { UNUSED_TEXTURE = -1 }; -class TextureBaker; - static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; +using FBXSDKManagerUniquePointer = std::unique_ptr>; class FBXBaker : public Baker { Q_OBJECT public: FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals = true); - ~FBXBaker(); + // all calls to bake must be from the same thread, because the Autodesk SDK will cause + // a crash if it is called from multiple threads Q_INVOKABLE virtual void bake() override; QUrl getFBXUrl() const { return _fbxURL; } QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } signals: - void finished(); void allTexturesBaked(); void sourceCopyReadyToLoad(); @@ -84,6 +84,8 @@ private: void removeEmbeddedMediaFolder(); void possiblyCleanupOriginals(); + void checkIfTexturesFinished(); + QString createBakedTextureFileName(const QFileInfo& textureFileInfo); QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); @@ -98,7 +100,7 @@ private: QString _uniqueOutputPath; QString _bakedFBXRelativePath; - fbxsdk::FbxManager* _sdkManager; + static FBXSDKManagerUniquePointer _sdkManager; fbxsdk::FbxScene* _scene { nullptr }; QStringList _errorList; @@ -107,7 +109,7 @@ private: QHash _textureNameMatchCount; QHash _textureTypes; - std::list> _bakingTextures; + QSet> _bakingTextures; QFutureSynchronizer _textureBakeSynchronizer; bool _copyOriginals { true }; diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index a717835ed7..bdd20ea270 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -29,6 +29,10 @@ void TextureBaker::bake() { // first load the texture (either locally or remotely) loadTexture(); + if (hasErrors()) { + return; + } + qCDebug(model_baking) << "Baking texture at" << _textureURL; emit finished(); @@ -41,9 +45,7 @@ void TextureBaker::loadTexture() { QFile localTexture { _textureURL.toLocalFile() }; if (!localTexture.open(QIODevice::ReadOnly)) { - qCWarning(model_baking) << "Unable to open local texture at" << _textureURL << "for baking"; - - emit finished(); + handleError("Unable to open texture " + _textureURL.toString()); return; } diff --git a/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h index 06bac0d066..4f793af37d 100644 --- a/libraries/model-baking/src/TextureBaker.h +++ b/libraries/model-baking/src/TextureBaker.h @@ -22,16 +22,13 @@ class TextureBaker : public Baker { public: TextureBaker(const QUrl& textureURL); - + void bake(); const QByteArray& getOriginalTexture() const { return _originalTexture; } const QUrl& getTextureURL() const { return _textureURL; } -signals: - void finished(); - private: void loadTexture(); void handleTextureNetworkReply(QNetworkReply* requestReply); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index c779fd3a40..b53b74f227 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -36,10 +36,29 @@ DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainNam void DomainBaker::bake() { setupOutputFolder(); + + if (hasErrors()) { + return; + } + loadLocalFile(); + + if (hasErrors()) { + return; + } + setupBakerThread(); + + if (hasErrors()) { + return; + } + enumerateEntities(); + if (hasErrors()) { + return; + } + if (!_entitiesNeedingRewrite.isEmpty()) { // use a QEventLoop to wait for all entity rewrites to be completed before writing the final models file QEventLoop eventLoop; @@ -47,8 +66,16 @@ void DomainBaker::bake() { eventLoop.exec(); } + if (hasErrors()) { + return; + } + writeNewEntitiesFile(); + if (hasErrors()) { + return; + } + // stop the FBX baker thread now that all our bakes have completed _fbxBakerThread->quit(); @@ -70,8 +97,8 @@ void DomainBaker::setupOutputFolder() { 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; } @@ -84,7 +111,7 @@ void DomainBaker::setupOutputFolder() { 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; } @@ -95,17 +122,18 @@ const QString ENTITIES_OBJECT_KEY = "Entities"; void DomainBaker::loadLocalFile() { // load up the local entities file - QFile modelsFile { _localEntitiesFileURL.toLocalFile() }; + QFile entitiesFile { _localEntitiesFileURL.toLocalFile() }; - if (!modelsFile.open(QIODevice::ReadOnly)) { + 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 = modelsFile.readAll(); + 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"; @@ -167,10 +195,10 @@ void DomainBaker::enumerateEntities() { // setup an FBXBaker for this URL, as long as we don't already have one if (!_bakers.contains(modelURL)) { - QSharedPointer baker { new FBXBaker(modelURL, _contentOutputPath) }; + QSharedPointer baker { new FBXBaker(modelURL, _contentOutputPath), &FBXBaker::deleteLater }; // make sure our handler is called when the baker is done - connect(baker.data(), &FBXBaker::finished, this, &DomainBaker::handleFinishedBaker); + connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedBaker); // insert it into our bakers hash so we hold a strong pointer to it _bakers.insert(modelURL, baker); @@ -194,35 +222,44 @@ void DomainBaker::handleFinishedBaker() { auto baker = qobject_cast(sender()); if (baker) { - // this FBXBaker is done and everything went according to plan + if (!baker->hasErrors()) { + // this FBXBaker is done and everything went according to plan - // 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())) { + // 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(); + // 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() }; + // 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().mid(1)); + // setup a new URL using the prefix we were passed + QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath().mid(1)); - // copy the fragment and query from the old model URL - newModelURL.setQuery(oldModelURL.query()); - newModelURL.setFragment(oldModelURL.fragment()); + // copy the fragment and query from the old model URL + newModelURL.setQuery(oldModelURL.query()); + newModelURL.setFragment(oldModelURL.fragment()); - // set the new model URL as the value in our temp QJsonObject - entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString(); - - // replace our temp object with the value referenced by our QJsonValueRef - entityValue = entity; + // set the new model URL as the value in our temp QJsonObject + entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString(); + + // 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 errors + appendWarnings(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 + _bakers.remove(baker->getFBXUrl()); + if (_entitiesNeedingRewrite.isEmpty()) { emit allModelsFinished(); } @@ -256,12 +293,14 @@ void DomainBaker::writeNewEntitiesFile() { if (!compressedEntitiesFile.open(QIODevice::WriteOnly) || (compressedEntitiesFile.write(compressedJson) == -1)) { - qWarning() << "Failed to export baked entities file to" << bakedEntitiesFilePath; + // 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; + qDebug() << "WARNINGS:" << _warningList; } diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 3eae758445..6c8555cc62 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -30,7 +30,6 @@ public: virtual void bake() override; signals: - void finished(); void allModelsFinished(); private slots: diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index b1f549dd00..7c8c462cfd 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -242,6 +242,8 @@ void DomainBakeWidget::handleFinishedBaker() { if (baker->hasErrors()) { resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else if (baker->hasWarnings()) { + resultsWindow->changeStatusForRow(resultRow, baker->getWarnings().join("\n")); } else { resultsWindow->changeStatusForRow(resultRow, "Success"); } From 49e7ae6dbc1d2f4a778f24a768a9056765e86b1f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 09:14:34 -0700 Subject: [PATCH 036/146] call image library for texture baking --- libraries/gpu/src/gpu/Texture.h | 2 + libraries/model-baking/CMakeLists.txt | 2 +- libraries/model-baking/src/Baker.cpp | 2 +- libraries/model-baking/src/Baker.h | 3 +- libraries/model-baking/src/FBXBaker.cpp | 170 +++++++++--------- libraries/model-baking/src/FBXBaker.h | 28 +-- libraries/model-baking/src/TextureBaker.cpp | 50 +++++- libraries/model-baking/src/TextureBaker.h | 12 +- .../src/model-networking/TextureCache.h | 2 - libraries/shared/src/Profile.cpp | 2 +- tools/oven/CMakeLists.txt | 2 +- tools/oven/src/DomainBaker.cpp | 4 +- tools/oven/src/ui/DomainBakeWidget.cpp | 3 - 13 files changed, 158 insertions(+), 124 deletions(-) diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 9b23b4e695..7c6d5d0659 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/model-baking/CMakeLists.txt b/libraries/model-baking/CMakeLists.txt index a0774cdcc1..2e08488d69 100644 --- a/libraries/model-baking/CMakeLists.txt +++ b/libraries/model-baking/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME model-baking) setup_hifi_library(Concurrent) -link_hifi_libraries(networking) +link_hifi_libraries(networking image gpu shared ktx) find_package(FBXSDK REQUIRED) target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) diff --git a/libraries/model-baking/src/Baker.cpp b/libraries/model-baking/src/Baker.cpp index b692c1d96b..666e59a073 100644 --- a/libraries/model-baking/src/Baker.cpp +++ b/libraries/model-baking/src/Baker.cpp @@ -19,7 +19,7 @@ void Baker::handleError(const QString& error) { emit finished(); } -void Baker::appendErrors(const QStringList& errors) { +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); diff --git a/libraries/model-baking/src/Baker.h b/libraries/model-baking/src/Baker.h index 6853620361..ab9c22ac53 100644 --- a/libraries/model-baking/src/Baker.h +++ b/libraries/model-baking/src/Baker.h @@ -33,8 +33,7 @@ protected: void handleError(const QString& error); void handleWarning(const QString& warning); - void appendErrors(const QStringList& errors); - void appendWarnings(const QStringList& warnings) { _warningList << warnings; } + void handleErrors(const QStringList& errors); QStringList _errorList; QStringList _warningList; diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index ff1d6659aa..0a25d3c299 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -207,8 +207,6 @@ void FBXBaker::importScene() { importer->Destroy(); } -static const QString BAKED_TEXTURE_EXT = ".ktx"; - 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); @@ -256,49 +254,25 @@ QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* f QString relativeFileName = fileTexture->GetRelativeFileName(); auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); -#ifndef Q_OS_WIN - // it turns out that paths that start with a drive letter and a colon appear to QFileInfo - // as a relative path on UNIX systems - we perform a special check here to handle that case - bool isAbsolute = relativeFileName[1] == ':' || apparentRelativePath.isAbsolute(); -#else - bool isAbsolute = apparentRelativePath.isAbsolute(); -#endif - - if (isAbsolute) { - // this is a relative file path which will require different handling - // depending on the location of the original FBX - if (_fbxURL.isLocalFile()) { - // since the loaded FBX is loaded, first check if we actually have the texture locally - // at the absolute path - if (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()); - } - } else { - // the original FBX was remote and downloaded - - // since this "relative" texture path is actually absolute, we have to assume it is beside the FBX - // which matches the behaviour of Interface - - // append that path to our list of unbaked textures - urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); - } + // 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 { - // simply construct a URL with the relative path to the asset, locally or remotely - // and append that to the list of unbaked textures - urlToTexture = _fbxURL.resolved(apparentRelativePath.filePath()); + // 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; } -TextureType textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { +gpu::TextureType textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { + using namespace gpu; + // 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 @@ -366,7 +340,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { // figure out the type of texture from the material property auto textureType = textureTypeForMaterialProperty(property, material); - if (textureType != UNUSED_TEXTURE) { + if (textureType != gpu::UNUSED_TEXTURE) { int numTextures = property.GetSrcObjectCount(); for (int j = 0; j < numTextures; j++) { @@ -401,7 +375,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { _unbakedTextures.insert(urlToTexture, bakedTextureFileName); // bake this texture asynchronously - bakeTexture(urlToTexture); + bakeTexture(urlToTexture, textureType, bakedTextureFilePath); } } } @@ -414,63 +388,91 @@ void FBXBaker::rewriteAndBakeSceneTextures() { } } -void FBXBaker::bakeTexture(const QUrl& textureURL) { +void FBXBaker::bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath) { // start a bake for this texture and add it to our list to keep track of - QSharedPointer bakingTexture { new TextureBaker(textureURL), &TextureBaker::deleteLater }; + QSharedPointer bakingTexture { + new TextureBaker(textureURL, textureType, destinationFilePath), + &TextureBaker::deleteLater + }; + // make sure we hear when the baking texture is done connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); - QtConcurrent::run(bakingTexture.data(), &TextureBaker::bake); - + // keep a shared pointer to the baking texture _bakingTextures.insert(bakingTexture); + + // start baking the texture on our thread pool + QtConcurrent::run(bakingTexture.data(), &TextureBaker::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 (!hasErrors() && bakedTexture) { - if (!bakedTexture->hasErrors()) { - // use the path to the texture being baked to determine if this was an embedded or a linked 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 - // 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 + // use the path to the texture being baked to determine if this was an embedded or a linked texture - auto originalOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER); + // 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 - if (!originalOutputFolder.isParentOf(bakedTexture->getTextureURL())) { - // for linked textures we want to save a copy of original texture beside the original FBX + auto originalOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER); - qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); + if (!originalOutputFolder.isParentOf(bakedTexture->getTextureURL())) { + // for linked textures we want to save a copy of original texture beside the original FBX - // check if we have a relative path to use for the texture - auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL()); + qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); - QFile originalTextureFile { - _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName() - }; + // check if we have a relative path to use for the texture + auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL()); - if (relativeTexturePath.length() > 0) { - // make the folders needed by the relative path + 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; + } + } } - 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 list + _unbakedTextures.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 + _unbakedTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); } - - // now that this texture has been baked and handled, we can remove that TextureBaker from our list + } 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 _unbakedTextures.remove(bakedTexture->getTextureURL()); checkIfTexturesFinished(); - } else { - // there was an error baking this texture - add it to our list of errors and stop processing this FBX - appendErrors(bakedTexture->getErrors()); } } } @@ -516,23 +518,27 @@ void FBXBaker::possiblyCleanupOriginals() { 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 (_unbakedTextures.isEmpty()) { // remove the embedded media folder that the FBX SDK produces when reading the original removeEmbeddedMediaFolder(); - if (hasErrors()) { - return; - } - // cleanup the originals if we weren't asked to keep them around possiblyCleanupOriginals(); if (hasErrors()) { - return; - } + // if we're checking for completion but we have errors + // that means one or more of our texture baking operations failed - qCDebug(model_baking) << "Finished baking" << _fbxURL; - - emit finished(); + if (_pendingErrorEmission) { + emit finished(); + } + + return; + } else { + qCDebug(model_baking) << "Finished baking" << _fbxURL; + + emit finished(); + } } } diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index ef94de7a2e..cca4878308 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -20,6 +20,8 @@ #include "Baker.h" #include "TextureBaker.h" +#include + namespace fbxsdk { class FbxManager; class FbxProperty; @@ -27,25 +29,6 @@ namespace fbxsdk { class FbxFileTexture; } -enum TextureType { - DEFAULT_TEXTURE, - STRICT_TEXTURE, - ALBEDO_TEXTURE, - NORMAL_TEXTURE, - BUMP_TEXTURE, - SPECULAR_TEXTURE, - METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey - ROUGHNESS_TEXTURE, - GLOSS_TEXTURE, - EMISSIVE_TEXTURE, - CUBE_TEXTURE, - OCCLUSION_TEXTURE, - SCATTERING_TEXTURE = OCCLUSION_TEXTURE, - LIGHTMAP_TEXTURE, - CUSTOM_TEXTURE, - UNUSED_TEXTURE = -1 -}; - static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; using FBXSDKManagerUniquePointer = std::unique_ptr>; @@ -89,7 +72,7 @@ private: QString createBakedTextureFileName(const QFileInfo& textureFileInfo); QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); - void bakeTexture(const QUrl& textureURL); + void bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath); QString pathToCopyOfOriginal() const; @@ -103,18 +86,15 @@ private: static FBXSDKManagerUniquePointer _sdkManager; fbxsdk::FbxScene* _scene { nullptr }; - QStringList _errorList; - QHash _unbakedTextures; QHash _textureNameMatchCount; - QHash _textureTypes; QSet> _bakingTextures; QFutureSynchronizer _textureBakeSynchronizer; bool _copyOriginals { true }; - bool _finishedNonTextureOperations { false }; + bool _pendingErrorEmission { false }; }; #endif // hifi_FBXBaker_h diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index bdd20ea270..82f42a5776 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -9,20 +9,27 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include #include +#include +#include #include #include "ModelBakingLoggingCategory.h" #include "TextureBaker.h" -TextureBaker::TextureBaker(const QUrl& textureURL) : - _textureURL(textureURL) +const QString BAKED_TEXTURE_EXT = ".ktx"; + +TextureBaker::TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath) : + _textureURL(textureURL), + _textureType(textureType), + _destinationFilePath(destinationFilePath) { - + } void TextureBaker::bake() { @@ -35,6 +42,14 @@ void TextureBaker::bake() { qCDebug(model_baking) << "Baking texture at" << _textureURL; + processTexture(); + + if (hasErrors()) { + return; + } + + qCDebug(model_baking) << "Baked texture at" << _textureURL; + emit finished(); } @@ -83,6 +98,33 @@ void TextureBaker::handleTextureNetworkReply(QNetworkReply* requestReply) { _originalTexture = requestReply->readAll(); } else { // add an error to our list stating that this texture could not be downloaded - qCDebug(model_baking) << "Error downloading texture" << requestReply->errorString(); + 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; + } + + 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 { _destinationFilePath }; + + if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { + handleError("Could not write baked texture for " + _textureURL.toString()); } } diff --git a/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h index 4f793af37d..17c725b57d 100644 --- a/libraries/model-baking/src/TextureBaker.h +++ b/libraries/model-baking/src/TextureBaker.h @@ -14,14 +14,19 @@ #include #include +#include + +#include #include "Baker.h" +extern const QString BAKED_TEXTURE_EXT; + class TextureBaker : public Baker { Q_OBJECT public: - TextureBaker(const QUrl& textureURL); + TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath); void bake(); @@ -33,8 +38,13 @@ private: void loadTexture(); void handleTextureNetworkReply(QNetworkReply* requestReply); + void processTexture(); + QUrl _textureURL; QByteArray _originalTexture; + gpu::TextureType _textureType; + + QString _destinationFilePath; }; #endif // hifi_TextureBaker_h diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 1e61b9ecee..ade1acdb64 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/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/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 2c5d0b98e5..1e644a2c62 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,4 +2,4 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(model-baking shared) +link_hifi_libraries(model-baking shared image gpu ktx) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index b53b74f227..6bee07986c 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -236,7 +236,7 @@ void DomainBaker::handleFinishedBaker() { QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; // setup a new URL using the prefix we were passed - QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath().mid(1)); + QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); // copy the fragment and query from the old model URL newModelURL.setQuery(oldModelURL.query()); @@ -251,7 +251,7 @@ void DomainBaker::handleFinishedBaker() { } 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 errors - appendWarnings(baker->getErrors()); + _warningList << baker->getErrors(); } // remove the baked URL from the multi hash of entities needing a re-write diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 7c8c462cfd..c0b9c85910 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -160,9 +160,6 @@ void DomainBakeWidget::chooseFileButtonClicked() { // save the directory containing this entities file so we can default to it next time we show the file dialog _browseStartDirectory.set(directoryOfEntitiesFile); - - // if our output directory is not yet set, set it to the directory of this entities file - _outputDirLineEdit->setText(directoryOfEntitiesFile); } } From 446cbf59b3a59c845d6c423b38e06d0aa3f1ee63 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 09:33:52 -0700 Subject: [PATCH 037/146] add domain bake progress to results table --- tools/oven/src/DomainBaker.cpp | 7 +++++++ tools/oven/src/DomainBaker.h | 3 +++ tools/oven/src/ui/DomainBakeWidget.cpp | 23 +++++++++++++++++++++++ tools/oven/src/ui/DomainBakeWidget.h | 1 + 4 files changed, 34 insertions(+) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 6bee07986c..5589df1645 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -212,10 +212,14 @@ void DomainBaker::enumerateEntities() { // 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); + ++_totalNumberOfEntities; } } } } + + // emit progress now to say we're just starting + emit bakeProgress(0, _totalNumberOfEntities); } void DomainBaker::handleFinishedBaker() { @@ -260,6 +264,9 @@ void DomainBaker::handleFinishedBaker() { // drop our shared pointer to this baker so that it gets cleaned up _bakers.remove(baker->getFBXUrl()); + // emit progress to tell listeners how many models we have baked + emit bakeProgress(_totalNumberOfEntities - _entitiesNeedingRewrite.keys().size(), _totalNumberOfEntities); + if (_entitiesNeedingRewrite.isEmpty()) { emit allModelsFinished(); } diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 6c8555cc62..23c332abab 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -31,6 +31,7 @@ public: signals: void allModelsFinished(); + void bakeProgress(int modelsBaked, int modelsTotal); private slots: void handleFinishedBaker(); @@ -54,6 +55,8 @@ private: std::unique_ptr _fbxBakerThread; QHash> _bakers; QMultiHash _entitiesNeedingRewrite; + + int _totalNumberOfEntities; }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index c0b9c85910..e9b59f005a 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -213,6 +213,9 @@ void DomainBakeWidget::bakeButtonClicked() { // 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); + // run the baker in our thread pool QtConcurrent::run(domainBaker.get(), &DomainBaker::bake); @@ -226,6 +229,26 @@ void DomainBakeWidget::bakeButtonClicked() { } } +void DomainBakeWidget::handleBakerProgress(int modelsBaked, int modelsTotal) { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::remove_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(); + + int percentage = roundf(float(modelsBaked) / float(modelsTotal) * 100.0f); + qDebug() << percentage; + + auto statusString = QString("Baking - %1 of %2 models baked - %3%").arg(modelsBaked).arg(modelsTotal).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 diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index 606f550203..ea9e6f7049 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -34,6 +34,7 @@ private slots: void outputDirectoryChanged(const QString& newDirectory); + void handleBakerProgress(int modelsBaked, int modelsTotal); void handleFinishedBaker(); private: From 1798a058da58f11c1e210b49054c7170fe8a8c8a Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 09:40:10 -0700 Subject: [PATCH 038/146] fix filename grabbing for windows absolute path --- libraries/model-baking/src/FBXBaker.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 0a25d3c299..ddf68b74c7 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -223,7 +223,7 @@ QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { 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()]; + auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; QString bakedTextureFileName { textureFileInfo.baseName() }; @@ -347,7 +347,8 @@ void FBXBaker::rewriteAndBakeSceneTextures() { FbxFileTexture* fileTexture = property.GetSrcObject(j); // use QFileInfo to easily split up the existing texture filename into its components - QFileInfo textureFileInfo { fileTexture->GetFileName() }; + 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() From 3388debe9f9f3515f4086062accac88d058d1c34 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 09:51:59 -0700 Subject: [PATCH 039/146] remove debug for percentage in DomainBakeWidget --- tools/oven/src/ui/DomainBakeWidget.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index e9b59f005a..9f33dbcc29 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -241,7 +241,6 @@ void DomainBakeWidget::handleBakerProgress(int modelsBaked, int modelsTotal) { auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); int percentage = roundf(float(modelsBaked) / float(modelsTotal) * 100.0f); - qDebug() << percentage; auto statusString = QString("Baking - %1 of %2 models baked - %3%").arg(modelsBaked).arg(modelsTotal).arg(percentage); resultsWindow->changeStatusForRow(resultRow, statusString); From 383d82fe1dc43c09f1d654c0645ee7efef5754af Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 10:04:38 -0700 Subject: [PATCH 040/146] fix multi-line display in results window --- tools/oven/src/ui/ResultsWindow.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/oven/src/ui/ResultsWindow.cpp b/tools/oven/src/ui/ResultsWindow.cpp index 99389f363c..36b7e83177 100644 --- a/tools/oven/src/ui/ResultsWindow.cpp +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -76,4 +76,7 @@ void ResultsWindow::changeStatusForRow(int rowIndex, const QString& result) { auto statusItem = new QTableWidgetItem(result); statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable); _resultsTable->setItem(rowIndex, 1, statusItem); + + // resize the row for the new contents + _resultsTable->resizeRowToContents(rowIndex); } From cdd9990fe8c36202663bbc69539f18f71e8a1834 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 13:06:22 -0700 Subject: [PATCH 041/146] use worker threads for Oven, re-write animation URLs --- libraries/model-baking/src/Baker.h | 5 +- libraries/model-baking/src/FBXBaker.cpp | 9 +- libraries/model-baking/src/FBXBaker.h | 18 ++-- libraries/model-baking/src/TextureBaker.cpp | 48 +++++---- libraries/model-baking/src/TextureBaker.h | 15 ++- tools/oven/src/DomainBaker.cpp | 102 +++++++++++--------- tools/oven/src/DomainBaker.h | 12 +-- tools/oven/src/Oven.cpp | 63 ++++++++++++ tools/oven/src/Oven.h | 14 +++ tools/oven/src/ui/DomainBakeWidget.cpp | 25 +++-- tools/oven/src/ui/ModelBakeWidget.cpp | 6 +- 11 files changed, 217 insertions(+), 100 deletions(-) diff --git a/libraries/model-baking/src/Baker.h b/libraries/model-baking/src/Baker.h index ab9c22ac53..8f73819dfe 100644 --- a/libraries/model-baking/src/Baker.h +++ b/libraries/model-baking/src/Baker.h @@ -18,14 +18,15 @@ class Baker : public QObject { Q_OBJECT public: - virtual void bake() = 0; - 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(); diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index ddf68b74c7..5b47f5023b 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -30,9 +30,11 @@ std::once_flag onceFlag; FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr }; -FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, bool copyOriginals) : +FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, + TextureBakerThreadGetter textureThreadGetter, bool copyOriginals) : _fbxURL(fbxURL), _baseOutputPath(baseOutputPath), + _textureThreadGetter(textureThreadGetter), _copyOriginals(copyOriginals) { std::call_once(onceFlag, [](){ @@ -402,8 +404,9 @@ void FBXBaker::bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, // keep a shared pointer to the baking texture _bakingTextures.insert(bakingTexture); - // start baking the texture on our thread pool - QtConcurrent::run(bakingTexture.data(), &TextureBaker::bake); + // start baking the texture on one of our available worker threads + bakingTexture->moveToThread(_textureThreadGetter()); + QMetaObject::invokeMethod(bakingTexture.data(), "bake"); } void FBXBaker::handleBakedTexture() { diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index cca4878308..8ad42d6de8 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -32,21 +32,23 @@ namespace fbxsdk { 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, bool copyOriginals = true); - - // all calls to bake must be from the same thread, because the Autodesk SDK will cause - // a crash if it is called from multiple threads - Q_INVOKABLE virtual void bake() override; + FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, + TextureBakerThreadGetter textureThreadGetter, bool copyOriginals = true); QUrl getFBXUrl() const { return _fbxURL; } QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } -signals: - void allTexturesBaked(); +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: @@ -92,6 +94,8 @@ private: QSet> _bakingTextures; QFutureSynchronizer _textureBakeSynchronizer; + TextureBakerThreadGetter _textureThreadGetter; + bool _copyOriginals { true }; bool _pendingErrorEmission { false }; diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index 82f42a5776..0ebde00bde 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -33,24 +33,11 @@ TextureBaker::TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, } 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(); - - if (hasErrors()) { - return; - } - - qCDebug(model_baking) << "Baking texture at" << _textureURL; - - processTexture(); - - if (hasErrors()) { - return; - } - - qCDebug(model_baking) << "Baked texture at" << _textureURL; - - emit finished(); } void TextureBaker::loadTexture() { @@ -65,6 +52,8 @@ void TextureBaker::loadTexture() { } _originalTexture = localTexture.readAll(); + + emit originalTextureLoaded(); } else { // remote file, kick off a download auto& networkAccessManager = NetworkAccessManager::getInstance(); @@ -79,23 +68,22 @@ void TextureBaker::loadTexture() { qCDebug(model_baking) << "Downloading" << _textureURL; + // kickoff the download, wait for slot to tell us it is done auto networkReply = networkAccessManager.get(networkRequest); - - // use an event loop to process events while we wait for the network reply - QEventLoop eventLoop; - connect(networkReply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); - eventLoop.exec(); - - handleTextureNetworkReply(networkReply); + connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply); } } -void TextureBaker::handleTextureNetworkReply(QNetworkReply* requestReply) { +void TextureBaker::handleTextureNetworkReply() { + auto requestReply = qobject_cast(sender()); + if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded texture at" << _textureURL; + 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()); @@ -111,6 +99,13 @@ void TextureBaker::processTexture() { 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 + QCryptographicHash hasher(QCryptographicHash::Md5); + hasher.addData(_originalTexture); + std::string hash = hasher.result().toHex().toStdString(); + processedTexture->setSourceHash(hash); + auto memKTX = gpu::Texture::serialize(*processedTexture); if (!memKTX) { @@ -127,4 +122,7 @@ void TextureBaker::processTexture() { 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/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h index 17c725b57d..c0cb8a377a 100644 --- a/libraries/model-baking/src/TextureBaker.h +++ b/libraries/model-baking/src/TextureBaker.h @@ -27,18 +27,23 @@ class TextureBaker : public Baker { public: TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath); - - void bake(); const QByteArray& getOriginalTexture() const { return _originalTexture; } const QUrl& getTextureURL() const { return _textureURL; } +public slots: + virtual void bake() override; + +signals: + void originalTextureLoaded(); + +private slots: + void processTexture(); + private: void loadTexture(); - void handleTextureNetworkReply(QNetworkReply* requestReply); - - void processTexture(); + void handleTextureNetworkReply(); QUrl _textureURL; QByteArray _originalTexture; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 5589df1645..a8fd464790 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -18,6 +18,8 @@ #include "Gzip.h" +#include "Oven.h" + #include "DomainBaker.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, @@ -47,40 +49,14 @@ void DomainBaker::bake() { return; } - setupBakerThread(); - - if (hasErrors()) { - return; - } - enumerateEntities(); if (hasErrors()) { return; } - if (!_entitiesNeedingRewrite.isEmpty()) { - // use a QEventLoop to wait for all entity rewrites to be completed before writing the final models file - QEventLoop eventLoop; - connect(this, &DomainBaker::allModelsFinished, &eventLoop, &QEventLoop::quit); - eventLoop.exec(); - } - - if (hasErrors()) { - return; - } - - writeNewEntitiesFile(); - - if (hasErrors()) { - return; - } - - // stop the FBX baker thread now that all our bakes have completed - _fbxBakerThread->quit(); - - // we've now written out our new models file - time to say that we are finished up - emit finished(); + // in case we've baked and re-written all of our entities already, check if we're done + checkIfRewritingComplete(); } void DomainBaker::setupOutputFolder() { @@ -158,15 +134,6 @@ void DomainBaker::loadLocalFile() { } } -void DomainBaker::setupBakerThread() { - // 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 on the same thread. - // We'll setup that thread now and then move the FBXBaker objects to the thread later when enumerating entities. - _fbxBakerThread = std::unique_ptr(new QThread); - _fbxBakerThread->setObjectName("Domain FBX Baker Thread"); - _fbxBakerThread->start(); -} - static const QString ENTITY_MODEL_URL_KEY = "modelURL"; void DomainBaker::enumerateEntities() { @@ -190,12 +157,15 @@ void DomainBaker::enumerateEntities() { if (BAKEABLE_MODEL_EXTENSIONS.contains(completeLowerExtension)) { // grab a clean version of the URL without a query or fragment - modelURL.setFragment(QString()); - modelURL.setQuery(QString()); + modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); // setup an FBXBaker for this URL, as long as we don't already have one if (!_bakers.contains(modelURL)) { - QSharedPointer baker { new FBXBaker(modelURL, _contentOutputPath), &FBXBaker::deleteLater }; + 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::handleFinishedBaker); @@ -205,7 +175,7 @@ void DomainBaker::enumerateEntities() { // move the baker to the baker thread // and kickoff the bake - baker->moveToThread(_fbxBakerThread.get()); + baker->moveToThread(qApp->getFBXBakerThread()); QMetaObject::invokeMethod(baker.data(), "bake"); } @@ -228,6 +198,7 @@ void DomainBaker::handleFinishedBaker() { 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 @@ -242,12 +213,42 @@ void DomainBaker::handleFinishedBaker() { // setup a new URL using the prefix we were passed QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); - // copy the fragment and query from the old model URL + // 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) + && entity[ENTITY_ANIMATION_KEY].toObject().contains(ENTITIY_ANIMATION_URL_KEY)) { + auto animationValue = entity[ENTITY_ANIMATION_KEY]; + auto animationObject = animationValue.toObject(); + + // 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::RemoveUserInfo | 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 referenced by the QJsonValueRef + animationValue = animationObject; + } + } // replace our temp object with the value referenced by our QJsonValueRef entityValue = entity; @@ -267,9 +268,21 @@ void DomainBaker::handleFinishedBaker() { // emit progress to tell listeners how many models we have baked emit bakeProgress(_totalNumberOfEntities - _entitiesNeedingRewrite.keys().size(), _totalNumberOfEntities); - if (_entitiesNeedingRewrite.isEmpty()) { - emit allModelsFinished(); + // check if this was the last model we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + +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(); } } @@ -308,6 +321,5 @@ void DomainBaker::writeNewEntitiesFile() { } qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; - qDebug() << "WARNINGS:" << _warningList; } diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 23c332abab..ddcb3cd006 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -23,24 +23,25 @@ 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); -public: - virtual void bake() override; - signals: void allModelsFinished(); void bakeProgress(int modelsBaked, int modelsTotal); private slots: + virtual void bake() override; void handleFinishedBaker(); private: void setupOutputFolder(); void loadLocalFile(); - void setupBakerThread(); void enumerateEntities(); + void checkIfRewritingComplete(); void writeNewEntitiesFile(); QUrl _localEntitiesFileURL; @@ -52,11 +53,10 @@ private: QJsonArray _entities; - std::unique_ptr _fbxBakerThread; QHash> _bakers; QMultiHash _entitiesNeedingRewrite; - int _totalNumberOfEntities; + int _totalNumberOfEntities { 0 }; }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 60754759f4..ac8ef505ba 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -10,6 +10,7 @@ // #include +#include #include @@ -33,5 +34,67 @@ Oven::Oven(int argc, char* argv[]) : // 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 index 3fc9a4c0f6..bf7f478b83 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -14,6 +14,8 @@ #include +#include + #if defined(qApp) #undef qApp #endif @@ -26,11 +28,23 @@ class Oven : public QApplication { 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; }; diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 9f33dbcc29..cd2d9f8e3c 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -216,8 +216,12 @@ void DomainBakeWidget::bakeButtonClicked() { // watch the baker's progress so that we can put its progress in the results table connect(domainBaker.get(), &DomainBaker::bakeProgress, this, &DomainBakeWidget::handleBakerProgress); - // run the baker in our thread pool - QtConcurrent::run(domainBaker.get(), &DomainBaker::bake); + // 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(); @@ -232,7 +236,7 @@ void DomainBakeWidget::bakeButtonClicked() { void DomainBakeWidget::handleBakerProgress(int modelsBaked, int modelsTotal) { if (auto baker = qobject_cast(sender())) { // add the results of this bake to the results window - auto it = std::remove_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { return value.first.get() == baker; }); @@ -251,7 +255,7 @@ void DomainBakeWidget::handleBakerProgress(int modelsBaked, int modelsTotal) { void DomainBakeWidget::handleFinishedBaker() { if (auto baker = qobject_cast(sender())) { // add the results of this bake to the results window - auto it = std::remove_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { return value.first.get() == baker; }); @@ -260,12 +264,21 @@ void DomainBakeWidget::handleFinishedBaker() { auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); if (baker->hasErrors()) { - resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + auto errors = baker->getErrors(); + errors.removeDuplicates(); + + resultsWindow->changeStatusForRow(resultRow, errors.join("\n")); } else if (baker->hasWarnings()) { - resultsWindow->changeStatusForRow(resultRow, baker->getWarnings().join("\n")); + 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/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 08b5402821..2841262fee 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -183,7 +183,11 @@ void ModelBakeWidget::bakeButtonClicked() { } // everything seems to be in place, kick off a bake for this model now - auto baker = std::unique_ptr { new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), false) }; + auto baker = std::unique_ptr { + new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), []() -> QThread* { + return qApp->getNextWorkerThread(); + }, false) + }; // move the baker to the baker thread baker->moveToThread(_bakerThread); From cbd6f6417c21c05f13437bc2b1a7b98aca145fa4 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 13:29:44 -0700 Subject: [PATCH 042/146] allow clicking on results row to show dir --- tools/oven/src/ui/DomainBakeWidget.cpp | 2 +- tools/oven/src/ui/ModelBakeWidget.cpp | 2 +- tools/oven/src/ui/ResultsWindow.cpp | 48 ++++++++++++++++++++++++-- tools/oven/src/ui/ResultsWindow.h | 7 +++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index cd2d9f8e3c..34ae0680af 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -226,7 +226,7 @@ void DomainBakeWidget::bakeButtonClicked() { // 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); + 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); diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 2841262fee..f5204020da 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -205,7 +205,7 @@ void ModelBakeWidget::bakeButtonClicked() { // 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()); + 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 diff --git a/tools/oven/src/ui/ResultsWindow.cpp b/tools/oven/src/ui/ResultsWindow.cpp index 36b7e83177..387e3698b8 100644 --- a/tools/oven/src/ui/ResultsWindow.cpp +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include #include #include #include @@ -50,12 +50,53 @@ void ResultsWindow::setupUI() { _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 revealDirectory(const QDir& dirToReveal) { -int ResultsWindow::addPendingResultRow(const QString& fileName) { + // See http://stackoverflow.com/questions/3490336/how-to-reveal-in-finder-or-show-in-explorer-with-qt + // for details + + // Mac, Windows support folder or file. +#if defined(Q_OS_WIN) + const QString explorer = Environment::systemEnvironment().searchInPath(QLatin1String("explorer.exe")); + if (explorer.isEmpty()) { + QMessageBox::warning(parent, + tr("Launching Windows Explorer failed"), + tr("Could not find explorer.exe in path to launch Windows Explorer.")); + return; + } + + QString param = QLatin1String("/select,") + QDir::toNativeSeparators(dirToReveal.absolutePath()); + + QString command = explorer + " " + param; + QProcess::startDetached(command); + +#elif defined(Q_OS_MAC) + QStringList scriptArgs; + scriptArgs << QLatin1String("-e") + << QString::fromLatin1("tell application \"Finder\" to reveal POSIX file \"%1\"").arg(dirToReveal.absolutePath()); + QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs); + + scriptArgs.clear(); + scriptArgs << QLatin1String("-e") << QLatin1String("tell application \"Finder\" to activate"); + QProcess::execute("/usr/bin/osascript", scriptArgs); +#endif + +} + +void ResultsWindow::handleCellClicked(int rowIndex, int columnIndex) { + // use revealDirectory to show the output directory for this row + revealDirectory(_outputDirectories[rowIndex]); +} + + +int ResultsWindow::addPendingResultRow(const QString& fileName, const QDir& outputDirectory) { int rowIndex = _resultsTable->rowCount(); _resultsTable->insertRow(rowIndex); @@ -69,6 +110,9 @@ int ResultsWindow::addPendingResultRow(const QString& fileName) { 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; } diff --git a/tools/oven/src/ui/ResultsWindow.h b/tools/oven/src/ui/ResultsWindow.h index b7e380a631..ae7bb0e327 100644 --- a/tools/oven/src/ui/ResultsWindow.h +++ b/tools/oven/src/ui/ResultsWindow.h @@ -12,6 +12,7 @@ #ifndef hifi_ResultsWindow_h #define hifi_ResultsWindow_h +#include #include class QTableWidget; @@ -24,11 +25,15 @@ public: void setupUI(); - int addPendingResultRow(const QString& fileName); + 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 From 7bc69e6eda61c0a8ea57b123e0fc2d72a0a73b1d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 14:11:46 -0700 Subject: [PATCH 043/146] add skybox baking to DomainBaker --- libraries/model-baking/src/TextureBaker.h | 2 + tools/oven/src/DomainBaker.cpp | 165 ++++++++++++++++++---- tools/oven/src/DomainBaker.h | 13 +- tools/oven/src/ui/DomainBakeWidget.cpp | 6 +- tools/oven/src/ui/DomainBakeWidget.h | 2 +- 5 files changed, 152 insertions(+), 36 deletions(-) diff --git a/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h index c0cb8a377a..65623c96c4 100644 --- a/libraries/model-baking/src/TextureBaker.h +++ b/libraries/model-baking/src/TextureBaker.h @@ -32,6 +32,8 @@ public: const QUrl& getTextureURL() const { return _textureURL; } + const QString& getDestinationFilePath() const { return _destinationFilePath; } + public slots: virtual void bake() override; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index a8fd464790..f869e1cded 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -134,7 +134,9 @@ void DomainBaker::loadLocalFile() { } } -static const QString ENTITY_MODEL_URL_KEY = "modelURL"; +const QString ENTITY_MODEL_URL_KEY = "modelURL"; +const QString ENTITY_SKYBOX_KEY = "skybox"; +const QString ENTITY_SKYBOX_URL_KEY = "url"; void DomainBaker::enumerateEntities() { qDebug() << "Enumerating" << _entities.size() << "entities from domain"; @@ -144,7 +146,7 @@ void DomainBaker::enumerateEntities() { if (it->isObject()) { auto entity = it->toObject(); - // check if this is an entity with a model URL + // 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() }; @@ -160,7 +162,7 @@ void DomainBaker::enumerateEntities() { modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); // setup an FBXBaker for this URL, as long as we don't already have one - if (!_bakers.contains(modelURL)) { + if (!_modelBakers.contains(modelURL)) { QSharedPointer baker { new FBXBaker(modelURL, _contentOutputPath, []() -> QThread* { return qApp->getNextWorkerThread(); @@ -168,31 +170,76 @@ void DomainBaker::enumerateEntities() { }; // make sure our handler is called when the baker is done - connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedBaker); + connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); // insert it into our bakers hash so we hold a strong pointer to it - _bakers.insert(modelURL, baker); + _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); - ++_totalNumberOfEntities; + } + } else if (entity.contains(ENTITY_SKYBOX_KEY) + && entity[ENTITY_SKYBOX_KEY].toObject().contains(ENTITY_SKYBOX_URL_KEY)) { + // we have a URL to a skybox, grab it + QUrl skyboxURL { entity[ENTITY_SKYBOX_KEY].toObject()[ENTITY_SKYBOX_URL_KEY].toString() }; + + auto skyboxFileName = skyboxURL.fileName(); + + static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { ".jpg" }; + 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)) { + // figure out the path for this baked skybox + auto skyboxFileName = skyboxURL.fileName(); + auto bakedSkyboxFileName = skyboxFileName.left(skyboxFileName.indexOf('.')) + BAKED_TEXTURE_EXT; + auto bakedTextureDestination = QDir(_contentOutputPath).absoluteFilePath(bakedSkyboxFileName); + + QSharedPointer skyboxBaker { + new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, bakedTextureDestination) + }; + + // 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, *it); } } } } // emit progress now to say we're just starting - emit bakeProgress(0, _totalNumberOfEntities); + emit bakeProgress(0, _totalNumberOfSubBakes); } -void DomainBaker::handleFinishedBaker() { +void DomainBaker::handleFinishedModelBaker() { auto baker = qobject_cast(sender()); if (baker) { @@ -226,27 +273,27 @@ void DomainBaker::handleFinishedBaker() { const QString ENTITY_ANIMATION_KEY = "animation"; const QString ENTITIY_ANIMATION_URL_KEY = "url"; - if (entity.contains(ENTITY_ANIMATION_KEY) - && entity[ENTITY_ANIMATION_KEY].toObject().contains(ENTITIY_ANIMATION_URL_KEY)) { - auto animationValue = entity[ENTITY_ANIMATION_KEY]; - auto animationObject = animationValue.toObject(); + if (entity.contains(ENTITY_ANIMATION_KEY)) { + auto animationObject = entity[ENTITY_ANIMATION_KEY].toObject(); - // grab the old animation URL - QUrl oldAnimationURL { animationObject[ENTITIY_ANIMATION_URL_KEY].toString() }; + 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::RemoveUserInfo | 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()); + // check if its stripped down version matches our stripped down model URL + if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveUserInfo | 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(); + animationObject[ENTITIY_ANIMATION_URL_KEY] = newAnimationURL.toString(); - // replace the animation object referenced by the QJsonValueRef - animationValue = animationObject; + // replace the animation object in the entity object + entity[ENTITY_ANIMATION_KEY] = animationObject; + } } } @@ -255,7 +302,7 @@ void DomainBaker::handleFinishedBaker() { } } 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 errors + // the errors from the model to our warnings _warningList << baker->getErrors(); } @@ -263,16 +310,78 @@ void DomainBaker::handleFinishedBaker() { _entitiesNeedingRewrite.remove(baker->getFBXUrl()); // drop our shared pointer to this baker so that it gets cleaned up - _bakers.remove(baker->getFBXUrl()); + _modelBakers.remove(baker->getFBXUrl()); // emit progress to tell listeners how many models we have baked - emit bakeProgress(_totalNumberOfEntities - _entitiesNeedingRewrite.keys().size(), _totalNumberOfEntities); + 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)) { + // grab the old skybox URL + QUrl oldSkyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() }; + + // 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 bakedSkyboxFileName = QFileInfo(baker->getDestinationFilePath()).fileName(); + + auto newSkyboxURL = _destinationPath.resolved(bakedSkyboxFileName); + newSkyboxURL.setQuery(oldSkyboxURL.query()); + newSkyboxURL.setFragment(oldSkyboxURL.fragment()); + newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); + + skyboxObject[ENTITY_SKYBOX_URL_KEY] = newSkyboxURL.toString(); + + // replace the skybox object referenced by the entity object + entity[ENTITY_SKYBOX_KEY] = skyboxObject; + } + } + + // 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(); + } + +} + void DomainBaker::checkIfRewritingComplete() { if (_entitiesNeedingRewrite.isEmpty()) { writeNewEntitiesFile(); diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index ddcb3cd006..1a100d2184 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -19,6 +19,7 @@ #include #include +#include class DomainBaker : public Baker { Q_OBJECT @@ -31,11 +32,12 @@ public: signals: void allModelsFinished(); - void bakeProgress(int modelsBaked, int modelsTotal); + void bakeProgress(int baked, int total); private slots: virtual void bake() override; - void handleFinishedBaker(); + void handleFinishedModelBaker(); + void handleFinishedSkyboxBaker(); private: void setupOutputFolder(); @@ -53,10 +55,13 @@ private: QJsonArray _entities; - QHash> _bakers; + QHash> _modelBakers; + QHash> _skyboxBakers; + QMultiHash _entitiesNeedingRewrite; - int _totalNumberOfEntities { 0 }; + int _totalNumberOfSubBakes { 0 }; + int _completedSubBakes { 0 }; }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 34ae0680af..7a8c5fc6a2 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -233,7 +233,7 @@ void DomainBakeWidget::bakeButtonClicked() { } } -void DomainBakeWidget::handleBakerProgress(int modelsBaked, int modelsTotal) { +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) { @@ -244,9 +244,9 @@ void DomainBakeWidget::handleBakerProgress(int modelsBaked, int modelsTotal) { auto resultRow = it->second; auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); - int percentage = roundf(float(modelsBaked) / float(modelsTotal) * 100.0f); + int percentage = roundf(float(baked) / float(total) * 100.0f); - auto statusString = QString("Baking - %1 of %2 models baked - %3%").arg(modelsBaked).arg(modelsTotal).arg(percentage); + auto statusString = QString("Baking - %1 of %2 - %3%").arg(baked).arg(total).arg(percentage); resultsWindow->changeStatusForRow(resultRow, statusString); } } diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index ea9e6f7049..16b0c76c11 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -34,7 +34,7 @@ private slots: void outputDirectoryChanged(const QString& newDirectory); - void handleBakerProgress(int modelsBaked, int modelsTotal); + void handleBakerProgress(int baked, int total); void handleFinishedBaker(); private: From 25d24c445db9373323f37ecedb8992fc0876709f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 14:57:11 -0700 Subject: [PATCH 044/146] handle ambient skybox textures for zones in domain bake --- tools/oven/src/DomainBaker.cpp | 152 +++++++++++++++++++++------------ tools/oven/src/DomainBaker.h | 3 + 2 files changed, 100 insertions(+), 55 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index f869e1cded..01dbaca3e6 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -137,6 +137,8 @@ void DomainBaker::loadLocalFile() { 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"; @@ -188,48 +190,28 @@ void DomainBaker::enumerateEntities() { // the model URL to the baked version once the baker is complete _entitiesNeedingRewrite.insert(modelURL, *it); } - } else if (entity.contains(ENTITY_SKYBOX_KEY) - && entity[ENTITY_SKYBOX_KEY].toObject().contains(ENTITY_SKYBOX_URL_KEY)) { - // we have a URL to a skybox, grab it - QUrl skyboxURL { entity[ENTITY_SKYBOX_KEY].toObject()[ENTITY_SKYBOX_URL_KEY].toString() }; + } 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() }; - auto skyboxFileName = skyboxURL.fileName(); - - static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { ".jpg" }; - 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)) { - // figure out the path for this baked skybox - auto skyboxFileName = skyboxURL.fileName(); - auto bakedSkyboxFileName = skyboxFileName.left(skyboxFileName.indexOf('.')) + BAKED_TEXTURE_EXT; - auto bakedTextureDestination = QDir(_contentOutputPath).absoluteFilePath(bakedSkyboxFileName); - - QSharedPointer skyboxBaker { - new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, bakedTextureDestination) - }; - - // 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; + // setup a bake of the skybox + bakeSkybox(skyboxURL, *it); } + } - // 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, *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); + } } } } @@ -239,6 +221,48 @@ void DomainBaker::enumerateEntities() { emit bakeProgress(0, _totalNumberOfSubBakes); } +void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { + + auto skyboxFileName = skyboxURL.fileName(); + + static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { ".jpg" }; + 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)) { + // figure out the path for this baked skybox + auto skyboxFileName = skyboxURL.fileName(); + auto bakedSkyboxFileName = skyboxFileName.left(skyboxFileName.indexOf('.')) + BAKED_TEXTURE_EXT; + auto bakedTextureDestination = QDir(_contentOutputPath).absoluteFilePath(bakedSkyboxFileName); + + QSharedPointer skyboxBaker { + new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, bakedTextureDestination) + }; + + // 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()); @@ -281,7 +305,7 @@ void DomainBaker::handleFinishedModelBaker() { 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::RemoveUserInfo | QUrl::RemoveQuery | QUrl::RemoveFragment)) { + 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()); @@ -338,23 +362,21 @@ void DomainBaker::handleFinishedSkyboxBaker() { auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { - // grab the old skybox URL - QUrl oldSkyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() }; + 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; + } + } + } - // the animation URL matched the old model URL, so make the animation URL point to the baked FBX - // with its original query and fragment + if (entity.contains(ENTITY_KEYLIGHT_KEY)) { + auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); - auto bakedSkyboxFileName = QFileInfo(baker->getDestinationFilePath()).fileName(); - - auto newSkyboxURL = _destinationPath.resolved(bakedSkyboxFileName); - newSkyboxURL.setQuery(oldSkyboxURL.query()); - newSkyboxURL.setFragment(oldSkyboxURL.fragment()); - newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); - - skyboxObject[ENTITY_SKYBOX_URL_KEY] = newSkyboxURL.toString(); - - // replace the skybox object referenced by the entity object - entity[ENTITY_SKYBOX_KEY] = skyboxObject; + 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; + } } } @@ -379,7 +401,27 @@ void DomainBaker::handleFinishedSkyboxBaker() { // 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 bakedSkyboxFileName = QFileInfo(baker->getDestinationFilePath()).fileName(); + + auto newSkyboxURL = _destinationPath.resolved(bakedSkyboxFileName); + newSkyboxURL.setQuery(oldSkyboxURL.query()); + newSkyboxURL.setFragment(oldSkyboxURL.fragment()); + newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); + + urlValue = newSkyboxURL.toString(); + + return true; + } else { + return false; + } } void DomainBaker::checkIfRewritingComplete() { diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 1a100d2184..54cbb18b06 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -46,6 +46,9 @@ private: void checkIfRewritingComplete(); void writeNewEntitiesFile(); + void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity); + bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker); + QUrl _localEntitiesFileURL; QString _domainName; QString _baseOutputPath; From 980de595a9b62eb92bbc026de46055cda1ed064c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 16:02:49 -0700 Subject: [PATCH 045/146] handle skybox baking from oven menu --- libraries/model-baking/src/FBXBaker.cpp | 6 +- libraries/model-baking/src/FBXBaker.h | 2 +- libraries/model-baking/src/TextureBaker.cpp | 10 +- libraries/model-baking/src/TextureBaker.h | 10 +- tools/oven/src/DomainBaker.cpp | 14 +- tools/oven/src/ui/ModelBakeWidget.cpp | 26 +-- tools/oven/src/ui/ModelBakeWidget.h | 3 - tools/oven/src/ui/ModesWidget.cpp | 29 ++- tools/oven/src/ui/ModesWidget.h | 1 + tools/oven/src/ui/SkyboxBakeWidget.cpp | 232 ++++++++++++++++++++ tools/oven/src/ui/SkyboxBakeWidget.h | 53 +++++ 11 files changed, 335 insertions(+), 51 deletions(-) create mode 100644 tools/oven/src/ui/SkyboxBakeWidget.cpp create mode 100644 tools/oven/src/ui/SkyboxBakeWidget.h diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index 5b47f5023b..fe48cc8372 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -378,7 +378,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { _unbakedTextures.insert(urlToTexture, bakedTextureFileName); // bake this texture asynchronously - bakeTexture(urlToTexture, textureType, bakedTextureFilePath); + bakeTexture(urlToTexture, textureType, _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER); } } } @@ -391,10 +391,10 @@ void FBXBaker::rewriteAndBakeSceneTextures() { } } -void FBXBaker::bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath) { +void FBXBaker::bakeTexture(const QUrl& textureURL, gpu::TextureType 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, destinationFilePath), + new TextureBaker(textureURL, textureType, outputDir), &TextureBaker::deleteLater }; diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 8ad42d6de8..903720a0a9 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -74,7 +74,7 @@ private: QString createBakedTextureFileName(const QFileInfo& textureFileInfo); QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); - void bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath); + void bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, const QDir& outputDir); QString pathToCopyOfOriginal() const; diff --git a/libraries/model-baking/src/TextureBaker.cpp b/libraries/model-baking/src/TextureBaker.cpp index 0ebde00bde..f0136fb454 100644 --- a/libraries/model-baking/src/TextureBaker.cpp +++ b/libraries/model-baking/src/TextureBaker.cpp @@ -24,12 +24,14 @@ const QString BAKED_TEXTURE_EXT = ".ktx"; -TextureBaker::TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath) : +TextureBaker::TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QDir& outputDirectory) : _textureURL(textureURL), _textureType(textureType), - _destinationFilePath(destinationFilePath) + _outputDirectory(outputDirectory) { - + // figure out the baked texture filename + auto originalFilename = textureURL.fileName(); + _bakedTextureFileName = originalFilename.left(originalFilename.indexOf('.')) + BAKED_TEXTURE_EXT; } void TextureBaker::bake() { @@ -117,7 +119,7 @@ void TextureBaker::processTexture() { const size_t length = memKTX->_storage->size(); // attempt to write the baked texture to the destination file path - QFile bakedTextureFile { _destinationFilePath }; + QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) }; if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { handleError("Could not write baked texture for " + _textureURL.toString()); diff --git a/libraries/model-baking/src/TextureBaker.h b/libraries/model-baking/src/TextureBaker.h index 65623c96c4..7a6d1d404b 100644 --- a/libraries/model-baking/src/TextureBaker.h +++ b/libraries/model-baking/src/TextureBaker.h @@ -26,13 +26,14 @@ class TextureBaker : public Baker { Q_OBJECT public: - TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QString& destinationFilePath); + TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QDir& outputDirectory); const QByteArray& getOriginalTexture() const { return _originalTexture; } - const QUrl& getTextureURL() const { return _textureURL; } + QUrl getTextureURL() const { return _textureURL; } - const QString& getDestinationFilePath() const { return _destinationFilePath; } + QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); } + QString getBakedTextureFileName() const { return _bakedTextureFileName; } public slots: virtual void bake() override; @@ -51,7 +52,8 @@ private: QByteArray _originalTexture; gpu::TextureType _textureType; - QString _destinationFilePath; + QDir _outputDirectory; + QString _bakedTextureFileName; }; #endif // hifi_TextureBaker_h diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 01dbaca3e6..eea73bb1b6 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -225,7 +225,9 @@ void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { auto skyboxFileName = skyboxURL.fileName(); - static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { ".jpg" }; + 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)) { @@ -234,13 +236,10 @@ void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { // setup a texture baker for this URL, as long as we aren't baking a skybox already if (!_skyboxBakers.contains(skyboxURL)) { - // figure out the path for this baked skybox - auto skyboxFileName = skyboxURL.fileName(); - auto bakedSkyboxFileName = skyboxFileName.left(skyboxFileName.indexOf('.')) + BAKED_TEXTURE_EXT; - auto bakedTextureDestination = QDir(_contentOutputPath).absoluteFilePath(bakedSkyboxFileName); + // setup a baker for this skybox QSharedPointer skyboxBaker { - new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, bakedTextureDestination) + new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, _contentOutputPath) }; // make sure our handler is called when the skybox baker is done @@ -409,9 +408,8 @@ bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) 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 bakedSkyboxFileName = QFileInfo(baker->getDestinationFilePath()).fileName(); - auto newSkyboxURL = _destinationPath.resolved(bakedSkyboxFileName); + auto newSkyboxURL = _destinationPath.resolved(baker->getBakedTextureFileName()); newSkyboxURL.setQuery(oldSkyboxURL.query()); newSkyboxURL.setFragment(oldSkyboxURL.fragment()); newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index f5204020da..f87210dbee 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -25,24 +25,17 @@ #include "ModelBakeWidget.h" -static const QString EXPORT_DIR_SETTING_KEY = "model_export_directory"; -static const QString MODEL_START_DIR_SETTING_KEY = "model_search_directory"; +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) : QWidget(parent, flags), _exportDirectory(EXPORT_DIR_SETTING_KEY), - _modelStartDirectory(MODEL_START_DIR_SETTING_KEY), - _bakerThread(new QThread(this)) + _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) { setupUI(); } -ModelBakeWidget::~ModelBakeWidget() { - // before we go down, stop the baker thread and make sure it's done - _bakerThread->quit(); - _bakerThread->wait(); -} - void ModelBakeWidget::setupUI() { // setup a grid layout to hold everything QGridLayout* gridLayout = new QGridLayout; @@ -189,13 +182,8 @@ void ModelBakeWidget::bakeButtonClicked() { }, false) }; - // move the baker to the baker thread - baker->moveToThread(_bakerThread); - - // make sure we start the baker thread if it isn't already running - if (!_bakerThread->isRunning()) { - _bakerThread->start(); - } + // 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"); @@ -216,7 +204,7 @@ void ModelBakeWidget::bakeButtonClicked() { void ModelBakeWidget::handleFinishedBaker() { if (auto baker = qobject_cast(sender())) { // add the results of this bake to the results window - auto it = std::remove_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { return value.first.get() == baker; }); @@ -229,6 +217,8 @@ void ModelBakeWidget::handleFinishedBaker() { } else { resultsWindow->changeStatusForRow(resultRow, "Success"); } + + _bakers.erase(it); } } } diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 9b7a2fed20..9a9394c386 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -26,7 +26,6 @@ class ModelBakeWidget : public QWidget { public: ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); - ~ModelBakeWidget(); private slots: void chooseFileButtonClicked(); @@ -50,8 +49,6 @@ private: Setting::Handle _exportDirectory; Setting::Handle _modelStartDirectory; - - QThread* _bakerThread; }; #endif // hifi_ModelBakeWidget_h diff --git a/tools/oven/src/ui/ModesWidget.cpp b/tools/oven/src/ui/ModesWidget.cpp index 867f89b4c4..624aa949cc 100644 --- a/tools/oven/src/ui/ModesWidget.cpp +++ b/tools/oven/src/ui/ModesWidget.cpp @@ -15,6 +15,7 @@ #include "DomainBakeWidget.h" #include "ModelBakeWidget.h" +#include "SkyboxBakeWidget.h" #include "ModesWidget.h" @@ -28,19 +29,20 @@ void ModesWidget::setupUI() { // setup a horizontal box layout to hold our mode buttons QHBoxLayout* horizontalLayout = new QHBoxLayout; - // 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 domain baking QPushButton* domainButton = new QPushButton("Bake Domain"); connect(domainButton, &QPushButton::clicked, this, &ModesWidget::showDomainBakingWidget); horizontalLayout->addWidget(domainButton); - // add a button for texture baking - QPushButton* textureButton = new QPushButton("Bake Textures"); - horizontalLayout->addWidget(textureButton); + // 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); } @@ -48,13 +50,20 @@ void ModesWidget::setupUI() { void ModesWidget::showModelBakingWidget() { auto stackedWidget = qobject_cast(parentWidget()); - // add a new widget for making baking to the stack, and switch to it + // 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 making baking to the stack, and switch to it + // 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 index e7e239d63e..fd660923f2 100644 --- a/tools/oven/src/ui/ModesWidget.h +++ b/tools/oven/src/ui/ModesWidget.h @@ -22,6 +22,7 @@ public: private slots: void showModelBakingWidget(); void showDomainBakingWidget(); + void showSkyboxBakingWidget(); private: void setupUI(); diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp new file mode 100644 index 0000000000..c6bca5819e --- /dev/null +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -0,0 +1,232 @@ +// +// 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) : + QWidget(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 model 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 model line edit to see how many models we need to bake + auto fileURLStrings = _selectionLineEdit->text().split(','); + foreach (QString fileURLString, fileURLStrings) { + // construct a URL from the path in the model file text box + QUrl skyboxToBakeURL(fileURLString); + + // if the URL doesn't have a scheme, assume it is a local file + if (skyboxToBakeURL.scheme().isEmpty()) { + skyboxToBakeURL.setScheme("file"); + } + + // everything seems to be in place, kick off a bake for this model now + auto baker = std::unique_ptr { + new TextureBaker(skyboxToBakeURL, gpu::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); + } + } +} + +void SkyboxBakeWidget::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/SkyboxBakeWidget.h b/tools/oven/src/ui/SkyboxBakeWidget.h new file mode 100644 index 0000000000..3dfd2fa4f8 --- /dev/null +++ b/tools/oven/src/ui/SkyboxBakeWidget.h @@ -0,0 +1,53 @@ +// +// 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 + +class QLineEdit; + +class SkyboxBakeWidget : public QWidget { + Q_OBJECT + +public: + SkyboxBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + void cancelButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + + void handleFinishedBaker(); + +private: + void setupUI(); + + using BakerRowPair = std::pair, int>; + using BakerRowPairList = std::list; + BakerRowPairList _bakers; + + QLineEdit* _selectionLineEdit; + QLineEdit* _outputDirLineEdit; + + Setting::Handle _exportDirectory; + Setting::Handle _selectionStartDirectory; +}; + +#endif // hifi_SkyboxBakeWidget_h From 19aa05281e8760982ce1a9c97b7b42cb2d83749f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 16:32:14 -0700 Subject: [PATCH 046/146] fix build errors for oven on windows --- libraries/model-baking/src/FBXBaker.cpp | 2 ++ tools/oven/CMakeLists.txt | 4 ++++ tools/oven/src/Oven.h | 2 ++ 3 files changed, 8 insertions(+) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index fe48cc8372..e7dffea158 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -9,6 +9,8 @@ // 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 diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 1e644a2c62..c9afc7660f 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -3,3 +3,7 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) link_hifi_libraries(model-baking shared image gpu ktx) + +if (WIN32) + package_libraries_for_deployment() +endif () diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h index bf7f478b83..350c615ce0 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -16,6 +16,8 @@ #include +#include + #if defined(qApp) #undef qApp #endif From a072f940858d75816a5a9f0e53743ebc7b191ce8 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 16:44:11 -0700 Subject: [PATCH 047/146] leverage QDesktopServices to show output directory --- tools/oven/src/ui/ResultsWindow.cpp | 45 +++++------------------------ 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/tools/oven/src/ui/ResultsWindow.cpp b/tools/oven/src/ui/ResultsWindow.cpp index 387e3698b8..cfbd07090f 100644 --- a/tools/oven/src/ui/ResultsWindow.cpp +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -9,7 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include +#include #include #include #include @@ -57,45 +58,15 @@ void ResultsWindow::setupUI() { setLayout(resultsLayout); } -void revealDirectory(const QDir& dirToReveal) { - - // See http://stackoverflow.com/questions/3490336/how-to-reveal-in-finder-or-show-in-explorer-with-qt - // for details - - // Mac, Windows support folder or file. -#if defined(Q_OS_WIN) - const QString explorer = Environment::systemEnvironment().searchInPath(QLatin1String("explorer.exe")); - if (explorer.isEmpty()) { - QMessageBox::warning(parent, - tr("Launching Windows Explorer failed"), - tr("Could not find explorer.exe in path to launch Windows Explorer.")); - return; - } - - QString param = QLatin1String("/select,") + QDir::toNativeSeparators(dirToReveal.absolutePath()); - - QString command = explorer + " " + param; - QProcess::startDetached(command); - -#elif defined(Q_OS_MAC) - QStringList scriptArgs; - scriptArgs << QLatin1String("-e") - << QString::fromLatin1("tell application \"Finder\" to reveal POSIX file \"%1\"").arg(dirToReveal.absolutePath()); - QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs); - - scriptArgs.clear(); - scriptArgs << QLatin1String("-e") << QLatin1String("tell application \"Finder\" to activate"); - QProcess::execute("/usr/bin/osascript", scriptArgs); -#endif - -} - void ResultsWindow::handleCellClicked(int rowIndex, int columnIndex) { - // use revealDirectory to show the output directory for this row - revealDirectory(_outputDirectories[rowIndex]); + // 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(); From 0bb3f1c9dc56aedd32919dc6bafebed544325c9a Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 16:55:31 -0700 Subject: [PATCH 048/146] assume local file if scheme is not remote --- tools/oven/src/ui/ModelBakeWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index f87210dbee..9e2ab842b4 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -171,7 +171,7 @@ void ModelBakeWidget::bakeButtonClicked() { QUrl modelToBakeURL(fileURLString); // if the URL doesn't have a scheme, assume it is a local file - if (modelToBakeURL.scheme().isEmpty()) { + if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") { modelToBakeURL.setScheme("file"); } From 3216202a8b436c4aca9f81e31803ec6f6f3ba6bb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 16:57:14 -0700 Subject: [PATCH 049/146] fix local skybox file reference on windows --- tools/oven/src/ui/SkyboxBakeWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp index c6bca5819e..1a5a53de21 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.cpp +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -171,7 +171,7 @@ void SkyboxBakeWidget::bakeButtonClicked() { QUrl skyboxToBakeURL(fileURLString); // if the URL doesn't have a scheme, assume it is a local file - if (skyboxToBakeURL.scheme().isEmpty()) { + if (skyboxToBakeURL.scheme() != "http" && skyboxToBakeURL.scheme() != "https" && skyboxToBakeURL.scheme() != "ftp") { skyboxToBakeURL.setScheme("file"); } From 9d8e493c2039f2e8332ac09ce6702d2308037153 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 16:58:16 -0700 Subject: [PATCH 050/146] remove suggestion that domain baker can load from URL --- tools/oven/src/ui/DomainBakeWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 7a8c5fc6a2..baea4c80a2 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -67,7 +67,7 @@ void DomainBakeWidget::setupUI() { QLabel* entitiesFileLabel = new QLabel("Entities File"); _entitiesFileLineEdit = new QLineEdit; - _entitiesFileLineEdit->setPlaceholderText("File or URL"); + _entitiesFileLineEdit->setPlaceholderText("File"); QPushButton* chooseFileButton = new QPushButton("Browse..."); connect(chooseFileButton, &QPushButton::clicked, this, &DomainBakeWidget::chooseFileButtonClicked); From 7a5bfb8c1945849ccae3c822306d438940937927 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 17:19:18 -0700 Subject: [PATCH 051/146] add cancellation handling for domain bake widget --- libraries/model-baking/src/FBXBaker.cpp | 18 +++++++----------- libraries/model-baking/src/FBXBaker.h | 5 +---- tools/oven/src/DomainBaker.cpp | 3 ++- tools/oven/src/ui/DomainBakeWidget.cpp | 14 ++++++++++++++ tools/oven/src/ui/DomainBakeWidget.h | 3 ++- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/libraries/model-baking/src/FBXBaker.cpp b/libraries/model-baking/src/FBXBaker.cpp index e7dffea158..bbd490447b 100644 --- a/libraries/model-baking/src/FBXBaker.cpp +++ b/libraries/model-baking/src/FBXBaker.cpp @@ -374,11 +374,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { // figure out the URL to this texture, embedded or external auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); - if (!_unbakedTextures.contains(urlToTexture)) { - // add the deduced url to the texture, associated with the resulting baked texture file name, - // to our hash of textures needing to be baked - _unbakedTextures.insert(urlToTexture, bakedTextureFileName); - + if (!_bakingTextures.contains(urlToTexture)) { // bake this texture asynchronously bakeTexture(urlToTexture, textureType, _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER); } @@ -404,7 +400,7 @@ void FBXBaker::bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); // keep a shared pointer to the baking texture - _bakingTextures.insert(bakingTexture); + _bakingTextures.insert(textureURL, bakingTexture); // start baking the texture on one of our available worker threads bakingTexture->moveToThread(_textureThreadGetter()); @@ -456,8 +452,8 @@ void FBXBaker::handleBakedTexture() { } - // now that this texture has been baked and handled, we can remove that TextureBaker from our list - _unbakedTextures.remove(bakedTexture->getTextureURL()); + // now that this texture has been baked and handled, we can remove that TextureBaker from our hash + _bakingTextures.remove(bakedTexture->getTextureURL()); checkIfTexturesFinished(); } else { @@ -468,7 +464,7 @@ void FBXBaker::handleBakedTexture() { _pendingErrorEmission = true; // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list - _unbakedTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove(bakedTexture->getTextureURL()); checkIfTexturesFinished(); } @@ -476,7 +472,7 @@ void FBXBaker::handleBakedTexture() { // 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 - _unbakedTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove(bakedTexture->getTextureURL()); checkIfTexturesFinished(); } @@ -525,7 +521,7 @@ 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 (_unbakedTextures.isEmpty()) { + if (_bakingTextures.isEmpty()) { // remove the embedded media folder that the FBX SDK produces when reading the original removeEmbeddedMediaFolder(); diff --git a/libraries/model-baking/src/FBXBaker.h b/libraries/model-baking/src/FBXBaker.h index 903720a0a9..5ec6427bfb 100644 --- a/libraries/model-baking/src/FBXBaker.h +++ b/libraries/model-baking/src/FBXBaker.h @@ -88,12 +88,9 @@ private: static FBXSDKManagerUniquePointer _sdkManager; fbxsdk::FbxScene* _scene { nullptr }; - QHash _unbakedTextures; + QMultiHash> _bakingTextures; QHash _textureNameMatchCount; - QSet> _bakingTextures; - QFutureSynchronizer _textureBakeSynchronizer; - TextureBakerThreadGetter _textureThreadGetter; bool _copyOriginals { true }; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index eea73bb1b6..07df9870aa 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -239,7 +239,8 @@ void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { // setup a baker for this skybox QSharedPointer skyboxBaker { - new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, _contentOutputPath) + new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, _contentOutputPath), + &TextureBaker::deleteLater }; // make sure our handler is called when the skybox baker is done diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index baea4c80a2..382e99f14e 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -41,6 +41,20 @@ DomainBakeWidget::DomainBakeWidget(QWidget* parent, Qt::WindowFlags flags) : setupUI(); } +DomainBakeWidget::~DomainBakeWidget() { + // 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 DomainBakeWidget::setupUI() { // setup a grid layout to hold everything QGridLayout* gridLayout = new QGridLayout; diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index 16b0c76c11..b37ed490bc 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -25,7 +25,8 @@ class DomainBakeWidget : public QWidget { public: DomainBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); - + ~DomainBakeWidget(); + private slots: void chooseFileButtonClicked(); void chooseOutputDirButtonClicked(); From 8a1eb5f0771f86e5fa592d40ded86cd874823c35 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 17:23:06 -0700 Subject: [PATCH 052/146] add cancellation handling to model bake widget --- tools/oven/src/ui/ModelBakeWidget.cpp | 14 ++++++++++++++ tools/oven/src/ui/ModelBakeWidget.h | 1 + 2 files changed, 15 insertions(+) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 9e2ab842b4..7b1adf05a7 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -36,6 +36,20 @@ ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) : setupUI(); } +ModelBakeWidget::~ModelBakeWidget() { + // if we're about to go down, whatever bakers we're managing are about to as well + // enumerate them, send the results table a cancelled status, and clean them up + 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 ModelBakeWidget::setupUI() { // setup a grid layout to hold everything QGridLayout* gridLayout = new QGridLayout; diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 9a9394c386..0711417d21 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -26,6 +26,7 @@ class ModelBakeWidget : public QWidget { public: ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + ~ModelBakeWidget(); private slots: void chooseFileButtonClicked(); From 95e2cc4eeaba04fce928e4e7b19889f9a2a8732c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 17:32:23 -0700 Subject: [PATCH 053/146] add BakeWidget, leverage for skybox widget cancellation --- tools/oven/src/ui/BakeWidget.cpp | 46 ++++++++++++++++++++++++++ tools/oven/src/ui/BakeWidget.h | 33 ++++++++++++++++++ tools/oven/src/ui/DomainBakeWidget.cpp | 26 +-------------- tools/oven/src/ui/DomainBakeWidget.h | 9 ++--- tools/oven/src/ui/ModelBakeWidget.cpp | 25 +------------- tools/oven/src/ui/ModelBakeWidget.h | 10 ++---- tools/oven/src/ui/SkyboxBakeWidget.cpp | 11 +----- tools/oven/src/ui/SkyboxBakeWidget.h | 9 ++--- 8 files changed, 90 insertions(+), 79 deletions(-) create mode 100644 tools/oven/src/ui/BakeWidget.cpp create mode 100644 tools/oven/src/ui/BakeWidget.h 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..00996128ed --- /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 + +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 index 382e99f14e..27364a54de 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include @@ -32,7 +31,7 @@ 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) : - QWidget(parent, flags), + BakeWidget(parent, flags), _domainNameSetting(DOMAIN_NAME_SETTING_KEY), _exportDirectory(EXPORT_DIR_SETTING_KEY), _browseStartDirectory(BROWSE_START_DIR_SETTING_KEY), @@ -41,20 +40,6 @@ DomainBakeWidget::DomainBakeWidget(QWidget* parent, Qt::WindowFlags flags) : setupUI(); } -DomainBakeWidget::~DomainBakeWidget() { - // 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 DomainBakeWidget::setupUI() { // setup a grid layout to hold everything QGridLayout* gridLayout = new QGridLayout; @@ -296,12 +281,3 @@ void DomainBakeWidget::handleFinishedBaker() { } } } - -void DomainBakeWidget::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/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index b37ed490bc..cd8c4a012e 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -17,21 +17,20 @@ #include #include "../DomainBaker.h" +#include "BakeWidget.h" class QLineEdit; -class DomainBakeWidget : public QWidget { +class DomainBakeWidget : public BakeWidget { Q_OBJECT public: DomainBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); - ~DomainBakeWidget(); private slots: void chooseFileButtonClicked(); void chooseOutputDirButtonClicked(); void bakeButtonClicked(); - void cancelButtonClicked(); void outputDirectoryChanged(const QString& newDirectory); @@ -41,10 +40,6 @@ private slots: private: void setupUI(); - using BakerRowPair = std::pair, int>; - using BakerRowPairList = std::list; - BakerRowPairList _bakers; - QLineEdit* _domainNameLineEdit; QLineEdit* _entitiesFileLineEdit; QLineEdit* _outputDirLineEdit; diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 7b1adf05a7..77f92c82e1 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -29,27 +29,13 @@ 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) : - QWidget(parent, flags), + BakeWidget(parent, flags), _exportDirectory(EXPORT_DIR_SETTING_KEY), _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) { setupUI(); } -ModelBakeWidget::~ModelBakeWidget() { - // if we're about to go down, whatever bakers we're managing are about to as well - // enumerate them, send the results table a cancelled status, and clean them up - 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 ModelBakeWidget::setupUI() { // setup a grid layout to hold everything QGridLayout* gridLayout = new QGridLayout; @@ -236,12 +222,3 @@ void ModelBakeWidget::handleFinishedBaker() { } } } - -void ModelBakeWidget::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/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index 0711417d21..b42b8725f6 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -18,21 +18,21 @@ #include +#include "BakeWidget.h" + class QLineEdit; class QThread; -class ModelBakeWidget : public QWidget { +class ModelBakeWidget : public BakeWidget { Q_OBJECT public: ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); - ~ModelBakeWidget(); private slots: void chooseFileButtonClicked(); void chooseOutputDirButtonClicked(); void bakeButtonClicked(); - void cancelButtonClicked(); void outputDirectoryChanged(const QString& newDirectory); @@ -41,10 +41,6 @@ private slots: private: void setupUI(); - using BakerRowPair = std::pair, int>; - using BakerRowPairList = std::list; - BakerRowPairList _bakers; - QLineEdit* _modelLineEdit; QLineEdit* _outputDirLineEdit; diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp index 1a5a53de21..d6dbb98bb7 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.cpp +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -29,7 +29,7 @@ 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) : - QWidget(parent, flags), + BakeWidget(parent, flags), _exportDirectory(EXPORT_DIR_SETTING_KEY), _selectionStartDirectory(SELECTION_START_DIR_SETTING_KEY) { @@ -221,12 +221,3 @@ void SkyboxBakeWidget::handleFinishedBaker() { } } } - -void SkyboxBakeWidget::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/SkyboxBakeWidget.h b/tools/oven/src/ui/SkyboxBakeWidget.h index 3dfd2fa4f8..f00ab07f33 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.h +++ b/tools/oven/src/ui/SkyboxBakeWidget.h @@ -18,9 +18,11 @@ #include +#include "BakeWidget.h" + class QLineEdit; -class SkyboxBakeWidget : public QWidget { +class SkyboxBakeWidget : public BakeWidget { Q_OBJECT public: @@ -30,7 +32,6 @@ private slots: void chooseFileButtonClicked(); void chooseOutputDirButtonClicked(); void bakeButtonClicked(); - void cancelButtonClicked(); void outputDirectoryChanged(const QString& newDirectory); @@ -39,10 +40,6 @@ private slots: private: void setupUI(); - using BakerRowPair = std::pair, int>; - using BakerRowPairList = std::list; - BakerRowPairList _bakers; - QLineEdit* _selectionLineEdit; QLineEdit* _outputDirLineEdit; From 822af3365b296a1acaa7d18686ddbe59a9ff5e6b Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 17 Apr 2017 17:41:34 -0700 Subject: [PATCH 054/146] always bring the results window to front when shown --- tools/oven/src/ui/OvenMainWindow.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp index 1cf2986d17..1987bab660 100644 --- a/tools/oven/src/ui/OvenMainWindow.cpp +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -41,11 +41,17 @@ ResultsWindow* OvenMainWindow::showResultsWindow() { 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, place it right below our window + // show the results window and make sure it is in front _resultsWindow->show(); - _resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom()); + _resultsWindow->raise(); // return a pointer to the results window the caller can use return _resultsWindow; From 26d13ce0028472f1ab571c88ad70858a4c8a1817 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 18 Apr 2017 15:13:42 -0700 Subject: [PATCH 055/146] make results window raising optional --- tools/oven/src/ui/DomainBakeWidget.cpp | 4 +++- tools/oven/src/ui/OvenMainWindow.cpp | 7 +++++-- tools/oven/src/ui/OvenMainWindow.h | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 27364a54de..bfae2d70d4 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -241,7 +241,9 @@ void DomainBakeWidget::handleBakerProgress(int baked, int total) { if (it != _bakers.end()) { auto resultRow = it->second; - auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + // 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); diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp index 1987bab660..dd40fb1f8f 100644 --- a/tools/oven/src/ui/OvenMainWindow.cpp +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -37,7 +37,7 @@ OvenMainWindow::~OvenMainWindow() { } } -ResultsWindow* OvenMainWindow::showResultsWindow() { +ResultsWindow* OvenMainWindow::showResultsWindow(bool shouldRaise) { if (!_resultsWindow) { // we don't have a results window right now, so make a new one _resultsWindow = new ResultsWindow; @@ -51,7 +51,10 @@ ResultsWindow* OvenMainWindow::showResultsWindow() { // show the results window and make sure it is in front _resultsWindow->show(); - _resultsWindow->raise(); + + 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 index 2d5d2aec99..a557d5e8dd 100644 --- a/tools/oven/src/ui/OvenMainWindow.h +++ b/tools/oven/src/ui/OvenMainWindow.h @@ -25,7 +25,7 @@ public: OvenMainWindow(QWidget *parent = Q_NULLPTR, Qt::WindowFlags flags = Qt::WindowFlags()); ~OvenMainWindow(); - ResultsWindow* showResultsWindow(); + ResultsWindow* showResultsWindow(bool shouldRaise = true); private: QPointer _resultsWindow; From 2478ddb379e2fe3cfc6c5b0f3dbf170be92c4370 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 18 Apr 2017 16:41:28 -0700 Subject: [PATCH 056/146] cleanup comments in skybox baking widget --- tools/oven/src/ui/SkyboxBakeWidget.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp index d6dbb98bb7..d0da0a98ba 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.cpp +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -51,7 +51,7 @@ void SkyboxBakeWidget::setupUI() { QPushButton* chooseFileButton = new QPushButton("Browse..."); connect(chooseFileButton, &QPushButton::clicked, this, &SkyboxBakeWidget::chooseFileButtonClicked); - // add the components for the model file picker to the layout + // 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); @@ -164,10 +164,10 @@ void SkyboxBakeWidget::bakeButtonClicked() { return; } - // split the list from the model line edit to see how many models we need to bake + // 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 model file text box + // 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 @@ -175,7 +175,7 @@ void SkyboxBakeWidget::bakeButtonClicked() { skyboxToBakeURL.setScheme("file"); } - // everything seems to be in place, kick off a bake for this model now + // everything seems to be in place, kick off a bake for this skybox now auto baker = std::unique_ptr { new TextureBaker(skyboxToBakeURL, gpu::CUBE_TEXTURE, outputDirectory.absolutePath()) }; From 6127b72834bb683d5e5eb0e5d447fe00676bf53a Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 18 Apr 2017 17:09:28 -0700 Subject: [PATCH 057/146] don't build oven and model-baking with default/ALL_BUILD --- libraries/model-baking/CMakeLists.txt | 4 +++- tools/oven/CMakeLists.txt | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/model-baking/CMakeLists.txt b/libraries/model-baking/CMakeLists.txt index 2e08488d69..d26874e488 100644 --- a/libraries/model-baking/CMakeLists.txt +++ b/libraries/model-baking/CMakeLists.txt @@ -4,6 +4,8 @@ setup_hifi_library(Concurrent) link_hifi_libraries(networking image gpu shared ktx) -find_package(FBXSDK REQUIRED) +find_package(FBXSDK) target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR}) + +set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index c9afc7660f..1328d7db5a 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -7,3 +7,5 @@ link_hifi_libraries(model-baking shared image gpu ktx) if (WIN32) package_libraries_for_deployment() endif () + +set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) From 4863c924d1db3fbf8bef3963751156340f773527 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 18 Apr 2017 17:15:33 -0700 Subject: [PATCH 058/146] don't link or include missing FBX library --- cmake/modules/{FindFBXSDK.cmake => FindFBX.cmake} | 0 libraries/model-baking/CMakeLists.txt | 10 +++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) rename cmake/modules/{FindFBXSDK.cmake => FindFBX.cmake} (100%) diff --git a/cmake/modules/FindFBXSDK.cmake b/cmake/modules/FindFBX.cmake similarity index 100% rename from cmake/modules/FindFBXSDK.cmake rename to cmake/modules/FindFBX.cmake diff --git a/libraries/model-baking/CMakeLists.txt b/libraries/model-baking/CMakeLists.txt index d26874e488..b3698270d7 100644 --- a/libraries/model-baking/CMakeLists.txt +++ b/libraries/model-baking/CMakeLists.txt @@ -4,8 +4,12 @@ setup_hifi_library(Concurrent) link_hifi_libraries(networking image gpu shared ktx) -find_package(FBXSDK) -target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) -target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR}) +# try to find the FBX SDK but fail silently if we don't +# because this library 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) From b520640fef44d04d759e7f37a065c13a8a7f93bf Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 10:30:01 -0700 Subject: [PATCH 059/146] grow status column when there are long results --- tools/oven/src/ui/ResultsWindow.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/oven/src/ui/ResultsWindow.cpp b/tools/oven/src/ui/ResultsWindow.cpp index cfbd07090f..35b5160f9b 100644 --- a/tools/oven/src/ui/ResultsWindow.cpp +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -88,10 +88,13 @@ int ResultsWindow::addPendingResultRow(const QString& fileName, const QDir& outp } 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, 1, statusItem); + _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); } From b3f3302f5c9953aae6f439123225a3cc4b798a8f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 10:34:14 -0700 Subject: [PATCH 060/146] add accepted file types to baker file pickers --- tools/oven/src/ui/DomainBakeWidget.cpp | 3 ++- tools/oven/src/ui/ModelBakeWidget.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index bfae2d70d4..7d667305bb 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -149,7 +149,8 @@ void DomainBakeWidget::chooseFileButtonClicked() { startDir = QDir::homePath(); } - auto selectedFile = QFileDialog::getOpenFileName(this, "Choose Entities File", startDir); + 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 diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 77f92c82e1..7f2a0c74b5 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -113,7 +113,7 @@ void ModelBakeWidget::chooseFileButtonClicked() { startDir = QDir::homePath(); } - auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir); + 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 From c71255d5feb42e60bf982e31029028386d8329d1 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 10:42:12 -0700 Subject: [PATCH 061/146] force oven dependency on model-baker since it is excluded --- tools/oven/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 1328d7db5a..10e2923e35 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -9,3 +9,7 @@ if (WIN32) endif () set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) + +# because the model-baking library is excluded from all and default builds, we force +# a dependency on it here +add_dependencies(${TARGET_NAME} model-baking) From 38cb998ca15eb5fac13613968703ec3531df759b Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 11:14:15 -0700 Subject: [PATCH 062/146] move model-baking library to oven for build exclusion --- libraries/model-baking/CMakeLists.txt | 15 --------------- tools/oven/CMakeLists.txt | 14 +++++++++----- .../model-baking => tools/oven}/src/Baker.cpp | 0 .../model-baking => tools/oven}/src/Baker.h | 0 tools/oven/src/DomainBaker.h | 6 +++--- .../model-baking => tools/oven}/src/FBXBaker.cpp | 0 .../model-baking => tools/oven}/src/FBXBaker.h | 0 .../oven}/src/ModelBakingLoggingCategory.cpp | 0 .../oven}/src/ModelBakingLoggingCategory.h | 0 .../oven}/src/TextureBaker.cpp | 0 .../oven}/src/TextureBaker.h | 0 tools/oven/src/ui/BakeWidget.h | 2 +- tools/oven/src/ui/ModelBakeWidget.h | 2 +- tools/oven/src/ui/SkyboxBakeWidget.h | 2 +- 14 files changed, 15 insertions(+), 26 deletions(-) delete mode 100644 libraries/model-baking/CMakeLists.txt rename {libraries/model-baking => tools/oven}/src/Baker.cpp (100%) rename {libraries/model-baking => tools/oven}/src/Baker.h (100%) rename {libraries/model-baking => tools/oven}/src/FBXBaker.cpp (100%) rename {libraries/model-baking => tools/oven}/src/FBXBaker.h (100%) rename {libraries/model-baking => tools/oven}/src/ModelBakingLoggingCategory.cpp (100%) rename {libraries/model-baking => tools/oven}/src/ModelBakingLoggingCategory.h (100%) rename {libraries/model-baking => tools/oven}/src/TextureBaker.cpp (100%) rename {libraries/model-baking => tools/oven}/src/TextureBaker.h (100%) diff --git a/libraries/model-baking/CMakeLists.txt b/libraries/model-baking/CMakeLists.txt deleted file mode 100644 index b3698270d7..0000000000 --- a/libraries/model-baking/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -set(TARGET_NAME model-baking) - -setup_hifi_library(Concurrent) - -link_hifi_libraries(networking image gpu shared ktx) - -# try to find the FBX SDK but fail silently if we don't -# because this library 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/CMakeLists.txt b/tools/oven/CMakeLists.txt index 10e2923e35..24c8a9a0e2 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,14 +2,18 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(model-baking shared image gpu ktx) +link_hifi_libraries(networking shared image gpu ktx) if (WIN32) package_libraries_for_deployment() endif () -set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) +# 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 () -# because the model-baking library is excluded from all and default builds, we force -# a dependency on it here -add_dependencies(${TARGET_NAME} model-baking) +set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) diff --git a/libraries/model-baking/src/Baker.cpp b/tools/oven/src/Baker.cpp similarity index 100% rename from libraries/model-baking/src/Baker.cpp rename to tools/oven/src/Baker.cpp diff --git a/libraries/model-baking/src/Baker.h b/tools/oven/src/Baker.h similarity index 100% rename from libraries/model-baking/src/Baker.h rename to tools/oven/src/Baker.h diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 54cbb18b06..5244408115 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -17,9 +17,9 @@ #include #include -#include -#include -#include +#include "Baker.h" +#include "FBXBaker.h" +#include "TextureBaker.h" class DomainBaker : public Baker { Q_OBJECT diff --git a/libraries/model-baking/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp similarity index 100% rename from libraries/model-baking/src/FBXBaker.cpp rename to tools/oven/src/FBXBaker.cpp diff --git a/libraries/model-baking/src/FBXBaker.h b/tools/oven/src/FBXBaker.h similarity index 100% rename from libraries/model-baking/src/FBXBaker.h rename to tools/oven/src/FBXBaker.h diff --git a/libraries/model-baking/src/ModelBakingLoggingCategory.cpp b/tools/oven/src/ModelBakingLoggingCategory.cpp similarity index 100% rename from libraries/model-baking/src/ModelBakingLoggingCategory.cpp rename to tools/oven/src/ModelBakingLoggingCategory.cpp diff --git a/libraries/model-baking/src/ModelBakingLoggingCategory.h b/tools/oven/src/ModelBakingLoggingCategory.h similarity index 100% rename from libraries/model-baking/src/ModelBakingLoggingCategory.h rename to tools/oven/src/ModelBakingLoggingCategory.h diff --git a/libraries/model-baking/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp similarity index 100% rename from libraries/model-baking/src/TextureBaker.cpp rename to tools/oven/src/TextureBaker.cpp diff --git a/libraries/model-baking/src/TextureBaker.h b/tools/oven/src/TextureBaker.h similarity index 100% rename from libraries/model-baking/src/TextureBaker.h rename to tools/oven/src/TextureBaker.h diff --git a/tools/oven/src/ui/BakeWidget.h b/tools/oven/src/ui/BakeWidget.h index 00996128ed..e7ab8d1840 100644 --- a/tools/oven/src/ui/BakeWidget.h +++ b/tools/oven/src/ui/BakeWidget.h @@ -14,7 +14,7 @@ #include -#include +#include "../Baker.h" class BakeWidget : public QWidget { Q_OBJECT diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index b42b8725f6..ed08990ba5 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -16,7 +16,7 @@ #include -#include +#include "../FBXBaker.h" #include "BakeWidget.h" diff --git a/tools/oven/src/ui/SkyboxBakeWidget.h b/tools/oven/src/ui/SkyboxBakeWidget.h index f00ab07f33..4063a5459b 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.h +++ b/tools/oven/src/ui/SkyboxBakeWidget.h @@ -16,7 +16,7 @@ #include -#include +#include "../TextureBaker.h" #include "BakeWidget.h" From 46fc69dd32c99fd594c043c7a09a4547a10a49f2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 12:25:14 -0700 Subject: [PATCH 063/146] fix filename parsing in TextureBaker --- tools/oven/src/TextureBaker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp index f0136fb454..1e22a56b52 100644 --- a/tools/oven/src/TextureBaker.cpp +++ b/tools/oven/src/TextureBaker.cpp @@ -31,7 +31,7 @@ TextureBaker::TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, { // figure out the baked texture filename auto originalFilename = textureURL.fileName(); - _bakedTextureFileName = originalFilename.left(originalFilename.indexOf('.')) + BAKED_TEXTURE_EXT; + _bakedTextureFileName = originalFilename.left(originalFilename.indexOf('.', -1)) + BAKED_TEXTURE_EXT; } void TextureBaker::bake() { From 258533de7b15a625c76bb1c817261ebf8d378dde Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 13:20:20 -0700 Subject: [PATCH 064/146] fix filename references in FBXBaker --- tools/oven/src/FBXBaker.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp index bbd490447b..f2fc4c2c96 100644 --- a/tools/oven/src/FBXBaker.cpp +++ b/tools/oven/src/FBXBaker.cpp @@ -48,7 +48,7 @@ FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, // grab the name of the FBX from the URL, this is used for folder output names auto fileName = fbxURL.fileName(); - _fbxName = fileName.left(fileName.indexOf('.')); + _fbxName = fileName.left(fileName.indexOf('.', -1)); } static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; @@ -229,7 +229,7 @@ QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { // in case another texture referenced by this model has the same base name auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; - QString bakedTextureFileName { textureFileInfo.baseName() }; + QString bakedTextureFileName { textureFileInfo.completeBaseName() }; if (nameMatches > 0) { // there are already nameMatches texture with this name @@ -356,7 +356,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { // make sure this texture points to something and isn't one we've already re-mapped if (!textureFileInfo.filePath().isEmpty() - && textureFileInfo.completeSuffix() != BAKED_TEXTURE_EXT.mid(1)) { + && 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 @@ -366,7 +366,8 @@ void FBXBaker::rewriteAndBakeSceneTextures() { _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + bakedTextureFileName }; - qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() << "to" << bakedTextureFilePath; + qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() + << "to" << bakedTextureFilePath; // write the new filename into the FBX scene fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); From c43a4eec86193c82d6d150870f684412070fe72c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 13:25:11 -0700 Subject: [PATCH 065/146] fix indexOf checks by using lastIndexOf --- tools/oven/src/FBXBaker.cpp | 2 +- tools/oven/src/TextureBaker.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp index f2fc4c2c96..9b2a30c109 100644 --- a/tools/oven/src/FBXBaker.cpp +++ b/tools/oven/src/FBXBaker.cpp @@ -48,7 +48,7 @@ FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, // grab the name of the FBX from the URL, this is used for folder output names auto fileName = fbxURL.fileName(); - _fbxName = fileName.left(fileName.indexOf('.', -1)); + _fbxName = fileName.left(fileName.lastIndexOf('.')); } static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp index 1e22a56b52..03b9fd669d 100644 --- a/tools/oven/src/TextureBaker.cpp +++ b/tools/oven/src/TextureBaker.cpp @@ -31,7 +31,7 @@ TextureBaker::TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, { // figure out the baked texture filename auto originalFilename = textureURL.fileName(); - _bakedTextureFileName = originalFilename.left(originalFilename.indexOf('.', -1)) + BAKED_TEXTURE_EXT; + _bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT; } void TextureBaker::bake() { From 683985aea9af80cd3a2be2165f2ca5c617184de9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 15:17:54 -0700 Subject: [PATCH 066/146] correct headers after move from model-baking to oven --- tools/oven/src/Baker.cpp | 2 +- tools/oven/src/Baker.h | 2 +- tools/oven/src/FBXBaker.cpp | 2 +- tools/oven/src/FBXBaker.h | 2 +- tools/oven/src/ModelBakingLoggingCategory.cpp | 2 +- tools/oven/src/ModelBakingLoggingCategory.h | 2 +- tools/oven/src/TextureBaker.cpp | 2 +- tools/oven/src/TextureBaker.h | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/oven/src/Baker.cpp b/tools/oven/src/Baker.cpp index 666e59a073..c0cbd8d124 100644 --- a/tools/oven/src/Baker.cpp +++ b/tools/oven/src/Baker.cpp @@ -1,6 +1,6 @@ // // Baker.cpp -// libraries/model-baking/src +// tools/oven/src // // Created by Stephen Birarda on 4/14/17. // Copyright 2017 High Fidelity, Inc. diff --git a/tools/oven/src/Baker.h b/tools/oven/src/Baker.h index 8f73819dfe..d7107428bf 100644 --- a/tools/oven/src/Baker.h +++ b/tools/oven/src/Baker.h @@ -1,6 +1,6 @@ // // Baker.h -// libraries/model-baking/src +// tools/oven/src // // Created by Stephen Birarda on 4/14/17. // Copyright 2017 High Fidelity, Inc. diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp index 9b2a30c109..7446c7ec67 100644 --- a/tools/oven/src/FBXBaker.cpp +++ b/tools/oven/src/FBXBaker.cpp @@ -1,6 +1,6 @@ // // FBXBaker.cpp -// libraries/model-baking/src +// tools/oven/src // // Created by Stephen Birarda on 3/30/17. // Copyright 2017 High Fidelity, Inc. diff --git a/tools/oven/src/FBXBaker.h b/tools/oven/src/FBXBaker.h index 5ec6427bfb..7532d587b6 100644 --- a/tools/oven/src/FBXBaker.h +++ b/tools/oven/src/FBXBaker.h @@ -1,6 +1,6 @@ // // FBXBaker.h -// libraries/model-baking/src +// tools/oven/src // // Created by Stephen Birarda on 3/30/17. // Copyright 2017 High Fidelity, Inc. diff --git a/tools/oven/src/ModelBakingLoggingCategory.cpp b/tools/oven/src/ModelBakingLoggingCategory.cpp index c2ad6360d2..f897ddf5ca 100644 --- a/tools/oven/src/ModelBakingLoggingCategory.cpp +++ b/tools/oven/src/ModelBakingLoggingCategory.cpp @@ -1,6 +1,6 @@ // // ModelBakingLoggingCategory.cpp -// libraries/model-baking/src +// tools/oven/src // // Created by Stephen Birarda on 4/5/17. // Copyright 2017 High Fidelity, Inc. diff --git a/tools/oven/src/ModelBakingLoggingCategory.h b/tools/oven/src/ModelBakingLoggingCategory.h index 600618ed5e..6c7d9d5db6 100644 --- a/tools/oven/src/ModelBakingLoggingCategory.h +++ b/tools/oven/src/ModelBakingLoggingCategory.h @@ -1,6 +1,6 @@ // // ModelBakingLoggingCategory.h -// libraries/model-baking/src +// tools/oven/src // // Created by Stephen Birarda on 4/5/17. // Copyright 2017 High Fidelity, Inc. diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp index 03b9fd669d..2a6c0f5699 100644 --- a/tools/oven/src/TextureBaker.cpp +++ b/tools/oven/src/TextureBaker.cpp @@ -1,6 +1,6 @@ // // TextureBaker.cpp -// libraries/model-baker/src +// tools/oven/src // // Created by Stephen Birarda on 4/5/17. // Copyright 2017 High Fidelity, Inc. diff --git a/tools/oven/src/TextureBaker.h b/tools/oven/src/TextureBaker.h index 7a6d1d404b..2bc4ffc607 100644 --- a/tools/oven/src/TextureBaker.h +++ b/tools/oven/src/TextureBaker.h @@ -1,6 +1,6 @@ // // TextureBaker.h -// libraries/model-baker/src +// tools/oven/src // // Created by Stephen Birarda on 4/5/17. // Copyright 2017 High Fidelity, Inc. From 3aa6757c326f5f6689a79b46739a92f2f0c31237 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Apr 2017 15:19:13 -0700 Subject: [PATCH 067/146] use QCryptographicHash static for cleaner hashing --- tools/oven/src/TextureBaker.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp index 2a6c0f5699..783cc426d0 100644 --- a/tools/oven/src/TextureBaker.cpp +++ b/tools/oven/src/TextureBaker.cpp @@ -103,9 +103,8 @@ void TextureBaker::processTexture() { // 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 - QCryptographicHash hasher(QCryptographicHash::Md5); - hasher.addData(_originalTexture); - std::string hash = hasher.result().toHex().toStdString(); + auto hashData = QCryptographicHash::hash(_originalTexture, QCryptographicHash::Md5); + std::string hash = hashData.toHex().toStdString(); processedTexture->setSourceHash(hash); auto memKTX = gpu::Texture::serialize(*processedTexture); From 93c35314ec22f40f9ed9ef321b9f004f5dab637f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 25 Apr 2017 10:57:53 -0700 Subject: [PATCH 068/146] use HF user agent for texture and FBX requests --- tools/oven/src/FBXBaker.cpp | 3 +++ tools/oven/src/TextureBaker.cpp | 2 ++ 2 files changed, 5 insertions(+) diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp index 7446c7ec67..c185488760 100644 --- a/tools/oven/src/FBXBaker.cpp +++ b/tools/oven/src/FBXBaker.cpp @@ -23,6 +23,7 @@ #include #include +#include #include "ModelBakingLoggingCategory.h" #include "TextureBaker.h" @@ -147,6 +148,8 @@ void FBXBaker::loadSourceFBX() { // 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); diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp index 783cc426d0..02bedeb659 100644 --- a/tools/oven/src/TextureBaker.cpp +++ b/tools/oven/src/TextureBaker.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "ModelBakingLoggingCategory.h" @@ -65,6 +66,7 @@ void TextureBaker::loadTexture() { // 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); From 0b4a8d05aace6762ffd24ca6eef4b469d9a920f1 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 26 Apr 2017 11:07:29 -0700 Subject: [PATCH 069/146] fix references to moved texture type enum --- libraries/image/src/image/Image.h | 3 ++- tools/oven/src/DomainBaker.cpp | 2 +- tools/oven/src/FBXBaker.cpp | 8 ++++---- tools/oven/src/FBXBaker.h | 2 +- tools/oven/src/TextureBaker.cpp | 2 +- tools/oven/src/TextureBaker.h | 6 +++--- tools/oven/src/ui/SkyboxBakeWidget.cpp | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index 3e5aa868d2..fd214daa2c 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/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 07df9870aa..b93e852daa 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -239,7 +239,7 @@ void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { // setup a baker for this skybox QSharedPointer skyboxBaker { - new TextureBaker(skyboxURL, gpu::CUBE_TEXTURE, _contentOutputPath), + new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath), &TextureBaker::deleteLater }; diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp index c185488760..26aea7a596 100644 --- a/tools/oven/src/FBXBaker.cpp +++ b/tools/oven/src/FBXBaker.cpp @@ -277,8 +277,8 @@ QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* f return urlToTexture; } -gpu::TextureType textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { - using namespace gpu; +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 @@ -347,7 +347,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { // figure out the type of texture from the material property auto textureType = textureTypeForMaterialProperty(property, material); - if (textureType != gpu::UNUSED_TEXTURE) { + if (textureType != image::TextureUsage::UNUSED_TEXTURE) { int numTextures = property.GetSrcObjectCount(); for (int j = 0; j < numTextures; j++) { @@ -393,7 +393,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { } } -void FBXBaker::bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, const QDir& outputDir) { +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), diff --git a/tools/oven/src/FBXBaker.h b/tools/oven/src/FBXBaker.h index 7532d587b6..bcfebbe2a8 100644 --- a/tools/oven/src/FBXBaker.h +++ b/tools/oven/src/FBXBaker.h @@ -74,7 +74,7 @@ private: QString createBakedTextureFileName(const QFileInfo& textureFileInfo); QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); - void bakeTexture(const QUrl& textureURL, gpu::TextureType textureType, const QDir& outputDir); + void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir); QString pathToCopyOfOriginal() const; diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp index 02bedeb659..70df511d2c 100644 --- a/tools/oven/src/TextureBaker.cpp +++ b/tools/oven/src/TextureBaker.cpp @@ -25,7 +25,7 @@ const QString BAKED_TEXTURE_EXT = ".ktx"; -TextureBaker::TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QDir& outputDirectory) : +TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) : _textureURL(textureURL), _textureType(textureType), _outputDirectory(outputDirectory) diff --git a/tools/oven/src/TextureBaker.h b/tools/oven/src/TextureBaker.h index 2bc4ffc607..ee1e968f20 100644 --- a/tools/oven/src/TextureBaker.h +++ b/tools/oven/src/TextureBaker.h @@ -16,7 +16,7 @@ #include #include -#include +#include #include "Baker.h" @@ -26,7 +26,7 @@ class TextureBaker : public Baker { Q_OBJECT public: - TextureBaker(const QUrl& textureURL, gpu::TextureType textureType, const QDir& outputDirectory); + TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory); const QByteArray& getOriginalTexture() const { return _originalTexture; } @@ -50,7 +50,7 @@ private: QUrl _textureURL; QByteArray _originalTexture; - gpu::TextureType _textureType; + image::TextureUsage::Type _textureType; QDir _outputDirectory; QString _bakedTextureFileName; diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp index d0da0a98ba..d5c280aebd 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.cpp +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -177,7 +177,7 @@ void SkyboxBakeWidget::bakeButtonClicked() { // everything seems to be in place, kick off a bake for this skybox now auto baker = std::unique_ptr { - new TextureBaker(skyboxToBakeURL, gpu::CUBE_TEXTURE, outputDirectory.absolutePath()) + new TextureBaker(skyboxToBakeURL, image::TextureUsage::CUBE_TEXTURE, outputDirectory.absolutePath()) }; // move the baker to a worker thread From 2637040f174d99828acac297194a8db57f0e566f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 26 Apr 2017 12:47:25 -0700 Subject: [PATCH 070/146] remove skybox baking from domain baking --- tools/oven/src/DomainBaker.cpp | 44 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index b93e852daa..cb2a6bca29 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -191,28 +191,28 @@ void DomainBaker::enumerateEntities() { _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); - } - } +// // 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); +// } +// } } } } From 7f69bdecb2829641d7fc55a0d8cbf6d0d4b2e2cc Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 May 2017 07:06:21 -0700 Subject: [PATCH 071/146] first try at angular-velocity setting action --- interface/src/InterfaceDynamicFactory.cpp | 3 ++ .../entities/src/EntityDynamicInterface.cpp | 5 +++ .../entities/src/EntityDynamicInterface.h | 3 +- libraries/physics/src/ObjectActionSpring.cpp | 35 ------------------- libraries/physics/src/ObjectActionSpring.h | 4 --- libraries/physics/src/ObjectDynamic.cpp | 35 +++++++++++++++++++ libraries/physics/src/ObjectDynamic.h | 4 +++ 7 files changed, 49 insertions(+), 40 deletions(-) diff --git a/interface/src/InterfaceDynamicFactory.cpp b/interface/src/InterfaceDynamicFactory.cpp index 5acc0700c0..4b9e3b96ed 100644 --- a/interface/src/InterfaceDynamicFactory.cpp +++ b/interface/src/InterfaceDynamicFactory.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "InterfaceDynamicFactory.h" @@ -47,6 +48,8 @@ EntityDynamicPointer interfaceDynamicFactory(EntityDynamicType type, const QUuid return std::make_shared(id, ownerEntity); case DYNAMIC_TYPE_CONE_TWIST: return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_MOTOR: + return std::make_shared(id, ownerEntity); } qDebug() << "Unknown entity dynamic type"; diff --git a/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp index 57f86105b2..71b3bda534 100644 --- a/libraries/entities/src/EntityDynamicInterface.cpp +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -126,6 +126,9 @@ EntityDynamicType EntityDynamicInterface::dynamicTypeFromString(QString dynamicT if (normalizedDynamicTypeString == "conetwist") { return DYNAMIC_TYPE_CONE_TWIST; } + if (normalizedDynamicTypeString == "motor") { + return DYNAMIC_TYPE_MOTOR; + } qCDebug(entities) << "Warning -- EntityDynamicInterface::dynamicTypeFromString got unknown dynamic-type name" << dynamicTypeString; @@ -154,6 +157,8 @@ QString EntityDynamicInterface::dynamicTypeToString(EntityDynamicType dynamicTyp return "ball-socket"; case DYNAMIC_TYPE_CONE_TWIST: return "cone-twist"; + case DYNAMIC_TYPE_MOTOR: + return "motor"; } assert(false); return "none"; diff --git a/libraries/entities/src/EntityDynamicInterface.h b/libraries/entities/src/EntityDynamicInterface.h index c04aaf67b2..536389ef2d 100644 --- a/libraries/entities/src/EntityDynamicInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -34,7 +34,8 @@ enum EntityDynamicType { DYNAMIC_TYPE_FAR_GRAB = 6000, DYNAMIC_TYPE_SLIDER = 7000, DYNAMIC_TYPE_BALL_SOCKET = 8000, - DYNAMIC_TYPE_CONE_TWIST = 9000 + DYNAMIC_TYPE_CONE_TWIST = 9000, + DYNAMIC_TYPE_MOTOR = 10000 }; diff --git a/libraries/physics/src/ObjectActionSpring.cpp b/libraries/physics/src/ObjectActionSpring.cpp index 3cd3926073..04bc53de83 100644 --- a/libraries/physics/src/ObjectActionSpring.cpp +++ b/libraries/physics/src/ObjectActionSpring.cpp @@ -42,41 +42,6 @@ ObjectActionSpring::~ObjectActionSpring() { #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, glm::vec3& linearVelocity, glm::vec3& angularVelocity, float& linearTimeScale, float& angularTimeScale) { diff --git a/libraries/physics/src/ObjectActionSpring.h b/libraries/physics/src/ObjectActionSpring.h index 8f810d7956..83a65b36b4 100644 --- a/libraries/physics/src/ObjectActionSpring.h +++ b/libraries/physics/src/ObjectActionSpring.h @@ -47,10 +47,6 @@ protected: glm::vec3 _linearVelocityTarget; glm::vec3 _angularVelocityTarget; - EntityItemID _otherID; - SpatiallyNestableWeakPointer _other; - SpatiallyNestablePointer getOther(); - virtual bool prepareForSpringUpdate(btScalar deltaTimeStep); void serializeParameters(QDataStream& dataStream) const; diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp index 3cb9f5b405..c11817041e 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -274,3 +274,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..3843647de8 100644 --- a/libraries/physics/src/ObjectDynamic.h +++ b/libraries/physics/src/ObjectDynamic.h @@ -67,6 +67,10 @@ protected: QString _tag; quint64 _expires { 0 }; // in seconds since epoch + EntityItemID _otherID; + SpatiallyNestableWeakPointer _other; + SpatiallyNestablePointer getOther(); + private: qint64 getEntityServerClockSkew() const; }; From 598937df89ea449d16bf36720d32e301f85183f4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 May 2017 15:26:17 -0700 Subject: [PATCH 072/146] first try at angular-velocity setting action --- libraries/physics/src/ObjectActionMotor.cpp | 203 ++++++++++++++++++++ libraries/physics/src/ObjectActionMotor.h | 40 ++++ 2 files changed, 243 insertions(+) create mode 100644 libraries/physics/src/ObjectActionMotor.cpp create mode 100644 libraries/physics/src/ObjectActionMotor.h diff --git a/libraries/physics/src/ObjectActionMotor.cpp b/libraries/physics/src/ObjectActionMotor.cpp new file mode 100644 index 0000000000..a3c1537535 --- /dev/null +++ b/libraries/physics/src/ObjectActionMotor.cpp @@ -0,0 +1,203 @@ +// +// ObjectActionMotor.cpp +// libraries/physics/src +// +// Created by Seth Alves 2017-4-30 +// 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 "QVariantGLM.h" + +#include "ObjectActionMotor.h" + +#include "PhysicsLogging.h" + +const glm::vec3 MOTOR_MAX_SPEED = glm::vec3(PI*10.0f, PI*10.0f, PI*10.0f); +const float MAX_MOTOR_TIMESCALE = 600.0f; // 10 min is a long time + +const uint16_t ObjectActionMotor::motorVersion = 1; + + +ObjectActionMotor::ObjectActionMotor(const QUuid& id, EntityItemPointer ownerEntity) : + ObjectAction(DYNAMIC_TYPE_MOTOR, id, ownerEntity) +{ + #if WANT_DEBUG + qCDebug(physics) << "ObjectActionMotor::ObjectActionMotor"; + #endif +} + +ObjectActionMotor::~ObjectActionMotor() { + #if WANT_DEBUG + qCDebug(physics) << "ObjectActionMotor::~ObjectActionMotor"; + #endif +} + +void ObjectActionMotor::updateActionWorker(btScalar deltaTimeStep) { + SpatiallyNestablePointer other = getOther(); + + withReadLock([&]{ + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + + void* physicsInfo = ownerEntity->getPhysicsInfo(); + if (!physicsInfo) { + return; + } + ObjectMotionState* motionState = static_cast(physicsInfo); + btRigidBody* rigidBody = motionState->getRigidBody(); + if (!rigidBody) { + qCDebug(physics) << "ObjectActionMotor::updateActionWorker no rigidBody"; + return; + } + + if (_angularTimeScale < MAX_MOTOR_TIMESCALE) { + + if (_otherID != QUuid()) { + if (other) { + glm::vec3 otherAngularVelocity = other->getAngularVelocity(); + rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget + otherAngularVelocity)); + } + } else { + rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget)); + } + } + }); +} + +const float MIN_TIMESCALE = 0.1f; + + +bool ObjectActionMotor::updateArguments(QVariantMap arguments) { + glm::vec3 angularVelocityTarget; + float angularTimeScale; + QUuid otherID; + bool ok; + + + bool needUpdate = false; + bool somethingChanged = ObjectDynamic::updateArguments(arguments); + withReadLock([&]{ + + ok = true; + angularVelocityTarget = EntityDynamicInterface::extractVec3Argument("motor action", arguments, + "targetAngularVelocity", ok, false); + if (!ok) { + angularVelocityTarget = _angularVelocityTarget; + } + + ok = true; + angularTimeScale = + EntityDynamicInterface::extractFloatArgument("motor action", arguments, "angularTimeScale", ok, false); + if (!ok) { + angularTimeScale = _angularTimeScale; + } + + ok = true; + otherID = QUuid(EntityDynamicInterface::extractStringArgument("motor action", arguments, "otherID", ok, false)); + if (!ok) { + otherID = _otherID; + } + + if (somethingChanged || + angularVelocityTarget != _angularVelocityTarget || + angularTimeScale != _angularTimeScale || + otherID != _otherID) { + // something changed + needUpdate = true; + } + }); + + if (needUpdate) { + withWriteLock([&] { + _angularVelocityTarget = angularVelocityTarget; + _angularTimeScale = glm::max(MIN_TIMESCALE, glm::abs(angularTimeScale)); + _otherID = otherID; + _active = true; + + auto ownerEntity = _ownerEntity.lock(); + if (ownerEntity) { + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); + } + }); + activateBody(); + } + + return true; +} + +QVariantMap ObjectActionMotor::getArguments() { + QVariantMap arguments = ObjectDynamic::getArguments(); + withReadLock([&] { + arguments["targetAngularVelocity"] = glmToQMap(_angularVelocityTarget); + arguments["angularTimeScale"] = _angularTimeScale; + + arguments["otherID"] = _otherID; + }); + return arguments; +} + +void ObjectActionMotor::serializeParameters(QDataStream& dataStream) const { + withReadLock([&] { + dataStream << localTimeToServerTime(_expires); + dataStream << _tag; + dataStream << _otherID; + + dataStream << _angularVelocityTarget; + dataStream << _angularTimeScale; + }); +} + +QByteArray ObjectActionMotor::serialize() const { + QByteArray serializedActionArguments; + QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_MOTOR; + dataStream << getID(); + dataStream << ObjectActionMotor::motorVersion; + + serializeParameters(dataStream); + + return serializedActionArguments; +} + +void ObjectActionMotor::deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream) { + withWriteLock([&] { + quint64 serverExpires; + dataStream >> serverExpires; + _expires = serverTimeToLocalTime(serverExpires); + dataStream >> _tag; + dataStream >> _otherID; + + dataStream >> _angularVelocityTarget; + dataStream >> _angularTimeScale; + + _active = true; + }); +} + +void ObjectActionMotor::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + assert(type == getType()); + + QUuid id; + dataStream >> id; + assert(id == getID()); + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectActionMotor::motorVersion) { + assert(false); + return; + } + + deserializeParameters(serializedArguments, dataStream); +} diff --git a/libraries/physics/src/ObjectActionMotor.h b/libraries/physics/src/ObjectActionMotor.h new file mode 100644 index 0000000000..60044db241 --- /dev/null +++ b/libraries/physics/src/ObjectActionMotor.h @@ -0,0 +1,40 @@ +// +// ObjectActionMotor.h +// libraries/physics/src +// +// Created by Seth Alves 2017-4-30 +// 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_ObjectActionMotor_h +#define hifi_ObjectActionMotor_h + +#include "ObjectAction.h" + +class ObjectActionMotor : public ObjectAction { +public: + ObjectActionMotor(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectActionMotor(); + + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; + + virtual void updateActionWorker(float deltaTimeStep) override; + + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; + +protected: + static const uint16_t motorVersion; + + glm::vec3 _angularVelocityTarget; + float _angularTimeScale { FLT_MAX }; + + void serializeParameters(QDataStream& dataStream) const; + void deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream); +}; + +#endif // hifi_ObjectActionMotor_h From b422ec3f88af9a9d979611c2d17ffb4783735cdf Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 2 May 2017 18:04:23 -0700 Subject: [PATCH 073/146] also set the relative filename to fix double references --- tools/oven/src/FBXBaker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp index 26aea7a596..8a72784d7c 100644 --- a/tools/oven/src/FBXBaker.cpp +++ b/tools/oven/src/FBXBaker.cpp @@ -375,6 +375,10 @@ void FBXBaker::rewriteAndBakeSceneTextures() { // 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); From fe280ab103abb7a5594e61ed9f2aaf5886d5e5d7 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 3 May 2017 14:24:23 -0700 Subject: [PATCH 074/146] remember the browse directory for model baker --- tools/oven/src/ui/ModelBakeWidget.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 7f2a0c74b5..c696fbad26 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -119,12 +119,15 @@ void ModelBakeWidget::chooseFileButtonClicked() { // set the contents of the model file text box to be the path to the selected file _modelLineEdit->setText(selectedFiles.join(',')); - if (_outputDirLineEdit->text().isEmpty()) { - auto directoryOfModel = QFileInfo(selectedFiles[0]).absolutePath(); + 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); } } From a9c1e2781e72b78ace4ce5aeeb855c3939e94102 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Thu, 4 May 2017 21:40:11 +0200 Subject: [PATCH 075/146] Implemented chat interface script and tablet web page for worklist #21280 --- unpublishedScripts/marketplace/chat/Chat.js | 990 ++++++++++++++++++ .../marketplace/chat/ChatPage.html | 511 +++++++++ 2 files changed, 1501 insertions(+) create mode 100644 unpublishedScripts/marketplace/chat/Chat.js create mode 100644 unpublishedScripts/marketplace/chat/ChatPage.html diff --git a/unpublishedScripts/marketplace/chat/Chat.js b/unpublishedScripts/marketplace/chat/Chat.js new file mode 100644 index 0000000000..5491b6c771 --- /dev/null +++ b/unpublishedScripts/marketplace/chat/Chat.js @@ -0,0 +1,990 @@ +"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); + print("loadSettings: chatName", chatName); + if (!chatName) { + chatName = randomAvatarName(); + } + print("loadSettings: now chatName", chatName); + 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); + print("saveSettings: chatName", chatName, "or", Settings.getValue('Chat_chatName', 'xxx')); + 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: identityAvatarDuration, + 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; + } + + receiveChatMessageTablet(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 +
+ +
+ +
+ +
+ +
+ + + + + + From c6eb1aa4e8ad34326a2ff69c53d0bea56362f72e Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Fri, 5 May 2017 23:32:05 +0100 Subject: [PATCH 076/146] Fixed silly bugs with /who response and drawing lines to identify avatars. DOH! --- unpublishedScripts/marketplace/chat/Chat.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/unpublishedScripts/marketplace/chat/Chat.js b/unpublishedScripts/marketplace/chat/Chat.js index 5491b6c771..33bfcfeb4d 100644 --- a/unpublishedScripts/marketplace/chat/Chat.js +++ b/unpublishedScripts/marketplace/chat/Chat.js @@ -47,11 +47,9 @@ // Load the persistent variables from the Settings, with defaults. function loadSettings() { chatName = Settings.getValue('Chat_chatName', MyAvatar.displayName); - print("loadSettings: chatName", chatName); if (!chatName) { chatName = randomAvatarName(); } - print("loadSettings: now chatName", chatName); chatLogMaxSize = Settings.getValue('Chat_chatLogMaxSize', 100); sendTyping = Settings.getValue('Chat_sendTyping', true); identifyAvatarDuration = Settings.getValue('Chat_identifyAvatarDuration', 10); @@ -71,7 +69,6 @@ // Save the persistent variables to the Settings. function saveSettings() { Settings.setValue('Chat_chatName', chatName); - print("saveSettings: chatName", chatName, "or", Settings.getValue('Chat_chatName', 'xxx')); Settings.setValue('Chat_chatLogMaxSize', chatLogMaxSize); Settings.setValue('Chat_sendTyping', sendTyping); Settings.setValue('Chat_identifyAvatarDuration', identifyAvatarDuration); @@ -195,7 +192,7 @@ // Notification that somebody started typing. function handleAvatarBeginTyping(avatarID, displayName) { - print("handleAvatarBeginTyping:", "avatarID", avatarID, displayName); + //print("handleAvatarBeginTyping:", "avatarID", avatarID, displayName); } // Notification that we stopped typing. @@ -217,7 +214,7 @@ // Notification that somebody stopped typing. function handleAvatarEndTyping(avatarID, displayName) { - print("handleAvatarEndTyping:", "avatarID", avatarID, displayName); + //print("handleAvatarEndTyping:", "avatarID", avatarID, displayName); } // Identify an avatar by drawing a line from our head to their head. @@ -280,7 +277,7 @@ var identifierParams = { parentID: myAvatarID, parentJointIndex: myJointIndex, - lifetime: identityAvatarDuration, + lifetime: identifyAvatarDuration, start: myJointPosition, endParentID: yourAvatarID, endParentJointIndex: yourJointIndex, @@ -438,7 +435,7 @@ return; } - receiveChatMessageTablet(avatarID, displayName, message, data); + handleTransmitChatMessage(avatarID, displayName, message, data); } // Handle input form the user, possibly multiple lines separated by newlines. @@ -646,7 +643,7 @@ Overlays.deleteOverlay(speechBubbleTextOverlayID); } catch (e) {} - print("updateSpeechBubble:", "speechBubbleMessage", speechBubbleMessage, "textSize", textSize.width, textSize.height); + //print("updateSpeechBubble:", "speechBubbleMessage", speechBubbleMessage, "textSize", textSize.width, textSize.height); var fudge = 0.02; var width = textSize.width + fudge; @@ -685,7 +682,7 @@ Entities.editEntity(speechBubbleTextID, speechBubbleParams); } - print("speechBubbleTextID:", speechBubbleTextID, "speechBubbleParams", JSON.stringify(speechBubbleParams)); + //print("speechBubbleTextID:", speechBubbleTextID, "speechBubbleParams", JSON.stringify(speechBubbleParams)); } // Hide the speech bubble. @@ -694,7 +691,7 @@ speechBubbleShowing = false; - print("popDownSpeechBubble speechBubbleTextID", speechBubbleTextID); + //print("popDownSpeechBubble speechBubbleTextID", speechBubbleTextID); if (speechBubbleTextID) { try { From 03490c81da5a6a76f06f13e1341d44d4fcea9f5e Mon Sep 17 00:00:00 2001 From: Mike Moody Date: Sat, 6 May 2017 15:36:18 -0700 Subject: [PATCH 077/146] v1 with bug, looking into. --- .../system/libraries/entitySelectionTool.js | 101 ++++++++++++++++-- 1 file changed, 94 insertions(+), 7 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 79d45d5cd2..70ded31ca4 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -340,6 +340,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 +410,23 @@ SelectionDisplay = (function() { borderSize: 1.4, }; + var grabberPropertiesCloner = { + position: { + x: 0, + y: 0, + z: 0 + }, + size: grabberSizeCorner + 0.025, + 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 +604,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, @@ -969,6 +993,9 @@ SelectionDisplay = (function() { grabberPointLightCircleX, grabberPointLightCircleY, grabberPointLightCircleZ, + + grabberCloner + ].concat(stretchHandles); overlayNames[highlightBox] = "highlightBox"; @@ -1015,7 +1042,7 @@ SelectionDisplay = (function() { overlayNames[rotateZeroOverlay] = "rotateZeroOverlay"; overlayNames[rotateCurrentOverlay] = "rotateCurrentOverlay"; - + overlayNames[grabberCloner] = "grabberCloner"; var activeTool = null; var grabberTools = {}; @@ -1107,7 +1134,7 @@ SelectionDisplay = (function() { if (event !== false) { pickRay = generalComputePickRay(event.x, event.y); - var wantDebug = false; + var wantDebug = true; if (wantDebug) { print("select() with EVENT...... "); print(" event.y:" + event.y); @@ -2291,7 +2318,11 @@ SelectionDisplay = (function() { }, rotation: Quat.fromPitchYawRollDegrees(90, 0, 0), }); - + Overlays.editOverlay(grabberCloner, { + visible: stretchHandlesVisible, + rotation: rotation, + position: RIGHT + }); }; @@ -2371,7 +2402,7 @@ SelectionDisplay = (function() { return (origin.y - intersection.y) / Vec3.distance(origin, intersection); }, onMove: function(event) { - var wantDebug = false; + var wantDebug = true; pickRay = generalComputePickRay(event.x, event.y); var pick = rayPlaneIntersection2(pickRay, translateXZTool.pickPlanePosition, { @@ -2556,7 +2587,7 @@ SelectionDisplay = (function() { vector.x = 0; vector.z = 0; - var wantDebug = false; + var wantDebug = true; if (wantDebug) { print("translateUpDown... "); print(" event.y:" + event.y); @@ -2580,6 +2611,52 @@ SelectionDisplay = (function() { }, }); + addGrabberTool(grabberCloner, { + mode: "CLONE", + pickPlanePosition: { x: 0, y: 0, z: 0 }, + greatestDimension: 0.0, + startingDistance: 0.0, + startingElevation: 0.0, + onBegin: function(event) { + SelectionManager.saveProperties(); + startPosition = SelectionManager.worldPosition; + var dimensions = SelectionManager.worldDimensions; + + var pickRay = generalComputePickRay(event.x, event.y); + initialXZPick = rayPlaneIntersection(pickRay, translateXZTool.pickPlanePosition, { + x: 0, + y: 1, + z: 0 + }); + + // Duplicate entities if alt is pressed. This will make a + // copy of the selected entities and move the _original_ entities, not + // the new ones. + + duplicatedEntityIDs = []; + for (var otherEntityID in SelectionManager.savedProperties) { + var properties = SelectionManager.savedProperties[otherEntityID]; + if (!properties.locked) { + var entityID = Entities.addEntity(properties); + duplicatedEntityIDs.push({ + entityID: entityID, + properties: properties, + }); + } + } + + isConstrained = false; + }, + elevation: translateXZTool.elevation, + + onEnd: translateXZTool.onEnd, + + onMove: translateXZTool.onMove + }); + + + + var vec3Mult = function(v1, v2) { return { x: v1.x * v2.x, @@ -2844,7 +2921,7 @@ SelectionDisplay = (function() { }); } - var wantDebug = false; + var wantDebug = true; if (wantDebug) { print(stretchMode); //Vec3.print(" newIntersection:", newIntersection); @@ -3861,7 +3938,7 @@ SelectionDisplay = (function() { }; that.mousePressEvent = function(event) { - var wantDebug = false; + var wantDebug = true; if (!event.isLeftButton && !that.triggered) { // if another mouse button than left is pressed ignore it return false; @@ -3958,6 +4035,10 @@ SelectionDisplay = (function() { mode = "STRETCH_LEFT"; somethingClicked = mode; break; + // case grabberCloner: + // mode = "CLONE"; + // somethingClicked = mode; + // break; default: mode = "UNKNOWN"; @@ -4344,6 +4425,12 @@ SelectionDisplay = (function() { highlightNeeded = true; break; + case grabberCloner: + pickedColor = grabberColorCloner; + pickedAlpha = grabberAlpha; + highlightNeeded = true; + break; + default: if (previousHandle) { Overlays.editOverlay(previousHandle, { From 1873558df15ec8d8bbbdcac7a275738469c1af10 Mon Sep 17 00:00:00 2001 From: Rob Kayson Date: Sat, 6 May 2017 18:41:57 -0700 Subject: [PATCH 078/146] added floating lantern box that spawns floating lanterns --- scripts/tutorials/createFloatingLanternBox.js | 42 +++++++ .../entity_scripts/floatingLantern.js | 106 ++++++++++++++++++ .../entity_scripts/floatingLanternBox.js | 101 +++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 scripts/tutorials/createFloatingLanternBox.js create mode 100644 scripts/tutorials/entity_scripts/floatingLantern.js create mode 100644 scripts/tutorials/entity_scripts/floatingLanternBox.js diff --git a/scripts/tutorials/createFloatingLanternBox.js b/scripts/tutorials/createFloatingLanternBox.js new file mode 100644 index 0000000000..611e995fcb --- /dev/null +++ b/scripts/tutorials/createFloatingLanternBox.js @@ -0,0 +1,42 @@ +"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 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, + y: 0.58531, + z: 0.9264 + }, + 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..8fa2828c90 --- /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..2c483f6129 --- /dev/null +++ b/scripts/tutorials/entity_scripts/floatingLanternBox.js @@ -0,0 +1,101 @@ +"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 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, + y: 0.4, + z: 0.2049 + }, + 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(); +}); From 7a1a9d6496b661e5618340de219ebec08e1b5246 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 7 May 2017 19:42:04 -0700 Subject: [PATCH 079/146] start on code to remap constraint IDs during import --- libraries/entities/src/EntityDynamicInterface.h | 4 ++++ libraries/physics/src/ObjectDynamic.cpp | 11 +++++++++++ libraries/physics/src/ObjectDynamic.h | 2 ++ 3 files changed, 17 insertions(+) diff --git a/libraries/entities/src/EntityDynamicInterface.h b/libraries/entities/src/EntityDynamicInterface.h index 536389ef2d..ccfbafec2e 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; @@ -45,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; } diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp index c11817041e..3bad6b135b 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -24,6 +24,17 @@ ObjectDynamic::ObjectDynamic(EntityDynamicType type, const QUuid& id, EntityItem ObjectDynamic::~ObjectDynamic() { } +void ObjectDynamic::remapIDs(QHash* map) { + withWriteLock([&]{ + if (map->contains(_id)) { + _id = (*map)[_id]; + } + if (map->contains(_otherID)) { + _otherID = (*map)[_otherID]; + } + }); +} + qint64 ObjectDynamic::getEntityServerClockSkew() const { auto nodeList = DependencyManager::get(); diff --git a/libraries/physics/src/ObjectDynamic.h b/libraries/physics/src/ObjectDynamic.h index 3843647de8..06cea4e525 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; } From cfe118e2c6312f56fddf63558070e3e2c0d24bd8 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 7 May 2017 19:42:24 -0700 Subject: [PATCH 080/146] start on code to remap constraint IDs during import --- assignment-client/src/AssignmentDynamic.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assignment-client/src/AssignmentDynamic.h b/assignment-client/src/AssignmentDynamic.h index 35db8b1524..477b8ea1d6 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; } From fd238f5438ebc7383e563c492fd033d591edca8a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 8 May 2017 11:02:35 -0700 Subject: [PATCH 081/146] working on fixing action/constraint export/import --- assignment-client/src/AssignmentDynamic.h | 2 +- .../entities/src/EntityDynamicInterface.h | 2 +- libraries/entities/src/EntityTree.cpp | 115 ++++++++++++------ .../physics/src/ObjectConstraintHinge.cpp | 22 ++-- libraries/physics/src/ObjectDynamic.cpp | 13 +- libraries/physics/src/ObjectDynamic.h | 2 +- 6 files changed, 104 insertions(+), 52 deletions(-) diff --git a/assignment-client/src/AssignmentDynamic.h b/assignment-client/src/AssignmentDynamic.h index 477b8ea1d6..604c418c13 100644 --- a/assignment-client/src/AssignmentDynamic.h +++ b/assignment-client/src/AssignmentDynamic.h @@ -24,7 +24,7 @@ public: AssignmentDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); virtual ~AssignmentDynamic(); - virtual void remapIDs(QHash* map) override {}; + virtual void remapIDs(QHash& map) override {}; virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } diff --git a/libraries/entities/src/EntityDynamicInterface.h b/libraries/entities/src/EntityDynamicInterface.h index ccfbafec2e..ef20f84da4 100644 --- a/libraries/entities/src/EntityDynamicInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -47,7 +47,7 @@ public: const QUuid& getID() const { return _id; } EntityDynamicType getType() const { return _type; } - virtual void remapIDs(QHash* map) = 0; + virtual void remapIDs(QHash& map) = 0; virtual bool isAction() const { return false; } virtual bool isConstraint() const { return false; } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 76483d0786..22bde03100 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 @@ -1549,13 +1551,23 @@ QVector EntityTree::sendEntities(EntityEditPacketSender* packetSen 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); + + auto getMapped = [&](EntityItemID oldID) { + if (oldID.isNull()) { + return EntityItemID(); } - EntityItemID newID = QUuid::createUuid(); - args->map->insert(oldID, newID); + if (args->map->contains(oldID)) { + return args->map->value(oldID); + } else { + EntityItemID newID = QUuid::createUuid(); + args->map->insert(oldID, newID); + return newID; + } + }; + + entityTreeElement->forEachEntity([&](EntityItemPointer item) { + EntityItemID oldID = item->getEntityItemID(); + EntityItemID newID = getMapped(oldID); EntityItemProperties properties = item->getProperties(); EntityItemID oldParentID = properties.getParentID(); @@ -1564,8 +1576,7 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } 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); @@ -1573,40 +1584,63 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } if (!properties.getXNNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID()); - if (neighborEntity) { - properties.setXNNeighborID(getMapped(neighborEntity)); - } + properties.setXNNeighborID(getMapped(properties.getXNNeighborID())); } if (!properties.getXPNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID()); - if (neighborEntity) { - properties.setXPNeighborID(getMapped(neighborEntity)); - } + properties.setXPNeighborID(getMapped(properties.getXPNeighborID())); } if (!properties.getYNNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID()); - if (neighborEntity) { - properties.setYNNeighborID(getMapped(neighborEntity)); - } + properties.setYNNeighborID(getMapped(properties.getYNNeighborID())); } if (!properties.getYPNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID()); - if (neighborEntity) { - properties.setYPNeighborID(getMapped(neighborEntity)); - } + properties.setYPNeighborID(getMapped(properties.getYPNeighborID())); } if (!properties.getZNNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID()); - if (neighborEntity) { - properties.setZNNeighborID(getMapped(neighborEntity)); - } + properties.setZNNeighborID(getMapped(properties.getZNNeighborID())); } if (!properties.getZPNeighborID().isInvalidID()) { - auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID()); - if (neighborEntity) { - properties.setZPNeighborID(getMapped(neighborEntity)); + properties.setZPNeighborID(getMapped(properties.getZPNeighborID())); + } + + QByteArray actionData = properties.getActionData(); + if (!actionData.isEmpty()) { + 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; + QUuid actionID = QUuid::createUuid(); // give the action a new ID + (*args->map)[oldActionID] = actionID; + EntityDynamicPointer action = actionFactory->factoryBA(nullptr, serializedAction); + if (action) { + action->remapIDs(*args->map); + remappedActions[actionID] = action; + } } + + QVector remappedSerializedActions; + + QHash::const_iterator i = remappedActions.begin(); + while (i != remappedActions.end()) { + const QUuid id = i.key(); + EntityDynamicPointer action = remappedActions[id]; + QByteArray bytesForAction = action->serialize(); + remappedSerializedActions << bytesForAction; + i++; + } + + QByteArray result; + QDataStream remappedSerializedActionsStream(&result, QIODevice::WriteOnly); + remappedSerializedActionsStream << remappedSerializedActions; + properties.setActionData(result); } // set creation time to "now" for imported entities @@ -1623,13 +1657,26 @@ 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; - }; + }); + + + + QHash::iterator i = (*args->map).begin(); + while (i != (*args->map).end()) { + EntityItemID newID = i.value(); + if (args->otherTree->findEntityByEntityItemID(newID)) { + i++; + } else { + EntityItemID oldID = i.key(); + i = (*args->map).erase(i); + } + } - entityTreeElement->forEachEntity(getMapped); return true; } diff --git a/libraries/physics/src/ObjectConstraintHinge.cpp b/libraries/physics/src/ObjectConstraintHinge.cpp index cf91ca904b..fa89107f6f 100644 --- a/libraries/physics/src/ObjectConstraintHinge.cpp +++ b/libraries/physics/src/ObjectConstraintHinge.cpp @@ -275,18 +275,20 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { QVariantMap ObjectConstraintHinge::getArguments() { QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { + 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; 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; diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp index 3bad6b135b..0982c9825d 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -24,14 +24,17 @@ ObjectDynamic::ObjectDynamic(EntityDynamicType type, const QUuid& id, EntityItem ObjectDynamic::~ObjectDynamic() { } -void ObjectDynamic::remapIDs(QHash* map) { +void ObjectDynamic::remapIDs(QHash& map) { withWriteLock([&]{ - if (map->contains(_id)) { - _id = (*map)[_id]; + if (!map.contains(_id)) { + map[_id] = QUuid::createUuid(); } - if (map->contains(_otherID)) { - _otherID = (*map)[_otherID]; + _id = map[_id]; + + if (!map.contains(_otherID)) { + map[_otherID] = QUuid::createUuid(); } + _otherID = map[_otherID]; }); } diff --git a/libraries/physics/src/ObjectDynamic.h b/libraries/physics/src/ObjectDynamic.h index 06cea4e525..582f47db77 100644 --- a/libraries/physics/src/ObjectDynamic.h +++ b/libraries/physics/src/ObjectDynamic.h @@ -29,7 +29,7 @@ public: ObjectDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); virtual ~ObjectDynamic(); - virtual void remapIDs(QHash* map) override; + virtual void remapIDs(QHash& map) override; virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } From dae652d3db31309f90bbbb63df61c9c1b692187f Mon Sep 17 00:00:00 2001 From: Mike Moody Date: Mon, 8 May 2017 11:25:24 -0700 Subject: [PATCH 082/146] v1 HMD duplication overlay. --- .../system/libraries/entitySelectionTool.js | 70 +++++++------------ 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 70ded31ca4..3e4b8d8518 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -416,7 +416,7 @@ SelectionDisplay = (function() { y: 0, z: 0 }, - size: grabberSizeCorner + 0.025, + size: grabberSizeCorner, color: grabberColorCloner, alpha: 1, solid: grabberSolid, @@ -652,6 +652,8 @@ SelectionDisplay = (function() { grabberPointLightR, grabberPointLightF, grabberPointLightN, + + grabberCloner ]; @@ -994,8 +996,6 @@ SelectionDisplay = (function() { grabberPointLightCircleY, grabberPointLightCircleZ, - grabberCloner - ].concat(stretchHandles); overlayNames[highlightBox] = "highlightBox"; @@ -1134,7 +1134,7 @@ SelectionDisplay = (function() { if (event !== false) { pickRay = generalComputePickRay(event.x, event.y); - var wantDebug = true; + var wantDebug = false; if (wantDebug) { print("select() with EVENT...... "); print(" event.y:" + event.y); @@ -2355,7 +2355,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; @@ -2370,7 +2370,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]; @@ -2402,7 +2402,7 @@ SelectionDisplay = (function() { return (origin.y - intersection.y) / Vec3.distance(origin, intersection); }, onMove: function(event) { - var wantDebug = true; + var wantDebug = false; pickRay = generalComputePickRay(event.x, event.y); var pick = rayPlaneIntersection2(pickRay, translateXZTool.pickPlanePosition, { @@ -2587,7 +2587,7 @@ SelectionDisplay = (function() { vector.x = 0; vector.z = 0; - var wantDebug = true; + var wantDebug = false; if (wantDebug) { print("translateUpDown... "); print(" event.y:" + event.y); @@ -2613,45 +2613,27 @@ SelectionDisplay = (function() { addGrabberTool(grabberCloner, { mode: "CLONE", - pickPlanePosition: { x: 0, y: 0, z: 0 }, - greatestDimension: 0.0, - startingDistance: 0.0, - startingElevation: 0.0, onBegin: function(event) { - SelectionManager.saveProperties(); - startPosition = SelectionManager.worldPosition; - var dimensions = SelectionManager.worldDimensions; var pickRay = generalComputePickRay(event.x, event.y); - initialXZPick = rayPlaneIntersection(pickRay, translateXZTool.pickPlanePosition, { - x: 0, - y: 1, - z: 0 - }); + var result = Overlays.findRayIntersection(pickRay); + translateXZTool.pickPlanePosition = result.intersection; + translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, SelectionManager.worldDimensions.y), + SelectionManager.worldDimensions.z); - // Duplicate entities if alt is pressed. This will make a - // copy of the selected entities and move the _original_ entities, not - // the new ones. - - duplicatedEntityIDs = []; - for (var otherEntityID in SelectionManager.savedProperties) { - var properties = SelectionManager.savedProperties[otherEntityID]; - if (!properties.locked) { - var entityID = Entities.addEntity(properties); - duplicatedEntityIDs.push({ - entityID: entityID, - properties: properties, - }); - } - } - - isConstrained = false; + translateXZTool.onBegin(event,true); + }, + elevation: function (event) { + translateXZTool.elevation(event); }, - elevation: translateXZTool.elevation, - onEnd: translateXZTool.onEnd, + onEnd: function (event) { + translateXZTool.onEnd(event); + }, - onMove: translateXZTool.onMove + onMove: function (event) { + translateXZTool.onMove(event); + } }); @@ -2921,7 +2903,7 @@ SelectionDisplay = (function() { }); } - var wantDebug = true; + var wantDebug = false; if (wantDebug) { print(stretchMode); //Vec3.print(" newIntersection:", newIntersection); @@ -3938,7 +3920,7 @@ SelectionDisplay = (function() { }; that.mousePressEvent = function(event) { - var wantDebug = true; + var wantDebug = false; if (!event.isLeftButton && !that.triggered) { // if another mouse button than left is pressed ignore it return false; @@ -4035,10 +4017,6 @@ SelectionDisplay = (function() { mode = "STRETCH_LEFT"; somethingClicked = mode; break; - // case grabberCloner: - // mode = "CLONE"; - // somethingClicked = mode; - // break; default: mode = "UNKNOWN"; From a260163aee756b185572f6a32f54323ca5056dc9 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 2 May 2017 18:22:29 -0700 Subject: [PATCH 083/146] WIP commit, first pass at generating limit center joints + debug draw --- interface/src/avatar/MyAvatar.cpp | 23 +++++++++++++++ .../animation/src/AnimInverseKinematics.cpp | 14 +++++++++ .../animation/src/AnimInverseKinematics.h | 5 ++++ libraries/animation/src/AnimUtil.cpp | 18 ++++++++++++ libraries/animation/src/AnimUtil.h | 4 +-- libraries/animation/src/ElbowConstraint.cpp | 8 +++++ libraries/animation/src/ElbowConstraint.h | 1 + libraries/animation/src/Rig.cpp | 29 +++++++++++-------- libraries/animation/src/Rig.h | 3 ++ libraries/animation/src/RotationConstraint.h | 3 ++ .../animation/src/SwingTwistConstraint.cpp | 27 +++++++++++++++++ .../animation/src/SwingTwistConstraint.h | 6 ++-- 12 files changed, 125 insertions(+), 16 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3f3ce7d9e9..7d2aefa7bf 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -1701,6 +1702,28 @@ void MyAvatar::postUpdate(float deltaTime) { initAnimGraph(); } + // AJT: REMOVE. + { + auto ikNode = _rig->getAnimInverseKinematicsNode(); + if (ikNode) { + // the rig is in the skeletonModel frame + AnimPose xform(glm::vec3(1), _skeletonModel->getRotation(), _skeletonModel->getTranslation()); + AnimPoseVec limitCenterPoses = ikNode->getLimitCenterPoses(); + + // HACK: convert joints from geom to avatar space + int hipsIndex = _rig->indexOfJoint("Hips"); + for (size_t i = 0; i < limitCenterPoses.size(); i++) { + if (i == hipsIndex) { + //limitCenterPoses[i].trans() = glm::vec3(); // zero the hips + } + // convert from cm to m + limitCenterPoses[i].trans() = 0.01f * limitCenterPoses[i].trans(); + } + _rig->getAnimSkeleton()->convertRelativePosesToAbsolute(limitCenterPoses); + AnimDebugDraw::getInstance().addAbsolutePoses("myAvatarLimitCenterPoses", _rig->getAnimSkeleton(), limitCenterPoses, xform, glm::vec4(1)); + } + } + if (_enableDebugDrawDefaultPose || _enableDebugDrawAnimPose) { auto animSkeleton = _rig->getAnimSkeleton(); diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 6edd969568..e21db11eed 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -957,6 +957,19 @@ 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); + } +} + void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { AnimNode::setSkeletonInternal(skeleton); @@ -973,6 +986,7 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele if (skeleton) { initConstraints(); + initLimitCenterPoses(); _headIndex = _skeleton->nameToJointIndex("Head"); _hipsIndex = _skeleton->nameToJointIndex("Hips"); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index c91b7aa9c4..e7427d9ebc 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -43,6 +43,9 @@ public: float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + // AJT: TODO REMOVE? for debugging. + const AnimPoseVec& getLimitCenterPoses() const { return _limitCenterPoses; } + protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); void solveWithCyclicCoordinateDescent(const std::vector& targets); @@ -55,6 +58,7 @@ protected: RotationConstraint* getConstraint(int index); void clearConstraints(); void initConstraints(); + void initLimitCenterPoses(); void computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt); // no copies @@ -85,6 +89,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 }; diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index c5643034e5..314f4a1c3a 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -33,6 +33,24 @@ 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(); + } + float alpha = 1.0f / (float)numQuats; + glm::quat accum(0, 0, 0, 0); + glm::quat firstRot = quats[0]; + for (size_t i = 0; i < numQuats; i++) { + glm::quat rot = quats[i]; + float dot = glm::dot(firstRot, rot); + if (dot < 0.0f) { + rot = -rot; + } + accum += alpha * 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..868f5cdc6b 100644 --- a/libraries/animation/src/ElbowConstraint.h +++ b/libraries/animation/src/ElbowConstraint.h @@ -18,6 +18,7 @@ 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; protected: glm::vec3 _axis; glm::vec3 _perpAxis; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 700761b248..b66b0eafa5 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); } } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2d024628f5..e0c5e9f421 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 Rig::getAnimInverseKinematicsNode() const; + void clearIKJointLimitHistory(); void setMaxHipsOffsetLength(float maxLength); float getMaxHipsOffsetLength() const; 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..c1b325a74a 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,29 @@ void SwingTwistConstraint::dynamicallyAdjustLimits(const glm::quat& rotation) { void SwingTwistConstraint::clearHistory() { _lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY; } + +glm::quat SwingTwistConstraint::computeCenterRotation() const { + const size_t NUM_MIN_DOTS = getMinDots().size(); + const size_t NUM_LIMITS = 2 * NUM_MIN_DOTS; + std::vector limits; + limits.reserve(NUM_LIMITS); + glm::quat minTwistRot; + glm::quat maxTwistRot; + if (_minTwist != _maxTwist) { + minTwistRot = glm::angleAxis(_minTwist, _referenceRotation * Vectors::UNIT_Y); + minTwistRot = glm::angleAxis(_maxTwist, _referenceRotation * Vectors::UNIT_Y); + } + const float D_THETA = TWO_PI / NUM_MIN_DOTS; + float theta = 0.0f; + for (size_t i = 0; i < NUM_MIN_DOTS; 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)); + glm::quat swing = glm::angleAxis(phi, glm::cross(Vectors::UNIT_Y, swungAxis)); + limits.push_back(swing * minTwistRot); + limits.push_back(swing * maxTwistRot); + } + return averageQuats(limits.size(), &limits[0]); +} diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index 06afd64232..295edb3ebf 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,8 @@ public: void clearHistory() override; + virtual glm::quat computeCenterRotation() const override; + private: float handleTwistBoundaryConditions(float twistAngle) const; From e992d6703a8faf21e5a38cab5a694bd48da8ff06 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 3 May 2017 18:51:17 -0700 Subject: [PATCH 084/146] WIP: debug render joint constraints. --- interface/src/avatar/MyAvatar.cpp | 4 +- libraries/animation/src/AnimContext.cpp | 6 +- libraries/animation/src/AnimContext.h | 4 +- .../animation/src/AnimInverseKinematics.cpp | 120 +++++++++++++++++- .../animation/src/AnimInverseKinematics.h | 3 +- libraries/animation/src/AnimPose.cpp | 7 +- libraries/animation/src/AnimPose.h | 3 +- libraries/animation/src/ElbowConstraint.h | 5 + libraries/animation/src/Rig.cpp | 6 +- libraries/animation/src/Rig.h | 2 +- .../animation/src/SwingTwistConstraint.cpp | 22 ++-- .../animation/src/SwingTwistConstraint.h | 3 + libraries/render-utils/src/Model.cpp | 3 +- 13 files changed, 164 insertions(+), 24 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 7d2aefa7bf..526b80e3f1 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1703,6 +1703,7 @@ void MyAvatar::postUpdate(float deltaTime) { } // AJT: REMOVE. + /* { auto ikNode = _rig->getAnimInverseKinematicsNode(); if (ikNode) { @@ -1714,7 +1715,7 @@ void MyAvatar::postUpdate(float deltaTime) { int hipsIndex = _rig->indexOfJoint("Hips"); for (size_t i = 0; i < limitCenterPoses.size(); i++) { if (i == hipsIndex) { - //limitCenterPoses[i].trans() = glm::vec3(); // zero the hips + limitCenterPoses[i].trans() = glm::vec3(); // zero the hips } // convert from cm to m limitCenterPoses[i].trans() = 0.01f * limitCenterPoses[i].trans(); @@ -1723,6 +1724,7 @@ void MyAvatar::postUpdate(float deltaTime) { AnimDebugDraw::getInstance().addAbsolutePoses("myAvatarLimitCenterPoses", _rig->getAnimSkeleton(), limitCenterPoses, xform, glm::vec4(1)); } } + */ if (_enableDebugDrawDefaultPose || _enableDebugDrawAnimPose) { diff --git a/libraries/animation/src/AnimContext.cpp b/libraries/animation/src/AnimContext.cpp index c8d3e7bcda..c59c75b191 100644 --- a/libraries/animation/src/AnimContext.cpp +++ b/libraries/animation/src/AnimContext.cpp @@ -10,7 +10,9 @@ #include "AnimContext.h" -AnimContext::AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix) : +AnimContext::AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix) : _enableDebugDrawIKTargets(enableDebugDrawIKTargets), - _geometryToRigMatrix(geometryToRigMatrix) { + _geometryToRigMatrix(geometryToRigMatrix), + _rigToWorldMatrix(rigToWorldMatrix) +{ } diff --git a/libraries/animation/src/AnimContext.h b/libraries/animation/src/AnimContext.h index 3170911e14..067e64026a 100644 --- a/libraries/animation/src/AnimContext.h +++ b/libraries/animation/src/AnimContext.h @@ -16,15 +16,17 @@ class AnimContext { public: - AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix); + AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix); bool getEnableDebugDrawIKTargets() const { return _enableDebugDrawIKTargets; } const glm::mat4& getGeometryToRigMatrix() const { return _geometryToRigMatrix; } + const glm::mat4& getRigToWorldMatrix() const { return _rigToWorldMatrix; } protected: bool _enableDebugDrawIKTargets { 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 e21db11eed..ae2ca94d66 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -399,6 +399,8 @@ 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) { + 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; @@ -604,9 +606,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; } @@ -1003,3 +1005,117 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele _hipsParentIndex = -1; } } + +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 absPoses = /*_limitCenterPoses;*/ _skeleton->getRelativeDefaultPoses(); + _skeleton->convertRelativePosesToAbsolute(absPoses); + + mat4 geomToWorldMatrix = context.getRigToWorldMatrix() * context.getGeometryToRigMatrix(); + for (int i = 0; i < absPoses.size(); i++) { + // transform local axes into world space. + auto pose = absPoses[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, absPoses[parentIndex].trans()); + DebugDraw::getInstance().drawRay(pos, parentPos, GRAY); + } + + glm::quat parentAbsRot; + if (parentIndex != -1) { + parentAbsRot = absPoses[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()); + + glm::vec3 minYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * minRot * refRot * Vectors::UNIT_Y); + glm::vec3 maxYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * maxRot * refRot * 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_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); + + glm::vec3 minTwistYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * minRot * refRot * Vectors::UNIT_X); + glm::vec3 maxTwistYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * maxRot * refRot * Vectors::UNIT_X); + + 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. + glm::vec3 previousSwingTip; + const size_t NUM_MIN_DOTS = swingTwistConstraint->getMinDots().size(); + const float D_THETA = TWO_PI / NUM_MIN_DOTS; + float theta = 0.0f; + for (size_t i = 0; i < NUM_MIN_DOTS; i++, theta += D_THETA) { + // compute swing rotation from theta and phi angles. + float phi = acos(swingTwistConstraint->getMinDots()[i]); + float cos_phi = swingTwistConstraint->getMinDots()[i]; + float sin_phi = sinf(phi); + glm::vec3 swungAxis(sin_phi * cosf(theta), cos_phi, sin_phi * sinf(theta)); + glm::vec3 worldSwungAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * refRot * swungAxis); + + glm::vec3 swingTip = pos + SWING_LENGTH * worldSwungAxis; + DebugDraw::getInstance().drawRay(pos, swingTip, PURPLE); + + if (previousSwingTipValid) { + DebugDraw::getInstance().drawRay(previousSwingTip, swingTip, PURPLE); + } + previousSwingTip = swingTip; + previousSwingTipValid = true; + } + } + } + pose.rot() = constraint->computeCenterRotation(); + } + } + } +} diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index e7427d9ebc..5ad5638709 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -51,11 +51,12 @@ protected: 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; // 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(); 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/ElbowConstraint.h b/libraries/animation/src/ElbowConstraint.h index 868f5cdc6b..d3f080374a 100644 --- a/libraries/animation/src/ElbowConstraint.h +++ b/libraries/animation/src/ElbowConstraint.h @@ -19,6 +19,11 @@ public: 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/Rig.cpp b/libraries/animation/src/Rig.cpp index b66b0eafa5..2689fe5be8 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -941,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, glm::mat4 rootTransform, glm::mat4 rigToWorldTransform) { PROFILE_RANGE_EX(simulation_animation_detail, __FUNCTION__, 0xffff00ff, 0); PerformanceTimer perfTimer("updateAnimations"); @@ -954,7 +954,7 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); - AnimContext context(_enableDebugDrawIKTargets, getGeometryToRigTransform()); + AnimContext context(_enableDebugDrawIKTargets, getGeometryToRigTransform(), rigToWorldTransform); // evaluate the animation AnimNode::Triggers triggersOut; @@ -1445,7 +1445,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, 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 e0c5e9f421..f8ae0bdfae 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -162,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, glm::mat4 rootTransform, glm::mat4 rigToWorldTransform); // legacy void inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index c1b325a74a..475ee6a59e 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -435,13 +435,13 @@ void SwingTwistConstraint::clearHistory() { glm::quat SwingTwistConstraint::computeCenterRotation() const { const size_t NUM_MIN_DOTS = getMinDots().size(); const size_t NUM_LIMITS = 2 * NUM_MIN_DOTS; - std::vector limits; - limits.reserve(NUM_LIMITS); - glm::quat minTwistRot; - glm::quat maxTwistRot; + std::vector swingLimits; + swingLimits.reserve(NUM_LIMITS); + + glm::quat twistLimits[2]; if (_minTwist != _maxTwist) { - minTwistRot = glm::angleAxis(_minTwist, _referenceRotation * Vectors::UNIT_Y); - minTwistRot = glm::angleAxis(_maxTwist, _referenceRotation * Vectors::UNIT_Y); + twistLimits[0] = glm::angleAxis(_minTwist, _referenceRotation * Vectors::UNIT_Y); + twistLimits[1] = glm::angleAxis(_maxTwist, _referenceRotation * Vectors::UNIT_Y); } const float D_THETA = TWO_PI / NUM_MIN_DOTS; float theta = 0.0f; @@ -451,9 +451,11 @@ glm::quat SwingTwistConstraint::computeCenterRotation() const { float cos_phi = getMinDots()[i]; float sin_phi = sinf(phi); glm::vec3 swungAxis(sin_phi * cosf(theta), cos_phi, sin_phi * sinf(theta)); - glm::quat swing = glm::angleAxis(phi, glm::cross(Vectors::UNIT_Y, swungAxis)); - limits.push_back(swing * minTwistRot); - limits.push_back(swing * maxTwistRot); + glm::quat swing = glm::angleAxis(phi, glm::normalize(glm::cross(Vectors::UNIT_Y, swungAxis))); + swingLimits.push_back(swing); } - return averageQuats(limits.size(), &limits[0]); + glm::quat limits[2]; + limits[0] = averageQuats(swingLimits.size(), &swingLimits[0]); + limits[1] = averageQuats(2, twistLimits); + return averageQuats(2, limits); } diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index 295edb3ebf..ffe9a1d800 100644 --- a/libraries/animation/src/SwingTwistConstraint.h +++ b/libraries/animation/src/SwingTwistConstraint.h @@ -101,6 +101,9 @@ public: virtual glm::quat computeCenterRotation() const override; + const float getMinTwist() const { return _minTwist; } + const float getMaxTwist() const { return _maxTwist; } + private: float handleTwistBoundaryConditions(float twistAngle) const; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index acc84646c5..766a584b85 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1046,7 +1046,8 @@ 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() { From 7af93f9fea98ac0538d414adc4a9fa88ff724043 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 4 May 2017 17:16:22 -0700 Subject: [PATCH 085/146] Hooked up IK constraint rendering --- interface/src/Menu.cpp | 2 + interface/src/Menu.h | 1 + interface/src/avatar/MyAvatar.cpp | 5 + interface/src/avatar/MyAvatar.h | 2 + libraries/animation/src/AnimContext.cpp | 4 +- libraries/animation/src/AnimContext.h | 5 +- .../animation/src/AnimInverseKinematics.cpp | 102 ++++++++++-------- libraries/animation/src/Rig.cpp | 5 +- libraries/animation/src/Rig.h | 2 + .../animation/src/SwingTwistConstraint.cpp | 15 +-- 10 files changed, 89 insertions(+), 54 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9688694287..560fe7582d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -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..1231e0c72d 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -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/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 526b80e3f1..368b403910 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -505,6 +505,7 @@ void MyAvatar::simulate(float deltaTime) { if (_rig) { _rig->setEnableDebugDrawIKTargets(_enableDebugDrawIKTargets); + _rig->setEnableDebugDrawIKConstraints(_enableDebugDrawIKConstraints); } _skeletonModel->simulate(deltaTime); @@ -930,6 +931,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()); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7c510f0556..5a7bd7c79c 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -518,6 +518,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); @@ -703,6 +704,7 @@ private: bool _enableDebugDrawHandControllers { false }; bool _enableDebugDrawSensorToWorldMatrix { false }; bool _enableDebugDrawIKTargets { false }; + bool _enableDebugDrawIKConstraints { false }; AudioListenerMode _audioListenerMode; glm::vec3 _customListenPosition; diff --git a/libraries/animation/src/AnimContext.cpp b/libraries/animation/src/AnimContext.cpp index c59c75b191..70ca3764b0 100644 --- a/libraries/animation/src/AnimContext.cpp +++ b/libraries/animation/src/AnimContext.cpp @@ -10,8 +10,10 @@ #include "AnimContext.h" -AnimContext::AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix) : +AnimContext::AnimContext(bool enableDebugDrawIKTargets, bool enableDebugDrawIKConstraints, + const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix) : _enableDebugDrawIKTargets(enableDebugDrawIKTargets), + _enableDebugDrawIKConstraints(enableDebugDrawIKConstraints), _geometryToRigMatrix(geometryToRigMatrix), _rigToWorldMatrix(rigToWorldMatrix) { diff --git a/libraries/animation/src/AnimContext.h b/libraries/animation/src/AnimContext.h index 067e64026a..f68535005c 100644 --- a/libraries/animation/src/AnimContext.h +++ b/libraries/animation/src/AnimContext.h @@ -16,15 +16,18 @@ class AnimContext { public: - AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix); + 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; }; diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index ae2ca94d66..5dcbfcafad 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -399,7 +399,9 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { - debugDrawConstraints(context); + 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) { @@ -624,17 +626,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); @@ -642,7 +646,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). @@ -746,30 +749,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(cosf(theta), -0.25f, sinf(theta))); theta += deltaTheta; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); + swungDirections.push_back(glm::vec3(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(cosf(theta), 0.25f, sinf(theta))); // posterior theta += deltaTheta; - swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); + swungDirections.push_back(glm::vec3(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(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(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(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(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 (int 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(); @@ -1006,8 +1006,13 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele } } -void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) const { +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); @@ -1020,13 +1025,26 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con const float TWIST_LENGTH = 4.0f; // cm const float HINGE_LENGTH = 6.0f; // cm const float SWING_LENGTH = 5.0f; // cm - AnimPoseVec absPoses = /*_limitCenterPoses;*/ _skeleton->getRelativeDefaultPoses(); - _skeleton->convertRelativePosesToAbsolute(absPoses); + + AnimPoseVec poses = _skeleton->getRelativeDefaultPoses(); + + // copy reference rotations into the relative poses + for (int i = 0; i < 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(); - for (int i = 0; i < absPoses.size(); i++) { + + // draw each pose and constraint + for (int i = 0; i < poses.size(); i++) { // transform local axes into world space. - auto pose = absPoses[i]; + 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); @@ -1038,13 +1056,13 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con // draw line to parent int parentIndex = _skeleton->getParentIndex(i); if (parentIndex != -1) { - glm::vec3 parentPos = transformPoint(geomToWorldMatrix, absPoses[parentIndex].trans()); + glm::vec3 parentPos = transformPoint(geomToWorldMatrix, poses[parentIndex].trans()); DebugDraw::getInstance().drawRay(pos, parentPos, GRAY); } glm::quat parentAbsRot; if (parentIndex != -1) { - parentAbsRot = absPoses[parentIndex].rot(); + parentAbsRot = poses[parentIndex].rot(); } const RotationConstraint* constraint = getConstraint(i); @@ -1091,26 +1109,24 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con } // draw swing constraints. - glm::vec3 previousSwingTip; const size_t NUM_MIN_DOTS = swingTwistConstraint->getMinDots().size(); - const float D_THETA = TWO_PI / NUM_MIN_DOTS; + const float D_THETA = TWO_PI / (NUM_MIN_DOTS - 1); float theta = 0.0f; - for (size_t i = 0; i < NUM_MIN_DOTS; i++, theta += D_THETA) { + 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 = acos(swingTwistConstraint->getMinDots()[i]); - float cos_phi = swingTwistConstraint->getMinDots()[i]; - float sin_phi = sinf(phi); - glm::vec3 swungAxis(sin_phi * cosf(theta), cos_phi, sin_phi * sinf(theta)); + 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; - DebugDraw::getInstance().drawRay(pos, swingTip, PURPLE); - if (previousSwingTipValid) { - DebugDraw::getInstance().drawRay(previousSwingTip, swingTip, PURPLE); - } - previousSwingTip = swingTip; - previousSwingTipValid = true; + 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); } } } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 2689fe5be8..2bcd71d5c3 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -954,7 +954,8 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform, glm::mat4 r updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); - AnimContext context(_enableDebugDrawIKTargets, getGeometryToRigTransform(), rigToWorldTransform); + AnimContext context(_enableDebugDrawIKTargets, _enableDebugDrawIKConstraints, + getGeometryToRigTransform(), rigToWorldTransform); // evaluate the animation AnimNode::Triggers triggersOut; @@ -1445,7 +1446,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(), 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 f8ae0bdfae..396e68d633 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -231,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; @@ -341,6 +342,7 @@ protected: float _maxHipsOffsetLength { 1.0f }; bool _enableDebugDrawIKTargets { false }; + bool _enableDebugDrawIKConstraints { false }; private: QMap _stateHandlers; diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index 475ee6a59e..aba0516b2a 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -443,19 +443,20 @@ glm::quat SwingTwistConstraint::computeCenterRotation() const { twistLimits[0] = glm::angleAxis(_minTwist, _referenceRotation * Vectors::UNIT_Y); twistLimits[1] = glm::angleAxis(_maxTwist, _referenceRotation * Vectors::UNIT_Y); } - const float D_THETA = TWO_PI / NUM_MIN_DOTS; + const float D_THETA = TWO_PI / (NUM_MIN_DOTS - 1); float theta = 0.0f; - for (size_t i = 0; i < NUM_MIN_DOTS; i++, theta += D_THETA) { + 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)); - glm::quat swing = glm::angleAxis(phi, glm::normalize(glm::cross(Vectors::UNIT_Y, swungAxis))); + + // 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 limits[2]; - limits[0] = averageQuats(swingLimits.size(), &swingLimits[0]); - limits[1] = averageQuats(2, twistLimits); - return averageQuats(2, limits); + glm::quat averageSwing = averageQuats(swingLimits.size(), &swingLimits[0]); + glm::quat averageTwist = averageQuats(2, twistLimits); + return averageSwing * averageTwist * _referenceRotation; } From 712fcbe27a9dce1fa59dc4a37a766575dfa7179f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 4 May 2017 18:41:42 -0700 Subject: [PATCH 086/146] Removed cruft/debug code & comments --- interface/src/avatar/MyAvatar.cpp | 24 ------------------- .../animation/src/AnimInverseKinematics.h | 3 --- 2 files changed, 27 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 368b403910..7870bd4968 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1707,30 +1707,6 @@ void MyAvatar::postUpdate(float deltaTime) { initAnimGraph(); } - // AJT: REMOVE. - /* - { - auto ikNode = _rig->getAnimInverseKinematicsNode(); - if (ikNode) { - // the rig is in the skeletonModel frame - AnimPose xform(glm::vec3(1), _skeletonModel->getRotation(), _skeletonModel->getTranslation()); - AnimPoseVec limitCenterPoses = ikNode->getLimitCenterPoses(); - - // HACK: convert joints from geom to avatar space - int hipsIndex = _rig->indexOfJoint("Hips"); - for (size_t i = 0; i < limitCenterPoses.size(); i++) { - if (i == hipsIndex) { - limitCenterPoses[i].trans() = glm::vec3(); // zero the hips - } - // convert from cm to m - limitCenterPoses[i].trans() = 0.01f * limitCenterPoses[i].trans(); - } - _rig->getAnimSkeleton()->convertRelativePosesToAbsolute(limitCenterPoses); - AnimDebugDraw::getInstance().addAbsolutePoses("myAvatarLimitCenterPoses", _rig->getAnimSkeleton(), limitCenterPoses, xform, glm::vec4(1)); - } - } - */ - if (_enableDebugDrawDefaultPose || _enableDebugDrawAnimPose) { auto animSkeleton = _rig->getAnimSkeleton(); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 5ad5638709..b40f025ecd 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -43,9 +43,6 @@ public: float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } - // AJT: TODO REMOVE? for debugging. - const AnimPoseVec& getLimitCenterPoses() const { return _limitCenterPoses; } - protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); void solveWithCyclicCoordinateDescent(const std::vector& targets); From 2166d8c159e15e20a9ff03e62486b426d511268d Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 8 May 2017 14:10:16 -0700 Subject: [PATCH 087/146] Added setSolutionSource to AnimInverseKinematics node. --- .../animation/src/AnimInverseKinematics.cpp | 58 +++++++++++++------ .../animation/src/AnimInverseKinematics.h | 13 +++++ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 5dcbfcafad..bf05d9358c 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -414,25 +414,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(underPoses); if (!underPoses.empty()) { // Sometimes the underpose itself can violate the constraints. Rather than @@ -1135,3 +1117,41 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con } } } + +void AnimInverseKinematics::relaxToPoses(const AnimPoseVec& poses) { + // relax toward poses + const float blend = (1.0f / 60.0f) / (0.25f); + int numJoints = (int)_relativePoses.size(); + for (int i = 0; i < numJoints; ++i) { + float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), poses[i].rot())); + if (_accumulators[i].isDirty()) { + // this joint is affected by IK --> blend toward each pose rotation + _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * poses[i].rot(), blend)); + } else { + // this joint is NOT affected by IK --> slam to underPose rotation + _relativePoses[i].rot() = poses[i].rot(); + } + _relativePoses[i].trans() = poses[i].trans(); + } +} + +void AnimInverseKinematics::initRelativePosesFromSolutionSource(const AnimPoseVec& underPoses) { + switch (_solutionSource) { + default: + case SolutionSource::RelaxToUnderPoses: + relaxToPoses(underPoses); + break; + case SolutionSource::RelaxToLimitCenterPoses: + relaxToPoses(_limitCenterPoses); + break; + case SolutionSource::PreviousSolution: + // do nothing... _relativePoses is already the previous solution + break; + case SolutionSource::UnderPoses: + _relativePoses = underPoses; + break; + case SolutionSource::LimitCenterPoses: + _relativePoses = _limitCenterPoses; + break; + } +} diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index b40f025ecd..9b7c095e6b 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -43,12 +43,24 @@ public: float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + enum class SolutionSource { + RelaxToUnderPoses = 0, + RelaxToLimitCenterPoses, + PreviousSolution, + UnderPoses, + LimitCenterPoses + }; + + void setSolutionSource(SolutionSource solutionSource) { _solutionSource = solutionSource; } + 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(const AnimPoseVec& underPose); + void relaxToPoses(const AnimPoseVec& poses); // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } @@ -103,6 +115,7 @@ protected: float _maxErrorOnLastSolve { FLT_MAX }; bool _previousEnableDebugIKTargets { false }; + SolutionSource _solutionSource { SolutionSource::RelaxToUnderPoses }; }; #endif // hifi_AnimInverseKinematics_h From 94dfbc81f3118629429d127a214b314c4fb783a1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 8 May 2017 15:44:31 -0700 Subject: [PATCH 088/146] entity export/import involving constraints now works --- libraries/entities/src/EntityTree.cpp | 94 +++++++++---------- libraries/entities/src/EntityTree.h | 2 + .../src/ObjectConstraintBallSocket.cpp | 16 ++-- .../physics/src/ObjectConstraintBallSocket.h | 2 - .../physics/src/ObjectConstraintConeTwist.cpp | 16 ++-- .../physics/src/ObjectConstraintConeTwist.h | 1 - .../physics/src/ObjectConstraintHinge.cpp | 16 ++-- libraries/physics/src/ObjectConstraintHinge.h | 1 - .../physics/src/ObjectConstraintSlider.cpp | 16 ++-- .../physics/src/ObjectConstraintSlider.h | 1 - libraries/physics/src/ObjectDynamic.cpp | 14 +-- 11 files changed, 84 insertions(+), 95 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 22bde03100..a770ea0ec0 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1529,6 +1529,44 @@ void EntityTree::pruneTree() { recurseTreeWithOperator(&theOperator); } + +QByteArray EntityTree::remapActionDataIDs(QByteArray actionData, QHash& map) { + 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; @@ -1552,7 +1590,7 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra SendEntitiesOperationArgs* args = static_cast(extraData); EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); - auto getMapped = [&](EntityItemID oldID) { + auto getMapped = [&args](EntityItemID oldID) { if (oldID.isNull()) { return EntityItemID(); } @@ -1565,7 +1603,8 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } }; - entityTreeElement->forEachEntity([&](EntityItemPointer item) { + entityTreeElement->forEachEntity([&args, &getMapped, &element](EntityItemPointer item) { + EntityItemID oldID = item->getEntityItemID(); EntityItemID newID = getMapped(oldID); @@ -1604,43 +1643,7 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra QByteArray actionData = properties.getActionData(); if (!actionData.isEmpty()) { - 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; - QUuid actionID = QUuid::createUuid(); // give the action a new ID - (*args->map)[oldActionID] = actionID; - EntityDynamicPointer action = actionFactory->factoryBA(nullptr, serializedAction); - if (action) { - action->remapIDs(*args->map); - remappedActions[actionID] = action; - } - } - - QVector remappedSerializedActions; - - QHash::const_iterator i = remappedActions.begin(); - while (i != remappedActions.end()) { - const QUuid id = i.key(); - EntityDynamicPointer action = remappedActions[id]; - QByteArray bytesForAction = action->serialize(); - remappedSerializedActions << bytesForAction; - i++; - } - - QByteArray result; - QDataStream remappedSerializedActionsStream(&result, QIODevice::WriteOnly); - remappedSerializedActionsStream << remappedSerializedActions; - properties.setActionData(result); + properties.setActionData(remapActionDataIDs(actionData, *args->map)); } // set creation time to "now" for imported entities @@ -1664,19 +1667,6 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra return newID; }); - - - QHash::iterator i = (*args->map).begin(); - while (i != (*args->map).end()) { - EntityItemID newID = i.value(); - if (args->otherTree->findEntityByEntityItemID(newID)) { - i++; - } else { - EntityItemID oldID = i.key(); - i = (*args->map).erase(i); - } - } - 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/physics/src/ObjectConstraintBallSocket.cpp b/libraries/physics/src/ObjectConstraintBallSocket.cpp index 35f138e840..4b6e72092e 100644 --- a/libraries/physics/src/ObjectConstraintBallSocket.cpp +++ b/libraries/physics/src/ObjectConstraintBallSocket.cpp @@ -40,7 +40,7 @@ QList ObjectConstraintBallSocket::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -76,7 +76,7 @@ btTypedConstraint* ObjectConstraintBallSocket::getConstraint() { withReadLock([&]{ constraint = static_cast(_constraint); pivotInA = _pivotInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pivotInB = _pivotInB; }); if (constraint) { @@ -136,7 +136,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 +147,7 @@ bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { if (somethingChanged || pivotInA != _pivotInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pivotInB != _pivotInB) { // something changed needUpdate = true; @@ -157,7 +157,7 @@ bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { if (needUpdate) { withWriteLock([&] { _pivotInA = pivotInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pivotInB = pivotInB; _active = true; @@ -180,7 +180,7 @@ QVariantMap ObjectConstraintBallSocket::getArguments() { withReadLock([&] { if (_constraint) { arguments["pivot"] = glmToQMap(_pivotInA); - arguments["otherEntityID"] = _otherEntityID; + arguments["otherEntityID"] = _otherID; arguments["otherPivot"] = glmToQMap(_pivotInB); } }); @@ -200,7 +200,7 @@ QByteArray ObjectConstraintBallSocket::serialize() const { dataStream << _tag; dataStream << _pivotInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pivotInB; }); @@ -232,7 +232,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..57950b4ce0 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.cpp +++ b/libraries/physics/src/ObjectConstraintConeTwist.cpp @@ -40,7 +40,7 @@ QList ObjectConstraintConeTwist::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -95,7 +95,7 @@ btTypedConstraint* ObjectConstraintConeTwist::getConstraint() { constraint = static_cast(_constraint); pivotInA = _pivotInA; axisInA = _axisInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pivotInB = _pivotInB; axisInB = _axisInB; }); @@ -180,7 +180,7 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("coneTwist constraint", arguments, "otherEntityID", ok, false)); if (!ok) { - otherEntityID = _otherEntityID; + otherEntityID = _otherID; } ok = true; @@ -235,7 +235,7 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { if (somethingChanged || pivotInA != _pivotInA || axisInA != _axisInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pivotInB != _pivotInB || axisInB != _axisInB || swingSpan1 != _swingSpan1 || @@ -253,7 +253,7 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { withWriteLock([&] { _pivotInA = pivotInA; _axisInA = axisInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pivotInB = pivotInB; _axisInB = axisInB; _swingSpan1 = swingSpan1; @@ -284,7 +284,7 @@ QVariantMap ObjectConstraintConeTwist::getArguments() { if (_constraint) { arguments["pivot"] = glmToQMap(_pivotInA); arguments["axis"] = glmToQMap(_axisInA); - arguments["otherEntityID"] = _otherEntityID; + arguments["otherEntityID"] = _otherID; arguments["otherPivot"] = glmToQMap(_pivotInB); arguments["otherAxis"] = glmToQMap(_axisInB); arguments["swingSpan1"] = _swingSpan1; @@ -312,7 +312,7 @@ QByteArray ObjectConstraintConeTwist::serialize() const { dataStream << _pivotInA; dataStream << _axisInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pivotInB; dataStream << _axisInB; dataStream << _swingSpan1; @@ -352,7 +352,7 @@ void ObjectConstraintConeTwist::deserialize(QByteArray serializedArguments) { dataStream >> _pivotInA; dataStream >> _axisInA; - dataStream >> _otherEntityID; + dataStream >> _otherID; dataStream >> _pivotInB; dataStream >> _axisInB; dataStream >> _swingSpan1; diff --git a/libraries/physics/src/ObjectConstraintConeTwist.h b/libraries/physics/src/ObjectConstraintConeTwist.h index 02297e2b91..459618f101 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.h +++ b/libraries/physics/src/ObjectConstraintConeTwist.h @@ -40,7 +40,6 @@ protected: glm::vec3 _pivotInA; glm::vec3 _axisInA; - EntityItemID _otherEntityID; glm::vec3 _pivotInB; glm::vec3 _axisInB; diff --git a/libraries/physics/src/ObjectConstraintHinge.cpp b/libraries/physics/src/ObjectConstraintHinge.cpp index fa89107f6f..1fcea2913a 100644 --- a/libraries/physics/src/ObjectConstraintHinge.cpp +++ b/libraries/physics/src/ObjectConstraintHinge.cpp @@ -40,7 +40,7 @@ QList ObjectConstraintHinge::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -90,7 +90,7 @@ btTypedConstraint* ObjectConstraintHinge::getConstraint() { constraint = static_cast(_constraint); pivotInA = _pivotInA; axisInA = _axisInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pivotInB = _pivotInB; axisInB = _axisInB; }); @@ -182,7 +182,7 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("hinge constraint", arguments, "otherEntityID", ok, false)); if (!ok) { - otherEntityID = _otherEntityID; + otherEntityID = _otherID; } ok = true; @@ -231,7 +231,7 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { if (somethingChanged || pivotInA != _pivotInA || axisInA != _axisInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pivotInB != _pivotInB || axisInB != _axisInB || low != _low || @@ -248,7 +248,7 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { withWriteLock([&] { _pivotInA = pivotInA; _axisInA = axisInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pivotInB = pivotInB; _axisInB = axisInB; _low = low; @@ -277,7 +277,7 @@ QVariantMap ObjectConstraintHinge::getArguments() { withReadLock([&] { arguments["pivot"] = glmToQMap(_pivotInA); arguments["axis"] = glmToQMap(_axisInA); - arguments["otherEntityID"] = _otherEntityID; + arguments["otherEntityID"] = _otherID; arguments["otherPivot"] = glmToQMap(_pivotInB); arguments["otherAxis"] = glmToQMap(_axisInB); arguments["low"] = _low; @@ -305,7 +305,7 @@ QByteArray ObjectConstraintHinge::serialize() const { withReadLock([&] { dataStream << _pivotInA; dataStream << _axisInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pivotInB; dataStream << _axisInB; dataStream << _low; @@ -342,7 +342,7 @@ void ObjectConstraintHinge::deserialize(QByteArray serializedArguments) { withWriteLock([&] { dataStream >> _pivotInA; dataStream >> _axisInA; - dataStream >> _otherEntityID; + dataStream >> _otherID; dataStream >> _pivotInB; dataStream >> _axisInB; dataStream >> _low; diff --git a/libraries/physics/src/ObjectConstraintHinge.h b/libraries/physics/src/ObjectConstraintHinge.h index 07ce8eb8a3..b3bb92c677 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; diff --git a/libraries/physics/src/ObjectConstraintSlider.cpp b/libraries/physics/src/ObjectConstraintSlider.cpp index d7d4df78af..6e092b40a0 100644 --- a/libraries/physics/src/ObjectConstraintSlider.cpp +++ b/libraries/physics/src/ObjectConstraintSlider.cpp @@ -34,7 +34,7 @@ QList ObjectConstraintSlider::getRigidBodies() { result += getRigidBody(); QUuid otherEntityID; withReadLock([&]{ - otherEntityID = _otherEntityID; + otherEntityID = _otherID; }); if (!otherEntityID.isNull()) { result += getOtherRigidBody(otherEntityID); @@ -77,7 +77,7 @@ btTypedConstraint* ObjectConstraintSlider::getConstraint() { constraint = static_cast(_constraint); pointInA = _pointInA; axisInA = _axisInA; - otherEntityID = _otherEntityID; + otherEntityID = _otherID; pointInB = _pointInB; axisInB = _axisInB; }); @@ -160,7 +160,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 +202,7 @@ bool ObjectConstraintSlider::updateArguments(QVariantMap arguments) { if (somethingChanged || pointInA != _pointInA || axisInA != _axisInA || - otherEntityID != _otherEntityID || + otherEntityID != _otherID || pointInB != _pointInB || axisInB != _axisInB || linearLow != _linearLow || @@ -218,7 +218,7 @@ bool ObjectConstraintSlider::updateArguments(QVariantMap arguments) { withWriteLock([&] { _pointInA = pointInA; _axisInA = axisInA; - _otherEntityID = otherEntityID; + _otherID = otherEntityID; _pointInB = pointInB; _axisInB = axisInB; _linearLow = linearLow; @@ -247,7 +247,7 @@ QVariantMap ObjectConstraintSlider::getArguments() { if (_constraint) { arguments["point"] = glmToQMap(_pointInA); arguments["axis"] = glmToQMap(_axisInA); - arguments["otherEntityID"] = _otherEntityID; + arguments["otherEntityID"] = _otherID; arguments["otherPoint"] = glmToQMap(_pointInB); arguments["otherAxis"] = glmToQMap(_axisInB); arguments["linearLow"] = _linearLow; @@ -275,7 +275,7 @@ QByteArray ObjectConstraintSlider::serialize() const { dataStream << _pointInA; dataStream << _axisInA; - dataStream << _otherEntityID; + dataStream << _otherID; dataStream << _pointInB; dataStream << _axisInB; dataStream << _linearLow; @@ -313,7 +313,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 0982c9825d..f41db5c17a 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -26,15 +26,17 @@ ObjectDynamic::~ObjectDynamic() { void ObjectDynamic::remapIDs(QHash& map) { withWriteLock([&]{ - if (!map.contains(_id)) { - map[_id] = QUuid::createUuid(); + if (!_id.isNull()) { + // just force our ID to something new -- action IDs don't go into the map + _id = QUuid::createUuid(); } - _id = map[_id]; - if (!map.contains(_otherID)) { - map[_otherID] = QUuid::createUuid(); + if (!_otherID.isNull()) { + if (!map.contains(_otherID)) { + map.insert(_otherID, QUuid::createUuid()); + } + _otherID = map.value(_otherID); } - _otherID = map[_otherID]; }); } From 1346ce22b92d9d2f7a3c7d31fad2e2f33b6e5e87 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 8 May 2017 15:51:25 -0700 Subject: [PATCH 089/146] don't crash if imported entities make a reference to an unknown entity --- libraries/entities/src/EntityTree.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index a770ea0ec0..c6a270ffb5 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1583,6 +1583,19 @@ 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(); } From de589a32f35fe2784bc96a577322c5e93dc7633e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 8 May 2017 16:36:51 -0700 Subject: [PATCH 090/146] clone spring action into one called tractor and use tractor is most places. this frees us to fix the math in spring so that it's actually a spring --- interface/src/InterfaceDynamicFactory.cpp | 3 + interface/src/avatar/AvatarActionFarGrab.cpp | 6 +- interface/src/avatar/AvatarActionFarGrab.h | 4 +- interface/src/avatar/AvatarActionHold.cpp | 6 +- interface/src/avatar/AvatarActionHold.h | 4 +- .../entities/src/EntityDynamicInterface.cpp | 5 + .../entities/src/EntityDynamicInterface.h | 3 +- libraries/physics/src/ObjectActionTractor.cpp | 378 ++++++++++++++++++ libraries/physics/src/ObjectActionTractor.h | 56 +++ 9 files changed, 454 insertions(+), 11 deletions(-) create mode 100644 libraries/physics/src/ObjectActionTractor.cpp create mode 100644 libraries/physics/src/ObjectActionTractor.h diff --git a/interface/src/InterfaceDynamicFactory.cpp b/interface/src/InterfaceDynamicFactory.cpp index 4b9e3b96ed..e4b3c0b500 100644 --- a/interface/src/InterfaceDynamicFactory.cpp +++ b/interface/src/InterfaceDynamicFactory.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,8 @@ EntityDynamicPointer interfaceDynamicFactory(EntityDynamicType type, const QUuid return std::make_shared(id, ownerEntity); case DYNAMIC_TYPE_SPRING: return std::make_shared(id, ownerEntity); + 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/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/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp index 71b3bda534..c44cf17b6c 100644 --- a/libraries/entities/src/EntityDynamicInterface.cpp +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -105,6 +105,9 @@ EntityDynamicType EntityDynamicInterface::dynamicTypeFromString(QString dynamicT if (normalizedDynamicTypeString == "spring") { return DYNAMIC_TYPE_SPRING; } + if (normalizedDynamicTypeString == "tractor") { + return DYNAMIC_TYPE_TRACTOR; + } if (normalizedDynamicTypeString == "hold") { return DYNAMIC_TYPE_HOLD; } @@ -143,6 +146,8 @@ QString EntityDynamicInterface::dynamicTypeToString(EntityDynamicType dynamicTyp 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 ef20f84da4..89147936bf 100644 --- a/libraries/entities/src/EntityDynamicInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -29,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, @@ -36,7 +37,7 @@ enum EntityDynamicType { DYNAMIC_TYPE_SLIDER = 7000, DYNAMIC_TYPE_BALL_SOCKET = 8000, DYNAMIC_TYPE_CONE_TWIST = 9000, - DYNAMIC_TYPE_MOTOR = 10000 + DYNAMIC_TYPE_MOTOR = 10000, }; diff --git a/libraries/physics/src/ObjectActionTractor.cpp b/libraries/physics/src/ObjectActionTractor.cpp new file mode 100644 index 0000000000..4bb5d850a9 --- /dev/null +++ b/libraries/physics/src/ObjectActionTractor.cpp @@ -0,0 +1,378 @@ +// +// ObjectActionTractor.cpp +// libraries/physics/src +// +// 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 +// + +#include "QVariantGLM.h" + +#include "ObjectActionTractor.h" + +#include "PhysicsLogging.h" + +const float TRACTOR_MAX_SPEED = 10.0f; +const float MAX_TRACTOR_TIMESCALE = 600.0f; // 10 min is a long time + +const uint16_t ObjectActionTractor::tractorVersion = 1; + + +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), + _positionalTargetSet(true), + _rotationalTarget(glm::quat()), + _desiredRotationalTarget(glm::quat()), + _angularTimeScale(FLT_MAX), + _rotationalTargetSet(true) { + #if WANT_DEBUG + qCDebug(physics) << "ObjectActionTractor::ObjectActionTractor"; + #endif +} + +ObjectActionTractor::~ObjectActionTractor() { + #if WANT_DEBUG + qCDebug(physics) << "ObjectActionTractor::~ObjectActionTractor"; + #endif +} + +bool ObjectActionTractor::getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, + glm::vec3& linearVelocity, glm::vec3& angularVelocity, + float& linearTimeScale, float& angularTimeScale) { + SpatiallyNestablePointer other = getOther(); + withReadLock([&]{ + linearTimeScale = _linearTimeScale; + angularTimeScale = _angularTimeScale; + + if (!_otherID.isNull()) { + if (other) { + rotation = _desiredRotationalTarget * other->getRotation(); + position = other->getRotation() * _desiredPositionalTarget + other->getPosition(); + } else { + // we should have an "other" but can't find it, so disable the tractor. + linearTimeScale = FLT_MAX; + angularTimeScale = FLT_MAX; + } + } else { + rotation = _desiredRotationalTarget; + position = _desiredPositionalTarget; + } + linearVelocity = glm::vec3(); + angularVelocity = glm::vec3(); + }); + return true; +} + +bool ObjectActionTractor::prepareForTractorUpdate(btScalar deltaTimeStep) { + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return false; + } + + glm::quat rotation; + glm::vec3 position; + glm::vec3 linearVelocity; + glm::vec3 angularVelocity; + + bool linearValid = false; + int linearTractorCount = 0; + bool angularValid = false; + int angularTractorCount = 0; + + 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, 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 = tractorAction->getTarget(deltaTimeStep, + rotationForAction, positionForAction, + linearVelocityForAction, angularVelocityForAction, + linearTimeScale, angularTimeScale); + if (success) { + if (angularTimeScale < MAX_TRACTOR_TIMESCALE) { + angularValid = true; + angularTractorCount++; + angularVelocity += angularVelocityForAction; + if (tractorAction.get() == this) { + // only use the rotation for this action + rotation = rotationForAction; + } + } + + if (linearTimeScale < MAX_TRACTOR_TIMESCALE) { + linearValid = true; + linearTractorCount++; + position += positionForAction; + linearVelocity += linearVelocityForAction; + } + } + } + + if ((angularValid && angularTractorCount > 0) || (linearValid && linearTractorCount > 0)) { + withWriteLock([&]{ + if (linearValid && linearTractorCount > 0) { + position /= linearTractorCount; + linearVelocity /= linearTractorCount; + _positionalTarget = position; + _linearVelocityTarget = linearVelocity; + _positionalTargetSet = true; + _active = true; + } + if (angularValid && angularTractorCount > 0) { + angularVelocity /= angularTractorCount; + _rotationalTarget = rotation; + _angularVelocityTarget = angularVelocity; + _rotationalTargetSet = true; + _active = true; + } + }); + } + + return linearValid || angularValid; +} + + +void ObjectActionTractor::updateActionWorker(btScalar deltaTimeStep) { + if (!prepareForTractorUpdate(deltaTimeStep)) { + return; + } + + withReadLock([&]{ + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + + void* physicsInfo = ownerEntity->getPhysicsInfo(); + if (!physicsInfo) { + return; + } + ObjectMotionState* motionState = static_cast(physicsInfo); + btRigidBody* rigidBody = motionState->getRigidBody(); + if (!rigidBody) { + qCDebug(physics) << "ObjectActionTractor::updateActionWorker no rigidBody"; + return; + } + + 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, TRACTOR_MAX_SPEED); + targetVelocity = (-speed / offsetLength) * offset; + if (speed > rigidBody->getLinearSleepingThreshold()) { + forceBodyNonStatic(); + rigidBody->activate(); + } + } + // this action is aggresively critically damped and defeats the current velocity + rigidBody->setLinearVelocity(targetVelocity); + } + + if (_angularTimeScale < MAX_TRACTOR_TIMESCALE) { + btVector3 targetVelocity(0.0f, 0.0f, 0.0f); + + btQuaternion bodyRotation = rigidBody->getOrientation(); + auto alignmentDot = bodyRotation.dot(glmToBullet(_rotationalTarget)); + const float ALMOST_ONE = 0.99999f; + if (glm::abs(alignmentDot) < ALMOST_ONE) { + btQuaternion target = glmToBullet(_rotationalTarget); + if (alignmentDot < 0.0f) { + target = -target; + } + // if dQ is the incremental rotation that gets an object from Q0 to Q1 then: + // + // Q1 = dQ * Q0 + // + // solving for dQ gives: + // + // dQ = Q1 * Q0^ + btQuaternion deltaQ = target * bodyRotation.inverse(); + float speed = deltaQ.getAngle() / _angularTimeScale; + targetVelocity = speed * deltaQ.getAxis(); + if (speed > rigidBody->getAngularSleepingThreshold()) { + rigidBody->activate(); + } + } + // this action is aggresively critically damped and defeats the current velocity + rigidBody->setAngularVelocity(targetVelocity); + } + }); +} + +const float MIN_TIMESCALE = 0.1f; + + +bool ObjectActionTractor::updateArguments(QVariantMap arguments) { + glm::vec3 positionalTarget; + float linearTimeScale; + glm::quat rotationalTarget; + float angularTimeScale; + QUuid otherID; + + + bool needUpdate = false; + bool somethingChanged = ObjectDynamic::updateArguments(arguments); + withReadLock([&]{ + // targets are required, tractor-constants are optional + bool ok = true; + positionalTarget = EntityDynamicInterface::extractVec3Argument("tractor action", arguments, "targetPosition", ok, false); + if (!ok) { + positionalTarget = _desiredPositionalTarget; + } + ok = true; + linearTimeScale = EntityDynamicInterface::extractFloatArgument("tractor action", arguments, "linearTimeScale", ok, false); + if (!ok || linearTimeScale <= 0.0f) { + linearTimeScale = _linearTimeScale; + } + + ok = true; + rotationalTarget = EntityDynamicInterface::extractQuatArgument("tractor action", arguments, "targetRotation", ok, false); + if (!ok) { + rotationalTarget = _desiredRotationalTarget; + } + + ok = true; + angularTimeScale = + EntityDynamicInterface::extractFloatArgument("tractor action", arguments, "angularTimeScale", ok, false); + if (!ok) { + angularTimeScale = _angularTimeScale; + } + + ok = true; + otherID = QUuid(EntityDynamicInterface::extractStringArgument("tractor action", + arguments, "otherID", ok, false)); + if (!ok) { + otherID = _otherID; + } + + if (somethingChanged || + positionalTarget != _desiredPositionalTarget || + linearTimeScale != _linearTimeScale || + rotationalTarget != _desiredRotationalTarget || + angularTimeScale != _angularTimeScale || + otherID != _otherID) { + // something changed + needUpdate = true; + } + }); + + if (needUpdate) { + withWriteLock([&] { + _desiredPositionalTarget = positionalTarget; + _linearTimeScale = glm::max(MIN_TIMESCALE, glm::abs(linearTimeScale)); + _desiredRotationalTarget = rotationalTarget; + _angularTimeScale = glm::max(MIN_TIMESCALE, glm::abs(angularTimeScale)); + _otherID = otherID; + _active = true; + + auto ownerEntity = _ownerEntity.lock(); + if (ownerEntity) { + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); + } + }); + activateBody(); + } + + return true; +} + +QVariantMap ObjectActionTractor::getArguments() { + QVariantMap arguments = ObjectDynamic::getArguments(); + withReadLock([&] { + arguments["linearTimeScale"] = _linearTimeScale; + arguments["targetPosition"] = glmToQMap(_desiredPositionalTarget); + + arguments["targetRotation"] = glmToQMap(_desiredRotationalTarget); + arguments["angularTimeScale"] = _angularTimeScale; + + arguments["otherID"] = _otherID; + }); + return arguments; +} + +void ObjectActionTractor::serializeParameters(QDataStream& dataStream) const { + withReadLock([&] { + dataStream << _desiredPositionalTarget; + dataStream << _linearTimeScale; + dataStream << _positionalTargetSet; + dataStream << _desiredRotationalTarget; + dataStream << _angularTimeScale; + dataStream << _rotationalTargetSet; + dataStream << localTimeToServerTime(_expires); + dataStream << _tag; + dataStream << _otherID; + }); +} + +QByteArray ObjectActionTractor::serialize() const { + QByteArray serializedActionArguments; + QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_TRACTOR; + dataStream << getID(); + dataStream << ObjectActionTractor::tractorVersion; + + serializeParameters(dataStream); + + return serializedActionArguments; +} + +void ObjectActionTractor::deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream) { + withWriteLock([&] { + dataStream >> _desiredPositionalTarget; + dataStream >> _linearTimeScale; + dataStream >> _positionalTargetSet; + + dataStream >> _desiredRotationalTarget; + dataStream >> _angularTimeScale; + dataStream >> _rotationalTargetSet; + + quint64 serverExpires; + dataStream >> serverExpires; + _expires = serverTimeToLocalTime(serverExpires); + + dataStream >> _tag; + + dataStream >> _otherID; + + _active = true; + }); +} + +void ObjectActionTractor::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + assert(type == getType()); + + QUuid id; + dataStream >> id; + assert(id == getID()); + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectActionTractor::tractorVersion) { + assert(false); + return; + } + + deserializeParameters(serializedArguments, dataStream); +} diff --git a/libraries/physics/src/ObjectActionTractor.h b/libraries/physics/src/ObjectActionTractor.h new file mode 100644 index 0000000000..c629d84998 --- /dev/null +++ b/libraries/physics/src/ObjectActionTractor.h @@ -0,0 +1,56 @@ +// +// ObjectActionTractor.h +// libraries/physics/src +// +// 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_ObjectActionTractor_h +#define hifi_ObjectActionTractor_h + +#include "ObjectAction.h" + +class ObjectActionTractor : public ObjectAction { +public: + ObjectActionTractor(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectActionTractor(); + + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; + + virtual void updateActionWorker(float deltaTimeStep) override; + + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; + + virtual bool getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, + glm::vec3& linearVelocity, glm::vec3& angularVelocity, + float& linearTimeScale, float& angularTimeScale); + +protected: + static const uint16_t tractorVersion; + + glm::vec3 _positionalTarget; + glm::vec3 _desiredPositionalTarget; + float _linearTimeScale; + bool _positionalTargetSet; + + glm::quat _rotationalTarget; + glm::quat _desiredRotationalTarget; + float _angularTimeScale; + bool _rotationalTargetSet; + + glm::vec3 _linearVelocityTarget; + glm::vec3 _angularVelocityTarget; + + virtual bool prepareForTractorUpdate(btScalar deltaTimeStep); + + void serializeParameters(QDataStream& dataStream) const; + void deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream); +}; + +#endif // hifi_ObjectActionTractor_h From 55056e730a939a0a499adc7a99bd6ca3fae8e815 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 8 May 2017 17:02:37 -0700 Subject: [PATCH 091/146] guard slider and cone-twist constraints against zero-length axisq --- .../physics/src/ObjectConstraintConeTwist.cpp | 26 ++++++++++++++----- .../physics/src/ObjectConstraintSlider.cpp | 26 ++++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/libraries/physics/src/ObjectConstraintConeTwist.cpp b/libraries/physics/src/ObjectConstraintConeTwist.cpp index 57950b4ce0..900e1b894a 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.cpp +++ b/libraries/physics/src/ObjectConstraintConeTwist.cpp @@ -17,12 +17,12 @@ const uint16_t ObjectConstraintConeTwist::constraintVersion = 1; - +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"; @@ -109,11 +109,25 @@ btTypedConstraint* ObjectConstraintConeTwist::getConstraint() { 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(glm::vec3(1.0f, 0.0f, 0.0f), axisInA); + glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), axisInB); btTransform frameInA(glmToBullet(rotA), glmToBullet(pivotInA)); btTransform frameInB(glmToBullet(rotB), glmToBullet(pivotInB)); @@ -127,7 +141,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(glm::vec3(1.0f, 0.0f, 0.0f), axisInA); btTransform frameInA(glmToBullet(rot), glmToBullet(pivotInA)); diff --git a/libraries/physics/src/ObjectConstraintSlider.cpp b/libraries/physics/src/ObjectConstraintSlider.cpp index 6e092b40a0..d8f42a1cbe 100644 --- a/libraries/physics/src/ObjectConstraintSlider.cpp +++ b/libraries/physics/src/ObjectConstraintSlider.cpp @@ -17,12 +17,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) { } @@ -91,11 +91,25 @@ btTypedConstraint* ObjectConstraintSlider::getConstraint() { 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(glm::vec3(1.0f, 0.0f, 0.0f), axisInA); + glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), axisInB); btTransform frameInA(glmToBullet(rotA), glmToBullet(pointInA)); btTransform frameInB(glmToBullet(rotB), glmToBullet(pointInB)); @@ -109,7 +123,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(glm::vec3(1.0f, 0.0f, 0.0f), axisInA); btTransform frameInA(glmToBullet(rot), glmToBullet(pointInA)); From 84aa86b4645ed5437c970af80f6089e49459e7c1 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 8 May 2017 18:07:45 -0700 Subject: [PATCH 092/146] Added animVar support for IK solutionSource. --- .../resources/avatar/avatar-animation.json | 2 + .../animation/src/AnimInverseKinematics.cpp | 9 +++-- .../animation/src/AnimInverseKinematics.h | 7 +++- libraries/animation/src/AnimNodeLoader.cpp | 37 +++++++++++++++++++ libraries/animation/src/Rig.cpp | 2 + 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 9efe3dd29b..eb8403634a 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -49,6 +49,8 @@ "id": "ik", "type": "inverseKinematics", "data": { + "solutionSource": "relaxToUnderPoses", + "solutionSourceVar": "solutionSource", "targets": [ { "jointName": "Hips", diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index bf05d9358c..513d770e64 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -399,6 +399,9 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + // allows solutionSource to be overridden by an animVar + auto solutionSource = animVars.lookup(_solutionSourceVar, (int)_solutionSource); + if (context.getEnableDebugDrawIKConstraints()) { debugDrawConstraints(context); } @@ -414,7 +417,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars PROFILE_RANGE_EX(simulation_animation, "ik/relax", 0xffff00ff, 0); - initRelativePosesFromSolutionSource(underPoses); + initRelativePosesFromSolutionSource((SolutionSource)solutionSource, underPoses); if (!underPoses.empty()) { // Sometimes the underpose itself can violate the constraints. Rather than @@ -1135,8 +1138,8 @@ void AnimInverseKinematics::relaxToPoses(const AnimPoseVec& poses) { } } -void AnimInverseKinematics::initRelativePosesFromSolutionSource(const AnimPoseVec& underPoses) { - switch (_solutionSource) { +void AnimInverseKinematics::initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPoses) { + switch (solutionSource) { default: case SolutionSource::RelaxToUnderPoses: relaxToPoses(underPoses); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 9b7c095e6b..a78662cbeb 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -48,10 +48,12 @@ public: RelaxToLimitCenterPoses, PreviousSolution, UnderPoses, - LimitCenterPoses + 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); @@ -59,7 +61,7 @@ protected: int solveTargetWithCCD(const IKTarget& target, AnimPoseVec& absolutePoses); virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; void debugDrawConstraints(const AnimContext& context) const; - void initRelativePosesFromSolutionSource(const AnimPoseVec& underPose); + void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose); void relaxToPoses(const AnimPoseVec& poses); // for AnimDebugDraw rendering @@ -116,6 +118,7 @@ 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..592667bb72 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -352,6 +352,23 @@ static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { return AnimOverlay::NumBoneSets; } +static const char* solutionSourceStrings[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); @@ -457,6 +474,26 @@ AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QS node->setTargetVars(jointName, positionVar, rotationVar, typeVar); }; + READ_OPTIONAL_STRING(solutionSource, jsonObj); + + if (!solutionSource.isEmpty()) { + qCDebug(animation) << "AJT: REMOVE solutionSource = " << solutionSource; + + 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()) { + qCDebug(animation) << "AJT: REMOVE solutionSourceVar = " << solutionSourceVar; + node->setSolutionSourceVar(solutionSourceVar); + } + return node; } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 2bcd71d5c3..c1ef443684 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1031,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::RelaxToCenterJointLimits); _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); } From 5a4b21c0a9833911c3786e5b3d4195a669615323 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 8 May 2017 18:10:56 -0700 Subject: [PATCH 093/146] Removed debug code --- libraries/animation/src/AnimNodeLoader.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 592667bb72..6e2c070ae5 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -477,8 +477,6 @@ AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QS READ_OPTIONAL_STRING(solutionSource, jsonObj); if (!solutionSource.isEmpty()) { - qCDebug(animation) << "AJT: REMOVE solutionSource = " << solutionSource; - AnimInverseKinematics::SolutionSource solutionSourceType = stringToSolutionSourceEnum(solutionSource); if (solutionSourceType != AnimInverseKinematics::SolutionSource::NumSolutionSources) { node->setSolutionSource(solutionSourceType); @@ -490,7 +488,6 @@ AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QS READ_OPTIONAL_STRING(solutionSourceVar, jsonObj); if (!solutionSourceVar.isEmpty()) { - qCDebug(animation) << "AJT: REMOVE solutionSourceVar = " << solutionSourceVar; node->setSolutionSourceVar(solutionSourceVar); } From 4816e4f82601aa7ae66697e8766a9221c10ef194 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 8 May 2017 21:39:41 -0700 Subject: [PATCH 094/146] what does VS mean when it uses words? --- libraries/entities/src/EntityTree.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index c6a270ffb5..d586fe9d06 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1608,7 +1608,7 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra return EntityItemID(); } if (args->map->contains(oldID)) { - return args->map->value(oldID); + return EntityItemID(args->map->value(oldID)); } else { EntityItemID newID = QUuid::createUuid(); args->map->insert(oldID, newID); From fe69f58174243438224fff7b88d779c1ae051966 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 9 May 2017 09:47:26 -0700 Subject: [PATCH 095/146] Bug fix centerLimit rot for LeftArm, also, lower arms in centerLimit pose Lowering the arms in centerLimit poses will help keep the elbows relaxed on the side of the body. --- libraries/animation/src/AnimInverseKinematics.cpp | 13 +++++++++++++ libraries/animation/src/Rig.cpp | 2 +- libraries/animation/src/SwingTwistConstraint.cpp | 13 +++++++------ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 513d770e64..9c3aaddcfa 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -955,6 +955,19 @@ void AnimInverseKinematics::initLimitCenterPoses() { } _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 = 3.0f * PI / 8.0f; // 67.5 deg + int leftArmIndex = _skeleton->nameToJointIndex("LeftArm"); + const glm::quat armRot = glm::angleAxis(UPPER_ARM_THETA, Vectors::UNIT_X); + if (leftArmIndex >= 0 && leftArmIndex < _limitCenterPoses.size()) { + _limitCenterPoses[leftArmIndex].rot() = _limitCenterPoses[leftArmIndex].rot() * armRot; + } + int rightArmIndex = _skeleton->nameToJointIndex("RightArm"); + if (rightArmIndex >= 0 && rightArmIndex < _limitCenterPoses.size()) { + _limitCenterPoses[rightArmIndex].rot() = _limitCenterPoses[rightArmIndex].rot() * armRot; + } } void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index c1ef443684..f933002c2c 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1031,7 +1031,7 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { _animVars.set("notIsTalking", !params.isTalking); if (params.hipsEnabled) { - _animVars.set("solutionSource", (int)AnimInverseKinematics::SolutionSource::RelaxToCenterJointLimits); + _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)); diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index aba0516b2a..212343d4eb 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -433,15 +433,16 @@ void SwingTwistConstraint::clearHistory() { } glm::quat SwingTwistConstraint::computeCenterRotation() const { + const size_t NUM_TWIST_LIMITS = 2; const size_t NUM_MIN_DOTS = getMinDots().size(); - const size_t NUM_LIMITS = 2 * NUM_MIN_DOTS; std::vector swingLimits; - swingLimits.reserve(NUM_LIMITS); + swingLimits.reserve(NUM_MIN_DOTS); - glm::quat twistLimits[2]; + glm::quat twistLimits[NUM_TWIST_LIMITS]; if (_minTwist != _maxTwist) { - twistLimits[0] = glm::angleAxis(_minTwist, _referenceRotation * Vectors::UNIT_Y); - twistLimits[1] = glm::angleAxis(_maxTwist, _referenceRotation * Vectors::UNIT_Y); + // 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; @@ -450,7 +451,7 @@ glm::quat SwingTwistConstraint::computeCenterRotation() const { 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)); + 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))); From 1c23f3b971bec5610cd158f77e30841d4f8bb9f7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 9 May 2017 10:10:35 -0700 Subject: [PATCH 096/146] bump protocol version --- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 82b4bf703d..2f02848a44 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_BULLET_DYNAMICS; + return VERSION_ENTITIES_DYNAMICS_MOTOR; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 746ae80361..49df4662b7 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -209,6 +209,7 @@ const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; const PacketVersion VERSION_ENTITIES_HINGE_CONSTRAINT = 69; const PacketVersion VERSION_ENTITIES_BULLET_DYNAMICS = 70; +const PacketVersion VERSION_ENTITIES_DYNAMICS_MOTOR = 71; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, From 4f24a7618a9ed9957bebfc29fe3fda31bb34ddac Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 9 May 2017 10:23:04 -0700 Subject: [PATCH 097/146] cleanups --- libraries/entities/src/EntityTree.cpp | 38 +++++++-------------- libraries/physics/src/ObjectActionMotor.cpp | 2 +- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index d586fe9d06..2285e6e4bd 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1531,6 +1531,10 @@ void EntityTree::pruneTree() { QByteArray EntityTree::remapActionDataIDs(QByteArray actionData, QHash& map) { + if (actionData.isEmpty()) { + return actionData; + } + QDataStream serializedActionsStream(actionData); QVector serializedActions; serializedActionsStream >> serializedActions; @@ -1617,11 +1621,9 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra }; entityTreeElement->forEachEntity([&args, &getMapped, &element](EntityItemPointer item) { - - EntityItemID oldID = item->getEntityItemID(); - EntityItemID newID = getMapped(oldID); - + EntityItemID newID = getMapped(item->getEntityItemID()); EntityItemProperties properties = item->getProperties(); + EntityItemID oldParentID = properties.getParentID(); if (oldParentID.isInvalidID()) { // no parent properties.setPosition(properties.getPosition() + args->root); @@ -1635,29 +1637,15 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } } - if (!properties.getXNNeighborID().isInvalidID()) { - properties.setXNNeighborID(getMapped(properties.getXNNeighborID())); - } - if (!properties.getXPNeighborID().isInvalidID()) { - properties.setXPNeighborID(getMapped(properties.getXPNeighborID())); - } - if (!properties.getYNNeighborID().isInvalidID()) { - properties.setYNNeighborID(getMapped(properties.getYNNeighborID())); - } - if (!properties.getYPNeighborID().isInvalidID()) { - properties.setYPNeighborID(getMapped(properties.getYPNeighborID())); - } - if (!properties.getZNNeighborID().isInvalidID()) { - properties.setZNNeighborID(getMapped(properties.getZNNeighborID())); - } - if (!properties.getZPNeighborID().isInvalidID()) { - properties.setZPNeighborID(getMapped(properties.getZPNeighborID())); - } + 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(); - if (!actionData.isEmpty()) { - properties.setActionData(remapActionDataIDs(actionData, *args->map)); - } + properties.setActionData(remapActionDataIDs(actionData, *args->map)); // set creation time to "now" for imported entities properties.setCreated(usecTimestampNow()); diff --git a/libraries/physics/src/ObjectActionMotor.cpp b/libraries/physics/src/ObjectActionMotor.cpp index a3c1537535..b3cc0ccc48 100644 --- a/libraries/physics/src/ObjectActionMotor.cpp +++ b/libraries/physics/src/ObjectActionMotor.cpp @@ -57,7 +57,7 @@ void ObjectActionMotor::updateActionWorker(btScalar deltaTimeStep) { if (_angularTimeScale < MAX_MOTOR_TIMESCALE) { - if (_otherID != QUuid()) { + if (!_otherID.isEmpty()) { if (other) { glm::vec3 otherAngularVelocity = other->getAngularVelocity(); rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget + otherAngularVelocity)); From b285a582bd84c35395f4fe32c86768a8fa75d687 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 9 May 2017 10:37:45 -0700 Subject: [PATCH 098/146] oops --- libraries/physics/src/ObjectActionMotor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/physics/src/ObjectActionMotor.cpp b/libraries/physics/src/ObjectActionMotor.cpp index b3cc0ccc48..eafb43c7a5 100644 --- a/libraries/physics/src/ObjectActionMotor.cpp +++ b/libraries/physics/src/ObjectActionMotor.cpp @@ -57,7 +57,7 @@ void ObjectActionMotor::updateActionWorker(btScalar deltaTimeStep) { if (_angularTimeScale < MAX_MOTOR_TIMESCALE) { - if (!_otherID.isEmpty()) { + if (!_otherID.isNull()) { if (other) { glm::vec3 otherAngularVelocity = other->getAngularVelocity(); rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget + otherAngularVelocity)); From 7b35e8c7fd6141b1e3797387e25ed48adee1d9bf Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 9 May 2017 11:57:41 -0700 Subject: [PATCH 099/146] Bug fix for hands, reduced elbow angle to 60 degrees from horizontal. --- .../animation/src/AnimInverseKinematics.cpp | 29 +++++++++++-------- .../animation/src/AnimInverseKinematics.h | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 9c3aaddcfa..11c14d8899 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -958,7 +958,7 @@ void AnimInverseKinematics::initLimitCenterPoses() { // 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 = 3.0f * PI / 8.0f; // 67.5 deg + 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 < _limitCenterPoses.size()) { @@ -1134,31 +1134,35 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con } } -void AnimInverseKinematics::relaxToPoses(const AnimPoseVec& poses) { +// 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 - const float blend = (1.0f / 60.0f) / (0.25f); int numJoints = (int)_relativePoses.size(); for (int i = 0; i < numJoints; ++i) { - float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), poses[i].rot())); + 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 each pose rotation - _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * poses[i].rot(), blend)); + // 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 underPose rotation - _relativePoses[i].rot() = poses[i].rot(); + // this joint is NOT affected by IK --> slam to underPoses rotation + _relativePoses[i].rot() = underPoses[i].rot(); } - _relativePoses[i].trans() = poses[i].trans(); + _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: - relaxToPoses(underPoses); + blendToPoses(underPoses, underPoses, RELAX_BLEND_FACTOR); break; case SolutionSource::RelaxToLimitCenterPoses: - relaxToPoses(_limitCenterPoses); + blendToPoses(_limitCenterPoses, underPoses, RELAX_BLEND_FACTOR); break; case SolutionSource::PreviousSolution: // do nothing... _relativePoses is already the previous solution @@ -1167,7 +1171,8 @@ void AnimInverseKinematics::initRelativePosesFromSolutionSource(SolutionSource s _relativePoses = underPoses; break; case SolutionSource::LimitCenterPoses: - _relativePoses = _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 a78662cbeb..f73ed95935 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -62,7 +62,7 @@ protected: virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; void debugDrawConstraints(const AnimContext& context) const; void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose); - void relaxToPoses(const AnimPoseVec& poses); + void blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPose, float blendFactor); // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } From 0bcc3c023efdf8fc57bf0846d998f1e4369a61b2 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 9 May 2017 13:07:06 -0700 Subject: [PATCH 100/146] warning fixes --- libraries/animation/src/AnimInverseKinematics.cpp | 10 +++++----- libraries/animation/src/Rig.h | 2 +- libraries/animation/src/SwingTwistConstraint.h | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 11c14d8899..6ece8cdc3d 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -751,7 +751,7 @@ void AnimInverseKinematics::initConstraints() { swungDirections.push_back(glm::vec3(cosf(theta), -0.5f, sinf(theta))); std::vector minDots; - for (int i = 0; i < swungDirections.size(); i++) { + for (size_t i = 0; i < swungDirections.size(); i++) { minDots.push_back(glm::dot(glm::normalize(swungDirections[i]), Vectors::UNIT_Y)); } stConstraint->setSwingLimits(minDots); @@ -961,11 +961,11 @@ void AnimInverseKinematics::initLimitCenterPoses() { 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 < _limitCenterPoses.size()) { + if (leftArmIndex >= 0 && leftArmIndex < (int)_limitCenterPoses.size()) { _limitCenterPoses[leftArmIndex].rot() = _limitCenterPoses[leftArmIndex].rot() * armRot; } int rightArmIndex = _skeleton->nameToJointIndex("RightArm"); - if (rightArmIndex >= 0 && rightArmIndex < _limitCenterPoses.size()) { + if (rightArmIndex >= 0 && rightArmIndex < (int)_limitCenterPoses.size()) { _limitCenterPoses[rightArmIndex].rot() = _limitCenterPoses[rightArmIndex].rot() * armRot; } } @@ -1027,7 +1027,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con AnimPoseVec poses = _skeleton->getRelativeDefaultPoses(); // copy reference rotations into the relative poses - for (int i = 0; i < poses.size(); i++) { + for (int i = 0; i < (int)poses.size(); i++) { const RotationConstraint* constraint = getConstraint(i); if (constraint) { poses[i].rot() = constraint->getReferenceRotation(); @@ -1040,7 +1040,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con mat4 geomToWorldMatrix = context.getRigToWorldMatrix() * context.getGeometryToRigMatrix(); // draw each pose and constraint - for (int i = 0; i < poses.size(); i++) { + 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); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 396e68d633..bee6518557 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -112,7 +112,7 @@ public: void clearJointStates(); void clearJointAnimationPriority(int index); - std::shared_ptr Rig::getAnimInverseKinematicsNode() const; + std::shared_ptr getAnimInverseKinematicsNode() const; void clearIKJointLimitHistory(); void setMaxHipsOffsetLength(float maxLength); diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index ffe9a1d800..a41664d353 100644 --- a/libraries/animation/src/SwingTwistConstraint.h +++ b/libraries/animation/src/SwingTwistConstraint.h @@ -101,8 +101,8 @@ public: virtual glm::quat computeCenterRotation() const override; - const float getMinTwist() const { return _minTwist; } - const float getMaxTwist() const { return _maxTwist; } + float getMinTwist() const { return _minTwist; } + float getMaxTwist() const { return _maxTwist; } private: float handleTwistBoundaryConditions(float twistAngle) const; From d7f195bc42cd48fae022797240c936114cdebd07 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 9 May 2017 13:17:06 -0700 Subject: [PATCH 101/146] warning fix --- libraries/animation/src/AnimNodeLoader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 6e2c070ae5..6bc7342d7f 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -352,7 +352,7 @@ static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { return AnimOverlay::NumBoneSets; } -static const char* solutionSourceStrings[AnimInverseKinematics::SolutionSource::NumSolutionSources] = { +static const char* solutionSourceStrings[(int)AnimInverseKinematics::SolutionSource::NumSolutionSources] = { "relaxToUnderPoses", "relaxToLimitCenterPoses", "previousSolution", From e63dc52ec9a2574926b573866846a70d807447a1 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 9 May 2017 13:59:07 -0700 Subject: [PATCH 102/146] moar warning fixes --- libraries/animation/src/AnimInverseKinematics.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 6ece8cdc3d..92c74b1793 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -1075,9 +1075,6 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con glm::quat minRot = glm::angleAxis(elbowConstraint->getMinAngle(), elbowConstraint->getHingeAxis()); glm::quat maxRot = glm::angleAxis(elbowConstraint->getMaxAngle(), elbowConstraint->getHingeAxis()); - glm::vec3 minYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * minRot * refRot * Vectors::UNIT_Y); - glm::vec3 maxYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * maxRot * refRot * 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))); @@ -1096,9 +1093,6 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con glm::quat minRot = glm::angleAxis(swingTwistConstraint->getMinTwist(), Vectors::UNIT_Y); glm::quat maxRot = glm::angleAxis(swingTwistConstraint->getMaxTwist(), Vectors::UNIT_Y); - glm::vec3 minTwistYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * minRot * refRot * Vectors::UNIT_X); - glm::vec3 maxTwistYAxis = transformVectorFast(geomToWorldMatrix, parentAbsRot * maxRot * refRot * Vectors::UNIT_X); - 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))); From 79d53d92a1e07de9d991f24fbb46bc7c894b8d3b Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 9 May 2017 17:04:29 -0700 Subject: [PATCH 103/146] Use baked default skybox --- .../images/Default-Sky-9-ambient.jpg | Bin 6223 -> 0 bytes .../images/Default-Sky-9-cubemap.jpg | Bin 403009 -> 0 bytes .../images/Default-Sky-9-cubemap.ktx | Bin 0 -> 33554432 bytes interface/src/Application.cpp | 14 ++++++++------ 4 files changed, 8 insertions(+), 6 deletions(-) delete mode 100644 interface/resources/images/Default-Sky-9-ambient.jpg delete mode 100644 interface/resources/images/Default-Sky-9-cubemap.jpg create mode 100644 interface/resources/images/Default-Sky-9-cubemap.ktx 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 8fb383c5e8d31fdb9aafeadddaa921aa6676c578..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6223 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 z?0s)s_8~@H1e50R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=VBmLU;9#}7)c9TfJGS_BR`*re*y`&#&;P>=Fu(u<3^2d|0}L?000Xgs>z;r9 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 z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFfjZEE?c}t90LXzV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~f&ZVq`+jreI1>bY1y_`(_nzHsnkLyaO>aaiN6I@<d?qlthY> zNSTo|v$H#+b+co3*6-}??A`sJxF>)LATlyC0w{E;o8tS!_e5ky0@da%AipmoG8tfi z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg 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 z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbY{9^zhi?>fzyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# 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 zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu=gu7}#9@#AFN@V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz 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 zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMzw#mT%er504y_IAP7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMzcGtjz)!v!XMO(PL+B=pmx_ssBu3gvGwPk>T%WRfFu=e`1H<=cdf!_AB5 zG`vPf`@OpFNw3^2gJS>Lb6&%eI+@OZlRVAV?LcLlCpy?-pF^K<;H#*g@9jN0 zmagqR8l?TzwMN>z{-l|Pd2d`lIF|OVJvx?7&JTw3h2!5Bob2B)KAc|+>hJ}^m@Vk{V@0C{BQpL zkN)pB%FobW++X>h_d9Cd|1$SS`F{Q9`v1Ux^Z$I!|9gIJ&A-p1_5XkWY?$-2>Fasp z&*#z3uaEq{XU;F?zC{U=dCbS<`S}=Yh|Z!=ZkO|$IbT|pFcVWgrat9wjXeS)i!DOO z?}>(0pxC3K*rEjH`hfaIZ7^E>K=+Ew^?_mym%RD@gIo`J))Vsg)BOE4|ND8?3+A5} z^Uudf=`8jriPav>Iif)<_IQcn zNYGjB@vz!sC5k9f-{9FF`QOa^ z@9SAVn0{XVa=kF}&#z~F^3T5C{QG6@|GR%4{pY{$KRNf``DeiV`#f6zpZ)pr7vIk# zAItf9wE6aE*FE#IIX}qfT1XR%ZgN&J>B_Oww5$iLg47BsNC^Y<23P`zhvn!e)(-7`0v)EX1@P zq5V8*b;KA;sN}EL6C?kA{q=gm{CPOqeu(M%8}HA(>F1C6>-$&loA+D))xX|1{rb%R zedhj~zTf}$`%S+O=HDlC-~9XM(Ko%H$M;vh?wPOU&l`{Y>yeM;{cb6LE0p`?`O_Fn zFr$55YM-CON7H_|l&i(Ot+q}3;U0Zcez&msxtQOFUe9Lx=61^AMy_UUdmJRrIEbvy zc$tfGw;AQ~WX_!eqIjh;R(Gn*D;*leI|CLk6YoqIvS9bl8392JZ94TOg`hnW!8p5M~5Z5B8t*$=|7d`ebY_X8g`3I3{t-)LmXlEO= z+Cl4UQL8t#y1@K%tSF%p&+`v&`aGum`{cQvAlKXG&j<7GqxtvWvz{>Jd(-EK=l@rP z{Q2T}|C?SnuV4Oe&lmss@Au4U|IF`+`RDDwyFc3RgZY1duY&> zxM=>oqxYg*op5aw5WU468CZO-4Lucxc~;cB$lV`Yw5SRHY&Iyo_SQ=6}$ z(>gk*&&OSfARhUQa+)g{iYo<*I|UXm6L%V{Uc|`byqy6n<#NjDOeU;8B+DclRwf6E zZw{<}`4Wp?fx*83cHd$THlHG$-KR)Z2%C2y?A`??`LGYc=9Q=7b%a(=*xXq%R~8gk zra)eQ@T?`$U>mN6aHfKNf9ZRy^7#In_A?ZFl1w=NKq|nC!YP0 zKkxo}eeutJ|2O-*{@45ZNBe%5|NG4S(O&=WechaYNAH_|pUmgaM%n*Xf*~>e{(Am? zkM{Xsx^CX@)^oR!!^NCT=g;-NJyJi5Zk$V}F%?}monO<|%Z(U~F(OAS%=A5QHHT}t zoJS*fvmCC?vxkWmSF*%P`C6TWTU@EIxJjCha<<6gmC0DWv!M87!Qz`UidJ0!rLY8*OC0`{!?OETz~NgivHO;e$l+7M*nNssg|G`8 zK7|tV`h#94XzLH|97w#VelV{=82yp=FT}kJwQXL9@aP-+8AGi{SgA%3$Y&vFJ%aCN zD0Vs%A)bri=O}IVIEl3OAg)7bZPR+hXnk${;hz=Fb%y!xK`53miTS*xXT4xvuXxkG z{P{Tg=c&AY`TzU=|9ScSUn@WVzyJ3$|GfQo_vPOQdG0Kq9p~rB<@v#=zxR5c*7CJE z3i*1-wzxL#**5QYYkTF|xYZdW5!cVHPMUk0_q*d^5m;RbCPS^AC$YV5+VA$DAlVMm-*k{{P)eQmJo?~{%o~| zjmTU-E4DDk$m{<|{*HiR=gjNl#=M$8_pRk$Esn8AN@$)Tu9<7|Z~b{Oc1BG-V@`Y9 zrt@m@+?v+M6ZN%mnqSk|G?B+SHQ_uu+b5>IZULWf)0~@AywW6m?Oaf??!bDu0yK4S`paDY)r^zX z64h|}nbp9la`;xm;a4tk`j#;cpE6cAIDJYa4&P!p*jfbN$8dOS^#s)s1;$#0T1Pm& z3*hj|*V(=DBo6O9#;7rJU>A7oZ)i1!D-+yEXCvqu-`~*UXzLN?{SBjzFzN}pe&FXU z_4AhYc!)gfhf$vQG++04enWnq!(308*DpM-D-jTRTsJ*0E8ff6roS)Y(O1GClh0## ztY4V!AN{P~^^~L9|jkcVB6(w9^ zwS}3CJYV+s`8JEtcA0-y5DW}N`=)s)pMWQI~_#v$zY8A?QAGCw^sMI zt?K@^#U~e5Ux6R3l^cw;axG66@|;b1n<;@p0y%BX+{oARoLdPlQ>pXrkV+^aRZxOS z4J;uwY_84nc4#d`3iZS41r4xg;p&TBmif9EHd4>0IVLi~e!+PKj>*hi`!Gv_e9nt|W zswYA;?QjRnTtO`!oPjNH1~zj6jSv~N1Jw{zJJd^D{`CwS_p8%u1+8Y#>Iqgm_*TJC z2?yKXFxDezZNlkW&hUDIY6>%BeZuZt0=svy&gNCj>kFzeG_-c%T?9KjJK=eLLaa5Y zZPXQf9mBJpkk3yjt_;qZ0jo0|HfM&6)-0TwG$E^ZZy`-{x)gc`wtsaHB7; zcZ+M^ylp)H#m|ka=fu^>&Y96!GnKYitwoRgtmo@=SlsCtHFM8u=p3l!sgaIYWv$1tpR1Q29`k~Y;Iivt3;e% zSF`nS%F#Se*Fp)YlZ*+e#h8#f#u8cwOIV|1On5!Ugg0PJcq3zpXo4l2=Gv5}+hB=o zmqoR+98Gze>w+~}(+x{B!>Y0*P397(z>+u>mc(hWCK-GqCmSLr!8dZENk4oe`{5HYK}BtYcf?qeK6pp; z!7IF%@eb=T>4tY$H@q~V-S7(Of|~?)!Yf4P4(@_Gs0*&3PR3kMcm=k?9Y9*Rz-Gvr z;0kCoX)x6lR8#PE373DJ#Hb_qI)uw#TaPf-DU6ze>I-eZLyNq=V0#@tbe5t-;;~-g zT?mP3zr)uUio=u^Vco@t&)FA+Yy72c;;96c`Vvf9_b$QezW=cEd6}9ns+_!_t<|n z??=n|SzrHF_olUV?LQrbbK(l^U7McwGOu@Ay|X1WrxrY;=+NN8Q0Yl0Hq z1WOprx0_)NZ-GMV2If~C!66v+JSOdb2#n`BXib9FCggg8yoY z?9(}m9N4_Hvle{a!d!o_^$qD65OqC6kMi>%L!Zeo?y)-QEQW#V537S4wH9Hu%V>Y! z!hBsm-=VFGYEj(lWPg`KIQ#LBv@MQgiAVl1otNVKue1lN63(jfwtT+IhKH1WoS=*1+qJ9=!eXh&mm#?!@PNwtV!kKXC zd|80DM@{?GD$^Npt*@CW#w0|8^4<)pLv4m27 zZeo=1W>{3(J~rF4*7mL=S}{i5!)7^~MgRwqQ*142(&h2}_D*7Ay?U z+m^K1JYT2J0kNddlTi*Q+IZSRSko6m$ymf#(ig#!z8KaFZcN5vSTZ@@UIJ?tS<3jw z&DZ(G&SP!A*!ir@`mu9h<`+AMij|x{@Qa!W|L7U;i=Ga@sA*Cy zL3M>#Pw=+59x)kyDz-L3>k|(;n5j+k|NFTg{d*Kz_Q=3GM)cXKkUKrBI`Jy~4j1j8lM7daL{>7rh@H%IQ=VR?0iKT9E8;w^79{_ z=QC+Pgg^5^QQQxqw!>F@epHL{^B{Jge2LAwK%%X0*xBAl9&GLc2_L6@6HY(psXaeJ z(arICMLw?~p536fvFw>dAkeNs4ghc-Hfq+ z&F9s$XDy<2ahAK|$ATyc6C~Dn>h~KGCqcyL+LJ)JS{X{pRETJ9P4jAvG2iCn*3?;0 zNZM@JwA`IG2MS4_3nfjJJ`cp2KA+2&4<%y(EEx-7$y}tfW-gK_SxY5W%HNsBm^FJT zlyy1g$RgU$fAGTLPHE6_m7zmf2uE{Ilt(6jDO5L39lt)!!KIq8$FBhi<-r13aTqO z>d%62B+95j57pNU19L1^@|~D3}dVdUThCUUdPbZG+b=Gg4Qf#&cHT?t!D(Z!4=TT z1vayF4QD_zoV13a_O&RUy%?(P@UJtehl8nu-H)CTsfFFYW*BsSvl@24D$bW;rOv^| zE7)_V^z13)TM4@l;m?oQd`k>;uG94QD{Q`HGM^Gi#MjfaC&Kv+YAZfP5?Uh>nXl8| z#28sVY?3_K-bgNNBO3Yrtll{?pFF$~?_B+Ug}s*gBKiFkpZt;e=0jxjEs$9K3S_?2 zFJu(ILOri)`E|5t^)HqQ=d*dc6jqTr-v(Ad5>yG9X}?*|zm$XN-fD)h(XJa4*)Fj}b_%+{ z_N$}mOqq(UYtz~`r_G-&>RGZev14H&aVmAcn)a&Mesz35+oKkvykAXoYJoPF*7mR2 zo;B@TtCFUHSd*u7DKjC`=GBy!MJz|NHSt-n>Uo;h#3?`X+$`m1HBVb;ojiSkYCgtL zz7}%!n2bfRkj%xf(mXqB38T%gDL?CI&Q1AQ$kCRZ<*;aZ+8lFNupF)BY8G?fg2N)HKW4!&=JVP^kG$USi=N8_M9qP}S>$Z^Mb3g>41FI*frclv6K4BAeR9}dFU#dmeb#;A%t_x^iMC7COY5gLkPv0k@ z{SvR>vG5M&k<}nUeQ*c&N?gI+Tu?WhOedTIS8$uE9WI9Vok4AI2DQQ!+{&Ues72Kb zr^-RKhW4yTU?UuX4R8cB>F5loBd}hk*B89ba0Y1g21V_eR3n;e5B6-TAFn&8&DKL` zO++O=yUN?c|2>S6{#M3t*X;fkGOe!BV;Ss{a-lD_`F%G3GKt;4WMtBNmq~lGe6LnL z|2$08@+CdX!~~Q>M(=5`1(uD-oL}jgW|1wZ%A^uDl|8r$w&3bF6H*6}LTk?w`TUo* zEAaX}O`#u`77Gv#C2i`LGPY5USV^8IL*o0BLv z&w!Gw?M;hu7OW|=prp)ZEUB|3`o49lwr{QNSM#W^d#mf;wDv8@SOi9$N7LL|YqOZO z2ujvsX-;j+UZ&IYwz@uU&Ej)uC5PtK%UJ%lW@}?Kzh1={^XgSlbgQA{zooP0QJeDf zTG&XzTD?!Xn)PXn+J);FrD(mZa03h*VJ#MIfKs$kwF$Q3P3*c-vI(}*&5TmAnX#3= z!-S@7WI|Fmj3_u|1AsP2pw++b=(jp6L2G;^-a0~2#t*{5R z!V%O42YZhe^*X=L8QdhX2Q`f>xB;?8*d?axLG_TBud97qPy=jqug7&7^W+F_V9eK) zkOqk@q(Nf7uJvu9jf@h~C<|={u`w;Mg_#-o)Xv(iu#G?oZ-o-x2HQ{~+SPnZIhV6W znDVRIw?%frM(fa#ov;#Gn~vsVG*0Z%-LOUV$fEn8s95f$Hs#=$v9QHZ9v&y5xiZa* z6*|8?L_W60P2}PyL5ZKlb8*5X5QXL9DXO3}2HqMjL8uwf_XV%uXSycC-wVbSEESAt*S<26}KRpD#&TZ^d8~Jz{ ztOA-#OLOY%m0)sK!kW7hO71F&HE)&4YFH(2!%tWX2XIBKTXyRIgCXh7#J6P$t04+WSjEM! z0=E(&B#z_tgi%8T#;lM8#V$u6vkZZ;%Mcv1)MPn=qRBEYW)Xs-xxnZ}2#i`Ri(UvB zuP;373|?;pM$c!Anqw{k1in#oBwFpEN3AZ=V&qKtN6cXSB0TF2y2i%!I30eX@EPz6 zpRUu^MvTbo6JCdCdo4VQb%@sXj_8+#PlP0F0z_Wn6J!zNF&uaJIJm>dj>r|>Cvk`O zNgUz5I%h;5oUGpihsqh=BXNXxkHUB@e-HV7XL$FB91-0TdwACpidysTa1WUf74!2Rkzs_Gr>4vBmU_VjOHLd(3#)V`aA3@r*rgJnV53 zg#HBHXY=8HC?ugDwuDI%d*Wn?Eq)4Y36r6a#3@h`rofgcNSY3jLj7dX4A@ADW)^HJ zyAbWEvl(0J9FuvlrwP*ML1asx&t)tCv8%MclDp&R)*;plKgkW9&_@ge{kHH05V?Pg;!X{rs^t*%&wVAfuZkoeUIj$6fOb%qv$V^>Ln zVpd3kqnAtQdSLW&L(DRefS9F>f6P*eytc6f0nv*j+FFNS)I!ETYN13QSJy;*qvp#Z z=R*=T53;%NHSyE>kyNY9Hj(f5iJT*go(++A)GVD3wIgRpyrO4vQ8VFYX27lDW7FXt zfmielM!UxuJ@pMuhI2&b_nQKzz!g13;*6aliB+by-l4g2J zo~h?o%CibRH^m~gDfiA{*gTlpsdJ@1n+MZeSY=C@4_oSdD5>;3)qL2}7RWLd!b3Xc z;)SqhEQFH27&gke8a>}q-ld$Ytv?=XJhV6^JcP9rOu&?HtIQ<=jP3@ z3M|E&b(Dwod9#p%DGzI`r5`X#*$0f)*CUOWf2gyTZylDFi^XWI{A6TR+o7S{Y_0xO zqEzqXYIZ_byAw+7XRy_N2Bmhl2V319ol?JtYxo?thR>nY?}e?wpfv22#O3Wl>}Ya# zBSsaQvxkYv-p#~j?~-NhgeW@uGel<@qOv~KMQ871#IdN1?XvWb5t%l;=(MefN;O2M zeI$uU-DdI;B2qs>xGIwRDdaLqB+rK)NuG z!j`oHwyfnkb8fX~E{831ITS+oWv|fLS$idH0z0+oJ+oKBPI)(*v*)a4Y`Jeq?76E= z-hy3V%UuJzD1R+Pw)}OBy{S=jR=;*6fhx%~oN|R`Z#jvst@#7nr(Tu+>qn=6z~2^?Sg54qN@_us7_L z82S23*c!fs(#Y8wzl5#nODIiW!Pd0TQ}ceotVf*6Io)?nZ3RZp3BnL2TA;E^{|D zF`2uV*sNU=ZCs1d8J|gFGIui38K0_lB3c!d{wbo;cSs`BcMK^aZ95Z{_OT=~?Gq*< z^%E7ZH>lkhsCNTHhJLqsH#57bCwA2EDfyY>OXleQo{={>WBZRGs(AtQn;P|zk65d8|{P2R}-eLmdRx|$b zt0e)fzY6|wE8!Qn68_X40o@~sUj>nG{7U$dgjEdnN0hJy}lfL%rJN3Rjb92qMay2p{e3XY7`aAvG#QM;zc%#{$?Ggr#8 zRzafXQ7t-h-jdL`gLw-M%A+}JV9$A5MQzHbYjuvib+G3WK1O|!J#RgizX6W?4Y1{} zmr40`1MG#P*#vvhW;j&#;>{9Uk=!?5x0k#pVSCBNVjebfFXdr7DScn?0qml(kDzgs zZ-t|5o1uIgY$o=KZK{vqsQeg8#U~zY6`vS+Sk1?J-ZkZ8t#7N^q52d`wTjJ=KZ7E$ z)$Wo|9u|pnWola+3`!%-m%lJH?1i=d3s@VzFy_pS`=B&_1#1JRP@Zn2octB+P5Ypb z=6x&=Q!Z9SEnh=vIUsBO1{z!2A>MBLhPA10YdZ*=)^Gh5w)Vp#>o@{p?<7azARR~G z=sW^v=TX=@kLes;$BXQtrQFhBRc0-cB3zD=?A=`l%L3HX4NmSY=Cfiv*GWBC7D&=EIB&~;}a7KG8 zB6Qk%h!JUBB%ErGm~040T0gRs4G{S!uN{$J%GzQ1rmTf;s=+UH9emT) zz$cZXws-1K*1{)^^G;hU@k(DS@y=K)ac8X6d1bC+ZLiF=g7t7`tb$WizX97QT>7rv`=7QF{Y(H7VX-(#rlD1Kj7@*#+`^drVm@{z3UBM@iVRybMy z^RMfZEe<4xWJoyXQNW&L! zG<*p=Y5Wp)mUAiZat@YvDff=RG#|FN9+cQx59;h~l$#Gi(R1)Ou(eZeHds3jslJ8M z`7M-o%F*A!*1;(q-!Zn%!^1cV8|CG$V~nlqn8e*!xaOQi=~t(n<~^wfMVXr4${K6@ClDAtV=!<{KpCbIGiI5J`gM z{DUUnAX$`m5J{2)NRpVY=N^D4G4}wM^EDE3zD8m$*^h)A4Iig#>^j{uoNVniMvTw? z%49#{N06Xh%i0GLm-Q9mvcA;CW>QUJpuS-*;??@|6A=BGYyvB8}EUKGQ{I>_Sw=XG~;nV{ zeAC~DAIbQDp*|Z^`~I2l!!L6SqqTiAw=mi`i&^i3_-2pHJNtc!J9`Vfa^9DC=YAmg z2wpiKGTzkA+bVJAe`vB5Zh@=dLkV4T6>NpOa2s5O+w@3nXW=$DC>C+Ny%o;lZEzKD zQ{%^Q37o|r%St|mr1TSvY#W@Svh85Xx5HJw1CH|Tpf;hlu3`t6@=u|0R_uhcQs$`K z$vCS%V;ogGWtF?ZRP6?_RqlelYPZhZchu~Gy_)Q1Y}LDAtJw`ZjSC#LpAW5WFGTjb zy|CBsg+oPciIz|G_!aD;rhRN)Yir!c*qip7@ax)r_NK2HN7L7gz2$3@gRl#n%?IHS z`u5guBw{|Mw!QTb>}+04bK^s>w|{5oI1GE|$ZVZQOpd|Ed=Goq_fWcyLFpbr&-W17 zyN^qh-V?C(p42J5C%L|pu=SmSweKX9zEg~C>}kWe)3A>_1Bx~lseK0a@n;2RV4rYS zqD(jo+k|tBz5l!(sZE>$*Tf63PrL}nBtv@XQAtMWF{GE69YtC(IfAs}Vzl%=NynEP(S-?Mr>r9y=k?oXYH48s?k;Z6`ZyEB#zpzxVo?4tlI}i z-F}m=;jA|}>c3{34F?#lPmzyN6f_=$v+*0an#duznhtQy2jOl$2#O+S%Qv#tLl}vp z^^l?MTR7UklTl6;b8P!zINFa0sBds~9D}pd;OIIcqxI;{qi}W|Gjtz=L*?xLLE`BC zK|^ljEiL>|Qh#X^2N$g{J9(Ig7!%$9^j6Va%gtIz({{@Mo|Dxa$ z>=QZ1#7mr(dnaFlbMhrPCSR7=CtqP4Q?4@3Dc9hfdKJ#8SKyp_8O|wJWYeyKnSKqf z>DOSNc9n5VzvgKMjopBAh9Rr$2T5kx5328xS@u1$N`Fv&kIWKsTtctUq-(>-Dn5ox zK}PZSOnTAxsv}7Ea1`mPjN)S|K9*T{6j?Wk6}kBs6N(gf-GN03o)6d46aSeyE? z!$=?2cSy_Qw0klNwEMMd-y)6SeYqB)+Jx#6UZYTrqrION&9zQ)-nW87NXq?=NzOYw zBJDnk$$8{3Qu2=o@5SFYG4GHp_Yg!0xra=?MS>tc_mIgqh#!)AU*18)=N?2{?g1t? z@1P_m?|{jE#4ukYhU6baOg_c05mT^F?b8?+m3KfFnfo;&@(hvr2h{$4L=>p<_d^t( zw@>cp?}JExo%;H)f-fcE1z#bo;46fx!V33ELW;hU1Q&lL2`c^yfkhm(LrV4`xOg7| zX?!Rp`#^$9_pA0Huyh}S%Jw6$TozEaAO2Gt~&%T z6}8>9xaz-S+zp5S6j#GxS<_+2j=&{wHXnhj`KXTiB3H{%PpwDcYCQ&b z>oGXnWRA9DjI;e1obBJk-hK>@_V0Dhjvu(rAK)Tg$92xG<8XBTFeJLBVXx^v4p;XH zPhEV>*?od>^qep`1xK&J*>}q1G@N8CIRn?&(l=i&}nlPB;h0__J`0KL^)@ zb8xGi{TJXM6UccuCtQ&AUxahw2qs;E$TjIA+>zluK&vrTojerd^Y` zr(c&ire9|{R?D-F8P_GwnK$5^bpx(hHyLx^HR~2P`xe}@Z^JeB&WN1zZ|htO?!Y0@^2BUqIOv2 zVT4wGhmc_eRei@*ehU&@eVD5`jNqCh2&q1TU_lVIYmPG1*63q3#}HV{1+aJ&fwe~w zKloTaUxL z^#tS7M)4%P+lJ@WerjYLCm`uO4UxO+G`zab42!Odyt>c8P1JE$_n9HNd(JX0ioFI` z?^(F|&cfYyj&ZRZdj{^YXNTk(cYa9j@fR4^_zR5IcTc#;xcV=`HSrR>`el6lBHR-% zag#1Vb{X!;m*F+}ii+9>_ms=BsaGMHb``{H`Zc(xUxnAS>)iC~@Sc8yp|-|78@5atS^H@3B|lJ?;v8#$9E+$6e*dUxm;3Yw($HRpLG2s>G-Nn#6nJ zHQD5=5P40yGBou*mS?A20r8%46<$-XN!(L-e$~dkr(M_iOuxa$g)z#pUNbniIhV4$ zO8NB$d}iHbyl36uX5RpJ6W((upWcEGi_{m)y#?8AcniGdQLeqsqW8Qz@ScB{TW}ZN z3+};Z!Ci^>qI(kGMfc%H7C#u0&*J;slKb#o@&Ga)-kR~z@~?^AUeqSC6#NRy6jh!cQc1-rNpblpNlC>SNpbmUlvJE%(R?rKmz{y6{4DDim7QUW&IiocgSo?mjB(d!YdE-606$MxFh1agbXDT#5N*Gaj>CuOCl zkz0BSIVEN%kzMR5r{tt0v-q^E^bANw$!T3?>1j4b<5^`ROD{Q#jM8(u^zw76Ge{rS zS!7h4W!FgBtvZL)YE{)aq*PyElB>=$Ni`QF$u$=wiM1C@E+Vn+ zBI0!y5HE8weeDQx`JsCW6P`MDT=L9)c#^L=cVlliLXDzaa_izaa^jc!LR? zcmsixZX#f^EO7EoiT{*alE5iM+A=`Wc@S4hUW-Xv2n4#@i|E7rWZ``rWf4imk4175Tbc!0HL};gsNzqUmHZo zd$Q2?Ug^TNyz=z^Ymo2{eqt)?&q=E4&!MXREGp~%r22EHs54a6o|9D8o|9D6oD-Zy zWz9Jg8XF0}zoPo=pHp=fX6039bd_|y@(d~}xU!1Vsxv6B^rYRV#q#nqC@T|`pTizt7A0l8W+^TeY8DzVKZ~M@b0{n~ z6qTJtQTaJtN!d9R(^%PA6qKDsVZ~V#l%FxEwg1m~e zs&mM%Bo~;xstY5^ufE9SR$WGJ)g@$;>PyJ2z9`A5xhTo5xrprAOUSOfgsl2Y$f&!B zOo}QRBQ(aw8?Hc<*?3iO4e580ZR$r& zxP!?4JBUzY_d=%$~ct^xnHnM&CUqz3(2< z1gT^1$;RJD^0<3QQ6-PR$4$5oP0ED(Na=ro`FUPb+9h+h2+F{_^;`mN_o%v;Zy=(nGnyg;;O%?re=`O$=~KSvaK z>lvcfJVW%_=ZIeS0x?4vZd|)JYW;IWZFr7o-3vr-c!8LWFI7f6cGFA5Y<_{5%@nz~ zcLyc0?+zf2QOCCoLKOc#^#|A(wPQbcrHlLUmFhL(80rs7;y-#NN!U7w_^pF1CTyel z6B4&+en#RaKS`24`9-$M7tvaOVOVYT7e`lj0fvic6SUS|kQn!guepG>+Vg0wJ+EJHt)thSM_cVhPJK@r zlgaPjT5}#Pg0`BAlGf@AdY{^6&DH18LTWB>)fb_wx`1Z0>I;xmT|kqH-Rp7vqJEv- zSA7wp#_EeEm(Zwcs=3TG)?7wI^(EBTTw;;hG)C%cubNyzy{e&(uF*ZBOQ;`7{S}Dn z>aQ}j4OdaaTtkgXZR0i6HeP3H8gDYyjWzRJYznP0J0`w2_;rYP*5z z)|;pz)NjAVRJPwjWydX4blgID`%M<9&H5d8z;xaLDeJn+ly~1ndG|e(bSjlItlj=ird8h0N>;~t=BJXbj30SfycvRE+T0rE-z z0~GZ0m^bkOH|ZhrCO=|wCqG2~l*h}=a*RwwR*}(cV_7k#q{fx}r zKOt-P&)lA$k+thrWbXQv#jM@GA!ql`$l3i9lfCOFWPS0oE_?4U$oWFX#=iUohM$ll z$o=YPN#4HSRKFvS`4xGB{QbY7;A`?b3ikho{QbYGeRcfc9}pED_+9k}3J=h=-$(Y% z?+HFVd>I_s`6 z9d%dHQE%vMxQ5P#>ynQ88|WkrH<^yc>m#^=z zn`m#jiH=sTt@S3?b`!1bx6s@sYwfs=){Z-9>m+y4&f*=kb=^U$s-^o5)7--&^_zOQ z#-4jz?>#h;zPn8G*n3RV*awDj)PBe`j(dp4@ek2B?t!F!`~%cac!)aE{}8nkWpxuD zF}0H(sUDz~On!j6$q$*@DGyOQ^$}{PK1R(nu6p`ore?+yRL^*Vs+n9h#aU08N;Ou^ zeu_#}#hj;1<=h_)^PZt%{xei8c#g{X&sn>C{xg&>c+SQu7QR5mq8BI^EqMvu(gBn& zBZH_|K7jHS11M9Kt{9M%tQ_E04S-oah+?v8fXCG@P`vsjO5Pek>DvQ(q&6vD^8zJn zUZP~JN^O^}9YE>20hFvCM9F%JT+#Xg6mJ-0s9muBB?{LMplHJ&iZ%|SXwxf6;iduE z<^dFK9%OiXfW^Xh2T=I#Ad237#l80$%$C;>6>J$qp`hS{0jBW7L8fTiYf0g@K@^dX zUonN-24q{QZ`@P3{gtlZlR*@2Hyc3F_CXZw807KOSCG9%(WkFbywg+3XMBvJrg+zD zrfAn|Zuct`?S75oJ>(}8fBu>&`TQr8eDM=X_L84b`sFX&zF$%L)h{S9p)tBv^3|^_ zmhS&e^)pKM|AMlw$*(9o@QbAU;IF!hZ+=6?pgj5B-jEx-O{v z_BT|0`#Y+>`*W%f|1LZ72V}pa=I9@SzoC{K`@8B7)Jo`@q3-*?jqLc}KKm6gL z{s(@&;rQRt-*#Db2@_g9O>Da=>2JG){?<#FNLnsoQp*(yU6Wlwf9n-YXd#!;-+UDl z1QVOD$eONTB>jz-(ceUdzHd^~)gfHLq{b_l)Nlp;4VNVo8!pT0FM~{K;LO)2HC$yT z)?a0``;5r18~0toL~LenkuYnpGPzvZUt z7WxU@-$HI-QtNHCeG`*f$t_H5zm17)x6rSeNMo(HF{zE*!G!jkhK^ep*L4fyncEoO zb({7181Ikox{L8$cQCf=4#sug#n_&^7}t9bV|Dj2u21s-y?yu5HeJN`a;$3I~DCOpL036Gd@{g0Wx{zn+w{}^K@KE}9-PtZ5{A$liqJ(C_{+{DM| zoAiX6`~*EyL{HH@?Gd`CKSuYAC+M2-1U=NBK^~)P=415Cd@AXl`2^jwo}hd7Q*_UH zik`VVcFlc?u6aMAYaYd?=$fxu@C>AL!83F$e2xw^b}oF*bS!#~j>Rv~vE&8XnU`o^ z@)B*!xQ?YS(7x;i+Lym%I+j02$BGx|Sjn}nc!{=^FVVVk0Bx%V(7Jk%XtCT|{VO!98rKh?X~Q6zHx8nC z(M8nossNePq4IjToJ=tdTY0SeXufV-V-um6_Fi1+$vSRT+(o8m~h%yXl5xPSZ`^Z@dBS8fKFw z6^%FDVE3_mo3BGOr|CM^d>xv(EjJjgFUHv|H<&rCH@Fra=eFKtW;frG{9ktN`%jLm z&i_4ARo%*?$x0I>%_s*;a?aV7vz2qSa*ob8flXSXfd!Ujm;HX11r`G~Q3k_0U}ec> z@4f%d=RVGe&B7{^3Wg3dH4^q`5`}>9{#;- zw)ul>HhJWaaUT7nY`!vgFUQWyzmpvzI^1mSykQY+e42Y+e2r*}UQ%* z<7$)tvDvWZ9oewXm+5Lg6-Ti^A+5I=0)qDOXt4#KOD638Ooh)bnNm+g1l+D@$r)<_7JSA%no{}|( zPRg3YCuOzCkyEnj=qXu!?6j;pep*%@J0)w5d?;&os`wbPRhzBKeSo()Ja+W z)G1l{)Ja+O^eI{S%t=}C%t=}G%t?ED<+G<`<#VTG`Ew_2Ry=>oX65szWaaaxWySNS zW%*}M%kmdadRg}3Nm>5VsZ>tNvX@WFvR6*Y($AfevP1Ps_5;pO)pX z>YSA2Hh+_)ubz};ubnDo>8l^gGJBu7|D-JY;z?QdrIWJMo>h z|K+ToAD)q=KRlh}j4b<+pXL8{wv-k2_8D28%F2H~Co6xP-e%=bF38INx*)6m>%7hC z|GFTne|kYy>HO@1#JMP|e|}L`{Va~Tz53@LCCj+Z-uK@Z#AJ=jFD{D9+SKjzedd0b z)xWqH=Z%Z9=8ca^_q+OaZ+uiS>wo!i?|I{6Z@cxs{3waP{)S(Eoa7_f@XL>6-7hct zdGMDK=M7o+;2W}DWy6EN^s@1xU&$tu2Y)4-ANZAQdf-hJbKkF%nDu^L&I7-e&G-LC za!TUwq#a_rK-m zzTZil-^$i|e=ASi|E8C1_x(=gH?l3Adw(O_@B6LIj{ANqJMQ~klK8&u_x@gX+*i)_ zd)2!4|3OV}?*D`Ay8n+hyYK&_>|T6Ac0KTWFS{3;`^z!w zyLI<0J|TM^_?_%|@b~3B^tPDnwfU3mdFVuvw`K3cZ_Dn7-O|2wj8={vH2 z*1>UYFt-@AC|3&t#OS1m2vVZ+w z<>2~v<$%e$|B18yT{*bny*L}+m;IaGlYN^_-j{t_-k1HGKac~d9NhZ8&7rOD+w9-= zzRkYv@5|op@5_NF-jf4Qye|i~y)Os0e;@~TzHf7I*9SHScD*nAO?H1E`}TYw`*;6M z_L=PYo9x^Bq3qxLL7aVmlLIFEKa~CZKa_n3K9qe2Ka~9kPsTZPQuZG@CHoJbl6@ve zPRrh-r{%zrld}J4oPEbm%Kl@gWdHHgvhT?_d!ITj`<^}{d!LE3=a~;>&$B0G&vPcH zWZ$!=Wbd;l<2-Xx_L=*hH#sSLUpOiIKKr5Uec?md`{GI2`_f6-_wp$(dz13sS5DdN z`TQv_yIwtMv-{Ok<-B%EcD;65c75Tr?EK3LmD950 zt7l}#SIzR2?D*6Q4by{}3enz&x9xr!% z`%IFvvcrpi|Mu^mmF?d>Cp-Q*&i3z~k!{~OW0$t>duL?Z_s+-@Nw$6eoNW8SS=sh4 zXJy+D&&t*xoslPO&dRobJ1g7%{j5A;^5e6z^~Yv;u9R&*J}2A$xXXEcBolP76XtR0!A7%6UKgj0wzgPLAY}s(a#;mh-{qJRq$-3X!Y+d(z zn=Na9m*j+ONoDJr6PJ`H*1m1CZOz-2vpTNdvF2@?ovYujnC+`i*p${^bwa!(+wWL) z!e;xb6JB<$dRyg$>`GbK*pdRum{I$`fObuMO|eW~)^ zm48x+w=wJ3byuH|eXGrFll1NQzWuBI9B1X9#N@!rx8=a9Kgq$>f3`Wa`p2({r||Z z4JzyZBF8qAb7bRR<81n?#Q7gNviV&(s$%Xtyyaavw&`6tw)s6dn#9~bzWIGQz9r7F zt?$dRtsltoCz5RYn;d)MgHn!ff6wO09q-#5+wno1oqrRPqdVV|qr2XhV73r{&2fRh~XAHqV@v z<0?-+dq$2wcSepqXO?H>`15Dw_-D_^u@{oOct(!D5a-y7XKkK*@vIzw>6{#Y`J5ax zdFiYifBCE&d*z%Q|J*q_`pOxbW3QZ*W1l-K$3J&gj(z^D9Dnt!9DVJq9DD7I9Q(o< zIrhb~a_mcI<*3TBFP)QPUp^UXFeJyd3@d zc{%cp^K$f?7v%6a&&rW+owGUm`guA0`Z+oL`dK;r`WZR=`e`}(t+R6U^;EudMvi{F zoMYcUD@VT*=g4=@$+7RAlcV1~CrAGIoE-h<^K#^S=jF)v%Q^gm^Ii`9%lRZ1E=WB!~a|qvZBQd;7?Lep0k;@`gt!stDfsK-7BAyo|P|n>0bGQ7r*{R=~?yJis@eYyqBI8&wJ@! z@tl`w%b)Wyefcvs)0RIY)0RCgJn`PXc6;@Fsx0>`f67bV_EVQ0_cCqSlV0++*X!q%yO%%d z3+@BRZn~AUj0mMS@W!z^sIeu zKr(ghb6%#ee_G`knVw4b`lqC4gUQo2-5Z`#d0M(n-Nq!zdZnei-z_&iBk4@r{G6Ab zEzf)D-tw%Mo-LnMc|m$onZES}FS9m(*3Xs~#m@_IdFzW}^Tf;I=Ovky%FHKTshn*u zi=UTdW-7C`zv5@x=OvxlJ3goKd6}KctR1g)X{hh;Gj_aqsqd6@#*XRm(%3Mrl!mcw(l}#c34M`fuOpvDf ziPC5?W`dvki88i+f|rK+3Gupan}&9&n6Z<@&qQfVrMa=aVj3FTrO~9RBT1JuHg(2n z?3AYFE-#I?-mDX+sd;h|U5@V`+uY@)xv@){o8q)InRV3un>(da#*zILS+M(|9jUjpMvD zH;(ht)X?Uqu}#uxY;N<@&?5?O zYUK(4?XI3ex_Nm2ucT;71TYsM` z?JMh}p|w+`LmDc^_oF>k-q!5fe?0wqvfJzR_3N!%9^2MYF%7MgdQaOV=`H5-cE?Ej z+3WAuxOPdW@@Lzgd2aSIHlMRg=JWJFfBpLO`>X47%Pk#}PTuFCA5WLfzRLSBpJlq- z%V(n=zrMA-VzfVfylQOe^3v2|j=AMzzQ3tDb31c>(9$km%xg$T*Q>IA{SD(L`ccQ1 zrnZS*8rmj$(a&GkH?>amqh4RLX~Z{V&%K zbW7K7bjwWtMt>ibH~sw5eJ6W6)jpfs>2o)G+kG$lTDo&((~q#{vgVvQ9e00xUY%~A znw3>+w{-p+J2va+^tJ!q9>4tj;&b}+To`h1_0_rJj$U((5GpX$x`(HwV^G&Fa5DffTkeo&L?qov}; zIoz_Lxyw&er=(-XO#F07V=C!&d%Hchxid~vr_?tkx7~fcE<0pw#k9-VRK~Qlt8~bi z3aM}Dten&ZMn^?c*+R{6V>zrF8WZ?FAk_vddv;PuVr$1~3@@A0zF-yg3_kKNhZ z?z3=9{rXh@F1J6S&wthN#2t6~Ui!y7ef;ZtySUGndHl@lQT<{4cuD`Tw4GXCw};us z)93&Gcx3MSYP?iGzwGv9ALcph{)X-k=9SIyJsq!aruqVz_rlbx<99>zq~sjm)pbdO z7yo#l+5S^jKen->Vj3DdDkg9Hfv(RyKmGYB-@3lOsl!XUt>w1g_vr2YsBfC&CEJ(r z_o3Gx>Sg_!^PaC>Kl^^@{+|E-WViS0OYMX@4i8jWKcCtkN4HP9?6)tv|CKFQyS{!r zUH13a|F7TQf9>z*<=&ru|8!YDzr1?Ae&uqueQ7VT?>?IMGYq{;IB5$JosC$$9YFF}X__cr)bsP|^tzW) z&g@V1v2^`Fm-XZ4mG$$JUbk<$UYGUj(e?dZ){mE4R{f=Z zFRSDCP%Dpa9Oq@IJ-<);^SRugk8a;G-w)+Gv%adYwVY``b$hDIx_#F5wU+h%^UC`1 zs$E}iugmKEG57eV+8e!nzn2>(NmWs=r`B@D??4~_bM~)aPu}|Z^^;^wr8G>Gbn-rn z^t$GD=J_f=L%lq@VS<Z3y!8jVzSj1X?T6NQQ*M7CufNdG?PdS(Z-O0%Fk=yy zmTa5T|IexX82#NI==MgJ_2cRK(PPK^QSDczUavpM+5>Gy)IX~zTW=RRUYX6ORf8- z|Kj&w`nzq^?NjaL@iL~WR2~n{KOU?0^AlbkRo^ziX_ZlA(B1>xzE@k;pOxNT*Y|(9 zSw>aJ=&>y-s{fR!*X!%D-d>lhUEkln*8Wwtf7dFbYD!*zq}u)K{nfgCwfocSS68mJ zKQYkT>;8&;2=)Mj4TcZi_EBr`FqYNL_;AP<76McTJ%f63+ ze?CL)@$~!Qm&?wttbV{h`n=QU7v^@g`^jvdIsee-D^;GK==nIR{nh=Ckz-pbCjVNF zYTt9q)%7p*`>Xq7x~$h9s%6)%4EFOIHM+&i$kEMJrb$MoGGcU7Zbpu2wz-sb^VS(Y z%Cr&X==I%m8!@WU#+CbCHp<8(BStlpZl~`v_q&WV>-3RPqZ>2nYh8E$h>>F}X4uHF z6_dBUdmOjSTR*Q}KcBqyb$gyy*89&}|8m#s{=h&#AN_c`tRG+3Q?A^<(*6JZa(rxa z$GH6U_4z>ceTczsGb%lvZh!O3mFLgsxiys@`yZ>*pO-G{&qLSia^Cv7-drE_`_TPa z9zLq6Vn&W^@-k{vb9GT|oc5k~e91dLRa0-;3cbE-=Pr3UvPnjaXzVk3Kf1kh_4@u& zxx&6}-2k`$dHb)nUboMA+w1iySMJYg&!4t(-tj}%>-R_34|sV@gLuj5Uu5fbTj2Lk z^zpU&a<={P+s(Z8EN}a2>-Bt<%4OX)`p>=E$Ifqi^?aOBqgwhoIqj`#Uo&OBect*x z^{)N8itzN>({T#{&TCmuin-jHhi?75%n^d(ftkm9CdxI&rR>I zzst4uCu-eZ_doRhbp7Qn>-JecA6@@RD~}r0)UO#is?m>Xk9B+Q+TDM2nb$sL+B@Bz z>2kI0V}Gx&pHEIXzx}ASy|}#F>-J{w%c^}E@Uq|E82HBfy?);QYp<{N`=|S0dE4vtbvbW)zh3u`hMQ}MspxuL*6pwEKjfA5_PVUM*Y%gX ztos-G`RMvu%l~M*+vj#88^n))J$&SdCNCpKnCrv+nZBhbd5o6%BCMQ@*9*55zBoZmn3+jsr_@9%P9_!uwMj*sZ}eW1&_ zeZJDlnf5NP{p{y@?I(BtIqk8o*ZbGyYS-7>4|Z9%$F-M-R(q+BXZgoR+Sf|&QK2}} zOQm(G8+c0t?OX3ZudF}wy!CZ`UjIQKpYqnv*6aR9ZQ~Pp$LqZQ#z6Nc^!{t@Pni8x z+DC4C+t2O2-e2D5apl$fKK1uSe;=~-wYA6TbA?O0?H}{>^U=qLpHt$KVG39FWX=6kH2~KBSzMHDUKMUQh7dVUVmYr>vex2 zudMq5`utayhkE;)ZBMnAYRk2?zuISimoNACUqAj}AD=&Mk2l!u@lbobPuu6MU$6eY zecH?YeLU6s%j+*>`v-abg}nOO`WM;u+3)|j{)fA+;>vEB-CmEk^lv%8z0PZYb$hPY z&#Tw#4{Uj`zpwiEaiy1sT6@Iv8Qk%KEB*Z*?C+m`zx3nl<56C{UVo^Sb^ATg<-&;3 zm80e_==EK@+h5CU``vr{-`C@1x38|f*B%D?{jYWZdCynZ>+gpy>-DQG=j~6|Uuk81 zJfd7}`#DS^FM4}jU%4DcMKAuoD{uShTvxx}L#sUS$AeK)@KX7DQnkmw%e{Uye3Td6 zzUcN_my5$kd(qqXcR7E2!XHo2?Yr&=^!M|tc7K=q^Vi=W{d{!&m0td|wZCCBe28V3 zgkJKVpWj|*UyrV*obKc3K1;gJ+}8Ek@^FdzHAAcaQ0?(RAOG^U&s$&D*IL%cZ(Y{= z4~LEPqSx2`gS@idUYB+Mq1yH`Z~emX(O&fW`uc^gueGdS??9LJ=Q-5NdEb{SuU;P? z{o`A~dtz zAyWy%f|qoixgCa)iod?zeyEqTug`xyy1v@7e*Jp;zS_n9yT2e|zoxSHAS`(4zt_?G zNWhgvz$OI~}IxBXz(XP&ak`%$y!-0yeZ_y6+N*WRDV8^5TnzmYwj z`p3&!kMFg%hr0dM`_uK+mUaI??|-P3YyCVf_x5?-^6RS|AFp=(K|Wr5+S|J@6hHp? z@~7|p8QAwD2%?G^a_{$Ge;h1H&L+*I^iEAHgYwxOU&-#0Py+8f=LTYoQq|CReonf5$q{kkCXlDB?dedc&p^Z8PB^D#27Kj-m>e*d7l_At}lMk6F0 z-5zGvuln(-ZNK&UemU>+8ti)Qx!Q8x_hsnU>*HCq<(%V{Y7h1MQCAmw$+RE3J;^Hv zf%vIBUaX&AwygI@*?)g)eLrgbeAM=NK6Jg>UVGGD_4>N3*UyfJ^zZj8`*@YVkC%J> zs*kTZ?fd1fzuf%`{ranK-z)b&^!|1KB3mBV@$>BSsqOK!$IJcw81%gie*VpC2_d2-a@%?{%RL3)Y zJj$!jtRG6?MeX0KGwbSlzg$aO()+gl_<}ya%P-Tu_x`^2*ZWfI=cBi;_Idf+=lRUm zYyW<^t}gPD`TpqdlP;_8N3G?-KVA;)@rb-V?+x3Bhmsq*^S_EaB_{o}1(|1+Nnz0`dssxDPNo__pnSwDZu+4s1X_oT9i z?B|iQevTLI>ocDTyj1;oIj=o`{GsyqssF$4et)~#{pTMat2-VJ_Wtzit@-hD&R?&z zdfgsZTdw*29B@Cn{mv`$V|pe|mk& zd2I~!{b?l6=fKuyzyH;KpQ~Np|NdvjAJlk6W;{c0pHm+ASJBC$D>-eul4ct<6qh3&wS=Hq{4gf$?Ka` z>%Z3Z_4lLN{rTHx$0JnxU*qvA|9Ge$uX4HOXz-+Uu?H`Bv>sx9@pn zbv-Y?tUdeffBx|>@B8h4pR@ggYPZ+>yS(K}ZAo0`kJsj}Py5V!bm<22zn_=)`_cd3 zcYVC5{Q9!}1AV;HWj!Avkh)Z|$5Xw1|CVcQZ`i(n$5*Ouziam3x5rt}mFx4559<4r zb3HOsueZ;XD}O)ppWonapKW_|`>o4)$Lea=&)XmM+0W?H{GMoEy6nFJ-HNIW_^ViqIb$egAzS{RY+rH&oFVFQ+x&6-i^Sx!C zkNSR8T^`)_w%YGM$A15K&ildpF_a8c`+Xk#^=myo`rBuZ@6{g9_4@wtU61$b`3(B_ zo_#)Qe>vsq+h5(@R$H#zKIcE5%D->Z?_b{QufAS?e!A?x|N8T~{N>Br-Vdc?7te*Z z6a#~6VDQ_2eZ2GA^T9qI=bhhW>npb})wf@M`;@nRetTS7`=9stRoAQQ<>_lV=63da zj(`0pTd&_Q|MSRt?r&e(@8Fg>J`V2jk@uVT8v~baAbb2B+~Z^Q{RjR0bbH~~XZr)X zJ<4mp^6K^e`nRn654wG>x_!@met!F$egCSf&wRdl*UK}XXXVd#@cn*9>QdoYbLqz# z-YecK3|v_T(#N{^w%@)E{_$5ozG@$;Y+q_^-}2kj{_PLw>lxMe2dZno_3O)TpVjA) zDeKSg@|HOsU0KH?J}*8m3|u(|E`R?(w=a3^Q{{Ty9#-Ff$mtJc#^2R^x!m^K-+#9K zt^E21etrFU^lzDC!)-Y{ z`+WWV>;LOa+u%0G3JhQX0~qMbKwkUO|Mdgg-s=89wbwVY=ktDh+yC!Jb^FhLj+K9p zn_Zvp0`Cb1Fn|FJWDQ*I_AUGPmHP|T{~tqj?QQn?)V4k5vgUYCcuz2Z0SsLE2Ku|b ztbTu$`wP|Yue$ag!&kmxK0`i33}65Q{cm8P+u!~kzsErT8{j?VJ;eY9Fo1zP0|VV( z$a{S1F^T~UU;qOc7oG$U*aFB*Uuj}gX4zxnfDn37{I{58qn>VF8?EK z=A~~Fg{>;O{n!12yt3ZjFW26Gz~jJroRbSTQExOEiw)*cZ?-}nI1~70X8qml8YRlO+yi%K#-(KtX&oAe- zx4HGjb}xFdCPWHVM;F{fYekga7{M^(%cAgHxC1 z=}P?0@SHi;V*mpKVL%@*t7~5e`k0<=zbm&tnf54bk#vHv#fxtLg0Qv9jFTW0J>DM< z@3aYqb@ZCB*r5{l7hJy~zrW!3C!)dxFPZ0~#vi=CKyq!uU869|OTJFwu2RPQoGuA$IwdeECQ+Uw={ezM!)YW?c-U;=Y3GGxWAhu0z#oZTbqPpWx#94yC2quJ7>&^I70f!sVyJYo*lz4otG!)>3Ugz zmAKT6nJ;1eJPF3kEhQW?S7o*YqfKVT8EN_o>BRkp-ue&0i0NM6!So}_;|}roL%RPE z3^V-&H_i|*>xOlD3G8cbm%8GlUgPyA%zKp?4@vH`?{D^VGM`;Kdi=rl)5^;>2~x@U z(7vKp38L~fpY*jLdpl~Dbh5tn_aWQANPnlw{e;Tj!@*lG@A#2dPkZ#qZjY|C&o|F& zUj0z{UjAdxBYmuiZ|mbzb!B}%t&g9&J_y^qgytMO6@C7#%X&UrrkvY$`{V6FWXk0> zJKIi~wkw?usY@l(UYEw-<8kMf-|Bc>ZP|^RCrjO)m)n!x=hM{<>+li`?^03iml-?v;@a_e8IA1m5{{gn zNi=FkDt>z#w%i=2`DO{4Z<46#hEl?&8zgMH&QIgD;u17mBT?fu5;k02O3<)K>c%dT zpp*p?B&oAw4f7>PCpFe!Zm0VWf$clYme7nhjGATRU#plU1@n4F&PZjtM5%;!%wbwG z?%=<7ai1u;HZg39^pn$^WRKB*++k6Q9`^@b3#qU4p z{;}&n*99#$nfVCnxTTL5dcLZz&mJ%I@x(9d$IF&=`>Btsx?at%sJ5J*dodWdGtXbY zo-4hqKd&KHuJ$v|{XF{j^<}?b{`V&{2T8T(+3#!R_BeaKq522GI7!DHPs&SmKCX^O z`g|vdCfL|^+qTXAn5-FRO`n6C_5EY2zrJ5*t|jaF>+!j{o2TZ+lS@lIeqQVnFT*BF zIJ}&`&cEH9Iy2YKW<-xfrFJQGPM&GM#6f+d#yex*NvWO+e*`3mZHzc z-FdoOj=M|Z+$rI>J0%)-heU00!nWHbY`xVcXuVZ}mRlrhxmChulUr<}mYZzC=9?sJ zzEQ%K8zpGIvDAl%ny#17rP1^yuC)o8u9cw4^c~8v;}E5OL)?eRj6aOIO42F44tw2V zp4WefU%Ts5%$8t`p6g)B@qCADKO*i^gd?ZLDSbw&50RShV6IoB%XU1bI9dFp=1LYj zCCo(mROPr2Q65jy{oC}lir%j~`SV51dr+yLnsXrM_OCI@T2?iJ22- zgQ!*+rY8&C5}H{@7m9(VWCafu+Yz>ha{M2@}NyP=>Z8R zE|$P${C(o*UJ1tED|O@Uv2p!}pzSU%Ve4HIw%#dWtLa0;30iN9({h^xEv7$F?njvZ zM9_4z1Wh;D)HU5Cbxk))XwrB?DM8~km81F(!Pu*lv519IXT~8?^Bug~W3G~L^n5kf z!JqpOjGkNiyv*lj<~_L2uT<9kH#`1d<~_Lhek#p>u>A*hy(e4Np8c|aZK?b}5AORD zsqa_%T2*|z@9{F*_g4S^kS*tp&-urb?D&L#{-BRvcJ73kKjEVKBc=OYecm$j`&)nh zaQ66Hx&Kn_`nvtd_7D8_=E|$jYfr1I&uc%ct=IF#b^DoD&iKpt)t>#bZg2CqSM$rG zapEN$FX`y-r#@fM<@C9>xoxgzr=yP-rS{pi)2a5^{FYoFkFw+E{_!h&ymP-li`#77 zc4v;K>UgWmW=@(HHGj>Y+vXqd?J?iYgG-j#`Eq_6l0ILx=d4M4QT*8jWN{Koiu?gEBu`y+LJ8XYgqDc=) zH0ePJCnlM&SWKcM;rRPX3C7(g!MMA+G0A`Ta=7AsXZP5AMCx{fDT2QIdrc)h|rVdvJ3g z%$&(`qA_#5xVaB*>Bef@(v3g3WqJNXdEP@j4`O;|84jP8oe!bLCxYRVz36Kk@%PJJ z+b~Q2`xg%D@RR%>gzIzb@wKSXp)ygT3em@x?D<1p|H1ulrCa8$U%6h-2hzvi?DZA@ z8fEtQ?~jk<)mPhIU3u$QJKj97>+9>y`t#2FytEg8EUmJ&OIlZ-PiM+~_s{irv$wK4 z4=625?RIjWoy@N;Ei>oW{`F9AY&azVqTBDNVUZWt&X}dWcHS6YLpQf=nywW; z*GZI4^Ys$v1}U^8*Xqr9drLg_UTC>V==JWwpdvDPfXe z@=6INudoRxFDoVJT3YTqbeMj_;}Uf|R!;k45_CQyLDFZ4+8>c<(jyX0d{{!8hsym1 z*MFF}*p5GxmgDailORbrAs&AS#@}P(`VXaLo9Q!{+@a#eAa0e=^c|Yx1T8oFX}-zM zgK*2JDZZ8wHk#`gai1Z*jLbES)SQUY^^CFCh)KCWvCz(ga6VkW&&-7g$6i%RG-g35 z;pnU4F^6;?GMP_l=2F_*>i3&v=QFK+e{Mv2o7jc ze+WHJ=3nb@ef9D(d%Z(n15y7sRWNfV((%Uw{kmS}SGeEJxTU>bX#RsL?w9EEAzjX# z57l~1SI6)2??&SP&8&X^e*Yt{e^PCIe*0Isf0KPYf4ryq*IQ$os^0%hS?}MDRhR$M zG~*+=UhS5;J~Z>c)5-YNp7YA-?}C|I)afPN=JtA9_3yp1nTwW;hnJRWK3cZyjtl;> z)Q;P8lxd}Nm(2KRFXQ9xT*xf-7^^_8Pvqjvpr;pTAq8@o~cOcS|__9*HI-nel{# zGqy@NV@sTAo5g1OMzNW;QNn2(N-_O~sAqlLZ|GhpVYlf!#Qle8${LBLtnw00HT{Sc zHqn$75=~wqVb}5`%OvVt;>GnN%F9P3H2nvc_J{4bLon%Kn`q)gHm?5=O?<${_8;sR z!~=Fb!i`IW6Yhy$zj+VF-6P?+yOa3yAk16{n^IrG^dHRii(9?8c@S=C`W5ze)6HVz z=0(_X31h>|sZ2-Bk7#uL!q}SeRq9ii`IciBN$ADD-edZs>ECDQ>qTB)qSUV_|7N3f z9ZK~pvSs((^#3nH_c!!-TQt1KOZhmF%!?T|RlKC;SD4%B|B{HWkHpvSO2;d2ETfn4 zj7)z+_dhC^OXo@M|FYHmjOvf?x9^a`xH#^5x>-i!?~=lVyS;9+qpv!4HmvY;;s@h(AZF+_TUdq?2?YZ=bnUaqFzPPe$ zhl7!3-cmVrqh{O0|LJ1BlXFVnSMy!=zq_W*_S@^w%$u^m2{-2*aXW3>YkLkIA4ANs z#Kz6(_O5A{=YN~?oAU9>U7yX4xyIwK?ssYB->cc*uBFbc&296$HXHXlw`5s5$8NdR z&S`f`bB_MF;VhHgHo>gjac1ullW>-a=|7n4kYI+% zb_r)}^Ab&e!iyc7n7&oQ>04|H(>6=gW3nlU8H+IE5UEUEmyAiIu4^RyiD1eq36n&V zS4m;=N{PBwl#-bb(XmXTj-?WHEU|MU%r%X4Y(K*ECE6dAVA7-Z+D3eR!(7{VNWuw8 z?DY*Z7NN6Pg7GHzOP!q`5zm*1=SrBqL_9tb_akCwLED|Bgl%`&xIToNS6N=(Vt=<` ze!J0IO#jyYZX=mD;l`@Ub1u#GjdbGQf12NZnw0w2rXP`+d+Fv*l$T~~Egipak)5BJ zx!z{}qiXcLRLcJ^5sw9`@jo|@Hd&hU5O*w$m+|;TY0Ocb?-WMP^rGu^IXnL0AJg^x zj^6*%qPPFK=;OZB^-z25MqQs7dS!o{I2bm~#*Gu_FZ*~OO81*NG3Ds%!@6AFF4?d8 zyi9$08#gDX@1^$O+F!SfM$RhrU+ryk9WR}X=d^WmyX-xlYcx-yRNQ#CTiV}BrOunf zF$=tyIZfFtlEPRM|6DqKUhQs&W3RP|8m{X-YCOFB`||j_I@4aOcG_O6HP>sC@$mGr zV1Jut+UxWRosWo3*CSHsa&2|IH0^b9%Hv)NQm8HKxP<9Tm@$cXu0(mhgqb%{o@*J8 zOPJqll#?ExsIy}d_ehY6>rbSY>ECXIX8uGedJe@MV&-6`6E@$L{2ny^U1-#Nt4-K+ zi%ryI{!=2Gu<<4d8_jP!uJ>ZcF5DP~snh2NNx!4Cw7>5#eGixNSVnLC5jVaOk8QXy zj`A{n?WDKc?mD7d>hTU;&i=oJnh$KoL(MUL)NF}Hn%@A;@#3`&ww>^Q!)muN+qtCY zHl@Z%hRW^q^Ru@{&a&-bd6{}G|IqC@5_$3W*VpF}pZA*UVCFZ(shHmcT}ssNlFPqQ zj_31)qZiouJi7f2$1L>Hn^|{XZ++}^|2w|zpHHX9yUlICFK_0S`RC8(+N8cOT{hP= z-F$1aEMJ$7eJrV=eYD$#;;j@hyui&$`23RlH3{fKD(A)B}_5sya{<{gx1?tyaV?3LaU z&e<#B>^(`$b&g#U&e@raOYD+xW)eFlVa6qT%QlIoZ%Gng(}>J?L@MdI683hFxz4e^ z6f;&4b+0YOjZ2i4lULen9PTIRWlHYBZ zYaXeX-*4D)id4VCyPf==!&@ff74h{C(?5={d6<4i=GsU4T8EuWk^C;S4mM`=i%NkTH=qV$7tI*wXG?*_#_=<_X2~#^Q0go1~!Ae6ysZ zzY~Sl+q}4OZnv~!+-96RNqlYkj`)9I%zweC#ODU(b7(ie%$!fe*QM{ZZM12pZCh>H z>poJjW8UWcIh~|!p14>FW-eFz1IhWcIfs5Y6WdOAJS+thcP?FFS?qeOlwp$}m%`-7 zC7S%0L{lD@Xv&gg`M5+=mq@{8nH19LUM_xCNTGX$6no+ndRBTVPFw9~##%`ynz72J z+)l5NLXzUFwNlVg?RR1JI-BC0byA$OUJ7$I_?f#wOp5b1+7#w*@)F+kYQ;o1eon$0 zUgo|jN((KF)bxpLW#LzwZ1@|c9{SA>h6 zl)&~YTz}%21f^>k?wUq&Us4yC@rkQkj!1CTVF~Ru4s)I3kOavXMQN;J?jZ@xSj61@ z63p2zb?#b+?Ms;cL^;7MGiM?_Zz36^n5o7nW^5CeIx|*b=U+Y{<{C#}u5%=FD1x3X zHfAnn(6cF%y6%lO>ApqX)O8X}UE?L3vL+d?h{q|y$*W5Vx>l9uQMfr3rk@dXnk=^o zJC=G0I+l8g`x}o*Fey$r=`ovN;-g6(mcUE=e-m{R9<*^|7^UU-2gHxNKH`?;--6iw zhq+$jMbEo%^DmNR>)qlfH5bF&ZZTsV72^KGB3ahic^YP}Mx6M+vD|-TWtTzIt$ysB zj`+VX%(+$k9~kLn{C^nke=&l_n^bO;AQf}nCa5Ci>pXFPsG4p2V_p6Dao^003teK& z92hUjyj!=l<4k6(DNfXQvzNNYo20HW{eGLzqVM$m`I>Ps^S>rK{&<-iJ1h5l-S3rp zU&hz^Qs=?R@1@-`J&tE@yZ(5xERS2q0vuX#2VQ%=S7C$2VqisvL)^sIz7&y@NU!J^|{qD4>I zq{k>szryw%79LCWAIv-ncdf%MkCgfjt`8B;KM=>vnV1)kQ^eOe%-o6n63y9XQ|?Rb zk#LqtJb%KDQ+U@s%JVVJyv!(xnO9-vRD@<8W+|pWVXk?U6HVJ9VUNjX3A)WZij6j5 z_XeA&dy|AyT{cQIb%U4qw;pRHm||jnGrBrX>35^1e_`fnCP|H5nA=g;a+|PonN2*m z!pyIT6Sgn$5=>g+#m%*F%gD^R@Dl&VH2!_*!=?Ec<{C~))R zd-F-F^V`j+^)8jWCGz6e$Ny7i{&(bVDfE)`cvf$>z0X@&NdNbV@nWtUoAx)pe(cV3 zllqV5&8TCt>+Uqq^oZff4U2f0UNpZ&dQcSyDn6=R+vTb#IyD)o`6m2$2F_mb} zrczuRZI;EkTWliJX7>`)cH8!Q{uc4GHEy>}+kMqmDK6NS%62I%+EGez(M~B`y+g&^ z*LRB7?)Fl+Znu}{j;~3fl&{CPZ~vOOM7Mt}$yX)3?We`n|(7wz|*uKn5Wcn|Y9xo*@b3;2GlfaCT*x#I#>&$w=#K*jZ6CSOciI0ev z38}KVJ>g+LZXRl~^xBR|=C>{J^&dB9HU7Pex!>g>yG}6vAqmF2`_jv#zOSX-#?65> z&&}_5l^-X)&+={eyxlTh-|f#`yZXnMrGDY0N2Jh8{Jy2`Yd$K4_Q!0ZNsqCdxgB+{mT0QU8i~5s#O<$XgOiwRmW7^mUZQDhQ&}g4>Fd2j z)7IJO`&~JjzTQh=#s)9Zj1682voEh*mhO(~e%`Av!L_=cZ5zb?_8 zUz6~TIIbU2UVc>yw|}+NZ!rCdbfVk7;^$V=U+6pGEnl>8>lDn`#LchSm}?vEc6w>< zFWeZ%T;E8iaO3B^6mR^T7jrEnGBM*7rXO*AGH)VTy0MDVcty_q2|JhKnr9_Q;^tA9 zYa+p-XCzwmoXS&nPDN>6#eyfrPwLu7(vNW0JfitWlX(0`J#7H(;N6B73~%rdgS4{^T-iTfD#Hz6~2N@OxUPB?w1 zgwuCOG<~~@nS)V|yI$g!QO_2euzO1!`e~N8y&3SL{vFE+!+&4+ov(`(& zw7IF6`f_=_6l~l&rmfwOWTO;jY)qBo`^|kO#hIII3Nts_L}p!6H*=Gpq#Z8K-kfBM z6jLe8+G10jz15~Ld#j&0Tcx)Y=RVPE3iGx}Vczyqiu29wZBjIC^nBAs@9-P2%({3LqrEvXz zDco?NlwmgDaKex~bZm}P0MrkS&OZ8GP=Tm!M!LE?Ue>06k%eugbC zd|JW^2^Ksh;ew}a%kFVA>7|d$##WU7Lw-m*;@EK23V*=7qRrxh|eB;_i3L_*##<<`egg zTp!6S!|u(gnEuaZ71J-;SWfBMP;!kZUDw-fw_dU|*O$`K+eF zm|PpDc-;Y;!gc$laNPl$;`IlmkV^3eS3V>Kmm3aA;fBLfxUrn#ji$XmVpF*3h)wb4 zqc#Q8Uf*)Gl)^1XODW!ZREoDAvnkwqOrqP5N%1y6h1;K$!W}M8CAXiF;+=5{cRnda z8&@vZ74Ljnig!KZC3^TrHid`(Ey<6hkV^5ve@*g3DLn8)DLnWuQg|SpA4tJuah&1< zKd^CSy(}&^{f%-Tqj=wUy%g^Iu9so=eMgG-x_n!TDusK$ErokrUYBAz_r~SIJ>N== zYkWhZyS^deU1pr(>k{7i4KMNdMOam=-l zuk>>5!yn5i-1229nEP&aG2<8UoQil}g+D(ty79GA!W&*KUH5R;KT7=#b6vzFxegLt z|2dn`%+9LRl5~b^An{qkf?DBjMH}4~wz0XT|9!Pm^ zsdr!U{~D!bGR|WEZ^K-(asRuKETfsby~O{&;r@r?|7YjC-=j9Vzx^b5t9z&S6lwwi zk^mtGdt!7xnPuFUvF^{<+RghmZz{!HNSH0F{^rL7WM^|o`#-S%gL&*l*0^PkDW z({3KQ6&g0WHHljgrd~M?XdJ(z5{)If94O}Ab{ENiXWih$B{Gzd#+=8x448B=H2207U(rp=e zx-BEux68;a@^klWlV#-ocB#Z8@^=q~p4sG)Ia=iDY!b2iyX6`y$gSY&wSwHeR+78- zO7iqxMeg1!B%VGiC1QDiKC8&1;U0RNGwe9Ih8_zLGvuhj;G-mP4?4n#8FbiS@L`G> ze1u$sIG%w=Ob(NKfZ&kALGlbZz;F#XV9~#@zcB7; zSao@Cqwg*f822~IDWCt>aI>fVmgZVJjFc>1?zXHfztX-iTPVq@!p1+8OsgV&pRx@tnn^-3n5%t^av8S-9FQ57ez?G4l7TODLvF7Rxz~nCAou zKCcn~KO^Qd1LOZ_1n-lG`z7N4X;}A7tkTtH5hJGcLXF`3#hA-1pcn;L>jhOJ`?Ol1 z&2_|YZFS~R3`6-XuB_|rdi>A&2j7{s`p=iY{k5J&vEKkI+m!Fv%6(ev@lyPLScQKU zajZYHWiI&+ukjf~-PLv;x!Q>Ti6;8}EbDSv+j-;`Z5lD3X9aMzn@_IKvkcnJCwIH~ zfi|n3tK9-dOuHriT>KBIVt3~Ac>X!iSuNN~sofk7)ofjL(yuE~Cx-KD?z{=I#mJ=tNJPNLFmy_F~ z`*MouzMNb=1S{T5&lTkExl-c#Zl%QCYn8;)YZZBVuO@Hr99fE-tm5jk%7(k|>Hr?A z-Y`Na?e&(LE5#B%qLqXAq)j*@H0VR8));vRB@ zFB^1#LL%>P?57wL_ke@s8nB;S;(o?}J>(Yk0ph-ffxG`cgYWlIRk-?V^NqlqLd-9$ zXC%SW-KSjQ>RnE*-eq=r?IZ*7TqHQRu;v(QDW03ulJ_peJq+_5jUL6MVm&XhN_D>*D{5xCHGN zSBd+JC7kw)Nuc+)^)uIVeX&Fm(|)nUimlS!VJWBMVp6Hq@fWJqX>kBo$Hf{;$fXd| zak0eJX)zph zcmH+d9TFie1Cvca~+hV%}k3-R}_hHjKH5D1)eT5m6U&4i&52 zS|;WvwZ1Ks$hpe5MO3R>VE}&bL;UuHBijSdT(+`%A?kgRs_GxgXD(YPhOvoaR5}~) zd03^Z%O-}kJXng~tB5&_iumn{fqf3MLH<@n{9YySn-%enh5ds?fsepmZX%a~3%4BL90R#PO%?SEzf>Mb;}`lw>4Gb>b8p9->#OZbz@l$xlKIXb0qHW zISf~i9ErQ+9(mrJ%cv|8M2Z7 z5kohUcj%@7B6f*&hi!3SbD-V(!xnP?u$kOHZYIxjFIQ5D&)Lm#Ch`mBsfRj5ogIeLfrc}EAb3J zBk>MDBa!isXUOy8X^m6l{^4W*&#;pc_t29P+x-vg`O1*va-QKCa*QLMsmSLl#dP@QCLv)-o~o=(jI``}_Si*gT})UW$>= zQp|TeQ%ulR&imT^#hIkg^63=RSl*(r^t{z1pad$7$C?vOh7sPnaRJ>p6 zg!=xet6M?Ud{aOcG2d(nAeO1IyXzK)>zgeM{rM03UmaaHlB^^S@7y^N0fTN%C0 z)mc0*Qs|OTp$Pnb*Zdu?-q+nVpP{$A%yv<_y5y0&E63GkgT&o+!yCvYR~Ny0iMwlV zNL*dknf=z2+v1yC@+er_5X(fHitC$PiCoq#m)zYpFudKww$=x5f14Y?(|rSZzRhKL zyXP`QAGg3<&(&?c#Qp7hbD3xh;u6cd=Sn<1HgI}wAc?hH#NQcszuO@3^xB}2OCAMp zuUv`eyWAjh*#`3V+CXlrZ|@D{?Je@LMIO2Pw28cfHj!u0W`=jjmH@t?1%bM?+%segd4_Hw zuV@=uK<;52uhllpXqTm^Tl~0{R6HUczAb?7$8F>l4Btkc;i9b|B)*@vGCU&-$TMnTQiHbh|@Qu655OWU;<6g)W z@<@#PB3@Y<_eI>JFOqlEMe>de;u&>O;+FSBE|7PGm}{trd53|=oO|eV5Nj^t{_%9+ zUPs`b$FP&6;vRN_;TkI5-v}a}xybi9h8&acap?Crtot8oDf$?568^4-c=y9KkR$Ga zsJP_w74eM4K%bwu`yXJq`t3K6a}j=TL@X2UoW}GE;_kPP;pw;Ei2Lp%fw)KVPm%Xg z^k+7`%Sc6kKh=6)M3&;+RP+5+Yu@7SwSy6SH`RPcRe8oEp6A%-Guz1}=vgZ9d?$Wy zYyRd|%w5di!&s&GEv|}O9{7E3F}ZsbYLt*$rRO#ZvNh1xcrRs}oa>1B4x>jQd3qE{ z*m5~9Dj-+)0&@2d+uCZhtNnY3wji#aTN&=21st_+_bqH)(dO!@Vea25)%9h)=Vnq7 z+qAapk;hU%?(en+@O-y5NH0;}5~SxA^7JeS;QekZd3zNw+`S7JVtscnW0|XW0eO0D zshZwf$g8aD=~FehYCn zle^!hD)ET6?>Ea*)Mea%3#oVpY;j;Sc~k}#kY~`=0N%j`oFO772a)+$6~@d5527cz!&~aQ`6g zhXnBsJIVRs6sdTIo-{c@9>I_x;vR`-uz`EfF>(()M&ka6dXL0A_$WiaU*a8fR3h$^ zh<8s#Y}MtvsAe3vpEBT(yhmb{-T{Xgo`DA$-a#ThK<0wy+F&Fe;pw}B;p(>|NMEIHE_e4WWqA6QYHTM@NJ_|K;_X*T zo_^aEcF4N7-;MxQU#oQYEg^TG5?P9U`@Y|0eBq0VNXefMSMkU*I3@#+!;3A24a1m!nAqhN#x5-lU4apGEPeHD? zjeJ82$v3o+d_Qo!!?xP_VXORJEWR82e%Qv)^RpWJekvs2Pel^{h$4yir$Ww%ZKUEG zS;+8@+Q#sX-o_bINEW`aMKZ^VJZs?_w=FEZqW}0p@=Yit|AZp)X~a(0%J5Gt3^J*Z z{FApye3Q0H{F93$u~UjDc1kg&uK1Hum;VtUW%(0M_7k#5&VEA4D>TG1!7_uSWlwA* zE_*_WOCK97`GZuFM7&t=M3V5!Lq`0fhYT@4vF0kGOjz_-62DL^e?$rMA4=q0Ma)+M za}#rplJ$TDaVD~l5?jlyGJfv;0C98g$+{>lVt>9zv2%mO&Av;qf?0P=exuk~zmb2| zZSv2&O|dF7jAdfEZ^kW2+|1u7PV|{!;Gg!Z#5e6G`KI2a*lD*oQ-38D|CE~slW&l3 z@(uD&xwSneAc z#6Rjn0BbqFm!jTRG3vgb&Pc51INqPc{NwGJNQs& zh8!eE1|K8~&*1&!8M2?eLk^gTdI$#ZCl$}2y$s)=y&>@q-plX|-lMUXJSN^D`>J3! zd4}vFuVgp*hU}4ehwLHm;N9dg@ebK7mj&X%yUq4pl#x%tJ9MYPurl)e zP)5G07*v1@X`Rb_Mb|~ z!uL}t`F<)1&B#*X6q9dcG5JT97>p_=72l{LhJSPs`NtHIZ%h$+Wm!l*hS4V4C8L9} ze@ro1v8c;~{SvOS zRZk`9tNvu9t$fNzU-8uBFG^qW7o{!#Q<9qfhk-hm$$mnq%l@L&rGh^tDds#SdFd00 zjF⍹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 z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_27Z!(fBUz8ljnc|1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz zev*OC#H>6A3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb 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 z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DDpaXP( z4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD7)b|y z{2_tx8PEYbKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{19X56&;dF?2j~DD zpaXP(4$uKQKnLgm9iRhrfDX_BIzR{L03DzMbbt=f0Xjej=l~s{1OK0W_x_WcNdEsH zmNc{LxOvw(XX9*4GT20uOwPt+15SW#f&YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzD zmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAf zfN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq z4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC? z(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQ zFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)q zU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@ z1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~Ef zG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$Xn zOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnK zz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzD zmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAf zfN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq z4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC? z(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQ zFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)q zU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@ z1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~Ef zG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$Xn zOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnK zz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzDmnKz%*bQFb$XnOarC?(|~EfG+-Jq4VVT@1EvAffN8)qU>YzD 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 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk4z;D+;#dTJo01Tf24bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp z4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$ z&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk4 z01eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp4bT7$&;Sk401eOp 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 zpOydnc(); + { + 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); From c9868640c115c69ed8e5d1343c1daed298768d12 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 10 May 2017 18:14:50 -0700 Subject: [PATCH 104/146] force data in MemoryStorage to be word aligned to avoid crash in glTextureSubImage2D --- .../src/avatars-renderer/OtherAvatar.h | 2 +- libraries/shared/src/SimpleMovingAverage.h | 2 +- libraries/shared/src/shared/Storage.cpp | 5 +++-- libraries/shared/src/shared/Storage.h | 13 ++++++++----- 4 files changed, 13 insertions(+), 9 deletions(-) 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/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/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index aae1f8455f..46c631c72a 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -38,8 +38,9 @@ StoragePointer Storage::toFileStorage(const QString& filename) const { return FileStorage::create(filename, size(), data()); } -MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) { - _data.resize(size); +MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) : _size(size) { + _data.resize((size + 3) / 4); // alloc smallest number of 4-byte chunks that will cover size bytes + if (data) { memcpy(_data.data(), data, size); } diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index da5b773d52..e0519c98f6 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -41,13 +41,16 @@ namespace storage { class MemoryStorage : public Storage { public: MemoryStorage(size_t size, const uint8_t* data = nullptr); - const uint8_t* data() const override { return _data.data(); } - uint8_t* data() { return _data.data(); } - uint8_t* mutableData() override { return _data.data(); } - size_t size() const override { return _data.size(); } + const uint8_t* data() const override { return reinterpret_cast(_data.data()); } + uint8_t* data() { return reinterpret_cast(_data.data()); } + uint8_t* mutableData() override { return reinterpret_cast(_data.data()); } + + size_t _size { 0 }; + size_t size() const override { return _size; } operator bool() const override { return true; } private: - std::vector _data; + // the vector is of uint32_t rather than uint8_t to force alignment + std::vector _data; }; class FileStorage : public Storage { From 89d93ee56db5022b6caa860ab0715f3b9c5ec915 Mon Sep 17 00:00:00 2001 From: Mike Moody Date: Thu, 11 May 2017 04:16:22 -0700 Subject: [PATCH 105/146] Updated HMD grab Clone to work with all entity types. --- scripts/system/libraries/entitySelectionTool.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 3e4b8d8518..3389ab0836 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -2162,6 +2162,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, { @@ -2318,11 +2324,6 @@ SelectionDisplay = (function() { }, rotation: Quat.fromPitchYawRollDegrees(90, 0, 0), }); - Overlays.editOverlay(grabberCloner, { - visible: stretchHandlesVisible, - rotation: rotation, - position: RIGHT - }); }; From dfb9fecf17181f4245b1d5cced535a846445600b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 11 May 2017 07:11:40 -0700 Subject: [PATCH 106/146] pad MemoryStorage size to multiple of 4 rather than using a vector of uint32_t -- this gets the same alignment without needs casts --- libraries/shared/src/shared/Storage.cpp | 5 +++-- libraries/shared/src/shared/Storage.h | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index 46c631c72a..ec29a8d7f8 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -39,8 +39,9 @@ StoragePointer Storage::toFileStorage(const QString& filename) const { } MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) : _size(size) { - _data.resize((size + 3) / 4); // alloc smallest number of 4-byte chunks that will cover size bytes - + // alloc smallest number of 4-byte chunks that will cover size bytes. The buffer is padded out to a multiple + // of 4 to force an alignment that glTextureSubImage2D can later use. + _data.resize((size + 3) & ~0x3); if (data) { memcpy(_data.data(), data, size); } diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index e0519c98f6..f51d95a95a 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -41,16 +41,15 @@ namespace storage { class MemoryStorage : public Storage { public: MemoryStorage(size_t size, const uint8_t* data = nullptr); - const uint8_t* data() const override { return reinterpret_cast(_data.data()); } - uint8_t* data() { return reinterpret_cast(_data.data()); } - uint8_t* mutableData() override { return reinterpret_cast(_data.data()); } + const uint8_t* data() const override { return _data.data(); } + uint8_t* data() { return _data.data(); } + uint8_t* mutableData() override { return _data.data(); } size_t _size { 0 }; size_t size() const override { return _size; } operator bool() const override { return true; } private: - // the vector is of uint32_t rather than uint8_t to force alignment - std::vector _data; + std::vector _data; }; class FileStorage : public Storage { From 80e6edda0c28969705227b19c978440921943271 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 11 May 2017 10:13:15 -0700 Subject: [PATCH 107/146] code review --- libraries/entities/src/EntityTree.cpp | 7 ++++--- libraries/physics/src/ObjectActionMotor.cpp | 2 ++ libraries/physics/src/ObjectConstraintSlider.cpp | 6 +++--- libraries/physics/src/ObjectDynamic.cpp | 10 +++++++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 2285e6e4bd..04a8e89fef 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1611,13 +1611,14 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra if (oldID.isNull()) { return EntityItemID(); } - if (args->map->contains(oldID)) { - return EntityItemID(args->map->value(oldID)); - } else { + + 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) { diff --git a/libraries/physics/src/ObjectActionMotor.cpp b/libraries/physics/src/ObjectActionMotor.cpp index eafb43c7a5..9a23c44569 100644 --- a/libraries/physics/src/ObjectActionMotor.cpp +++ b/libraries/physics/src/ObjectActionMotor.cpp @@ -27,6 +27,8 @@ ObjectActionMotor::ObjectActionMotor(const QUuid& id, EntityItemPointer ownerEnt #if WANT_DEBUG qCDebug(physics) << "ObjectActionMotor::ObjectActionMotor"; #endif + + qCWarning(physics) << "action type \"motor\" doesn't yet work."; } ObjectActionMotor::~ObjectActionMotor() { diff --git a/libraries/physics/src/ObjectConstraintSlider.cpp b/libraries/physics/src/ObjectConstraintSlider.cpp index d8f42a1cbe..d4e6b6b95b 100644 --- a/libraries/physics/src/ObjectConstraintSlider.cpp +++ b/libraries/physics/src/ObjectConstraintSlider.cpp @@ -108,8 +108,8 @@ btTypedConstraint* ObjectConstraintSlider::getConstraint() { axisInB = glm::normalize(axisInB); } - glm::quat rotA = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), axisInA); - glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), 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)); @@ -123,7 +123,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), axisInA); + glm::quat rot = glm::rotation(DEFAULT_SLIDER_AXIS, axisInA); btTransform frameInA(glmToBullet(rot), glmToBullet(pointInA)); diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp index f41db5c17a..16be10a8f0 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -32,10 +32,14 @@ void ObjectDynamic::remapIDs(QHash& map) { } if (!_otherID.isNull()) { - if (!map.contains(_otherID)) { - map.insert(_otherID, QUuid::createUuid()); + QHash::iterator iter = map.find(_otherID); + if (iter == map.end()) { + // not found, add it + _otherID = QUuid::createUuid(); + map.insert(_otherID, _otherID); + } else { + _otherID = iter.value(); } - _otherID = map.value(_otherID); } }); } From a14fa5dab9f3da5942a8f1d08f263a5c909821dd Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 11 May 2017 10:54:15 -0700 Subject: [PATCH 108/146] code review feedback --- .../animation/src/AnimInverseKinematics.cpp | 16 ++++++++-------- libraries/animation/src/AnimUtil.cpp | 7 +++---- libraries/animation/src/Rig.cpp | 2 +- libraries/animation/src/Rig.h | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 92c74b1793..5e6927afcb 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -734,21 +734,21 @@ void AnimInverseKinematics::initConstraints() { std::vector swungDirections; float deltaTheta = PI / 4.0f; float theta = 0.0f; - swungDirections.push_back(glm::vec3(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(cosf(theta), 0.0f, sinf(theta))); + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); theta += deltaTheta; - swungDirections.push_back(glm::vec3(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(cosf(theta), 0.0f, sinf(theta))); + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); theta += deltaTheta; - swungDirections.push_back(glm::vec3(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(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(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(cosf(theta), -0.5f, sinf(theta))); + swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.5f, sinf(theta))); std::vector minDots; for (size_t i = 0; i < swungDirections.size(); i++) { diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index 314f4a1c3a..a4659f1e76 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -37,16 +37,15 @@ glm::quat averageQuats(size_t numQuats, const glm::quat* quats) { if (numQuats == 0) { return glm::quat(); } - float alpha = 1.0f / (float)numQuats; - glm::quat accum(0, 0, 0, 0); + glm::quat accum = quats[0]; glm::quat firstRot = quats[0]; - for (size_t i = 0; i < numQuats; i++) { + 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 += alpha * rot; + accum += rot; } return glm::normalize(accum); } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index f933002c2c..cb5aebe930 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -941,7 +941,7 @@ void Rig::updateAnimationStateHandlers() { // called on avatar update thread (wh } } -void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform, glm::mat4 rigToWorldTransform) { +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"); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index bee6518557..33b66f91ea 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -162,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, glm::mat4 rigToWorldTransform); + 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, From e0863aa50f3509b3102f33d694f7d129bc33fbcb Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 11 May 2017 11:15:36 -0700 Subject: [PATCH 109/146] Only open files for write if necessary --- libraries/shared/src/shared/Storage.cpp | 30 ++++++++++++++++++++++++- libraries/shared/src/shared/Storage.h | 5 ++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index aae1f8455f..943dad9f56 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -68,7 +68,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 +90,31 @@ 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; + + 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(); + } + } else { + qCWarning(storagelogging) << "Failed to open file " << _file.fileName(); + } +} \ 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 }; }; From 5d457eaa3951b9b19fa9762506f85fb9c3850756 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 11 May 2017 20:44:08 +0100 Subject: [PATCH 110/146] better handling of when lost tracking of pucks --- plugins/openvr/src/ViveControllerManager.cpp | 30 ++++++++++++++++---- plugins/openvr/src/ViveControllerManager.h | 2 ++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 6e5697730b..411cac3d2b 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -32,8 +32,6 @@ #include -#include "OpenVrHelpers.h" - extern PoseData _nextSimPoseData; vr::IVRSystem* acquireOpenVrSystem(); @@ -207,6 +205,7 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle } updateCalibratedLimbs(); + _lastSimPoseData = _nextSimPoseData; } void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) { @@ -217,11 +216,30 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde _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]; + + } + +/* const mat4& mat; + const vec3 linearVelocity; + const vec3 angularVelocity;*/ + controller::Pose pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity); // transform into avatar frame diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 4e8b2b3a04..ca78fd0b37 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -25,6 +25,7 @@ #include #include #include +#include "OpenVrHelpers.h" namespace vr { class IVRSystem; @@ -108,6 +109,7 @@ private: std::vector> _validTrackedObjects; std::map _pucksOffset; std::map _jointToPuckMap; + PoseData _lastSimPoseData; // perform an action when the InputDevice mutex is acquired. using Locker = std::unique_lock; template From a26f9c788744d621d7f4f3d1d8df156bd5f8892b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 11 May 2017 13:36:05 -0700 Subject: [PATCH 111/146] make _size const --- libraries/shared/src/shared/Storage.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index f51d95a95a..ed1c1dd0f3 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -45,7 +45,7 @@ namespace storage { uint8_t* data() { return _data.data(); } uint8_t* mutableData() override { return _data.data(); } - size_t _size { 0 }; + const size_t _size; size_t size() const override { return _size; } operator bool() const override { return true; } private: From 9138ec53e95637a56db1689804c9d0c272574925 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 8 May 2017 11:53:14 -0700 Subject: [PATCH 112/146] Don't send avatars to previous lookup on server restart --- libraries/networking/src/AddressManager.cpp | 5 +++++ 1 file changed, 5 insertions(+) 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); } } From ac2bc39a27ca30ff9b95b549b78fa5d3193b9487 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 11 May 2017 14:10:03 -0700 Subject: [PATCH 113/146] try, try again --- libraries/shared/src/shared/Storage.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index ec29a8d7f8..6a6393ff73 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -39,9 +39,11 @@ StoragePointer Storage::toFileStorage(const QString& filename) const { } MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) : _size(size) { - // alloc smallest number of 4-byte chunks that will cover size bytes. The buffer is padded out to a multiple - // of 4 to force an alignment that glTextureSubImage2D can later use. - _data.resize((size + 3) & ~0x3); + // we end up calling glCompressedTextureSubImage2D with this, and the rows of the image are expected + // to be word aligned. This is fine in all cases except for 1x1 images and 2x2 images. For 1x1, + // there is only one row, so the problem is avoided. For 2x2, we add 2 extra bytes so there's + // room for the second row. + _data.resize(size == 4 ? 6 : size); if (data) { memcpy(_data.data(), data, size); } From 52c288bfed215e817081542302a3b4766a2f0da2 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 11 May 2017 14:20:42 -0700 Subject: [PATCH 114/146] Desktop users of different height can shake hands --- scripts/system/makeUserConnection.js | 36 ++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index a8afad2e1c..37a334bd70 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -198,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 @@ -209,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() { @@ -347,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; @@ -436,6 +464,10 @@ handStringMessageSend({ key: "waiting", }); + // potentially adjust height of handshake + if (fromKeyboard) { + adjustAnimationHeight(); + } lookForWaitingAvatar(); } } From 6378b96b4c9eca5683e44726aedede1b293e48d1 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 11 May 2017 14:41:02 -0700 Subject: [PATCH 115/146] CR --- libraries/shared/src/shared/Storage.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index 943dad9f56..f6585e6ecb 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -105,6 +105,7 @@ void FileStorage::ensureWriteAccess() { _file.close(); } _valid = false; + _mapped = nullptr; if (_file.open(QFile::ReadWrite)) { _mapped = _file.map(0, _file.size()); @@ -113,8 +114,10 @@ void FileStorage::ensureWriteAccess() { _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 From f4df223d2337870dbb14177c9561d403c13432d5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 9 May 2017 14:48:42 -0700 Subject: [PATCH 116/146] cleanup Head::simulation() code --- interface/src/avatar/MyHead.cpp | 27 ++- .../src/avatars-renderer/Head.cpp | 199 +++++++++--------- .../src/avatars-renderer/Head.h | 5 +- libraries/avatars/src/HeadData.cpp | 6 - libraries/avatars/src/HeadData.h | 14 +- 5 files changed, 127 insertions(+), 124 deletions(-) diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index 34a75c5461..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) { _transientBlendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); if (typeid(*faceTracker) == typeid(DdeFaceTracker)) { - if (Menu::getInstance()->isOptionChecked(MenuOption::UseAudioForMouth)) { calculateMouthShapes(deltaTime); @@ -68,9 +71,19 @@ void MyHead::simulate(float deltaTime) { } 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/libraries/avatars-renderer/src/avatars-renderer/Head.cpp b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp index 93fe246266..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,116 +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; - } else if (_timeWithoutTalking - deltaTime < BLINK_AFTER_TALKING && _timeWithoutTalking >= BLINK_AFTER_TALKING) { - forceBlink = true; - } + 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); - // 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); + // 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; + } - 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, - _transientBlendshapeCoefficients); - - 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) { 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/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 2e4eec73a8..2704b6539c 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -28,12 +28,6 @@ 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), _blendshapeCoefficients(QVector(0, 0.0f)), _transientBlendshapeCoefficients(QVector(0, 0.0f)), _summedBlendshapeCoefficients(QVector(0, 0.0f)), diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 9b28616b3f..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,12 +85,12 @@ 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 _transientBlendshapeCoefficients; From 04827c3a2a39544cdddda68d0bde52d9beb2a512 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 11 May 2017 14:51:52 -0700 Subject: [PATCH 117/146] use const auto& where appropritate --- libraries/avatars/src/AvatarData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 51b69711dc..e2188478ed 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -445,7 +445,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (hasFaceTrackerInfo) { auto startSection = destinationBuffer; auto faceTrackerInfo = reinterpret_cast(destinationBuffer); - auto blendshapeCoefficients = _headData->getSummedBlendshapeCoefficients(); + const auto& blendshapeCoefficients = _headData->getSummedBlendshapeCoefficients(); faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink; faceTrackerInfo->rightEyeBlink = _headData->_rightEyeBlink; From 539aaf8c598d1ffde91fe402a5b9acfc314b90c5 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 11 May 2017 23:00:41 +0100 Subject: [PATCH 118/146] add debug statment and fixed sorting order --- plugins/openvr/src/ViveControllerManager.cpp | 19 +++++++++++++------ plugins/openvr/src/ViveControllerManager.h | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index e5cd7ffecc..8c357103c9 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -66,7 +66,7 @@ static glm::mat4 computeOffset(glm::mat4 defaultToReferenceMat, glm::mat4 defaul } static bool sortPucksYPosition(std::pair firstPuck, std::pair secondPuck) { - return (firstPuck.second.translation.y < firstPuck.second.translation.y); + return (firstPuck.second.translation.y < secondPuck.second.translation.y); } bool ViveControllerManager::isSupported() const { @@ -245,6 +245,7 @@ void ViveControllerManager::InputDevice::calibrateOrUncalibrate(const controller } 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; @@ -264,18 +265,24 @@ void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibr 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; } @@ -283,8 +290,6 @@ void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibr std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksYPosition); - - auto& firstFoot = _validTrackedObjects[FIRST_FOOT]; auto& secondFoot = _validTrackedObjects[SECOND_FOOT]; controller::Pose& firstFootPose = firstFoot.second; @@ -314,10 +319,12 @@ void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibr _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() { @@ -575,9 +582,9 @@ void ViveControllerManager::InputDevice::saveSettings() const { settings.endGroup(); } -QString ViveControllerManager::InputDevice::configToString() { +QString ViveControllerManager::InputDevice::configToString(Config config) { QString currentConfig; - switch (_preferedConfig) { + switch (config) { case Config::Auto: currentConfig = "Auto"; break; @@ -615,7 +622,7 @@ void ViveControllerManager::InputDevice::createPreferences() { static const QString VIVE_PUCKS_CONFIG = "Vive Pucks Configuration"; { - auto getter = [this]()->QString { return configToString(); }; + auto getter = [this]()->QString { return configToString(_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"); diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 9920fb003a..c2ebdc6144 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -126,7 +126,7 @@ private: bool _timeTilCalibrationSet { false }; mutable std::recursive_mutex _lock; - QString configToString(); + QString configToString(Config config); void setConfigFromString(const QString& value); void loadSettings(); void saveSettings() const; From ae5f9e4fb15e274aa9a143dfa101a5e6b1186730 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 11 May 2017 15:10:19 -0700 Subject: [PATCH 119/146] animate scroll of partially visible cards --- interface/resources/qml/hifi/Feed.qml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index fc108f47e3..3fd28aadb8 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -238,8 +238,24 @@ 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: 500; + } + 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; + anim.running = true; + } } From a43447310df5174942bc864bd1ed8333568ae2a9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 11 May 2017 15:22:22 -0700 Subject: [PATCH 120/146] remove physics dependency from avatars-render lib --- interface/src/avatar/AvatarManager.h | 2 +- .../src/avatar}/AvatarMotionState.cpp | 0 .../src/avatar}/AvatarMotionState.h | 2 +- libraries/avatars-renderer/CMakeLists.txt | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename {libraries/avatars-renderer/src/avatars-renderer => interface/src/avatar}/AvatarMotionState.cpp (100%) rename {libraries/avatars-renderer/src/avatars-renderer => interface/src/avatar}/AvatarMotionState.h (98%) 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 100% rename from libraries/avatars-renderer/src/avatars-renderer/AvatarMotionState.cpp rename to interface/src/avatar/AvatarMotionState.cpp 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/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() From 4aa683363c56673c3bc7709b7fc1dcc58f0e59ba Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 11 May 2017 16:22:12 -0700 Subject: [PATCH 121/146] back out previous changes, do compressionOptions.setPitchAlignment(4) instead --- libraries/image/src/image/Image.cpp | 6 ++++++ libraries/shared/src/shared/Storage.cpp | 8 ++------ libraries/shared/src/shared/Storage.h | 4 +--- 3 files changed, 9 insertions(+), 9 deletions(-) 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/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index 6a6393ff73..aae1f8455f 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -38,12 +38,8 @@ StoragePointer Storage::toFileStorage(const QString& filename) const { return FileStorage::create(filename, size(), data()); } -MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) : _size(size) { - // we end up calling glCompressedTextureSubImage2D with this, and the rows of the image are expected - // to be word aligned. This is fine in all cases except for 1x1 images and 2x2 images. For 1x1, - // there is only one row, so the problem is avoided. For 2x2, we add 2 extra bytes so there's - // room for the second row. - _data.resize(size == 4 ? 6 : size); +MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) { + _data.resize(size); if (data) { memcpy(_data.data(), data, size); } diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index ed1c1dd0f3..da5b773d52 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -44,9 +44,7 @@ namespace storage { const uint8_t* data() const override { return _data.data(); } uint8_t* data() { return _data.data(); } uint8_t* mutableData() override { return _data.data(); } - - const size_t _size; - size_t size() const override { return _size; } + size_t size() const override { return _data.size(); } operator bool() const override { return true; } private: std::vector _data; From 5042d4d3129b667992769f5dbd62699a608241d4 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 12 May 2017 00:28:31 +0100 Subject: [PATCH 122/146] add print when device changes tracking result --- plugins/openvr/src/ViveControllerManager.cpp | 41 ++++++++++++++++---- plugins/openvr/src/ViveControllerManager.h | 1 + 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 606cc38da2..a5b742c32c 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -29,7 +29,6 @@ #include #include - #include #include @@ -67,6 +66,28 @@ static bool sortPucksYPosition(std::pair firstPuck, return (firstPuck.second.translation.y < firstPuck.second.translation.y); } +static QString deviceTrackingResultToString(vr::ETrackingResult trackingResult) { + QString result; + switch (trackingResult) { + case vr::TrackingResult_Uninitialized: + result = "vr::TrackingResult_Uninitialized"; + break; + case vr::TrackingResult_Calibrating_InProgress: + result = "vr::TrackingResult_Calibrating_InProgess"; + break; + case vr::TrackingResult_Calibrating_OutOfRange: + result = "vr::TrackingResult_Calibrating_OutOfRange"; + break; + case vr::TrackingResult_Running_OK: + result = "vr::TrackingResult_Running_OK"; + break; + case vr::TrackingResult_Running_OutOfRange: + result = "vr::TrackingResult_Running_OutOfRange"; + break; + } + return result; +} + bool ViveControllerManager::isSupported() const { return openVrSupported(); } @@ -212,7 +233,7 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle 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 && @@ -222,7 +243,7 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde 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) { + if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult != vr::TrackingResult_Running_OutOfRange) { mat = _nextSimPoseData.poses[deviceIndex]; linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; @@ -235,13 +256,9 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde _nextSimPoseData.poses[deviceIndex] = _lastSimPoseData.poses[deviceIndex]; _nextSimPoseData.linearVelocities[deviceIndex] = _lastSimPoseData.linearVelocities[deviceIndex]; _nextSimPoseData.angularVelocities[deviceIndex] = _lastSimPoseData.angularVelocities[deviceIndex]; - + } -/* const mat4& mat; - const vec3 linearVelocity; - const vec3 angularVelocity;*/ - controller::Pose pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity); // transform into avatar frame @@ -466,6 +483,14 @@ 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); diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 30680ec264..c815506770 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -77,6 +77,7 @@ private: 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: From e1c805e76f0a02d499a78c52732764cceddb7354 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 11 May 2017 17:02:00 -0700 Subject: [PATCH 123/146] lock in Overlays::findRayIntersectionInternal --- interface/src/ui/overlays/Overlays.cpp | 1 + 1 file changed, 1 insertion(+) 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; From a9716313be317edfa72cdf10495e94a556cd9419 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 11 May 2017 17:02:37 -0700 Subject: [PATCH 124/146] cr feedback -- really good feedback in fact --- interface/resources/qml/hifi/Feed.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 3fd28aadb8..c1bd35f49d 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -246,7 +246,7 @@ Column { id: anim; target: scroll; property: "contentX"; - duration: 500; + duration: 250; } function scrollToIndex(index) { anim.running = false; @@ -256,6 +256,7 @@ Column { destPos = scroll.contentX; anim.from = pos; anim.to = destPos; + scroll.currentIndex = index; anim.running = true; } } From bc6fb182eb531d6b23fa75a3590124d2f1224b93 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 12 May 2017 10:32:28 -0700 Subject: [PATCH 125/146] pull out motor action, fix import of slider and cone-twist constraints --- interface/src/InterfaceDynamicFactory.cpp | 3 -- .../entities/src/EntityDynamicInterface.cpp | 5 ---- .../entities/src/EntityDynamicInterface.h | 3 +- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 - .../physics/src/ObjectConstraintConeTwist.cpp | 30 +++++++++---------- .../physics/src/ObjectConstraintSlider.cpp | 21 +++++++------ 7 files changed, 28 insertions(+), 37 deletions(-) diff --git a/interface/src/InterfaceDynamicFactory.cpp b/interface/src/InterfaceDynamicFactory.cpp index e4b3c0b500..49edaae90f 100644 --- a/interface/src/InterfaceDynamicFactory.cpp +++ b/interface/src/InterfaceDynamicFactory.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include "InterfaceDynamicFactory.h" @@ -51,8 +50,6 @@ EntityDynamicPointer interfaceDynamicFactory(EntityDynamicType type, const QUuid return std::make_shared(id, ownerEntity); case DYNAMIC_TYPE_CONE_TWIST: return std::make_shared(id, ownerEntity); - case DYNAMIC_TYPE_MOTOR: - return std::make_shared(id, ownerEntity); } qDebug() << "Unknown entity dynamic type"; diff --git a/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp index c44cf17b6c..822ac1c9d6 100644 --- a/libraries/entities/src/EntityDynamicInterface.cpp +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -129,9 +129,6 @@ EntityDynamicType EntityDynamicInterface::dynamicTypeFromString(QString dynamicT if (normalizedDynamicTypeString == "conetwist") { return DYNAMIC_TYPE_CONE_TWIST; } - if (normalizedDynamicTypeString == "motor") { - return DYNAMIC_TYPE_MOTOR; - } qCDebug(entities) << "Warning -- EntityDynamicInterface::dynamicTypeFromString got unknown dynamic-type name" << dynamicTypeString; @@ -162,8 +159,6 @@ QString EntityDynamicInterface::dynamicTypeToString(EntityDynamicType dynamicTyp return "ball-socket"; case DYNAMIC_TYPE_CONE_TWIST: return "cone-twist"; - case DYNAMIC_TYPE_MOTOR: - return "motor"; } assert(false); return "none"; diff --git a/libraries/entities/src/EntityDynamicInterface.h b/libraries/entities/src/EntityDynamicInterface.h index 89147936bf..b27e600660 100644 --- a/libraries/entities/src/EntityDynamicInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -36,8 +36,7 @@ enum EntityDynamicType { DYNAMIC_TYPE_FAR_GRAB = 6000, DYNAMIC_TYPE_SLIDER = 7000, DYNAMIC_TYPE_BALL_SOCKET = 8000, - DYNAMIC_TYPE_CONE_TWIST = 9000, - DYNAMIC_TYPE_MOTOR = 10000, + DYNAMIC_TYPE_CONE_TWIST = 9000 }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 2f02848a44..82b4bf703d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_DYNAMICS_MOTOR; + return VERSION_ENTITIES_BULLET_DYNAMICS; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 49df4662b7..746ae80361 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -209,7 +209,6 @@ const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; const PacketVersion VERSION_ENTITIES_HINGE_CONSTRAINT = 69; const PacketVersion VERSION_ENTITIES_BULLET_DYNAMICS = 70; -const PacketVersion VERSION_ENTITIES_DYNAMICS_MOTOR = 71; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, diff --git a/libraries/physics/src/ObjectConstraintConeTwist.cpp b/libraries/physics/src/ObjectConstraintConeTwist.cpp index 900e1b894a..8f5a347f6d 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.cpp +++ b/libraries/physics/src/ObjectConstraintConeTwist.cpp @@ -126,8 +126,8 @@ btTypedConstraint* ObjectConstraintConeTwist::getConstraint() { axisInB = glm::normalize(axisInB); } - glm::quat rotA = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), axisInA); - glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), 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)); @@ -141,7 +141,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), axisInA); + glm::quat rot = glm::rotation(DEFAULT_CONE_TWIST_AXIS, axisInA); btTransform frameInA(glmToBullet(rot), glmToBullet(pivotInA)); @@ -295,19 +295,17 @@ 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"] = _otherID; - 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; + arguments["softness"] = _softness; + arguments["biasFactor"] = _biasFactor; + arguments["relaxationFactor"] = _relaxationFactor; }); return arguments; } diff --git a/libraries/physics/src/ObjectConstraintSlider.cpp b/libraries/physics/src/ObjectConstraintSlider.cpp index d4e6b6b95b..3716960a6e 100644 --- a/libraries/physics/src/ObjectConstraintSlider.cpp +++ b/libraries/physics/src/ObjectConstraintSlider.cpp @@ -258,18 +258,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"] = _otherID; - 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; From 026daef8427078a7d9db4cfd436b77518e0e004b Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 12 May 2017 19:26:37 +0100 Subject: [PATCH 126/146] fixed input recorder crash --- libraries/controllers/src/controllers/InputRecorder.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/controllers/src/controllers/InputRecorder.cpp b/libraries/controllers/src/controllers/InputRecorder.cpp index 7433f181a1..c97bd99121 100644 --- a/libraries/controllers/src/controllers/InputRecorder.cpp +++ b/libraries/controllers/src/controllers/InputRecorder.cpp @@ -25,7 +25,7 @@ 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 = ".gz"; namespace controller { QJsonObject poseToJsonObject(const Pose pose) { @@ -220,6 +220,7 @@ namespace controller { void InputRecorder::stopRecording() { _recording = false; + _framesRecorded = (int)_actionStateList.size(); } void InputRecorder::startPlayback() { @@ -282,7 +283,7 @@ namespace controller { if (_playback) { _playCount++; - if (_playCount == _framesRecorded) { + if (_playCount == (_framesRecorded - 1)) { _playCount = 0; } } From 9a0fd78e41bc80fccb19a36c605d729f0b707be1 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 12 May 2017 19:34:13 +0100 Subject: [PATCH 127/146] fixed indentation --- .../src/controllers/InputRecorder.cpp | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/libraries/controllers/src/controllers/InputRecorder.cpp b/libraries/controllers/src/controllers/InputRecorder.cpp index c97bd99121..8fbc2de210 100644 --- a/libraries/controllers/src/controllers/InputRecorder.cpp +++ b/libraries/controllers/src/controllers/InputRecorder.cpp @@ -185,37 +185,38 @@ 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(); + 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(); + qDebug() << "frame count" << _framesRecorded; + 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].toDouble(); - } - _actionStateList.push_back(_currentFrameActions); - _currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS)); - } + 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)); - } - } + 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; + _loading = false; } void InputRecorder::stopRecording() { From 1f328cc923d756aa9d2085da64f375433127330a Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 12 May 2017 19:46:12 +0100 Subject: [PATCH 128/146] made requested changes --- libraries/controllers/src/controllers/InputRecorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/controllers/src/controllers/InputRecorder.cpp b/libraries/controllers/src/controllers/InputRecorder.cpp index 8fbc2de210..99167e5a97 100644 --- a/libraries/controllers/src/controllers/InputRecorder.cpp +++ b/libraries/controllers/src/controllers/InputRecorder.cpp @@ -25,7 +25,7 @@ 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 = ".gz"; +QString COMPRESS_EXTENSION = "json.gz"; namespace controller { QJsonObject poseToJsonObject(const Pose pose) { From 6155d31513e2764182cfb854a0db85f4630b777d Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 12 May 2017 19:48:31 +0100 Subject: [PATCH 129/146] removed debug statment --- libraries/controllers/src/controllers/InputRecorder.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/controllers/src/controllers/InputRecorder.cpp b/libraries/controllers/src/controllers/InputRecorder.cpp index 99167e5a97..60ff592144 100644 --- a/libraries/controllers/src/controllers/InputRecorder.cpp +++ b/libraries/controllers/src/controllers/InputRecorder.cpp @@ -193,7 +193,6 @@ namespace controller { QJsonObject data = openFile(info.absoluteFilePath(), success); if (success) { _framesRecorded = data["frameCount"].toInt(); - qDebug() << "frame count" << _framesRecorded; QJsonArray actionArrayList = data["actionList"].toArray(); QJsonArray poseArrayList = data["poseList"].toArray(); From 18b1bb8b7fb389c65e10d285b1cf522c4341efa2 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 12 May 2017 12:01:54 -0700 Subject: [PATCH 130/146] remove unused parameters from ObjectConstraintHinge --- .../physics/src/ObjectConstraintHinge.cpp | 58 ++++--------------- libraries/physics/src/ObjectConstraintHinge.h | 24 +------- 2 files changed, 14 insertions(+), 68 deletions(-) diff --git a/libraries/physics/src/ObjectConstraintHinge.cpp b/libraries/physics/src/ObjectConstraintHinge.cpp index 1fcea2913a..dbadf433c3 100644 --- a/libraries/physics/src/ObjectConstraintHinge.cpp +++ b/libraries/physics/src/ObjectConstraintHinge.cpp @@ -16,7 +16,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) : @@ -56,25 +57,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); } @@ -159,9 +154,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); @@ -209,25 +201,6 @@ 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 || @@ -235,10 +208,7 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { pivotInB != _pivotInB || axisInB != _axisInB || low != _low || - high != _high || - softness != _softness || - biasFactor != _biasFactor || - relaxationFactor != _relaxationFactor) { + high != _high) { // something changed needUpdate = true; } @@ -253,9 +223,6 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { _axisInB = axisInB; _low = low; _high = high; - _softness = softness; - _biasFactor = biasFactor; - _relaxationFactor = relaxationFactor; _active = true; @@ -282,9 +249,6 @@ QVariantMap ObjectConstraintHinge::getArguments() { arguments["otherAxis"] = glmToQMap(_axisInB); arguments["low"] = _low; arguments["high"] = _high; - arguments["softness"] = _softness; - arguments["biasFactor"] = _biasFactor; - arguments["relaxationFactor"] = _relaxationFactor; if (_constraint) { arguments["angle"] = static_cast(_constraint)->getHingeAngle(); // [-PI,PI] } else { @@ -310,9 +274,6 @@ QByteArray ObjectConstraintHinge::serialize() const { dataStream << _axisInB; dataStream << _low; dataStream << _high; - dataStream << _softness; - dataStream << _biasFactor; - dataStream << _relaxationFactor; dataStream << localTimeToServerTime(_expires); dataStream << _tag; @@ -334,7 +295,7 @@ void ObjectConstraintHinge::deserialize(QByteArray serializedArguments) { uint16_t serializationVersion; dataStream >> serializationVersion; - if (serializationVersion != ObjectConstraintHinge::constraintVersion) { + if (serializationVersion > ObjectConstraintHinge::constraintVersion) { assert(false); return; } @@ -347,9 +308,12 @@ void ObjectConstraintHinge::deserialize(QByteArray serializedArguments) { 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 b3bb92c677..bb9505fbae 100644 --- a/libraries/physics/src/ObjectConstraintHinge.h +++ b/libraries/physics/src/ObjectConstraintHinge.h @@ -48,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 From 10d058af02fc4d09ab04264d60295be507c86a23 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 12 May 2017 12:02:25 -0700 Subject: [PATCH 131/146] remove unused parameters from ObjectConstraintConeTwist --- libraries/physics/src/ObjectActionMotor.cpp | 8 +-- .../physics/src/ObjectConstraintConeTwist.cpp | 62 ++++--------------- .../physics/src/ObjectConstraintConeTwist.h | 3 - 3 files changed, 14 insertions(+), 59 deletions(-) diff --git a/libraries/physics/src/ObjectActionMotor.cpp b/libraries/physics/src/ObjectActionMotor.cpp index 9a23c44569..e7a8dcfa70 100644 --- a/libraries/physics/src/ObjectActionMotor.cpp +++ b/libraries/physics/src/ObjectActionMotor.cpp @@ -59,11 +59,9 @@ void ObjectActionMotor::updateActionWorker(btScalar deltaTimeStep) { if (_angularTimeScale < MAX_MOTOR_TIMESCALE) { - if (!_otherID.isNull()) { - if (other) { - glm::vec3 otherAngularVelocity = other->getAngularVelocity(); - rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget + otherAngularVelocity)); - } + if (other) { + glm::vec3 otherAngularVelocity = other->getAngularVelocity(); + rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget + otherAngularVelocity)); } else { rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget)); } diff --git a/libraries/physics/src/ObjectConstraintConeTwist.cpp b/libraries/physics/src/ObjectConstraintConeTwist.cpp index 8f5a347f6d..15d9378443 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.cpp +++ b/libraries/physics/src/ObjectConstraintConeTwist.cpp @@ -15,8 +15,8 @@ #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) : @@ -56,18 +56,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 +70,7 @@ void ObjectConstraintConeTwist::updateConeTwist() { constraint->setLimit(swingSpan1, swingSpan2, - twistSpan, - softness, - biasFactor, - relaxationFactor); + twistSpan); } @@ -171,9 +162,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); @@ -227,25 +215,6 @@ 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 || @@ -254,10 +223,7 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { axisInB != _axisInB || swingSpan1 != _swingSpan1 || swingSpan2 != _swingSpan2 || - twistSpan != _twistSpan || - softness != _softness || - biasFactor != _biasFactor || - relaxationFactor != _relaxationFactor) { + twistSpan != _twistSpan) { // something changed needUpdate = true; } @@ -273,9 +239,6 @@ bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { _swingSpan1 = swingSpan1; _swingSpan2 = swingSpan2; _twistSpan = twistSpan; - _softness = softness; - _biasFactor = biasFactor; - _relaxationFactor = relaxationFactor; _active = true; @@ -303,9 +266,6 @@ QVariantMap ObjectConstraintConeTwist::getArguments() { arguments["swingSpan1"] = _swingSpan1; arguments["swingSpan2"] = _swingSpan2; arguments["twistSpan"] = _twistSpan; - arguments["softness"] = _softness; - arguments["biasFactor"] = _biasFactor; - arguments["relaxationFactor"] = _relaxationFactor; }); return arguments; } @@ -330,9 +290,6 @@ QByteArray ObjectConstraintConeTwist::serialize() const { dataStream << _swingSpan1; dataStream << _swingSpan2; dataStream << _twistSpan; - dataStream << _softness; - dataStream << _biasFactor; - dataStream << _relaxationFactor; }); return serializedConstraintArguments; @@ -351,7 +308,7 @@ void ObjectConstraintConeTwist::deserialize(QByteArray serializedArguments) { uint16_t serializationVersion; dataStream >> serializationVersion; - if (serializationVersion != ObjectConstraintConeTwist::constraintVersion) { + if (serializationVersion > ObjectConstraintConeTwist::constraintVersion) { assert(false); return; } @@ -370,9 +327,12 @@ void ObjectConstraintConeTwist::deserialize(QByteArray serializedArguments) { 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 459618f101..ea8b2aadb6 100644 --- a/libraries/physics/src/ObjectConstraintConeTwist.h +++ b/libraries/physics/src/ObjectConstraintConeTwist.h @@ -46,9 +46,6 @@ protected: 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 From d189f3f110c3875f2f8c4cc12e281215598c4a25 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 12 May 2017 12:39:10 -0700 Subject: [PATCH 132/146] Fix the twitter mention --- scripts/system/html/js/SnapshotReview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 32a0956615..946e04beef 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -415,7 +415,7 @@ function updateShareInfo(containerID, storyID) { facebookButton.setAttribute("href", 'https://www.facebook.com/dialog/feed?app_id=1585088821786423&link=' + shareURL); twitterButton.setAttribute("target", "_blank"); - twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=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); } From 630d95a812f8a52f67dab8e6169633472b35953e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 12 May 2017 13:03:51 -0700 Subject: [PATCH 133/146] fix constraint import some more --- libraries/entities/src/EntityTree.cpp | 3 ++- libraries/physics/src/ObjectConstraintBallSocket.cpp | 8 +++----- libraries/physics/src/ObjectDynamic.cpp | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 04a8e89fef..457b7c21f6 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1622,7 +1622,8 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra }; entityTreeElement->forEachEntity([&args, &getMapped, &element](EntityItemPointer item) { - EntityItemID newID = getMapped(item->getEntityItemID()); + EntityItemID oldID = item->getEntityItemID(); + EntityItemID newID = getMapped(oldID); EntityItemProperties properties = item->getProperties(); EntityItemID oldParentID = properties.getParentID(); diff --git a/libraries/physics/src/ObjectConstraintBallSocket.cpp b/libraries/physics/src/ObjectConstraintBallSocket.cpp index 4b6e72092e..fb36a49e92 100644 --- a/libraries/physics/src/ObjectConstraintBallSocket.cpp +++ b/libraries/physics/src/ObjectConstraintBallSocket.cpp @@ -178,11 +178,9 @@ bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { QVariantMap ObjectConstraintBallSocket::getArguments() { QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { - if (_constraint) { - arguments["pivot"] = glmToQMap(_pivotInA); - arguments["otherEntityID"] = _otherID; - arguments["otherPivot"] = glmToQMap(_pivotInB); - } + arguments["pivot"] = glmToQMap(_pivotInA); + arguments["otherEntityID"] = _otherID; + arguments["otherPivot"] = glmToQMap(_pivotInB); }); return arguments; } diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp index 16be10a8f0..253739375a 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -35,8 +35,9 @@ void ObjectDynamic::remapIDs(QHash& map) { QHash::iterator iter = map.find(_otherID); if (iter == map.end()) { // not found, add it + QUuid oldOtherID = _otherID; _otherID = QUuid::createUuid(); - map.insert(_otherID, _otherID); + map.insert(oldOtherID, _otherID); } else { _otherID = iter.value(); } From fcc5e1221196fcbb58ae5d7185e6cb3e81c245d4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 12 May 2017 13:19:15 -0700 Subject: [PATCH 134/146] redelete these --- libraries/physics/src/ObjectActionMotor.cpp | 203 -------------------- libraries/physics/src/ObjectActionMotor.h | 40 ---- 2 files changed, 243 deletions(-) delete mode 100644 libraries/physics/src/ObjectActionMotor.cpp delete mode 100644 libraries/physics/src/ObjectActionMotor.h diff --git a/libraries/physics/src/ObjectActionMotor.cpp b/libraries/physics/src/ObjectActionMotor.cpp deleted file mode 100644 index e7a8dcfa70..0000000000 --- a/libraries/physics/src/ObjectActionMotor.cpp +++ /dev/null @@ -1,203 +0,0 @@ -// -// ObjectActionMotor.cpp -// libraries/physics/src -// -// Created by Seth Alves 2017-4-30 -// 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 "QVariantGLM.h" - -#include "ObjectActionMotor.h" - -#include "PhysicsLogging.h" - -const glm::vec3 MOTOR_MAX_SPEED = glm::vec3(PI*10.0f, PI*10.0f, PI*10.0f); -const float MAX_MOTOR_TIMESCALE = 600.0f; // 10 min is a long time - -const uint16_t ObjectActionMotor::motorVersion = 1; - - -ObjectActionMotor::ObjectActionMotor(const QUuid& id, EntityItemPointer ownerEntity) : - ObjectAction(DYNAMIC_TYPE_MOTOR, id, ownerEntity) -{ - #if WANT_DEBUG - qCDebug(physics) << "ObjectActionMotor::ObjectActionMotor"; - #endif - - qCWarning(physics) << "action type \"motor\" doesn't yet work."; -} - -ObjectActionMotor::~ObjectActionMotor() { - #if WANT_DEBUG - qCDebug(physics) << "ObjectActionMotor::~ObjectActionMotor"; - #endif -} - -void ObjectActionMotor::updateActionWorker(btScalar deltaTimeStep) { - SpatiallyNestablePointer other = getOther(); - - withReadLock([&]{ - auto ownerEntity = _ownerEntity.lock(); - if (!ownerEntity) { - return; - } - - void* physicsInfo = ownerEntity->getPhysicsInfo(); - if (!physicsInfo) { - return; - } - ObjectMotionState* motionState = static_cast(physicsInfo); - btRigidBody* rigidBody = motionState->getRigidBody(); - if (!rigidBody) { - qCDebug(physics) << "ObjectActionMotor::updateActionWorker no rigidBody"; - return; - } - - if (_angularTimeScale < MAX_MOTOR_TIMESCALE) { - - if (other) { - glm::vec3 otherAngularVelocity = other->getAngularVelocity(); - rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget + otherAngularVelocity)); - } else { - rigidBody->setAngularVelocity(glmToBullet(_angularVelocityTarget)); - } - } - }); -} - -const float MIN_TIMESCALE = 0.1f; - - -bool ObjectActionMotor::updateArguments(QVariantMap arguments) { - glm::vec3 angularVelocityTarget; - float angularTimeScale; - QUuid otherID; - bool ok; - - - bool needUpdate = false; - bool somethingChanged = ObjectDynamic::updateArguments(arguments); - withReadLock([&]{ - - ok = true; - angularVelocityTarget = EntityDynamicInterface::extractVec3Argument("motor action", arguments, - "targetAngularVelocity", ok, false); - if (!ok) { - angularVelocityTarget = _angularVelocityTarget; - } - - ok = true; - angularTimeScale = - EntityDynamicInterface::extractFloatArgument("motor action", arguments, "angularTimeScale", ok, false); - if (!ok) { - angularTimeScale = _angularTimeScale; - } - - ok = true; - otherID = QUuid(EntityDynamicInterface::extractStringArgument("motor action", arguments, "otherID", ok, false)); - if (!ok) { - otherID = _otherID; - } - - if (somethingChanged || - angularVelocityTarget != _angularVelocityTarget || - angularTimeScale != _angularTimeScale || - otherID != _otherID) { - // something changed - needUpdate = true; - } - }); - - if (needUpdate) { - withWriteLock([&] { - _angularVelocityTarget = angularVelocityTarget; - _angularTimeScale = glm::max(MIN_TIMESCALE, glm::abs(angularTimeScale)); - _otherID = otherID; - _active = true; - - auto ownerEntity = _ownerEntity.lock(); - if (ownerEntity) { - ownerEntity->setDynamicDataDirty(true); - ownerEntity->setDynamicDataNeedsTransmit(true); - } - }); - activateBody(); - } - - return true; -} - -QVariantMap ObjectActionMotor::getArguments() { - QVariantMap arguments = ObjectDynamic::getArguments(); - withReadLock([&] { - arguments["targetAngularVelocity"] = glmToQMap(_angularVelocityTarget); - arguments["angularTimeScale"] = _angularTimeScale; - - arguments["otherID"] = _otherID; - }); - return arguments; -} - -void ObjectActionMotor::serializeParameters(QDataStream& dataStream) const { - withReadLock([&] { - dataStream << localTimeToServerTime(_expires); - dataStream << _tag; - dataStream << _otherID; - - dataStream << _angularVelocityTarget; - dataStream << _angularTimeScale; - }); -} - -QByteArray ObjectActionMotor::serialize() const { - QByteArray serializedActionArguments; - QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); - - dataStream << DYNAMIC_TYPE_MOTOR; - dataStream << getID(); - dataStream << ObjectActionMotor::motorVersion; - - serializeParameters(dataStream); - - return serializedActionArguments; -} - -void ObjectActionMotor::deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream) { - withWriteLock([&] { - quint64 serverExpires; - dataStream >> serverExpires; - _expires = serverTimeToLocalTime(serverExpires); - dataStream >> _tag; - dataStream >> _otherID; - - dataStream >> _angularVelocityTarget; - dataStream >> _angularTimeScale; - - _active = true; - }); -} - -void ObjectActionMotor::deserialize(QByteArray serializedArguments) { - QDataStream dataStream(serializedArguments); - - EntityDynamicType type; - dataStream >> type; - assert(type == getType()); - - QUuid id; - dataStream >> id; - assert(id == getID()); - - uint16_t serializationVersion; - dataStream >> serializationVersion; - if (serializationVersion != ObjectActionMotor::motorVersion) { - assert(false); - return; - } - - deserializeParameters(serializedArguments, dataStream); -} diff --git a/libraries/physics/src/ObjectActionMotor.h b/libraries/physics/src/ObjectActionMotor.h deleted file mode 100644 index 60044db241..0000000000 --- a/libraries/physics/src/ObjectActionMotor.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// ObjectActionMotor.h -// libraries/physics/src -// -// Created by Seth Alves 2017-4-30 -// 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_ObjectActionMotor_h -#define hifi_ObjectActionMotor_h - -#include "ObjectAction.h" - -class ObjectActionMotor : public ObjectAction { -public: - ObjectActionMotor(const QUuid& id, EntityItemPointer ownerEntity); - virtual ~ObjectActionMotor(); - - virtual bool updateArguments(QVariantMap arguments) override; - virtual QVariantMap getArguments() override; - - virtual void updateActionWorker(float deltaTimeStep) override; - - virtual QByteArray serialize() const override; - virtual void deserialize(QByteArray serializedArguments) override; - -protected: - static const uint16_t motorVersion; - - glm::vec3 _angularVelocityTarget; - float _angularTimeScale { FLT_MAX }; - - void serializeParameters(QDataStream& dataStream) const; - void deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream); -}; - -#endif // hifi_ObjectActionMotor_h From a12e8e34dbd5833b8013cfbcfbeac267a32538e1 Mon Sep 17 00:00:00 2001 From: Rob Kayson Date: Fri, 12 May 2017 15:06:22 -0700 Subject: [PATCH 135/146] coding standard --- scripts/tutorials/createFloatingLanternBox.js | 7 +- .../entity_scripts/floatingLantern.js | 168 +++++++++--------- .../entity_scripts/floatingLanternBox.js | 20 ++- 3 files changed, 99 insertions(+), 96 deletions(-) diff --git a/scripts/tutorials/createFloatingLanternBox.js b/scripts/tutorials/createFloatingLanternBox.js index 611e995fcb..a925cfff22 100644 --- a/scripts/tutorials/createFloatingLanternBox.js +++ b/scripts/tutorials/createFloatingLanternBox.js @@ -19,6 +19,7 @@ var SCRIPT_URL = Script.resolvePath("./entity_scripts/floatingLanternBox.js?v=" 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", @@ -31,9 +32,9 @@ var lanternBox = { position: START_POSITION, lifetime: LIFETIME, dimensions: { - x: 0.8696, - y: 0.58531, - z: 0.9264 + x: 0.8696 * SCALE_FACTOR, + y: 0.58531 * SCALE_FACTOR, + z: 0.9264 * SCALE_FACTOR }, owningAvatarID: MyAvatar.sessionUUID }; diff --git a/scripts/tutorials/entity_scripts/floatingLantern.js b/scripts/tutorials/entity_scripts/floatingLantern.js index 8fa2828c90..aa25dc0003 100644 --- a/scripts/tutorials/entity_scripts/floatingLantern.js +++ b/scripts/tutorials/entity_scripts/floatingLantern.js @@ -14,93 +14,93 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html (function() { - var _this; + 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}; + 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) { + floatingLantern = function() { + _this = this; this.updateConnected = false; - Script.update.disconnect(this.update); - } - } - }; + }; - return new floatingLantern(); + 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 index 2c483f6129..ba44fbaa9d 100644 --- a/scripts/tutorials/entity_scripts/floatingLanternBox.js +++ b/scripts/tutorials/entity_scripts/floatingLanternBox.js @@ -21,6 +21,7 @@ var LIFETIME = 120; var RESPAWN_INTERVAL = 1000; var MAX_LANTERNS = 4; + var SCALE_FACTOR = 1; var LANTERN = { type: "Model", @@ -29,9 +30,9 @@ modelURL: LANTERN_MODEL_URL, script: LANTERN_SCRIPT_URL, dimensions: { - x: 0.2049, - y: 0.4, - z: 0.2049 + x: 0.2049 * SCALE_FACTOR, + y: 0.4 * SCALE_FACTOR, + z: 0.2049 * SCALE_FACTOR }, gravity: { x: 0, @@ -57,14 +58,15 @@ this.entityID = entityID; var props = Entities.getEntityProperties(this.entityID); - if(props.owningAvatarID === MyAvatar.sessionUUID){ + 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); + if (this.respawnTimer) { + Script.clearInterval(this.respawnTimer); + } }, spawnAllLanterns: function() { @@ -72,14 +74,14 @@ var lanternCount = 0; var nearbyEntities = Entities.findEntities(props.position, props.dimensions.x * 0.75); - for(var i = 0; i < nearbyEntities.length; i++) { + for (var i = 0; i < nearbyEntities.length; i++) { var name = Entities.getEntityProperties(nearbyEntities[i], ["name"]).name; - if(name === "Floating Lantern") { + if (name === "Floating Lantern") { lanternCount++; } } - while(lanternCount++ < MAX_LANTERNS) { + while (lanternCount++ < MAX_LANTERNS) { this.spawnLantern(); } }, From f3d8d1641f62b36365671dcf8f085bbfc97aa40e Mon Sep 17 00:00:00 2001 From: Rob Kayson Date: Fri, 12 May 2017 15:10:40 -0700 Subject: [PATCH 136/146] fix indent createFloatingLanternBox.js --- scripts/tutorials/createFloatingLanternBox.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/scripts/tutorials/createFloatingLanternBox.js b/scripts/tutorials/createFloatingLanternBox.js index a925cfff22..c84214e295 100644 --- a/scripts/tutorials/createFloatingLanternBox.js +++ b/scripts/tutorials/createFloatingLanternBox.js @@ -22,21 +22,21 @@ 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 + 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); From 9fc0ad0d28dc79aa258e82f7c9af0229163570a6 Mon Sep 17 00:00:00 2001 From: Rob Kayson Date: Fri, 12 May 2017 15:12:43 -0700 Subject: [PATCH 137/146] fix indent floatingLanternBox --- .../entity_scripts/floatingLanternBox.js | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/scripts/tutorials/entity_scripts/floatingLanternBox.js b/scripts/tutorials/entity_scripts/floatingLanternBox.js index ba44fbaa9d..b5fb0c27d9 100644 --- a/scripts/tutorials/entity_scripts/floatingLanternBox.js +++ b/scripts/tutorials/entity_scripts/floatingLanternBox.js @@ -15,89 +15,89 @@ (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 _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 - }; + 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 = function() { + _this = this; + }; - lanternBox.prototype = { + lanternBox.prototype = { - preload: function(entityID) { - this.entityID = entityID; - var props = Entities.getEntityProperties(this.entityID); + 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); - } - }, + 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); - } - }, + 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); + 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++; + 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); } - } + }; - 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(); + return new lanternBox(); }); From e6020e0137ae2e9abd0d1b88c235efc62002b498 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Sat, 13 May 2017 00:40:07 +0100 Subject: [PATCH 138/146] added lookup table for tracking result and configs --- plugins/openvr/src/ViveControllerManager.cpp | 59 ++++++++------------ plugins/openvr/src/ViveControllerManager.h | 3 +- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index ccc843b5cc..5e22272dd6 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -56,6 +56,14 @@ 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; @@ -68,22 +76,10 @@ static bool sortPucksYPosition(std::pair firstPuck, static QString deviceTrackingResultToString(vr::ETrackingResult trackingResult) { QString result; - switch (trackingResult) { - case vr::TrackingResult_Uninitialized: - result = "vr::TrackingResult_Uninitialized"; - break; - case vr::TrackingResult_Calibrating_InProgress: - result = "vr::TrackingResult_Calibrating_InProgess"; - break; - case vr::TrackingResult_Calibrating_OutOfRange: - result = "vr::TrackingResult_Calibrating_OutOfRange"; - break; - case vr::TrackingResult_Running_OK: - result = "vr::TrackingResult_Running_OK"; - break; - case vr::TrackingResult_Running_OutOfRange: - result = "vr::TrackingResult_Running_OutOfRange"; - break; + auto iterator = TRACKING_RESULT_TO_STRING.find(trackingResult); + + if (iterator != TRACKING_RESULT_TO_STRING.end()) { + return iterator->second; } return result; } @@ -166,6 +162,15 @@ 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(); @@ -626,25 +631,7 @@ void ViveControllerManager::InputDevice::saveSettings() const { } QString ViveControllerManager::InputDevice::configToString(Config config) { - QString currentConfig; - switch (config) { - case Config::Auto: - currentConfig = "Auto"; - break; - - case Config::Feet: - currentConfig = "Feet"; - break; - - case Config::FeetAndHips: - currentConfig = "FeetAndHips"; - break; - - case Config::FeetHipsAndChest: - currentConfig = "FeetHipsAndChest"; - break; - } - return currentConfig; + return _configStringMap[config]; } void ViveControllerManager::InputDevice::setConfigFromString(const QString& value) { @@ -665,7 +652,7 @@ void ViveControllerManager::InputDevice::createPreferences() { static const QString VIVE_PUCKS_CONFIG = "Vive Pucks Configuration"; { - auto getter = [this]()->QString { return configToString(_preferedConfig); }; + 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"); diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index d7ab77ddbc..fa2566da45 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -51,7 +51,7 @@ public: private: class InputDevice : public controller::InputDevice { public: - InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) { createPreferences(); } + InputDevice(vr::IVRSystem*& system); private: // Device functions controller::Input::NamedVector getAvailableInputs() const override; @@ -111,6 +111,7 @@ private: 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; From bdb0414add26b3b792d9c1d16c36a933069acabb Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 12 May 2017 18:04:22 -0700 Subject: [PATCH 139/146] Adding a validation step at runtime for the cached KTX file in order to regenerate them if anything seems wrong --- libraries/gpu/src/gpu/Texture.cpp | 7 ++++++- libraries/gpu/src/gpu/Texture_ktx.cpp | 7 +++++++ libraries/ktx/src/ktx/KTX.cpp | 6 ++++++ libraries/ktx/src/ktx/Reader.cpp | 19 ++++++++++++++++++- libraries/ktx/src/ktx/Writer.cpp | 3 ++- .../src/model-networking/KTXCache.cpp | 2 +- .../src/model-networking/TextureCache.cpp | 4 +++- libraries/networking/src/FileCache.cpp | 11 ++++++++--- libraries/networking/src/FileCache.h | 2 +- libraries/shared/src/shared/Storage.cpp | 4 ++++ 10 files changed, 56 insertions(+), 9 deletions(-) diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 0f84d2a3c9..a94a0e1621 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -441,7 +441,10 @@ 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) { + if (storage->size() < expectedSize) { + _storage->assignMipData(level, storage); + _stamp++; + } else if (size == expectedSize) { _storage->assignMipData(level, storage); _stamp++; } else if (size > expectedSize) { @@ -469,6 +472,8 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); auto size = storage->size(); if (size <= expectedSize) { + _stamp++; + } else if (size == expectedSize) { _storage->assignMipFaceData(level, face, storage); _stamp++; } else if (size > expectedSize) { diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 3fc4e0d432..524fd0a88c 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -542,6 +542,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/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp index 38bb91e5c2..ff80695280 100644 --- a/libraries/ktx/src/ktx/KTX.cpp +++ b/libraries/ktx/src/ktx/KTX.cpp @@ -114,6 +114,9 @@ size_t Header::evalFaceSize(uint32_t level) const { } size_t Header::evalImageSize(uint32_t level) const { auto faceSize = evalFaceSize(level); + if ((faceSize < 4) || ((faceSize & 0x3) != 0)) { + return 0; + } if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { return faceSize; } else { @@ -139,6 +142,9 @@ ImageDescriptors Header::generateImageDescriptors() const { size_t imageOffset = 0; for (uint32_t level = 0; level < numberOfMipmapLevels; ++level) { auto imageSize = static_cast(evalImageSize(level)); + if ((imageSize < 4) || ((imageSize & 0x3) != 0)) { + return ImageDescriptors(); + } if (imageSize == 0) { return ImageDescriptors(); } diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp index 440e2f048c..49fc8bac70 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(images.size()); + if (imageSize != expectedImageSize) { + break; + } else if ((imageSize < 4) || (imageSize & 0x3)) { + 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..50f63767a0 100644 --- a/libraries/ktx/src/ktx/Writer.cpp +++ b/libraries/ktx/src/ktx/Writer.cpp @@ -210,7 +210,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..47fc62a9b5 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -792,6 +792,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 +837,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/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp index 43055e7ed6..8f3509d8f3 100644 --- a/libraries/networking/src/FileCache.cpp +++ b/libraries/networking/src/FileCache.cpp @@ -97,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); @@ -107,8 +107,13 @@ 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(); + } } QSaveFile saveFile(QString::fromStdString(filepath)); 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/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index f6585e6ecb..7cff876aa0 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -21,6 +21,10 @@ ViewStorage::ViewStorage(const storage::StoragePointer& owner, size_t size, cons StoragePointer Storage::createView(size_t viewSize, size_t offset) const { auto selfSize = size(); + if ((viewSize < 4) || ((viewSize & 0x3) != 0)) { + throw std::runtime_error("Invalid mapping range"); + } + if (0 == viewSize) { viewSize = selfSize; } From 5bc8e098654bb0cbaa7897c02067278f38af4652 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 12 May 2017 18:10:48 -0700 Subject: [PATCH 140/146] Fixing the test... --- libraries/gpu/src/gpu/Texture.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index a94a0e1621..b027c25907 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -471,7 +471,8 @@ 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) { + if (size < expectedSize) { + _storage->assignMipFaceData(level, face, storage); _stamp++; } else if (size == expectedSize) { _storage->assignMipFaceData(level, face, storage); From d734358290cd4168591f1f4ac53d1471cb3b9e91 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 12 May 2017 18:15:00 -0700 Subject: [PATCH 141/146] Adding a comment for debug sake --- libraries/gpu/src/gpu/Texture.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index b027c25907..a545be9088 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -441,6 +441,7 @@ 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(); + // 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++; @@ -471,6 +472,7 @@ 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(); + // 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++; From f35b0297fa991ed2794500940aa53b69dbdda58f Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 12 May 2017 18:25:07 -0700 Subject: [PATCH 142/146] Replace the alignment test by a nice function --- libraries/ktx/src/ktx/KTX.cpp | 7 +++++-- libraries/ktx/src/ktx/KTX.h | 1 + libraries/ktx/src/ktx/Reader.cpp | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp index ff80695280..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,7 +117,7 @@ size_t Header::evalFaceSize(uint32_t level) const { } size_t Header::evalImageSize(uint32_t level) const { auto faceSize = evalFaceSize(level); - if ((faceSize < 4) || ((faceSize & 0x3) != 0)) { + if (!checkAlignment(faceSize)) { return 0; } if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { @@ -142,7 +145,7 @@ ImageDescriptors Header::generateImageDescriptors() const { size_t imageOffset = 0; for (uint32_t level = 0; level < numberOfMipmapLevels; ++level) { auto imageSize = static_cast(evalImageSize(level)); - if ((imageSize < 4) || ((imageSize & 0x3) != 0)) { + if (!checkAlignment(imageSize)) { return ImageDescriptors(); } if (imageSize == 0) { diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h index e8fa019a07..656cf93f34 100644 --- a/libraries/ktx/src/ktx/KTX.h +++ b/libraries/ktx/src/ktx/KTX.h @@ -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(); diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp index 49fc8bac70..e69ce53551 100644 --- a/libraries/ktx/src/ktx/Reader.cpp +++ b/libraries/ktx/src/ktx/Reader.cpp @@ -151,7 +151,7 @@ namespace ktx { auto expectedImageSize = header.evalImageSize(images.size()); if (imageSize != expectedImageSize) { break; - } else if ((imageSize < 4) || (imageSize & 0x3)) { + } else if (!Header::checkAlignment(imageSize)) { break; } From c4e8885842ef82793227f1716887b20c41c28856 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 12 May 2017 18:27:58 -0700 Subject: [PATCH 143/146] not testing in storage for alignment --- libraries/shared/src/shared/Storage.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index 7cff876aa0..f6585e6ecb 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -21,10 +21,6 @@ ViewStorage::ViewStorage(const storage::StoragePointer& owner, size_t size, cons StoragePointer Storage::createView(size_t viewSize, size_t offset) const { auto selfSize = size(); - if ((viewSize < 4) || ((viewSize & 0x3) != 0)) { - throw std::runtime_error("Invalid mapping range"); - } - if (0 == viewSize) { viewSize = selfSize; } From b38be7e561f5a2b30a8179cab40fc8626ac74b65 Mon Sep 17 00:00:00 2001 From: Sam Cake Date: Fri, 12 May 2017 20:20:03 -0700 Subject: [PATCH 144/146] warning -1 --- libraries/ktx/src/ktx/Reader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp index e69ce53551..1b63af5262 100644 --- a/libraries/ktx/src/ktx/Reader.cpp +++ b/libraries/ktx/src/ktx/Reader.cpp @@ -148,7 +148,7 @@ namespace ktx { size_t imageSize = *reinterpret_cast(currentPtr); currentPtr += sizeof(uint32_t); - auto expectedImageSize = header.evalImageSize(images.size()); + auto expectedImageSize = header.evalImageSize((uint32_t) images.size()); if (imageSize != expectedImageSize) { break; } else if (!Header::checkAlignment(imageSize)) { From 20c27fc1338593498cb851498ddb38d004337589 Mon Sep 17 00:00:00 2001 From: Vladyslav Stelmakhovskyi Date: Sat, 13 May 2017 14:20:28 +0200 Subject: [PATCH 145/146] Fix crash on save Map data settings from scripts --- libraries/shared/src/SettingHelpers.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/SettingHelpers.cpp b/libraries/shared/src/SettingHelpers.cpp index 9e2d15fcd0..cbda4e4096 100644 --- a/libraries/shared/src/SettingHelpers.cpp +++ b/libraries/shared/src/SettingHelpers.cpp @@ -126,7 +126,16 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { } switch (variantType) { - case QVariant::Map: + case QVariant::Map: { + auto varmap = variant.toMap(); + for (auto mapit = varmap.cbegin(); mapit != varmap.cend(); ++mapit) { + auto& mapkey = mapit.key(); + auto& mapvariant = mapit.value(); + object.insert(key + "/" + mapkey, QJsonValue::fromVariant(mapvariant)); + } + break; + } + case QVariant::List: case QVariant::Hash: { qCritical() << "Unsupported variant type" << variant.typeName(); From cc10fc81b79cdd7139e683b3967dd844f30f2e7c Mon Sep 17 00:00:00 2001 From: Vladyslav Stelmakhovskyi Date: Sat, 13 May 2017 20:57:04 +0200 Subject: [PATCH 146/146] Fix crash in sit script --- interface/src/avatar/MyAvatar.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index fd547e39e0..3da9b8a214 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -291,6 +291,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); }