mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-04-07 10:02:24 +02:00
Merge pull request #10243 from birarda/bake-textures
Add an internal tool for domain/model/skybox KTX texture baking
This commit is contained in:
commit
a88f72024d
36 changed files with 3116 additions and 5 deletions
114
cmake/modules/FindFBX.cmake
Normal file
114
cmake/modules/FindFBX.cmake
Normal 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()
|
|
@ -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>;
|
||||
|
|
|
@ -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&)>;
|
||||
|
|
|
@ -27,8 +27,6 @@
|
|||
|
||||
#include "KTXCache.h"
|
||||
|
||||
const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192;
|
||||
|
||||
namespace gpu {
|
||||
class Batch;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -106,6 +106,10 @@ namespace Setting {
|
|||
return (_isSet) ? _value : other;
|
||||
}
|
||||
|
||||
bool isSet() const {
|
||||
return _isSet;
|
||||
}
|
||||
|
||||
const T& getDefault() const {
|
||||
return _defaultValue;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ namespace Setting {
|
|||
class Manager;
|
||||
|
||||
void init();
|
||||
void cleanupSettings();
|
||||
|
||||
class Interface {
|
||||
public:
|
||||
|
|
|
@ -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
19
tools/oven/CMakeLists.txt
Normal 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
32
tools/oven/src/Baker.cpp
Normal 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
43
tools/oven/src/Baker.h
Normal 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
|
475
tools/oven/src/DomainBaker.cpp
Normal file
475
tools/oven/src/DomainBaker.cpp
Normal 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;
|
||||
}
|
||||
|
70
tools/oven/src/DomainBaker.h
Normal file
70
tools/oven/src/DomainBaker.h
Normal 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
554
tools/oven/src/FBXBaker.cpp
Normal 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
101
tools/oven/src/FBXBaker.h
Normal 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
|
14
tools/oven/src/ModelBakingLoggingCategory.cpp
Normal file
14
tools/oven/src/ModelBakingLoggingCategory.cpp
Normal 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");
|
19
tools/oven/src/ModelBakingLoggingCategory.h
Normal file
19
tools/oven/src/ModelBakingLoggingCategory.h
Normal 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
100
tools/oven/src/Oven.cpp
Normal 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
53
tools/oven/src/Oven.h
Normal 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
|
131
tools/oven/src/TextureBaker.cpp
Normal file
131
tools/oven/src/TextureBaker.cpp
Normal 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();
|
||||
}
|
59
tools/oven/src/TextureBaker.h
Normal file
59
tools/oven/src/TextureBaker.h
Normal 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
16
tools/oven/src/main.cpp
Normal 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();
|
||||
}
|
46
tools/oven/src/ui/BakeWidget.cpp
Normal file
46
tools/oven/src/ui/BakeWidget.cpp
Normal 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();
|
||||
}
|
33
tools/oven/src/ui/BakeWidget.h
Normal file
33
tools/oven/src/ui/BakeWidget.h
Normal 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
|
286
tools/oven/src/ui/DomainBakeWidget.cpp
Normal file
286
tools/oven/src/ui/DomainBakeWidget.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
54
tools/oven/src/ui/DomainBakeWidget.h
Normal file
54
tools/oven/src/ui/DomainBakeWidget.h
Normal 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
|
227
tools/oven/src/ui/ModelBakeWidget.cpp
Normal file
227
tools/oven/src/ui/ModelBakeWidget.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
51
tools/oven/src/ui/ModelBakeWidget.h
Normal file
51
tools/oven/src/ui/ModelBakeWidget.h
Normal 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
|
69
tools/oven/src/ui/ModesWidget.cpp
Normal file
69
tools/oven/src/ui/ModesWidget.cpp
Normal 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));
|
||||
}
|
31
tools/oven/src/ui/ModesWidget.h
Normal file
31
tools/oven/src/ui/ModesWidget.h
Normal 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
|
61
tools/oven/src/ui/OvenMainWindow.cpp
Normal file
61
tools/oven/src/ui/OvenMainWindow.cpp
Normal 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;
|
||||
}
|
34
tools/oven/src/ui/OvenMainWindow.h
Normal file
34
tools/oven/src/ui/OvenMainWindow.h
Normal 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
|
100
tools/oven/src/ui/ResultsWindow.cpp
Normal file
100
tools/oven/src/ui/ResultsWindow.cpp
Normal 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);
|
||||
}
|
39
tools/oven/src/ui/ResultsWindow.h
Normal file
39
tools/oven/src/ui/ResultsWindow.h
Normal 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
|
223
tools/oven/src/ui/SkyboxBakeWidget.cpp
Normal file
223
tools/oven/src/ui/SkyboxBakeWidget.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
50
tools/oven/src/ui/SkyboxBakeWidget.h
Normal file
50
tools/oven/src/ui/SkyboxBakeWidget.h
Normal 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
|
Loading…
Reference in a new issue