From 39f04adc8db1d9861815313af960e33599e77531 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 16 Aug 2017 13:51:40 -0700 Subject: [PATCH] Improve baking interface and add automatic baking to asset server --- assignment-client/CMakeLists.txt | 2 +- assignment-client/src/assets/AssetServer.cpp | 190 +++++- assignment-client/src/assets/AssetServer.h | 35 +- cmake/modules/FindFBX.cmake | 2 +- libraries/baking/CMakeLists.txt | 17 + .../oven => libraries/baking}/src/Baker.cpp | 0 {tools/oven => libraries/baking}/src/Baker.h | 0 libraries/baking/src/FBXBaker.cpp | 568 ++++++++++++++++++ libraries/baking/src/FBXBaker.h | 105 ++++ .../src/ModelBakingLoggingCategory.cpp | 0 .../baking}/src/ModelBakingLoggingCategory.h | 0 libraries/baking/src/TextureBaker.cpp | 131 ++++ libraries/baking/src/TextureBaker.h | 59 ++ libraries/fbx/CMakeLists.txt | 7 +- libraries/fbx/src/FBXReader.cpp | 4 + .../src/model-networking/ModelCache.cpp | 11 +- .../src/model-networking/ModelCache.h | 1 + .../src/model-networking/TextureCache.cpp | 10 +- .../networking/src/AssetResourceRequest.cpp | 6 +- libraries/networking/src/MappingRequest.cpp | 3 + libraries/networking/src/ResourceCache.cpp | 7 + libraries/networking/src/ResourceCache.h | 1 + libraries/networking/src/ResourceRequest.h | 1 + libraries/networking/src/udt/PacketHeaders.h | 15 +- tools/oven/CMakeLists.txt | 2 +- 25 files changed, 1139 insertions(+), 38 deletions(-) create mode 100644 libraries/baking/CMakeLists.txt rename {tools/oven => libraries/baking}/src/Baker.cpp (100%) rename {tools/oven => libraries/baking}/src/Baker.h (100%) create mode 100644 libraries/baking/src/FBXBaker.cpp create mode 100644 libraries/baking/src/FBXBaker.h rename {tools/oven => libraries/baking}/src/ModelBakingLoggingCategory.cpp (100%) rename {tools/oven => libraries/baking}/src/ModelBakingLoggingCategory.h (100%) create mode 100644 libraries/baking/src/TextureBaker.cpp create mode 100644 libraries/baking/src/TextureBaker.h diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 1a27ddd479..0421195612 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -13,7 +13,7 @@ setup_memory_debugger() link_hifi_libraries( audio avatars octree gpu model fbx entities networking animation recording shared script-engine embedded-webserver - controllers physics plugins midi + controllers physics plugins midi baking image ) if (WIN32) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 2082856f56..6e3195f570 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include @@ -33,6 +35,9 @@ #include "SendAssetTask.h" #include "UploadAssetTask.h" #include +#include + +#include static const uint8_t MIN_CORES_FOR_MULTICORE = 4; static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2; @@ -43,6 +48,128 @@ static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000; const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; +static const QStringList BAKEABLE_MODEL_EXTENSIONS = { "fbx" }; +static const QList BAKEABLE_TEXTURE_EXTENSIONS = QImageReader::supportedImageFormats(); +static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx"; +static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx"; + +BakeAssetTask::BakeAssetTask(const QString& assetHash, const QString& assetPath, const QString& filePath) + : _assetHash(assetHash), _assetPath(assetPath), _filePath(filePath) { +} + +void BakeAssetTask::run() { + qRegisterMetaType >("QVector"); + TextureBakerThreadGetter fn = []() -> QThread* { return QThread::currentThread(); }; + + if (_filePath.endsWith(".fbx")) { + FBXBaker baker(QUrl("file:///" + _filePath), fn, PathUtils::generateTemporaryDir()); + + QEventLoop loop; + connect(&baker, &Baker::finished, &loop, &QEventLoop::quit); + QMetaObject::invokeMethod(&baker, "bake", Qt::QueuedConnection); + qDebug() << "Running the bake!"; + loop.exec(); + + qDebug() << "Finished baking: " << _assetHash << _assetPath << baker.getOutputFiles(); + emit bakeComplete(_assetHash, _assetPath, QVector::fromStdVector(baker.getOutputFiles())); + } else { + TextureBaker baker(QUrl("file:///" + _filePath), image::TextureUsage::CUBE_TEXTURE, PathUtils::generateTemporaryDir()); + + QEventLoop loop; + connect(&baker, &Baker::finished, &loop, &QEventLoop::quit); + QMetaObject::invokeMethod(&baker, "bake", Qt::QueuedConnection); + qDebug() << "Running the bake!"; + loop.exec(); + + qDebug() << "Finished baking: " << _assetHash << _assetPath << baker.getBakedTextureFileName(); + emit bakeComplete(_assetHash, _assetPath, { baker.getDestinationFilePath() }); + } +} + +void AssetServer::bakeAsset(const QString& assetHash, const QString& assetPath, const QString& filePath) { + qDebug() << "Starting bake for: " << assetPath << assetHash; + auto it = _pendingBakes.find(assetHash); + if (it == _pendingBakes.end()) { + auto task = std::make_shared(assetHash, assetPath, filePath); + task->setAutoDelete(false); + _pendingBakes[assetHash] = task; + + connect(task.get(), &BakeAssetTask::bakeComplete, this, [this, assetPath](QString assetHash, QString assetPath, QVector outputFiles) { + handleCompletedBake(assetPath, assetHash, outputFiles); + }); + + _bakingTaskPool.start(task.get()); + } else { + qDebug() << "Already in queue"; + } +} + +QString AssetServer::getPathToAssetHash(const AssetHash& assetHash) { + return _filesDirectory.absoluteFilePath(assetHash); +} + +void AssetServer::bakeAssets() { + auto it = _fileMappings.cbegin(); + for (; it != _fileMappings.cend(); ++it) { + auto path = it.key(); + auto hash = it.value().toString(); + maybeBake(path, hash); + } +} + +void AssetServer::maybeBake(const AssetPath& path, const AssetHash& hash) { + if (needsToBeBaked(path, hash)) { + qDebug() << "Queuing bake of: " << path; + bakeAsset(hash, path, getPathToAssetHash(hash)); + } +} + +void AssetServer::createEmptyMetaFile(const AssetHash& hash) { + QString metaFilePath = "atp:/" + hash + "/meta.json"; + QFile metaFile { metaFilePath }; + + if (!metaFile.exists()) { + qDebug() << "Creating metfaile for " << hash; + if (metaFile.open(QFile::WriteOnly)) { + qDebug() << "Created metfaile for " << hash; + metaFile.write("{}"); + } + } +} + +bool AssetServer::hasMetaFile(const AssetHash& hash) { + QString metaFilePath = "/.baked/" + hash + "/meta.json"; + qDebug() << "in mappings?" << metaFilePath; + + return _fileMappings.contains(metaFilePath); +} + +bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHash) { + if (path.startsWith("/.baked/")) { + return false; + } + + auto dotIndex = path.lastIndexOf("."); + if (dotIndex == -1) { + return false; + } + + auto extension = path.mid(dotIndex + 1); + + QString bakedFilename; + + if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) { + bakedFilename = BAKED_MODEL_SIMPLE_NAME; + } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(assetHash)) { + bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; + } else { + return false; + } + + auto bakedPath = "/.baked/" + assetHash + "/" + bakedFilename; + return !_fileMappings.contains(bakedPath); +} + bool interfaceRunning() { bool result = false; @@ -76,13 +203,15 @@ void updateConsumedCores() { AssetServer::AssetServer(ReceivedMessage& message) : ThreadedAssignment(message), - _taskPool(this) + _transferTaskPool(this), + _bakingTaskPool(this) { // Most of the work will be I/O bound, reading from disk and constructing packet objects, // so the ideal is greater than the number of cores on the system. static const int TASK_POOL_THREAD_COUNT = 50; - _taskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT); + _transferTaskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT); + _bakingTaskPool.setMaxThreadCount(1); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); @@ -196,6 +325,8 @@ void AssetServer::completeSetup() { } nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); + + bakeAssets(); } else { qCritical() << "Asset Server assignment will not continue because mapping file could not be loaded."; setFinished(true); @@ -265,27 +396,27 @@ void AssetServer::handleAssetMappingOperation(QSharedPointer me nodeList->sendPacketList(std::move(replyPacket), *senderNode); } -static const QStringList BAKEABLE_MODEL_EXTENSIONS = { ".fbx" }; -static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx"; -static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx"; - void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { QString assetPath = message.readString(); + QUrl url { assetPath }; + assetPath = url.path(); + auto it = _fileMappings.find(assetPath); if (it != _fileMappings.end()) { // check if we should re-direct to a baked asset // first, figure out from the mapping extension what type of file this is - auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.')).toLower(); + auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.') + 1).toLower(); QString bakedRootFile; if (BAKEABLE_MODEL_EXTENSIONS.contains(assetPathExtension)) { bakedRootFile = BAKED_MODEL_SIMPLE_NAME; - } else if (QImageReader::supportedImageFormats().contains(assetPathExtension.toLocal8Bit())) { + } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(assetPathExtension.toLocal8Bit())) { bakedRootFile = BAKED_TEXTURE_SIMPLE_NAME; } + qDebug() << bakedRootFile << assetPathExtension; auto originalAssetHash = it->toString(); QString redirectedAssetHash; @@ -298,9 +429,12 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode auto bakedIt = _fileMappings.find(bakedAssetPath); if (bakedIt != _fileMappings.end()) { + qDebug() << "Did find baked version for: " << originalAssetHash << assetPath; // we found a baked version of the requested asset to serve, redirect to that redirectedAssetHash = bakedIt->toString(); wasRedirected = true; + } else { + qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath; } } @@ -319,6 +453,15 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode } else { replyPacket.write(QByteArray::fromHex(originalAssetHash.toUtf8())); replyPacket.writePrimitive(wasRedirected); + + + auto query = QUrlQuery(url.query()); + bool isSkybox = query.hasQueryItem("skybox"); + qDebug() << "Is skybox? " << isSkybox; + if (isSkybox) { + createMetaFile(originalAssetHash); + maybeBake(originalAssetHash, assetPath); + } } } else { replyPacket.writePrimitive(AssetServerError::AssetNotFound); @@ -437,7 +580,7 @@ void AssetServer::handleAssetGet(QSharedPointer message, Shared // Queue task auto task = new SendAssetTask(message, senderNode, _filesDirectory); - _taskPool.start(task); + _transferTaskPool.start(task); } void AssetServer::handleAssetUpload(QSharedPointer message, SharedNodePointer senderNode) { @@ -446,7 +589,7 @@ void AssetServer::handleAssetUpload(QSharedPointer message, Sha qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID()); auto task = new UploadAssetTask(message, senderNode, _filesDirectory); - _taskPool.start(task); + _transferTaskPool.start(task); } else { // this is a node the domain told us is not allowed to rez entities // for now this also means it isn't allowed to add assets @@ -632,6 +775,7 @@ bool AssetServer::setMapping(AssetPath path, AssetHash hash) { if (writeMappingsToFile()) { // persistence succeeded, we are good to go qDebug() << "Set mapping:" << path << "=>" << hash; + maybeBake(path, hash); return true; } else { // failed to persist this mapping to file - put back the old one in our in-memory representation @@ -836,18 +980,17 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { static const QString HIDDEN_BAKED_CONTENT_FOLDER = "/.baked/"; -void AssetServer::handleCompletedBake(AssetHash originalAssetHash, QDir temporaryOutputDir) { - // enumerate the baking result files in the temporary directory - QDirIterator dirIterator(temporaryOutputDir.absolutePath(), QDir::Files, QDirIterator::Subdirectories); - +void AssetServer::handleCompletedBake(AssetPath originalAssetPath, AssetHash originalAssetHash, QVector bakedFilePaths) { bool errorCompletingBake { false }; - while (dirIterator.hasNext()) { - QString filePath = dirIterator.next(); + qDebug() << "Completing bake for " << originalAssetHash; + for (auto& filePath: bakedFilePaths) { // figure out the hash for the contents of this file QFile file(filePath); + qDebug() << "File path: " << filePath; + AssetHash bakedFileHash; if (file.open(QIODevice::ReadOnly)) { @@ -873,26 +1016,33 @@ void AssetServer::handleCompletedBake(AssetHash originalAssetHash, QDir temporar } // setup the mapping for this bake file - auto relativeFilePath = temporaryOutputDir.relativeFilePath(filePath); - static const QString BAKED_ASSET_SIMPLE_NAME = "asset.fbx"; + auto relativeFilePath = QUrl(filePath).fileName(); + qDebug() << "Relative file path is: " << relativeFilePath; + static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx"; + static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx"; if (relativeFilePath.endsWith(".fbx", Qt::CaseInsensitive)) { // for an FBX file, we replace the filename with the simple name // (to handle the case where two mapped assets have the same hash but different names) - relativeFilePath = BAKED_ASSET_SIMPLE_NAME; + relativeFilePath = BAKED_ASSET_SIMPLE_FBX_NAME; + } else if (!originalAssetPath.endsWith(".fbx")) { + relativeFilePath = BAKED_ASSET_SIMPLE_TEXTURE_NAME; + } QString bakeMapping = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + relativeFilePath; // add a mapping (under the hidden baked folder) for this file resulting from the bake - if (setMapping(bakeMapping , bakedFileHash)) { + if (setMapping(bakeMapping, bakedFileHash)) { qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash; } else { + qDebug() << "Failed to set mapping"; // stop handling this bake, couldn't add a mapping for this bake file errorCompletingBake = true; break; } } else { + qDebug() << "Failed to open baked file: " << filePath; // stop handling this bake, we couldn't open one of the files for reading errorCompletingBake = true; break; diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index 191ad25b1f..2dfc5f6c35 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -14,12 +14,29 @@ #include #include +#include #include #include "AssetUtils.h" #include "ReceivedMessage.h" +class BakeAssetTask : public QObject, public QRunnable { + Q_OBJECT +public: + BakeAssetTask(const QString& assetHash, const QString& assetPath, const QString& filePath); + + void run() override; + +signals: + void bakeComplete(QString assetHash, QString assetPath, QVector outputFiles); + +private: + QString _assetHash; + QString _assetPath; + QString _filePath; +}; + class AssetServer : public ThreadedAssignment { Q_OBJECT public: @@ -63,8 +80,17 @@ private: /// Delete any unmapped files from the local asset directory void cleanupUnmappedFiles(); + QString getPathToAssetHash(const AssetHash& assetHash); + + void bakeAssets(); + void maybeBake(const AssetPath& path, const AssetHash& hash); + void createEmptyMetaFile(const AssetHash& hash); + bool hasMetaFile(const AssetHash& hash); + bool needsToBeBaked(const AssetPath& path, const AssetHash& assetHash); + void bakeAsset(const QString& assetHash, const QString& assetPath, const QString& filePath); + /// Move baked content for asset to baked directory and update baked status - void handleCompletedBake(AssetHash originalAssetHash, QDir temporaryOutputDir); + void handleCompletedBake(AssetPath assetPath, AssetHash originalAssetHash, QVector bakedFilePaths); /// Create meta file to describe baked content for original asset bool createMetaFile(AssetHash originalAssetHash); @@ -73,7 +99,12 @@ private: QDir _resourcesDirectory; QDir _filesDirectory; - QThreadPool _taskPool; + + /// Task pool for handling uploads and downloads of assets + QThreadPool _transferTaskPool; + + QHash> _pendingBakes; + QThreadPool _bakingTaskPool; }; #endif diff --git a/cmake/modules/FindFBX.cmake b/cmake/modules/FindFBX.cmake index 9a1d08a010..6eb6d11f9d 100644 --- a/cmake/modules/FindFBX.cmake +++ b/cmake/modules/FindFBX.cmake @@ -57,7 +57,7 @@ endif() function(_fbx_find_library _name _lib _suffix) if (MSVC_VERSION EQUAL 1910) - set(VS_PREFIX vs2017) + set(VS_PREFIX vs2015) elseif (MSVC_VERSION EQUAL 1900) set(VS_PREFIX vs2015) elseif (MSVC_VERSION EQUAL 1800) diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt new file mode 100644 index 0000000000..44c52ae0d8 --- /dev/null +++ b/libraries/baking/CMakeLists.txt @@ -0,0 +1,17 @@ +set(TARGET_NAME baking) +setup_hifi_library(Concurrent) + +find_package(FBX) +if (FBX_FOUND) + if (CMAKE_THREAD_LIBS_INIT) + target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES} "${CMAKE_THREAD_LIBS_INIT}") + else () + target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES}) + endif () + 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) + +link_hifi_libraries(shared model networking ktx image) +include_hifi_library_headers(gpu) diff --git a/tools/oven/src/Baker.cpp b/libraries/baking/src/Baker.cpp similarity index 100% rename from tools/oven/src/Baker.cpp rename to libraries/baking/src/Baker.cpp diff --git a/tools/oven/src/Baker.h b/libraries/baking/src/Baker.h similarity index 100% rename from tools/oven/src/Baker.h rename to libraries/baking/src/Baker.h diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp new file mode 100644 index 0000000000..1a11071b5b --- /dev/null +++ b/libraries/baking/src/FBXBaker.cpp @@ -0,0 +1,568 @@ +// +// FBXBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 3/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include // need this include so we don't get an error looking for std::isnan + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#include "ModelBakingLoggingCategory.h" +#include "TextureBaker.h" + +#include "FBXBaker.h" + +std::once_flag onceFlag; +FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr }; + +FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter, + const QString& bakedOutputDir, const QString& originalOutputDir) : + _fbxURL(fbxURL), + _bakedOutputDir(bakedOutputDir), + _originalOutputDir(originalOutputDir), + _textureThreadGetter(textureThreadGetter) +{ + std::call_once(onceFlag, [](){ + // create the static FBX SDK manager + _sdkManager = FBXSDKManagerUniquePointer(FbxManager::Create(), [](FbxManager* manager){ + manager->Destroy(); + }); + }); +} + +void FBXBaker::bake() { + auto tempDir = PathUtils::generateTemporaryDir(); + + if (tempDir.isEmpty()) { + handleError("Failed to create a temporary directory."); + return; + } + + _tempDir = tempDir; + + _originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName()); + qDebug() << "Made temporary dir " << _tempDir; + qDebug() << "Origin file path: " << _originalFBXFilePath; + + // 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() { + // make sure there isn't already an output directory using the same name + int iteration = 0; + + if (QDir(_bakedOutputDir).exists()) { + qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + //_bakedOutputDir = _baseOutputPath + "/" + _fbxName + "-" + QString::number(++iteration) + "/"; + } else { + qCDebug(model_baking) << "Creating FBX output folder" << _bakedOutputDir; + + // attempt to make the output folder + if (!QDir().mkpath(_bakedOutputDir)) { + handleError("Failed to create FBX output folder " + _bakedOutputDir); + return; + } + // attempt to make the output folder + if (!QDir().mkpath(_originalOutputDir)) { + handleError("Failed to create FBX output folder " + _bakedOutputDir); + 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() }; + + qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath; + + if (!localFBX.exists()) { + //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); + handleError("Could not find " + _fbxURL.toString()); + return; + } + + // make a copy in the output folder + if (!_originalOutputDir.isEmpty()) { + qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName(); + localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName()); + } + + localFBX.copy(_originalFBXFilePath); + + // emit our signal to start the import of the FBX source copy + emit sourceCopyReadyToLoad(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + + networkRequest.setUrl(_fbxURL); + + qCDebug(model_baking) << "Downloading" << _fbxURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); + } +} + +void FBXBaker::handleFBXNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _fbxURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(_originalFBXFilePath); + + qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly)) { + // 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() + " (Failed to open " + _originalFBXFilePath + ")"); + return; + } + if (copyOfOriginal.write(requestReply->readAll()) == -1) { + handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)"); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + if (!_originalOutputDir.isEmpty()) { + copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName()); + } + + // 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(), ""); + + qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists(); + // import the copy of the original FBX file + bool importStatus = importer->Initialize(_originalFBXFilePath.toLocal8Bit().data()); + + if (!importStatus) { + // failed to initialize importer, print an error and return + handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString()); + return; + } else { + qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene"; + } + + // setup a new scene to hold the imported file + _scene = FbxScene::Create(_sdkManager.get(), "bakeScene"); + + // import the file to the created scene + importer->Import(_scene); + + // destroy the importer that is no longer needed + importer->Destroy(); +} + +QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { + auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (texturePath.startsWith(fbxPath)) { + // texture path is a child of the FBX path, return the texture path without the fbx path + return texturePath.mid(fbxPath.length()); + } else { + // the texture path was not a child of the FBX path, return the empty string + return ""; + } +} + +QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { + // first make sure we have a unique base name for this texture + // in case another texture referenced by this model has the same base name + auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; + + QString bakedTextureFileName { textureFileInfo.completeBaseName() }; + + if (nameMatches > 0) { + // there are already nameMatches texture with this name + // append - and that number to our baked texture file name so that it is unique + bakedTextureFileName += "-" + QString::number(nameMatches); + } + + bakedTextureFileName += BAKED_TEXTURE_EXT; + + // increment the number of name matches + ++nameMatches; + + return bakedTextureFileName; +} + +QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) { + QUrl urlToTexture; + + if (textureFileInfo.exists() && textureFileInfo.isFile()) { + // set the texture URL to the local texture that we have confirmed exists + urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); + } else { + // external texture that we'll need to download or find + + // first check if it the RelativePath to the texture in the FBX was relative + QString relativeFileName = fileTexture->GetRelativeFileName(); + auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); + + // this is a relative file path which will require different handling + // depending on the location of the original FBX + if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + // the absolute path we ran into for the texture in the FBX exists on this machine + // so use that file + urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); + } else { + // we didn't find the texture on this machine at the absolute path + // so assume that it is right beside the FBX to match the behaviour of interface + urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); + } + } + + return urlToTexture; +} + +image::TextureUsage::Type textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) { + using namespace image::TextureUsage; + + // this is a property we know has a texture, we need to match it to a High Fidelity known texture type + // since that information is passed to the baking process + + // grab the hierarchical name for this property and lowercase it for case-insensitive compare + auto propertyName = QString(property.GetHierarchicalName()).toLower(); + + // figure out the type of the property based on what known value string it matches + if ((propertyName.contains("diffuse") && !propertyName.contains("tex_global_diffuse")) + || propertyName.contains("tex_color_map")) { + return ALBEDO_TEXTURE; + } else if (propertyName.contains("transparentcolor") || propertyName.contains("transparencyfactor")) { + return ALBEDO_TEXTURE; + } else if (propertyName.contains("bump")) { + return BUMP_TEXTURE; + } else if (propertyName.contains("normal")) { + return NORMAL_TEXTURE; + } else if ((propertyName.contains("specular") && !propertyName.contains("tex_global_specular")) + || propertyName.contains("reflection")) { + return SPECULAR_TEXTURE; + } else if (propertyName.contains("tex_metallic_map")) { + return METALLIC_TEXTURE; + } else if (propertyName.contains("shininess")) { + return GLOSS_TEXTURE; + } else if (propertyName.contains("tex_roughness_map")) { + return ROUGHNESS_TEXTURE; + } else if (propertyName.contains("emissive")) { + return EMISSIVE_TEXTURE; + } else if (propertyName.contains("ambientcolor")) { + return LIGHTMAP_TEXTURE; + } else if (propertyName.contains("ambientfactor")) { + // we need to check what the ambient factor is, since that tells Interface to process this texture + // either as an occlusion texture or a light map + auto lambertMaterial = FbxCast(material); + + if (lambertMaterial->AmbientFactor == 0) { + return LIGHTMAP_TEXTURE; + } else if (lambertMaterial->AmbientFactor > 0) { + return OCCLUSION_TEXTURE; + } else { + return UNUSED_TEXTURE; + } + + } else if (propertyName.contains("tex_ao_map")) { + return OCCLUSION_TEXTURE; + } + + return UNUSED_TEXTURE; +} + +void FBXBaker::rewriteAndBakeSceneTextures() { + + // enumerate the surface materials to find the textures used in the scene + int numMaterials = _scene->GetMaterialCount(); + for (int i = 0; i < numMaterials; i++) { + FbxSurfaceMaterial* material = _scene->GetMaterial(i); + + if (material) { + // enumerate the properties of this material to see what texture channels it might have + FbxProperty property = material->GetFirstProperty(); + + while (property.IsValid()) { + // first check if this property has connected textures, if not we don't need to bother with it here + if (property.GetSrcObjectCount() > 0) { + + // figure out the type of texture from the material property + auto textureType = textureTypeForMaterialProperty(property, material); + + if (textureType != image::TextureUsage::UNUSED_TEXTURE) { + int numTextures = property.GetSrcObjectCount(); + + for (int j = 0; j < numTextures; j++) { + FbxFileTexture* fileTexture = property.GetSrcObject(j); + + // use QFileInfo to easily split up the existing texture filename into its components + QString fbxTextureFileName { fileTexture->GetFileName() }; + QFileInfo textureFileInfo { fbxTextureFileName.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 { + _bakedOutputDir + "/" + bakedTextureFileName + }; + _outputFiles.push_back(bakedTextureFilePath); + + qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName() + << "to" << bakedTextureFilePath; + + // figure out the URL to this texture, embedded or external + auto urlToTexture = getTextureURL(textureFileInfo, fileTexture); + + // write the new filename into the FBX scene + fileTexture->SetFileName(bakedTextureFilePath.toUtf8().data()); + + // write the relative filename to be the baked texture file name since it will + // be right beside the FBX + fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData()); + + if (!_bakingTextures.contains(urlToTexture)) { + // bake this texture asynchronously + bakeTexture(urlToTexture, textureType, _bakedOutputDir); + } + } + } + } + } + + property = material->GetNextProperty(property); + } + } + } +} + +void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir) { + // start a bake for this texture and add it to our list to keep track of + QSharedPointer bakingTexture { + new TextureBaker(textureURL, textureType, outputDir), + &TextureBaker::deleteLater + }; + + // make sure we hear when the baking texture is done + connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); + + // keep a shared pointer to the baking texture + _bakingTextures.insert(textureURL, bakingTexture); + + // start baking the texture on one of our available worker threads + bakingTexture->moveToThread(_textureThreadGetter()); + QMetaObject::invokeMethod(bakingTexture.data(), "bake"); +} + +void FBXBaker::handleBakedTexture() { + TextureBaker* bakedTexture = qobject_cast(sender()); + + // make sure we haven't already run into errors, and that this is a valid texture + if (bakedTexture) { + if (!hasErrors()) { + if (!bakedTexture->hasErrors()) { + if (!_originalOutputDir.isEmpty()) { + // 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(_originalOutputDir); + + 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 { + _originalOutputDir + "/" + 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(), ""); + + // save the relative path to this FBX inside our passed output folder + + auto fileName = _fbxURL.fileName(); + auto baseName = fileName.left(fileName.lastIndexOf('.')); + auto bakedFilename = baseName + BAKED_FBX_EXTENSION; + + _bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename; + + bool exportStatus = exporter->Initialize(_bakedFBXFilePath.toLocal8Bit().data()); + + if (!exportStatus) { + // failed to initialize exporter, print an error and return + handleError("Failed to export FBX file at " + _fbxURL.toString() + " to " + _bakedFBXFilePath + + "- error: " + exporter->GetStatus().GetErrorString()); + } + + _outputFiles.push_back(_bakedFBXFilePath); + + // export the scene + exporter->Export(_scene); + + qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath; +} + + +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(_bakedOutputDir + ORIGINAL_OUTPUT_SUBFOLDER + embeddedMediaFolderName).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(); + + 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, emitting finsihed" << _fbxURL; + + emit finished(); + } + } +} diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h new file mode 100644 index 0000000000..0e52f2e4c3 --- /dev/null +++ b/libraries/baking/src/FBXBaker.h @@ -0,0 +1,105 @@ +// +// FBXBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 3/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_FBXBaker_h +#define hifi_FBXBaker_h + +#include +#include +#include +#include + +#include "Baker.h" +#include "TextureBaker.h" + +#include "ModelBakingLoggingCategory.h" + +#include + +namespace fbxsdk { + class FbxManager; + class FbxProperty; + class FbxScene; + class FbxFileTexture; +} + +static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; +using FBXSDKManagerUniquePointer = std::unique_ptr>; + +using TextureBakerThreadGetter = std::function; + +class FBXBaker : public Baker { + Q_OBJECT +public: + FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter, + const QString& bakedOutputDir, const QString& originalOutputDir = ""); + + QUrl getFBXUrl() const { return _fbxURL; } + QString getBakedFBXFilePath() const { return _bakedFBXFilePath; } + std::vector getOutputFiles() const { return _outputFiles; } + +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 importScene(); + void rewriteAndBakeSceneTextures(); + void exportScene(); + void removeEmbeddedMediaFolder(); + + 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); + + QUrl _fbxURL; + + QString _bakedFBXFilePath; + + QString _bakedOutputDir; + + // If set, the original FBX and textures will also be copied here + QString _originalOutputDir; + + QDir _tempDir; + QString _originalFBXFilePath; + + // List of baked output files, includes the FBX and textures + std::vector _outputFiles; + + static FBXSDKManagerUniquePointer _sdkManager; + fbxsdk::FbxScene* _scene { nullptr }; + + QMultiHash> _bakingTextures; + QHash _textureNameMatchCount; + + TextureBakerThreadGetter _textureThreadGetter; + + bool _pendingErrorEmission { false }; +}; + +#endif // hifi_FBXBaker_h diff --git a/tools/oven/src/ModelBakingLoggingCategory.cpp b/libraries/baking/src/ModelBakingLoggingCategory.cpp similarity index 100% rename from tools/oven/src/ModelBakingLoggingCategory.cpp rename to libraries/baking/src/ModelBakingLoggingCategory.cpp diff --git a/tools/oven/src/ModelBakingLoggingCategory.h b/libraries/baking/src/ModelBakingLoggingCategory.h similarity index 100% rename from tools/oven/src/ModelBakingLoggingCategory.h rename to libraries/baking/src/ModelBakingLoggingCategory.h diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp new file mode 100644 index 0000000000..70df511d2c --- /dev/null +++ b/libraries/baking/src/TextureBaker.cpp @@ -0,0 +1,131 @@ +// +// TextureBaker.cpp +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ModelBakingLoggingCategory.h" + +#include "TextureBaker.h" + +const QString BAKED_TEXTURE_EXT = ".ktx"; + +TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) : + _textureURL(textureURL), + _textureType(textureType), + _outputDirectory(outputDirectory) +{ + // figure out the baked texture filename + auto originalFilename = textureURL.fileName(); + _bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT; +} + +void TextureBaker::bake() { + // once our texture is loaded, kick off a the processing + connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); + + // first load the texture (either locally or remotely) + loadTexture(); +} + +void TextureBaker::loadTexture() { + // check if the texture is local or first needs to be downloaded + if (_textureURL.isLocalFile()) { + // load up the local file + QFile localTexture { _textureURL.toLocalFile() }; + + if (!localTexture.open(QIODevice::ReadOnly)) { + handleError("Unable to open texture " + _textureURL.toString()); + return; + } + + _originalTexture = localTexture.readAll(); + + emit originalTextureLoaded(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_textureURL); + + qCDebug(model_baking) << "Downloading" << _textureURL; + + // kickoff the download, wait for slot to tell us it is done + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply); + } +} + +void TextureBaker::handleTextureNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded texture" << _textureURL; + + // store the original texture so it can be passed along for the bake + _originalTexture = requestReply->readAll(); + + emit originalTextureLoaded(); + } else { + // add an error to our list stating that this texture could not be downloaded + handleError("Error downloading " + _textureURL.toString() + " - " + requestReply->errorString()); + } +} + +void TextureBaker::processTexture() { + auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(), + ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType); + + if (!processedTexture) { + handleError("Could not process texture " + _textureURL.toString()); + return; + } + + // the baked textures need to have the source hash added for cache checks in Interface + // so we add that to the processed texture before handling it off to be serialized + auto hashData = QCryptographicHash::hash(_originalTexture, QCryptographicHash::Md5); + std::string hash = hashData.toHex().toStdString(); + processedTexture->setSourceHash(hash); + + auto memKTX = gpu::Texture::serialize(*processedTexture); + + if (!memKTX) { + handleError("Could not serialize " + _textureURL.toString() + " to KTX"); + return; + } + + const char* data = reinterpret_cast(memKTX->_storage->data()); + const size_t length = memKTX->_storage->size(); + + // attempt to write the baked texture to the destination file path + QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) }; + + if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { + handleError("Could not write baked texture for " + _textureURL.toString()); + } + + qCDebug(model_baking) << "Baked texture" << _textureURL; + emit finished(); +} diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h new file mode 100644 index 0000000000..ee1e968f20 --- /dev/null +++ b/libraries/baking/src/TextureBaker.h @@ -0,0 +1,59 @@ +// +// TextureBaker.h +// tools/oven/src +// +// Created by Stephen Birarda on 4/5/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TextureBaker_h +#define hifi_TextureBaker_h + +#include +#include +#include + +#include + +#include "Baker.h" + +extern const QString BAKED_TEXTURE_EXT; + +class TextureBaker : public Baker { + Q_OBJECT + +public: + TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory); + + const QByteArray& getOriginalTexture() const { return _originalTexture; } + + QUrl getTextureURL() const { return _textureURL; } + + QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); } + QString getBakedTextureFileName() const { return _bakedTextureFileName; } + +public slots: + virtual void bake() override; + +signals: + void originalTextureLoaded(); + +private slots: + void processTexture(); + +private: + void loadTexture(); + void handleTextureNetworkReply(); + + QUrl _textureURL; + QByteArray _originalTexture; + image::TextureUsage::Type _textureType; + + QDir _outputDirectory; + QString _bakedTextureFileName; +}; + +#endif // hifi_TextureBaker_h diff --git a/libraries/fbx/CMakeLists.txt b/libraries/fbx/CMakeLists.txt index d9c073f213..3355ccd06e 100644 --- a/libraries/fbx/CMakeLists.txt +++ b/libraries/fbx/CMakeLists.txt @@ -1,4 +1,7 @@ set(TARGET_NAME fbx) setup_hifi_library() -link_hifi_libraries(shared model networking) -include_hifi_library_headers(gpu) + + + +link_hifi_libraries(shared model networking image) +include_hifi_library_headers(gpu image) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 6d4f586c52..b1d2cf17d4 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -846,12 +846,14 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QByteArray filename = subobject.properties.at(0).toByteArray(); QByteArray filepath = filename.replace('\\', '/'); filename = fileOnUrl(filepath, url); + qDebug() << "Filename" << filepath << filename; _textureFilepaths.insert(getID(object.properties), filepath); _textureFilenames.insert(getID(object.properties), filename); } else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) { // trim the name from the timestamp QString name = QString(subobject.properties.at(0).toByteArray()); name = name.left(name.indexOf('[')); + qDebug() << "Filename" << name; _textureNames.insert(getID(object.properties), name); } else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) { tex.assign(tex.alphaSource, subobject.properties.at(0).value()); @@ -1840,5 +1842,7 @@ FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri reader._loadLightmaps = loadLightmaps; reader._lightmapLevel = lightmapLevel; + qDebug() << "Reading FBX: " << url; + return reader.extractFBXGeometry(mapping, url); } diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 704455c981..468d22ce9e 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -77,6 +77,8 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { texdir += '/'; } _textureBaseUrl = resolveTextureBaseUrl(url, _url.resolved(texdir)); + } else { + _textureBaseUrl = _effectiveBaseURL; } auto animGraphVariant = mapping.value("animGraphUrl"); @@ -239,7 +241,10 @@ private: }; void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { - QThreadPool::globalInstance()->start(new GeometryReader(_self, _url, _mapping, data, _combineParts)); + qDebug() << "Processing geometry: " << _effectiveBaseURL; + _url = _effectiveBaseURL; + _textureBaseUrl = _effectiveBaseURL; + QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts)); } void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxGeometry) { @@ -250,6 +255,7 @@ void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxG QHash materialIDAtlas; for (const FBXMaterial& material : _fbxGeometry->materials) { materialIDAtlas[material.materialID] = _materials.size(); + qDebug() << "setGeometryDefinition() " << _textureBaseUrl; _materials.push_back(std::make_shared(material, _textureBaseUrl)); } @@ -342,6 +348,7 @@ Geometry::Geometry(const Geometry& geometry) { _materials.reserve(geometry._materials.size()); for (const auto& material : geometry._materials) { + qDebug() << "Geometry() no base url..."; _materials.push_back(std::make_shared(*material)); } @@ -427,6 +434,7 @@ void GeometryResource::deleter() { void GeometryResource::setTextures() { if (_fbxGeometry) { for (const FBXMaterial& material : _fbxGeometry->materials) { + qDebug() << "setTextures() " << _textureBaseUrl; _materials.push_back(std::make_shared(material, _textureBaseUrl)); } } @@ -528,6 +536,7 @@ model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) : model::Material(*material._material) { + qDebug() << "Created network material with base url: " << textureBaseUrl; _textures = Textures(MapChannel::NUM_MAP_CHANNELS); if (!material.albedoTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 6a1cc4c466..a122e03eb9 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -131,6 +131,7 @@ private: Geometry::Pointer& _geometryRef; }; + /// Stores cached model geometries. class ModelCache : public ResourceCache, public Dependency { Q_OBJECT diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 85bde4c2f1..5b43a4090f 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #if DEBUG_DUMP_TEXTURE_LOADS #include @@ -189,8 +190,15 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs if (url.scheme() == RESOURCE_SCHEME) { return getResourceTexture(url); } + auto modifiedUrl = url; + if (type == image::TextureUsage::CUBE_TEXTURE) { + QUrlQuery query { url.query() }; + query.addQueryItem("skybox", ""); + qDebug() << "Updating cubemap texture query from" << url.query() << "to" << query.toString(); + modifiedUrl.setQuery(query.toString()); + } TextureExtra extra = { type, content, maxNumPixels }; - return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); + return ResourceCache::getResource(modifiedUrl, QUrl(), &extra).staticCast(); } gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index e228582ca9..01fe971125 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -63,7 +63,7 @@ void AssetResourceRequest::doSend() { // This is an ATP path, we'll need to figure out what the mapping is. // This may incur a roundtrip to the asset-server, or it may return immediately from the cache in AssetClient. - auto path = _url.path(); + auto path = _url.path() + (_url.hasQuery() ? "?" + _url.query() : ""); requestMappingForPath(path); } } @@ -93,10 +93,8 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { // if we got a redirected path we need to store that with the resource request as relative path URL if (request->wasRedirected()) { + qDebug() << "Request was redirected"; _relativePathURL = ATP_SCHEME + request->getRedirectedPath(); - - // truncate the filename for the re-directed asset so we actually have a path - _relativePathURL = _relativePathURL.adjusted(QUrl::RemoveFilename); } break; diff --git a/libraries/networking/src/MappingRequest.cpp b/libraries/networking/src/MappingRequest.cpp index bd7c05bdf7..5fb9c32a0b 100644 --- a/libraries/networking/src/MappingRequest.cpp +++ b/libraries/networking/src/MappingRequest.cpp @@ -96,6 +96,9 @@ void GetMappingRequest::doStart() { // if it did grab that re-directed path if (_wasRedirected) { _redirectedPath = message->readString(); + qDebug() << "Got redirected from " << _path << " to " << _redirectedPath; + } else { + qDebug() << "Not redirected: " << _path; } } diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index fbdfa4b87a..b7ab1269f9 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -731,6 +731,13 @@ void Resource::handleReplyFinished() { if (result == ResourceRequest::Success) { auto extraInfo = _url == _activeUrl ? "" : QString(", %1").arg(_activeUrl.toDisplayString()); qCDebug(networking).noquote() << QString("Request finished for %1%2").arg(_url.toDisplayString(), extraInfo); + + auto relativePathURL = _request->getRelativePathUrl(); + qDebug() << "Relative path is: " << relativePathURL; + if (!relativePathURL.isEmpty()) { + qDebug() << "setting effective path"; + _effectiveBaseURL = relativePathURL; + } auto data = _request->getData(); emit loaded(data); diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index f94e1e26d2..40df6418a5 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -452,6 +452,7 @@ protected: bool handleFailedRequest(ResourceRequest::Result result); QUrl _url; + QUrl _effectiveBaseURL{ _url }; QUrl _activeUrl; ByteRange _requestByteRange; diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index bae0882e97..0a4dc12293 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -66,6 +66,7 @@ public: Result getResult() const { return _result; } QString getResultString() const; QUrl getUrl() const { return _url; } + QUrl getRelativePathUrl() const { return _relativePathURL; } bool loadedFromCache() const { return _loadedFromCache; } bool getRangeRequestSuccessful() const { return _rangeRequestSuccessful; } bool getTotalSizeOfResource() const { return _totalSizeOfResource; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ea831bd415..6b57c8a3cb 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -139,11 +139,16 @@ public: const static QSet getNonVerifiedPackets() { const static QSet NON_VERIFIED_PACKETS = QSet() - << PacketTypeEnum::Value::NodeJsonStats << PacketTypeEnum::Value::EntityQuery - << PacketTypeEnum::Value::OctreeDataNack << PacketTypeEnum::Value::EntityEditNack - << PacketTypeEnum::Value::DomainListRequest << PacketTypeEnum::Value::StopNode - << PacketTypeEnum::Value::DomainDisconnectRequest << PacketTypeEnum::Value::UsernameFromIDRequest - << PacketTypeEnum::Value::NodeKickRequest << PacketTypeEnum::Value::NodeMuteRequest; + << PacketTypeEnum::Value::NodeJsonStats + << PacketTypeEnum::Value::EntityQuery + << PacketTypeEnum::Value::OctreeDataNack + << PacketTypeEnum::Value::EntityEditNack + << PacketTypeEnum::Value::DomainListRequest + << PacketTypeEnum::Value::StopNode + << PacketTypeEnum::Value::DomainDisconnectRequest + << PacketTypeEnum::Value::UsernameFromIDRequest + << PacketTypeEnum::Value::NodeKickRequest + << PacketTypeEnum::Value::NodeMuteRequest; return NON_VERIFIED_PACKETS; } diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index f4fca3304c..0d692b5465 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(networking shared image gpu ktx) +link_hifi_libraries(networking shared image gpu ktx fbx baking) setup_memory_debugger()