Merge pull request #10243 from birarda/bake-textures

Add an internal tool for domain/model/skybox KTX texture baking
This commit is contained in:
Brad Hefta-Gaub 2017-05-12 12:26:33 -07:00 committed by GitHub
commit a88f72024d
36 changed files with 3116 additions and 5 deletions

114
cmake/modules/FindFBX.cmake Normal file
View file

@ -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()

View file

@ -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<KTX>;

View file

@ -37,7 +37,8 @@ enum Type {
CUBE_TEXTURE,
OCCLUSION_TEXTURE,
SCATTERING_TEXTURE = OCCLUSION_TEXTURE,
LIGHTMAP_TEXTURE
LIGHTMAP_TEXTURE,
UNUSED_TEXTURE
};
using TextureLoader = std::function<gpu::TexturePointer(const QImage&, const std::string&)>;

View file

@ -27,8 +27,6 @@
#include "KTXCache.h"
const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192;
namespace gpu {
class Batch;
}

View file

@ -34,7 +34,7 @@ Q_LOGGING_CATEGORY(trace_simulation_physics_detail, "trace.simulation.physics.de
#endif
static bool tracingEnabled() {
return DependencyManager::get<tracing::Tracer>()->isEnabled();
return DependencyManager::isSet<tracing::Tracer>() && DependencyManager::get<tracing::Tracer>()->isEnabled();
}
Duration::Duration(const QLoggingCategory& category, const QString& name, uint32_t argbColor, uint64_t payload, const QVariantMap& baseArgs) : _name(name), _category(category) {

View file

@ -106,6 +106,10 @@ namespace Setting {
return (_isSet) ? _value : other;
}
bool isSet() const {
return _isSet;
}
const T& getDefault() const {
return _defaultValue;
}

View file

@ -21,7 +21,6 @@ namespace Setting {
class Manager;
void init();
void cleanupSettings();
class Interface {
public:

View file

@ -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")

19
tools/oven/CMakeLists.txt Normal file
View file

@ -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)

32
tools/oven/src/Baker.cpp Normal file
View file

@ -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);
}

43
tools/oven/src/Baker.h Normal file
View file

@ -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 <QtCore/QObject>
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

View file

@ -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 <QtConcurrent>
#include <QtCore/QEventLoop>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#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<FBXBaker> 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<TextureBaker> 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<FBXBaker*>(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<TextureBaker*>(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;
}

View file

@ -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 <QtCore/QJsonArray>
#include <QtCore/QObject>
#include <QtCore/QUrl>
#include <QtCore/QThread>
#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<QUrl, QSharedPointer<FBXBaker>> _modelBakers;
QHash<QUrl, QSharedPointer<TextureBaker>> _skyboxBakers;
QMultiHash<QUrl, QJsonValueRef> _entitiesNeedingRewrite;
int _totalNumberOfSubBakes { 0 };
int _completedSubBakes { 0 };
};
#endif // hifi_DomainBaker_h

554
tools/oven/src/FBXBaker.cpp Normal file
View file

@ -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 <cmath> // need this include so we don't get an error looking for std::isnan
#include <fbxsdk.h>
#include <QtConcurrent>
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QEventLoop>
#include <QtCore/QFileInfo>
#include <QtCore/QThread>
#include <mutex>
#include <NetworkAccessManager.h>
#include <SharedUtil.h>
#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<QNetworkReply*>(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<FbxSurfaceLambert>(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<FbxTexture>() > 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<FbxFileTexture>();
for (int j = 0; j < numTextures; j++) {
FbxFileTexture* fileTexture = property.GetSrcObject<FbxFileTexture>(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<TextureBaker> 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<TextureBaker*>(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();
}
}
}

101
tools/oven/src/FBXBaker.h Normal file
View file

@ -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 <QtCore/QFutureSynchronizer>
#include <QtCore/QDir>
#include <QtCore/QUrl>
#include <QtNetwork/QNetworkReply>
#include "Baker.h"
#include "TextureBaker.h"
#include <gpu/Texture.h>
namespace fbxsdk {
class FbxManager;
class FbxProperty;
class FbxScene;
class FbxFileTexture;
}
static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
using FBXSDKManagerUniquePointer = std::unique_ptr<fbxsdk::FbxManager, std::function<void (fbxsdk::FbxManager *)>>;
using TextureBakerThreadGetter = std::function<QThread*()>;
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<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
TextureBakerThreadGetter _textureThreadGetter;
bool _copyOriginals { true };
bool _pendingErrorEmission { false };
};
#endif // hifi_FBXBaker_h

