diff --git a/cmake/modules/FindFBX.cmake b/cmake/modules/FindFBX.cmake new file mode 100644 index 0000000000..7f6a424aa1 --- /dev/null +++ b/cmake/modules/FindFBX.cmake @@ -0,0 +1,114 @@ +# Locate the FBX SDK +# +# Defines the following variables: +# +# FBX_FOUND - Found the FBX SDK +# FBX_VERSION - Version number +# FBX_INCLUDE_DIRS - Include directories +# FBX_LIBRARIES - The libraries to link to +# +# Accepts the following variables as input: +# +# FBX_VERSION - as a CMake variable, e.g. 2017.0.1 +# FBX_ROOT - (as a CMake or environment variable) +# The root directory of the FBX SDK install + +# adapted from https://github.com/ufz-vislab/VtkFbxConverter/blob/master/FindFBX.cmake +# which uses the MIT license (https://github.com/ufz-vislab/VtkFbxConverter/blob/master/LICENSE.txt) + +if (NOT FBX_VERSION) + if (WIN32) + set(FBX_VERSION 2017.1) + else() + set(FBX_VERSION 2017.0.1) + endif() +endif() + +string(REGEX REPLACE "^([0-9]+).*$" "\\1" FBX_VERSION_MAJOR "${FBX_VERSION}") +string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_MINOR "${FBX_VERSION}") +string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_PATCH "${FBX_VERSION}") + +set(FBX_MAC_LOCATIONS "/Applications/Autodesk/FBX\ SDK/${FBX_VERSION}") + +if (WIN32) + string(REGEX REPLACE "\\\\" "/" WIN_PROGRAM_FILES_X64_DIRECTORY $ENV{ProgramW6432}) +endif() + +set(FBX_WIN_LOCATIONS "${WIN_PROGRAM_FILES_X64_DIRECTORY}/Autodesk/FBX/FBX SDK/${FBX_VERSION}") + +set(FBX_SEARCH_LOCATIONS $ENV{FBX_ROOT} ${FBX_ROOT} ${FBX_MAC_LOCATIONS} ${FBX_WIN_LOCATIONS}) + +function(_fbx_append_debugs _endvar _library) + if (${_library} AND ${_library}_DEBUG) + set(_output optimized ${${_library}} debug ${${_library}_DEBUG}) + else() + set(_output ${${_library}}) + endif() + + set(${_endvar} ${_output} PARENT_SCOPE) +endfunction() + +if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + set(fbx_compiler clang) +elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") + set(fbx_compiler gcc4) +endif() + +function(_fbx_find_library _name _lib _suffix) + if (MSVC12) + set(VS_PREFIX vs2013) + endif() + + if (MSVC11) + set(VS_PREFIX vs2012) + endif() + + if (MSVC10) + set(VS_PREFIX vs2010) + endif() + + if (MSVC90) + set(VS_PREFIX vs2008) + endif() + + find_library(${_name} + NAMES ${_lib} + HINTS ${FBX_SEARCH_LOCATIONS} + PATH_SUFFIXES lib/${fbx_compiler}/${_suffix} lib/${fbx_compiler}/ub/${_suffix} lib/${VS_PREFIX}/x64/${_suffix} + ) + + mark_as_advanced(${_name}) +endfunction() + +find_path(FBX_INCLUDE_DIR fbxsdk.h + PATHS ${FBX_SEARCH_LOCATIONS} + PATH_SUFFIXES include +) +mark_as_advanced(FBX_INCLUDE_DIR) + +if (WIN32) + _fbx_find_library(FBX_LIBRARY libfbxsdk-md release) + _fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk-md debug) +elseif (APPLE) + find_library(CARBON NAMES Carbon) + find_library(SYSTEM_CONFIGURATION NAMES SystemConfiguration) + _fbx_find_library(FBX_LIBRARY libfbxsdk.a release) + _fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk.a debug) +endif() + +include(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(FBX DEFAULT_MSG FBX_LIBRARY FBX_INCLUDE_DIR) + +if (FBX_FOUND) + set(FBX_INCLUDE_DIRS ${FBX_INCLUDE_DIR}) + _fbx_append_debugs(FBX_LIBRARIES FBX_LIBRARY) + add_definitions(-DFBXSDK_NEW_API) + + if (WIN32) + add_definitions(-DK_PLUGIN -DK_FBXSDK -DK_NODLL) + set(CMAKE_EXE_LINKER_FLAGS /NODEFAULTLIB:\"LIBCMT\") + set(FBX_LIBRARIES ${FBX_LIBRARIES} Wininet.lib) + elseif (APPLE) + set(FBX_LIBRARIES ${FBX_LIBRARIES} ${CARBON} ${SYSTEM_CONFIGURATION}) + endif() +endif() diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 7f91d8bb2e..3777f2bb50 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -22,6 +22,8 @@ #include "Forward.h" #include "Resource.h" +const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; + namespace ktx { class KTX; using KTXUniquePointer = std::unique_ptr; diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index d9dd1105cd..3bf45ace98 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -37,7 +37,8 @@ enum Type { CUBE_TEXTURE, OCCLUSION_TEXTURE, SCATTERING_TEXTURE = OCCLUSION_TEXTURE, - LIGHTMAP_TEXTURE + LIGHTMAP_TEXTURE, + UNUSED_TEXTURE }; using TextureLoader = std::function; diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index aabc7fcb85..7dab18d457 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -27,8 +27,6 @@ #include "KTXCache.h" -const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; - namespace gpu { class Batch; } diff --git a/libraries/shared/src/Profile.cpp b/libraries/shared/src/Profile.cpp index 7a8a8f0570..eb7440f4b3 100644 --- a/libraries/shared/src/Profile.cpp +++ b/libraries/shared/src/Profile.cpp @@ -34,7 +34,7 @@ Q_LOGGING_CATEGORY(trace_simulation_physics_detail, "trace.simulation.physics.de #endif static bool tracingEnabled() { - return DependencyManager::get()->isEnabled(); + return DependencyManager::isSet() && DependencyManager::get()->isEnabled(); } Duration::Duration(const QLoggingCategory& category, const QString& name, uint32_t argbColor, uint64_t payload, const QVariantMap& baseArgs) : _name(name), _category(category) { diff --git a/libraries/shared/src/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/CMakeLists.txt b/tools/CMakeLists.txt index 8dc993e6fe..0561956709 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -19,3 +19,6 @@ set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") add_subdirectory(atp-get) set_target_properties(atp-get PROPERTIES FOLDER "Tools") + +add_subdirectory(oven) +set_target_properties(oven PROPERTIES FOLDER "Tools") diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt new file mode 100644 index 0000000000..24c8a9a0e2 --- /dev/null +++ b/tools/oven/CMakeLists.txt @@ -0,0 +1,19 @@ +set(TARGET_NAME oven) + +setup_hifi_project(Widgets Gui Concurrent) + +link_hifi_libraries(networking shared image gpu ktx) + +if (WIN32) + package_libraries_for_deployment() +endif () + +# try to find the FBX SDK but fail silently if we don't +# because this tool is not built by default +find_package(FBX) +if (FBX_FOUND) + target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) + target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR}) +endif () + +set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) diff --git a/tools/oven/src/Baker.cpp b/tools/oven/src/Baker.cpp new file mode 100644 index 0000000000..c0cbd8d124 --- /dev/null +++ b/tools/oven/src/Baker.cpp @@ -0,0 +1,32 @@ +// +// Baker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ModelBakingLoggingCategory.h" + +#include "Baker.h" + +void Baker::handleError(const QString& error) { + qCCritical(model_baking).noquote() << error; + _errorList.append(error); + emit finished(); +} + +void Baker::handleErrors(const QStringList& errors) { + // we're appending errors, presumably from a baking operation we called + // add those to our list and emit that we are finished + _errorList.append(errors); + emit finished(); +} + +void Baker::handleWarning(const QString& warning) { + qCWarning(model_baking).noquote() << warning; + _warningList.append(warning); +} diff --git a/tools/oven/src/Baker.h b/tools/oven/src/Baker.h new file mode 100644 index 0000000000..d7107428bf --- /dev/null +++ b/tools/oven/src/Baker.h @@ -0,0 +1,43 @@ +// +// Baker.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Baker_h +#define hifi_Baker_h + +#include + +class Baker : public QObject { + Q_OBJECT + +public: + bool hasErrors() const { return !_errorList.isEmpty(); } + QStringList getErrors() const { return _errorList; } + + bool hasWarnings() const { return !_warningList.isEmpty(); } + QStringList getWarnings() const { return _warningList; } + +public slots: + virtual void bake() = 0; + +signals: + void finished(); + +protected: + void handleError(const QString& error); + void handleWarning(const QString& warning); + + void handleErrors(const QStringList& errors); + + QStringList _errorList; + QStringList _warningList; +}; + +#endif // hifi_Baker_h diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp new file mode 100644 index 0000000000..cb2a6bca29 --- /dev/null +++ b/tools/oven/src/DomainBaker.cpp @@ -0,0 +1,475 @@ +// +// DomainBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include "Gzip.h" + +#include "Oven.h" + +#include "DomainBaker.h" + +DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, + const QString& baseOutputPath, const QUrl& destinationPath) : + _localEntitiesFileURL(localModelFileURL), + _domainName(domainName), + _baseOutputPath(baseOutputPath) +{ + // make sure the destination path has a trailing slash + if (!destinationPath.toString().endsWith('/')) { + _destinationPath = destinationPath.toString() + '/'; + } else { + _destinationPath = destinationPath; + } +} + +void DomainBaker::bake() { + setupOutputFolder(); + + if (hasErrors()) { + return; + } + + loadLocalFile(); + + if (hasErrors()) { + return; + } + + enumerateEntities(); + + if (hasErrors()) { + return; + } + + // in case we've baked and re-written all of our entities already, check if we're done + checkIfRewritingComplete(); +} + +void DomainBaker::setupOutputFolder() { + // in order to avoid overwriting previous bakes, we create a special output folder with the domain name and timestamp + + // first, construct the directory name + auto domainPrefix = !_domainName.isEmpty() ? _domainName + "-" : ""; + auto timeNow = QDateTime::currentDateTime(); + + static const QString FOLDER_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; + QString outputDirectoryName = domainPrefix + timeNow.toString(FOLDER_TIMESTAMP_FORMAT); + + // make sure we can create that directory + QDir outputDir { _baseOutputPath }; + + if (!outputDir.mkpath(outputDirectoryName)) { + // add an error to specify that the output directory could not be created + handleError("Could not create output folder"); + + return; + } + + // store the unique output path so we can re-use it when saving baked models + outputDir.cd(outputDirectoryName); + _uniqueOutputPath = outputDir.absolutePath(); + + // add a content folder inside the unique output folder + static const QString CONTENT_OUTPUT_FOLDER_NAME = "content"; + if (!outputDir.mkpath(CONTENT_OUTPUT_FOLDER_NAME)) { + // add an error to specify that the content output directory could not be created + handleError("Could not create content folder"); + return; + } + + _contentOutputPath = outputDir.absoluteFilePath(CONTENT_OUTPUT_FOLDER_NAME); +} + +const QString ENTITIES_OBJECT_KEY = "Entities"; + +void DomainBaker::loadLocalFile() { + // load up the local entities file + QFile entitiesFile { _localEntitiesFileURL.toLocalFile() }; + + if (!entitiesFile.open(QIODevice::ReadOnly)) { + // add an error to our list to specify that the file could not be read + handleError("Could not open entities file"); + + // return to stop processing + return; + } + + // grab a byte array from the file + auto fileContents = entitiesFile.readAll(); + + // check if we need to inflate a gzipped models file or if this was already decompressed + static const QString GZIPPED_ENTITIES_FILE_SUFFIX = "gz"; + if (QFileInfo(_localEntitiesFileURL.toLocalFile()).suffix() == "gz") { + // this was a gzipped models file that we need to decompress + QByteArray uncompressedContents; + gunzip(fileContents, uncompressedContents); + fileContents = uncompressedContents; + } + + // read the file contents to a JSON document + auto jsonDocument = QJsonDocument::fromJson(fileContents); + + // grab the entities object from the root JSON object + _entities = jsonDocument.object()[ENTITIES_OBJECT_KEY].toArray(); + + if (_entities.isEmpty()) { + // add an error to our list stating that the models file was empty + + // return to stop processing + return; + } +} + +const QString ENTITY_MODEL_URL_KEY = "modelURL"; +const QString ENTITY_SKYBOX_KEY = "skybox"; +const QString ENTITY_SKYBOX_URL_KEY = "url"; +const QString ENTITY_KEYLIGHT_KEY = "keyLight"; +const QString ENTITY_KEYLIGHT_AMBIENT_URL_KEY = "ambientURL"; + +void DomainBaker::enumerateEntities() { + qDebug() << "Enumerating" << _entities.size() << "entities from domain"; + + for (auto it = _entities.begin(); it != _entities.end(); ++it) { + // make sure this is a JSON object + if (it->isObject()) { + auto entity = it->toObject(); + + // check if this is an entity with a model URL or is a skybox texture + if (entity.contains(ENTITY_MODEL_URL_KEY)) { + // grab a QUrl for the model URL + QUrl modelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + + // check if the file pointed to by this URL is a bakeable model, by comparing extensions + auto modelFileName = modelURL.fileName(); + + static const QStringList BAKEABLE_MODEL_EXTENSIONS { ".fbx" }; + auto completeLowerExtension = modelFileName.mid(modelFileName.indexOf('.')).toLower(); + + if (BAKEABLE_MODEL_EXTENSIONS.contains(completeLowerExtension)) { + // grab a clean version of the URL without a query or fragment + modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup an FBXBaker for this URL, as long as we don't already have one + if (!_modelBakers.contains(modelURL)) { + QSharedPointer baker { + new FBXBaker(modelURL, _contentOutputPath, []() -> QThread* { + return qApp->getNextWorkerThread(); + }), &FBXBaker::deleteLater + }; + + // make sure our handler is called when the baker is done + connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _modelBakers.insert(modelURL, baker); + + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(qApp->getFBXBakerThread()); + QMetaObject::invokeMethod(baker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that we can easily re-write + // the model URL to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(modelURL, *it); + } + } else { +// // We check now to see if we have either a texture for a skybox or a keylight, or both. +// if (entity.contains(ENTITY_SKYBOX_KEY)) { +// auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); +// if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { +// // we have a URL to a skybox, grab it +// QUrl skyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() }; +// +// // setup a bake of the skybox +// bakeSkybox(skyboxURL, *it); +// } +// } +// +// if (entity.contains(ENTITY_KEYLIGHT_KEY)) { +// auto keyLightObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); +// if (keyLightObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { +// // we have a URL to a skybox, grab it +// QUrl skyboxURL { keyLightObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY].toString() }; +// +// // setup a bake of the skybox +// bakeSkybox(skyboxURL, *it); +// } +// } + } + } + } + + // emit progress now to say we're just starting + emit bakeProgress(0, _totalNumberOfSubBakes); +} + +void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { + + auto skyboxFileName = skyboxURL.fileName(); + + static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { + ".jpg", ".png", ".gif", ".bmp", ".pbm", ".pgm", ".ppm", ".xbm", ".xpm", ".svg" + }; + auto completeLowerExtension = skyboxFileName.mid(skyboxFileName.indexOf('.')).toLower(); + + if (BAKEABLE_SKYBOX_EXTENSIONS.contains(completeLowerExtension)) { + // grab a clean version of the URL without a query or fragment + skyboxURL = skyboxURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a texture baker for this URL, as long as we aren't baking a skybox already + if (!_skyboxBakers.contains(skyboxURL)) { + // setup a baker for this skybox + + QSharedPointer skyboxBaker { + new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath), + &TextureBaker::deleteLater + }; + + // make sure our handler is called when the skybox baker is done + connect(skyboxBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedSkyboxBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _skyboxBakers.insert(skyboxURL, skyboxBaker); + + // move the baker to a worker thread and kickoff the bake + skyboxBaker->moveToThread(qApp->getNextWorkerThread()); + QMetaObject::invokeMethod(skyboxBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the skybox URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(skyboxURL, entity); + } +} + +void DomainBaker::handleFinishedModelBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this FBXBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getFBXUrl(); + + // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // entity objects needing a URL re-write + for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getFBXUrl())) { + + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = entityValue.toObject(); + + // grab the old URL + QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + + // setup a new URL using the prefix we were passed + QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); + + // copy the fragment and query, and user info from the old model URL + newModelURL.setQuery(oldModelURL.query()); + newModelURL.setFragment(oldModelURL.fragment()); + newModelURL.setUserInfo(oldModelURL.userInfo()); + + // set the new model URL as the value in our temp QJsonObject + entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString(); + + // check if the entity also had an animation at the same URL + // in which case it should be replaced with our baked model URL too + const QString ENTITY_ANIMATION_KEY = "animation"; + const QString ENTITIY_ANIMATION_URL_KEY = "url"; + + if (entity.contains(ENTITY_ANIMATION_KEY)) { + auto animationObject = entity[ENTITY_ANIMATION_KEY].toObject(); + + if (animationObject.contains(ENTITIY_ANIMATION_URL_KEY)) { + // grab the old animation URL + QUrl oldAnimationURL { animationObject[ENTITIY_ANIMATION_URL_KEY].toString() }; + + // check if its stripped down version matches our stripped down model URL + if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveQuery | QUrl::RemoveFragment)) { + // the animation URL matched the old model URL, so make the animation URL point to the baked FBX + // with its original query and fragment + auto newAnimationURL = _destinationPath.resolved(baker->getBakedFBXRelativePath()); + newAnimationURL.setQuery(oldAnimationURL.query()); + newAnimationURL.setFragment(oldAnimationURL.fragment()); + newAnimationURL.setUserInfo(oldAnimationURL.userInfo()); + + animationObject[ENTITIY_ANIMATION_URL_KEY] = newAnimationURL.toString(); + + // replace the animation object in the entity object + entity[ENTITY_ANIMATION_KEY] = animationObject; + } + } + } + + // replace our temp object with the value referenced by our QJsonValueRef + entityValue = entity; + } + } else { + // this model failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the model to our warnings + _warningList << baker->getErrors(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getFBXUrl()); + + // drop our shared pointer to this baker so that it gets cleaned up + _modelBakers.remove(baker->getFBXUrl()); + + // emit progress to tell listeners how many models we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last model we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + +void DomainBaker::handleFinishedSkyboxBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this FBXBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getTextureURL(); + + // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // entity objects needing a URL re-write + for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = entityValue.toObject(); + + if (entity.contains(ENTITY_SKYBOX_KEY)) { + auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); + + if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { + if (rewriteSkyboxURL(skyboxObject[ENTITY_SKYBOX_URL_KEY], baker)) { + // we re-wrote the URL, replace the skybox object referenced by the entity object + entity[ENTITY_SKYBOX_KEY] = skyboxObject; + } + } + } + + if (entity.contains(ENTITY_KEYLIGHT_KEY)) { + auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); + + if (ambientObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { + if (rewriteSkyboxURL(ambientObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY], baker)) { + // we re-wrote the URL, replace the ambient object referenced by the entity object + entity[ENTITY_KEYLIGHT_KEY] = ambientObject; + } + } + } + + // replace our temp object with the value referenced by our QJsonValueRef + entityValue = entity; + } + } else { + // this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the model to our warnings + _warningList << baker->getWarnings(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getTextureURL()); + + // drop our shared pointer to this baker so that it gets cleaned up + _skyboxBakers.remove(baker->getTextureURL()); + + // emit progress to tell listeners how many models we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last model we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + +bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) { + // grab the old skybox URL + QUrl oldSkyboxURL { urlValue.toString() }; + + if (oldSkyboxURL.matches(baker->getTextureURL(), QUrl::RemoveQuery | QUrl::RemoveFragment)) { + // change the URL to point to the baked texture with its original query and fragment + + auto newSkyboxURL = _destinationPath.resolved(baker->getBakedTextureFileName()); + newSkyboxURL.setQuery(oldSkyboxURL.query()); + newSkyboxURL.setFragment(oldSkyboxURL.fragment()); + newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); + + urlValue = newSkyboxURL.toString(); + + return true; + } else { + return false; + } +} + +void DomainBaker::checkIfRewritingComplete() { + if (_entitiesNeedingRewrite.isEmpty()) { + writeNewEntitiesFile(); + + if (hasErrors()) { + return; + } + + // we've now written out our new models file - time to say that we are finished up + emit finished(); + } +} + +void DomainBaker::writeNewEntitiesFile() { + // we've enumerated all of our entities and re-written all the URLs we'll be able to re-write + // time to write out a main models.json.gz file + + // first setup a document with the entities array below the entities key + QJsonDocument entitiesDocument; + + QJsonObject rootObject; + rootObject[ENTITIES_OBJECT_KEY] = _entities; + + entitiesDocument.setObject(rootObject); + + // turn that QJsonDocument into a byte array ready for compression + QByteArray jsonByteArray = entitiesDocument.toJson(); + + // compress the json byte array using gzip + QByteArray compressedJson; + gzip(jsonByteArray, compressedJson); + + // write the gzipped json to a new models file + static const QString MODELS_FILE_NAME = "models.json.gz"; + + auto bakedEntitiesFilePath = QDir(_uniqueOutputPath).filePath(MODELS_FILE_NAME); + QFile compressedEntitiesFile { bakedEntitiesFilePath }; + + if (!compressedEntitiesFile.open(QIODevice::WriteOnly) + || (compressedEntitiesFile.write(compressedJson) == -1)) { + + // add an error to our list to state that the output models file could not be created or could not be written to + handleError("Failed to export baked entities file"); + + return; + } + + qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; +} + diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h new file mode 100644 index 0000000000..5244408115 --- /dev/null +++ b/tools/oven/src/DomainBaker.h @@ -0,0 +1,70 @@ +// +// DomainBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DomainBaker_h +#define hifi_DomainBaker_h + +#include +#include +#include +#include + +#include "Baker.h" +#include "FBXBaker.h" +#include "TextureBaker.h" + +class DomainBaker : public Baker { + Q_OBJECT +public: + // This is a real bummer, but the FBX SDK is not thread safe - even with separate FBXManager objects. + // This means that we need to put all of the FBX importing/exporting from the same process on the same thread. + // That means you must pass a usable running QThread when constructing a domain baker. + DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, + const QString& baseOutputPath, const QUrl& destinationPath); + +signals: + void allModelsFinished(); + void bakeProgress(int baked, int total); + +private slots: + virtual void bake() override; + void handleFinishedModelBaker(); + void handleFinishedSkyboxBaker(); + +private: + void setupOutputFolder(); + void loadLocalFile(); + void enumerateEntities(); + void checkIfRewritingComplete(); + void writeNewEntitiesFile(); + + void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity); + bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker); + + QUrl _localEntitiesFileURL; + QString _domainName; + QString _baseOutputPath; + QString _uniqueOutputPath; + QString _contentOutputPath; + QUrl _destinationPath; + + QJsonArray _entities; + + QHash> _modelBakers; + QHash> _skyboxBakers; + + QMultiHash _entitiesNeedingRewrite; + + int _totalNumberOfSubBakes { 0 }; + int _completedSubBakes { 0 }; +}; + +#endif // hifi_DomainBaker_h diff --git a/tools/oven/src/FBXBaker.cpp b/tools/oven/src/FBXBaker.cpp new file mode 100644 index 0000000000..8a72784d7c --- /dev/null +++ b/tools/oven/src/FBXBaker.cpp @@ -0,0 +1,554 @@ +// +// FBXBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 3/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include // need this include so we don't get an error looking for std::isnan + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "ModelBakingLoggingCategory.h" +#include "TextureBaker.h" + +#include "FBXBaker.h" + +std::once_flag onceFlag; +FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr }; + +FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, + TextureBakerThreadGetter textureThreadGetter, bool copyOriginals) : + _fbxURL(fbxURL), + _baseOutputPath(baseOutputPath), + _textureThreadGetter(textureThreadGetter), + _copyOriginals(copyOriginals) +{ + std::call_once(onceFlag, [](){ + // create the static FBX SDK manager + _sdkManager = FBXSDKManagerUniquePointer(FbxManager::Create(), [](FbxManager* manager){ + manager->Destroy(); + }); + }); + + // grab the name of the FBX from the URL, this is used for folder output names + auto fileName = fbxURL.fileName(); + _fbxName = fileName.left(fileName.lastIndexOf('.')); +} + +static const QString BAKED_OUTPUT_SUBFOLDER = "baked/"; +static const QString ORIGINAL_OUTPUT_SUBFOLDER = "original/"; + +QString FBXBaker::pathToCopyOfOriginal() const { + return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName(); +} + +void FBXBaker::bake() { + qCDebug(model_baking) << "Baking" << _fbxURL; + + // setup the output folder for the results of this bake + setupOutputFolder(); + + if (hasErrors()) { + return; + } + + connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy); + + // make a local copy of the FBX file + loadSourceFBX(); +} + +void FBXBaker::bakeSourceCopy() { + // load the scene from the FBX file + importScene(); + + if (hasErrors()) { + return; + } + + // enumerate the textures found in the scene and start a bake for them + rewriteAndBakeSceneTextures(); + + if (hasErrors()) { + return; + } + + // export the FBX with re-written texture references + exportScene(); + + if (hasErrors()) { + return; + } + + // check if we're already done with textures (in case we had none to re-write) + checkIfTexturesFinished(); +} + +void FBXBaker::setupOutputFolder() { + // construct the output path using the name of the fbx and the base output path + _uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "/"; + + // make sure there isn't already an output directory using the same name + int iteration = 0; + + while (QDir(_uniqueOutputPath).exists()) { + _uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "-" + QString::number(++iteration) + "/"; + } + + qCDebug(model_baking) << "Creating FBX output folder" << _uniqueOutputPath; + + // attempt to make the output folder + if (!QDir().mkdir(_uniqueOutputPath)) { + handleError("Failed to create FBX output folder " + _uniqueOutputPath); + return; + } + + // make the baked and original sub-folders used during export + QDir uniqueOutputDir = _uniqueOutputPath; + if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) { + handleError("Failed to create baked/original subfolders in " + _uniqueOutputPath); + return; + } +} + +void FBXBaker::loadSourceFBX() { + // check if the FBX is local or first needs to be downloaded + if (_fbxURL.isLocalFile()) { + // load up the local file + QFile localFBX { _fbxURL.toLocalFile() }; + + // make a copy in the output folder + localFBX.copy(pathToCopyOfOriginal()); + + // emit our signal to start the import of the FBX source copy + emit sourceCopyReadyToLoad(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + + networkRequest.setUrl(_fbxURL); + + qCDebug(model_baking) << "Downloading" << _fbxURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); + } +} + +void FBXBaker::handleFBXNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _fbxURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(pathToCopyOfOriginal()); + + qDebug(model_baking) << "Writing copy of original FBX to" << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly) || (copyOfOriginal.write(requestReply->readAll()) == -1)) { + // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made + handleError("Could not create copy of " + _fbxURL.toString()); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + // emit our signal to start the import of the FBX source copy + emit sourceCopyReadyToLoad(); + } else { + // add an error to our list stating that the FBX could not be downloaded + handleError("Failed to download " + _fbxURL.toString()); + } +} + +void FBXBaker::importScene() { + // create an FBX SDK importer + FbxImporter* importer = FbxImporter::Create(_sdkManager.get(), ""); + + // import the copy of the original FBX file + QString originalCopyPath = pathToCopyOfOriginal(); + bool importStatus = importer->Initialize(originalCopyPath.toLocal8Bit().data()); + + if (!importStatus) { + // failed to initialize importer, print an error and return + handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString()); + return; + } else { + qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene"; + } + + // setup a new scene to hold the imported file + _scene = FbxScene::Create(_sdkManager.get(), "bakeScene"); + + // import the file to the created scene + importer->Import(_scene); + + // destroy the importer that is no longer needed + importer->Destroy(); +} + +QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { + auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (texturePath.startsWith(fbxPath)) { + // texture path is a child of the FBX path, return the texture path without the fbx path + return texturePath.mid(fbxPath.length()); + } else { + // the texture path was not a child of the FBX path, return the empty string + return ""; + } +} + +QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { + // first make sure we have a unique base name for this texture + // in case another texture referenced by this model has the same base name + auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; + + QString bakedTextureFileName { textureFileInfo.completeBaseName() }; + + if (nameMatches > 0) { + // there are already nameMatches texture with this name + // append - and that number to our baked texture file name so that it is unique + bakedTextureFileName += "-" + QString::number(nameMatches); + } + + bakedTextureFileName += BAKED_TEXTURE_EXT; + + // increment the number of name matches + ++nameMatches; + + return bakedTextureFileName; +} + +QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) { + QUrl urlToTexture; + + if (textureFileInfo.exists() && textureFileInfo.isFile()) { + // set the texture URL to the local texture that we have confirmed exists + urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); + } else { + // external texture that we'll need to download or find + + // first check if it the RelativePath to the texture in the FBX was relative + QString relativeFileName = fileTexture->GetRelativeFileName(); + auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); + + // this is a relative file path which will require different handling + // depending on the location of the original FBX + if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + // the absolute path we ran into for the texture in the FBX exists on this machine + // so use that file + urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); + } else { + // we didn't find the texture on this machine at the absolute path + // so assume that it is right beside the FBX to match the behaviour of interface + urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); + } + } + + return urlToTexture; +} + +image::TextureUsage::Type textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { + using namespace image::TextureUsage; + + // this is a property we know has a texture, we need to match it to a High Fidelity known texture type + // since that information is passed to the baking process + + // grab the hierarchical name for this property and lowercase it for case-insensitive compare + auto propertyName = QString(property.GetHierarchicalName()).toLower(); + + // figure out the type of the property based on what known value string it matches + if ((propertyName.contains("diffuse") && !propertyName.contains("tex_global_diffuse")) + || propertyName.contains("tex_color_map")) { + return ALBEDO_TEXTURE; + } else if (propertyName.contains("transparentcolor") || propertyName.contains("transparencyfactor")) { + return ALBEDO_TEXTURE; + } else if (propertyName.contains("bump")) { + return BUMP_TEXTURE; + } else if (propertyName.contains("normal")) { + return NORMAL_TEXTURE; + } else if ((propertyName.contains("specular") && !propertyName.contains("tex_global_specular")) + || propertyName.contains("reflection")) { + return SPECULAR_TEXTURE; + } else if (propertyName.contains("tex_metallic_map")) { + return METALLIC_TEXTURE; + } else if (propertyName.contains("shininess")) { + return GLOSS_TEXTURE; + } else if (propertyName.contains("tex_roughness_map")) { + return ROUGHNESS_TEXTURE; + } else if (propertyName.contains("emissive")) { + return EMISSIVE_TEXTURE; + } else if (propertyName.contains("ambientcolor")) { + return LIGHTMAP_TEXTURE; + } else if (propertyName.contains("ambientfactor")) { + // we need to check what the ambient factor is, since that tells Interface to process this texture + // either as an occlusion texture or a light map + auto lambertMaterial = FbxCast(material); + + if (lambertMaterial->AmbientFactor == 0) { + return LIGHTMAP_TEXTURE; + } else if (lambertMaterial->AmbientFactor > 0) { + return OCCLUSION_TEXTURE; + } else { + return UNUSED_TEXTURE; + } + + } else if (propertyName.contains("tex_ao_map")) { + return OCCLUSION_TEXTURE; + } + + return UNUSED_TEXTURE; +} + +void FBXBaker::rewriteAndBakeSceneTextures() { + + // enumerate the surface materials to find the textures used in the scene + int numMaterials = _scene->GetMaterialCount(); + for (int i = 0; i < numMaterials; i++) { + FbxSurfaceMaterial* material = _scene->GetMaterial(i); + + if (material) { + // enumerate the properties of this material to see what texture channels it might have + FbxProperty property = material->GetFirstProperty(); + + while (property.IsValid()) { + // first check if this property has connected textures, if not we don't need to bother with it here + if (property.GetSrcObjectCount() > 0) { + + // figure out the type of texture from the material property + auto textureType = textureTypeForMaterialProperty(property, material); + + if (textureType != image::TextureUsage::UNUSED_TEXTURE) { + int numTextures = property.GetSrcObjectCount(); + + for (int j = 0; j < numTextures; j++) { + FbxFileTexture* fileTexture = property.GetSrcObject(j); + + // use QFileInfo to easily split up the existing texture filename into its components + QString fbxFileName { fileTexture->GetFileName() }; + QFileInfo textureFileInfo { fbxFileName.replace("\\", "/") }; + + // make sure this texture points to something and isn't one we've already re-mapped + if (!textureFileInfo.filePath().isEmpty() + && textureFileInfo.suffix() != BAKED_TEXTURE_EXT.mid(1)) { + + // construct the new baked texture file name and file path + // ensuring that the baked texture will have a unique name + // even if there was another texture with the same name at a different path + auto bakedTextureFileName = createBakedTextureFileName(textureFileInfo); + QString bakedTextureFilePath { + _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + bakedTextureFileName + }; + + qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() + << "to" << bakedTextureFilePath; + + // write the new filename into the FBX scene + fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit()); + + // write the relative filename to be the baked texture file name since it will + // be right beside the FBX + fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData()); + + // figure out the URL to this texture, embedded or external + auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); + + if (!_bakingTextures.contains(urlToTexture)) { + // bake this texture asynchronously + bakeTexture(urlToTexture, textureType, _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER); + } + } + } + } + } + + property = material->GetNextProperty(property); + } + } + } +} + +void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir) { + // start a bake for this texture and add it to our list to keep track of + QSharedPointer bakingTexture { + new TextureBaker(textureURL, textureType, outputDir), + &TextureBaker::deleteLater + }; + + // make sure we hear when the baking texture is done + connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); + + // keep a shared pointer to the baking texture + _bakingTextures.insert(textureURL, bakingTexture); + + // start baking the texture on one of our available worker threads + bakingTexture->moveToThread(_textureThreadGetter()); + QMetaObject::invokeMethod(bakingTexture.data(), "bake"); +} + +void FBXBaker::handleBakedTexture() { + TextureBaker* bakedTexture = qobject_cast(sender()); + + // make sure we haven't already run into errors, and that this is a valid texture + if (bakedTexture) { + if (!hasErrors()) { + if (!bakedTexture->hasErrors()) { + if (_copyOriginals) { + // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture + + // use the path to the texture being baked to determine if this was an embedded or a linked texture + + // it is embeddded if the texure being baked was inside the original output folder + // since that is where the FBX SDK places the .fbm folder it generates when importing the FBX + + auto originalOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER); + + if (!originalOutputFolder.isParentOf(bakedTexture->getTextureURL())) { + // for linked textures we want to save a copy of original texture beside the original FBX + + qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); + + // check if we have a relative path to use for the texture + auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL()); + + QFile originalTextureFile { + _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName() + }; + + if (relativeTexturePath.length() > 0) { + // make the folders needed by the relative path + } + + if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { + qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() + << "for" << _fbxURL; + } else { + handleError("Could not save original external texture " + originalTextureFile.fileName() + + " for " + _fbxURL.toString()); + return; + } + } + } + + + // now that this texture has been baked and handled, we can remove that TextureBaker from our hash + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } else { + // there was an error baking this texture - add it to our list of errors + _errorList.append(bakedTexture->getErrors()); + + // we don't emit finished yet so that the other textures can finish baking first + _pendingErrorEmission = true; + + // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } + } else { + // we have errors to attend to, so we don't do extra processing for this texture + // but we do need to remove that TextureBaker from our list + // and then check if we're done with all textures + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } + } +} + +void FBXBaker::exportScene() { + // setup the exporter + FbxExporter* exporter = FbxExporter::Create(_sdkManager.get(), ""); + + auto rewrittenFBXPath = _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + _fbxName + BAKED_FBX_EXTENSION; + + // save the relative path to this FBX inside our passed output folder + _bakedFBXRelativePath = rewrittenFBXPath; + _bakedFBXRelativePath.remove(_baseOutputPath + "/"); + + bool exportStatus = exporter->Initialize(rewrittenFBXPath.toLocal8Bit().data()); + + if (!exportStatus) { + // failed to initialize exporter, print an error and return + handleError("Failed to export FBX file at " + _fbxURL.toString() + " to " + rewrittenFBXPath + + "- error: " + exporter->GetStatus().GetErrorString()); + } + + // export the scene + exporter->Export(_scene); + + qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << rewrittenFBXPath; +} + + +void FBXBaker::removeEmbeddedMediaFolder() { + // now that the bake is complete, remove the embedded media folder produced by the FBX SDK when it imports an FBX + auto embeddedMediaFolderName = _fbxURL.fileName().replace(".fbx", ".fbm"); + QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively(); +} + +void FBXBaker::possiblyCleanupOriginals() { + if (!_copyOriginals) { + // caller did not ask us to keep the original around, so delete the original output folder now + QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER).removeRecursively(); + } +} + +void FBXBaker::checkIfTexturesFinished() { + // check if we're done everything we need to do for this FBX + // and emit our finished signal if we're done + + if (_bakingTextures.isEmpty()) { + // remove the embedded media folder that the FBX SDK produces when reading the original + removeEmbeddedMediaFolder(); + + // cleanup the originals if we weren't asked to keep them around + possiblyCleanupOriginals(); + + if (hasErrors()) { + // if we're checking for completion but we have errors + // that means one or more of our texture baking operations failed + + if (_pendingErrorEmission) { + emit finished(); + } + + return; + } else { + qCDebug(model_baking) << "Finished baking" << _fbxURL; + + emit finished(); + } + } +} diff --git a/tools/oven/src/FBXBaker.h b/tools/oven/src/FBXBaker.h new file mode 100644 index 0000000000..bcfebbe2a8 --- /dev/null +++ b/tools/oven/src/FBXBaker.h @@ -0,0 +1,101 @@ +// +// FBXBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 3/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_FBXBaker_h +#define hifi_FBXBaker_h + +#include +#include +#include +#include + +#include "Baker.h" +#include "TextureBaker.h" + +#include + +namespace fbxsdk { + class FbxManager; + class FbxProperty; + class FbxScene; + class FbxFileTexture; +} + +static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; +using FBXSDKManagerUniquePointer = std::unique_ptr>; + +using TextureBakerThreadGetter = std::function; + +class FBXBaker : public Baker { + Q_OBJECT +public: + FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath, + TextureBakerThreadGetter textureThreadGetter, bool copyOriginals = true); + + QUrl getFBXUrl() const { return _fbxURL; } + QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; } + +public slots: + // all calls to FBXBaker::bake for FBXBaker instances must be from the same thread + // because the Autodesk SDK will cause a crash if it is called from multiple threads + virtual void bake() override; + +signals: + void sourceCopyReadyToLoad(); + +private slots: + void bakeSourceCopy(); + void handleFBXNetworkReply(); + void handleBakedTexture(); + +private: + void setupOutputFolder(); + + void loadSourceFBX(); + + void bakeCopiedFBX(); + + void importScene(); + void rewriteAndBakeSceneTextures(); + void exportScene(); + void removeEmbeddedMediaFolder(); + void possiblyCleanupOriginals(); + + void checkIfTexturesFinished(); + + QString createBakedTextureFileName(const QFileInfo& textureFileInfo); + QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture); + + void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir); + + QString pathToCopyOfOriginal() const; + + QUrl _fbxURL; + QString _fbxName; + + QString _baseOutputPath; + QString _uniqueOutputPath; + QString _bakedFBXRelativePath; + + static FBXSDKManagerUniquePointer _sdkManager; + fbxsdk::FbxScene* _scene { nullptr }; + + QMultiHash> _bakingTextures; + QHash _textureNameMatchCount; + + TextureBakerThreadGetter _textureThreadGetter; + + bool _copyOriginals { true }; + + bool _pendingErrorEmission { false }; +}; + +#endif // hifi_FBXBaker_h diff --git a/tools/oven/src/ModelBakingLoggingCategory.cpp b/tools/oven/src/ModelBakingLoggingCategory.cpp new file mode 100644 index 0000000000..f897ddf5ca --- /dev/null +++ b/tools/oven/src/ModelBakingLoggingCategory.cpp @@ -0,0 +1,14 @@ +// +// ModelBakingLoggingCategory.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ModelBakingLoggingCategory.h" + +Q_LOGGING_CATEGORY(model_baking, "hifi.model-baking"); diff --git a/tools/oven/src/ModelBakingLoggingCategory.h b/tools/oven/src/ModelBakingLoggingCategory.h new file mode 100644 index 0000000000..6c7d9d5db6 --- /dev/null +++ b/tools/oven/src/ModelBakingLoggingCategory.h @@ -0,0 +1,19 @@ +// +// ModelBakingLoggingCategory.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ModelBakingLoggingCategory_h +#define hifi_ModelBakingLoggingCategory_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(model_baking) + +#endif // hifi_ModelBakingLoggingCategory_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp new file mode 100644 index 0000000000..ac8ef505ba --- /dev/null +++ b/tools/oven/src/Oven.cpp @@ -0,0 +1,100 @@ +// +// Oven.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include + +#include + +#include "ui/OvenMainWindow.h" + +#include "Oven.h" + +static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/export"; + +Oven::Oven(int argc, char* argv[]) : + QApplication(argc, argv) +{ + QCoreApplication::setOrganizationName("High Fidelity"); + QCoreApplication::setApplicationName("Oven"); + + // init the settings interface so we can save and load settings + Setting::init(); + + // check if we were passed any command line arguments that would tell us just to run without the GUI + + // setup the GUI + _mainWindow = new OvenMainWindow; + _mainWindow->show(); + + // setup our worker threads + setupWorkerThreads(QThread::idealThreadCount() - 1); + + // Autodesk's SDK means that we need a single thread for all FBX importing/exporting in the same process + // setup the FBX Baker thread + setupFBXBakerThread(); +} + +Oven::~Oven() { + // cleanup the worker threads + for (auto i = 0; i < _workerThreads.size(); ++i) { + _workerThreads[i]->quit(); + _workerThreads[i]->wait(); + } + + // cleanup the FBX Baker thread + _fbxBakerThread->quit(); + _fbxBakerThread->wait(); +} + +void Oven::setupWorkerThreads(int numWorkerThreads) { + for (auto i = 0; i < numWorkerThreads; ++i) { + // setup a worker thread yet and add it to our concurrent vector + auto newThread = new QThread(this); + newThread->setObjectName("Oven Worker Thread " + QString::number(i + 1)); + + _workerThreads.push_back(newThread); + } +} + +void Oven::setupFBXBakerThread() { + // we're being asked for the FBX baker thread, but we don't have one yet + // so set that up now + _fbxBakerThread = new QThread(this); + _fbxBakerThread->setObjectName("Oven FBX Baker Thread"); +} + +QThread* Oven::getFBXBakerThread() { + if (!_fbxBakerThread->isRunning()) { + // start the FBX baker thread if it isn't running yet + _fbxBakerThread->start(); + } + + return _fbxBakerThread; +} + +QThread* Oven::getNextWorkerThread() { + // Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use. + // We can't use QThreadPool because we want to put QObjects with signals/slots on these threads. + // So instead we setup our own list of threads, up to one less than the ideal thread count + // (for the FBX Baker Thread to have room), and cycle through them to hand a usable running thread back to our callers. + + auto nextIndex = ++_nextWorkerThreadIndex; + auto nextThread = _workerThreads[nextIndex % _workerThreads.size()]; + + // start the thread if it isn't running yet + if (!nextThread->isRunning()) { + nextThread->start(); + } + + return nextThread; +} + diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h new file mode 100644 index 0000000000..350c615ce0 --- /dev/null +++ b/tools/oven/src/Oven.h @@ -0,0 +1,53 @@ +// +// Oven.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Oven_h +#define hifi_Oven_h + +#include + +#include + +#include + +#if defined(qApp) +#undef qApp +#endif +#define qApp (static_cast(QCoreApplication::instance())) + +class OvenMainWindow; + +class Oven : public QApplication { + Q_OBJECT + +public: + Oven(int argc, char* argv[]); + ~Oven(); + + OvenMainWindow* getMainWindow() const { return _mainWindow; } + + QThread* getFBXBakerThread(); + QThread* getNextWorkerThread(); + +private: + void setupWorkerThreads(int numWorkerThreads); + void setupFBXBakerThread(); + + OvenMainWindow* _mainWindow; + QThread* _fbxBakerThread; + QList _workerThreads; + + std::atomic _nextWorkerThreadIndex; + int _numWorkerThreads; +}; + + +#endif // hifi_Oven_h diff --git a/tools/oven/src/TextureBaker.cpp b/tools/oven/src/TextureBaker.cpp new file mode 100644 index 0000000000..70df511d2c --- /dev/null +++ b/tools/oven/src/TextureBaker.cpp @@ -0,0 +1,131 @@ +// +// TextureBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ModelBakingLoggingCategory.h" + +#include "TextureBaker.h" + +const QString BAKED_TEXTURE_EXT = ".ktx"; + +TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) : + _textureURL(textureURL), + _textureType(textureType), + _outputDirectory(outputDirectory) +{ + // figure out the baked texture filename + auto originalFilename = textureURL.fileName(); + _bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT; +} + +void TextureBaker::bake() { + // once our texture is loaded, kick off a the processing + connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); + + // first load the texture (either locally or remotely) + loadTexture(); +} + +void TextureBaker::loadTexture() { + // check if the texture is local or first needs to be downloaded + if (_textureURL.isLocalFile()) { + // load up the local file + QFile localTexture { _textureURL.toLocalFile() }; + + if (!localTexture.open(QIODevice::ReadOnly)) { + handleError("Unable to open texture " + _textureURL.toString()); + return; + } + + _originalTexture = localTexture.readAll(); + + emit originalTextureLoaded(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_textureURL); + + qCDebug(model_baking) << "Downloading" << _textureURL; + + // kickoff the download, wait for slot to tell us it is done + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply); + } +} + +void TextureBaker::handleTextureNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded texture" << _textureURL; + + // store the original texture so it can be passed along for the bake + _originalTexture = requestReply->readAll(); + + emit originalTextureLoaded(); + } else { + // add an error to our list stating that this texture could not be downloaded + handleError("Error downloading " + _textureURL.toString() + " - " + requestReply->errorString()); + } +} + +void TextureBaker::processTexture() { + auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(), + ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType); + + if (!processedTexture) { + handleError("Could not process texture " + _textureURL.toString()); + return; + } + + // the baked textures need to have the source hash added for cache checks in Interface + // so we add that to the processed texture before handling it off to be serialized + auto hashData = QCryptographicHash::hash(_originalTexture, QCryptographicHash::Md5); + std::string hash = hashData.toHex().toStdString(); + processedTexture->setSourceHash(hash); + + auto memKTX = gpu::Texture::serialize(*processedTexture); + + if (!memKTX) { + handleError("Could not serialize " + _textureURL.toString() + " to KTX"); + return; + } + + const char* data = reinterpret_cast(memKTX->_storage->data()); + const size_t length = memKTX->_storage->size(); + + // attempt to write the baked texture to the destination file path + QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) }; + + if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { + handleError("Could not write baked texture for " + _textureURL.toString()); + } + + qCDebug(model_baking) << "Baked texture" << _textureURL; + emit finished(); +} diff --git a/tools/oven/src/TextureBaker.h b/tools/oven/src/TextureBaker.h new file mode 100644 index 0000000000..ee1e968f20 --- /dev/null +++ b/tools/oven/src/TextureBaker.h @@ -0,0 +1,59 @@ +// +// TextureBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TextureBaker_h +#define hifi_TextureBaker_h + +#include +#include +#include + +#include + +#include "Baker.h" + +extern const QString BAKED_TEXTURE_EXT; + +class TextureBaker : public Baker { + Q_OBJECT + +public: + TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory); + + const QByteArray& getOriginalTexture() const { return _originalTexture; } + + QUrl getTextureURL() const { return _textureURL; } + + QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); } + QString getBakedTextureFileName() const { return _bakedTextureFileName; } + +public slots: + virtual void bake() override; + +signals: + void originalTextureLoaded(); + +private slots: + void processTexture(); + +private: + void loadTexture(); + void handleTextureNetworkReply(); + + QUrl _textureURL; + QByteArray _originalTexture; + image::TextureUsage::Type _textureType; + + QDir _outputDirectory; + QString _bakedTextureFileName; +}; + +#endif // hifi_TextureBaker_h diff --git a/tools/oven/src/main.cpp b/tools/oven/src/main.cpp new file mode 100644 index 0000000000..9c778245b5 --- /dev/null +++ b/tools/oven/src/main.cpp @@ -0,0 +1,16 @@ +// +// main.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 3/28/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include "Oven.h" + +int main (int argc, char** argv) { + Oven app(argc, argv); + return app.exec(); +} diff --git a/tools/oven/src/ui/BakeWidget.cpp b/tools/oven/src/ui/BakeWidget.cpp new file mode 100644 index 0000000000..23a4822d82 --- /dev/null +++ b/tools/oven/src/ui/BakeWidget.cpp @@ -0,0 +1,46 @@ +// +// BakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "BakeWidget.h" + +BakeWidget::BakeWidget(QWidget* parent, Qt::WindowFlags flags) : + QWidget(parent, flags) +{ + +} + +BakeWidget::~BakeWidget() { + // if we're going down, our bakers are about to too + // enumerate them, send a cancelled status to the results table, and remove them + auto it = _bakers.begin(); + while (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + resultsWindow->changeStatusForRow(resultRow, "Cancelled"); + + it = _bakers.erase(it); + } +} + +void BakeWidget::cancelButtonClicked() { + // the user wants to go back to the mode selection screen + // remove ourselves from the stacked widget and call delete later so we'll be cleaned up + auto stackedWidget = qobject_cast(parentWidget()); + stackedWidget->removeWidget(this); + + this->deleteLater(); +} diff --git a/tools/oven/src/ui/BakeWidget.h b/tools/oven/src/ui/BakeWidget.h new file mode 100644 index 0000000000..e7ab8d1840 --- /dev/null +++ b/tools/oven/src/ui/BakeWidget.h @@ -0,0 +1,33 @@ +// +// BakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BakeWidget_h +#define hifi_BakeWidget_h + +#include + +#include "../Baker.h" + +class BakeWidget : public QWidget { + Q_OBJECT +public: + BakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + ~BakeWidget(); + + void cancelButtonClicked(); + +protected: + using BakerRowPair = std::pair, int>; + using BakerRowPairList = std::list; + BakerRowPairList _bakers; +}; + +#endif // hifi_BakeWidget_h diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp new file mode 100644 index 0000000000..7d667305bb --- /dev/null +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -0,0 +1,286 @@ +// +// DomainBakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "DomainBakeWidget.h" + +static const QString DOMAIN_NAME_SETTING_KEY = "domain_name"; +static const QString EXPORT_DIR_SETTING_KEY = "domain_export_directory"; +static const QString BROWSE_START_DIR_SETTING_KEY = "domain_search_directory"; +static const QString DESTINATION_PATH_SETTING_KEY = "destination_path"; + +DomainBakeWidget::DomainBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + BakeWidget(parent, flags), + _domainNameSetting(DOMAIN_NAME_SETTING_KEY), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _browseStartDirectory(BROWSE_START_DIR_SETTING_KEY), + _destinationPathSetting(DESTINATION_PATH_SETTING_KEY) +{ + setupUI(); +} + +void DomainBakeWidget::setupUI() { + // setup a grid layout to hold everything + QGridLayout* gridLayout = new QGridLayout; + + int rowIndex = 0; + + // setup a section to enter the name of the domain being baked + QLabel* domainNameLabel = new QLabel("Domain Name"); + + _domainNameLineEdit = new QLineEdit; + _domainNameLineEdit->setPlaceholderText("welcome"); + + // set the text of the domain name from whatever was used during last bake + if (!_domainNameSetting.get().isEmpty()) { + _domainNameLineEdit->setText(_domainNameSetting.get()); + } + + gridLayout->addWidget(domainNameLabel); + gridLayout->addWidget(_domainNameLineEdit, rowIndex, 1, 1, -1); + + ++rowIndex; + + // setup a section to choose the file being baked + QLabel* entitiesFileLabel = new QLabel("Entities File"); + + _entitiesFileLineEdit = new QLineEdit; + _entitiesFileLineEdit->setPlaceholderText("File"); + + QPushButton* chooseFileButton = new QPushButton("Browse..."); + connect(chooseFileButton, &QPushButton::clicked, this, &DomainBakeWidget::chooseFileButtonClicked); + + // add the components for the entities file picker to the layout + gridLayout->addWidget(entitiesFileLabel, rowIndex, 0); + gridLayout->addWidget(_entitiesFileLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseFileButton, rowIndex, 4); + + // start a new row for next component + ++rowIndex; + + // setup a section to choose the output directory + QLabel* outputDirectoryLabel = new QLabel("Output Directory"); + + _outputDirLineEdit = new QLineEdit; + + // set the current export directory to whatever was last used + _outputDirLineEdit->setText(_exportDirectory.get()); + + // whenever the output directory line edit changes, update the value in settings + connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &DomainBakeWidget::outputDirectoryChanged); + + QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse..."); + connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &DomainBakeWidget::chooseOutputDirButtonClicked); + + // add the components for the output directory picker to the layout + gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0); + gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4); + + // start a new row for the next component + ++rowIndex; + + // setup a section to choose the upload prefix - the URL where baked models will be made available + QLabel* uploadPrefixLabel = new QLabel("Destination URL Path"); + + _destinationPathLineEdit = new QLineEdit; + _destinationPathLineEdit->setPlaceholderText("http://cdn.example.com/baked-domain/"); + + if (!_destinationPathSetting.get().isEmpty()) { + _destinationPathLineEdit->setText(_destinationPathSetting.get()); + } + + gridLayout->addWidget(uploadPrefixLabel, rowIndex, 0); + gridLayout->addWidget(_destinationPathLineEdit, rowIndex, 1, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a horizontal line to split the bake/cancel buttons off + QFrame* lineFrame = new QFrame; + lineFrame->setFrameShape(QFrame::HLine); + lineFrame->setFrameShadow(QFrame::Sunken); + gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a button that will kickoff the bake + QPushButton* bakeButton = new QPushButton("Bake"); + connect(bakeButton, &QPushButton::clicked, this, &DomainBakeWidget::bakeButtonClicked); + gridLayout->addWidget(bakeButton, rowIndex, 3); + + // add a cancel button to go back to the modes page + QPushButton* cancelButton = new QPushButton("Cancel"); + connect(cancelButton, &QPushButton::clicked, this, &DomainBakeWidget::cancelButtonClicked); + gridLayout->addWidget(cancelButton, rowIndex, 4); + + setLayout(gridLayout); +} + +void DomainBakeWidget::chooseFileButtonClicked() { + // pop a file dialog so the user can select the entities file + + // if we have picked an FBX before, start in the folder that matches the last path + // otherwise start in the home directory + auto startDir = _browseStartDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedFile = QFileDialog::getOpenFileName(this, "Choose Entities File", startDir, + "Entities File (*.json *.gz)"); + + if (!selectedFile.isEmpty()) { + // set the contents of the entities file text box to be the path to the selected file + _entitiesFileLineEdit->setText(selectedFile); + + auto directoryOfEntitiesFile = QFileInfo(selectedFile).absolutePath(); + + // save the directory containing this entities file so we can default to it next time we show the file dialog + _browseStartDirectory.set(directoryOfEntitiesFile); + } +} + +void DomainBakeWidget::chooseOutputDirButtonClicked() { + // pop a file dialog so the user can select the output directory + + // if we have a previously selected output directory, use that as the initial path in the choose dialog + // otherwise use the user's home directory + auto startDir = _exportDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir); + + if (!selectedDir.isEmpty()) { + // set the contents of the output directory text box to be the path to the directory + _outputDirLineEdit->setText(selectedDir); + } +} + +void DomainBakeWidget::outputDirectoryChanged(const QString& newDirectory) { + // update the export directory setting so we can re-use it next time + _exportDirectory.set(newDirectory); +} + +void DomainBakeWidget::bakeButtonClicked() { + + // save whatever the current domain name is in settings, we'll re-use it next time the widget is shown + _domainNameSetting.set(_domainNameLineEdit->text()); + + // save whatever the current destination path is in settings, we'll re-use it next time the widget is shown + _destinationPathSetting.set(_destinationPathLineEdit->text()); + + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + + if (!outputDirectory.exists()) { + return; + } + + // make sure we have a non empty URL to an entities file to bake + if (!_entitiesFileLineEdit->text().isEmpty()) { + // everything seems to be in place, kick off a bake for this entities file now + auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text()); + auto domainBaker = std::unique_ptr { + new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), + outputDirectory.absolutePath(), _destinationPathLineEdit->text()) + }; + + // make sure we hear from the baker when it is done + connect(domainBaker.get(), &DomainBaker::finished, this, &DomainBakeWidget::handleFinishedBaker); + + // watch the baker's progress so that we can put its progress in the results table + connect(domainBaker.get(), &DomainBaker::bakeProgress, this, &DomainBakeWidget::handleBakerProgress); + + // move the baker to the next available Oven worker thread + auto nextThread = qApp->getNextWorkerThread(); + domainBaker->moveToThread(nextThread); + + // kickoff the domain baker on its thread + QMetaObject::invokeMethod(domainBaker.get(), "bake"); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + auto resultsRowName = _domainNameLineEdit->text().isEmpty() ? fileToBakeURL.fileName() : _domainNameLineEdit->text(); + auto resultsRow = resultsWindow->addPendingResultRow(resultsRowName, outputDirectory); + + // keep the unique ptr to the domain baker and the index to the row representing it in the results table + _bakers.emplace_back(std::move(domainBaker), resultsRow); + } +} + +void DomainBakeWidget::handleBakerProgress(int baked, int total) { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + + // grab the results window, don't force it to be brought to the top + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(false); + + int percentage = roundf(float(baked) / float(total) * 100.0f); + + auto statusString = QString("Baking - %1 of %2 - %3%").arg(baked).arg(total).arg(percentage); + resultsWindow->changeStatusForRow(resultRow, statusString); + } + } +} + +void DomainBakeWidget::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + if (baker->hasErrors()) { + auto errors = baker->getErrors(); + errors.removeDuplicates(); + + resultsWindow->changeStatusForRow(resultRow, errors.join("\n")); + } else if (baker->hasWarnings()) { + auto warnings = baker->getWarnings(); + warnings.removeDuplicates(); + + resultsWindow->changeStatusForRow(resultRow, warnings.join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } + + // remove the DomainBaker now that it has completed + _bakers.erase(it); + } + } +} diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h new file mode 100644 index 0000000000..cd8c4a012e --- /dev/null +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -0,0 +1,54 @@ +// +// DomainBakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DomainBakeWidget_h +#define hifi_DomainBakeWidget_h + +#include + +#include + +#include "../DomainBaker.h" +#include "BakeWidget.h" + +class QLineEdit; + +class DomainBakeWidget : public BakeWidget { + Q_OBJECT + +public: + DomainBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + + void handleBakerProgress(int baked, int total); + void handleFinishedBaker(); + +private: + void setupUI(); + + QLineEdit* _domainNameLineEdit; + QLineEdit* _entitiesFileLineEdit; + QLineEdit* _outputDirLineEdit; + QLineEdit* _destinationPathLineEdit; + + Setting::Handle _domainNameSetting; + Setting::Handle _exportDirectory; + Setting::Handle _browseStartDirectory; + Setting::Handle _destinationPathSetting; +}; + +#endif // hifi_ModelBakeWidget_h diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp new file mode 100644 index 0000000000..c696fbad26 --- /dev/null +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -0,0 +1,227 @@ +// +// ModelBakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "ModelBakeWidget.h" + +static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory"; +static const auto MODEL_START_DIR_SETTING_KEY = "model_search_directory"; + +ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + BakeWidget(parent, flags), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) +{ + setupUI(); +} + +void ModelBakeWidget::setupUI() { + // setup a grid layout to hold everything + QGridLayout* gridLayout = new QGridLayout; + + int rowIndex = 0; + + // setup a section to choose the file being baked + QLabel* modelFileLabel = new QLabel("Model File(s)"); + + _modelLineEdit = new QLineEdit; + _modelLineEdit->setPlaceholderText("File or URL"); + + QPushButton* chooseFileButton = new QPushButton("Browse..."); + connect(chooseFileButton, &QPushButton::clicked, this, &ModelBakeWidget::chooseFileButtonClicked); + + // add the components for the model file picker to the layout + gridLayout->addWidget(modelFileLabel, rowIndex, 0); + gridLayout->addWidget(_modelLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseFileButton, rowIndex, 4); + + // start a new row for next component + ++rowIndex; + + // setup a section to choose the output directory + QLabel* outputDirectoryLabel = new QLabel("Output Directory"); + + _outputDirLineEdit = new QLineEdit; + + // set the current export directory to whatever was last used + _outputDirLineEdit->setText(_exportDirectory.get()); + + // whenever the output directory line edit changes, update the value in settings + connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &ModelBakeWidget::outputDirectoryChanged); + + QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse..."); + connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &ModelBakeWidget::chooseOutputDirButtonClicked); + + // add the components for the output directory picker to the layout + gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0); + gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4); + + // start a new row for the next component + ++rowIndex; + + // add a horizontal line to split the bake/cancel buttons off + QFrame* lineFrame = new QFrame; + lineFrame->setFrameShape(QFrame::HLine); + lineFrame->setFrameShadow(QFrame::Sunken); + gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a button that will kickoff the bake + QPushButton* bakeButton = new QPushButton("Bake"); + connect(bakeButton, &QPushButton::clicked, this, &ModelBakeWidget::bakeButtonClicked); + gridLayout->addWidget(bakeButton, rowIndex, 3); + + // add a cancel button to go back to the modes page + QPushButton* cancelButton = new QPushButton("Cancel"); + connect(cancelButton, &QPushButton::clicked, this, &ModelBakeWidget::cancelButtonClicked); + gridLayout->addWidget(cancelButton, rowIndex, 4); + + setLayout(gridLayout); +} + +void ModelBakeWidget::chooseFileButtonClicked() { + // pop a file dialog so the user can select the model file + + // if we have picked an FBX before, start in the folder that matches the last path + // otherwise start in the home directory + auto startDir = _modelStartDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx)"); + + if (!selectedFiles.isEmpty()) { + // set the contents of the model file text box to be the path to the selected file + _modelLineEdit->setText(selectedFiles.join(',')); + + auto directoryOfModel = QFileInfo(selectedFiles[0]).absolutePath(); + + if (_outputDirLineEdit->text().isEmpty()) { + // if our output directory is not yet set, set it to the directory of this model + _outputDirLineEdit->setText(directoryOfModel); + } + + // save the directory containing the file(s) so we can default to it next time we show the file dialog + _modelStartDirectory.set(directoryOfModel); + } +} + +void ModelBakeWidget::chooseOutputDirButtonClicked() { + // pop a file dialog so the user can select the output directory + + // if we have a previously selected output directory, use that as the initial path in the choose dialog + // otherwise use the user's home directory + auto startDir = _exportDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir); + + if (!selectedDir.isEmpty()) { + // set the contents of the output directory text box to be the path to the directory + _outputDirLineEdit->setText(selectedDir); + } +} + +void ModelBakeWidget::outputDirectoryChanged(const QString& newDirectory) { + // update the export directory setting so we can re-use it next time + _exportDirectory.set(newDirectory); +} + +void ModelBakeWidget::bakeButtonClicked() { + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + + if (!outputDirectory.exists()) { + return; + } + + // make sure we have a non empty URL to a model to bake + if (_modelLineEdit->text().isEmpty()) { + return; + } + + // split the list from the model line edit to see how many models we need to bake + auto fileURLStrings = _modelLineEdit->text().split(','); + foreach (QString fileURLString, fileURLStrings) { + // construct a URL from the path in the model file text box + QUrl modelToBakeURL(fileURLString); + + // if the URL doesn't have a scheme, assume it is a local file + if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") { + modelToBakeURL.setScheme("file"); + } + + // everything seems to be in place, kick off a bake for this model now + auto baker = std::unique_ptr { + new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), []() -> QThread* { + return qApp->getNextWorkerThread(); + }, false) + }; + + // move the baker to the FBX baker thread + baker->moveToThread(qApp->getFBXBakerThread()); + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(baker.get(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(baker.get(), &FBXBaker::finished, this, &ModelBakeWidget::handleFinishedBaker); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory); + + // keep a unique_ptr to this baker + // and remember the row that represents it in the results table + _bakers.emplace_back(std::move(baker), resultsRow); + } +} + +void ModelBakeWidget::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + if (baker->hasErrors()) { + resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } + + _bakers.erase(it); + } + } +} diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h new file mode 100644 index 0000000000..ed08990ba5 --- /dev/null +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -0,0 +1,51 @@ +// +// ModelBakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ModelBakeWidget_h +#define hifi_ModelBakeWidget_h + +#include + +#include + +#include "../FBXBaker.h" + +#include "BakeWidget.h" + +class QLineEdit; +class QThread; + +class ModelBakeWidget : public BakeWidget { + Q_OBJECT + +public: + ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + + void handleFinishedBaker(); + +private: + void setupUI(); + + QLineEdit* _modelLineEdit; + QLineEdit* _outputDirLineEdit; + + Setting::Handle _exportDirectory; + Setting::Handle _modelStartDirectory; +}; + +#endif // hifi_ModelBakeWidget_h diff --git a/tools/oven/src/ui/ModesWidget.cpp b/tools/oven/src/ui/ModesWidget.cpp new file mode 100644 index 0000000000..624aa949cc --- /dev/null +++ b/tools/oven/src/ui/ModesWidget.cpp @@ -0,0 +1,69 @@ +// +// ModesWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/7/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include + +#include "DomainBakeWidget.h" +#include "ModelBakeWidget.h" +#include "SkyboxBakeWidget.h" + +#include "ModesWidget.h" + +ModesWidget::ModesWidget(QWidget* parent, Qt::WindowFlags flags) : + QWidget(parent, flags) +{ + setupUI(); +} + +void ModesWidget::setupUI() { + // setup a horizontal box layout to hold our mode buttons + QHBoxLayout* horizontalLayout = new QHBoxLayout; + + // add a button for domain baking + QPushButton* domainButton = new QPushButton("Bake Domain"); + connect(domainButton, &QPushButton::clicked, this, &ModesWidget::showDomainBakingWidget); + horizontalLayout->addWidget(domainButton); + + // add a button for model baking + QPushButton* modelsButton = new QPushButton("Bake Models"); + connect(modelsButton, &QPushButton::clicked, this, &ModesWidget::showModelBakingWidget); + horizontalLayout->addWidget(modelsButton); + + // add a button for skybox baking + QPushButton* skyboxButton = new QPushButton("Bake Skyboxes"); + connect(skyboxButton, &QPushButton::clicked, this, &ModesWidget::showSkyboxBakingWidget); + horizontalLayout->addWidget(skyboxButton); + + setLayout(horizontalLayout); +} + +void ModesWidget::showModelBakingWidget() { + auto stackedWidget = qobject_cast(parentWidget()); + + // add a new widget for model baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new ModelBakeWidget)); +} + +void ModesWidget::showDomainBakingWidget() { + auto stackedWidget = qobject_cast(parentWidget()); + + // add a new widget for domain baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new DomainBakeWidget)); +} + +void ModesWidget::showSkyboxBakingWidget() { + auto stackedWidget = qobject_cast(parentWidget()); + + // add a new widget for skybox baking to the stack, and switch to it + stackedWidget->setCurrentIndex(stackedWidget->addWidget(new SkyboxBakeWidget)); +} diff --git a/tools/oven/src/ui/ModesWidget.h b/tools/oven/src/ui/ModesWidget.h new file mode 100644 index 0000000000..fd660923f2 --- /dev/null +++ b/tools/oven/src/ui/ModesWidget.h @@ -0,0 +1,31 @@ +// +// ModesWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/7/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ModesWidget_h +#define hifi_ModesWidget_h + +#include + +class ModesWidget : public QWidget { + Q_OBJECT +public: + ModesWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void showModelBakingWidget(); + void showDomainBakingWidget(); + void showSkyboxBakingWidget(); + +private: + void setupUI(); +}; + +#endif // hifi_ModesWidget_h diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp new file mode 100644 index 0000000000..dd40fb1f8f --- /dev/null +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -0,0 +1,61 @@ +// +// OvenMainWindow.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "ModesWidget.h" + +#include "OvenMainWindow.h" + +OvenMainWindow::OvenMainWindow(QWidget *parent, Qt::WindowFlags flags) : + QMainWindow(parent, flags) +{ + setWindowTitle("High Fidelity Oven"); + + // give the window a fixed width that will never change + setFixedWidth(FIXED_WINDOW_WIDTH); + + // setup a stacked layout for the main "modes" menu and subseq + QStackedWidget* stackedWidget = new QStackedWidget(this); + stackedWidget->addWidget(new ModesWidget); + + setCentralWidget(stackedWidget); +} + +OvenMainWindow::~OvenMainWindow() { + if (_resultsWindow) { + _resultsWindow->close(); + _resultsWindow->deleteLater(); + } +} + +ResultsWindow* OvenMainWindow::showResultsWindow(bool shouldRaise) { + if (!_resultsWindow) { + // we don't have a results window right now, so make a new one + _resultsWindow = new ResultsWindow; + + // even though we're about to show the results window, we do it here so that the move below works + _resultsWindow->show(); + + // place the results window initially below our window + _resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom()); + } + + // show the results window and make sure it is in front + _resultsWindow->show(); + + if (shouldRaise) { + _resultsWindow->raise(); + } + + // return a pointer to the results window the caller can use + return _resultsWindow; +} diff --git a/tools/oven/src/ui/OvenMainWindow.h b/tools/oven/src/ui/OvenMainWindow.h new file mode 100644 index 0000000000..a557d5e8dd --- /dev/null +++ b/tools/oven/src/ui/OvenMainWindow.h @@ -0,0 +1,34 @@ +// +// OvenMainWindow.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_OvenMainWindow_h +#define hifi_OvenMainWindow_h + +#include +#include + +#include "ResultsWindow.h" + +const int FIXED_WINDOW_WIDTH = 640; + +class OvenMainWindow : public QMainWindow { + Q_OBJECT +public: + OvenMainWindow(QWidget *parent = Q_NULLPTR, Qt::WindowFlags flags = Qt::WindowFlags()); + ~OvenMainWindow(); + + ResultsWindow* showResultsWindow(bool shouldRaise = true); + +private: + QPointer _resultsWindow; +}; + +#endif // hifi_OvenMainWindow_h diff --git a/tools/oven/src/ui/ResultsWindow.cpp b/tools/oven/src/ui/ResultsWindow.cpp new file mode 100644 index 0000000000..35b5160f9b --- /dev/null +++ b/tools/oven/src/ui/ResultsWindow.cpp @@ -0,0 +1,100 @@ +// +// ResultsWindow.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include + +#include "OvenMainWindow.h" + +#include "ResultsWindow.h" + +ResultsWindow::ResultsWindow(QWidget* parent) : + QWidget(parent) +{ + // add a title to this window to identify it + setWindowTitle("High Fidelity Oven - Bake Results"); + + // give this dialog the same starting width as the main application window + resize(FIXED_WINDOW_WIDTH, size().height()); + + // have the window delete itself when closed + setAttribute(Qt::WA_DeleteOnClose); + + setupUI(); +} + +void ResultsWindow::setupUI() { + QVBoxLayout* resultsLayout = new QVBoxLayout(this); + + // add a results table to the widget + _resultsTable = new QTableWidget(0, 2, this); + + // add the header to the table widget + _resultsTable->setHorizontalHeaderLabels({"File", "Status"}); + + // add that table widget to the vertical box layout, so we can make it stretch to the size of the parent + resultsLayout->insertWidget(0, _resultsTable); + + // make the filename column hold 25% of the total width + // strech the last column of the table (that holds the results) to fill up the remaining available size + _resultsTable->horizontalHeader()->resizeSection(0, 0.25 * FIXED_WINDOW_WIDTH); + _resultsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + // make sure we hear about cell clicks so that we can show the output directory for the given row + connect(_resultsTable, &QTableWidget::cellClicked, this, &ResultsWindow::handleCellClicked); + + // set the layout of this widget to the created layout + setLayout(resultsLayout); +} + +void ResultsWindow::handleCellClicked(int rowIndex, int columnIndex) { + // make sure this click was on the file/domain being baked + if (columnIndex == 0) { + // use QDesktopServices to show the output directory for this row + auto directory = _outputDirectories[rowIndex]; + QDesktopServices::openUrl(QUrl::fromLocalFile(directory.absolutePath())); + } +} + +int ResultsWindow::addPendingResultRow(const QString& fileName, const QDir& outputDirectory) { + int rowIndex = _resultsTable->rowCount(); + + _resultsTable->insertRow(rowIndex); + + // add a new item for the filename, make it non-editable + auto fileNameItem = new QTableWidgetItem(fileName); + fileNameItem->setFlags(fileNameItem->flags() & ~Qt::ItemIsEditable); + _resultsTable->setItem(rowIndex, 0, fileNameItem); + + auto statusItem = new QTableWidgetItem("Baking..."); + statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable); + _resultsTable->setItem(rowIndex, 1, statusItem); + + // push an output directory to our list so we can show it if the user clicks on this bake in the results table + _outputDirectories.push_back(outputDirectory); + + return rowIndex; +} + +void ResultsWindow::changeStatusForRow(int rowIndex, const QString& result) { + const int STATUS_COLUMN = 1; + auto statusItem = new QTableWidgetItem(result); + statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable); + _resultsTable->setItem(rowIndex, STATUS_COLUMN, statusItem); + + // resize the row for the new contents + _resultsTable->resizeRowToContents(rowIndex); + // reszie the column for the new contents + _resultsTable->resizeColumnToContents(STATUS_COLUMN); +} diff --git a/tools/oven/src/ui/ResultsWindow.h b/tools/oven/src/ui/ResultsWindow.h new file mode 100644 index 0000000000..ae7bb0e327 --- /dev/null +++ b/tools/oven/src/ui/ResultsWindow.h @@ -0,0 +1,39 @@ +// +// ResultsWindow.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/14/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ResultsWindow_h +#define hifi_ResultsWindow_h + +#include +#include + +class QTableWidget; + +class ResultsWindow : public QWidget { + Q_OBJECT + +public: + ResultsWindow(QWidget* parent = nullptr); + + void setupUI(); + + int addPendingResultRow(const QString& fileName, const QDir& outputDirectory); + void changeStatusForRow(int rowIndex, const QString& result); + +private slots: + void handleCellClicked(int rowIndex, int columnIndex); + +private: + QTableWidget* _resultsTable { nullptr }; + QList _outputDirectories; +}; + +#endif // hifi_ResultsWindow_h diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp new file mode 100644 index 0000000000..d5c280aebd --- /dev/null +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -0,0 +1,223 @@ +// +// SkyboxBakeWidget.cpp +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../Oven.h" +#include "OvenMainWindow.h" + +#include "SkyboxBakeWidget.h" + +static const auto EXPORT_DIR_SETTING_KEY = "skybox_export_directory"; +static const auto SELECTION_START_DIR_SETTING_KEY = "skybox_search_directory"; + +SkyboxBakeWidget::SkyboxBakeWidget(QWidget* parent, Qt::WindowFlags flags) : + BakeWidget(parent, flags), + _exportDirectory(EXPORT_DIR_SETTING_KEY), + _selectionStartDirectory(SELECTION_START_DIR_SETTING_KEY) +{ + setupUI(); +} + +void SkyboxBakeWidget::setupUI() { + // setup a grid layout to hold everything + QGridLayout* gridLayout = new QGridLayout; + + int rowIndex = 0; + + // setup a section to choose the file being baked + QLabel* skyboxFileLabel = new QLabel("Skybox File(s)"); + + _selectionLineEdit = new QLineEdit; + _selectionLineEdit->setPlaceholderText("File or URL"); + + QPushButton* chooseFileButton = new QPushButton("Browse..."); + connect(chooseFileButton, &QPushButton::clicked, this, &SkyboxBakeWidget::chooseFileButtonClicked); + + // add the components for the skybox file picker to the layout + gridLayout->addWidget(skyboxFileLabel, rowIndex, 0); + gridLayout->addWidget(_selectionLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseFileButton, rowIndex, 4); + + // start a new row for next component + ++rowIndex; + + // setup a section to choose the output directory + QLabel* outputDirectoryLabel = new QLabel("Output Directory"); + + _outputDirLineEdit = new QLineEdit; + + // set the current export directory to whatever was last used + _outputDirLineEdit->setText(_exportDirectory.get()); + + // whenever the output directory line edit changes, update the value in settings + connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &SkyboxBakeWidget::outputDirectoryChanged); + + QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse..."); + connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &SkyboxBakeWidget::chooseOutputDirButtonClicked); + + // add the components for the output directory picker to the layout + gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0); + gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3); + gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4); + + // start a new row for the next component + ++rowIndex; + + // add a horizontal line to split the bake/cancel buttons off + QFrame* lineFrame = new QFrame; + lineFrame->setFrameShape(QFrame::HLine); + lineFrame->setFrameShadow(QFrame::Sunken); + gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1); + + // start a new row for the next component + ++rowIndex; + + // add a button that will kickoff the bake + QPushButton* bakeButton = new QPushButton("Bake"); + connect(bakeButton, &QPushButton::clicked, this, &SkyboxBakeWidget::bakeButtonClicked); + gridLayout->addWidget(bakeButton, rowIndex, 3); + + // add a cancel button to go back to the modes page + QPushButton* cancelButton = new QPushButton("Cancel"); + connect(cancelButton, &QPushButton::clicked, this, &SkyboxBakeWidget::cancelButtonClicked); + gridLayout->addWidget(cancelButton, rowIndex, 4); + + setLayout(gridLayout); +} + +void SkyboxBakeWidget::chooseFileButtonClicked() { + // pop a file dialog so the user can select the skybox file(s) + + // if we have picked a skybox before, start in the folder that matches the last path + // otherwise start in the home directory + auto startDir = _selectionStartDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Skybox", startDir); + + if (!selectedFiles.isEmpty()) { + // set the contents of the file select text box to be the path to the selected file + _selectionLineEdit->setText(selectedFiles.join(',')); + + if (_outputDirLineEdit->text().isEmpty()) { + auto directoryOfSkybox = QFileInfo(selectedFiles[0]).absolutePath(); + + // if our output directory is not yet set, set it to the directory of this skybox + _outputDirLineEdit->setText(directoryOfSkybox); + } + } +} + +void SkyboxBakeWidget::chooseOutputDirButtonClicked() { + // pop a file dialog so the user can select the output directory + + // if we have a previously selected output directory, use that as the initial path in the choose dialog + // otherwise use the user's home directory + auto startDir = _exportDirectory.get(); + if (startDir.isEmpty()) { + startDir = QDir::homePath(); + } + + auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir); + + if (!selectedDir.isEmpty()) { + // set the contents of the output directory text box to be the path to the directory + _outputDirLineEdit->setText(selectedDir); + } +} + +void SkyboxBakeWidget::outputDirectoryChanged(const QString& newDirectory) { + // update the export directory setting so we can re-use it next time + _exportDirectory.set(newDirectory); +} + +void SkyboxBakeWidget::bakeButtonClicked() { + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + + if (!outputDirectory.exists()) { + return; + } + + // make sure we have a non empty URL to a skybox to bake + if (_selectionLineEdit->text().isEmpty()) { + return; + } + + // split the list from the selection line edit to see how many skyboxes we need to bake + auto fileURLStrings = _selectionLineEdit->text().split(','); + foreach (QString fileURLString, fileURLStrings) { + // construct a URL from the path in the skybox file text box + QUrl skyboxToBakeURL(fileURLString); + + // if the URL doesn't have a scheme, assume it is a local file + if (skyboxToBakeURL.scheme() != "http" && skyboxToBakeURL.scheme() != "https" && skyboxToBakeURL.scheme() != "ftp") { + skyboxToBakeURL.setScheme("file"); + } + + // everything seems to be in place, kick off a bake for this skybox now + auto baker = std::unique_ptr { + new TextureBaker(skyboxToBakeURL, image::TextureUsage::CUBE_TEXTURE, outputDirectory.absolutePath()) + }; + + // move the baker to a worker thread + baker->moveToThread(qApp->getNextWorkerThread()); + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(baker.get(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(baker.get(), &TextureBaker::finished, this, &SkyboxBakeWidget::handleFinishedBaker); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + auto resultsRow = resultsWindow->addPendingResultRow(skyboxToBakeURL.fileName(), outputDirectory); + + // keep a unique_ptr to this baker + // and remember the row that represents it in the results table + _bakers.emplace_back(std::move(baker), resultsRow); + } +} + +void SkyboxBakeWidget::handleFinishedBaker() { + if (auto baker = qobject_cast(sender())) { + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = qApp->getMainWindow()->showResultsWindow(); + + if (baker->hasErrors()) { + resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); + } + + // drop our strong pointer to the baker now that we are done with it + _bakers.erase(it); + } + } +} diff --git a/tools/oven/src/ui/SkyboxBakeWidget.h b/tools/oven/src/ui/SkyboxBakeWidget.h new file mode 100644 index 0000000000..4063a5459b --- /dev/null +++ b/tools/oven/src/ui/SkyboxBakeWidget.h @@ -0,0 +1,50 @@ +// +// SkyboxBakeWidget.h +// tools/oven/src/ui +// +// Created by Stephen Birarda on 4/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SkyboxBakeWidget_h +#define hifi_SkyboxBakeWidget_h + +#include + +#include + +#include "../TextureBaker.h" + +#include "BakeWidget.h" + +class QLineEdit; + +class SkyboxBakeWidget : public BakeWidget { + Q_OBJECT + +public: + SkyboxBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + +private slots: + void chooseFileButtonClicked(); + void chooseOutputDirButtonClicked(); + void bakeButtonClicked(); + + void outputDirectoryChanged(const QString& newDirectory); + + void handleFinishedBaker(); + +private: + void setupUI(); + + QLineEdit* _selectionLineEdit; + QLineEdit* _outputDirLineEdit; + + Setting::Handle _exportDirectory; + Setting::Handle _selectionStartDirectory; +}; + +#endif // hifi_SkyboxBakeWidget_h