View file

@ -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");

View file

@ -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 <QtCore/QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(model_baking)
#endif // hifi_ModelBakingLoggingCategory_h

100
tools/oven/src/Oven.cpp Normal file
View file

@ -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 <QtCore/QDebug>
#include <QtCore/QThread>
#include <SettingInterface.h>
#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;
}

53
tools/oven/src/Oven.h Normal file
View file

@ -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 <QtWidgets/QApplication>
#include <tbb/concurrent_vector.h>
#include <atomic>
#if defined(qApp)
#undef qApp
#endif
#define qApp (static_cast<Oven*>(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<QThread*> _workerThreads;
std::atomic<uint> _nextWorkerThreadIndex;
int _numWorkerThreads;
};
#endif // hifi_Oven_h

View file

@ -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 <QtCore/QDir>
#include <QtCore/QEventLoop>
#include <QtCore/QFile>
#include <QtNetwork/QNetworkReply>
#include <image/Image.h>
#include <ktx/KTX.h>
#include <NetworkAccessManager.h>
#include <SharedUtil.h>
#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<QNetworkReply*>(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<const char*>(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();
}

View file

@ -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 <QtCore/QObject>
#include <QtCore/QUrl>
#include <QtCore/QRunnable>
#include <image/Image.h>
#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

16
tools/oven/src/main.cpp Normal file
View file

@ -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();
}

View file

@ -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 <QtWidgets/QStackedWidget>
#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<QStackedWidget*>(parentWidget());
stackedWidget->removeWidget(this);
this->deleteLater();
}

View file

@ -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 <QtWidgets/QWidget>
#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<std::unique_ptr<Baker>, int>;
using BakerRowPairList = std::list<BakerRowPair>;
BakerRowPairList _bakers;
};
#endif // hifi_BakeWidget_h

View file

@ -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 <QtConcurrent>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#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<DomainBaker> {
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<DomainBaker*>(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<DomainBaker*>(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);
}
}
}

View file

@ -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 <QtWidgets/QWidget>
#include <SettingHandle.h>
#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<QString> _domainNameSetting;
Setting::Handle<QString> _exportDirectory;
Setting::Handle<QString> _browseStartDirectory;
Setting::Handle<QString> _destinationPathSetting;
};
#endif // hifi_ModelBakeWidget_h

View file

@ -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 <QtWidgets/QFileDialog>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStackedWidget>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#include <QtCore/QThread>
#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<FBXBaker> {
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<FBXBaker*>(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);
}
}
}

View file

@ -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 <QtWidgets/QWidget>
#include <SettingHandle.h>
#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<QString> _exportDirectory;
Setting::Handle<QString> _modelStartDirectory;
};
#endif // hifi_ModelBakeWidget_h

View file

@ -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 <QtWidgets/QHBoxLayout>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStackedWidget>
#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<QStackedWidget*>(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<QStackedWidget*>(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<QStackedWidget*>(parentWidget());
// add a new widget for skybox baking to the stack, and switch to it
stackedWidget->setCurrentIndex(stackedWidget->addWidget(new SkyboxBakeWidget));
}

View file

@ -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 <QtWidgets/QWidget>
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

View file

@ -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 <QtWidgets/QStackedWidget>
#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;
}

View file

@ -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 <QtCore/QPointer>
#include <QtWidgets/QMainWindow>
#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> _resultsWindow;
};
#endif // hifi_OvenMainWindow_h

View file

@ -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 <QtCore/QUrl>
#include <QtGui/QDesktopServices>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QVBoxLayout>
#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);
}

View file

@ -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 <QtCore/QDir>
#include <QtWidgets/QWidget>
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<QDir> _outputDirectories;
};
#endif // hifi_ResultsWindow_h

View file

@ -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 <QtWidgets/QFileDialog>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStackedWidget>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#include <QtCore/QThread>
#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<TextureBaker> {
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<TextureBaker*>(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);
}
}
}

View file

@ -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 <QtWidgets/QWidget>
#include <SettingHandle.h>
#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<QString> _exportDirectory;
Setting::Handle<QString> _selectionStartDirectory;
};
#endif // hifi_SkyboxBakeWidget_h